# Quotation Delivery — PDF + Email + WhatsApp + Fixed Reminders

**Date:** 2026-02-26
**Status:** Approved
**Branch:** performance-fixes-local

## Goal

When a quotation is dispatched (created with sendImmediately or manually sent), the customer receives:

1. An email with the quotation as a PDF attachment and a Stripe payment link
2. A WhatsApp message with the portal/payment link

If the quotation remains unpaid after the due date, a fixed 3-step reminder sequence fires automatically:

- Day 0 (on due): reminder
- Day +2: reminder
- Day +5: overdue notice

## Decisions

| Decision          | Choice                          | Reason                                        |
| ----------------- | ------------------------------- | --------------------------------------------- |
| PDF library       | `@react-pdf/renderer`           | Server-side, no headless Chrome, lightweight  |
| Channels          | Email (PDF attached) + WhatsApp | Both when contact info available              |
| Reminder schedule | Fixed 3-step sequence           | Non-configurable, simpler, always predictable |

## Architecture: Option B — Dedicated Services

### New files

#### `lib/services/quotation-pdf.service.ts`

Single export: `generateQuotationPdf(quotation) → Promise<Buffer>`

- Builds a React PDF document using `@react-pdf/renderer`
- Content: header (number, title, dates), customer block, line items table, totals (subtotal/tax/discount/grand total), footer (portal URL, expiry)
- Bilingual: detects from `quotation.customer.language`, falls back to `'en'`
- Returns an in-memory Buffer — never written to disk

#### `lib/services/quotation-notifier.service.ts`

Single export: `notifyQuotationCustomer(quotation, type: 'dispatch' | 'reminder' | 'overdue') → Promise<{ emailQueued: boolean, whatsappSent: boolean }>`

**Flow:**

1. Generate PDF buffer (failure → log warning, continue without attachment)
2. If `quotation.customer.email` exists → enqueue email job with PDF attachment via `enqueueJob('email', { ..., attachments: [{ filename: 'quotation-{number}.pdf', content: base64, encoding: 'base64' }] })`
   - `dispatch` → `createQuotationSentEmail` template
   - `reminder` → `createQuotationReminderEmail` template
   - `overdue` → `createQuotationOverdueEmail` template
3. If `quotation.customer.phone` exists AND org has Twilio credentials → call `twilioWhatsAppService.sendTextMessage`
   - `dispatch`: `"📋 New Quotation #{number} - {title}\nTotal: {currency} {amount}\nPay/view here: {portalUrl}"`
   - `reminder`: `"⏰ Payment reminder: Quotation #{number} due {dueDate}\nRemaining: {currency} {amount}\nPay now: {portalUrl}"`
   - `overdue`: `"🚨 Payment overdue: Quotation #{number} - {daysOverdue} days past due\nPay now: {portalUrl}"`
4. Return `{ emailQueued, whatsappSent }`

All failures are non-blocking (try/catch → log warning → continue).

### Modified files

#### `lib/services/quotations.service.ts`

- Remove inline email enqueue + mock SMS log from `dispatchQuotation`
- Replace with: `const notify = await notifyQuotationCustomer(quotation, 'dispatch')`
- Expand `DispatchResult` type: add `whatsappSent: boolean`
- Return `{ quotation, phoneStatus: 'sent', emailQueued: notify.emailQueued, whatsappSent: notify.whatsappSent }`

#### `lib/services/quotation-reminders.service.ts`

- Add hard-coded constant at top of file:
  ```typescript
  const REMINDER_SEQUENCE = [
    { trigger: "on_due", dayOffset: 0 },
    { trigger: "after_due", dayOffset: 2 },
    { trigger: "after_due", dayOffset: 5 },
  ];
  ```
- `processDueReminders` uses `REMINDER_SEQUENCE` instead of `settings.reminderSchedule`; removes `reminderEnabled` check — runs for all unpaid/overdue quotations with a due date
- `sendQuotationReminder` replaces inline email + WhatsApp placeholder with `notifyQuotationCustomer(quotation, type)` where type is `'reminder'` for dayOffset ≤ 2, `'overdue'` for dayOffset 5

#### `package.json`

- Add `@react-pdf/renderer` (server-side peer)

## Error Handling

| Failure                                  | Behaviour                                                                                |
| ---------------------------------------- | ---------------------------------------------------------------------------------------- |
| PDF generation fails                     | Log warning, send email without attachment, send WhatsApp                                |
| Email enqueue fails                      | Log warning, WhatsApp still sends, `emailQueued: false`                                  |
| WhatsApp fails (no creds / Twilio error) | Log warning, email still sends, `whatsappSent: false`                                    |
| Both fail                                | Log error, return `{ emailQueued: false, whatsappSent: false }` — quotation remains SENT |

## Tests

| File                                                 | Coverage                                                                                                                                     |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `__tests__/lib/services/quotation-pdf.test.ts`       | `generateQuotationPdf` returns non-empty Buffer                                                                                              |
| `__tests__/lib/services/quotation-notifier.test.ts`  | sends both; skips email when no email; skips WhatsApp when no phone; skips WhatsApp when no Twilio creds; PDF failure doesn't block delivery |
| `__tests__/lib/services/quotation-reminders.test.ts` | 3-step sequence fires; dedup prevents double-send; paid quotations skipped                                                                   |

## API Response Changes

Both `POST /api/dashboard/quotations` (with sendImmediately) and `POST /api/dashboard/quotations/[id]/send` will now return:

```json
{
  "success": true,
  "quotation": { ... },
  "phoneStatus": "sent",
  "emailQueued": true,
  "whatsappSent": true
}
```
