@stacksjs/defaults 0.72.103 → 0.73.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/ai/AGENTS.md +25 -4
  2. package/ai/README.md +26 -4
  3. package/ai/skills/stacks-actions/SKILL.md +1 -1
  4. package/ai/skills/stacks-ai/SKILL.md +1 -1
  5. package/ai/skills/stacks-alias/SKILL.md +1 -1
  6. package/ai/skills/stacks-analytics/SKILL.md +1 -1
  7. package/ai/skills/stacks-api/SKILL.md +1 -1
  8. package/ai/skills/stacks-arrays/SKILL.md +1 -1
  9. package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
  10. package/ai/skills/stacks-browse/SKILL.md +1 -1
  11. package/ai/skills/stacks-browser/SKILL.md +1 -1
  12. package/ai/skills/stacks-buddy/SKILL.md +1 -1
  13. package/ai/skills/stacks-build/SKILL.md +79 -5
  14. package/ai/skills/stacks-cache/SKILL.md +1 -1
  15. package/ai/skills/stacks-calendar/SKILL.md +1 -1
  16. package/ai/skills/stacks-chat/SKILL.md +1 -1
  17. package/ai/skills/stacks-cli/SKILL.md +1 -1
  18. package/ai/skills/stacks-cloud/SKILL.md +1 -1
  19. package/ai/skills/stacks-cms/SKILL.md +1 -1
  20. package/ai/skills/stacks-codebase-design/DEEPENING.md +79 -0
  21. package/ai/skills/stacks-codebase-design/DESIGN-IT-TWICE.md +72 -0
  22. package/ai/skills/stacks-codebase-design/SKILL.md +180 -0
  23. package/ai/skills/stacks-collections/SKILL.md +1 -1
  24. package/ai/skills/stacks-commerce/SKILL.md +141 -35
  25. package/ai/skills/stacks-composables/SKILL.md +1 -1
  26. package/ai/skills/stacks-config/SKILL.md +1 -1
  27. package/ai/skills/stacks-configuration/SKILL.md +1 -1
  28. package/ai/skills/stacks-cron/SKILL.md +1 -1
  29. package/ai/skills/stacks-crosswind/SKILL.md +1 -1
  30. package/ai/skills/stacks-database/SKILL.md +1 -1
  31. package/ai/skills/stacks-datetime/SKILL.md +1 -1
  32. package/ai/skills/stacks-dependencies/SKILL.md +1 -1
  33. package/ai/skills/stacks-deploy/SKILL.md +1 -1
  34. package/ai/skills/stacks-desktop/SKILL.md +1 -1
  35. package/ai/skills/stacks-development/SKILL.md +1 -1
  36. package/ai/skills/stacks-dns/SKILL.md +1 -1
  37. package/ai/skills/stacks-docs/SKILL.md +1 -1
  38. package/ai/skills/stacks-domain-modeling/FORMATS.md +123 -0
  39. package/ai/skills/stacks-domain-modeling/SKILL.md +109 -0
  40. package/ai/skills/stacks-enums/SKILL.md +1 -1
  41. package/ai/skills/stacks-error-handling/SKILL.md +1 -1
  42. package/ai/skills/stacks-events/SKILL.md +1 -1
  43. package/ai/skills/stacks-faker/SKILL.md +1 -1
  44. package/ai/skills/stacks-flow/PHASE-BOUNDARIES.md +91 -0
  45. package/ai/skills/stacks-flow/SKILL.md +117 -0
  46. package/ai/skills/stacks-git/SKILL.md +37 -9
  47. package/ai/skills/stacks-grilling/SKILL.md +85 -0
  48. package/ai/skills/stacks-guard/SKILL.md +86 -11
  49. package/ai/skills/stacks-guard/scripts/block-destructive.sh +67 -0
  50. package/ai/skills/stacks-handoff/SKILL.md +70 -0
  51. package/ai/skills/stacks-health/SKILL.md +1 -1
  52. package/ai/skills/stacks-http/SKILL.md +1 -1
  53. package/ai/skills/stacks-i18n/SKILL.md +1 -1
  54. package/ai/skills/stacks-investigate/SKILL.md +234 -106
  55. package/ai/skills/stacks-investigate/scripts/hitl-loop.template.sh +47 -0
  56. package/ai/skills/stacks-jobs/SKILL.md +1 -1
  57. package/ai/skills/stacks-listeners/SKILL.md +1 -1
  58. package/ai/skills/stacks-logging/SKILL.md +1 -1
  59. package/ai/skills/stacks-mail/SKILL.md +1 -1
  60. package/ai/skills/stacks-middleware/SKILL.md +1 -1
  61. package/ai/skills/stacks-migrations/SKILL.md +1 -1
  62. package/ai/skills/stacks-models/SKILL.md +1 -1
  63. package/ai/skills/stacks-new-feature/SKILL.md +65 -5
  64. package/ai/skills/stacks-notifications/SKILL.md +1 -1
  65. package/ai/skills/stacks-objects/SKILL.md +1 -1
  66. package/ai/skills/stacks-office-hours/SKILL.md +16 -2
  67. package/ai/skills/stacks-orm/SKILL.md +1 -1
  68. package/ai/skills/stacks-path/SKILL.md +1 -1
  69. package/ai/skills/stacks-payments/SKILL.md +35 -1
  70. package/ai/skills/stacks-plan-review/SKILL.md +27 -6
  71. package/ai/skills/stacks-plugins/SKILL.md +1 -1
  72. package/ai/skills/stacks-prototype/LOGIC.md +103 -0
  73. package/ai/skills/stacks-prototype/SKILL.md +66 -0
  74. package/ai/skills/stacks-prototype/UI.md +112 -0
  75. package/ai/skills/stacks-push/SKILL.md +1 -1
  76. package/ai/skills/stacks-query-builder/SKILL.md +1 -1
  77. package/ai/skills/stacks-queue/SKILL.md +1 -1
  78. package/ai/skills/stacks-realtime/SKILL.md +1 -1
  79. package/ai/skills/stacks-registry/SKILL.md +1 -1
  80. package/ai/skills/stacks-repl/SKILL.md +1 -1
  81. package/ai/skills/stacks-retro/SKILL.md +121 -75
  82. package/ai/skills/stacks-review/SKILL.md +182 -74
  83. package/ai/skills/stacks-router/SKILL.md +1 -1
  84. package/ai/skills/stacks-routes/SKILL.md +1 -1
  85. package/ai/skills/stacks-scaffolding/SKILL.md +1 -1
  86. package/ai/skills/stacks-scheduler/SKILL.md +1 -1
  87. package/ai/skills/stacks-search-engine/SKILL.md +1 -1
  88. package/ai/skills/stacks-security/SKILL.md +1 -1
  89. package/ai/skills/stacks-security-audit/SKILL.md +1 -1
  90. package/ai/skills/stacks-server/SKILL.md +1 -1
  91. package/ai/skills/stacks-shell/SKILL.md +1 -1
  92. package/ai/skills/stacks-slug/SKILL.md +1 -1
  93. package/ai/skills/stacks-sms/SKILL.md +1 -1
  94. package/ai/skills/stacks-socials/SKILL.md +1 -1
  95. package/ai/skills/stacks-storage/SKILL.md +1 -1
  96. package/ai/skills/stacks-strings/SKILL.md +1 -1
  97. package/ai/skills/stacks-stx/SKILL.md +1 -1
  98. package/ai/skills/stacks-tdd/EXAMPLES.md +136 -0
  99. package/ai/skills/stacks-tdd/SKILL.md +125 -0
  100. package/ai/skills/stacks-testing/SKILL.md +13 -3
  101. package/ai/skills/stacks-tunnel/SKILL.md +1 -1
  102. package/ai/skills/stacks-types/SKILL.md +1 -1
  103. package/ai/skills/stacks-ui/SKILL.md +1 -1
  104. package/ai/skills/stacks-utils/SKILL.md +1 -1
  105. package/ai/skills/stacks-validation/SKILL.md +1 -1
  106. package/ai/skills/stacks-whois/SKILL.md +1 -1
  107. package/ai/skills/stacks-wizard/SKILL.md +127 -0
  108. package/ai/skills/stacks-wizard/scripts/template.sh +208 -0
  109. package/ai/skills/stacks-writing-for-agents/MECHANICS.md +125 -0
  110. package/ai/skills/stacks-writing-for-agents/SKILL.md +218 -0
  111. package/app/Actions/Auth/GenerateTwoFactorSecretAction.ts +12 -2
  112. package/app/Actions/Commerce/Shipping/{DriverDestroyAction.ts → CourierDestroyAction.ts} +6 -6
  113. package/app/Actions/Commerce/Shipping/{DriverIndexAction.ts → CourierIndexAction.ts} +3 -3
  114. package/app/Actions/Commerce/Shipping/CourierPingStoreAction.ts +61 -0
  115. package/app/Actions/Commerce/Shipping/{DriverShowAction.ts → CourierShowAction.ts} +5 -5
  116. package/app/Actions/Commerce/Shipping/{DriverStoreAction.ts → CourierStoreAction.ts} +4 -4
  117. package/app/Actions/Commerce/Shipping/{DriverUpdateAction.ts → CourierUpdateAction.ts} +6 -6
  118. package/app/Actions/Commerce/Shipping/DeliveryRouteStartAction.ts +38 -0
  119. package/app/Actions/Commerce/Shipping/DeliveryStopCompleteAction.ts +40 -0
  120. package/app/Actions/Commerce/Shipping/DeliveryStopFailAction.ts +44 -0
  121. package/app/Actions/Commerce/Shipping/DeliveryStopStartAction.ts +39 -0
  122. package/app/Actions/Commerce/Shipping/courier-session.ts +75 -0
  123. package/app/Actions/Commerce/commerce-action.test.ts +5 -5
  124. package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +5 -5
  125. package/app/Actions/Dashboard/Commerce/CourierIndexAction.ts +24 -0
  126. package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +7 -7
  127. package/app/Actions/Dashboard/Commerce/commerce-delivery.test.ts +11 -11
  128. package/app/Actions/Dashboard/Commerce/commerce-delivery.ts +29 -29
  129. package/app/Actions/Dashboard/Commerce/{driver-records.test.ts → courier-records.test.ts} +9 -9
  130. package/app/Actions/Dashboard/Commerce/{driver-records.ts → courier-records.ts} +14 -14
  131. package/app/Actions/Dashboard/Commerce/delivery-route-records.test.ts +13 -13
  132. package/app/Actions/Dashboard/Commerce/delivery-route-records.ts +25 -25
  133. package/app/Models/User.ts +1 -1
  134. package/app/Models/commerce/{Driver.ts → Courier.ts} +7 -7
  135. package/app/Models/commerce/{DriverPing.ts → CourierPing.ts} +7 -7
  136. package/app/Models/commerce/DeliveryRoute.ts +6 -6
  137. package/app/Models/commerce/DeliveryStop.ts +31 -8
  138. package/bootstrap.ts +7 -0
  139. package/functions/commerce/shippings/couriers.ts +19 -0
  140. package/ide/vscode/package.json +1 -1
  141. package/package.json +4 -3
  142. package/resources/components/Dashboard/Commerce/Delivery/{DriverDeleteDialog.stx → CourierDeleteDialog.stx} +5 -5
  143. package/resources/components/Dashboard/Commerce/Delivery/{DriverDialog.stx → CourierDialog.stx} +10 -10
  144. package/resources/components/Dashboard/Commerce/Delivery/{DriversDashboard.stx → CouriersDashboard.stx} +43 -43
  145. package/resources/components/Dashboard/Commerce/Delivery/{DriversTable.stx → CouriersTable.stx} +21 -21
  146. package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +18 -18
  147. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +2 -2
  148. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +22 -22
  149. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +24 -24
  150. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +8 -8
  151. package/resources/components/Dashboard/Commerce/Delivery/TabNavigation.stx +1 -1
  152. package/resources/functions/dashboard/data.ts +1 -1
  153. package/resources/functions/dashboard/sidebar.ts +2 -2
  154. package/routes/dashboard-api.ts +6 -6
  155. package/routes/dashboard.ts +7 -7
  156. package/routes/delivery.ts +24 -0
  157. package/types/defaults.ts +3 -3
  158. package/views/dashboard/.discovered-models.json +18 -18
  159. package/views/dashboard/AUDIT.md +1 -1
  160. package/views/dashboard/commerce/delivery/{drivers.stx → couriers.stx} +2 -2
  161. package/views/dashboard/composables/useChart.ts +16 -2
  162. package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +0 -24
  163. package/functions/commerce/shippings/drivers.ts +0 -19
