You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
106 lines
5.3 KiB
TypeScript
106 lines
5.3 KiB
TypeScript
import { formatINR } from '@sims/domain'
|
|
|
|
/** Templated reminder emails — one place so WhatsApp can reuse them later (spec §7). */
|
|
|
|
export type ReminderRuleKind =
|
|
| 'invoice_overdue' | 'renewal_due' | 'amc_expiring'
|
|
| 'follow_up' | 'recurring_generated' | 'email_bounced' | 'quote_followup'
|
|
|
|
export interface ReminderContext {
|
|
clientName: string; companyName: string
|
|
docNo?: string; amountPaise?: number; dueDate?: string
|
|
daysOverdue?: number; coverage?: string; period?: string
|
|
/** quote_followup: client-facing reference — the docNo, or 'dated YYYY-MM-DD' when unissued (never "null"). */
|
|
ref?: string
|
|
/** quote_followup: the live public share link (resolved read-only; minted only in the send path). */
|
|
shareUrl?: string
|
|
/** quote_followup: dated message text from resolveSchedule; falls back to the code constants below. */
|
|
subjectTemplate?: string
|
|
bodyTemplate?: string
|
|
}
|
|
|
|
/** Code-constant fallback text for quote_followup (spec §7) — reminder_schedule rows
|
|
* override these with dated subject/body; they live here so repos-reminders can
|
|
* reuse them without an import cycle. */
|
|
export const QUOTE_FOLLOWUP_DEFAULT_SUBJECT = 'Following up on our quotation {ref} ({companyName})'
|
|
export const QUOTE_FOLLOWUP_DEFAULT_BODY =
|
|
'Dear {clientName}, we wanted to check whether you had a chance to review our quotation {ref}. '
|
|
+ 'You can view it any time here: {shareUrl}. We\'d be glad to answer any questions. '
|
|
+ 'Warm regards, {companyName}.'
|
|
|
|
export interface ReminderMail { subject: string; bodyText: string }
|
|
|
|
const signOff = (ctx: ReminderContext): string => `Warm regards,\n${ctx.companyName}`
|
|
|
|
export function reminderEmail(kind: ReminderRuleKind, ctx: ReminderContext): ReminderMail {
|
|
switch (kind) {
|
|
case 'invoice_overdue': {
|
|
const amt = ctx.amountPaise !== undefined ? formatINR(ctx.amountPaise) : ''
|
|
// D18: with a due date the mail says the honest thing ("was due on X, N days
|
|
// overdue"); legacy invoices without one keep the past-due-since-issue wording.
|
|
const lateness = ctx.dueDate !== undefined
|
|
? ` was due on ${ctx.dueDate}` + (ctx.daysOverdue !== undefined ? ` and is now ${ctx.daysOverdue} day(s) overdue` : '')
|
|
: ` is outstanding` + (ctx.daysOverdue !== undefined ? ` and is now ${ctx.daysOverdue} day(s) past due` : '')
|
|
return {
|
|
subject: `Payment reminder — Invoice ${ctx.docNo} (${ctx.companyName})`,
|
|
bodyText:
|
|
`Dear ${ctx.clientName},\n\n`
|
|
+ `This is a gentle reminder that Invoice ${ctx.docNo}${amt !== '' ? ` for ${amt}` : ''}${lateness}`
|
|
+ `. We would be grateful if you could arrange payment at your earliest convenience.\n\n`
|
|
+ `If payment has already been made, kindly ignore this message.\n\n`
|
|
+ signOff(ctx),
|
|
}
|
|
}
|
|
case 'recurring_generated': {
|
|
const amt = ctx.amountPaise !== undefined ? formatINR(ctx.amountPaise) : ''
|
|
const forPeriod = ctx.period !== undefined ? ` for ${ctx.period}` : ''
|
|
return {
|
|
subject: `Invoice ${ctx.docNo}${forPeriod} (${ctx.companyName})`,
|
|
bodyText:
|
|
`Dear ${ctx.clientName},\n\n`
|
|
+ `Please find attached Invoice ${ctx.docNo}${forPeriod}${amt !== '' ? `, amounting to ${amt}` : ''}.\n\n`
|
|
+ `Kindly arrange payment as per the agreed terms. Thank you for your continued association.\n\n`
|
|
+ signOff(ctx),
|
|
}
|
|
}
|
|
case 'renewal_due':
|
|
return {
|
|
subject: `Subscription renewal reminder (${ctx.companyName})`,
|
|
bodyText:
|
|
`Dear ${ctx.clientName},\n\n`
|
|
+ `Your subscription is due for renewal${ctx.dueDate !== undefined ? ` on ${ctx.dueDate}` : ' shortly'}. `
|
|
+ `Please let us know if you would like us to raise the renewal invoice.\n\n`
|
|
+ `We value your continued association and look forward to serving you.\n\n`
|
|
+ signOff(ctx),
|
|
}
|
|
case 'amc_expiring':
|
|
return {
|
|
subject: `AMC renewal reminder${ctx.coverage !== undefined && ctx.coverage !== '' ? ` — ${ctx.coverage}` : ''} (${ctx.companyName})`,
|
|
bodyText:
|
|
`Dear ${ctx.clientName},\n\n`
|
|
+ `Your Annual Maintenance Contract${ctx.coverage !== undefined && ctx.coverage !== '' ? ` (${ctx.coverage})` : ''} `
|
|
+ `is due to expire${ctx.dueDate !== undefined ? ` on ${ctx.dueDate}` : ' soon'}. `
|
|
+ `To ensure uninterrupted support, please confirm the renewal at your convenience.\n\n`
|
|
+ signOff(ctx),
|
|
}
|
|
case 'quote_followup': {
|
|
// Dated subject/body come from reminder_schedule via the context builder;
|
|
// placeholders are filled here so a schedule row can rewrite the copy freely.
|
|
const fill = (t: string): string => t
|
|
.replaceAll('{ref}', ctx.ref ?? '')
|
|
.replaceAll('{clientName}', ctx.clientName)
|
|
.replaceAll('{companyName}', ctx.companyName)
|
|
.replaceAll('{shareUrl}', ctx.shareUrl ?? '')
|
|
return {
|
|
subject: fill(ctx.subjectTemplate ?? QUOTE_FOLLOWUP_DEFAULT_SUBJECT),
|
|
bodyText: fill(ctx.bodyTemplate ?? QUOTE_FOLLOWUP_DEFAULT_BODY),
|
|
}
|
|
}
|
|
case 'follow_up':
|
|
case 'email_bounced':
|
|
// Internal dashboard items — never emailed to the client. A neutral value
|
|
// exists so callers can't crash; repos-reminders refuses to send these kinds.
|
|
return { subject: '(internal reminder)', bodyText: '' }
|
|
}
|
|
}
|