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 writesaudit_logsrows on create / update / delete. UsesRecordAuditContextmiddleware to inject actor + IP + user-agent. ExposesauditCustomEvent($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
| Model | Notes |
|---|---|
SystemAdmin | Filament panel auth, separate guard. |
User | Both staff and pet owners. tenants() relation via tenant_user; currentTenant() accessor reads current_tenant_id. account_type is staff or owner. |
Tenant | Marketplace + locale + RBAC owner. SUPPORTED_BUSINESS_TYPES, SUPPORTED_CURRENCIES, SUPPORTED_TIMEZONES, and PREMIUM_FEATURES constants are used by both FormRequests and the Filament admin. |
Permission | Read-only registry. App\Auth\PermissionRegistry enumerates the canonical key set; the seeder writes them to the table. |
Role | tenant_id nullable for platform-default roles. Pivot role_permission. is_system blocks edits/deletes from the admin endpoints. |
Auth helpers
| Model | Notes |
|---|---|
PasswordResetToken | static issue(User, ip) mints + emails. verify($plain) compares hashes. consume() marks used_at. |
Sanctum PersonalAccessToken | Default; no override. |
Audit
| Model | Notes |
|---|---|
AuditLog | Polymorphic via auditable_type / auditable_id. Static helpers record($model, $action, $before, $after) used by the Auditable trait. |
Patients & medical
| Model | Highlights |
|---|---|
Client | pets(), invoices(). (appointments() recommended — see audit.) |
Pet | client(), tenant(), vaccinations(), documents(), healthLogs(), caregivers(), shares(), prescriptions(), visits(). Public-profile + lost-mode columns sit on this model. |
Visit | pet, client, veterinarian, prescription, labResults, attachments. SOAP narrative + vitals on the row. |
Prescription | items, visit, pet, client, prescribedBy. STATUS_* constants. |
PrescriptionItem | Belongs to prescription; optional medicine. consumeRefill() decrements refills_remaining. |
LabResult | pet, visit, recordedBy, attachments. flag enum (normal/low/high/critical). |
MedicalAttachment | Polymorphic — attachable(). Hard-coded disk='private'. |
PetVaccination | pet, tenant, recordedBy. SOURCE_OWNER/SOURCE_CLINIC. |
PetDocument | generateSignedUrl(int $minutes) → uses URL::temporarySignedRoute('owner.pet_documents.download', …). (Audit: catches Throwable silently — fix on roadmap.) |
PetHealthLog | pet, tenant, loggedBy. SOURCE_OWNER/SOURCE_CLINIC. |
Caregivers & sharing
| Model | Highlights |
|---|---|
PetShare | static generateCode() produces an 8-char human-friendly token. redeem(User) increments redeemed_count, idempotent on revoked_at. |
PetCaregiver | static generateToken() → 48-char URL-safe. acceptUrl() builds the /caregivers/accept?token=… link. ACCESS_VIEW/ACCESS_FULL. |
Scheduling
| Model | Highlights |
|---|---|
Appointment | STATUS_* constants including the workflow values CHECKED_IN, WITH_VET. Helpers markChecked(), markCompleted() stamp the workflow timestamp. isReschedulable() / isCancellable() consulted by the policy. |
Billing
| Model | Highlights |
|---|---|
Invoice | payments(), client, pet, tenant. recomputeBalance() recomputes balance_due from the payment ledger (recommended for all paths — see audit). STATUS_*. Inherits auditCustomEvent from Auditable. |
Payment | invoice, tenant. STATUS_*. Carries Razorpay's order/payment/signature triple + raw_payload. |
Inventory
| Model | Highlights |
|---|---|
Medicine | stockMovements(), tenant. kind/category/status constants. lowStock() and expiringSoon() query scopes. |
StockMovement | medicine, tenant, pet, performedBy. TYPE_* constants. Append-only — no update/delete in normal paths. |
Supplier | purchaseOrders(). Address + payment-terms metadata. |
PurchaseOrder | items(), supplier, tenant. STATUS_*. markReceived() writes the matching stock_movements rows inside a single transaction. |
PurchaseOrderItem | medicine, purchaseOrder. remainingQuantity() returns quantity - received_quantity (clamped). |
Notifications
| Model | Highlights |
|---|---|
Reminder | STATUS_*. isTerminal(), last_error_code(). Backed by App\Jobs\SendReminderJob. |
ReminderTemplate | Resolved by App\Notifications\TemplateRenderer::renderBySlug(tenantId, slug, vars); tenant-scoped row beats platform default. availableSmsProviders() static helper for the Filament admin. |
CommunicationOptOut | Static helpers isOptedOut(tenant, channel, contact), optOut(...), normalizeContact($value) (lowercase + trim). |
ServiceSetting | Key/value store backing the Filament "Notification settings" page. Secret values are encrypted via the cast. |
Marketplace & commerce
| Model | Highlights |
|---|---|
OrderRequest | targetTenant, 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_tokenare in$hiddenso they don't leak through default JSON serialisation.- Boolean columns and JSON columns (
services,premium_features,payload,items) cast in each model'scasts(). decimal:2is 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.permissionmiddleware 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).Notifiableis not used; outbound mail goes throughApp\Notifications\NotificationDriverManagerand the queue pipeline rather than Laravel's notification system.