@@ -6,7 +6,7 @@ export interface DeliveryOverviewStat {
6
6
 
7
7
  export interface DeliveryOverviewRoute {
8
8
  id: number
9
- driver: string
9
+ courier: string
10
10
  vehicle: string
11
11
  stops: number
12
12
  duration: string
@@ -35,7 +35,7 @@ export interface DeliveryOverviewResult {
35
35
  routes: DeliveryOverviewRoute[]
36
36
  methods: DeliveryOverviewMethod[]
37
37
  zones: DeliveryOverviewZone[]
38
- drivers: {
38
+ couriers: {
39
39
  total: number
40
40
  active: number
41
41
  onDelivery: number
@@ -87,30 +87,30 @@ export function buildDeliveryOverview(
87
87
  methodRows: any[],
88
88
  routeRows: any[],
89
89
  zoneRows: any[],
90
- driverRows: any[],
90
+ courierRows: any[],
91
91
  currency = 'USD',
92
92
  now = new Date(),
93
93
  ): DeliveryOverviewResult {
94
94
  const normalizedCurrency = commerceCurrency(currency, 'Commerce configuration')
95
- const normalizedDrivers = driverRows.map((driver) => {
96
- const identifier = commerceIdentifier(commerceValue(driver, 'id', 'uuid'), 'Driver')
97
- const source = `Driver ${identifier}`
95
+ const normalizedCouriers = courierRows.map((courier) => {
96
+ const identifier = commerceIdentifier(commerceValue(courier, 'id', 'uuid'), 'Courier')
97
+ const source = `Courier ${identifier}`
98
98
  return {
99
99
  id: identifier,
100
- name: commerceRequiredString(commerceValue(driver, 'name'), source, 'name'),
100
+ name: commerceRequiredString(commerceValue(courier, 'name'), source, 'name'),
101
101
  vehicle: commerceRequiredString(
102
- commerceValue(driver, 'vehicle_number', 'vehicleNumber'),
102
+ commerceValue(courier, 'vehicle_number', 'vehicleNumber'),
103
103
  source,
104
104
  'vehicle_number',
105
105
  ),
106
- status: commerceEnum(commerceValue(driver, 'status'), source, 'status', [
106
+ status: commerceEnum(commerceValue(courier, 'status'), source, 'status', [
107
107
  'active',
108
108
  'on_delivery',
109
109
  'on_break',
110
110
  ]),
111
111
  }
112
112
  })
113
- const driversById = new Map(normalizedDrivers.map(driver => [driver.id, driver]))
113
+ const couriersById = new Map(normalizedCouriers.map(courier => [courier.id, courier]))
114
114
 
115
115
  const routeMinutesById = new Map<number, number>()
116
116
  const allRoutes = routeRows.map((route): DeliveryOverviewRoute => {
@@ -119,14 +119,14 @@ export function buildDeliveryOverview(
119
119
  integer: true,
120
120
  })
121
121
  const source = `DeliveryRoute ${numericId}`
122
- const driverId = commerceOptionalIdentifier(
123
- commerceValue(route, 'driver_id', 'driverId'),
122
+ const courierId = commerceOptionalIdentifier(
123
+ commerceValue(route, 'courier_id', 'courierId'),
124
124
  source,
125
- 'driver_id',
125
+ 'courier_id',
126
126
  )
127
- const linkedDriver = driverId ? driversById.get(driverId) : undefined
128
- if (driverId && !linkedDriver)
129
- throw new TypeError(`${source}.driver_id references missing Driver ${driverId}.`)
127
+ const linkedCourier = courierId ? couriersById.get(courierId) : undefined
128
+ if (courierId && !linkedCourier)
129
+ throw new TypeError(`${source}.courier_id references missing Courier ${courierId}.`)
130
130
  const deliveryTime = commerceNumber(
131
131
  commerceValue(route, 'delivery_time', 'deliveryTime'),
132
132
  source,
@@ -136,9 +136,9 @@ export function buildDeliveryOverview(
136
136
  routeMinutesById.set(numericId, deliveryTime)
137
137
  return {
138
138
  id: numericId,
139
- driver: linkedDriver?.name
140
- || commerceRequiredString(commerceValue(route, 'driver'), source, 'driver'),
141
- vehicle: linkedDriver?.vehicle
139
+ courier: linkedCourier?.name
140
+ || commerceRequiredString(commerceValue(route, 'courier'), source, 'courier'),
141
+ vehicle: linkedCourier?.vehicle
142
142
  || commerceRequiredString(commerceValue(route, 'vehicle'), source, 'vehicle'),
143
143
  stops: commerceNumber(commerceValue(route, 'stops'), source, 'stops', {
144
144
  min: 0,
@@ -224,8 +224,8 @@ export function buildDeliveryOverview(
224
224
  return statusOrder || left.name.localeCompare(right.name)
225
225
  })
226
226
 
227
- const driverStatusCounts = normalizedDrivers.reduce<Record<string, number>>((counts, driver) => {
228
- counts[driver.status] = (counts[driver.status] || 0) + 1
227
+ const courierStatusCounts = normalizedCouriers.reduce<Record<string, number>>((counts, courier) => {
228
+ counts[courier.status] = (counts[courier.status] || 0) + 1
229
229
  return counts
230
230
  }, {})
231
231
 
@@ -242,9 +242,9 @@ export function buildDeliveryOverview(
242
242
  detail: `${activeRoutes.reduce((total, route) => total + route.stops, 0).toLocaleString('en-US')} stops active in 24h`,
243
243
  },
244
244
  {
245
- label: 'Drivers on Delivery',
246
- value: (driverStatusCounts.on_delivery || 0).toLocaleString('en-US'),
247
- detail: `${driverRows.length.toLocaleString('en-US')} drivers total`,
245
+ label: 'Couriers on Delivery',
246
+ value: (courierStatusCounts.on_delivery || 0).toLocaleString('en-US'),
247
+ detail: `${courierRows.length.toLocaleString('en-US')} couriers total`,
248
248
  },
249
249
  {
250
250
  label: 'Average Route',
@@ -260,11 +260,11 @@ export function buildDeliveryOverview(
260
260
  routes: activeRoutes.slice(0, 5),
261
261
  methods: methods.slice(0, 5),
262
262
  zones: zones.slice(0, 5),
263
- drivers: {
264
- total: normalizedDrivers.length,
265
- active: driverStatusCounts.active || 0,
266
- onDelivery: driverStatusCounts.on_delivery || 0,
267
- onBreak: driverStatusCounts.on_break || 0,
263
+ couriers: {
264
+ total: normalizedCouriers.length,
265
+ active: courierStatusCounts.active || 0,
266
+ onDelivery: courierStatusCounts.on_delivery || 0,
267
+ onBreak: courierStatusCounts.on_break || 0,
268
268
  },
269
269
  }
270
270
  }
@@ -1,9 +1,9 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { indexDriverUsers, normalizeDriverRecord } from './driver-records'
2
+ import { indexCourierUsers, normalizeCourierRecord } from './courier-records'
3
3
 
4
- describe('driver records', () => {
4
+ describe('courier records', () => {
5
5
  const user = { id: 2, name: 'Ada Lovelace', email: 'ada@example.com' }
6
- const users = indexDriverUsers([user])
6
+ const users = indexCourierUsers([user])
7
7
  const base = {
8
8
  id: 1,
9
9
  name: 'Alex Morgan',
@@ -17,8 +17,8 @@ describe('driver records', () => {
17
17
  uuid: null,
18
18
  }
19
19
 
20
- test('normalizes drivers and linked users', () => {
21
- expect(normalizeDriverRecord(base, users)).toEqual({
20
+ test('normalizes couriers and linked users', () => {
21
+ expect(normalizeCourierRecord(base, users)).toEqual({
22
22
  id: 1,
23
23
  name: 'Alex Morgan',
24
24
  phone: '+1 555 0100',
@@ -34,9 +34,9 @@ describe('driver records', () => {
34
34
  })
35
35
 
36
36
  test('rejects missing users and unknown statuses', () => {
37
- expect(() => normalizeDriverRecord(base, new Map()))
38
- .toThrow('Driver 1.user_id references missing User 2')
39
- expect(() => normalizeDriverRecord({ ...base, user_id: null, status: 'offline' }, users))
40
- .toThrow('Driver 1.status must be active or on_delivery or on_break')
37
+ expect(() => normalizeCourierRecord(base, new Map()))
38
+ .toThrow('Courier 1.user_id references missing User 2')
39
+ expect(() => normalizeCourierRecord({ ...base, user_id: null, status: 'offline' }, users))
40
+ .toThrow('Courier 1.status must be active or on_delivery or on_break')
41
41
  })
42
42
  })
@@ -10,24 +10,24 @@ import {
10
10
  commerceValue,
11
11
  } from './commerce-record'
12
12
 
13
- export const driverStatuses = ['active', 'on_delivery', 'on_break'] as const
14
- export type DriverStatus = typeof driverStatuses[number]
13
+ export const courierStatuses = ['active', 'on_delivery', 'on_break'] as const
14
+ export type CourierStatus = typeof courierStatuses[number]
15
15
 
16
- export interface DriverUser {
16
+ export interface CourierUser {
17
17
  id: number
18
18
  name: string
19
19
  email: string
20
20
  }
21
21
 
22
- export interface DriverRecord {
22
+ export interface CourierRecord {
23
23
  id: number
24
24
  name: string
25
25
  phone: string
26
26
  vehicle_number: string
27
27
  license: string
28
- status: DriverStatus
28
+ status: CourierStatus
29
29
  user_id: number | null
30
- user: DriverUser | null
30
+ user: CourierUser | null
31
31
  created_at: string
32
32
  updated_at: string
33
33
  uuid: string
@@ -40,8 +40,8 @@ function numericIdentifier(input: unknown, source: string, field = 'id'): number
40
40
  return id
41
41
  }
42
42
 
43
- export function indexDriverUsers(records: any[]): Map<number, DriverUser> {
44
- const result = new Map<number, DriverUser>()
43
+ export function indexCourierUsers(records: any[]): Map<number, CourierUser> {
44
+ const result = new Map<number, CourierUser>()
45
45
  for (const record of records) {
46
46
  const id = numericIdentifier(commerceValue(record, 'id'), 'User')
47
47
  const source = `User ${id}`
@@ -56,12 +56,12 @@ export function indexDriverUsers(records: any[]): Map<number, DriverUser> {
56
56
  return result
57
57
  }
58
58
 
59
- export function normalizeDriverRecord(
59
+ export function normalizeCourierRecord(
60
60
  record: any,
61
- users: ReadonlyMap<number, DriverUser>,
62
- ): DriverRecord {
63
- const id = numericIdentifier(commerceValue(record, 'id'), 'Driver')
64
- const source = `Driver ${id}`
61
+ users: ReadonlyMap<number, CourierUser>,
62
+ ): CourierRecord {
63
+ const id = numericIdentifier(commerceValue(record, 'id'), 'Courier')
64
+ const source = `Courier ${id}`
65
65
  const userIdentifier = commerceOptionalIdentifier(
66
66
  commerceValue(record, 'user_id', 'userId'),
67
67
  source,
@@ -82,7 +82,7 @@ export function normalizeDriverRecord(
82
82
  'vehicle_number',
83
83
  ),
84
84
  license: commerceRequiredString(commerceValue(record, 'license'), source, 'license'),
85
- status: commerceEnum(commerceValue(record, 'status'), source, 'status', driverStatuses),
85
+ status: commerceEnum(commerceValue(record, 'status'), source, 'status', courierStatuses),
86
86
  user_id: userId,
87
87
  user: user || null,
88
88
  created_at: commerceTimestamp(commerceValue(record, 'created_at', 'createdAt'), source),
@@ -1,21 +1,21 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import {
3
- indexDeliveryRouteDrivers,
3
+ indexDeliveryRouteCouriers,
4
4
  normalizeDeliveryRouteRecord,
5
5
  } from './delivery-route-records'
6
6
 
7
7
  describe('delivery route records', () => {
8
- const driver = {
8
+ const courier = {
9
9
  id: 2,
10
10
  name: 'Alex Morgan',
11
11
  vehicle_number: 'VAN-204',
12
12
  status: 'active',
13
13
  }
14
- const drivers = indexDeliveryRouteDrivers([driver])
14
+ const couriers = indexDeliveryRouteCouriers([courier])
15
15
  const base = {
16
16
  id: 1,
17
- driver: 'Alex Morgan',
18
- driver_id: 2,
17
+ courier: 'Alex Morgan',
18
+ courier_id: 2,
19
19
  vehicle: 'VAN-204',
20
20
  stops: 8,
21
21
  delivery_time: 90,
@@ -26,12 +26,12 @@ describe('delivery route records', () => {
26
26
  uuid: null,
27
27
  }
28
28
 
29
- test('normalizes route metrics and the linked driver', () => {
30
- expect(normalizeDeliveryRouteRecord(base, drivers)).toEqual({
29
+ test('normalizes route metrics and the linked courier', () => {
30
+ expect(normalizeDeliveryRouteRecord(base, couriers)).toEqual({
31
31
  id: 1,
32
- driver: 'Alex Morgan',
33
- driver_id: 2,
34
- driver_record: driver,
32
+ courier: 'Alex Morgan',
33
+ courier_id: 2,
34
+ courier_record: courier,
35
35
  vehicle: 'VAN-204',
36
36
  stops: 8,
37
37
  delivery_time: 90,
@@ -43,10 +43,10 @@ describe('delivery route records', () => {
43
43
  })
44
44
  })
45
45
 
46
- test('rejects missing drivers and fractional operational counts', () => {
46
+ test('rejects missing couriers and fractional operational counts', () => {
47
47
  expect(() => normalizeDeliveryRouteRecord(base, new Map()))
48
- .toThrow('DeliveryRoute 1.driver_id references missing Driver 2')
49
- expect(() => normalizeDeliveryRouteRecord({ ...base, driver_id: null, stops: 1.5 }, drivers))
48
+ .toThrow('DeliveryRoute 1.courier_id references missing Courier 2')
49
+ expect(() => normalizeDeliveryRouteRecord({ ...base, courier_id: null, stops: 1.5 }, couriers))
50
50
  .toThrow('DeliveryRoute 1.stops must be an integer')
51
51
  })
52
52
  })
@@ -8,21 +8,21 @@ import {
8
8
  commerceTimestamp,
9
9
  commerceValue,
10
10
  } from './commerce-record'
11
- import type { DriverStatus } from './driver-records'
12
- import { driverStatuses } from './driver-records'
11
+ import type { CourierStatus } from './courier-records'
12
+ import { courierStatuses } from './courier-records'
13
13
 
14
- export interface DeliveryRouteDriver {
14
+ export interface DeliveryRouteCourier {
15
15
  id: number
16
16
  name: string
17
17
  vehicle_number: string
18
- status: DriverStatus
18
+ status: CourierStatus
19
19
  }
20
20
 
21
21
  export interface DeliveryRouteRecord {
22
22
  id: number
23
- driver: string
24
- driver_id: number | null
25
- driver_record: DeliveryRouteDriver | null
23
+ courier: string
24
+ courier_id: number | null
25
+ courier_record: DeliveryRouteCourier | null
26
26
  vehicle: string
27
27
  stops: number
28
28
  delivery_time: number
@@ -40,13 +40,13 @@ function numericIdentifier(input: unknown, source: string, field = 'id'): number
40
40
  return id
41
41
  }
42
42
 
43
- export function indexDeliveryRouteDrivers(records: any[]): Map<number, DeliveryRouteDriver> {
44
- const result = new Map<number, DeliveryRouteDriver>()
43
+ export function indexDeliveryRouteCouriers(records: any[]): Map<number, DeliveryRouteCourier> {
44
+ const result = new Map<number, DeliveryRouteCourier>()
45
45
  for (const record of records) {
46
- const id = numericIdentifier(commerceValue(record, 'id'), 'Driver')
47
- const source = `Driver ${id}`
46
+ const id = numericIdentifier(commerceValue(record, 'id'), 'Courier')
47
+ const source = `Courier ${id}`
48
48
  if (result.has(id))
49
- throw new TypeError(`Duplicate Driver ${id}.`)
49
+ throw new TypeError(`Duplicate Courier ${id}.`)
50
50
  result.set(id, {
51
51
  id,
52
52
  name: commerceRequiredString(commerceValue(record, 'name'), source, 'name'),
@@ -55,7 +55,7 @@ export function indexDeliveryRouteDrivers(records: any[]): Map<number, DeliveryR
55
55
  source,
56
56
  'vehicle_number',
57
57
  ),
58
- status: commerceEnum(commerceValue(record, 'status'), source, 'status', driverStatuses),
58
+ status: commerceEnum(commerceValue(record, 'status'), source, 'status', courierStatuses),
59
59
  })
60
60
  }
61
61
  return result
@@ -63,27 +63,27 @@ export function indexDeliveryRouteDrivers(records: any[]): Map<number, DeliveryR
63
63
 
64
64
  export function normalizeDeliveryRouteRecord(
65
65
  record: any,
66
- drivers: ReadonlyMap<number, DeliveryRouteDriver>,
66
+ couriers: ReadonlyMap<number, DeliveryRouteCourier>,
67
67
  ): DeliveryRouteRecord {
68
68
  const id = numericIdentifier(commerceValue(record, 'id'), 'DeliveryRoute')
69
69
  const source = `DeliveryRoute ${id}`
70
- const driverIdentifier = commerceOptionalIdentifier(
71
- commerceValue(record, 'driver_id', 'driverId'),
70
+ const courierIdentifier = commerceOptionalIdentifier(
71
+ commerceValue(record, 'courier_id', 'courierId'),
72
72
  source,
73
- 'driver_id',
73
+ 'courier_id',
74
74
  )
75
- const driverId = driverIdentifier
76
- ? numericIdentifier(driverIdentifier, source, 'driver_id')
75
+ const courierId = courierIdentifier
76
+ ? numericIdentifier(courierIdentifier, source, 'courier_id')
77
77
  : null
78
- const driverRecord = driverId ? drivers.get(driverId) : undefined
79
- if (driverId && !driverRecord)
80
- throw new TypeError(`${source}.driver_id references missing Driver ${driverId}.`)
78
+ const courierRecord = courierId ? couriers.get(courierId) : undefined
79
+ if (courierId && !courierRecord)
80
+ throw new TypeError(`${source}.courier_id references missing Courier ${courierId}.`)
81
81
 
82
82
  return {
83
83
  id,
84
- driver: commerceRequiredString(commerceValue(record, 'driver'), source, 'driver'),
85
- driver_id: driverId,
86
- driver_record: driverRecord || null,
84
+ courier: commerceRequiredString(commerceValue(record, 'courier'), source, 'courier'),
85
+ courier_id: courierId,
86
+ courier_record: courierRecord || null,
87
87
  vehicle: commerceRequiredString(commerceValue(record, 'vehicle'), source, 'vehicle'),
88
88
  stops: commerceNumber(commerceValue(record, 'stops'), source, 'stops', {
89
89
  integer: true,
@@ -58,7 +58,7 @@ export default defineModel({
58
58
  },
59
59
  },
60
60
 
61
- hasOne: ['Subscriber', 'Driver', 'Author'],
61
+ hasOne: ['Subscriber', 'Courier', 'Author'],
62
62
 
63
63
  hasMany: [
64
64
  'PersonalAccessToken',
@@ -2,13 +2,13 @@ import { defineModel } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  export default defineModel({
5
- name: 'Driver',
6
- table: 'drivers',
5
+ name: 'Courier',
6
+ table: 'couriers',
7
7
  primaryKey: 'id',
8
8
  autoIncrement: true,
9
9
 
10
10
  belongsTo: ['User'],
11
- hasMany: ['DeliveryRoute', 'DriverPing'],
11
+ hasMany: ['DeliveryRoute', 'CourierPing'],
12
12
 
13
13
  traits: {
14
14
  useUuid: true,
@@ -25,7 +25,7 @@ export default defineModel({
25
25
  },
26
26
 
27
27
  useApi: {
28
- uri: 'drivers',
28
+ uri: 'couriers',
29
29
  middleware: ['auth'],
30
30
  },
31
31
 
@@ -94,8 +94,8 @@ export default defineModel({
94
94
  /*
95
95
  * Last known position.
96
96
  *
97
- * Denormalised from the `driver_pings` series on purpose: "where is this
98
- * driver right now" is asked on every map frame and by every tracking
97
+ * Denormalised from the `courier_pings` series on purpose: "where is this
98
+ * courier right now" is asked on every map frame and by every tracking
99
99
  * page, and answering it with a MAX(recorded_at) subquery over a table
100
100
  * that grows by a row every few seconds is the wrong shape. The series is
101
101
  * the history; these three columns are the present.
@@ -149,7 +149,7 @@ export default defineModel({
149
149
 
150
150
  /*
151
151
  * When the last fix landed. A tracking map needs this to say "updated 4
152
- * seconds ago" and, more importantly, to stop claiming a driver is at a
152
+ * seconds ago" and, more importantly, to stop claiming a courier is at a
153
153
  * position that is ten minutes stale.
154
154
  */
155
155
  lastPingAt: {
@@ -2,9 +2,9 @@ import { defineModel } from '@stacksjs/orm'
2
2
  import { schema } from '@stacksjs/validation'
3
3
 
4
4
  /**
5
- * One position fix from a driver's device.
5
+ * One position fix from a courier's device.
6
6
  *
7
- * The append-only history behind `drivers.latitude/longitude`. A tracking map
7
+ * The append-only history behind `couriers.latitude/longitude`. A tracking map
8
8
  * needs the present position, but everything else about a delivery needs the
9
9
  * series: drawing the path already travelled, replaying a disputed drop,
10
10
  * measuring how long a stop actually took, and deriving an ETA from recent
@@ -12,12 +12,12 @@ import { schema } from '@stacksjs/validation'
12
12
  *
13
13
  * Deliberately not `useSearch` (indexing a row every few seconds is pointless
14
14
  * and expensive) and not `useApi` (writes come through
15
- * `recordDriverPing`, which does the fan-out, and reads are scoped to a
15
+ * `recordCourierPing`, which does the fan-out, and reads are scoped to a
16
16
  * route). `usePrunable` keeps the table from growing without bound.
17
17
  */
18
18
  export default defineModel({
19
- name: 'DriverPing',
20
- table: 'driver_pings',
19
+ name: 'CourierPing',
20
+ table: 'courier_pings',
21
21
  primaryKey: 'id',
22
22
  autoIncrement: true,
23
23
 
@@ -27,7 +27,7 @@ export default defineModel({
27
27
  useSeeder: { count: 0 },
28
28
  },
29
29
 
30
- belongsTo: ['Driver', 'DeliveryRoute'],
30
+ belongsTo: ['Courier', 'DeliveryRoute'],
31
31
 
32
32
  attributes: {
33
33
  latitude: {
@@ -89,7 +89,7 @@ export default defineModel({
89
89
 
90
90
  /**
91
91
  * When the device took the fix, which is not when the server received it.
92
- * A driver through a tunnel sends five fixes at once on the far side, and
92
+ * A courier through a tunnel sends five fixes at once on the far side, and
93
93
  * ordering them by arrival draws a path that never happened.
94
94
  */
95
95
  recordedAt: {
@@ -11,10 +11,10 @@ export default defineModel({
11
11
  useUuid: true,
12
12
  useTimestamps: true,
13
13
  useSearch: {
14
- displayable: ['id', 'driver', 'vehicle', 'stops', 'deliveryTime', 'totalDistance', 'lastActive'],
15
- searchable: ['driver', 'vehicle'],
14
+ displayable: ['id', 'courier', 'vehicle', 'stops', 'deliveryTime', 'totalDistance', 'lastActive'],
15
+ searchable: ['courier', 'vehicle'],
16
16
  sortable: ['stops', 'deliveryTime', 'totalDistance', 'lastActive', 'createdAt', 'updatedAt'],
17
- filterable: ['driver', 'vehicle'],
17
+ filterable: ['courier', 'vehicle'],
18
18
  },
19
19
 
20
20
  useSeeder: {
@@ -29,11 +29,11 @@ export default defineModel({
29
29
  observe: true,
30
30
  },
31
31
 
32
- belongsTo: ['Driver'],
33
- hasMany: ['DeliveryStop', 'DriverPing'],
32
+ belongsTo: ['Courier'],
33
+ hasMany: ['DeliveryStop', 'CourierPing'],
34
34
 
35
35
  attributes: {
36
- driver: {
36
+ courier: {
37
37
  order: 1,
38
38
  fillable: true,
39
39
  validation: {
@@ -6,11 +6,11 @@ import { schema } from '@stacksjs/validation'
6
6
  *
7
7
  * `DeliveryRoute` records that a route happened and how far it went; this is
8
8
  * the route itself. Without it `stops` is an integer, which is enough to bill
9
- * a driver and not nearly enough to tell a customer where their order is: the
9
+ * a courier and not nearly enough to tell a customer where their order is: the
10
10
  * link from an order to the vehicle carrying it runs through here.
11
11
  *
12
12
  * Sequence is the planned order. `status` is what actually happened, which
13
- * diverges the moment a driver skips a building and comes back to it.
13
+ * diverges the moment a courier skips a building and comes back to it.
14
14
  */
15
15
  export default defineModel({
16
16
  name: 'DeliveryStop',
@@ -26,7 +26,7 @@ export default defineModel({
26
26
  displayable: ['id', 'sequence', 'status', 'address', 'etaAt'],
27
27
  searchable: ['address', 'recipientName'],
28
28
  sortable: ['sequence', 'etaAt', 'createdAt'],
29
- filterable: ['status', 'deliveryRouteId'],
29
+ filterable: ['status', 'type', 'deliveryRouteId'],
30
30
  },
31
31
 
32
32
  useSeeder: { count: 0 },
@@ -82,7 +82,7 @@ export default defineModel({
82
82
 
83
83
  /**
84
84
  * Geocoded destination. Nullable because an address can arrive before it
85
- * resolves, and a stop with no coordinates is still a stop the driver can
85
+ * resolves, and a stop with no coordinates is still a stop the courier can
86
86
  * complete; it just cannot be drawn.
87
87
  */
88
88
  latitude: {
@@ -113,7 +113,7 @@ export default defineModel({
113
113
  factory: faker => faker.phone.number(),
114
114
  },
115
115
 
116
- /** Current estimate, rewritten as the driver moves. */
116
+ /** Current estimate, rewritten as the courier moves. */
117
117
  etaAt: {
118
118
  order: 8,
119
119
  fillable: true,
@@ -122,7 +122,7 @@ export default defineModel({
122
122
  },
123
123
 
124
124
  /**
125
- * When the customer was told the driver was close.
125
+ * When the customer was told the courier was close.
126
126
  *
127
127
  * The latch that makes "nearly there" fire once. Without it every ping
128
128
  * inside the radius sends another text, which is roughly one text every
@@ -135,7 +135,7 @@ export default defineModel({
135
135
  factory: () => null,
136
136
  },
137
137
 
138
- /** When the driver crossed the arrival radius. */
138
+ /** When the courier crossed the arrival radius. */
139
139
  arrivedAt: {
140
140
  order: 10,
141
141
  fillable: true,
@@ -151,13 +151,36 @@ export default defineModel({
151
151
  factory: () => null,
152
152
  },
153
153
 
154
- /** Why a stop failed, or anything the driver needs to record. */
154
+ /** Why a stop failed, or anything the courier needs to record. */
155
155
  notes: {
156
156
  order: 12,
157
157
  fillable: true,
158
158
  validation: { rule: schema.string().max(1000) },
159
159
  factory: () => '',
160
160
  },
161
+
162
+ /**
163
+ * Which leg of the run this stop is.
164
+ *
165
+ * A delivery is two stops, not one: collect from the merchant, then hand
166
+ * over to the customer. Without the distinction both legs look like a
167
+ * dropoff, so arriving at the restaurant would tell the customer their
168
+ * order is out for delivery, and collecting it would mark the order
169
+ * delivered before it had left the kitchen.
170
+ *
171
+ * Defaults to `dropoff` so existing single-stop routes keep their meaning.
172
+ */
173
+ type: {
174
+ order: 13,
175
+ required: true,
176
+ fillable: true,
177
+ default: 'dropoff',
178
+ validation: {
179
+ rule: schema.enum(['pickup', 'dropoff']),
180
+ message: { enum: 'Type must be one of: pickup, dropoff' },
181
+ },
182
+ factory: () => 'dropoff',
183
+ },
161
184
  },
162
185
 
163
186
  dashboard: {
package/bootstrap.ts CHANGED
@@ -175,6 +175,13 @@ if (mounts('forms', feature('forms'))) {
175
175
  await route.register(frameworkPath('defaults/routes/forms.ts'))
176
176
  }
177
177
 
178
+ // Courier delivery endpoints: position ingest and the stop/route lifecycle a
179
+ // courier's device drives. Gated with the rest of the commerce surface, since
180
+ // they are meaningless without the delivery models behind them.
181
+ if (mounts('delivery', feature('commerce'))) {
182
+ await route.register(frameworkPath('defaults/routes/delivery.ts'))
183
+ }
184
+
178
185
  // Social sign-in: `/auth/{provider}` + `/auth/{provider}/callback`
179
186
  // (stacksjs/stacks#2276). An opt-in bundle, NOT part of the implicit default
180
187
  // set or `all` — OAuth callback URLs in an app that configured no provider
@@ -0,0 +1,19 @@
1
+ import type { Couriers, NewCourier } from '../../../types/defaults'
2
+ import { createDashboardResource } from './dashboard-resource'
3
+
4
+ const resource = createDashboardResource<Couriers, NewCourier>({
5
+ path: '/api/dashboard/commerce/couriers',
6
+ storageKey: 'couriers',
7
+ singular: 'courier',
8
+ plural: 'couriers',
9
+ })
10
+
11
+ export function useCouriers() {
12
+ return {
13
+ couriers: resource.items,
14
+ fetchCouriers: resource.fetchAll,
15
+ createCourier: resource.create,
16
+ updateCourier: resource.update,
17
+ deleteCourier: resource.destroy,
18
+ }
19
+ }