Credit Notes v2 — Documents, Store-Credit Application & Audit Completeness (2026-06-16)
Builds on the shipped v1 (commit 8416d573d) and its design spec2026-06-15-credit-notes-design.md. v1 delivered: route + schema, Credits tab,
Totals card, full-screen create flow, void, and cash-basis reports wiring.
This spec covers the four follow-ups the user requested.
Goals
- Credit note is a first-class document — its own detail page with on-screen render, PDF, print and a public share link, an exact replica of the invoice/receipt/quotation experience.
- Store credit is usable — when a patient with
store_credit_balance > 0gets a new invoice, staff can apply it (one tap); on a new quote it is shown for information only. - Audit/history is complete and reliable — every invoice/quote/credit-note lifecycle event lands on the parent entity’s History timeline.
- UI polish — tighten the Credits tab, create page and new detail page to match the finance document bar.
billing.credits.manage permission.
Workstream 1 — Credit note document, detail page, PDF, print, share
Detail page
- New
ui/src/pages/finance/CreditNoteDetailPage.tsx, modeled onui/src/pages/finance/ReceiptDetailPage.tsx. - Route in
ui/src/App.tsx:/dashboard/finance/invoices/:id/credit-notes/:cnId(admin/superadmin only,RequireModule module="finance"). Reached by clicking a credit-note row in the invoice Credits tab (replaces today’s inert row). - Data: new serverComm
getCreditNote(cnId)hitting the existingGET /credit-notes/:id(returns the credit note + items). Loads the parent invoice + patient for header context. - Actions: Print, Download (letterhead), Share link, and (when status===‘issued’) Void — reusing the existing void mutation/dialog.
On-screen document
- New
ui/src/components/documents/CreditNoteDocument.tsx, mirroringInvoiceDocument.tsx: clinic letterhead/branding, “CREDIT NOTE” title +credit_note_number, patient block, reference to the original invoice number, credited line items (description, qty, unit, credited amount, cost reversed?), then the split block — Credited revenue, Cost reversed, Adjustment to bill, Against money paid, and the outcome (Refund: early-exit fee + cash refunded; or Store credit added). Reason + status stamp (draft / issued / void). - Accepts optional
signatureDataUrl/stampDataUrl(same processing as the invoice page) for the on-screen + clinic-download copy.
- New
ui/src/lib/pdf/CreditNote.tsx(react-pdf) — the live path, modeled onui/src/lib/pdf/Receipt.tsx/Invoice.tsx. Thetemplates/*Pdf.tsxfiles are DEAD; do not touch them. - Wired through
openPdfInNewTab(...)andLetterheadDownloadButtonexactly as ReceiptDetailPage does.
PrintButton+useDocumentPrinthook +doc-print-wrapper, same as the invoice/receipt pages.
Public share link (server + public page)
server/src/routes/public-documents-protected.ts: add'credit_note'to thedocumentTypez.enum([...])and a validation branch (load credit note by id, confirm it belongs to the clinic) alongside the existing invoice/receipt branches.server/src/routes/public-documents.ts: add a fetch-by-token branch that returns the credit note + items + clinic branding fordocumentType:'credit_note'.ui/src/pages/PublicDocumentPage.tsx: add'credit_note'to the type union and a render branch usingCreditNoteDocumentwith no signature/stamp (public copy stays unsigned — anti-misuse rule). No accept/reject CTA (credit notes are not actionable by the patient, unlike quotations).public_document_links.documentTypeis free-text; only the comment needs updating, no DDL.
Workstream 2 — Store credit: show + one-tap apply
New invoice (apply)
- Server
POST /invoices(and the appointment-invoice + with-installments create paths) accept an optionalappliedStoreCreditnumber. The server:- re-reads
patients.store_credit_balanceand clampsapplied = min(requested, balance, invoiceTotal)— never trusts the client; - within the create transaction: sets
invoices.applied_store_credit = applied, decrementspatients.store_credit_balance -= applied, and setsinvoices.balance = total - amount_paid - applied(status → ‘paid’ if ≤ 0). - No receipt is created and cash reports are untouched — store credit is previously-collected cash, not new revenue, so folding it into receipts would double-count. (Consistent with v1’s cash-basis reports.)
- records an audit event (Workstream 3).
- re-reads
- UI: in the invoice create surfaces (
ui/src/components/billing/InvoiceForm.tsxandAppointmentInvoiceDialog.tsx/AppointmentInvoiceSheet.tsx), when the selected patient hasstore_credit_balance > 0, show a banner with an Apply control (numeric input defaulting tomin(balance, total), capped). Submit passesappliedStoreCredit. InvoiceDocumentshows an “Store credit applied” line whenapplied_store_credit > 0.
New quote (show only)
- In the quotation create/detail surfaces, when the patient has store credit, show an informational note (“Patient has PKR X store credit”) with a link to the source credit note. No application — a quote is not money.
Patient store-credit visibility
- Surface
store_credit_balanceon the patient header/billing area so staff can see it outside the invoice flow (small addition; read-only).
Workstream 3 — Audit / history completeness (mirror-log)
Root cause:getEntityActivity(entityType, entityId, clinicId) filters
strictly by entityType + entityId, so only rows logged as entityType:'invoice'
for that invoice appear on its History tab. Child-entity events (receipts,
payments, credit notes) and several invoice/quote endpoints either log under a
different entityType or don’t log at all.
Approach (A — mirror-log to parent). Add two thin helpers in
server/src/lib/activity.ts:
logInvoiceEvent({ db, clinicId, invoiceId, patientId, userId, userRole, action, message, changes })logQuotationEvent({ ... quotationId ... })
recordActivity with entityType:'invoice' / 'quotation'. Child
flows continue to log their own entity AND call the parent helper so the event
shows on the parent timeline.
Events to guarantee on the INVOICE timeline:
- created, updated, sent, status→cancelled/closed, split, convert-to-installments
- payment recorded / receipt issued (mirror from
receipts.tsx) - share link created + revoked (from
public-documents*) - credit note: draft created, issued, voided (from
credit-notes.tsx— today onlyissuelogs; add draft + void) - store credit applied / reversed (Workstream 2)
- created, updated, sent, accepted, rejected, revision_requested, reissued, converted-to-invoice, share link created/revoked.
invoices.tsx /:id/split (no log);
credit-notes.tsx draft + void (no log); receipts/payments not consistently
mirrored to the invoice; quote→invoice conversion only logs one side; share/revoke
coverage. Each gets the appropriate logInvoiceEvent/logQuotationEvent call.
No timeline/query change — rendering and getEntityActivity stay as-is; we
only ensure the rows exist. Messages stay human-readable (they render verbatim in
the timeline).
Workstream 4 — UI polish
- Credits tab: row click → new detail page; consistent finance styling; clearer empty/loading states; the Totals card and refund/fee footnotes already shipped — align spacing/typography with the receipt/invoice pages.
- Create page: align field styling, confirm the live-preview matches the server split, tidy the type selector and line picker on mobile.
- Store-credit banner styled to match finance cards (theme-aware light + dark).
- Get a visual pass (reload + screenshot or Playwright) before calling UI done — tsc/build does not prove UI quality.
Data model
No new tables. Columns already exist from v1 (invoices.amount_credited,
invoices.applied_store_credit, patients.store_credit_balance,
credit_notes, credit_note_items) and are live on prod. The store-credit
application path is the first writer of invoices.applied_store_credit.
Reports
Already cash-basis and correct from v1 (subtract refunded cash + reversed cost). Store-credit application creates no receipt, so it correctly does not change realized cash revenue. No further reports work in this spec.Out of scope (YAGNI)
- Standalone “Credit Notes” sidebar list (credit notes always belong to an invoice).
- Emailed-PDF auto-share beyond the copy link (the separate
autoShareDocumentsByEmailswitch is unchanged). - Spending store credit anywhere except a new invoice.
- EOD report credit integration (still deferred from v1; daily accrual-vs-prior- invoice semantics need their own pass).
- Editable early-exit-fee % settings editor (still deferred; preview uses the 15% server default).
- Credit-note numbering reconciliation with
generateDocumentNumber(count-basedCN-####with the unique index stays).
Testing / verification
- Server: permission parity tests still green; add/verify the activity helpers.
- Money math unchanged (server
computeSplitis the authority); store-credit clamp enforced server-side and unit-checkable. - Visual pass on the new detail page, public share render, store-credit banner, and a real PDF (eyeball it — react-pdf layout must be checked, per the Urdu/PDF lessons) before shipping.

