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.
sims-hq/apps/hq/src/reminder-templates.ts

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: '' }
}
}