@stacksjs/defaults 0.72.103 → 0.73.0

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
@@ -0,0 +1,61 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { shippings } from '@stacksjs/commerce'
4
+ import { response } from '@stacksjs/router'
5
+ import { courierFromSession } from './courier-session'
6
+
7
+ /**
8
+ * A position fix from a courier's device.
9
+ *
10
+ * The one write path into `courier_pings`: `recordCourierPing` also moves the
11
+ * courier's denormalised position, recomputes the stop's ETA, broadcasts to
12
+ * everyone watching the order, and latches the nearby and arrival thresholds so
13
+ * each fires once. Writing the row directly skips all of it.
14
+ */
15
+ export default new Action({
16
+ name: 'Courier Ping Store',
17
+ description: 'Record a position fix from the authenticated courier\'s device.',
18
+ method: 'POST',
19
+
20
+ async handle(request: RequestInstance) {
21
+ const session = await courierFromSession(request)
22
+ if (session.error)
23
+ return session.error
24
+
25
+ const latitude = numeric(request.get('latitude'))
26
+ const longitude = numeric(request.get('longitude'))
27
+
28
+ if (latitude == null || latitude < -90 || latitude > 90)
29
+ return invalid('latitude must be a number between -90 and 90')
30
+
31
+ if (longitude == null || longitude < -180 || longitude > 180)
32
+ return invalid('longitude must be a number between -180 and 180')
33
+
34
+ const result = await shippings.tracking.recordCourierPing({
35
+ courierId: session.courierId,
36
+ latitude,
37
+ longitude,
38
+ heading: numeric(request.get('heading')),
39
+ speed: numeric(request.get('speed')),
40
+ accuracy: numeric(request.get('accuracy')),
41
+ // The device's own clock. A phone that buffered fixes through a tunnel
42
+ // sends them late, and stamping them with arrival time would draw the
43
+ // courier teleporting.
44
+ recordedAt: typeof request.get('recordedAt') === 'string' ? String(request.get('recordedAt')) : undefined,
45
+ })
46
+
47
+ return response.json(result)
48
+ },
49
+ })
50
+
51
+ function numeric(value: unknown): number | null {
52
+ if (value === null || value === undefined || value === '')
53
+ return null
54
+
55
+ const n = Number(value)
56
+ return Number.isFinite(n) ? n : null
57
+ }
58
+
59
+ function invalid(message: string): Response {
60
+ return response.json({ message }, 422)
61
+ }
@@ -6,18 +6,18 @@ import { response } from '@stacksjs/router'
6
6
  import { commerceIdentifier, commerceNotFound } from '../commerce-action'
7
7
 
