refactor: improve notification naming and introduce type-safe enums
- Rename billFwd* to billsNotification* for clarity - Rename rentDueNotification* to rentNotification* for consistency - Rename utilBillsProofOfPayment to billsProofOfPayment - Introduce enums for type safety: - BillsNotificationStrategy (WhenPayed, WhenAttached) - BillsNotificationStatus (Scheduled, Sent, Failed) - RentNotificationStatus (Sent, Failed) - Replace "pending" status with "scheduled" for better semantics - Fix function names to proper camelCase - Fix incorrect import path in web-app/app/lib/format.ts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { Db, ObjectId } from 'mongodb';
|
import { Db, ObjectId } from 'mongodb';
|
||||||
import { BillingLocation, EmailStatus, UserSettings, generateShareId } from '@evidencija-rezija/shared-code';
|
import { BillingLocation, BillsNotificationStatus, EmailStatus, RentNotificationStatus, UserSettings, generateShareId } from '@evidencija-rezija/shared-code';
|
||||||
import { sendEmail } from './mailgunService';
|
import { sendEmail } from './mailgunService';
|
||||||
import { createLogger } from './logger';
|
import { createLogger } from './logger';
|
||||||
import { loadAndRender } from './emailTemplates';
|
import { loadAndRender } from './emailTemplates';
|
||||||
@@ -115,11 +115,11 @@ export async function sendRentDueNotifications(db: Db, budget: number): Promise<
|
|||||||
'yearMonth.year': currentYear,
|
'yearMonth.year': currentYear,
|
||||||
'yearMonth.month': currentMonth,
|
'yearMonth.month': currentMonth,
|
||||||
'tenantEmailStatus': EmailStatus.Verified,
|
'tenantEmailStatus': EmailStatus.Verified,
|
||||||
'rentDueNotificationEnabled': true,
|
'rentNotificationEnabled': true,
|
||||||
'rentDueDay': currentDay,
|
'rentDueDay': currentDay,
|
||||||
$or: [
|
$or: [
|
||||||
{ 'rentDueNotificationStatus': { $exists: false } },
|
{ 'rentNotificationStatus': { $exists: false } },
|
||||||
{ 'rentDueNotificationStatus': null }
|
{ 'rentNotificationStatus': null }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.toArray();
|
.toArray();
|
||||||
@@ -170,10 +170,10 @@ export async function sendRentDueNotifications(db: Db, budget: number): Promise<
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update location status
|
// Update location status
|
||||||
const newStatus = success ? 'sent' : 'failed';
|
const newStatus = success ? RentNotificationStatus.Sent : RentNotificationStatus.Failed;
|
||||||
await db.collection<BillingLocation>('lokacije').updateOne(
|
await db.collection<BillingLocation>('lokacije').updateOne(
|
||||||
{ _id: location._id },
|
{ _id: location._id },
|
||||||
{ $set: { rentDueNotificationStatus: newStatus } }
|
{ $set: { rentNotificationStatus: newStatus } }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -213,8 +213,8 @@ export async function sendUtilityBillsNotifications(db: Db, budget: number): Pro
|
|||||||
'yearMonth.year': currentYear,
|
'yearMonth.year': currentYear,
|
||||||
'yearMonth.month': currentMonth,
|
'yearMonth.month': currentMonth,
|
||||||
'tenantEmailStatus': EmailStatus.Verified,
|
'tenantEmailStatus': EmailStatus.Verified,
|
||||||
'billFwdEnabled': true,
|
'billsNotificationEnabled': true,
|
||||||
'billFwdStatus': 'pending'
|
'billsNotificationStatus': BillsNotificationStatus.Scheduled
|
||||||
})
|
})
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
@@ -263,10 +263,10 @@ export async function sendUtilityBillsNotifications(db: Db, budget: number): Pro
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update location status
|
// Update location status
|
||||||
const newStatus = success ? 'sent' : 'failed';
|
const newStatus = success ? BillsNotificationStatus.Sent : BillsNotificationStatus.Failed;
|
||||||
await db.collection<BillingLocation>('lokacije').updateOne(
|
await db.collection<BillingLocation>('lokacije').updateOne(
|
||||||
{ _id: location._id },
|
{ _id: location._id },
|
||||||
{ $set: { billFwdStatus: newStatus } }
|
{ $set: { billsNotificationStatus: newStatus } }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
|
|||||||
@@ -49,6 +49,29 @@ export enum EmailStatus {
|
|||||||
Unsubscribed = "unsubscribed"
|
Unsubscribed = "unsubscribed"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum BillsNotificationStrategy {
|
||||||
|
/** Notify tenant when bill is payed */
|
||||||
|
WhenPayed = "when-payed",
|
||||||
|
/** Notify tenant when bill gets an attachment */
|
||||||
|
WhenAttached = "when-attached"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BillsNotificationStatus {
|
||||||
|
/** Bills notification is scheduled to be sent */
|
||||||
|
Scheduled = "scheduled",
|
||||||
|
/** Bills notification has been sent */
|
||||||
|
Sent = "sent",
|
||||||
|
/** Sending of bills notification failed */
|
||||||
|
Failed = "failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum RentNotificationStatus {
|
||||||
|
/** notification has been sent */
|
||||||
|
Sent = "sent",
|
||||||
|
/** Sending of notification failed */
|
||||||
|
Failed = "failed"
|
||||||
|
}
|
||||||
|
|
||||||
/** bill object in the form returned by MongoDB */
|
/** bill object in the form returned by MongoDB */
|
||||||
export interface BillingLocation {
|
export interface BillingLocation {
|
||||||
_id: string;
|
_id: string;
|
||||||
@@ -84,25 +107,25 @@ export interface BillingLocation {
|
|||||||
/** (optional) language for tenant notification emails */
|
/** (optional) language for tenant notification emails */
|
||||||
tenantEmailLanguage?: "hr" | "en" | null;
|
tenantEmailLanguage?: "hr" | "en" | null;
|
||||||
/** (optional) whether to automatically notify tenant */
|
/** (optional) whether to automatically notify tenant */
|
||||||
billFwdEnabled?: boolean | null;
|
billsNotificationEnabled?: boolean | null;
|
||||||
/** (optional) bill forwarding strategy */
|
/** (optional) bill forwarding strategy */
|
||||||
billFwdStrategy?: "when-payed" | "when-attached" | null;
|
billsNotificationStrategy?: BillsNotificationStrategy | null;
|
||||||
/** (optional) bill forwarding status */
|
/** (optional) bill forwarding status */
|
||||||
billFwdStatus?: "pending" | "sent" | "failed" | null;
|
billsNotificationStatus?: BillsNotificationStatus | null;
|
||||||
|
/** (optional) utility bills proof of payment attachment */
|
||||||
|
billsProofOfPayment?: FileAttachment|null;
|
||||||
/** (optional) whether to automatically send rent notification */
|
/** (optional) whether to automatically send rent notification */
|
||||||
rentDueNotificationEnabled?: boolean | null;
|
rentNotificationEnabled?: boolean | null;
|
||||||
|
/** (optional) when was the rent due notification sent */
|
||||||
|
rentNotificationStatus?: RentNotificationStatus | null;
|
||||||
|
/** (optional) rent proof of payment attachment */
|
||||||
|
rentProofOfPayment?: FileAttachment|null;
|
||||||
/** (optional) day of month when rent is due (1-31) */
|
/** (optional) day of month when rent is due (1-31) */
|
||||||
rentDueDay?: number | null;
|
rentDueDay?: number | null;
|
||||||
/** (optional) when was the rent due notification sent */
|
|
||||||
rentDueNotificationStatus?: "sent" | "failed" | null;
|
|
||||||
/** (optional) monthly rent amount in cents */
|
/** (optional) monthly rent amount in cents */
|
||||||
rentAmount?: number | null;
|
rentAmount?: number | null;
|
||||||
/** (optional) whether the location has been seen by tenant */
|
/** (optional) whether the location has been seen by tenant */
|
||||||
seenByTenantAt?: Date | null;
|
seenByTenantAt?: Date | null;
|
||||||
/** (optional) utility bills proof of payment attachment */
|
|
||||||
utilBillsProofOfPayment?: FileAttachment|null;
|
|
||||||
/** (optional) rent proof of payment attachment */
|
|
||||||
rentProofOfPayment?: FileAttachment|null;
|
|
||||||
/** (optional) share link expiry timestamp */
|
/** (optional) share link expiry timestamp */
|
||||||
shareTTL?: Date;
|
shareTTL?: Date;
|
||||||
/** (optional) when tenant first visited the share link */
|
/** (optional) when tenant first visited the share link */
|
||||||
|
|||||||
@@ -90,16 +90,16 @@ The process of sending rent-due e-mail notifications is as follows:
|
|||||||
- fetch all `BillingLocations` from `lokacije` collection (see `locationActions.ts` in `web-app` workspace) - filter only records which satisfy the following:
|
- fetch all `BillingLocations` from `lokacije` collection (see `locationActions.ts` in `web-app` workspace) - filter only records which satisfy the following:
|
||||||
- `yearMonth.year` and `yearMonth.month` equal to current year and month
|
- `yearMonth.year` and `yearMonth.month` equal to current year and month
|
||||||
- `tenantEmailStatus` is equal to `EmailStatus.Verified`
|
- `tenantEmailStatus` is equal to `EmailStatus.Verified`
|
||||||
- `rentDueNotificationEnabled === true`
|
- `rentNotificationEnabled === true`
|
||||||
- `rentDueDay` = current day (1-31) in CET timezone
|
- `rentDueDay` = current day (1-31) in CET timezone
|
||||||
- `rentDueNotificationStatus` === undefined OR `rentDueNotificationStatus` === null
|
- `rentNotificationStatus` === undefined OR `rentNotificationStatus` === null
|
||||||
- for each record found
|
- for each record found
|
||||||
- check the e-mail budget counter
|
- check the e-mail budget counter
|
||||||
-> if the value is 0 then exit
|
-> if the value is 0 then exit
|
||||||
- compile an e-mail containing the content listed below
|
- compile an e-mail containing the content listed below
|
||||||
- send the e-mail
|
- send the e-mail
|
||||||
- if send OK set `rentDueNotificationStatus` to `sent`
|
- if send OK set `rentNotificationStatus` to `sent`
|
||||||
- if send failed set `rentDueNotificationStatus` to `failed`
|
- if send failed set `rentNotificationStatus` to `failed`
|
||||||
- decrement the e-mail budget counter
|
- decrement the e-mail budget counter
|
||||||
|
|
||||||
### Rent due notifications E-mail content
|
### Rent due notifications E-mail content
|
||||||
@@ -132,15 +132,15 @@ The process of bills due notifications e-mail is as follows:
|
|||||||
- fetch all `BillingLocations` from `lokacije` collection (see `locationActions.ts` in `web-app` workspace) - filter only records which satisfy the following:
|
- fetch all `BillingLocations` from `lokacije` collection (see `locationActions.ts` in `web-app` workspace) - filter only records which satisfy the following:
|
||||||
- `yearMonth.year` and `yearMonth.month` equal to current year and month
|
- `yearMonth.year` and `yearMonth.month` equal to current year and month
|
||||||
- `tenantEmailStatus` is equal to `EmailStatus.Verified`
|
- `tenantEmailStatus` is equal to `EmailStatus.Verified`
|
||||||
- `billFwdEnabled === true`
|
- `billsNotificationEnabled === true`
|
||||||
- `billFwdStatus === 'pending'`
|
- `billsNotificationStatus === 'scheduled'`
|
||||||
- for each record found
|
- for each record found
|
||||||
- check the e-mail budget counter
|
- check the e-mail budget counter
|
||||||
-> if the value is 0 then exit
|
-> if the value is 0 then exit
|
||||||
- compile an e-mail containing the content listed below
|
- compile an e-mail containing the content listed below
|
||||||
- send the e-mail
|
- send the e-mail
|
||||||
- if send OK set `billFwdStatus` to `sent`
|
- if send OK set `billsNotificationStatus` to `sent`
|
||||||
- if send failed set `billFwdStatus` to `failed`
|
- if send failed set `billsNotificationStatus` to `failed`
|
||||||
- decrement the e-mail budget counter
|
- decrement the e-mail budget counter
|
||||||
|
|
||||||
### Utility bills due notifications E-mail content
|
### Utility bills due notifications E-mail content
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ The web page served at this path contains an text explanation and "Verify e-mail
|
|||||||
|
|
||||||
The text includes the following information:
|
The text includes the following information:
|
||||||
* what the web app is about - very short into
|
* what the web app is about - very short into
|
||||||
* why the e-mail was sent = because the landloard of the property `BillingLocation.name` configured the rent (`BillingLocation.rentDueNotificationEnabled`) and/or utility bills (`BillingLocation.billFwdStrategy`) to be delivered to that e-mail address
|
* why the e-mail was sent = because the landloard of the property `BillingLocation.name` configured the rent (`BillingLocation.rentNotificationEnabled`) and/or utility bills (`BillingLocation.billsNotificationStrategy`) to be delivered to that e-mail address
|
||||||
* what will hapen if he/she clicks on the "Verify e-mail" button = they will be receiving rent due (`BillingLocation.rentDueNotificationEnabled`) or utility bills due (`BillingLocation.billFwdStrategy`) notification or both - 2x a month - depending on the config set by the landloard
|
* what will hapen if he/she clicks on the "Verify e-mail" button = they will be receiving rent due (`BillingLocation.rentNotificationEnabled`) or utility bills due (`BillingLocation.billsNotificationStrategy`) notification or both - 2x a month - depending on the config set by the landloard
|
||||||
* opt-out infomation (they can ignore this e-mail, but can also opt-out at any moment)
|
* opt-out infomation (they can ignore this e-mail, but can also opt-out at any moment)
|
||||||
|
|
||||||
If the user clicks the button "Verify e-mail" this triggers update of `BillingLocation.tenantEmailStatus`.
|
If the user clicks the button "Verify e-mail" this triggers update of `BillingLocation.tenantEmailStatus`.
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ export async function GET(request: Request, { params: { id } }: { params: { id:
|
|||||||
const location = await dbClient.collection<BillingLocation>("lokacije")
|
const location = await dbClient.collection<BillingLocation>("lokacije")
|
||||||
.findOne({ _id: locationID }, {
|
.findOne({ _id: locationID }, {
|
||||||
projection: {
|
projection: {
|
||||||
utilBillsProofOfPayment: 1,
|
billsProofOfPayment: 1,
|
||||||
shareTTL: 1,
|
shareTTL: 1,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!location?.utilBillsProofOfPayment) {
|
if (!location?.billsProofOfPayment) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export async function GET(request: Request, { params: { id } }: { params: { id:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert fileContentsBase64 from Base64 string to binary
|
// Convert fileContentsBase64 from Base64 string to binary
|
||||||
const fileContentsBuffer = Buffer.from(location.utilBillsProofOfPayment.fileContentsBase64, 'base64');
|
const fileContentsBuffer = Buffer.from(location.billsProofOfPayment.fileContentsBase64, 'base64');
|
||||||
|
|
||||||
// Convert fileContentsBuffer to format that can be sent to the client
|
// Convert fileContentsBuffer to format that can be sent to the client
|
||||||
const fileContents = new Uint8Array(fileContentsBuffer);
|
const fileContents = new Uint8Array(fileContentsBuffer);
|
||||||
@@ -47,8 +47,8 @@ export async function GET(request: Request, { params: { id } }: { params: { id:
|
|||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/pdf',
|
'Content-Type': 'application/pdf',
|
||||||
'Content-Disposition': `attachment; filename="${location.utilBillsProofOfPayment.fileName}"`,
|
'Content-Disposition': `attachment; filename="${location.billsProofOfPayment.fileName}"`,
|
||||||
'Last-Modified': `${location.utilBillsProofOfPayment.fileLastModified}`
|
'Last-Modified': `${location.billsProofOfPayment.fileLastModified}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { getDbClient } from '../dbClient';
|
import { getDbClient } from '../dbClient';
|
||||||
import { Bill, BilledTo, FileAttachment, BillingLocation } from '@evidencija-rezija/shared-code';
|
import { Bill, BilledTo, FileAttachment, BillingLocation, BillsNotificationStatus } from '@evidencija-rezija/shared-code';
|
||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import { withUser } from '@/app/lib/auth';
|
import { withUser } from '@/app/lib/auth';
|
||||||
import { AuthenticatedUser } from '../types/next-auth';
|
import { AuthenticatedUser } from '../types/next-auth';
|
||||||
@@ -13,7 +13,7 @@ import { unstable_noStore, revalidatePath } from 'next/cache';
|
|||||||
import { extractShareId, validateShareChecksum } from '@evidencija-rezija/shared-code';
|
import { extractShareId, validateShareChecksum } from '@evidencija-rezija/shared-code';
|
||||||
import { validatePdfFile } from '../validators/pdfValidator';
|
import { validatePdfFile } from '../validators/pdfValidator';
|
||||||
import { checkUploadRateLimit } from '../uploadRateLimiter';
|
import { checkUploadRateLimit } from '../uploadRateLimiter';
|
||||||
import { shouldUpdateBillFwdStatusWhenAttached, shouldUpdateBillFwdStatusWhenPayed } from '../billForwardingHelpers';
|
import { shouldUpdateBillsNotificationStatusWhenAttached, shouldUpdateBillsNotificationStatusWhenPayed } from '../billForwardingHelpers';
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
errors?: {
|
errors?: {
|
||||||
@@ -178,7 +178,7 @@ export const updateOrAddBill = withUser(async (user: AuthenticatedUser, location
|
|||||||
|
|
||||||
const billAttachment = await serializeAttachment(attachmentFile);
|
const billAttachment = await serializeAttachment(attachmentFile);
|
||||||
|
|
||||||
// Fetch the location to check billFwdStatus conditions
|
// Fetch the location to check billsNotificationStatus conditions
|
||||||
const location = await dbClient.collection<BillingLocation>("lokacije").findOne({
|
const location = await dbClient.collection<BillingLocation>("lokacije").findOne({
|
||||||
_id: locationId,
|
_id: locationId,
|
||||||
userId
|
userId
|
||||||
@@ -188,9 +188,9 @@ export const updateOrAddBill = withUser(async (user: AuthenticatedUser, location
|
|||||||
return { success: false, error: 'Location not found' };
|
return { success: false, error: 'Location not found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we should update billFwdStatus to "pending"
|
// Check if we should update billsNotificationStatus to `Scheduled`
|
||||||
const shouldSetFwdPendingWhenAttached = shouldUpdateBillFwdStatusWhenAttached(location, billId, billAttachment !== null);
|
const shouldSetFwdPendingWhenAttached = shouldUpdateBillsNotificationStatusWhenAttached(location, billId, billAttachment !== null);
|
||||||
const shouldSetFwdPendingWhenPayed = shouldUpdateBillFwdStatusWhenPayed(location, billId, billPaid);
|
const shouldSetFwdPendingWhenPayed = shouldUpdateBillsNotificationStatusWhenPayed(location, billId, billPaid);
|
||||||
const shouldSetFwdPending = shouldSetFwdPendingWhenAttached || shouldSetFwdPendingWhenPayed;
|
const shouldSetFwdPending = shouldSetFwdPendingWhenAttached || shouldSetFwdPendingWhenPayed;
|
||||||
|
|
||||||
if (billId) {
|
if (billId) {
|
||||||
@@ -215,9 +215,9 @@ export const updateOrAddBill = withUser(async (user: AuthenticatedUser, location
|
|||||||
"bills.$[elem].hub3aText": hub3aText,
|
"bills.$[elem].hub3aText": hub3aText,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add billFwdStatus if needed
|
// Add billsNotificationStatus if needed
|
||||||
if (shouldSetFwdPending) {
|
if (shouldSetFwdPending) {
|
||||||
(mongoDbSet as any).billFwdStatus = "pending";
|
(mongoDbSet as any).billsNotificationStatus = BillsNotificationStatus.Scheduled;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update bill in given location with the given locationID
|
// update bill in given location with the given locationID
|
||||||
@@ -253,10 +253,10 @@ export const updateOrAddBill = withUser(async (user: AuthenticatedUser, location
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add billFwdStatus update if needed
|
// Add billsNotificationStatus update if needed
|
||||||
if (shouldSetFwdPending) {
|
if (shouldSetFwdPending) {
|
||||||
updateOp.$set = {
|
updateOp.$set = {
|
||||||
billFwdStatus: "pending"
|
billsNotificationStatus: BillsNotificationStatus.Scheduled
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { getDbClient } from '../dbClient';
|
import { getDbClient } from '../dbClient';
|
||||||
import { BillingLocation, FileAttachment, YearMonth, EmailStatus } from '@evidencija-rezija/shared-code';
|
import { BillingLocation, FileAttachment, YearMonth, EmailStatus, BillsNotificationStrategy } from '@evidencija-rezija/shared-code';
|
||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import { withUser } from '@/app/lib/auth';
|
import { withUser } from '@/app/lib/auth';
|
||||||
import { AuthenticatedUser } from '../types/next-auth';
|
import { AuthenticatedUser } from '../types/next-auth';
|
||||||
@@ -20,11 +20,11 @@ export type State = {
|
|||||||
tenantName?: string[];
|
tenantName?: string[];
|
||||||
tenantStreet?: string[];
|
tenantStreet?: string[];
|
||||||
tenantTown?: string[];
|
tenantTown?: string[];
|
||||||
billFwdEnabled?: string[];
|
billsNotificationEnabled?: string[];
|
||||||
tenantEmail?: string[];
|
tenantEmail?: string[];
|
||||||
tenantEmailStatus?: string[];
|
tenantEmailStatus?: string[];
|
||||||
billFwdStrategy?: string[];
|
billsNotificationStrategy?: string[];
|
||||||
rentDueNotificationEnabled?: string[];
|
rentNotificationEnabled?: string[];
|
||||||
rentDueDay?: string[];
|
rentDueDay?: string[];
|
||||||
rentAmount?: string[];
|
rentAmount?: string[];
|
||||||
updateScope?: string[];
|
updateScope?: string[];
|
||||||
@@ -44,12 +44,12 @@ const FormSchema = (t:IntlTemplateFn) => z.object({
|
|||||||
tenantName: z.string().max(30).optional().nullable(),
|
tenantName: z.string().max(30).optional().nullable(),
|
||||||
tenantStreet: z.string().max(27).optional().nullable(),
|
tenantStreet: z.string().max(27).optional().nullable(),
|
||||||
tenantTown: z.string().max(27).optional().nullable(),
|
tenantTown: z.string().max(27).optional().nullable(),
|
||||||
billFwdEnabled: z.boolean().optional().nullable(),
|
billsNotificationEnabled: z.boolean().optional().nullable(),
|
||||||
tenantEmail: z.string().email(t("tenant-email-invalid")).optional().or(z.literal("")).nullable(),
|
tenantEmail: z.string().email(t("tenant-email-invalid")).optional().or(z.literal("")).nullable(),
|
||||||
tenantEmailStatus: z.enum([EmailStatus.Unverified, EmailStatus.VerificationPending, EmailStatus.Verified, EmailStatus.Unsubscribed]).optional().nullable(),
|
tenantEmailStatus: z.enum([EmailStatus.Unverified, EmailStatus.VerificationPending, EmailStatus.Verified, EmailStatus.Unsubscribed]).optional().nullable(),
|
||||||
tenantEmailLanguage: z.enum(["hr", "en"]).optional().nullable(),
|
tenantEmailLanguage: z.enum(["hr", "en"]).optional().nullable(),
|
||||||
billFwdStrategy: z.enum(["when-payed", "when-attached"]).optional().nullable(),
|
billsNotificationStrategy: z.enum([BillsNotificationStrategy.WhenPayed, BillsNotificationStrategy.WhenAttached]).optional().nullable(),
|
||||||
rentDueNotificationEnabled: z.boolean().optional().nullable(),
|
rentNotificationEnabled: z.boolean().optional().nullable(),
|
||||||
rentDueDay: z.coerce.number().min(1).max(31).optional().nullable(),
|
rentDueDay: z.coerce.number().min(1).max(31).optional().nullable(),
|
||||||
rentAmount: z.coerce.number().int(t("rent-amount-integer")).positive(t("rent-amount-positive")).optional().nullable(),
|
rentAmount: z.coerce.number().int(t("rent-amount-integer")).positive(t("rent-amount-positive")).optional().nullable(),
|
||||||
addToSubsequentMonths: z.boolean().optional().nullable(),
|
addToSubsequentMonths: z.boolean().optional().nullable(),
|
||||||
@@ -86,7 +86,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({
|
|||||||
path: ["tenantTown"],
|
path: ["tenantTown"],
|
||||||
})
|
})
|
||||||
.refine((data) => {
|
.refine((data) => {
|
||||||
if (data.billFwdEnabled || data.rentDueNotificationEnabled) {
|
if (data.billsNotificationEnabled || data.rentNotificationEnabled) {
|
||||||
return !!data.tenantEmail && data.tenantEmail.trim().length > 0;
|
return !!data.tenantEmail && data.tenantEmail.trim().length > 0;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -95,7 +95,7 @@ const FormSchema = (t:IntlTemplateFn) => z.object({
|
|||||||
path: ["tenantEmail"],
|
path: ["tenantEmail"],
|
||||||
})
|
})
|
||||||
.refine((data) => {
|
.refine((data) => {
|
||||||
if (data.rentDueNotificationEnabled) {
|
if (data.rentNotificationEnabled) {
|
||||||
return !!data.rentAmount && data.rentAmount > 0;
|
return !!data.rentAmount && data.rentAmount > 0;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -134,12 +134,12 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
|
|||||||
tenantName: formData.get('tenantName') || null,
|
tenantName: formData.get('tenantName') || null,
|
||||||
tenantStreet: formData.get('tenantStreet') || null,
|
tenantStreet: formData.get('tenantStreet') || null,
|
||||||
tenantTown: formData.get('tenantTown') || null,
|
tenantTown: formData.get('tenantTown') || null,
|
||||||
billFwdEnabled: formData.get('billFwdEnabled') === 'on',
|
billsNotificationEnabled: formData.get('billsNotificationEnabled') === 'on',
|
||||||
tenantEmail: formData.get('tenantEmail') || null,
|
tenantEmail: formData.get('tenantEmail') || null,
|
||||||
tenantEmailStatus: formData.get('tenantEmailStatus') as "unverified" | "verification-pending" | "verified" | "unsubscribed" | undefined,
|
tenantEmailStatus: formData.get('tenantEmailStatus') as "unverified" | "verification-pending" | "verified" | "unsubscribed" | undefined,
|
||||||
tenantEmailLanguage: formData.get('tenantEmailLanguage') as "hr" | "en" | undefined,
|
tenantEmailLanguage: formData.get('tenantEmailLanguage') as "hr" | "en" | undefined,
|
||||||
billFwdStrategy: formData.get('billFwdStrategy') as "when-payed" | "when-attached" | undefined,
|
billsNotificationStrategy: formData.get('billsNotificationStrategy') as BillsNotificationStrategy | undefined,
|
||||||
rentDueNotificationEnabled: formData.get('rentDueNotificationEnabled') === 'on',
|
rentNotificationEnabled: formData.get('rentNotificationEnabled') === 'on',
|
||||||
rentDueDay: formData.get('rentDueDay') || null,
|
rentDueDay: formData.get('rentDueDay') || null,
|
||||||
rentAmount: formData.get('rentAmount') || null,
|
rentAmount: formData.get('rentAmount') || null,
|
||||||
addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on',
|
addToSubsequentMonths: formData.get('addToSubsequentMonths') === 'on',
|
||||||
@@ -161,12 +161,12 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
|
|||||||
tenantName,
|
tenantName,
|
||||||
tenantStreet,
|
tenantStreet,
|
||||||
tenantTown,
|
tenantTown,
|
||||||
billFwdEnabled,
|
billsNotificationEnabled,
|
||||||
tenantEmail,
|
tenantEmail,
|
||||||
tenantEmailStatus,
|
tenantEmailStatus,
|
||||||
tenantEmailLanguage,
|
tenantEmailLanguage,
|
||||||
billFwdStrategy,
|
billsNotificationStrategy,
|
||||||
rentDueNotificationEnabled,
|
rentNotificationEnabled,
|
||||||
rentDueDay,
|
rentDueDay,
|
||||||
rentAmount,
|
rentAmount,
|
||||||
addToSubsequentMonths,
|
addToSubsequentMonths,
|
||||||
@@ -220,12 +220,12 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
|
|||||||
tenantName: tenantName || null,
|
tenantName: tenantName || null,
|
||||||
tenantStreet: tenantStreet || null,
|
tenantStreet: tenantStreet || null,
|
||||||
tenantTown: tenantTown || null,
|
tenantTown: tenantTown || null,
|
||||||
billFwdEnabled: billFwdEnabled || false,
|
billsNotificationEnabled: billsNotificationEnabled || false,
|
||||||
tenantEmail: tenantEmail || null,
|
tenantEmail: tenantEmail || null,
|
||||||
tenantEmailStatus: finalEmailStatus,
|
tenantEmailStatus: finalEmailStatus,
|
||||||
tenantEmailLanguage: tenantEmailLanguage || null,
|
tenantEmailLanguage: tenantEmailLanguage || null,
|
||||||
billFwdStrategy: billFwdStrategy || "when-payed",
|
billsNotificationStrategy: billsNotificationStrategy || BillsNotificationStrategy.WhenPayed,
|
||||||
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
|
rentNotificationEnabled: rentNotificationEnabled || false,
|
||||||
rentDueDay: rentDueDay || null,
|
rentDueDay: rentDueDay || null,
|
||||||
rentAmount: rentAmount || null,
|
rentAmount: rentAmount || null,
|
||||||
}
|
}
|
||||||
@@ -253,12 +253,12 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
|
|||||||
tenantName: tenantName || null,
|
tenantName: tenantName || null,
|
||||||
tenantStreet: tenantStreet || null,
|
tenantStreet: tenantStreet || null,
|
||||||
tenantTown: tenantTown || null,
|
tenantTown: tenantTown || null,
|
||||||
billFwdEnabled: billFwdEnabled || false,
|
billsNotificationEnabled: billsNotificationEnabled || false,
|
||||||
tenantEmail: tenantEmail || null,
|
tenantEmail: tenantEmail || null,
|
||||||
tenantEmailStatus: finalEmailStatus,
|
tenantEmailStatus: finalEmailStatus,
|
||||||
tenantEmailLanguage: tenantEmailLanguage || null,
|
tenantEmailLanguage: tenantEmailLanguage || null,
|
||||||
billFwdStrategy: billFwdStrategy || "when-payed",
|
billsNotificationStrategy: billsNotificationStrategy || BillsNotificationStrategy.WhenPayed,
|
||||||
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
|
rentNotificationEnabled: rentNotificationEnabled || false,
|
||||||
rentDueDay: rentDueDay || null,
|
rentDueDay: rentDueDay || null,
|
||||||
rentAmount: rentAmount || null,
|
rentAmount: rentAmount || null,
|
||||||
}
|
}
|
||||||
@@ -279,12 +279,12 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
|
|||||||
tenantName: tenantName || null,
|
tenantName: tenantName || null,
|
||||||
tenantStreet: tenantStreet || null,
|
tenantStreet: tenantStreet || null,
|
||||||
tenantTown: tenantTown || null,
|
tenantTown: tenantTown || null,
|
||||||
billFwdEnabled: billFwdEnabled || false,
|
billsNotificationEnabled: billsNotificationEnabled || false,
|
||||||
tenantEmail: tenantEmail || null,
|
tenantEmail: tenantEmail || null,
|
||||||
tenantEmailStatus: finalEmailStatus,
|
tenantEmailStatus: finalEmailStatus,
|
||||||
tenantEmailLanguage: tenantEmailLanguage || null,
|
tenantEmailLanguage: tenantEmailLanguage || null,
|
||||||
billFwdStrategy: billFwdStrategy || "when-payed",
|
billsNotificationStrategy: billsNotificationStrategy || BillsNotificationStrategy.WhenPayed,
|
||||||
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
|
rentNotificationEnabled: rentNotificationEnabled || false,
|
||||||
rentDueDay: rentDueDay || null,
|
rentDueDay: rentDueDay || null,
|
||||||
rentAmount: rentAmount || null,
|
rentAmount: rentAmount || null,
|
||||||
}
|
}
|
||||||
@@ -304,12 +304,12 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
|
|||||||
tenantName: tenantName || null,
|
tenantName: tenantName || null,
|
||||||
tenantStreet: tenantStreet || null,
|
tenantStreet: tenantStreet || null,
|
||||||
tenantTown: tenantTown || null,
|
tenantTown: tenantTown || null,
|
||||||
billFwdEnabled: billFwdEnabled || false,
|
billsNotificationEnabled: billsNotificationEnabled || false,
|
||||||
tenantEmail: tenantEmail || null,
|
tenantEmail: tenantEmail || null,
|
||||||
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified,
|
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified,
|
||||||
tenantEmailLanguage: tenantEmailLanguage || null,
|
tenantEmailLanguage: tenantEmailLanguage || null,
|
||||||
billFwdStrategy: billFwdStrategy || "when-payed",
|
billsNotificationStrategy: billsNotificationStrategy || BillsNotificationStrategy.WhenPayed,
|
||||||
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
|
rentNotificationEnabled: rentNotificationEnabled || false,
|
||||||
rentDueDay: rentDueDay || null,
|
rentDueDay: rentDueDay || null,
|
||||||
rentAmount: rentAmount || null,
|
rentAmount: rentAmount || null,
|
||||||
yearMonth: yearMonth,
|
yearMonth: yearMonth,
|
||||||
@@ -381,12 +381,12 @@ export const updateOrAddLocation = withUser(async (user:AuthenticatedUser, locat
|
|||||||
tenantName: tenantName || null,
|
tenantName: tenantName || null,
|
||||||
tenantStreet: tenantStreet || null,
|
tenantStreet: tenantStreet || null,
|
||||||
tenantTown: tenantTown || null,
|
tenantTown: tenantTown || null,
|
||||||
billFwdEnabled: billFwdEnabled || false,
|
billsNotificationEnabled: billsNotificationEnabled || false,
|
||||||
tenantEmail: tenantEmail || null,
|
tenantEmail: tenantEmail || null,
|
||||||
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified,
|
tenantEmailStatus: tenantEmailStatus as EmailStatus || EmailStatus.Unverified,
|
||||||
tenantEmailLanguage: tenantEmailLanguage || null,
|
tenantEmailLanguage: tenantEmailLanguage || null,
|
||||||
billFwdStrategy: billFwdStrategy || "when-payed",
|
billsNotificationStrategy: billsNotificationStrategy || BillsNotificationStrategy.WhenPayed,
|
||||||
rentDueNotificationEnabled: rentDueNotificationEnabled || false,
|
rentNotificationEnabled: rentNotificationEnabled || false,
|
||||||
rentDueDay: rentDueDay || null,
|
rentDueDay: rentDueDay || null,
|
||||||
rentAmount: rentAmount || null,
|
rentAmount: rentAmount || null,
|
||||||
yearMonth: { year: monthData.year, month: monthData.month },
|
yearMonth: { year: monthData.year, month: monthData.month },
|
||||||
@@ -495,8 +495,8 @@ export const fetchAllLocations = withUser(async (user:AuthenticatedUser, year:nu
|
|||||||
// "bills.hub3aText": 1,
|
// "bills.hub3aText": 1,
|
||||||
// project only file name - leave out file content so that
|
// project only file name - leave out file content so that
|
||||||
// less data is transferred to the client
|
// less data is transferred to the client
|
||||||
"utilBillsProofOfPayment.fileName": 1,
|
"billsProofOfPayment.fileName": 1,
|
||||||
"utilBillsProofOfPayment.uploadedAt": 1,
|
"billsProofOfPayment.uploadedAt": 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -558,7 +558,7 @@ export const fetchLocationById = async (locationID:string) => {
|
|||||||
projection: {
|
projection: {
|
||||||
// don't include the attachment binary data in the response
|
// don't include the attachment binary data in the response
|
||||||
"bills.attachment.fileContentsBase64": 0,
|
"bills.attachment.fileContentsBase64": 0,
|
||||||
"utilBillsProofOfPayment.fileContentsBase64": 0,
|
"billsProofOfPayment.fileContentsBase64": 0,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -691,7 +691,7 @@ const serializeAttachment = async (file: File | null):Promise<FileAttachment | n
|
|||||||
* @param ipAddress - Optional IP address for rate limiting
|
* @param ipAddress - Optional IP address for rate limiting
|
||||||
* @returns Promise with success status
|
* @returns Promise with success status
|
||||||
*/
|
*/
|
||||||
export const uploadUtilBillsProofOfPayment = async (
|
export const uploadBillsProofOfPayment = async (
|
||||||
shareId: string,
|
shareId: string,
|
||||||
formData: FormData,
|
formData: FormData,
|
||||||
ipAddress?: string
|
ipAddress?: string
|
||||||
@@ -729,7 +729,7 @@ export const uploadUtilBillsProofOfPayment = async (
|
|||||||
const dbClient = await getDbClient();
|
const dbClient = await getDbClient();
|
||||||
|
|
||||||
const location = await dbClient.collection<BillingLocation>("lokacije")
|
const location = await dbClient.collection<BillingLocation>("lokacije")
|
||||||
.findOne({ _id: locationID }, { projection: { userId: 1, utilBillsProofOfPayment: 1, shareTTL: 1 } });
|
.findOne({ _id: locationID }, { projection: { userId: 1, billsProofOfPayment: 1, shareTTL: 1 } });
|
||||||
|
|
||||||
if (!location || !location.userId) {
|
if (!location || !location.userId) {
|
||||||
return { success: false, error: 'Invalid request' };
|
return { success: false, error: 'Invalid request' };
|
||||||
@@ -741,12 +741,12 @@ export const uploadUtilBillsProofOfPayment = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if proof of payment already uploaded
|
// Check if proof of payment already uploaded
|
||||||
if (location.utilBillsProofOfPayment) {
|
if (location.billsProofOfPayment) {
|
||||||
return { success: false, error: 'Proof of payment already uploaded for this location' };
|
return { success: false, error: 'Proof of payment already uploaded for this location' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. FILE VALIDATION
|
// 4. FILE VALIDATION
|
||||||
const file = formData.get('utilBillsProofOfPayment') as File;
|
const file = formData.get('billsProofOfPayment') as File;
|
||||||
|
|
||||||
if (!file || file.size === 0) {
|
if (!file || file.size === 0) {
|
||||||
return { success: false, error: 'No file provided' };
|
return { success: false, error: 'No file provided' };
|
||||||
@@ -770,7 +770,7 @@ export const uploadUtilBillsProofOfPayment = async (
|
|||||||
.updateOne(
|
.updateOne(
|
||||||
{ _id: locationID },
|
{ _id: locationID },
|
||||||
{ $set: {
|
{ $set: {
|
||||||
utilBillsProofOfPayment: attachment
|
billsProofOfPayment: attachment
|
||||||
} }
|
} }
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -786,7 +786,7 @@ export const uploadUtilBillsProofOfPayment = async (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Upload rent proof of payment (for tenants via share link)
|
* Upload rent proof of payment (for tenants via share link)
|
||||||
* Similar to uploadUtilBillsProofOfPayment but for rent payments
|
* Similar to uploadBillsProofOfPayment but for rent payments
|
||||||
*/
|
*/
|
||||||
export const uploadRentProofOfPayment = async (
|
export const uploadRentProofOfPayment = async (
|
||||||
shareId: string,
|
shareId: string,
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
import { getDbClient } from '../dbClient';
|
import { getDbClient } from '../dbClient';
|
||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import { Bill, BillingLocation, YearMonth } from '@evidencija-rezija/shared-code';
|
import { Bill, BillingLocation, BillsNotificationStatus, YearMonth } from '@evidencija-rezija/shared-code';
|
||||||
import { AuthenticatedUser } from '../types/next-auth';
|
import { AuthenticatedUser } from '../types/next-auth';
|
||||||
import { withUser } from '../auth';
|
import { withUser } from '../auth';
|
||||||
import { unstable_noStore as noStore, unstable_noStore, revalidatePath } from 'next/cache';
|
import { unstable_noStore as noStore, unstable_noStore, revalidatePath } from 'next/cache';
|
||||||
import { getLocale } from 'next-intl/server';
|
import { getLocale } from 'next-intl/server';
|
||||||
import { gotoHomeWithMessage } from './navigationActions';
|
import { gotoHomeWithMessage } from './navigationActions';
|
||||||
import { shouldUpdateBillFwdStatusWhenPayed } from '../billForwardingHelpers';
|
import { shouldUpdateBillsNotificationStatusWhenPayed } from '../billForwardingHelpers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Server-side action which adds a new month to the database
|
* Server-side action which adds a new month to the database
|
||||||
@@ -44,7 +44,7 @@ export const addMonth = withUser(async (user:AuthenticatedUser, { year, month }:
|
|||||||
...prevLocation,
|
...prevLocation,
|
||||||
// clear properties specific to the month
|
// clear properties specific to the month
|
||||||
seenByTenantAt: undefined,
|
seenByTenantAt: undefined,
|
||||||
utilBillsProofOfPayment: undefined,
|
billsProofOfPayment: undefined,
|
||||||
// assign a new ID
|
// assign a new ID
|
||||||
_id: (new ObjectId()).toHexString(),
|
_id: (new ObjectId()).toHexString(),
|
||||||
yearMonth: {
|
yearMonth: {
|
||||||
@@ -234,11 +234,11 @@ export const updateMonth = withUser(async (
|
|||||||
// Check if any paid bill triggers forwarding (only need to check once)
|
// Check if any paid bill triggers forwarding (only need to check once)
|
||||||
const firstPaidUpdate = locationUpdates.find(update => update.paid === true);
|
const firstPaidUpdate = locationUpdates.find(update => update.paid === true);
|
||||||
|
|
||||||
if (firstPaidUpdate && shouldUpdateBillFwdStatusWhenPayed(location, firstPaidUpdate.billId, true)) {
|
if (firstPaidUpdate && shouldUpdateBillsNotificationStatusWhenPayed(location, firstPaidUpdate.billId, true)) {
|
||||||
// Update billFwdStatus to "pending"
|
// Update billsNotificationStatus to "scheduled"
|
||||||
await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
await dbClient.collection<BillingLocation>("lokacije").updateOne(
|
||||||
{ _id: locationId },
|
{ _id: locationId },
|
||||||
{ $set: { billFwdStatus: "pending" } }
|
{ $set: { billsNotificationStatus: BillsNotificationStatus.Scheduled } }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { BillingLocation, BilledTo } from '@evidencija-rezija/shared-code';
|
import { BillingLocation, BilledTo, BillsNotificationStatus, BillsNotificationStrategy } from '@evidencija-rezija/shared-code';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if billFwdStatus should be updated to "pending" based on attachment status
|
* Checks if billsNotificationStatus should be updated to `Scheduled` based on attachment status
|
||||||
* @param location - The billing location containing the bill
|
* @param location - The billing location containing the bill
|
||||||
* @param currentBillId - The ID of the bill being updated (to exclude from check)
|
* @param currentBillId - The ID of the bill being updated (to exclude from check)
|
||||||
* @param hasNewAttachment - Whether a new attachment is being added to the current bill
|
* @param hasNewAttachment - Whether a new attachment is being added to the current bill
|
||||||
* @returns true if billFwdStatus should be set to "pending"
|
* @returns true if billsNotificationStatus should be set to `Scheduled`
|
||||||
*/
|
*/
|
||||||
export const shouldUpdateBillFwdStatusWhenAttached = (
|
export const shouldUpdateBillsNotificationStatusWhenAttached = (
|
||||||
location: BillingLocation,
|
location: BillingLocation,
|
||||||
currentBillId: string | undefined,
|
currentBillId: string | undefined,
|
||||||
hasNewAttachment: boolean
|
hasNewAttachment: boolean
|
||||||
@@ -17,18 +17,18 @@ export const shouldUpdateBillFwdStatusWhenAttached = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check billFwdEnabled is true
|
// Check billsNotificationEnabled is true
|
||||||
if (location.billFwdEnabled !== true) {
|
if (location.billsNotificationEnabled !== true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check billFwdStrategy is "when-attached"
|
// Check billsNotificationStrategy is `WhenAttached`
|
||||||
if (location.billFwdStrategy !== "when-attached") {
|
if (location.billsNotificationStrategy !== BillsNotificationStrategy.WhenAttached) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check bills have already been sent or are pending -> don't sent them again
|
// Check bills have already been sent or are scheduled -> don't send them again
|
||||||
if (location.billFwdStatus === "pending" || location.billFwdStatus === "sent") {
|
if (location.billsNotificationStatus === BillsNotificationStatus.Scheduled || location.billsNotificationStatus === BillsNotificationStatus.Sent) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,13 +49,13 @@ export const shouldUpdateBillFwdStatusWhenAttached = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if billFwdStatus should be updated to "pending" based on paid status
|
* Checks if billsNotificationStatus should be updated to "scheduled" based on paid status
|
||||||
* @param location - The billing location containing the bill
|
* @param location - The billing location containing the bill
|
||||||
* @param currentBillId - The ID of the bill being updated (to exclude from check)
|
* @param currentBillId - The ID of the bill being updated (to exclude from check)
|
||||||
* @param isPaid - Whether the current bill is being marked as paid
|
* @param isPaid - Whether the current bill is being marked as paid
|
||||||
* @returns true if billFwdStatus should be set to "pending"
|
* @returns true if billsNotificationStatus should be set to "scheduled"
|
||||||
*/
|
*/
|
||||||
export const shouldUpdateBillFwdStatusWhenPayed = (
|
export const shouldUpdateBillsNotificationStatusWhenPayed = (
|
||||||
location: BillingLocation,
|
location: BillingLocation,
|
||||||
currentBillId: string | undefined,
|
currentBillId: string | undefined,
|
||||||
isPaid: boolean
|
isPaid: boolean
|
||||||
@@ -65,18 +65,18 @@ export const shouldUpdateBillFwdStatusWhenPayed = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check billFwdEnabled is true
|
// Check billsNotificationEnabled is true
|
||||||
if (location.billFwdEnabled !== true) {
|
if (location.billsNotificationEnabled !== true) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check billFwdStrategy is "when-payed"
|
// Check billsNotificationStrategy is `WhenPayed`
|
||||||
if (location.billFwdStrategy !== "when-payed") {
|
if (location.billsNotificationStrategy !== BillsNotificationStrategy.WhenPayed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check bills have already been sent or are pending -> don't sent them again
|
// Check bills have already been sent or are scheduled -> don't sent them again
|
||||||
if (location.billFwdStatus === "pending" || location.billFwdStatus === "sent") {
|
if (location.billsNotificationStatus === BillsNotificationStatus.Scheduled || location.billsNotificationStatus === BillsNotificationStatus.Sent) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { YearMonth } from "./db-types";
|
import { YearMonth } from "@evidencija-rezija/shared-code";
|
||||||
|
|
||||||
export const formatYearMonth = ({ year, month }: YearMonth): string => {
|
export const formatYearMonth = ({ year, month }: YearMonth): string => {
|
||||||
return `${year}-${month<10?"0":""}${month}`;
|
return `${year}-${month<10?"0":""}${month}`;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const LocationCard: FC<LocationCardProps> = ({ location, currency }) => {
|
|||||||
bills,
|
bills,
|
||||||
seenByTenantAt,
|
seenByTenantAt,
|
||||||
// NOTE: only the fileName is projected from the DB to reduce data transfer
|
// NOTE: only the fileName is projected from the DB to reduce data transfer
|
||||||
utilBillsProofOfPayment,
|
billsProofOfPayment,
|
||||||
tenantEmail,
|
tenantEmail,
|
||||||
tenantEmailStatus,
|
tenantEmailStatus,
|
||||||
} = location;
|
} = location;
|
||||||
@@ -70,7 +70,7 @@ export const LocationCard: FC<LocationCardProps> = ({ location, currency }) => {
|
|||||||
</Link>
|
</Link>
|
||||||
<ShareIcon className="h-[1em] w-[1em] cursor-pointer text-2xl inline hover:text-red-500" title="create sharable link" onClick={handleCopyLinkClick} />
|
<ShareIcon className="h-[1em] w-[1em] cursor-pointer text-2xl inline hover:text-red-500" title="create sharable link" onClick={handleCopyLinkClick} />
|
||||||
</div>
|
</div>
|
||||||
{ totalUnpaid > 0 || totalPayed > 0 || seenByTenantAt || utilBillsProofOfPayment?.uploadedAt || (tenantEmail && tenantEmailStatus && tenantEmailStatus !== EmailStatus.Verified) ?
|
{ totalUnpaid > 0 || totalPayed > 0 || seenByTenantAt || billsProofOfPayment?.uploadedAt || (tenantEmail && tenantEmailStatus && tenantEmailStatus !== EmailStatus.Verified) ?
|
||||||
<>
|
<>
|
||||||
<div className="flex ml-1">
|
<div className="flex ml-1">
|
||||||
<div className="divider divider-horizontal p-0 m-0"></div>
|
<div className="divider divider-horizontal p-0 m-0"></div>
|
||||||
@@ -125,7 +125,7 @@ export const LocationCard: FC<LocationCardProps> = ({ location, currency }) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{utilBillsProofOfPayment?.uploadedAt && (
|
{billsProofOfPayment?.uploadedAt && (
|
||||||
<Link
|
<Link
|
||||||
href={`/share/proof-of-payment/${_id}/`}
|
href={`/share/proof-of-payment/${_id}/`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { TrashIcon, ExclamationTriangleIcon, ClockIcon, EnvelopeIcon, CheckCircleIcon, PencilSquareIcon, XCircleIcon } from "@heroicons/react/24/outline";
|
import { TrashIcon, ExclamationTriangleIcon, ClockIcon, EnvelopeIcon, CheckCircleIcon, PencilSquareIcon, XCircleIcon } from "@heroicons/react/24/outline";
|
||||||
import { FC, useState } from "react";
|
import { FC, useState } from "react";
|
||||||
import { BillingLocation, UserSettings, YearMonth, EmailStatus } from '@evidencija-rezija/shared-code';
|
import { BillingLocation, UserSettings, YearMonth, EmailStatus, BillsNotificationStrategy } from '@evidencija-rezija/shared-code';
|
||||||
import { updateOrAddLocation } from "../lib/actions/locationActions";
|
import { updateOrAddLocation } from "../lib/actions/locationActions";
|
||||||
import { useFormState } from "react-dom";
|
import { useFormState } from "react-dom";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -45,9 +45,9 @@ export const LocationEditForm: FC<LocationEditFormProps> = ({ location, yearMont
|
|||||||
tenantEmailLanguage: location?.tenantEmailLanguage ?? (locale as "hr" | "en"),
|
tenantEmailLanguage: location?.tenantEmailLanguage ?? (locale as "hr" | "en"),
|
||||||
tenantPaymentMethod: location?.tenantPaymentMethod ?? "none",
|
tenantPaymentMethod: location?.tenantPaymentMethod ?? "none",
|
||||||
proofOfPaymentType: location?.proofOfPaymentType ?? "none",
|
proofOfPaymentType: location?.proofOfPaymentType ?? "none",
|
||||||
billFwdEnabled: location?.billFwdEnabled ?? false,
|
billsNotificationEnabled: location?.billsNotificationEnabled ?? false,
|
||||||
billFwdStrategy: location?.billFwdStrategy ?? "when-payed",
|
billsNotificationStrategy: location?.billsNotificationStrategy ?? BillsNotificationStrategy.WhenPayed,
|
||||||
rentDueNotificationEnabled: location?.rentDueNotificationEnabled ?? false,
|
rentNotificationEnabled: location?.rentNotificationEnabled ?? false,
|
||||||
rentAmount: location?.rentAmount ?? "",
|
rentAmount: location?.rentAmount ?? "",
|
||||||
rentDueDay: location?.rentDueDay ?? 1,
|
rentDueDay: location?.rentDueDay ?? 1,
|
||||||
});
|
});
|
||||||
@@ -279,21 +279,21 @@ export const LocationEditForm: FC<LocationEditFormProps> = ({ location, yearMont
|
|||||||
<label className="label cursor-pointer justify-start gap-3">
|
<label className="label cursor-pointer justify-start gap-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="billFwdEnabled"
|
name="billsNotificationEnabled"
|
||||||
className="toggle toggle-primary"
|
className="toggle toggle-primary"
|
||||||
checked={formValues.billFwdEnabled}
|
checked={formValues.billsNotificationEnabled}
|
||||||
onChange={(e) => handleInputChange("billFwdEnabled", e.target.checked)}
|
onChange={(e) => handleInputChange("billsNotificationEnabled", e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<legend className="fieldset-legend">{t("auto-utility-bill-forwarding-toggle-label")}</legend>
|
<legend className="fieldset-legend">{t("auto-utility-bill-forwarding-toggle-label")}</legend>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{formValues.billFwdEnabled && (
|
{formValues.billsNotificationEnabled && (
|
||||||
<fieldset className="fieldset mt-2 p-2">
|
<fieldset className="fieldset mt-2 p-2">
|
||||||
<legend className="fieldset-legend">{t("utility-bill-forwarding-strategy-label")}</legend>
|
<legend className="fieldset-legend">{t("utility-bill-forwarding-strategy-label")}</legend>
|
||||||
<select defaultValue={formValues.billFwdStrategy} className="select input-bordered w-full" name="billFwdStrategy">
|
<select defaultValue={formValues.billsNotificationStrategy} className="select input-bordered w-full" name="billsNotificationStrategy">
|
||||||
<option value="when-payed">{t("utility-bill-forwarding-when-payed")}</option>
|
<option value={BillsNotificationStrategy.WhenPayed}>{t("utility-bill-forwarding-when-payed")}</option>
|
||||||
<option value="when-attached">{t("utility-bill-forwarding-when-attached")}</option>
|
<option value={BillsNotificationStrategy.WhenAttached}>{t("utility-bill-forwarding-when-attached")}</option>
|
||||||
</select>
|
</select>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
@@ -307,16 +307,16 @@ export const LocationEditForm: FC<LocationEditFormProps> = ({ location, yearMont
|
|||||||
<label className="label cursor-pointer justify-start gap-3">
|
<label className="label cursor-pointer justify-start gap-3">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="rentDueNotificationEnabled"
|
name="rentNotificationEnabled"
|
||||||
className="toggle toggle-primary"
|
className="toggle toggle-primary"
|
||||||
checked={formValues.rentDueNotificationEnabled}
|
checked={formValues.rentNotificationEnabled}
|
||||||
onChange={(e) => handleInputChange("rentDueNotificationEnabled", e.target.checked)}
|
onChange={(e) => handleInputChange("rentNotificationEnabled", e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<legend className="fieldset-legend">{t("auto-rent-notification-toggle-label")}</legend>
|
<legend className="fieldset-legend">{t("auto-rent-notification-toggle-label")}</legend>
|
||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{formValues.rentDueNotificationEnabled && (
|
{formValues.rentNotificationEnabled && (
|
||||||
<div className="animate-expand-fade-in origin-top">
|
<div className="animate-expand-fade-in origin-top">
|
||||||
<fieldset className="fieldset mt-2 p-2">
|
<fieldset className="fieldset mt-2 p-2">
|
||||||
<legend className="fieldset-legend">{t("rent-due-day-label")}</legend>
|
<legend className="fieldset-legend">{t("rent-due-day-label")}</legend>
|
||||||
@@ -356,7 +356,7 @@ export const LocationEditForm: FC<LocationEditFormProps> = ({ location, yearMont
|
|||||||
)}
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{(formValues.billFwdEnabled || formValues.rentDueNotificationEnabled) && (
|
{(formValues.billsNotificationEnabled || formValues.rentNotificationEnabled) && (
|
||||||
<fieldset className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4 pb-2 mt-4">
|
<fieldset className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4 pb-2 mt-4">
|
||||||
<legend className="fieldset-legend font-semibold uppercase">{t("tenant-email-legend")}</legend>
|
<legend className="fieldset-legend font-semibold uppercase">{t("tenant-email-legend")}</legend>
|
||||||
<label className="label">
|
<label className="label">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { ViewBillBadge } from "./ViewBillBadge";
|
|||||||
import { EncodePayment, PaymentParams } from "hub-3a-payment-encoder";
|
import { EncodePayment, PaymentParams } from "hub-3a-payment-encoder";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { LinkIcon } from "@heroicons/react/24/outline";
|
import { LinkIcon } from "@heroicons/react/24/outline";
|
||||||
import { uploadUtilBillsProofOfPayment } from "../lib/actions/locationActions";
|
import { uploadBillsProofOfPayment } from "../lib/actions/locationActions";
|
||||||
import QRCode from "react-qr-code";
|
import QRCode from "react-qr-code";
|
||||||
import { TicketIcon } from "@heroicons/react/24/solid";
|
import { TicketIcon } from "@heroicons/react/24/solid";
|
||||||
import { Pdf417Barcode } from "./Pdf417Barcode";
|
import { Pdf417Barcode } from "./Pdf417Barcode";
|
||||||
@@ -33,7 +33,7 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
|
|||||||
tenantTown,
|
tenantTown,
|
||||||
tenantPaymentMethod,
|
tenantPaymentMethod,
|
||||||
// NOTE: only the fileName is projected from the DB to reduce data transfer
|
// NOTE: only the fileName is projected from the DB to reduce data transfer
|
||||||
utilBillsProofOfPayment,
|
billsProofOfPayment,
|
||||||
proofOfPaymentType,
|
proofOfPaymentType,
|
||||||
} = location;
|
} = location;
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
|
|||||||
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
const [attachmentUploadedAt, setAttachmentUploadedAt] = useState<Date | null>(utilBillsProofOfPayment?.uploadedAt ?? null);
|
const [attachmentUploadedAt, setAttachmentUploadedAt] = useState<Date | null>(billsProofOfPayment?.uploadedAt ?? null);
|
||||||
const [attachmentFilename, setAttachmentFilename] = useState(utilBillsProofOfPayment?.fileName);
|
const [attachmentFilename, setAttachmentFilename] = useState(billsProofOfPayment?.fileName);
|
||||||
|
|
||||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
@@ -66,9 +66,9 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('utilBillsProofOfPayment', file);
|
formData.append('billsProofOfPayment', file);
|
||||||
|
|
||||||
const result = await uploadUtilBillsProofOfPayment(shareId, formData);
|
const result = await uploadBillsProofOfPayment(shareId, formData);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setAttachmentFilename(file.name);
|
setAttachmentFilename(file.name);
|
||||||
@@ -212,8 +212,8 @@ export const ViewLocationCard: FC<ViewLocationCardProps> = ({ location, userSett
|
|||||||
</label>
|
</label>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
id="utilBillsProofOfPayment"
|
id="billsProofOfPayment"
|
||||||
name="utilBillsProofOfPayment"
|
name="billsProofOfPayment"
|
||||||
type="file"
|
type="file"
|
||||||
accept="application/pdf"
|
accept="application/pdf"
|
||||||
className="file-input file-input-bordered grow file-input-sm my-2 block max-w-[17em] md:max-w-[80em] break-words"
|
className="file-input file-input-bordered grow file-input-sm my-2 block max-w-[17em] md:max-w-[80em] break-words"
|
||||||
|
|||||||
Reference in New Issue
Block a user