Skip to main content

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 restrict Rule::exists with where('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. ProviderListingResource vs full TenantResource).
  • Permissions are checked twice — require.permission:<key> middleware on the route + a second enforcePermission in the controller for row-level decisions.
  • Audit writes happen automatically through the Auditable trait; controllers only need to call auditCustomEvent for non-Eloquent events (e.g. receipt.emailed, appointment.checked_in).

Auth & system

  • AuthControllerlogin, logout, me, registerOwner. Returns the user + permission flags + active tenant.
  • PasswordResetControllerrequestReset, verify, reset. Rate-limited at the route layer.
  • SystemControllerhealth returns {ok: true} for uptime monitors.
  • DevMailTestController — invokable. Local-only (controller short-circuits unless APP_ENV=local). Sends a hand-tuned message through the configured driver to validate Mailgun/SES creds end-to-end.

Public surfaces

  • ProviderDirectoryControllerindex paginates tenants WHERE is_public=true, supports business_type, q (full-text), and near[lat,lng,radius_km] (PHP-side haversine + bounding-box pre-filter — see audit). show returns one tenant.
  • PublicPetControllershowJson for /api/v1/p/{slug}. (Audit: rate-limit + opt-in + no fallback to client.phone recommended.)
  • PetShareControllerredeem (auth'd, called by vetdoctor), public show for the pre-install landing page, plus owner CRUD inside the auth group.

Tenant-scoped clinic surfaces

  • Clinic\AppointmentControllerindex, show, store, update, updateStatus (legacy), cancel, queue (today's kanban with status grouping).
  • Clinic\ClientControllerindex for now (more endpoints on the audit punch-list).
  • Clinic\DashboardControllershow returns the homepage roll-up (counts + due today + low stock).
  • Clinic\InvoiceControllerindex, show, store, recordPayment, refundPayment, convert (estimate → invoice with lockForUpdate), downloadReceipt, emailReceipt.
  • Clinic\PrescriptionController — full CRUD; delete audited as cancel.
  • Clinic\VisitController — full CRUD + dischargeSummary (PDF).
  • Clinic\LabResultController — full CRUD.
  • Clinic\MedicalAttachmentControllerindex / store / show / download / destroy. Polymorphic over visits, prescriptions, lab results.
  • Clinic\PatientOnboardingControllerstore (single-call create-client + create-pet), update (combined edit).
  • Clinic\PetControllerindex, show (clinic patient briefing), timeline.
  • Clinic\ReminderControllerhealth, full reminder CRUD + retry / dismiss, opt-out CRUD.

Admin

  • Admin\RoleControllerpermissionCatalog, role CRUD, role↔user assignment, role lookup by user.
  • Admin\AuditLogControllerindex returns paginated audit rows scoped to the tenant (with optional filters).

Inventory

  • Inventory\MedicineController — full CRUD + summary, lowStock, expiryAlerts.
  • Inventory\StockMovementControllerindex, 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\OwnerPaymentControllercreateOrder, verify. Razorpay integration; signature verified on verify.
  • Owner\OrderRequestController — owner-side order CRUD, prescription list, refill draft.
  • Owner\PetCaregiverController — primary-owner-side invite mgmt; the unauth'd accept is in this same controller class but routed outside the auth group.
  • Owner\PetDocumentController — file upload + listing + signed-URL download.
  • Owner\PetPassportControllerdownload (single-shot PDF, no caching).
  • Owner\PetPublicProfileControllerupdate / disable (collar tag) and markLost / markFound.

Tenant (seller side)

  • Tenant\OrderFulfillmentController — pharmacy/retail's incoming order inbox. index, show, accept, reject, fulfill. Asserts business_type IN (pharmacy, retail) so a clinic hitting these routes 404s.

Webhooks

  • Webhooks\RazorpayWebhookController — invokable. Verifies signature, dispatches per event (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 avoid ControllerDispatcher injecting it positionally — see PetVaccinationController for the canonical pattern.
  • Dispatching jobs uses SendReminderJob::dispatch($id) etc. None of the jobs declare a queue() method — they call $this->onQueue(...) in the constructor instead, because the Bus dispatcher interprets a queue() 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).