8
8
  export default new Action({
9
- name: 'Driver Show',
10
- description: 'Driver Show ORM Action',
9
+ name: 'Courier Show',
10
+ description: 'Courier Show ORM Action',
11
11
  method: 'GET',
12
12
  async handle(request: RequestInstance) {
13
- const identifier = commerceIdentifier(request, 'Driver')
13
+ const identifier = commerceIdentifier(request, 'Courier')
14
14
  if (identifier.error)
15
15
  return identifier.error
16
16
  const { id } = identifier
17
17
 
18
- const model = await shippings.drivers.fetchById(id)
18
+ const model = await shippings.couriers.fetchById(id)
19
19
  if (!model)
20
- return commerceNotFound('Driver', id)
20
+ return commerceNotFound('Courier', id)
21
21
 
22
22
  return response.json(model)
23
23
  },
@@ -5,14 +5,14 @@ import { shippings } from '@stacksjs/commerce'
5
5
  import { response } from '@stacksjs/router'
6
6
 
7
7
  export default new Action({
8
- name: 'Driver Store',
9
- description: 'Driver Store ORM Action',
8
+ name: 'Courier Store',
9
+ description: 'Courier Store ORM Action',
10
10
  method: 'POST',
11
- model: Driver,
11
+ model: Courier,
12
12
  async handle(request: RequestInstance) {
13
13
  await request.validate()
14
14
  const data = await request.all()
15
- const model = await shippings.drivers.store(data)
15
+ const model = await shippings.couriers.store(data)
16
16
 
17
17
  return response.json(model)
18
18
  },
@@ -6,12 +6,12 @@ import { response } from '@stacksjs/router'
6
6
  import { commerceIdentifier, commerceNotFound } from '../commerce-action'
7
7
 
8
8
  export default new Action({
9
- name: 'Driver Update',
10
- description: 'Driver Update ORM Action',
9
+ name: 'Courier Update',
10
+ description: 'Courier Update ORM Action',
11
11
  method: 'PATCH',
12
- model: Driver,
12
+ model: Courier,
13
13
  async handle(request: RequestInstance) {
14
- const identifier = commerceIdentifier(request, 'Driver')
14
+ const identifier = commerceIdentifier(request, 'Courier')
15
15
  if (identifier.error)
16
16
  return identifier.error
17
17
  const { id } = identifier
@@ -19,9 +19,9 @@ export default new Action({
19
19
  await request.validate()
20
20
  const data = await request.all()
21
21
 
22
- const model = await shippings.drivers.update(id, data)
22
+ const model = await shippings.couriers.update(id, data)
23
23
  if (!model)
24
- return commerceNotFound('Driver', id)
24
+ return commerceNotFound('Courier', id)
25
25
 
26
26
  return response.json(model)
27
27
  },
@@ -0,0 +1,38 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { shippings } from '@stacksjs/commerce'
4
+ import { response } from '@stacksjs/router'
5
+ import { commerceIdentifier } from '../commerce-action'
6
+ import { assertRouteBelongsToCourier, courierFromSession } from './courier-session'
7
+
8
+ /**
9
+ * Begin a run.
10
+ *
11
+ * Until a route is active, pings from its courier find no active route, so
12
+ * positions are stored but no ETA is recomputed and no arrival ever fires. A
13
+ * courier app that forgets this call looks like a courier who never moves.
14
+ */
15
+ export default new Action({
16
+ name: 'Delivery Route Start',
17
+ description: 'Start a delivery route for the authenticated courier.',
18
+ method: 'POST',
19
+
20
+ async handle(request: RequestInstance) {
21
+ const session = await courierFromSession(request)
22
+ if (session.error)
23
+ return session.error
24
+
25
+ const identifier = commerceIdentifier(request, 'DeliveryRoute')
26
+ if (identifier.error)
27
+ return identifier.error
28
+ const { id } = identifier
29
+
30
+ const denied = await assertRouteBelongsToCourier(id, session.courierId)
31
+ if (denied)
32
+ return denied
33
+
34
+ await shippings.tracking.startRoute(id)
35
+
36
+ return response.json({ id, status: 'active' })
37
+ },
38
+ })
@@ -0,0 +1,40 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { shippings } from '@stacksjs/commerce'
4
+ import { response } from '@stacksjs/router'
5
+ import { commerceIdentifier, commerceNotFound } from '../commerce-action'
6
+ import { assertStopBelongsToCourier, courierFromSession } from './courier-session'
7
+
8
+ /**
9
+ * The handover is done.
10
+ *
11
+ * Completing a dropoff marks the order delivered; completing a pickup means the
12
+ * order is on the vehicle and moves it out for delivery instead.
13
+ */
14
+ export default new Action({
15
+ name: 'Delivery Stop Complete',
16
+ description: 'Complete a stop for the authenticated courier.',
17
+ method: 'POST',
18
+
19
+ async handle(request: RequestInstance) {
20
+ const session = await courierFromSession(request)
21
+ if (session.error)
22
+ return session.error
23
+
24
+ const identifier = commerceIdentifier(request, 'DeliveryStop')
25
+ if (identifier.error)
26
+ return identifier.error
27
+ const { id } = identifier
28
+
29
+ const denied = await assertStopBelongsToCourier(id, session.courierId)
30
+ if (denied)
31
+ return denied
32
+
33
+ const notes = request.get<string>('notes')
34
+ const stop = await shippings.tracking.completeStop(id, typeof notes === 'string' && notes ? notes : undefined)
35
+ if (!stop)
36
+ return commerceNotFound('DeliveryStop', id)
37
+
38
+ return response.json(stop)
39
+ },
40
+ })
@@ -0,0 +1,44 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { shippings } from '@stacksjs/commerce'
4
+ import { response } from '@stacksjs/router'
5
+ import { commerceIdentifier, commerceNotFound } from '../commerce-action'
6
+ import { assertStopBelongsToCourier, courierFromSession } from './courier-session'
7
+
8
+ /**
9
+ * The stop could not be completed.
10
+ *
11
+ * A reason is required rather than optional: this is the record a customer
12
+ * service agent reads back when the customer asks why nobody came, and "failed"
13
+ * on its own does not answer that.
14
+ */
15
+ export default new Action({
16
+ name: 'Delivery Stop Fail',
17
+ description: 'Mark a stop as failed for the authenticated courier.',
18
+ method: 'POST',
19
+
20
+ async handle(request: RequestInstance) {
21
+ const session = await courierFromSession(request)
22
+ if (session.error)
23
+ return session.error
24
+
25
+ const identifier = commerceIdentifier(request, 'DeliveryStop')
26
+ if (identifier.error)
27
+ return identifier.error
28
+ const { id } = identifier
29
+
30
+ const reason = request.get<string>('reason')
31
+ if (typeof reason !== 'string' || reason.trim() === '')
32
+ return response.json({ message: 'reason is required' }, 422)
33
+
34
+ const denied = await assertStopBelongsToCourier(id, session.courierId)
35
+ if (denied)
36
+ return denied
37
+
38
+ const stop = await shippings.tracking.failStop(id, reason.trim())
39
+ if (!stop)
40
+ return commerceNotFound('DeliveryStop', id)
41
+
42
+ return response.json(stop)
43
+ },
44
+ })
@@ -0,0 +1,39 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { Action } from '@stacksjs/actions'
3
+ import { shippings } from '@stacksjs/commerce'
4
+ import { response } from '@stacksjs/router'
5
+ import { commerceIdentifier, commerceNotFound } from '../commerce-action'
6
+ import { assertStopBelongsToCourier, courierFromSession } from './courier-session'
7
+
8
+ /**
9
+ * The courier is driving to this stop.
10
+ *
11
+ * On a dropoff this is what puts the order out for delivery and starts the
12
+ * customer's tracking page; on a pickup it only moves the stop.
13
+ */
14
+ export default new Action({
15
+ name: 'Delivery Stop Start',
16
+ description: 'Mark a stop as en route for the authenticated courier.',
17
+ method: 'POST',
18
+
19
+ async handle(request: RequestInstance) {
20
+ const session = await courierFromSession(request)
21
+ if (session.error)
22
+ return session.error
23
+
24
+ const identifier = commerceIdentifier(request, 'DeliveryStop')
25
+ if (identifier.error)
26
+ return identifier.error
27
+ const { id } = identifier
28
+
29
+ const denied = await assertStopBelongsToCourier(id, session.courierId)
30
+ if (denied)
31
+ return denied
32
+
33
+ const stop = await shippings.tracking.startStop(id)
34
+ if (!stop)
35
+ return commerceNotFound('DeliveryStop', id)
36
+
37
+ return response.json(stop)
38
+ },
39
+ })
@@ -0,0 +1,75 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
2
+ import { db } from '@stacksjs/database'
3
+ import { response } from '@stacksjs/router'
4
+
5
+ export type CourierSessionResult =
6
+ | { courierId: number, error?: never }
7
+ | { courierId?: never, error: Response }
8
+
9
+ /**
10
+ * The courier making this request.
11
+ *
12
+ * Resolved from the session, never from the body. A courier id accepted as
13
+ * input would let any authenticated account post positions as somebody else -
14
+ * moving another courier's marker on a customer's live map, and tripping their
15
+ * arrival notifications.
16
+ */
17
+ export async function courierFromSession(request: RequestInstance): Promise<CourierSessionResult> {
18
+ const user = await request.user()
19
+ if (!user)
20
+ return { error: response.unauthorized('Authentication required') }
21
+
22
+ const courier = await db
23
+ .selectFrom('couriers')
24
+ .where('user_id', '=', user.id)
25
+ .select(['id'])
26
+ .executeTakeFirst() as { id: number } | undefined
27
+
28
+ if (!courier)
29
+ return { error: response.forbidden('This account is not a courier') }
30
+
31
+ return { courierId: Number(courier.id) }
32
+ }
33
+
34
+ /**
35
+ * Check that a stop belongs to the run this courier is on.
36
+ *
37
+ * Stops are addressed by id, so without this any courier could complete or
38
+ * fail another courier's delivery - which marks somebody else's order as
39
+ * handed over.
40
+ */
41
+ export async function assertStopBelongsToCourier(stopId: number, courierId: number): Promise<Response | null> {
42
+ const row = await db
43
+ .selectFrom('delivery_stops')
44
+ .innerJoin('delivery_routes', 'delivery_routes.id', 'delivery_stops.delivery_route_id')
45
+ .where('delivery_stops.id', '=', stopId)
46
+ .select(['delivery_routes.courier_id as courier_id'])
47
+ .executeTakeFirst() as { courier_id: number | null } | undefined
48
+
49
+ if (!row)
50
+ return response.json({ message: `DeliveryStop ${stopId} was not found.` }, 404)
51
+
52
+ if (row.courier_id == null || Number(row.courier_id) !== courierId)
53
+ return response.forbidden('This stop belongs to another courier')
54
+
55
+ return null
56
+ }
57
+
58
+ /**
59
+ * Check that a route is this courier's own.
60
+ */
61
+ export async function assertRouteBelongsToCourier(routeId: number, courierId: number): Promise<Response | null> {
62
+ const row = await db
63
+ .selectFrom('delivery_routes')
64
+ .where('id', '=', routeId)
65
+ .select(['courier_id'])
66
+ .executeTakeFirst() as { courier_id: number | null } | undefined
67
+
68
+ if (!row)
69
+ return response.json({ message: `DeliveryRoute ${routeId} was not found.` }, 404)
70
+
71
+ if (row.courier_id == null || Number(row.courier_id) !== courierId)
72
+ return response.forbidden('This route belongs to another courier')
73
+
74
+ return null
75
+ }
@@ -10,22 +10,22 @@ function request(id: string): RequestInstance {
10
10
 
11
11
  describe('commerce action responses', () => {
12
12
  test('accepts safe positive route identifiers', () => {
13
- expect(commerceIdentifier(request('42'), 'Driver')).toEqual({ id: 42 })
13
+ expect(commerceIdentifier(request('42'), 'Courier')).toEqual({ id: 42 })
14
14
  })
15
15
 
16
16
  test('returns 422 for invalid identifiers', async () => {
17
- const result = commerceIdentifier(request('not-an-id'), 'Driver')
17
+ const result = commerceIdentifier(request('not-an-id'), 'Courier')
18
18
  expect(result.error?.status).toBe(422)
19
19
  expect(await result.error?.json()).toEqual({
20
- message: 'Driver id must be a positive integer.',
20
+ message: 'Courier id must be a positive integer.',
21
21
  })
22
22
  })
23
23
 
24
24
  test('returns a consistent not-found response', async () => {
25
- const result = commerceNotFound('Driver', 42)
25
+ const result = commerceNotFound('Courier', 42)
26
26
  expect(result.status).toBe(404)
27
27
  expect(await result.json()).toEqual({
28
- message: 'Driver 42 was not found.',
28
+ message: 'Courier 42 was not found.',
29
29
  })
30
30
  })
31
31
  })
@@ -1,29 +1,29 @@
1
1
  import { Action } from '@stacksjs/actions'
2
2
  import { config } from '@stacksjs/config'
3
- import { DeliveryRoute, Driver, ShippingMethod, ShippingZone } from '@stacksjs/orm'
3
+ import { DeliveryRoute, Courier, ShippingMethod, ShippingZone } from '@stacksjs/orm'
4
4
  import { dashboardOperationalError } from '../dashboard-response'
5
5
  import { buildDeliveryOverview } from './commerce-delivery'
6
6
 
7
7
  export default new Action({
8
8
  name: 'CommerceDelivery',
9
- description: 'Returns the dashboard delivery overview from routes, drivers, shipping methods, and zones.',
9
+ description: 'Returns the dashboard delivery overview from routes, couriers, shipping methods, and zones.',
10
10
  method: 'GET',
11
11
  apiResponse: true,
12
12
 
13
13
  async handle() {
14
14
  try {
15
- const [allMethods, allRoutes, allZones, allDrivers] = await Promise.all([
15
+ const [allMethods, allRoutes, allZones, allCouriers] = await Promise.all([
16
16
  ShippingMethod.orderBy('id', 'asc').limit(500).get(),
17
17
  DeliveryRoute.orderBy('id', 'asc').limit(500).get(),
18
18
  ShippingZone.orderBy('id', 'asc').limit(500).get(),
19
- Driver.orderBy('id', 'asc').limit(500).get(),
19
+ Courier.orderBy('id', 'asc').limit(500).get(),
20
20
  ])
21
21
 
22
22
  return buildDeliveryOverview(
23
23
  allMethods,
24
24
  allRoutes,
25
25
  allZones,
26
- allDrivers,
26
+ allCouriers,
27
27
  config.commerce?.currency,
28
28
  )
29
29
  }
@@ -0,0 +1,24 @@
1
+ import { Action } from '@stacksjs/actions'
2
+ import { Courier, User } from '@stacksjs/orm'
3
+ import { dashboardOperationalError } from '../dashboard-response'
4
+ import { indexCourierUsers, normalizeCourierRecord } from './courier-records'
5
+
6
+ export default new Action({
7
+ name: 'Dashboard Couriers',
8
+ description: 'Returns validated couriers and their optional User relationships.',
9
+ method: 'GET',
10
+ apiResponse: true,
11
+
12
+ async handle() {
13
+ try {
14
+ const couriers = await Courier.orderBy('name', 'asc').limit(500).get()
15
+ const userIds = [...new Set(couriers.map(courier => courier.get('user_id')).filter(Boolean))]
16
+ const users = userIds.length ? await User.whereIn('id', userIds).get() : []
17
+ const usersById = indexCourierUsers(users)
18
+ return couriers.map(courier => normalizeCourierRecord(courier, usersById))
19
+ }
20
+ catch (error) {
21
+ return dashboardOperationalError(error, 'Courier records could not be read.', 'CourierIndexAction')
22
+ }
23
+ },
24
+ })
@@ -1,24 +1,24 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { DeliveryRoute, Driver } from '@stacksjs/orm'
2
+ import { DeliveryRoute, Courier } from '@stacksjs/orm'
3
3
  import { dashboardOperationalError } from '../dashboard-response'
4
4
  import {
5
- indexDeliveryRouteDrivers,
5
+ indexDeliveryRouteCouriers,
6
6
  normalizeDeliveryRouteRecord,
7
7
  } from './delivery-route-records'
8
8
 
9
9
  export default new Action({
10
10
  name: 'Dashboard Delivery Routes',
11
- description: 'Returns validated delivery routes and their optional Driver relationships.',
11
+ description: 'Returns validated delivery routes and their optional Courier relationships.',
12
12
  method: 'GET',
13
13
  apiResponse: true,
14
14
 
15
15
  async handle() {
16
16
  try {
17
17
  const routes = await DeliveryRoute.orderByDesc('last_active').limit(500).get()
18
- const driverIds = [...new Set(routes.map(route => route.get('driver_id')).filter(Boolean))]
19
- const drivers = driverIds.length ? await Driver.whereIn('id', driverIds).get() : []
20
- const driversById = indexDeliveryRouteDrivers(drivers)
21
- return routes.map(route => normalizeDeliveryRouteRecord(route, driversById))
18
+ const courierIds = [...new Set(routes.map(route => route.get('courier_id')).filter(Boolean))]
19
+ const couriers = courierIds.length ? await Courier.whereIn('id', courierIds).get() : []
20
+ const couriersById = indexDeliveryRouteCouriers(couriers)
21
+ return routes.map(route => normalizeDeliveryRouteRecord(route, couriersById))
22
22
  }
23
23
  catch (error) {
24
24
  return dashboardOperationalError(error, 'Delivery route records could not be read.', 'DeliveryRouteIndexAction')
@@ -26,8 +26,8 @@ describe('commerce delivery overview', () => {
26
26
  [
27
27
  {
28
28
  id: 1,
29
- driver_id: 7,
30
- driver: 'Old Driver Name',
29
+ courier_id: 7,
30
+ courier: 'Old Courier Name',
31
31
  vehicle: 'Old Vehicle',
32
32
  stops: 4,
33
33
  delivery_time: 90,
@@ -36,7 +36,7 @@ describe('commerce delivery overview', () => {
36
36
  },
37
37
  {
38
38
  id: 2,
39
- driver: 'Stale Driver',
39
+ courier: 'Stale Courier',
40
40
  vehicle: 'Van S1',
41
41
  stops: 8,
42
42
  delivery_time: 180,
@@ -49,8 +49,8 @@ describe('commerce delivery overview', () => {
49
49
  { id: 2, name: 'Active Zone', status: 'active', countries: '["US","CA"]', regions: '["West"]' },
50
50
  ],
51
51
  [
52
- { id: 7, name: 'Current Driver', vehicle_number: 'VAN-7', status: 'on_delivery' },
53
- { id: 8, name: 'Available Driver', vehicle_number: 'VAN-8', status: 'active' },
52
+ { id: 7, name: 'Current Courier', vehicle_number: 'VAN-7', status: 'on_delivery' },
53
+ { id: 8, name: 'Available Courier', vehicle_number: 'VAN-8', status: 'active' },
54
54
  ],
55
55
  'EUR',
56
56
  now,
@@ -60,7 +60,7 @@ describe('commerce delivery overview', () => {
60
60
  expect(result.routes).toEqual([
61
61
  {
62
62
  id: 1,
63
- driver: 'Current Driver',
63
+ courier: 'Current Courier',
64
64
  vehicle: 'VAN-7',
65
65
  stops: 4,
66
66
  duration: '1h 30m',
@@ -76,7 +76,7 @@ describe('commerce delivery overview', () => {
76
76
  })
77
77
  expect(result.methods[1]?.freeShipping).toBe('Not enabled')
78
78
  expect(result.zones[0]).toMatchObject({ name: 'Active Zone', countries: 2, regions: 1 })
79
- expect(result.drivers).toEqual({
79
+ expect(result.couriers).toEqual({
80
80
  total: 2,
81
81
  active: 1,
82
82
  onDelivery: 1,
@@ -84,7 +84,7 @@ describe('commerce delivery overview', () => {
84
84
  })
85
85
  })
86
86
 
87
- test('rejects corrupt delivery records and missing driver relationships', () => {
87
+ test('rejects corrupt delivery records and missing courier relationships', () => {
88
88
  expect(() => buildDeliveryOverview(
89
89
  [{ id: 1, name: 'Ground', status: 'active', base_rate: 'free', free_shipping: null }],
90
90
  [],
@@ -98,8 +98,8 @@ describe('commerce delivery overview', () => {
98
98
  [],
99
99
  [{
100
100
  id: 1,
101
- driver_id: 99,
102
- driver: 'Recorded driver',
101
+ courier_id: 99,
102
+ courier: 'Recorded courier',
103
103
  vehicle: 'Recorded vehicle',
104
104
  stops: 1,
105
105
  delivery_time: 30,
@@ -110,6 +110,6 @@ describe('commerce delivery overview', () => {
110
110
  [],
111
111
  'USD',
112
112
  now,
113
- )).toThrow('DeliveryRoute 1.driver_id references missing Driver 99')
113
+ )).toThrow('DeliveryRoute 1.courier_id references missing Courier 99')
114
114
  })
115
115
  })