@aglyn/plugins-commerce 1.0.0-beta.232 → 1.0.0-beta.233
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/package.json +13 -13
- package/src/lib/components/cart-extras.d.ts +6 -0
- package/src/lib/components/cart-extras.js +4 -1
- package/src/lib/components/cart-extras.js.map +1 -1
- package/src/lib/components/cart-fulfillment.d.ts +117 -0
- package/src/lib/components/cart-fulfillment.js +367 -0
- package/src/lib/components/cart-fulfillment.js.map +1 -0
- package/src/lib/components/cart.js +52 -10
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/commerce-console-page.js +42 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +38 -10
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/local-delivery-settings-card.component.d.ts +37 -0
- package/src/lib/components/console/local-delivery-settings-card.component.js +542 -0
- package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -0
- package/src/lib/components/console/locations-card.component.js +177 -3
- package/src/lib/components/console/locations-card.component.js.map +1 -1
- package/src/lib/components/console/order-detail-dialog.component.js +10 -0
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-local-fulfillment-panel.component.d.ts +44 -0
- package/src/lib/components/console/order-local-fulfillment-panel.component.js +265 -0
- package/src/lib/components/console/order-local-fulfillment-panel.component.js.map +1 -0
- package/src/lib/components/console/order-offline-notice.component.d.ts +12 -0
- package/src/lib/components/console/order-offline-notice.component.js +53 -0
- package/src/lib/components/console/order-offline-notice.component.js.map +1 -0
- package/src/lib/components/console/pickup-delivery-queue-card.component.d.ts +35 -0
- package/src/lib/components/console/pickup-delivery-queue-card.component.js +222 -0
- package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -0
- package/src/lib/components/console/pos/pos-api.d.ts +24 -1
- package/src/lib/components/console/pos/pos-api.js +13 -0
- package/src/lib/components/console/pos/pos-api.js.map +1 -1
- package/src/lib/components/console/pos/pos-item-dialog.component.d.ts +12 -0
- package/src/lib/components/console/pos/pos-item-dialog.component.js +10 -8
- package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-kiosk-queue.component.d.ts +22 -0
- package/src/lib/components/console/pos/pos-kiosk-queue.component.js +168 -0
- package/src/lib/components/console/pos/pos-kiosk-queue.component.js.map +1 -0
- package/src/lib/components/console/pos/pos-tender-dialogs.component.d.ts +18 -0
- package/src/lib/components/console/pos/pos-tender-dialogs.component.js +116 -1
- package/src/lib/components/console/pos/pos-tender-dialogs.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-tender-panel.component.d.ts +8 -0
- package/src/lib/components/console/pos/pos-tender-panel.component.js +77 -18
- package/src/lib/components/console/pos/pos-tender-panel.component.js.map +1 -1
- package/src/lib/components/console/pos/use-pos-display.d.ts +5 -2
- package/src/lib/components/console/pos/use-pos-display.js +3 -2
- package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
- package/src/lib/components/console/pos-devices-card.component.js +2 -2
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-display/pairing-screen.d.ts +5 -2
- package/src/lib/components/console/pos-display/pairing-screen.js +7 -5
- package/src/lib/components/console/pos-display/pairing-screen.js.map +1 -1
- package/src/lib/components/console/pos-display/pos-display-api.d.ts +7 -1
- package/src/lib/components/console/pos-display/pos-display-api.js +7 -2
- package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
- package/src/lib/components/console/pos-kiosk/kiosk-checkout.d.ts +41 -0
- package/src/lib/components/console/pos-kiosk/kiosk-checkout.js +275 -0
- package/src/lib/components/console/pos-kiosk/kiosk-checkout.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/kiosk-menu.d.ts +32 -0
- package/src/lib/components/console/pos-kiosk/kiosk-menu.js +344 -0
- package/src/lib/components/console/pos-kiosk/kiosk-menu.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/kiosk-staff.d.ts +12 -0
- package/src/lib/components/console/pos-kiosk/kiosk-staff.js +257 -0
- package/src/lib/components/console/pos-kiosk/kiosk-staff.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/pos-kiosk-api.d.ts +114 -0
- package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js +189 -0
- package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/use-kiosk-idle.d.ts +16 -0
- package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js +80 -0
- package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js.map +1 -0
- package/src/lib/components/console/pos-kiosk-page.component.d.ts +28 -0
- package/src/lib/components/console/pos-kiosk-page.component.js +791 -0
- package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-api.d.ts +44 -0
- package/src/lib/components/console/pos-offline/pos-offline-api.js +69 -0
- package/src/lib/components/console/pos-offline/pos-offline-api.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-banner.component.d.ts +7 -0
- package/src/lib/components/console/pos-offline/pos-offline-banner.component.js +135 -0
- package/src/lib/components/console/pos-offline/pos-offline-banner.component.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-checkout.component.d.ts +32 -0
- package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js +287 -0
- package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-store.d.ts +63 -0
- package/src/lib/components/console/pos-offline/pos-offline-store.js +145 -0
- package/src/lib/components/console/pos-offline/pos-offline-store.js.map +1 -0
- package/src/lib/components/console/pos-offline/register-offline.d.ts +24 -0
- package/src/lib/components/console/pos-offline/register-offline.js +24 -0
- package/src/lib/components/console/pos-offline/register-offline.js.map +1 -0
- package/src/lib/components/console/pos-offline/use-pos-offline.d.ts +85 -0
- package/src/lib/components/console/pos-offline/use-pos-offline.js +542 -0
- package/src/lib/components/console/pos-offline/use-pos-offline.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js +72 -17
- package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +174 -27
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +35 -10
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/shipping-settings-card.component.js +1 -1
- package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
- package/src/lib/components/console/stock-movements-card.component.js +48 -16
- package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
- package/src/lib/components/console/store-zones.d.ts +8 -1
- package/src/lib/components/console/store-zones.js +11 -1
- package/src/lib/components/console/store-zones.js.map +1 -1
- package/src/lib/components/order-status.js +66 -0
- package/src/lib/components/order-status.js.map +1 -1
- package/src/lib/components/product-detail.js +42 -15
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/constants/local-fulfillment-list-query.d.ts +48 -0
- package/src/lib/constants/local-fulfillment-list-query.js +84 -0
- package/src/lib/constants/local-fulfillment-list-query.js.map +1 -0
- package/src/lib/constants/orders-list-query.d.ts +7 -2
- package/src/lib/constants/orders-list-query.js +89 -8
- package/src/lib/constants/orders-list-query.js.map +1 -1
- package/src/lib/constants/pos-shift-list-query.d.ts +22 -0
- package/src/lib/constants/pos-shift-list-query.js +115 -0
- package/src/lib/constants/pos-shift-list-query.js.map +1 -0
- package/src/lib/constants/product-list-query.d.ts +6 -1
- package/src/lib/constants/product-list-query.js +77 -8
- package/src/lib/constants/product-list-query.js.map +1 -1
- package/src/lib/constants/stock-movement-query.d.ts +3 -2
- package/src/lib/constants/stock-movement-query.js +52 -8
- package/src/lib/constants/stock-movement-query.js.map +1 -1
- package/src/lib/model/buyer-notifications.d.ts +5 -3
- package/src/lib/model/buyer-notifications.js +25 -4
- package/src/lib/model/buyer-notifications.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +59 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-pos-kiosk.d.ts +175 -0
- package/src/lib/model/commerce-pos-kiosk.js +147 -0
- package/src/lib/model/commerce-pos-kiosk.js.map +1 -0
- package/src/lib/model/commerce-pos-offline.d.ts +234 -0
- package/src/lib/model/commerce-pos-offline.js +301 -0
- package/src/lib/model/commerce-pos-offline.js.map +1 -0
- package/src/lib/model/commerce-pos-ops.d.ts +16 -6
- package/src/lib/model/commerce-pos-ops.js +12 -4
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +13 -1
- package/src/lib/model/commerce-pos.js +5 -4
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-stock-holds.d.ts +24 -3
- package/src/lib/model/commerce-stock-holds.js +26 -8
- package/src/lib/model/commerce-stock-holds.js.map +1 -1
- package/src/lib/model/commerce.d.ts +3 -0
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +4 -0
- package/src/lib/model/index.js +4 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/local-fulfillment-settings.d.ts +178 -0
- package/src/lib/model/local-fulfillment-settings.js +361 -0
- package/src/lib/model/local-fulfillment-settings.js.map +1 -0
- package/src/lib/model/order-list-fields.d.ts +10 -1
- package/src/lib/model/order-list-fields.js +1 -0
- package/src/lib/model/order-list-fields.js.map +1 -1
- package/src/lib/model/order-local-fulfillment.d.ts +131 -0
- package/src/lib/model/order-local-fulfillment.js +125 -0
- package/src/lib/model/order-local-fulfillment.js.map +1 -0
- package/src/lib/model/order-shipping-export.js +4 -0
- package/src/lib/model/order-shipping-export.js.map +1 -1
- package/src/lib/model/order-status-view.d.ts +20 -1
- package/src/lib/model/order-status-view.js +60 -6
- package/src/lib/model/order-status-view.js.map +1 -1
- package/src/lib/plugin-config.js +6 -2
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +8 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/pos-kiosk-config.d.ts +26 -0
- package/src/lib/pos-kiosk-config.js +59 -0
- package/src/lib/pos-kiosk-config.js.map +1 -0
- package/src/lib/server/api-v1/openapi.js +8 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -1
- package/src/lib/server/api-v1/order-view.d.ts +19 -0
- package/src/lib/server/api-v1/order-view.js +35 -0
- package/src/lib/server/api-v1/order-view.js.map +1 -1
- package/src/lib/server/billing-webhook.js +238 -87
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.d.ts +2 -0
- package/src/lib/server/cart-checkout.js +173 -13
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/checkout-extras.js +18 -4
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +71 -14
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/local-fulfillment-status.d.ts +86 -0
- package/src/lib/server/local-fulfillment-status.js +379 -0
- package/src/lib/server/local-fulfillment-status.js.map +1 -0
- package/src/lib/server/local-fulfillment.d.ts +190 -0
- package/src/lib/server/local-fulfillment.js +580 -0
- package/src/lib/server/local-fulfillment.js.map +1 -0
- package/src/lib/server/membership-register.js +3 -1
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/order-notifications.js +61 -0
- package/src/lib/server/order-notifications.js.map +1 -1
- package/src/lib/server/pos-display.d.ts +27 -1
- package/src/lib/server/pos-display.js +50 -9
- package/src/lib/server/pos-display.js.map +1 -1
- package/src/lib/server/pos-kiosk-principal.d.ts +29 -0
- package/src/lib/server/pos-kiosk-principal.js +42 -0
- package/src/lib/server/pos-kiosk-principal.js.map +1 -0
- package/src/lib/server/pos-kiosk.d.ts +54 -0
- package/src/lib/server/pos-kiosk.js +972 -0
- package/src/lib/server/pos-kiosk.js.map +1 -0
- package/src/lib/server/pos-offline-sync.d.ts +85 -0
- package/src/lib/server/pos-offline-sync.js +765 -0
- package/src/lib/server/pos-offline-sync.js.map +1 -0
- package/src/lib/server/pos-order.js +22 -4
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.d.ts +6 -0
- package/src/lib/server/pos-payment.js +225 -6
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-print.js +5 -0
- package/src/lib/server/pos-print.js.map +1 -1
- package/src/lib/server/pos-return.js +27 -0
- package/src/lib/server/pos-return.js.map +1 -1
- package/src/lib/server/pos-sale.d.ts +6 -0
- package/src/lib/server/pos-sale.js +2 -0
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-shift.js +8 -1
- package/src/lib/server/pos-shift.js.map +1 -1
- package/src/lib/server/refund.js +3 -0
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/stock-hold.d.ts +9 -1
- package/src/lib/server/stock-hold.js +15 -7
- package/src/lib/server/stock-hold.js.map +1 -1
- package/src/lib/server.js +16 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.js +161 -0
- package/src/lib/tenant-emails.js.map +1 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { pluginShippingRateQuoter } from "@aglyn/aglyn/plugin-manager/plugin-shipping-rates";
|
|
18
|
+
import { createResourceUid } from "@aglyn/aglyn/server";
|
|
19
|
+
import * as Aglyn from "@aglyn/aglyn/server";
|
|
20
|
+
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
21
|
+
import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
22
|
+
import * as CommerceModel from "../model/index.js";
|
|
23
|
+
import { ORDER_DELIVERED_EVENT, ORDER_FULFILLED_EVENT } from "../model/order-events.js";
|
|
24
|
+
import { notifyOrderBuyer } from "./order-notifications.js";
|
|
25
|
+
import { fulfillmentEventView, stageOrderEvent } from "./order-events.js";
|
|
26
|
+
import { placeDeliveryAddress } from "./local-fulfillment.js";
|
|
27
|
+
const PICKUP_ACTIONS = [
|
|
28
|
+
'ready',
|
|
29
|
+
'preparing',
|
|
30
|
+
'picked_up'
|
|
31
|
+
];
|
|
32
|
+
const DELIVERY_ACTIONS = [
|
|
33
|
+
'out_for_delivery',
|
|
34
|
+
'delivered',
|
|
35
|
+
'delivery_failed'
|
|
36
|
+
];
|
|
37
|
+
const NOTE_MAX = 200;
|
|
38
|
+
/**
|
|
39
|
+
* Moves one order a step. The transaction, with NO authorization — the
|
|
40
|
+
* caller has already proven the member may.
|
|
41
|
+
*/ export async function moveLocalFulfillment(request) {
|
|
42
|
+
const { hostId, orderId, action } = request;
|
|
43
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
44
|
+
const orderRef = firestore.collection('hosts').doc(hostId).collection('orders').doc(orderId);
|
|
45
|
+
return firestore.runTransaction(async (transaction)=>{
|
|
46
|
+
var _snapshot_data, _request_nowMs;
|
|
47
|
+
const snapshot = await transaction.get(orderRef);
|
|
48
|
+
if (!snapshot.exists) return {
|
|
49
|
+
outcome: 'no_such_order'
|
|
50
|
+
};
|
|
51
|
+
const raw = (_snapshot_data = snapshot.data()) != null ? _snapshot_data : {};
|
|
52
|
+
const order = CommerceModel.liftLegacyOrder(raw);
|
|
53
|
+
const atMs = (_request_nowMs = request.nowMs) != null ? _request_nowMs : Date.now();
|
|
54
|
+
const isPickup = PICKUP_ACTIONS.includes(action);
|
|
55
|
+
if (isPickup ? order.fulfillmentMethod !== 'pickup' || !order.pickup : order.fulfillmentMethod !== 'local_delivery' || !order.localDelivery) {
|
|
56
|
+
return {
|
|
57
|
+
outcome: 'wrong_method'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// Nothing to hand over on an order that was cancelled or refunded.
|
|
61
|
+
if (order.status === 'cancelled' || order.status === 'refunded' || order.status === 'pending') {
|
|
62
|
+
return {
|
|
63
|
+
outcome: 'blocked',
|
|
64
|
+
from: order.status
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const handover = action === 'picked_up' || action === 'delivered';
|
|
68
|
+
let next;
|
|
69
|
+
let detail;
|
|
70
|
+
let event;
|
|
71
|
+
let notify;
|
|
72
|
+
let status = order.status;
|
|
73
|
+
if (isPickup) {
|
|
74
|
+
var _request_pickedUpBy;
|
|
75
|
+
const pickup = order.pickup;
|
|
76
|
+
const from = CommerceModel.orderPickupStatus(pickup.status);
|
|
77
|
+
const to = action;
|
|
78
|
+
if (from === to) return {
|
|
79
|
+
outcome: 'already'
|
|
80
|
+
};
|
|
81
|
+
if (!CommerceModel.canTransitionPickup(from, to)) return {
|
|
82
|
+
outcome: 'blocked',
|
|
83
|
+
from: CommerceModel.ORDER_PICKUP_STATUS_LABELS[from]
|
|
84
|
+
};
|
|
85
|
+
const pickedUpBy = String((_request_pickedUpBy = request.pickedUpBy) != null ? _request_pickedUpBy : '').trim().slice(0, NOTE_MAX);
|
|
86
|
+
const updated = _extends({}, pickup, {
|
|
87
|
+
status: to,
|
|
88
|
+
updatedAtMs: atMs
|
|
89
|
+
}, to === 'ready' ? {
|
|
90
|
+
readyAtMs: atMs
|
|
91
|
+
} : {}, to === 'picked_up' ? _extends({
|
|
92
|
+
pickedUpAtMs: atMs
|
|
93
|
+
}, pickedUpBy ? {
|
|
94
|
+
pickedUpBy
|
|
95
|
+
} : {}) : {});
|
|
96
|
+
if (to === 'preparing') delete updated.readyAtMs;
|
|
97
|
+
next = {
|
|
98
|
+
pickup: updated
|
|
99
|
+
};
|
|
100
|
+
event = to === 'ready' ? 'ready-for-pickup' : to === 'picked_up' ? 'picked-up' : 'pickup-preparing';
|
|
101
|
+
detail = to === 'ready' ? `Ready for pickup at ${pickup.locationName}` : to === 'picked_up' ? `Picked up at ${pickup.locationName}${pickedUpBy ? ` by ${pickedUpBy}` : ''}` : 'Back to preparing';
|
|
102
|
+
notify = to === 'ready' ? 'ready_for_pickup' : to === 'picked_up' ? 'picked_up' : null;
|
|
103
|
+
} else {
|
|
104
|
+
var _request_reason;
|
|
105
|
+
const delivery = order.localDelivery;
|
|
106
|
+
const from = CommerceModel.orderLocalDeliveryStatus(delivery.status);
|
|
107
|
+
const to = action === 'delivery_failed' ? 'failed' : action;
|
|
108
|
+
if (from === to) return {
|
|
109
|
+
outcome: 'already'
|
|
110
|
+
};
|
|
111
|
+
if (!CommerceModel.canTransitionLocalDelivery(from, to)) {
|
|
112
|
+
return {
|
|
113
|
+
outcome: 'blocked',
|
|
114
|
+
from: CommerceModel.ORDER_LOCAL_DELIVERY_STATUS_LABELS[from]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const reason = String((_request_reason = request.reason) != null ? _request_reason : '').trim().slice(0, NOTE_MAX);
|
|
118
|
+
const updated = _extends({}, delivery, {
|
|
119
|
+
status: to,
|
|
120
|
+
updatedAtMs: atMs
|
|
121
|
+
}, to === 'out_for_delivery' ? {
|
|
122
|
+
outForDeliveryAtMs: atMs
|
|
123
|
+
} : {}, to === 'delivered' ? {
|
|
124
|
+
deliveredAtMs: atMs
|
|
125
|
+
} : {}, to === 'failed' ? _extends({
|
|
126
|
+
failedAtMs: atMs
|
|
127
|
+
}, reason ? {
|
|
128
|
+
failedReason: reason
|
|
129
|
+
} : {}) : {});
|
|
130
|
+
next = {
|
|
131
|
+
localDelivery: updated
|
|
132
|
+
};
|
|
133
|
+
event = to === 'out_for_delivery' ? 'out-for-delivery' : to === 'delivered' ? 'delivered-locally' : 'delivery-failed';
|
|
134
|
+
detail = to === 'out_for_delivery' ? 'Out for delivery' : to === 'delivered' ? 'Delivered by the store' : `Delivery failed${reason ? `: ${reason}` : ''}`;
|
|
135
|
+
notify = to === 'out_for_delivery' ? 'out_for_delivery' : to === 'delivered' ? 'delivered' : null;
|
|
136
|
+
}
|
|
137
|
+
let timeline = CommerceModel.appendOrderEvent(order, event, detail, atMs);
|
|
138
|
+
let fulfillment = null;
|
|
139
|
+
if (handover) {
|
|
140
|
+
var _order_fulfillments;
|
|
141
|
+
// Every unit still open, whatever it is: the buyer has the whole order.
|
|
142
|
+
const lines = CommerceModel.orderLineFulfillmentStates(order).filter((state)=>state.remainingQuantity > 0).map((state)=>({
|
|
143
|
+
lineItemId: state.lineItemId,
|
|
144
|
+
quantity: state.remainingQuantity
|
|
145
|
+
}));
|
|
146
|
+
if (lines.length) {
|
|
147
|
+
fulfillment = {
|
|
148
|
+
id: createResourceUid(),
|
|
149
|
+
lineItemIds: lines.map((entry)=>entry.lineItemId),
|
|
150
|
+
lines,
|
|
151
|
+
status: 'active',
|
|
152
|
+
handover: isPickup ? 'pickup' : 'local_delivery',
|
|
153
|
+
// The buyer is told by this route's own message, never "shipped".
|
|
154
|
+
notify: false,
|
|
155
|
+
atMs
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const fulfillments = [
|
|
159
|
+
...(_order_fulfillments = order.fulfillments) != null ? _order_fulfillments : [],
|
|
160
|
+
...fulfillment ? [
|
|
161
|
+
fulfillment
|
|
162
|
+
] : []
|
|
163
|
+
];
|
|
164
|
+
const fulfilled = CommerceModel.statusFromFulfillments({
|
|
165
|
+
lineItems: order.lineItems,
|
|
166
|
+
fulfillments
|
|
167
|
+
});
|
|
168
|
+
const through = fulfilled === 'paid' ? 'fulfilled' : fulfilled;
|
|
169
|
+
if (order.status === 'delivered') {
|
|
170
|
+
// Marked delivered by hand already: the handover only records itself.
|
|
171
|
+
status = 'delivered';
|
|
172
|
+
} else {
|
|
173
|
+
if (through !== order.status && !CommerceModel.canTransitionOrder(order.status, through)) {
|
|
174
|
+
return {
|
|
175
|
+
outcome: 'blocked',
|
|
176
|
+
from: order.status
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
status = CommerceModel.canTransitionOrder(through, 'delivered') ? 'delivered' : through;
|
|
180
|
+
}
|
|
181
|
+
if (fulfillment) {
|
|
182
|
+
timeline = [
|
|
183
|
+
...timeline,
|
|
184
|
+
{
|
|
185
|
+
atMs,
|
|
186
|
+
event: 'fulfilled',
|
|
187
|
+
detail: isPickup ? 'Handed over at pickup' : 'Handed over on delivery'
|
|
188
|
+
}
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
if (status === 'delivered') timeline = [
|
|
192
|
+
...timeline,
|
|
193
|
+
{
|
|
194
|
+
atMs,
|
|
195
|
+
event: 'delivered'
|
|
196
|
+
}
|
|
197
|
+
];
|
|
198
|
+
next = _extends({}, next, {
|
|
199
|
+
fulfillments
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const written = _extends({}, next, {
|
|
203
|
+
status,
|
|
204
|
+
updatedAtMs: atMs,
|
|
205
|
+
timeline
|
|
206
|
+
});
|
|
207
|
+
const listFields = CommerceModel.orderLocalFulfillmentListFields(_extends({}, order, written));
|
|
208
|
+
transaction.update(orderRef, _extends({}, written, listFields != null ? listFields : {}));
|
|
209
|
+
const after = _extends({}, raw, written);
|
|
210
|
+
if (fulfillment) {
|
|
211
|
+
stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, {
|
|
212
|
+
hostId,
|
|
213
|
+
orderId,
|
|
214
|
+
key: fulfillment.id,
|
|
215
|
+
order: after,
|
|
216
|
+
extra: {
|
|
217
|
+
fulfillment: fulfillmentEventView(order, fulfillment)
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (status === 'delivered' && order.status !== 'delivered') {
|
|
222
|
+
stageOrderEvent(transaction, ORDER_DELIVERED_EVENT, {
|
|
223
|
+
hostId,
|
|
224
|
+
orderId,
|
|
225
|
+
key: 'delivered',
|
|
226
|
+
order: after
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
outcome: 'updated',
|
|
231
|
+
status,
|
|
232
|
+
notify
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function readBody(req) {
|
|
237
|
+
if (typeof req.body === 'string') {
|
|
238
|
+
try {
|
|
239
|
+
const parsed = JSON.parse(req.body || '{}');
|
|
240
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
241
|
+
} catch (unused) {
|
|
242
|
+
return {};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return req.body && typeof req.body === 'object' ? req.body : {};
|
|
246
|
+
}
|
|
247
|
+
const badId = (value)=>!value || value.includes('/') || /^__.*__$/.test(value);
|
|
248
|
+
export const localFulfillmentHandler = async (req, res)=>{
|
|
249
|
+
var _req_headers_authorization, _req_query, _body_hostId, _body_action, _body_orderId;
|
|
250
|
+
if (req.method !== 'POST' && req.method !== 'GET') {
|
|
251
|
+
return res.status(405).json({
|
|
252
|
+
error: 'Method not allowed'
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
256
|
+
if (!authorization.startsWith('Bearer ')) return res.status(401).json({
|
|
257
|
+
error: 'Unauthenticated'
|
|
258
|
+
});
|
|
259
|
+
const body = req.method === 'POST' ? readBody(req) : (_req_query = req.query) != null ? _req_query : {};
|
|
260
|
+
const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
|
|
261
|
+
if (badId(hostId)) return res.status(400).json({
|
|
262
|
+
error: 'Missing hostId'
|
|
263
|
+
});
|
|
264
|
+
const action = String((_body_action = body['action']) != null ? _body_action : '');
|
|
265
|
+
const orderId = String((_body_orderId = body['orderId']) != null ? _body_orderId : '');
|
|
266
|
+
if (req.method === 'POST' && action !== 'locate') {
|
|
267
|
+
if (!PICKUP_ACTIONS.includes(action) && !DELIVERY_ACTIONS.includes(action)) {
|
|
268
|
+
return res.status(400).json({
|
|
269
|
+
error: 'Unknown action'
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (badId(orderId)) return res.status(400).json({
|
|
273
|
+
error: 'Missing orderId'
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
let uid;
|
|
277
|
+
try {
|
|
278
|
+
uid = (await firebaseAdmin.app().auth().verifyIdToken(authorization.slice('Bearer '.length))).uid;
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (!isRefusedIdToken(error)) throw error;
|
|
281
|
+
return res.status(401).json({
|
|
282
|
+
error: 'Unauthenticated'
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
var _host_get;
|
|
287
|
+
const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId);
|
|
288
|
+
const host = await hostRef.get();
|
|
289
|
+
if (!host.exists) return res.status(404).json({
|
|
290
|
+
error: 'Unknown site'
|
|
291
|
+
});
|
|
292
|
+
const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
|
|
293
|
+
if (role !== 'admin' && role !== 'editor') return res.status(403).json({
|
|
294
|
+
error: 'Not permitted'
|
|
295
|
+
});
|
|
296
|
+
if (req.method === 'GET') {
|
|
297
|
+
// Distance zones need the store and the buyer placed on a map, which
|
|
298
|
+
// only the shipping plugin's address check can do here.
|
|
299
|
+
const quoter = pluginShippingRateQuoter();
|
|
300
|
+
const radius = Boolean((quoter == null ? void 0 : quoter.validateAddress) && await quoter.available(hostId).catch(()=>false));
|
|
301
|
+
return res.status(200).json({
|
|
302
|
+
radius
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const ownerOrg = await getOrgForHost(hostId).catch(()=>null);
|
|
306
|
+
if (!Aglyn.checkEntitlement(ownerOrg == null ? void 0 : ownerOrg.org, 'commerce')) {
|
|
307
|
+
return res.status(403).json({
|
|
308
|
+
error: 'Selling is not enabled on this plan'
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (action === 'locate') {
|
|
312
|
+
var _body_locationId;
|
|
313
|
+
const locationId = String((_body_locationId = body['locationId']) != null ? _body_locationId : '');
|
|
314
|
+
if (badId(locationId)) return res.status(400).json({
|
|
315
|
+
error: 'Choose the location deliveries leave from'
|
|
316
|
+
});
|
|
317
|
+
const location = await hostRef.collection('locations').doc(locationId).get();
|
|
318
|
+
const address = CommerceModel.normalizePostalAddress(location.get('postalAddress'));
|
|
319
|
+
if (!location.exists || !(address == null ? void 0 : address.line1) || !address.postalCode || !address.country) {
|
|
320
|
+
return res.status(409).json({
|
|
321
|
+
error: 'Add a full street address to that location first.'
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const coordinates = await placeDeliveryAddress(hostId, _extends({}, address, {
|
|
325
|
+
country: address.country
|
|
326
|
+
}));
|
|
327
|
+
if (!coordinates) {
|
|
328
|
+
return res.status(409).json({
|
|
329
|
+
error: 'Your address check could not place this address on a map. Use postal-code zones instead.'
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
return res.status(200).json({
|
|
333
|
+
ok: true,
|
|
334
|
+
origin: coordinates
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const outcome = await moveLocalFulfillment({
|
|
338
|
+
hostId,
|
|
339
|
+
orderId,
|
|
340
|
+
action: action,
|
|
341
|
+
pickedUpBy: typeof body['pickedUpBy'] === 'string' ? body['pickedUpBy'] : undefined,
|
|
342
|
+
reason: typeof body['reason'] === 'string' ? body['reason'] : undefined
|
|
343
|
+
});
|
|
344
|
+
if (outcome.outcome === 'no_such_order') return res.status(404).json({
|
|
345
|
+
error: 'Unknown order'
|
|
346
|
+
});
|
|
347
|
+
if (outcome.outcome === 'wrong_method') {
|
|
348
|
+
return res.status(409).json({
|
|
349
|
+
error: PICKUP_ACTIONS.includes(action) ? 'This order is not a pickup order' : 'This order is not a local delivery order'
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (outcome.outcome === 'blocked') {
|
|
353
|
+
return res.status(409).json({
|
|
354
|
+
error: `This order can’t move on from "${outcome.from}"`
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
if (outcome.outcome === 'already') return res.status(200).json({
|
|
358
|
+
ok: true,
|
|
359
|
+
already: true
|
|
360
|
+
});
|
|
361
|
+
// After the write, never inside it, and never failing it (AGL-3610).
|
|
362
|
+
if (outcome.notify) await notifyOrderBuyer({
|
|
363
|
+
hostId,
|
|
364
|
+
orderId
|
|
365
|
+
}, outcome.notify);
|
|
366
|
+
return res.status(200).json({
|
|
367
|
+
ok: true,
|
|
368
|
+
status: outcome.status
|
|
369
|
+
});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
// The transaction is all-or-nothing: nothing landed, a retry is safe.
|
|
372
|
+
console.error('[local-fulfillment] move failed', hostId, orderId, action, error);
|
|
373
|
+
return res.status(500).json({
|
|
374
|
+
error: 'The order could not be updated'
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
//# sourceMappingURL=local-fulfillment-status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/local-fulfillment-status.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { pluginShippingRateQuoter } from '@aglyn/aglyn/plugin-manager/plugin-shipping-rates'\nimport { createResourceUid, type PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport * as CommerceModel from '../model'\nimport { ORDER_DELIVERED_EVENT, ORDER_FULFILLED_EVENT } from '../model/order-events'\nimport { notifyOrderBuyer, type OrderBuyerEvent } from './order-notifications'\nimport { fulfillmentEventView, stageOrderEvent } from './order-events'\nimport { placeDeliveryAddress } from './local-fulfillment'\n\n/**\n * `commerce/local-fulfillment` (AGL-3624): the store moving a pickup or a\n * local delivery along — from the console's queue, the order dialog, or the\n * native Aglyn app, which posts the same body with the member's ID token.\n *\n * POST { hostId, orderId, action }\n * pickup: `ready` · `preparing` (undo a ready) · `picked_up`\n * delivery: `out_for_delivery` · `delivered` · `delivery_failed`\n * POST { hostId, action: 'locate' } places the store on the map for\n * distance zones, through the shipping plugin's address check\n * GET ?hostId whether distance zones can work here\n *\n * Every status write is ONE transaction that re-reads the order and re-asks\n * the transition, so a stale queue row or a second device cannot skip a\n * step or hand the same order over twice: a repeat of the move the order\n * already made answers `already` and writes nothing.\n *\n * HANDING OVER IS FULFILLING. `picked_up` and `delivered` record a\n * fulfillment covering every unit still open (marked `handover`, so no\n * \"shipped\" message and no carrier), move the order to `fulfilled` and on to\n * `delivered`, and raise `order.fulfilled` and `order.delivered` exactly as a\n * shipment does — accounting, outbound webhooks and workflows hear the same\n * events whichever way the goods left. No stock moves: it came off the\n * location's shelf when the money landed.\n *\n * The buyer hears about each step through the AGL-3610 notifier, once per\n * step, and only after the write lands.\n *\n * Role gate: `admin` and `editor`, as `fulfill-order.ts`.\n */\n\ntype PickupAction = 'ready' | 'preparing' | 'picked_up'\ntype DeliveryAction = 'out_for_delivery' | 'delivered' | 'delivery_failed'\nexport type LocalFulfillmentAction = PickupAction | DeliveryAction\n\nconst PICKUP_ACTIONS: readonly string[] = ['ready', 'preparing', 'picked_up']\nconst DELIVERY_ACTIONS: readonly string[] = ['out_for_delivery', 'delivered', 'delivery_failed']\n\nexport type LocalFulfillmentOutcome =\n | { outcome: 'updated'; status: CommerceModel.OrderStatus; notify: OrderBuyerEvent | null }\n | { outcome: 'already' }\n | { outcome: 'no_such_order' }\n /** The order is not a pickup (or not a local delivery) order. */\n | { outcome: 'wrong_method' }\n /** The step is not allowed from where the order is; `from` names it. */\n | { outcome: 'blocked'; from: string }\n\nconst NOTE_MAX = 200\n\n/**\n * Moves one order a step. The transaction, with NO authorization — the\n * caller has already proven the member may.\n */\nexport async function moveLocalFulfillment(request: {\n hostId: string\n orderId: string\n action: LocalFulfillmentAction\n /** `picked_up`: who collected it, when the counter noted a name. */\n pickedUpBy?: string\n /** `delivery_failed`: why. */\n reason?: string\n nowMs?: number\n}): Promise<LocalFulfillmentOutcome> {\n const { hostId, orderId, action } = request\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = firestore.collection('hosts').doc(hostId).collection('orders').doc(orderId)\n return firestore.runTransaction(async (transaction): Promise<LocalFulfillmentOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_order' }\n const raw = (snapshot.data() ?? {}) as Record<string, unknown>\n const order = CommerceModel.liftLegacyOrder(raw as never)\n const atMs = request.nowMs ?? Date.now()\n const isPickup = PICKUP_ACTIONS.includes(action)\n if (isPickup ? order.fulfillmentMethod !== 'pickup' || !order.pickup : order.fulfillmentMethod !== 'local_delivery' || !order.localDelivery) {\n return { outcome: 'wrong_method' }\n }\n // Nothing to hand over on an order that was cancelled or refunded.\n if (order.status === 'cancelled' || order.status === 'refunded' || order.status === 'pending') {\n return { outcome: 'blocked', from: order.status }\n }\n\n const handover = action === 'picked_up' || action === 'delivered'\n let next: Record<string, unknown>\n let detail: string\n let event: string\n let notify: OrderBuyerEvent | null\n let status: CommerceModel.OrderStatus = order.status\n\n if (isPickup) {\n const pickup = order.pickup as CommerceModel.OrderPickup\n const from = CommerceModel.orderPickupStatus(pickup.status)\n const to: CommerceModel.OrderPickupStatus = action as PickupAction\n if (from === to) return { outcome: 'already' }\n if (!CommerceModel.canTransitionPickup(from, to)) return { outcome: 'blocked', from: CommerceModel.ORDER_PICKUP_STATUS_LABELS[from] }\n const pickedUpBy = String(request.pickedUpBy ?? '').trim().slice(0, NOTE_MAX)\n const updated: CommerceModel.OrderPickup = {\n ...pickup,\n status: to,\n updatedAtMs: atMs,\n ...(to === 'ready' ? { readyAtMs: atMs } : {}),\n ...(to === 'picked_up' ? { pickedUpAtMs: atMs, ...(pickedUpBy ? { pickedUpBy } : {}) } : {}),\n }\n if (to === 'preparing') delete updated.readyAtMs\n next = { pickup: updated }\n event = to === 'ready' ? 'ready-for-pickup' : to === 'picked_up' ? 'picked-up' : 'pickup-preparing'\n detail =\n to === 'ready'\n ? `Ready for pickup at ${pickup.locationName}`\n : to === 'picked_up'\n ? `Picked up at ${pickup.locationName}${pickedUpBy ? ` by ${pickedUpBy}` : ''}`\n : 'Back to preparing'\n notify = to === 'ready' ? 'ready_for_pickup' : to === 'picked_up' ? 'picked_up' : null\n } else {\n const delivery = order.localDelivery as CommerceModel.OrderLocalDelivery\n const from = CommerceModel.orderLocalDeliveryStatus(delivery.status)\n const to: CommerceModel.OrderLocalDeliveryStatus =\n action === 'delivery_failed' ? 'failed' : (action as 'out_for_delivery' | 'delivered')\n if (from === to) return { outcome: 'already' }\n if (!CommerceModel.canTransitionLocalDelivery(from, to)) {\n return { outcome: 'blocked', from: CommerceModel.ORDER_LOCAL_DELIVERY_STATUS_LABELS[from] }\n }\n const reason = String(request.reason ?? '').trim().slice(0, NOTE_MAX)\n const updated: CommerceModel.OrderLocalDelivery = {\n ...delivery,\n status: to,\n updatedAtMs: atMs,\n ...(to === 'out_for_delivery' ? { outForDeliveryAtMs: atMs } : {}),\n ...(to === 'delivered' ? { deliveredAtMs: atMs } : {}),\n ...(to === 'failed' ? { failedAtMs: atMs, ...(reason ? { failedReason: reason } : {}) } : {}),\n }\n next = { localDelivery: updated }\n event = to === 'out_for_delivery' ? 'out-for-delivery' : to === 'delivered' ? 'delivered-locally' : 'delivery-failed'\n detail =\n to === 'out_for_delivery'\n ? 'Out for delivery'\n : to === 'delivered'\n ? 'Delivered by the store'\n : `Delivery failed${reason ? `: ${reason}` : ''}`\n notify = to === 'out_for_delivery' ? 'out_for_delivery' : to === 'delivered' ? 'delivered' : null\n }\n\n let timeline = CommerceModel.appendOrderEvent(order, event, detail, atMs)\n let fulfillment: CommerceModel.OrderFulfillment | null = null\n if (handover) {\n // Every unit still open, whatever it is: the buyer has the whole order.\n const lines = CommerceModel.orderLineFulfillmentStates(order)\n .filter((state) => state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n if (lines.length) {\n fulfillment = {\n id: createResourceUid(),\n lineItemIds: lines.map((entry) => entry.lineItemId),\n lines,\n status: 'active',\n handover: isPickup ? 'pickup' : 'local_delivery',\n // The buyer is told by this route's own message, never \"shipped\".\n notify: false,\n atMs,\n }\n }\n const fulfillments = [...(order.fulfillments ?? []), ...(fulfillment ? [fulfillment] : [])]\n const fulfilled = CommerceModel.statusFromFulfillments({ lineItems: order.lineItems, fulfillments })\n const through = fulfilled === 'paid' ? 'fulfilled' : fulfilled\n if (order.status === 'delivered') {\n // Marked delivered by hand already: the handover only records itself.\n status = 'delivered'\n } else {\n if (through !== order.status && !CommerceModel.canTransitionOrder(order.status, through)) {\n return { outcome: 'blocked', from: order.status }\n }\n status = CommerceModel.canTransitionOrder(through, 'delivered') ? 'delivered' : through\n }\n if (fulfillment) {\n timeline = [...timeline, { atMs, event: 'fulfilled', detail: isPickup ? 'Handed over at pickup' : 'Handed over on delivery' }]\n }\n if (status === 'delivered') timeline = [...timeline, { atMs, event: 'delivered' }]\n next = { ...next, fulfillments }\n }\n const written = {\n ...next,\n status,\n updatedAtMs: atMs,\n timeline,\n }\n const listFields = CommerceModel.orderLocalFulfillmentListFields({\n ...order,\n ...(written as Partial<CommerceModel.HostOrder>),\n })\n transaction.update(orderRef, { ...written, ...(listFields ?? {}) })\n const after = { ...raw, ...written }\n if (fulfillment) {\n stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, {\n hostId,\n orderId,\n key: fulfillment.id,\n order: after,\n extra: { fulfillment: fulfillmentEventView(order, fulfillment) },\n })\n }\n if (status === 'delivered' && order.status !== 'delivered') {\n stageOrderEvent(transaction, ORDER_DELIVERED_EVENT, { hostId, orderId, key: 'delivered', order: after })\n }\n return { outcome: 'updated', status, notify }\n })\n}\n\nfunction readBody(req: Parameters<PluginApiHandler>[0]): Record<string, unknown> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body || '{}')\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? (req.body as Record<string, unknown>) : {}\n}\n\nconst badId = (value: string) => !value || value.includes('/') || /^__.*__$/.test(value)\n\nexport const localFulfillmentHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST' && req.method !== 'GET') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n if (!authorization.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthenticated' })\n const body = req.method === 'POST' ? readBody(req) : ((req.query ?? {}) as Record<string, unknown>)\n const hostId = String(body['hostId'] ?? '')\n if (badId(hostId)) return res.status(400).json({ error: 'Missing hostId' })\n const action = String(body['action'] ?? '')\n const orderId = String(body['orderId'] ?? '')\n if (req.method === 'POST' && action !== 'locate') {\n if (!PICKUP_ACTIONS.includes(action) && !DELIVERY_ACTIONS.includes(action)) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n if (badId(orderId)) return res.status(400).json({ error: 'Missing orderId' })\n }\n\n let uid: string\n try {\n uid = (await firebaseAdmin.app().auth().verifyIdToken(authorization.slice('Bearer '.length))).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return res.status(401).json({ error: 'Unauthenticated' })\n }\n try {\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n const host = await hostRef.get()\n if (!host.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (host.get('memberRoles') ?? {})[uid]\n if (role !== 'admin' && role !== 'editor') return res.status(403).json({ error: 'Not permitted' })\n\n if (req.method === 'GET') {\n // Distance zones need the store and the buyer placed on a map, which\n // only the shipping plugin's address check can do here.\n const quoter = pluginShippingRateQuoter()\n const radius = Boolean(\n quoter?.validateAddress && (await quoter.available(hostId).catch(() => false)),\n )\n return res.status(200).json({ radius })\n }\n\n const ownerOrg = await getOrgForHost(hostId).catch(() => null)\n if (!Aglyn.checkEntitlement(ownerOrg?.org as any, 'commerce')) {\n return res.status(403).json({ error: 'Selling is not enabled on this plan' })\n }\n\n if (action === 'locate') {\n const locationId = String(body['locationId'] ?? '')\n if (badId(locationId)) return res.status(400).json({ error: 'Choose the location deliveries leave from' })\n const location = await hostRef.collection('locations').doc(locationId).get()\n const address = CommerceModel.normalizePostalAddress(location.get('postalAddress'))\n if (!location.exists || !address?.line1 || !address.postalCode || !address.country) {\n return res.status(409).json({ error: 'Add a full street address to that location first.' })\n }\n const coordinates = await placeDeliveryAddress(hostId, { ...address, country: address.country })\n if (!coordinates) {\n return res.status(409).json({\n error: 'Your address check could not place this address on a map. Use postal-code zones instead.',\n })\n }\n return res.status(200).json({ ok: true, origin: coordinates })\n }\n\n const outcome = await moveLocalFulfillment({\n hostId,\n orderId,\n action: action as LocalFulfillmentAction,\n pickedUpBy: typeof body['pickedUpBy'] === 'string' ? body['pickedUpBy'] : undefined,\n reason: typeof body['reason'] === 'string' ? body['reason'] : undefined,\n })\n if (outcome.outcome === 'no_such_order') return res.status(404).json({ error: 'Unknown order' })\n if (outcome.outcome === 'wrong_method') {\n return res.status(409).json({\n error: PICKUP_ACTIONS.includes(action)\n ? 'This order is not a pickup order'\n : 'This order is not a local delivery order',\n })\n }\n if (outcome.outcome === 'blocked') {\n return res.status(409).json({ error: `This order can’t move on from \"${outcome.from}\"` })\n }\n if (outcome.outcome === 'already') return res.status(200).json({ ok: true, already: true })\n // After the write, never inside it, and never failing it (AGL-3610).\n if (outcome.notify) await notifyOrderBuyer({ hostId, orderId }, outcome.notify)\n return res.status(200).json({ ok: true, status: outcome.status })\n } catch (error) {\n // The transaction is all-or-nothing: nothing landed, a retry is safe.\n console.error('[local-fulfillment] move failed', hostId, orderId, action, error)\n return res.status(500).json({ error: 'The order could not be updated' })\n }\n}\n"],"names":["pluginShippingRateQuoter","createResourceUid","Aglyn","firebaseAdmin","getOrgForHost","isRefusedIdToken","CommerceModel","ORDER_DELIVERED_EVENT","ORDER_FULFILLED_EVENT","notifyOrderBuyer","fulfillmentEventView","stageOrderEvent","placeDeliveryAddress","PICKUP_ACTIONS","DELIVERY_ACTIONS","NOTE_MAX","moveLocalFulfillment","request","hostId","orderId","action","firestore","app","orderRef","collection","doc","runTransaction","transaction","snapshot","get","exists","outcome","raw","data","order","liftLegacyOrder","atMs","nowMs","Date","now","isPickup","includes","fulfillmentMethod","pickup","localDelivery","status","from","handover","next","detail","event","notify","orderPickupStatus","to","canTransitionPickup","ORDER_PICKUP_STATUS_LABELS","pickedUpBy","String","trim","slice","updated","updatedAtMs","readyAtMs","pickedUpAtMs","locationName","delivery","orderLocalDeliveryStatus","canTransitionLocalDelivery","ORDER_LOCAL_DELIVERY_STATUS_LABELS","reason","outForDeliveryAtMs","deliveredAtMs","failedAtMs","failedReason","timeline","appendOrderEvent","fulfillment","lines","orderLineFulfillmentStates","filter","state","remainingQuantity","map","lineItemId","quantity","length","id","lineItemIds","entry","fulfillments","fulfilled","statusFromFulfillments","lineItems","through","canTransitionOrder","written","listFields","orderLocalFulfillmentListFields","update","after","key","extra","readBody","req","body","parsed","JSON","parse","badId","value","test","localFulfillmentHandler","res","method","json","error","authorization","headers","startsWith","query","uid","auth","verifyIdToken","host","hostRef","role","quoter","radius","Boolean","validateAddress","available","catch","ownerOrg","checkEntitlement","org","locationId","location","address","normalizePostalAddress","line1","postalCode","country","coordinates","ok","origin","undefined","already","console"],"mappings":"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"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { PluginApiHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
import * as CommerceModel from '../model';
|
|
19
|
+
/**
|
|
20
|
+
* Buy online, pick up in store, and local delivery at checkout (AGL-3624).
|
|
21
|
+
*
|
|
22
|
+
* The buyer chooses at the CART, before payment — the only place the store
|
|
23
|
+
* still controls what is offered: a Stripe Checkout Session prices shipping
|
|
24
|
+
* from the options it is created with and asks nothing back. So the cart
|
|
25
|
+
* declares one of three ways, and this module turns the declaration into the
|
|
26
|
+
* session it needs, or refuses it in words the cart can show:
|
|
27
|
+
*
|
|
28
|
+
* - `pickup` at one location: no shipping charged, no address collected,
|
|
29
|
+
* the stock reserved at that location, and the order routed there.
|
|
30
|
+
* - `local_delivery`: the store's OWN driver (not a marketplace courier —
|
|
31
|
+
* that is the delivery-apps plugin, which never reads this). The postal
|
|
32
|
+
* code — or, for a distance zone, the address — picks a zone, the zone
|
|
33
|
+
* prices the fee and enforces its minimum, and the buyer books a window.
|
|
34
|
+
* The session carries the fee as its one shipping option and collects an
|
|
35
|
+
* address only in the store's country.
|
|
36
|
+
* - `shipping`: everything as it was.
|
|
37
|
+
*
|
|
38
|
+
* A DECLARATION IS A REQUEST, NEVER A PRICE. The cart sends a location id, a
|
|
39
|
+
* postal code and a window; the fee, the minimum and whether the window is
|
|
40
|
+
* still open are all decided here from the store's own settings, on every
|
|
41
|
+
* attempt.
|
|
42
|
+
*
|
|
43
|
+
* DISTANCE ZONES need both ends on a map, and the repository has no geocoder
|
|
44
|
+
* of its own: the destination is placed by the shipping plugin's address
|
|
45
|
+
* check (core's `core.shipping-rate-quoter` seam) when its provider returns a
|
|
46
|
+
* position. Where none does, a distance zone matches nothing and the buyer is
|
|
47
|
+
* told the store does not deliver there; postal-code zones need nothing.
|
|
48
|
+
*/
|
|
49
|
+
/** What the cart declares. */
|
|
50
|
+
export interface LocalFulfillmentRequest {
|
|
51
|
+
method: CommerceModel.OrderFulfillmentMethod;
|
|
52
|
+
locationId?: string;
|
|
53
|
+
postalCode?: string;
|
|
54
|
+
windowStartMs?: number;
|
|
55
|
+
/** The street address, asked only when the store has distance zones. */
|
|
56
|
+
address?: {
|
|
57
|
+
line1?: string;
|
|
58
|
+
line2?: string;
|
|
59
|
+
city?: string;
|
|
60
|
+
state?: string;
|
|
61
|
+
postalCode?: string;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* The mobile number the buyer gave AT THE CART for texts about this pickup
|
|
65
|
+
* or delivery (AGL-3624), in E.164. The only phone an online order's texts
|
|
66
|
+
* go to: asked for that purpose, never taken from an address.
|
|
67
|
+
*/
|
|
68
|
+
textPhone?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Reads the cart's declaration; `null` when it made none (a shipped order). */
|
|
71
|
+
export declare function readLocalFulfillmentRequest(value: unknown): LocalFulfillmentRequest | null;
|
|
72
|
+
/** The store's pickup locations and delivery settings, read once per attempt. */
|
|
73
|
+
export interface LocalFulfillmentStore {
|
|
74
|
+
pickupLocations: CommerceModel.PickupLocationOption[];
|
|
75
|
+
delivery: CommerceModel.LocalDeliverySettings;
|
|
76
|
+
timeZone: string;
|
|
77
|
+
}
|
|
78
|
+
/** Reads what checkout and the cart need. Never throws: a failed read offers nothing. */
|
|
79
|
+
export declare function readLocalFulfillmentStore(input: {
|
|
80
|
+
hostRef: any;
|
|
81
|
+
/** `settings/store` as already read, to save a read. */
|
|
82
|
+
storeSettings?: Record<string, unknown> | null;
|
|
83
|
+
org?: {
|
|
84
|
+
timeZone?: string;
|
|
85
|
+
} | null;
|
|
86
|
+
host?: {
|
|
87
|
+
timeZone?: string;
|
|
88
|
+
} | null;
|
|
89
|
+
}): Promise<LocalFulfillmentStore>;
|
|
90
|
+
/** How long checkout waits for an address to be placed on a map. */
|
|
91
|
+
export declare const LOCAL_DELIVERY_GEOCODE_TIMEOUT_MS = 3000;
|
|
92
|
+
/**
|
|
93
|
+
* Places an address on the map through the shipping plugin's address check,
|
|
94
|
+
* or answers `null`: no plugin, not available on this site, no position in
|
|
95
|
+
* the answer, or slower than the timeout. Never throws.
|
|
96
|
+
*/
|
|
97
|
+
export declare function placeDeliveryAddress(hostId: string, address: {
|
|
98
|
+
line1?: string;
|
|
99
|
+
line2?: string;
|
|
100
|
+
city?: string;
|
|
101
|
+
state?: string;
|
|
102
|
+
postalCode?: string;
|
|
103
|
+
country: string;
|
|
104
|
+
}): Promise<CommerceModel.LocalDeliveryCoordinates | null>;
|
|
105
|
+
/** The shipping option id a local delivery fee is declared under. */
|
|
106
|
+
export declare const LOCAL_DELIVERY_RATE_ID = "local-delivery";
|
|
107
|
+
export type LocalFulfillmentPlan = {
|
|
108
|
+
kind: 'shipping';
|
|
109
|
+
} | {
|
|
110
|
+
kind: 'refusal';
|
|
111
|
+
status: 400 | 409;
|
|
112
|
+
error: string;
|
|
113
|
+
/** What the cart should re-ask: the options changed under the buyer. */
|
|
114
|
+
changed?: 'pickup' | 'delivery';
|
|
115
|
+
} | {
|
|
116
|
+
kind: 'pickup';
|
|
117
|
+
location: CommerceModel.PickupLocationOption;
|
|
118
|
+
metadata: Record<string, string>;
|
|
119
|
+
} | {
|
|
120
|
+
kind: 'local_delivery';
|
|
121
|
+
zone: CommerceModel.LocalDeliveryZone;
|
|
122
|
+
window: CommerceModel.LocalDeliveryWindow;
|
|
123
|
+
feeCents: number;
|
|
124
|
+
country: string;
|
|
125
|
+
locationId?: string;
|
|
126
|
+
option: CommerceModel.ResolvedShippingRate;
|
|
127
|
+
metadata: Record<string, string>;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Turns the cart's declaration into what the session needs, or a refusal.
|
|
131
|
+
* Taken BEFORE any reservation or Stripe call, so a refusal costs nothing
|
|
132
|
+
* and the buyer's same button works once they choose again.
|
|
133
|
+
*/
|
|
134
|
+
export declare function planLocalFulfillment(input: {
|
|
135
|
+
hostId: string;
|
|
136
|
+
request: LocalFulfillmentRequest | null;
|
|
137
|
+
/** The goods, pre-discount — what a minimum and a free-over threshold read. */
|
|
138
|
+
itemsCents: number;
|
|
139
|
+
hasPhysicalLine: boolean;
|
|
140
|
+
store: LocalFulfillmentStore;
|
|
141
|
+
nowMs?: number;
|
|
142
|
+
}): Promise<LocalFulfillmentPlan>;
|
|
143
|
+
/** Sets a plan's metadata on a session's form body. */
|
|
144
|
+
export declare function appendLocalFulfillmentMetadata(params: URLSearchParams, metadata: Record<string, string>): void;
|
|
145
|
+
/** The location a plan takes its stock from, if any. */
|
|
146
|
+
export declare function localFulfillmentLocationId(plan: LocalFulfillmentPlan): string | undefined;
|
|
147
|
+
/** A stock refusal for a plan that reserves at one location (AGL-3624). */
|
|
148
|
+
export declare function localStockRefusalMessage(plan: LocalFulfillmentPlan, productName: string): string;
|
|
149
|
+
/** What a paid session's declaration becomes on its order. */
|
|
150
|
+
export interface OrderLocalFulfillment {
|
|
151
|
+
fields: {
|
|
152
|
+
fulfillmentMethod: CommerceModel.OrderFulfillmentMethod;
|
|
153
|
+
pickup?: CommerceModel.OrderPickup;
|
|
154
|
+
localDelivery?: CommerceModel.OrderLocalDelivery;
|
|
155
|
+
locationId?: string;
|
|
156
|
+
/** The number the buyer asked to be texted at, at the cart. */
|
|
157
|
+
customerPhone?: string;
|
|
158
|
+
} & CommerceModel.OrderLocalFulfillmentListFields;
|
|
159
|
+
/** The line the receipt adds, so the buyer knows where and when. */
|
|
160
|
+
summary: string;
|
|
161
|
+
/** The address entered at payment is outside the zone the fee was charged for. */
|
|
162
|
+
outsideZone: boolean;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Reads a paid session's `fulfillment` metadata into order fields, or `null`
|
|
166
|
+
* for a shipped order. The location and zone are re-read so the order keeps
|
|
167
|
+
* their names as they were when the buyer paid.
|
|
168
|
+
*/
|
|
169
|
+
export declare function orderLocalFulfillmentFromSession(input: {
|
|
170
|
+
hostId: string;
|
|
171
|
+
hostRef: any;
|
|
172
|
+
metadata: Record<string, unknown> | null | undefined;
|
|
173
|
+
/** The address the buyer entered at payment, when the session collected one. */
|
|
174
|
+
shippingAddress?: {
|
|
175
|
+
postalCode?: string | null;
|
|
176
|
+
country?: string | null;
|
|
177
|
+
} | null;
|
|
178
|
+
createdAtMs: number;
|
|
179
|
+
}): Promise<OrderLocalFulfillment | null>;
|
|
180
|
+
/**
|
|
181
|
+
* `POST commerce/local-fulfillment-options` (AGL-3624): what the cart offers
|
|
182
|
+
* besides shipping — the pickup locations and, with a postal code, the
|
|
183
|
+
* delivery zone's fee, minimum and the windows open to book.
|
|
184
|
+
*
|
|
185
|
+
* Public, because a shopper asks it; it returns only what a storefront would
|
|
186
|
+
* print anyway (location names, addresses, hours, fees). It reads the
|
|
187
|
+
* visitor's own cart through the cart cookie and offers nothing for a
|
|
188
|
+
* basket with nothing physical in it.
|
|
189
|
+
*/
|
|
190
|
+
export declare const localFulfillmentOptionsHandler: PluginApiHandler;
|