@stacksjs/defaults 0.72.102 → 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/views/dashboard/layouts/default.stx +1 -1
- package/app/Actions/Dashboard/Commerce/DriverIndexAction.ts +0 -24
- 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: '
|
|
10
|
-
description: '
|
|
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, '
|
|
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.
|
|
18
|
+
const model = await shippings.couriers.fetchById(id)
|
|
19
19
|
if (!model)
|
|
20
|
-
return commerceNotFound('
|
|
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: '
|
|
9
|
-
description: '
|
|
8
|
+
name: 'Courier Store',
|
|
9
|
+
description: 'Courier Store ORM Action',
|
|
10
10
|
method: 'POST',
|
|
11
|
-
model:
|
|
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.
|
|
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: '
|
|
10
|
-
description: '
|
|
9
|
+
name: 'Courier Update',
|
|
10
|
+
description: 'Courier Update ORM Action',
|
|
11
11
|
method: 'PATCH',
|
|
12
|
-
model:
|
|
12
|
+
model: Courier,
|
|
13
13
|
async handle(request: RequestInstance) {
|
|
14
|
-
const identifier = commerceIdentifier(request, '
|
|
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.
|
|
22
|
+
const model = await shippings.couriers.update(id, data)
|
|
23
23
|
if (!model)
|
|
24
|
-
return commerceNotFound('
|
|
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'), '
|
|
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'), '
|
|
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: '
|
|
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('
|
|
25
|
+
const result = commerceNotFound('Courier', 42)
|
|
26
26
|
expect(result.status).toBe(404)
|
|
27
27
|
expect(await result.json()).toEqual({
|
|
28
|
-
message: '
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
2
|
+
import { DeliveryRoute, Courier } from '@stacksjs/orm'
|
|
3
3
|
import { dashboardOperationalError } from '../dashboard-response'
|
|
4
4
|
import {
|
|
5
|
-
|
|
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
|
|
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
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
return routes.map(route => normalizeDeliveryRouteRecord(route,
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
53
|
-
{ id: 8, name: 'Available
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
102
|
-
|
|
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.
|
|
113
|
+
)).toThrow('DeliveryRoute 1.courier_id references missing Courier 99')
|
|
114
114
|
})
|
|
115
115
|
})
|