Stacks Commerce
Comprehensive e-commerce module with 13 sub-modules and 20+ models.
Key Paths
- Core package:
storage/framework/core/commerce/src/
- Default functions:
storage/framework/defaults/functions/commerce/
- Default models:
storage/framework/defaults/app/Models/commerce/
Commerce Namespace
import { commerce } from '@stacksjs/commerce'
// 13 sub-modules
commerce.products // Product CRUD
commerce.coupons // Coupon management
commerce.customers // Customer management
commerce.errors // Error tracking
commerce.giftCards // Gift card management
commerce.orders // Order management
commerce.payments // Payment processing
commerce.restaurant // Restaurant features
commerce.shippings // Shipping management
commerce.tax // Tax rate management
commerce.waitlists // Waitlist management
commerce.devices // Print device management
commerce.receipts // Receipt management
Sub-Module Operations
Each sub-module typically provides:
index() — list all
fetch(id) — get one
store(data) — create
update(id, data) — update
destroy(id) — delete
Products Sub-Module
- Products: items, variants, units
- Manufacturers
- Reviews
- Categories
- Product waitlists
Orders Sub-Module
- Order CRUD
- Order items
- Order export
Shipping Sub-Module
- Shipping methods
- Shipping rates (weight-based)
- Shipping zones
- Delivery routes and their stops
- Drivers
- Digital deliveries
- License keys
- Live tracking (
commerce.shippings.tracking) - see below
Commerce Models (20+)
| Model |
Key Fields |
Relationships |
| Product |
name, price, inventoryCount, allergens(JSON) |
belongsTo: Category, Manufacturer; hasMany: Review, ProductUnit, ProductVariant |
| ProductVariant |
SKU, options, pricing |
belongsTo: Product |
| ProductUnit |
unit pricing |
belongsTo: Product |
| Category |
name, slug, isActive, displayOrder |
hasMany: Product |
| Cart |
status, total, currency(USD), expiresAt |
hasMany: CartItem; belongsTo: Customer |
| CartItem |
quantity, unitPrice, totalPrice |
belongsTo: Cart |
| Order |
status, totalAmount, orderType, deliveryAddress |
hasMany: OrderItem, Payment; belongsTo: Customer |
| OrderItem |
quantity, price |
belongsTo: Order, Product |
| Coupon |
code(unique), discountType, discountValue, usageLimit |
hasMany: Order |
| GiftCard |
code(unique), initialBalance, currentBalance, isReloadable |
belongsTo: Customer |
| Customer |
name, email, totalSpent, status |
hasMany: Order, GiftCard, Review, Payment |
| Manufacturer |
manufacturer info |
hasMany: Product |
| Review |
rating(1-5), content, isVerifiedPurchase, helpfulVotes |
belongsTo: Product, Customer |
| ShippingRate |
weightFrom, weightTo, rate |
belongsTo: ShippingMethod, ShippingZone |
| DeliveryRoute |
stops, totalDistance, status(planned/active/completed), startedAt |
belongsTo: Driver; hasMany: DeliveryStop, DriverPing |
| DeliveryStop |
sequence, status, address, latitude, longitude, etaAt, arrivedAt |
belongsTo: DeliveryRoute, Order |
| Driver |
name, phone, vehicleNumber, status, latitude, longitude, heading, lastPingAt |
hasMany: DeliveryRoute, DriverPing |
| DriverPing |
latitude, longitude, heading, speed, accuracy, recordedAt |
belongsTo: Driver, DeliveryRoute |
| TaxRate |
name, rate(0-100), type(VAT/GST/Sales Tax) |
|
| LicenseKey |
key(XXXX-XXXX-XXXX-XXXX-XXXX), template, status |
belongsTo: Customer, Product, Order |
| DigitalDelivery |
downloadLimit, expiryDays, automaticDelivery |
|
| WaitlistProduct |
product waitlist tracking |
|
| Receipt |
receipt records |
|
Live Delivery Tracking
commerce.shippings.tracking is the moving part of shipping: position ingest,
the stop lifecycle, and the fan-out that drives a customer's tracking map.
import { commerce } from '@stacksjs/commerce'
const { tracking } = commerce.shippings
// Put an order on a route, then set the vehicle moving.
const stop = await tracking.assignStop({
deliveryRouteId: route.id,
orderId: order.id,
address: '3821 Grand View Blvd, Los Angeles CA 90066',
latitude: 34.0128,
longitude: -118.4361,
})
await tracking.startRoute(route.id)
await tracking.startStop(stop.id) // order -> OUT_FOR_DELIVERY
// One call per position fix from the driver's device.
await tracking.recordDriverPing({
driverId, latitude, longitude, speed, accuracy,
})
await tracking.completeStop(stop.id) // order -> DELIVERED, route closes itself
What recordDriverPing does
One entry point, so a tracking page never shows a position its ETA disagrees
with. Per fix it: appends to driver_pings, updates the driver's denormalised
present position, recomputes the served stop's ETA, broadcasts the position,
and latches delivery:nearby / delivery:arrived so each fires exactly once.
A fix reporting worse than 250m accuracy is stored but does not move the driver
or trip a threshold.
Two fan-outs, on purpose
| Path |
Carries |
Why |
Realtime channel (@stacksjs/realtime) |
delivery:position, plus every state change |
Fires every few seconds per delivery; only browsers care |
Event bus (@stacksjs/events) |
delivery:assigned, :started, :nearby, :arrived, :completed, :failed |
Where notifications, analytics and fulfilment subscribe |
Position never reaches the event bus. Subscribe to the state changes to send an
SMS without being woken several times a minute per active delivery.
Channels are order.{id} for a customer's page and delivery-route.{id} for a
dispatch map, both private: authorise them in your setWsAuthenticator.
Order status
OUT_FOR_DELIVERY sits between SHIPPED and DELIVERED, reachable from
PROCESSING too (a local kitchen goes straight out on its own van), and falls
back to SHIPPED when a drop fails and the parcel returns to the depot.
canTransition enforces it.
Geodesy
distanceInMeters, bearingInDegrees, estimateSecondsRemaining, isWithin
and hasCoordinates are exported for building dispatch views. The ETA pads
straight-line distance by a detour factor and returns null for a stationary
driver rather than Infinity.
Integration with Payments
Commerce works with @stacksjs/payments for Stripe integration:
import { Payment } from '@stacksjs/payments'
await Payment.charge(customer, order.totalAmount, paymentMethodId)
Dashboard Routes
All commerce models have dashboard views at /dashboard/commerce/*.
Gotchas
- Commerce models are auto-generated — edit definitions, not generated files
- Use
buddy make:migration when changing commerce schemas
- Order
observe: true emits events on create/update/delete
- Products have JSON fields for allergens and nutritionalInfo
- Cart expiry is tracked via
expiresAt field
- Coupon types:
fixed_amount or percentage
- Gift card codes are unique and auto-generated
- License keys follow XXXX-XXXX-XXXX-XXXX-XXXX format
- Product dashboard is highlighted (
dashboard: { highlight: true })
- Default seeder counts: Product(10), Order(20), Review(50), Payment(50)
1---2name: stacks-commerce-43description: Use when building e-commerce features in Stacks — the commerce namespace with 13 sub-modules (products, orders, customers, coupons, payments, shipping, tax, gift cards, waitlists, devices, receipts, restaurant), 20+ commerce models, default commerce functions, or the commerce configuration. Covers @stacksjs/commerce.4license: MIT5---67# Stacks Commerce89Comprehensive e-commerce module with 13 sub-modules and 20+ models.1011## Key Paths12- Core package: `storage/framework/core/commerce/src/`13- Default functions: `storage/framework/defaults/functions/commerce/`14- Default models: `storage/framework/defaults/app/Models/commerce/`1516## Commerce Namespace1718```typescript19import { commerce } from '@stacksjs/commerce'2021// 13 sub-modules22commerce.products // Product CRUD23commerce.coupons // Coupon management24commerce.customers // Customer management25commerce.errors // Error tracking26commerce.giftCards // Gift card management27commerce.orders // Order management28commerce.payments // Payment processing29commerce.restaurant // Restaurant features30commerce.shippings // Shipping management31commerce.tax // Tax rate management32commerce.waitlists // Waitlist management33commerce.devices // Print device management34commerce.receipts // Receipt management35```3637## Sub-Module Operations3839Each sub-module typically provides:40- `index()` — list all41- `fetch(id)` — get one42- `store(data)` — create43- `update(id, data)` — update44- `destroy(id)` — delete4546### Products Sub-Module47- Products: items, variants, units48- Manufacturers49- Reviews50- Categories51- Product waitlists5253### Orders Sub-Module54- Order CRUD55- Order items56- Order export5758### Shipping Sub-Module59- Shipping methods60- Shipping rates (weight-based)61- Shipping zones62- Delivery routes and their stops63- Drivers64- Digital deliveries65- License keys66- **Live tracking** (`commerce.shippings.tracking`) - see below6768## Commerce Models (20+)6970| Model | Key Fields | Relationships |71|-------|-----------|---------------|72| Product | name, price, inventoryCount, allergens(JSON) | belongsTo: Category, Manufacturer; hasMany: Review, ProductUnit, ProductVariant |73| ProductVariant | SKU, options, pricing | belongsTo: Product |74| ProductUnit | unit pricing | belongsTo: Product |75| Category | name, slug, isActive, displayOrder | hasMany: Product |76| Cart | status, total, currency(USD), expiresAt | hasMany: CartItem; belongsTo: Customer |77| CartItem | quantity, unitPrice, totalPrice | belongsTo: Cart |78| Order | status, totalAmount, orderType, deliveryAddress | hasMany: OrderItem, Payment; belongsTo: Customer |79| OrderItem | quantity, price | belongsTo: Order, Product |80| Coupon | code(unique), discountType, discountValue, usageLimit | hasMany: Order |81| GiftCard | code(unique), initialBalance, currentBalance, isReloadable | belongsTo: Customer |82| Customer | name, email, totalSpent, status | hasMany: Order, GiftCard, Review, Payment |83| Manufacturer | manufacturer info | hasMany: Product |84| Review | rating(1-5), content, isVerifiedPurchase, helpfulVotes | belongsTo: Product, Customer |85| ShippingRate | weightFrom, weightTo, rate | belongsTo: ShippingMethod, ShippingZone |86| DeliveryRoute | stops, totalDistance, status(planned/active/completed), startedAt | belongsTo: Driver; hasMany: DeliveryStop, DriverPing |87| DeliveryStop | sequence, status, address, latitude, longitude, etaAt, arrivedAt | belongsTo: DeliveryRoute, Order |88| Driver | name, phone, vehicleNumber, status, latitude, longitude, heading, lastPingAt | hasMany: DeliveryRoute, DriverPing |89| DriverPing | latitude, longitude, heading, speed, accuracy, recordedAt | belongsTo: Driver, DeliveryRoute |90| TaxRate | name, rate(0-100), type(VAT/GST/Sales Tax) | |91| LicenseKey | key(XXXX-XXXX-XXXX-XXXX-XXXX), template, status | belongsTo: Customer, Product, Order |92| DigitalDelivery | downloadLimit, expiryDays, automaticDelivery | |93| WaitlistProduct | product waitlist tracking | |94| Receipt | receipt records | |9596## Live Delivery Tracking9798`commerce.shippings.tracking` is the moving part of shipping: position ingest,99the stop lifecycle, and the fan-out that drives a customer's tracking map.100101```ts102import { commerce } from '@stacksjs/commerce'103104const { tracking } = commerce.shippings105106// Put an order on a route, then set the vehicle moving.107const stop = await tracking.assignStop({108 deliveryRouteId: route.id,109 orderId: order.id,110 address: '3821 Grand View Blvd, Los Angeles CA 90066',111 latitude: 34.0128,112 longitude: -118.4361,113})114await tracking.startRoute(route.id)115await tracking.startStop(stop.id) // order -> OUT_FOR_DELIVERY116117// One call per position fix from the driver's device.118await tracking.recordDriverPing({119 driverId, latitude, longitude, speed, accuracy,120})121122await tracking.completeStop(stop.id) // order -> DELIVERED, route closes itself123```124125### What `recordDriverPing` does126127One entry point, so a tracking page never shows a position its ETA disagrees128with. Per fix it: appends to `driver_pings`, updates the driver's denormalised129present position, recomputes the served stop's ETA, broadcasts the position,130and latches `delivery:nearby` / `delivery:arrived` so each fires exactly once.131132A fix reporting worse than 250m accuracy is stored but does not move the driver133or trip a threshold.134135### Two fan-outs, on purpose136137| Path | Carries | Why |138|---|---|---|139| Realtime channel (`@stacksjs/realtime`) | `delivery:position`, plus every state change | Fires every few seconds per delivery; only browsers care |140| Event bus (`@stacksjs/events`) | `delivery:assigned`, `:started`, `:nearby`, `:arrived`, `:completed`, `:failed` | Where notifications, analytics and fulfilment subscribe |141142Position never reaches the event bus. Subscribe to the state changes to send an143SMS without being woken several times a minute per active delivery.144145Channels are `order.{id}` for a customer's page and `delivery-route.{id}` for a146dispatch map, both private: authorise them in your `setWsAuthenticator`.147148### Order status149150`OUT_FOR_DELIVERY` sits between `SHIPPED` and `DELIVERED`, reachable from151`PROCESSING` too (a local kitchen goes straight out on its own van), and falls152back to `SHIPPED` when a drop fails and the parcel returns to the depot.153`canTransition` enforces it.154155### Geodesy156157`distanceInMeters`, `bearingInDegrees`, `estimateSecondsRemaining`, `isWithin`158and `hasCoordinates` are exported for building dispatch views. The ETA pads159straight-line distance by a detour factor and returns `null` for a stationary160driver rather than `Infinity`.161162## Integration with Payments163Commerce works with `@stacksjs/payments` for Stripe integration:164```typescript165import { Payment } from '@stacksjs/payments'166await Payment.charge(customer, order.totalAmount, paymentMethodId)167```168169## Dashboard Routes170All commerce models have dashboard views at `/dashboard/commerce/*`.171172## Gotchas173- Commerce models are auto-generated — edit definitions, not generated files174- Use `buddy make:migration` when changing commerce schemas175- Order `observe: true` emits events on create/update/delete176- Products have JSON fields for allergens and nutritionalInfo177- Cart expiry is tracked via `expiresAt` field178- Coupon types: `fixed_amount` or `percentage`179- Gift card codes are unique and auto-generated180- License keys follow XXXX-XXXX-XXXX-XXXX-XXXX format181- Product dashboard is highlighted (`dashboard: { highlight: true }`)182- Default seeder counts: Product(10), Order(20), Review(50), Payment(50)