Skip to main content

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 overridesservice_settings carries optional mail.driver, mail.from.address, etc. for a tenant. The NotificationDriverManager falls back to platform config('notifications.*') values when there's no tenant override.
  • Encrypted secrets — provider keys live in service_settings.value with an encrypted cast, so dumps never expose them in plaintext.
  • Driver health — each driver implements a name() method; the admin UI hits GET /reminders/health to verify a driver is configured.
  • Future channels — SMS and Push providers are listed in the admin UI scaffold but the corresponding Driver implementations 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:

  1. 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: call payments/{id} from the Razorpay client inside verify() and assert against invoice.balance_due * 100.
  2. 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 an Invoice plus a regenerate flag; 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.dart is attached per request.
  • 401 responses clear the local session immediately (no token refresh — audit P1).
  • Errors are surfaced as a typed ApiException with the backend's message and errors fields.

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-TOKEN cookie + header.

Roadmap

  • Stripe + PayU adapters behind the PaymentGateway contract.
  • 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.