Skip to main content

Eloquent Models

Models live in backend/app/Models. Every model that's tenant-scoped includes a tenant_id column and (where relevant) policies in backend/app/Policies enforce ownership. Cross-cutting concerns sit in backend/app/Concerns/:

  • Auditable — automatically writes audit_logs rows on create / update / delete. Uses RecordAuditContext middleware to inject actor + IP + user-agent. Exposes auditCustomEvent($action, $before, $after) for non-Eloquent events (e.g. receipt.emailed).
  • Soft-delete is not used; deletes are hard. The audit log is the long-lived record.

The model catalog has grown to 30+ classes, grouped by the same clusters as the schema doc.

Identity & RBAC

ModelNotes
SystemAdminFilament panel auth, separate guard.
UserBoth staff and pet owners. tenants() relation via tenant_user; currentTenant() accessor reads current_tenant_id. account_type is staff or owner.
TenantMarketplace + locale + RBAC owner. SUPPORTED_BUSINESS_TYPES, SUPPORTED_CURRENCIES, SUPPORTED_TIMEZONES, and PREMIUM_FEATURES constants are used by both FormRequests and the Filament admin.
PermissionRead-only registry. App\Auth\PermissionRegistry enumerates the canonical key set; the seeder writes them to the table.
Roletenant_id nullable for platform-default roles. Pivot role_permission. is_system blocks edits/deletes from the admin endpoints.

Auth helpers

ModelNotes
PasswordResetTokenstatic issue(User, ip) mints + emails. verify($plain) compares hashes. consume() marks used_at.
Sanctum PersonalAccessTokenDefault; no override.

Audit

ModelNotes
AuditLogPolymorphic via auditable_type / auditable_id. Static helpers record($model, $action, $before, $after) used by the Auditable trait.

Patients & medical

ModelHighlights
Clientpets(), invoices(). (appointments() recommended — see audit.)
Petclient(), tenant(), vaccinations(), documents(), healthLogs(), caregivers(), shares(), prescriptions(), visits(). Public-profile + lost-mode columns sit on this model.
Visitpet, client, veterinarian, prescription, labResults, attachments. SOAP narrative + vitals on the row.
Prescriptionitems, visit, pet, client, prescribedBy. STATUS_* constants.
PrescriptionItemBelongs to prescription; optional medicine. consumeRefill() decrements refills_remaining.
LabResultpet, visit, recordedBy, attachments. flag enum (normal/low/high/critical).
MedicalAttachmentPolymorphic — attachable(). Hard-coded disk='private'.
PetVaccinationpet, tenant, recordedBy. SOURCE_OWNER/SOURCE_CLINIC.
PetDocumentgenerateSignedUrl(int $minutes) → uses URL::temporarySignedRoute('owner.pet_documents.download', …). (Audit: catches Throwable silently — fix on roadmap.)
PetHealthLogpet, tenant, loggedBy. SOURCE_OWNER/SOURCE_CLINIC.

Caregivers & sharing

ModelHighlights
PetSharestatic generateCode() produces an 8-char human-friendly token. redeem(User) increments redeemed_count, idempotent on revoked_at.
PetCaregiverstatic generateToken() → 48-char URL-safe. acceptUrl() builds the /caregivers/accept?token=… link. ACCESS_VIEW/ACCESS_FULL.

Scheduling

ModelHighlights
AppointmentSTATUS_* constants including the workflow values CHECKED_IN, WITH_VET. Helpers markChecked(), markCompleted() stamp the workflow timestamp. isReschedulable() / isCancellable() consulted by the policy.

Billing

ModelHighlights
Invoicepayments(), client, pet, tenant. recomputeBalance() recomputes balance_due from the payment ledger (recommended for all paths — see audit). STATUS_*. Inherits auditCustomEvent from Auditable.
Paymentinvoice, tenant. STATUS_*. Carries Razorpay's order/payment/signature triple + raw_payload.

Inventory

ModelHighlights
MedicinestockMovements(), tenant. kind/category/status constants. lowStock() and expiringSoon() query scopes.
StockMovementmedicine, tenant, pet, performedBy. TYPE_* constants. Append-only — no update/delete in normal paths.
SupplierpurchaseOrders(). Address + payment-terms metadata.
PurchaseOrderitems(), supplier, tenant. STATUS_*. markReceived() writes the matching stock_movements rows inside a single transaction.
PurchaseOrderItemmedicine, purchaseOrder. remainingQuantity() returns quantity - received_quantity (clamped).

Notifications

ModelHighlights
ReminderSTATUS_*. isTerminal(), last_error_code(). Backed by App\Jobs\SendReminderJob.
ReminderTemplateResolved by App\Notifications\TemplateRenderer::renderBySlug(tenantId, slug, vars); tenant-scoped row beats platform default. availableSmsProviders() static helper for the Filament admin.
CommunicationOptOutStatic helpers isOptedOut(tenant, channel, contact), optOut(...), normalizeContact($value) (lowercase + trim).
ServiceSettingKey/value store backing the Filament "Notification settings" page. Secret values are encrypted via the cast.

Marketplace & commerce

ModelHighlights
OrderRequesttargetTenant, pet, requester, sourcePrescription. KIND_* (refill/reorder/custom) and STATUS_* (pending/accepted/fulfilled/rejected/cancelled). resolvedSummary() flattens items[] into a printable line. DELIVERY_DELIVERY/DELIVERY_PICKUP.

Casting & hidden fields

A few patterns recur across models:

  • password, remember_token, gateway_signature, gateway_payment_id, raw_payload, invite_token are in $hidden so they don't leak through default JSON serialisation.
  • Boolean columns and JSON columns (services, premium_features, payload, items) cast in each model's casts().
  • decimal:2 is used on every money column. (Audit note: this truncates rather than rounds; a custom rounding cast is on the roadmap.)

Policies & gates

Every clinical and billing model has a matching policy in backend/app/Policies/. Policies usually delegate to Gate::allows('permission.key', $modelOrTenant) so the wire-up reads:

  • require.permission middleware on the route → fast 403 even before the controller runs.
  • Policy class on the model → same key, gives the action-on-model nuance (e.g. only the prescribing vet can update the Rx).

See App\Auth\PermissionRegistry for the canonical permission catalog and the bootstrapping role bundles per business_type.

Concerns and traits used widely

  • HasFactory — every model.
  • Auditable — every model that logs domain-meaningful changes (everything except infra tables).
  • Notifiable is not used; outbound mail goes through App\Notifications\NotificationDriverManager and the queue pipeline rather than Laravel's notification system.