Billing & Payments
The billing module covers estimates, invoices, manual payments, gateway payments via Razorpay, refunds, and receipts (PDF + email).
Document lifecycle
estimate (draft) ──convert──▶ invoice (pending)
│
├─ payments accumulate ──▶ partial / paid
│
└─ void
- An estimate is an
invoicesrow withinvoice_type=estimate. It records the proposal but doesn't enter the AR ledger. POST /invoices/{invoice}/convertatomically flips the type toinvoice, stamps aninvoice_number, and recomputesbalance_duefrom the payments ledger. Wrapped inDB::transaction+lockForUpdate.- Status transitions:
draft → pending → partial → paidorpending → void.
Recording a payment
POST /api/v1/tenant/{tenant}/invoices/{invoice}/payments creates a payments row and updates the invoice. The recommended pattern (already used by convert, on the roadmap for recordPayment — see audit P1) is to recompute balance_due from the ledger rather than subtracting the new payment, so concurrent payments can't race.
Payment methods enum: cash, card, upi, bank_transfer, online.
Razorpay integration
The owner-side payment flow runs through two endpoints:
POST /api/v1/owner/me/invoices/{invoice}/razorpay/order— creates a Razorpay order viaRazorpayGateway::createOrder, returns the order id and prefill data.- The vetty app launches the Razorpay checkout SDK with that order. The user pays.
POST /api/v1/owner/me/invoices/{invoice}/razorpay/verify— receives{razorpay_order_id, razorpay_payment_id, razorpay_signature}, verifies the HMAC signature, persists apaymentsrow withgateway=razorpayand the triple, recomputes the balance.
A separate webhook (POST /api/v1/webhooks/razorpay) handles backend-to-backend events (payment.captured, payment.failed, refund.processed) — signature is verified inside the handler against RAZORPAY_WEBHOOK_SECRET.
Critical audit items (security)
The audit (AUDIT.md §5) flags two P0 issues in the Razorpay flow:
- The
verifyendpoint trustsrazorpay_payment_idafter signature checks, but doesn't re-fetch the canonical amount from the Razorpay API. An attacker who triggers a small payment + intercepts the response can swap in theirpayment_idfor the larger invoice. The fix is to callpayments/{id}from the Razorpay client insideverify()and assertamount === invoice.balance_due * 100. - The webhook trusts
payload.amountfrom the body. Same mitigation — re-fetch.
A third issue: the gateway silently rejects all events when RAZORPAY_WEBHOOK_SECRET is unset — operator never finds out until the first payment fails to mark paid. Fail loudly in production.
Refunds
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). Two surfaces:
- 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/emailenqueuesSendReceiptEmailJobwhich renders thereceipt_emailtemplate (subject + cover note) and attaches the freshly generated PDF.
After a successful send, the invoice gets receipt_emailed_at and receipt_emailed_to stamped, plus a custom audit event receipt.emailed.
Permissions
Permission keys: invoice.read, invoice.create, invoice.update, invoice.convert, payment.create, payment.refund. Reminder/email fanout reuses reminder.send for the email-receipt action.
Money columns
Every monetary column is decimal(12,2). Currency is per-tenant (tenants.currency). Audit notes the decimal:2 cast truncates instead of rounding — a custom rounding cast is on the roadmap.
Audit + roadmap (selected)
- P0 — Razorpay verify and webhook need the re-fetch + amount assertion described above.
- P1 — Make
invoice_numberunique on(tenant_id, invoice_number)rather than globally; the current global-unique constraint will collide across tenants. - P1 — Wrap
recordPaymentinDB::transaction+ recompute balance from ledger. - P1 — Add the "your invoice was issued" notification (only the post-payment receipt fires today).
- P1 — Structured invoice line items (currently
notescarries the line description). The receipt PDF synthesises a single fake line — line items are on the Wave 3 roadmap. - P2 — Customer ledger / statement of account. The owner-side
/me/statement.pdfis implemented; the clinic-side AR view is still on the roadmap. - P2 — GST e-invoicing (India NIC integration), credit notes, late-fee automation.
- P3 — Stripe / PayU adapters behind the existing
PaymentGatewayinterface.