Order Requests
The commerce surface is intentionally lightweight: the owner pings a pharmacy or retail tenant with a refill / reorder / custom request, the seller accepts and fulfils outside the app, and both sides see status updates inside their respective apps.
It's not a checkout flow — payment for fulfilled orders runs through the existing invoice pipeline.
Data shape
The order_requests table carries the entire order body in one row:
{
"kind": "refill", // refill | reorder | custom
"status": "pending", // pending | accepted | fulfilled | rejected | cancelled
"target_tenant_id": 42, // the seller
"pet_id": 17,
"requester_user_id": 8,
"source_prescription_id": 91, // nullable — set when refilling an existing Rx
"items": [
{"medicine_name": "Amoxicillin 500mg", "quantity": 30, "notes": "as before"}
],
"delivery_preference": "pickup", // pickup | delivery
"delivery_address": null,
"notes": null,
"accepted_at": null,
"fulfilled_at": null,
"rejected_at": null,
"reject_reason": null
}
OrderRequest::resolvedSummary() flattens the items array into a printable single-line description used in emails and the seller's inbox.
Owner flow (vetty)
The vetty Orders tab provides three entry points:
- Refill from prescription. From a pet's prescription detail screen, the user taps Request refill. The app calls
GET /api/v1/owner/me/pets/{pet}/prescriptions/{prescription}/refill-draft, which returns a pre-built payload (target_tenant_id = prescribing tenant, items derived from the Rx lines, kind =refill). The user reviews and submits. - Reorder. Reorder a non-Rx item (pet food, accessory) from a tenant the owner has previously ordered from. Same flow without a
source_prescription_id. - Custom. Free-text order against a marketplace pharmacy / retail tenant.
Submission: POST /api/v1/owner/me/pets/{pet}/orders with the payload above. The controller verifies the requester is the primary owner or an active full-access caregiver, and that the target tenant's business_type is pharmacy or retail.
Status updates are visible in Orders → [order] (GET /api/v1/owner/me/orders/{order}). The owner can cancel while status=pending (POST /api/v1/owner/me/orders/{order}/cancel).
Seller flow (admin web / future seller-app)
The seller's inbox lives at /api/v1/tenant/{tenant}/orders. Permissions order.read and order.manage are seeded onto the pharmacy and retail_counter roles.
| Endpoint | Action |
|---|---|
GET /orders | Inbox — pending + recent. |
GET /orders/{order} | Detail. |
POST /orders/{order}/accept | Acknowledge — sets accepted_at. |
POST /orders/{order}/reject | Decline — captures reject_reason. |
POST /orders/{order}/fulfill | Mark as ready / completed. |
OrderFulfillmentController asserts the resolved tenant's business_type is pharmacy or retail. A clinic tenant hitting these endpoints 404s.
Notification
When an order is created, SendOrderRequestEmailJob is enqueued. The job:
- Loads the order with
targetTenant,pet.client,requester,sourcePrescription.items. - Short-circuits if the order is no longer
pendingor the target tenant has no contact email. - Renders the platform-default
order_request_receivedtemplate (tenants can override with a tenant-scoped row). - Sends through the configured driver. Subject and body reflect the kind, items summary, owner contact, and delivery preference.
The notes field is escaped before insertion into the HTML body via htmlspecialchars (the template variable is named order.notes_block_html with the _html suffix to bypass the renderer's default escaping — see Notifications module).
Audit + roadmap
target_tenant_idshould be constrained to a tenant the requester has prior contact with (or that's marketplace-public). Today any tenant id is accepted. Audit P2.- Item
medicine_nameis rendered raw inresolvedSummary()— escape at storage. Audit P2. - No order-status push to the owner; they have to refresh. Audit P2 (UX).
- Roadmap: two-step accept (seller proposes timing → owner confirms), in-app chat for clarifications, integrated checkout (deposit at order time), inventory deduction on fulfillment.