Controllers
Controllers live under backend/app/Http/Controllers/Api/V1/. The directory tree mirrors the route prefixes:
Api/V1/
├── AuthController.php
├── PasswordResetController.php
├── DevMailTestController.php (local-only)
├── PetVaccinationController.php (split owner / clinic in routes)
├── PetShareController.php
├── PetHealthLogController.php (split owner / clinic in routes)
├── PublicPetController.php
├── ProviderDirectoryController.php
├── SystemController.php
├── TenantController.php
├── Admin/
│ ├── RoleController.php
│ └── AuditLogController.php
├── Clinic/
│ ├── AppointmentController.php
│ ├── ClientController.php
│ ├── DashboardController.php
│ ├── InvoiceController.php
│ ├── LabResultController.php
│ ├── MedicalAttachmentController.php
│ ├── PatientOnboardingController.php
│ ├── PetController.php
│ ├── PrescriptionController.php
│ ├── ReminderController.php
│ └── VisitController.php
├── Inventory/
│ ├── MedicineController.php
│ ├── PurchaseOrderController.php
│ ├── StockMovementController.php
│ └── SupplierController.php
├── Owner/
│ ├── OrderRequestController.php
│ ├── OwnerPaymentController.php
│ ├── OwnerPortalController.php
│ ├── PetCaregiverController.php
│ ├── PetDocumentController.php
│ ├── PetPassportController.php
│ └── PetPublicProfileController.php
├── Tenant/
│ └── OrderFulfillmentController.php
└── Webhooks/
└── RazorpayWebhookController.php
The base App\Http\Controllers\Controller exposes enforcePermission($key, $tenant) — a thin wrapper around Gate::authorize that the controllers call when middleware can't see enough context (e.g. row-level checks).
Cross-cutting conventions
- Form requests in
backend/app/Http/Requests/Api/V1/validate every write. Tenant-scoped writes always restrictRule::existswithwhere('tenant_id', $tenantId). - Resources in
backend/app/Http/Resources/Api/V1/shape the response. Public-facing endpoints use a narrower resource than authenticated ones (e.g.ProviderListingResourcevs fullTenantResource). - Permissions are checked twice —
require.permission:<key>middleware on the route + a secondenforcePermissionin the controller for row-level decisions. - Audit writes happen automatically through the
Auditabletrait; controllers only need to callauditCustomEventfor non-Eloquent events (e.g.receipt.emailed,appointment.checked_in).
Auth & system
AuthController—login,logout,me,registerOwner. Returns the user + permission flags + active tenant.PasswordResetController—requestReset,verify,reset. Rate-limited at the route layer.SystemController—healthreturns{ok: true}for uptime monitors.DevMailTestController— invokable. Local-only (controller short-circuits unlessAPP_ENV=local). Sends a hand-tuned message through the configured driver to validate Mailgun/SES creds end-to-end.
Public surfaces
ProviderDirectoryController—indexpaginatestenants WHERE is_public=true, supportsbusiness_type,q(full-text), andnear[lat,lng,radius_km](PHP-side haversine + bounding-box pre-filter — see audit).showreturns one tenant.PublicPetController—showJsonfor/api/v1/p/{slug}. (Audit: rate-limit + opt-in + no fallback toclient.phonerecommended.)PetShareController—redeem(auth'd, called by vetdoctor), publicshowfor the pre-install landing page, plus owner CRUD inside the auth group.
Tenant-scoped clinic surfaces
Clinic\AppointmentController—index,show,store,update,updateStatus(legacy),cancel,queue(today's kanban with status grouping).Clinic\ClientController—indexfor now (more endpoints on the audit punch-list).Clinic\DashboardController—showreturns the homepage roll-up (counts + due today + low stock).Clinic\InvoiceController—index,show,store,recordPayment,refundPayment,convert(estimate → invoice withlockForUpdate),downloadReceipt,emailReceipt.Clinic\PrescriptionController— full CRUD;deleteaudited as cancel.Clinic\VisitController— full CRUD +dischargeSummary(PDF).Clinic\LabResultController— full CRUD.Clinic\MedicalAttachmentController—index/store/show/download/destroy. Polymorphic over visits, prescriptions, lab results.Clinic\PatientOnboardingController—store(single-call create-client + create-pet),update(combined edit).Clinic\PetController—index,show(clinic patient briefing),timeline.Clinic\ReminderController—health, full reminder CRUD +retry/dismiss, opt-out CRUD.
Admin
Admin\RoleController—permissionCatalog, role CRUD, role↔user assignment, role lookup by user.Admin\AuditLogController—indexreturns paginated audit rows scoped to the tenant (with optional filters).
Inventory
Inventory\MedicineController— full CRUD +summary,lowStock,expiryAlerts.Inventory\StockMovementController—index,forMedicine,store(handles purchase / dispense / wastage / adjustment in one endpoint).Inventory\SupplierController— full CRUD.Inventory\PurchaseOrderController— full CRUD +cancel,receive(writes one stock movement per received item).
Owner portal
Owner\OwnerPortalController— pets, appointments, invoices, tenants, vaccinations list, vaccinations-due feed, timeline, statement PDF, vaccination certificate PDF, store/update pet.Owner\OwnerPaymentController—createOrder,verify. Razorpay integration; signature verified onverify.Owner\OrderRequestController— owner-side order CRUD, prescription list, refill draft.Owner\PetCaregiverController— primary-owner-side invite mgmt; the unauth'dacceptis in this same controller class but routed outside the auth group.Owner\PetDocumentController— file upload + listing + signed-URL download.Owner\PetPassportController—download(single-shot PDF, no caching).Owner\PetPublicProfileController—update/disable(collar tag) andmarkLost/markFound.
Tenant (seller side)
Tenant\OrderFulfillmentController— pharmacy/retail's incoming order inbox.index,show,accept,reject,fulfill. Assertsbusiness_type IN (pharmacy, retail)so a clinic hitting these routes 404s.
Webhooks
Webhooks\RazorpayWebhookController— invokable. Verifies signature, dispatches perevent(payment.captured,payment.failed,refund.processed). (Audit: re-fetch payment from Razorpay before mutating local state — on roadmap.)
Wiring patterns
- Implicit route binding is used wherever the route includes a model placeholder (
{pet},{appointment},{invoice}, …). Controllers that share routes with and without{tenant}declare it as an optional nullable parameter to avoidControllerDispatcherinjecting it positionally — seePetVaccinationControllerfor the canonical pattern. - Dispatching jobs uses
SendReminderJob::dispatch($id)etc. None of the jobs declare aqueue()method — they call$this->onQueue(...)in the constructor instead, because the Bus dispatcher interprets aqueue()method as a custom push-hook and short-circuits the normal push. - Transactions wrap any read-then-write path that touches a money column or stock count (
Invoice::convert,PurchaseOrder::receive). The audit punch-list extends this discipline to the remaining paths (recordPayment, stock dispense — both currently underprotected).