@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.
- package/ai/AGENTS.md +25 -4
- package/ai/README.md +26 -4
- package/ai/skills/stacks-actions/SKILL.md +1 -1
- package/ai/skills/stacks-ai/SKILL.md +1 -1
- package/ai/skills/stacks-alias/SKILL.md +1 -1
- package/ai/skills/stacks-analytics/SKILL.md +1 -1
- package/ai/skills/stacks-api/SKILL.md +1 -1
- package/ai/skills/stacks-arrays/SKILL.md +1 -1
- package/ai/skills/stacks-auto-imports/SKILL.md +1 -1
- package/ai/skills/stacks-browse/SKILL.md +1 -1
- package/ai/skills/stacks-browser/SKILL.md +1 -1
- package/ai/skills/stacks-buddy/SKILL.md +1 -1
- package/ai/skills/stacks-build/SKILL.md +79 -5
- package/ai/skills/stacks-cache/SKILL.md +1 -1
- package/ai/skills/stacks-calendar/SKILL.md +1 -1
- package/ai/skills/stacks-chat/SKILL.md +1 -1
- package/ai/skills/stacks-cli/SKILL.md +1 -1
- package/ai/skills/stacks-cloud/SKILL.md +1 -1
- package/ai/skills/stacks-cms/SKILL.md +1 -1
- package/ai/skills/stacks-codebase-design/DEEPENING.md +79 -0
- package/ai/skills/stacks-codebase-design/DESIGN-IT-TWICE.md +72 -0
- package/ai/skills/stacks-codebase-design/SKILL.md +180 -0
- package/ai/skills/stacks-collections/SKILL.md +1 -1
- package/ai/skills/stacks-commerce/SKILL.md +141 -35
- package/ai/skills/stacks-composables/SKILL.md +1 -1
- package/ai/skills/stacks-config/SKILL.md +1 -1
- package/ai/skills/stacks-configuration/SKILL.md +1 -1
- package/ai/skills/stacks-cron/SKILL.md +1 -1
- package/ai/skills/stacks-crosswind/SKILL.md +1 -1
- package/ai/skills/stacks-database/SKILL.md +1 -1
- package/ai/skills/stacks-datetime/SKILL.md +1 -1
- package/ai/skills/stacks-dependencies/SKILL.md +1 -1
- package/ai/skills/stacks-deploy/SKILL.md +1 -1
- package/ai/skills/stacks-desktop/SKILL.md +1 -1
- package/ai/skills/stacks-development/SKILL.md +1 -1
- package/ai/skills/stacks-dns/SKILL.md +1 -1
- package/ai/skills/stacks-docs/SKILL.md +1 -1
- package/ai/skills/stacks-domain-modeling/FORMATS.md +123 -0
- package/ai/skills/stacks-domain-modeling/SKILL.md +109 -0
- package/ai/skills/stacks-enums/SKILL.md +1 -1
- package/ai/skills/stacks-error-handling/SKILL.md +1 -1
- package/ai/skills/stacks-events/SKILL.md +1 -1
- package/ai/skills/stacks-faker/SKILL.md +1 -1
- package/ai/skills/stacks-flow/PHASE-BOUNDARIES.md +91 -0
- package/ai/skills/stacks-flow/SKILL.md +117 -0
- package/ai/skills/stacks-git/SKILL.md +37 -9
- package/ai/skills/stacks-grilling/SKILL.md +85 -0
- package/ai/skills/stacks-guard/SKILL.md +86 -11
- package/ai/skills/stacks-guard/scripts/block-destructive.sh +67 -0
- package/ai/skills/stacks-handoff/SKILL.md +70 -0
- package/ai/skills/stacks-health/SKILL.md +1 -1
- package/ai/skills/stacks-http/SKILL.md +1 -1
- package/ai/skills/stacks-i18n/SKILL.md +1 -1
- package/ai/skills/stacks-investigate/SKILL.md +234 -106
- package/ai/skills/stacks-investigate/scripts/hitl-loop.template.sh +47 -0
- package/ai/skills/stacks-jobs/SKILL.md +1 -1
- package/ai/skills/stacks-listeners/SKILL.md +1 -1
- package/ai/skills/stacks-logging/SKILL.md +1 -1
- package/ai/skills/stacks-mail/SKILL.md +1 -1
- package/ai/skills/stacks-middleware/SKILL.md +1 -1
- package/ai/skills/stacks-migrations/SKILL.md +1 -1
- package/ai/skills/stacks-models/SKILL.md +1 -1
- package/ai/skills/stacks-new-feature/SKILL.md +65 -5
- package/ai/skills/stacks-notifications/SKILL.md +1 -1
- package/ai/skills/stacks-objects/SKILL.md +1 -1
- package/ai/skills/stacks-office-hours/SKILL.md +16 -2
- package/ai/skills/stacks-orm/SKILL.md +1 -1
- package/ai/skills/stacks-path/SKILL.md +1 -1
- package/ai/skills/stacks-payments/SKILL.md +35 -1
- package/ai/skills/stacks-plan-review/SKILL.md +27 -6
- package/ai/skills/stacks-plugins/SKILL.md +1 -1
- package/ai/skills/stacks-prototype/LOGIC.md +103 -0
- package/ai/skills/stacks-prototype/SKILL.md +66 -0
- package/ai/skills/stacks-prototype/UI.md +112 -0
- package/ai/skills/stacks-push/SKILL.md +1 -1
- package/ai/skills/stacks-query-builder/SKILL.md +1 -1
- package/ai/skills/stacks-queue/SKILL.md +1 -1
- package/ai/skills/stacks-realtime/SKILL.md +1 -1
- package/ai/skills/stacks-registry/SKILL.md +1 -1
- package/ai/skills/stacks-repl/SKILL.md +1 -1
- package/ai/skills/stacks-retro/SKILL.md +121 -75
- package/ai/skills/stacks-review/SKILL.md +182 -74
- package/ai/skills/stacks-router/SKILL.md +1 -1
- package/ai/skills/stacks-routes/SKILL.md +1 -1
- package/ai/skills/stacks-scaffolding/SKILL.md +1 -1
- package/ai/skills/stacks-scheduler/SKILL.md +1 -1
- package/ai/skills/stacks-search-engine/SKILL.md +1 -1
- package/ai/skills/stacks-security/SKILL.md +1 -1
- package/ai/skills/stacks-security-audit/SKILL.md +1 -1
- package/ai/skills/stacks-server/SKILL.md +1 -1
- package/ai/skills/stacks-shell/SKILL.md +1 -1
- package/ai/skills/stacks-slug/SKILL.md +1 -1
- package/ai/skills/stacks-sms/SKILL.md +1 -1
- package/ai/skills/stacks-socials/SKILL.md +1 -1
- package/ai/skills/stacks-storage/SKILL.md +1 -1
- package/ai/skills/stacks-strings/SKILL.md +1 -1
- package/ai/skills/stacks-stx/SKILL.md +1 -1
- package/ai/skills/stacks-tdd/EXAMPLES.md +136 -0
- package/ai/skills/stacks-tdd/SKILL.md +125 -0
- package/ai/skills/stacks-testing/SKILL.md +13 -3
- package/ai/skills/stacks-tunnel/SKILL.md +1 -1
- package/ai/skills/stacks-types/SKILL.md +1 -1
- package/ai/skills/stacks-ui/SKILL.md +1 -1
- package/ai/skills/stacks-utils/SKILL.md +1 -1
- package/ai/skills/stacks-validation/SKILL.md +1 -1
- package/ai/skills/stacks-whois/SKILL.md +1 -1
- package/ai/skills/stacks-wizard/SKILL.md +127 -0
- package/ai/skills/stacks-wizard/scripts/template.sh +208 -0
- package/ai/skills/stacks-writing-for-agents/MECHANICS.md +125 -0
- package/ai/skills/stacks-writing-for-agents/SKILL.md +218 -0
- package/app/Actions/Auth/GenerateTwoFactorSecretAction.ts +12 -2
- package/app/Actions/Commerce/Shipping/{DriverDestroyAction.ts → CourierDestroyAction.ts} +6 -6
- package/app/Actions/Commerce/Shipping/{DriverIndexAction.ts → CourierIndexAction.ts} +3 -3
- package/app/Actions/Commerce/Shipping/CourierPingStoreAction.ts +61 -0
- package/app/Actions/Commerce/Shipping/{DriverShowAction.ts → CourierShowAction.ts} +5 -5
- package/app/Actions/Commerce/Shipping/{DriverStoreAction.ts → CourierStoreAction.ts} +4 -4
- package/app/Actions/Commerce/Shipping/{DriverUpdateAction.ts → CourierUpdateAction.ts} +6 -6
- package/app/Actions/Commerce/Shipping/DeliveryRouteStartAction.ts +38 -0
- package/app/Actions/Commerce/Shipping/DeliveryStopCompleteAction.ts +40 -0
- package/app/Actions/Commerce/Shipping/DeliveryStopFailAction.ts +44 -0
- package/app/Actions/Commerce/Shipping/DeliveryStopStartAction.ts +39 -0
- package/app/Actions/Commerce/Shipping/courier-session.ts +75 -0
- package/app/Actions/Commerce/commerce-action.test.ts +5 -5
- package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +5 -5
- package/app/Actions/Dashboard/Commerce/CourierIndexAction.ts +24 -0
- package/app/Actions/Dashboard/Commerce/DeliveryRouteIndexAction.ts +7 -7
- package/app/Actions/Dashboard/Commerce/commerce-delivery.test.ts +11 -11
- package/app/Actions/Dashboard/Commerce/commerce-delivery.ts +29 -29
- package/app/Actions/Dashboard/Commerce/{driver-records.test.ts → courier-records.test.ts} +9 -9
- package/app/Actions/Dashboard/Commerce/{driver-records.ts → courier-records.ts} +14 -14
- package/app/Actions/Dashboard/Commerce/delivery-route-records.test.ts +13 -13
- package/app/Actions/Dashboard/Commerce/delivery-route-records.ts +25 -25
- package/app/Models/User.ts +1 -1
- package/app/Models/commerce/{Driver.ts → Courier.ts} +7 -7
- package/app/Models/commerce/{DriverPing.ts → CourierPing.ts} +7 -7
- package/app/Models/commerce/DeliveryRoute.ts +6 -6
- package/app/Models/commerce/DeliveryStop.ts +31 -8
- package/bootstrap.ts +7 -0
- package/functions/commerce/shippings/couriers.ts +19 -0
- package/ide/vscode/package.json +1 -1
- package/package.json +4 -3
- package/resources/components/Dashboard/Commerce/Delivery/{DriverDeleteDialog.stx → CourierDeleteDialog.stx} +5 -5
- package/resources/components/Dashboard/Commerce/Delivery/{DriverDialog.stx → CourierDialog.stx} +10 -10
- package/resources/components/Dashboard/Commerce/Delivery/{DriversDashboard.stx → CouriersDashboard.stx} +43 -43
- package/resources/components/Dashboard/Commerce/Delivery/{DriversTable.stx → CouriersTable.stx} +21 -21
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +18 -18
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +2 -2
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +22 -22
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +24 -24
- package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesTable.stx +8 -8
- package/resources/components/Dashboard/Commerce/Delivery/TabNavigation.stx +1 -1
- package/resources/functions/dashboard/data.ts +1 -1
- package/resources/functions/dashboard/sidebar.ts +2 -2
- package/routes/dashboard-api.ts +6 -6
- package/routes/dashboard.ts +7 -7
- package/routes/delivery.ts +24 -0
- package/types/defaults.ts +3 -3
- package/views/dashboard/.discovered-models.json +18 -18
- package/views/dashboard/AUDIT.md +1 -1
- package/views/dashboard/commerce/delivery/{drivers.stx → couriers.stx} +2 -2
- package/views/dashboard/composables/useChart.ts +16 -2
- package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +0 -24
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
96
|
-
const identifier = commerceIdentifier(commerceValue(
|
|
97
|
-
const source = `
|
|
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(
|
|
100
|
+
name: commerceRequiredString(commerceValue(courier, 'name'), source, 'name'),
|
|
101
101
|
vehicle: commerceRequiredString(
|
|
102
|
-
commerceValue(
|
|
102
|
+
commerceValue(courier, 'vehicle_number', 'vehicleNumber'),
|
|
103
103
|
source,
|
|
104
104
|
'vehicle_number',
|
|
105
105
|
),
|
|
106
|
-
status: commerceEnum(commerceValue(
|
|
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
|
|
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
|
|
123
|
-
commerceValue(route, '
|
|
122
|
+
const courierId = commerceOptionalIdentifier(
|
|
123
|
+
commerceValue(route, 'courier_id', 'courierId'),
|
|
124
124
|
source,
|
|
125
|
-
'
|
|
125
|
+
'courier_id',
|
|
126
126
|
)
|
|
127
|
-
const
|
|
128
|
-
if (
|
|
129
|
-
throw new TypeError(`${source}.
|
|
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
|
-
|
|
140
|
-
|| commerceRequiredString(commerceValue(route, '
|
|
141
|
-
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
|
|
228
|
-
counts[
|
|
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: '
|
|
246
|
-
value: (
|
|
247
|
-
detail: `${
|
|
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
|
-
|
|
264
|
-
total:
|
|
265
|
-
active:
|
|
266
|
-
onDelivery:
|
|
267
|
-
onBreak:
|
|
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 {
|
|
2
|
+
import { indexCourierUsers, normalizeCourierRecord } from './courier-records'
|
|
3
3
|
|
|
4
|
-
describe('
|
|
4
|
+
describe('courier records', () => {
|
|
5
5
|
const user = { id: 2, name: 'Ada Lovelace', email: 'ada@example.com' }
|
|
6
|
-
const users =
|
|
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
|
|
21
|
-
expect(
|
|
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(() =>
|
|
38
|
-
.toThrow('
|
|
39
|
-
expect(() =>
|
|
40
|
-
.toThrow('
|
|
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
|
|
14
|
-
export type
|
|
13
|
+
export const courierStatuses = ['active', 'on_delivery', 'on_break'] as const
|
|
14
|
+
export type CourierStatus = typeof courierStatuses[number]
|
|
15
15
|
|
|
16
|
-
export interface
|
|
16
|
+
export interface CourierUser {
|
|
17
17
|
id: number
|
|
18
18
|
name: string
|
|
19
19
|
email: string
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export interface
|
|
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:
|
|
28
|
+
status: CourierStatus
|
|
29
29
|
user_id: number | null
|
|
30
|
-
user:
|
|
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
|
|
44
|
-
const result = new Map<number,
|
|
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
|
|
59
|
+
export function normalizeCourierRecord(
|
|
60
60
|
record: any,
|
|
61
|
-
users: ReadonlyMap<number,
|
|
62
|
-
):
|
|
63
|
-
const id = numericIdentifier(commerceValue(record, 'id'), '
|
|
64
|
-
const source = `
|
|
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',
|
|
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
|
-
|
|
3
|
+
indexDeliveryRouteCouriers,
|
|
4
4
|
normalizeDeliveryRouteRecord,
|
|
5
5
|
} from './delivery-route-records'
|
|
6
6
|
|
|
7
7
|
describe('delivery route records', () => {
|
|
8
|
-
const
|
|
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
|
|
14
|
+
const couriers = indexDeliveryRouteCouriers([courier])
|
|
15
15
|
const base = {
|
|
16
16
|
id: 1,
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
30
|
-
expect(normalizeDeliveryRouteRecord(base,
|
|
29
|
+
test('normalizes route metrics and the linked courier', () => {
|
|
30
|
+
expect(normalizeDeliveryRouteRecord(base, couriers)).toEqual({
|
|
31
31
|
id: 1,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
|
46
|
+
test('rejects missing couriers and fractional operational counts', () => {
|
|
47
47
|
expect(() => normalizeDeliveryRouteRecord(base, new Map()))
|
|
48
|
-
.toThrow('DeliveryRoute 1.
|
|
49
|
-
expect(() => normalizeDeliveryRouteRecord({ ...base,
|
|
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 {
|
|
12
|
-
import {
|
|
11
|
+
import type { CourierStatus } from './courier-records'
|
|
12
|
+
import { courierStatuses } from './courier-records'
|
|
13
13
|
|
|
14
|
-
export interface
|
|
14
|
+
export interface DeliveryRouteCourier {
|
|
15
15
|
id: number
|
|
16
16
|
name: string
|
|
17
17
|
vehicle_number: string
|
|
18
|
-
status:
|
|
18
|
+
status: CourierStatus
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
export interface DeliveryRouteRecord {
|
|
22
22
|
id: number
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
44
|
-
const result = new Map<number,
|
|
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'), '
|
|
47
|
-
const source = `
|
|
46
|
+
const id = numericIdentifier(commerceValue(record, 'id'), 'Courier')
|
|
47
|
+
const source = `Courier ${id}`
|
|
48
48
|
if (result.has(id))
|
|
49
|
-
throw new TypeError(`Duplicate
|
|
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',
|
|
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
|
-
|
|
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
|
|
71
|
-
commerceValue(record, '
|
|
70
|
+
const courierIdentifier = commerceOptionalIdentifier(
|
|
71
|
+
commerceValue(record, 'courier_id', 'courierId'),
|
|
72
72
|
source,
|
|
73
|
-
'
|
|
73
|
+
'courier_id',
|
|
74
74
|
)
|
|
75
|
-
const
|
|
76
|
-
? numericIdentifier(
|
|
75
|
+
const courierId = courierIdentifier
|
|
76
|
+
? numericIdentifier(courierIdentifier, source, 'courier_id')
|
|
77
77
|
: null
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
throw new TypeError(`${source}.
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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,
|
package/app/Models/User.ts
CHANGED
|
@@ -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: '
|
|
6
|
-
table: '
|
|
5
|
+
name: 'Courier',
|
|
6
|
+
table: 'couriers',
|
|
7
7
|
primaryKey: 'id',
|
|
8
8
|
autoIncrement: true,
|
|
9
9
|
|
|
10
10
|
belongsTo: ['User'],
|
|
11
|
-
hasMany: ['DeliveryRoute', '
|
|
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: '
|
|
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 `
|
|
98
|
-
*
|
|
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
|
|
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
|
|
5
|
+
* One position fix from a courier's device.
|
|
6
6
|
*
|
|
7
|
-
* The append-only history behind `
|
|
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
|
-
* `
|
|
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: '
|
|
20
|
-
table: '
|
|
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: ['
|
|
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
|
|
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', '
|
|
15
|
-
searchable: ['
|
|
14
|
+
displayable: ['id', 'courier', 'vehicle', 'stops', 'deliveryTime', 'totalDistance', 'lastActive'],
|
|
15
|
+
searchable: ['courier', 'vehicle'],
|
|
16
16
|
sortable: ['stops', 'deliveryTime', 'totalDistance', 'lastActive', 'createdAt', 'updatedAt'],
|
|
17
|
-
filterable: ['
|
|
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: ['
|
|
33
|
-
hasMany: ['DeliveryStop', '
|
|
32
|
+
belongsTo: ['Courier'],
|
|
33
|
+
hasMany: ['DeliveryStop', 'CourierPing'],
|
|
34
34
|
|
|
35
35
|
attributes: {
|
|
36
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|