Workflow: Billing & Payments
This is the operational view. The data model and module surface live in Billing module.
Status machine
Two parallel write paths
The billing module supports two ways an invoice can come into existence:
- Direct create — staff fills the form, hits Save, an invoice is born.
- Estimate → invoice conversion —
POST /invoices/{invoice}/convert(requiresinvoice.convert). The estimate row is upgraded to a full invoice atomically, withlockForUpdateon the row to prevent double-conversion.
Quick-checkout (invoice + payment in one call)
Record an additional payment
POST /api/v1/tenant/{tenant}/invoices/{invoice}/payments with {amount, payment_method, reference_number?}. The controller writes the payments row and updates the invoice. The recommended pattern (already used by convert, on the audit roadmap for recordPayment) is to recompute balance_due from the ledger inside DB::transaction + lockForUpdate, not subtract.
Razorpay payment flow (owner side)
The Razorpay webhook (POST /api/v1/webhooks/razorpay) handles backend-to-backend events (payment.captured, payment.failed, refund.processed).
Audit P0 — both
verifyand the webhook should re-fetch the canonical payment amount from Razorpay before mutating local state. The HMAC signature only proves the request originated from Razorpay; it doesn't prove the amount in the body matches what the customer actually paid. See Billing module for the fix.
Refund flow
POST /api/v1/tenant/{tenant}/invoices/{invoice}/refunds (gated on payment.refund). Records a refund payment row with status=refunded and, when the source payment was Razorpay, dispatches a real refund call through the gateway.
Receipts
Generated on demand by App\Support\Receipts\ReceiptPdfGenerator (uses dompdf).
- Download:
GET /api/v1/tenant/{tenant}/invoices/{invoice}/receipt(clinic) andGET /api/v1/owner/me/invoices/{invoice}/receipt(owner). - Email:
POST /api/v1/tenant/{tenant}/invoices/{invoice}/receipt/emailenqueuesSendReceiptEmailJob. The job renders thereceipt_emailtemplate (subject + cover note), attaches the freshly generated PDF, and sends through the configured driver. After a successful send,receipt_emailed_atandreceipt_emailed_toare stamped, plus a custom audit eventreceipt.emailed.