Integration Patterns
Vetty integrates with three external systems today: Razorpay (payments), an email provider (Mailgun or AWS SES, switchable per tenant), and dompdf (in-process PDF rendering). Two more channels (SMS, Push) are scaffolded but not implemented.
Notification driver abstraction
The single contract App\Notifications\Contracts\NotificationDriver lets every outbound message take the same code path:
Job ──▶ TemplateRenderer ──▶ OutboundMessage ──▶ DriverManager ──▶ Mailgun / SES / Mail / Log
▲
│
resolved per request
from config + service_settings
See Notifications module for the operational story. Architecturally:
- Per-tenant overrides —
service_settingscarries optionalmail.driver,mail.from.address, etc. for a tenant. TheNotificationDriverManagerfalls back to platformconfig('notifications.*')values when there's no tenant override. - Encrypted secrets — provider keys live in
service_settings.valuewith an encrypted cast, so dumps never expose them in plaintext. - Driver health — each driver implements a
name()method; the admin UI hitsGET /reminders/healthto verify a driver is configured. - Future channels — SMS and Push providers are listed in the admin UI scaffold but the corresponding
Driverimplementations don't exist yet (audit P3).
Razorpay payments
App\Services\Payments\RazorpayGateway wraps the official razorpay/razorpay SDK. It implements the same PaymentGateway contract that future Stripe / PayU adapters can plug into.
Order + verify flow (vetty → Razorpay)
1. POST /me/invoices/{id}/razorpay/order
──▶ RazorpayGateway::createOrder(amount, currency, receipt)
──▶ Razorpay API
◀── {order_id, amount, currency}
2. App launches Razorpay SDK with order_id + key
──▶ User completes payment on the SDK sheet
◀── SDK returns {payment_id, signature}
3. POST /me/invoices/{id}/razorpay/verify
──▶ RazorpayGateway::verifyPaymentSignature({order_id, payment_id, signature})
──▶ HMAC check
──▶ Persist Payment row, recompute balance
Webhook flow (Razorpay → backend)
1. Razorpay POSTs to /api/v1/webhooks/razorpay with the payload + signature
2. RazorpayGateway::verifyWebhookSignature(rawBody, signature) using RAZORPAY_WEBHOOK_SECRET
3. Dispatch on event:
- payment.captured ──▶ ensure Payment row exists / is captured
- payment.failed ──▶ stamp the failure reason
- refund.processed ──▶ reflect refund on the original Payment
4. Audit-log the event
Critical audit items
The audit (AUDIT.md §5) flags two P0 trust-boundary issues:
- The verify endpoint validates the HMAC over the
(order_id, payment_id, signature)triple but doesn't re-fetch the canonical amount from Razorpay — an attacker who hijacks a small payment id can mark a large invoice paid. Fix: callpayments/{id}from the Razorpay client insideverify()and assert againstinvoice.balance_due * 100. - The webhook trusts
payload.amount— same mitigation.
A third issue: when RAZORPAY_WEBHOOK_SECRET is unset the gateway silently rejects all events. Fail loudly.
These are scheduled into Wave 1 of the audit's implementation plan.
PDF generation (dompdf)
In-process via barryvdh/laravel-dompdf. Two generators today:
App\Support\Receipts\ReceiptPdfGenerator— for invoice receipts. Accepts anInvoiceplus aregenerateflag; caches the result on the model.- Owner portal PDF helpers in
Owner\OwnerPortalController— vaccination certificate, statement, pet passport. Generated on demand, no caching.
PDFs are streamed back through Laravel rather than served from disk, so every fetch re-runs the auth + permission checks.
Webhooks: shared trust boundary
Razorpay is the only inbound webhook today, but the audit recommends a small WebhookController base class that enforces:
- Signature verification (each provider's HMAC scheme).
- Idempotency key (don't double-apply on replay).
- Re-fetch from upstream before mutating local state.
- Raw payload size cap (DoS guard).
- Audit-log on receipt regardless of whether processing succeeded.
This becomes more relevant once Mailgun bounces, Twilio inbound SMS, and FCM push delivery receipts wire in.
Mail providers
MailgunDriver and SesDriver are the production-ready implementations. MailDriver (Laravel mail-stack passthrough) is used by tests and works with the array driver so feature tests can assert on outbound messages without sending.
The dev-only endpoint POST /api/v1/dev/mail-test is wired to bypass the queue and send a hand-built test message through the active driver — useful for verifying creds without going through the full reminder flow. The route only registers when APP_ENV=local; the controller also guards.
Flutter HTTP clients
Both Flutter apps share a thin ApiClient wrapper in core/api/. Conventions:
- Bearer token from
auth_store.dartis attached per request. - 401 responses clear the local session immediately (no token refresh — audit P1).
- Errors are surfaced as a typed
ApiExceptionwith the backend'smessageanderrorsfields.
The audit calls out (P0) the lack of certificate pinning on both apps.
React HTTP client
ui/src/services/index.js carries the React app's HTTP wrapper. Audit (P1) calls out:
- The Bearer header reads
getAccessToken()once at construction time — token rotation breaks subsequent requests. - Tokens live in
localStorage(XSS-stealable). Recommended fix: backend issues an httpOnly Secure cookie; frontend never sees the token. - No CSRF token on POST forms — Sanctum's stateful flow needs the
XSRF-TOKENcookie + header.
Roadmap
- Stripe + PayU adapters behind the
PaymentGatewaycontract. - Twilio + MSG91 SMS drivers.
- FCM + APNS push drivers.
- WhatsApp Business API.
- GST e-invoicing (India NIC) for India-mode tenants.
- Mailgun bounce webhook → auto-opt-out.
See AUDIT.md §6 (notifications), §5 (billing/payments), and §8 (frontends) for the full punch-list.