@aglyn/plugins-commerce 1.0.0-beta.278 → 1.0.0-beta.282
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 +17 -14
- package/src/lib/components/commerce-console-page.js +33 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/payments-settings-card.component.js +1 -1
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-hardware-api.d.ts +122 -0
- package/src/lib/components/console/pos/pos-hardware-api.js +90 -0
- package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.d.ts +58 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js +101 -0
- package/src/lib/components/console/pos/pos-reader-splash-api.js.map +1 -0
- package/src/lib/components/console/pos-devices-card.component.js +5 -0
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-hardware-card.component.d.ts +26 -0
- package/src/lib/components/console/pos-hardware-card.component.js +456 -0
- package/src/lib/components/console/pos-hardware-card.component.js.map +1 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +519 -0
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.d.ts +27 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +444 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.d.ts +9 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js +37 -0
- package/src/lib/components/console/pos-ops/pos-check-in-zone.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +24 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +346 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.d.ts +5 -1
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js +83 -4
- package/src/lib/components/console/pos-ops/pos-customer-lookup.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.d.ts +35 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js +546 -0
- package/src/lib/components/console/pos-ops/pos-day-totals-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +2 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +20 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.d.ts +1 -1
- package/src/lib/components/console/pos-ops/pos-ops-api.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +6 -3
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-reader-splash-setting.component.d.ts +22 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js +275 -0
- package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.d.ts +14 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js +301 -0
- package/src/lib/components/console/pos-reader-splash-staff-page.component.js.map +1 -0
- package/src/lib/components/console/pos-zones.d.ts +37 -3
- package/src/lib/components/console/pos-zones.js +11 -1
- package/src/lib/components/console/pos-zones.js.map +1 -1
- package/src/lib/components/console/product-check-in-fields.component.d.ts +20 -0
- package/src/lib/components/console/product-check-in-fields.component.js +172 -0
- package/src/lib/components/console/product-check-in-fields.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +48 -9
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-register-fields.component.d.ts +12 -0
- package/src/lib/components/console/product-register-fields.component.js +39 -1
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +21 -6
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/reservations-card.component.js +239 -70
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/reservation-widget.js +16 -0
- package/src/lib/components/reservation-widget.js.map +1 -1
- package/src/lib/constants/operator-alerts.d.ts +6 -0
- package/src/lib/constants/operator-alerts.js +20 -1
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/declarations.console-server.js +7 -0
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +26 -0
- package/src/lib/declarations.js +34 -0
- package/src/lib/declarations.js.map +1 -0
- package/src/lib/model/commerce-check-in-product.d.ts +37 -0
- package/src/lib/model/commerce-check-in-product.js +49 -0
- package/src/lib/model/commerce-check-in-product.js.map +1 -0
- package/src/lib/model/commerce-check-in.d.ts +187 -0
- package/src/lib/model/commerce-check-in.js +468 -0
- package/src/lib/model/commerce-check-in.js.map +1 -0
- package/src/lib/model/commerce-pos-day-report.d.ts +209 -0
- package/src/lib/model/commerce-pos-day-report.js +572 -0
- package/src/lib/model/commerce-pos-day-report.js.map +1 -0
- package/src/lib/model/commerce-pos-offline.d.ts +65 -3
- package/src/lib/model/commerce-pos-offline.js +104 -7
- package/src/lib/model/commerce-pos-offline.js.map +1 -1
- package/src/lib/model/commerce-pos-ops.d.ts +3 -0
- package/src/lib/model/commerce-pos-ops.js +1 -0
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +43 -3
- package/src/lib/model/commerce-pos.js +73 -5
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-reservations.d.ts +73 -7
- package/src/lib/model/commerce-reservations.js +90 -10
- package/src/lib/model/commerce-reservations.js.map +1 -1
- package/src/lib/model/commerce.d.ts +40 -0
- package/src/lib/model/commerce.js +22 -4
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/plugin-permissions.d.ts +1 -1
- package/src/lib/model/plugin-permissions.js +2 -1
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/model/pos-hardware.d.ts +288 -0
- package/src/lib/model/pos-hardware.js +499 -0
- package/src/lib/model/pos-hardware.js.map +1 -0
- package/src/lib/model/pos-offline-card-limits.d.ts +20 -0
- package/src/lib/model/pos-offline-card-limits.js +28 -0
- package/src/lib/model/pos-offline-card-limits.js.map +1 -0
- package/src/lib/model/pos-reader-splash.d.ts +114 -0
- package/src/lib/model/pos-reader-splash.js +141 -0
- package/src/lib/model/pos-reader-splash.js.map +1 -0
- package/src/lib/model/product-write.d.ts +6 -0
- package/src/lib/plugin-config.d.ts +10 -0
- package/src/lib/plugin-config.js +40 -0
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +28 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/cart-checkout.js +3 -1
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.js +12 -1
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/catalog.js +49 -12
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout-extras.js +1 -1
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +7 -0
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/person-eraser.d.ts +8 -1
- package/src/lib/server/person-eraser.js +23 -1
- package/src/lib/server/person-eraser.js.map +1 -1
- package/src/lib/server/pos-check-in.d.ts +49 -0
- package/src/lib/server/pos-check-in.js +652 -0
- package/src/lib/server/pos-check-in.js.map +1 -0
- package/src/lib/server/pos-day-report.d.ts +46 -0
- package/src/lib/server/pos-day-report.js +299 -0
- package/src/lib/server/pos-day-report.js.map +1 -0
- package/src/lib/server/pos-hardware-admin.d.ts +50 -0
- package/src/lib/server/pos-hardware-admin.js +366 -0
- package/src/lib/server/pos-hardware-admin.js.map +1 -0
- package/src/lib/server/pos-hardware-notify.d.ts +27 -0
- package/src/lib/server/pos-hardware-notify.js +142 -0
- package/src/lib/server/pos-hardware-notify.js.map +1 -0
- package/src/lib/server/pos-hardware-user-eraser.d.ts +26 -0
- package/src/lib/server/pos-hardware-user-eraser.js +67 -0
- package/src/lib/server/pos-hardware-user-eraser.js.map +1 -0
- package/src/lib/server/pos-hardware-webhook.d.ts +31 -0
- package/src/lib/server/pos-hardware-webhook.js +176 -0
- package/src/lib/server/pos-hardware-webhook.js.map +1 -0
- package/src/lib/server/pos-hardware.d.ts +58 -0
- package/src/lib/server/pos-hardware.js +234 -0
- package/src/lib/server/pos-hardware.js.map +1 -0
- package/src/lib/server/pos-offline-card-reconcile.d.ts +38 -0
- package/src/lib/server/pos-offline-card-reconcile.js +58 -0
- package/src/lib/server/pos-offline-card-reconcile.js.map +1 -0
- package/src/lib/server/pos-offline-sync.d.ts +32 -0
- package/src/lib/server/pos-offline-sync.js +191 -15
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-ops-routes.d.ts +4 -2
- package/src/lib/server/pos-ops-routes.js +14 -2
- package/src/lib/server/pos-ops-routes.js.map +1 -1
- package/src/lib/server/pos-order.js +39 -51
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.js +8 -4
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-reader-splash-admin.d.ts +36 -0
- package/src/lib/server/pos-reader-splash-admin.js +295 -0
- package/src/lib/server/pos-reader-splash-admin.js.map +1 -0
- package/src/lib/server/pos-reader-splash-render.d.ts +40 -0
- package/src/lib/server/pos-reader-splash-render.js +138 -0
- package/src/lib/server/pos-reader-splash-render.js.map +1 -0
- package/src/lib/server/pos-reader-splash-server.d.ts +32 -0
- package/src/lib/server/pos-reader-splash-server.js +32 -0
- package/src/lib/server/pos-reader-splash-server.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +105 -0
- package/src/lib/server/pos-reader-splash.js +548 -0
- package/src/lib/server/pos-reader-splash.js.map +1 -0
- package/src/lib/server/pos-sale.d.ts +2 -1
- package/src/lib/server/pos-sale.js +3 -1
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-terminal.d.ts +67 -4
- package/src/lib/server/pos-terminal.js +239 -32
- package/src/lib/server/pos-terminal.js.map +1 -1
- package/src/lib/server/process-restock.js +4 -0
- package/src/lib/server/process-restock.js.map +1 -1
- package/src/lib/server/product-card.js +2 -2
- package/src/lib/server/product-card.js.map +1 -1
- package/src/lib/server/product-catalog.d.ts +2 -0
- package/src/lib/server/product-catalog.js +11 -4
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product.js +3 -1
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/related.js +8 -1
- package/src/lib/server/related.js.map +1 -1
- package/src/lib/server/reservation-availability-query.d.ts +35 -0
- package/src/lib/server/reservation-availability-query.js +34 -0
- package/src/lib/server/reservation-availability-query.js.map +1 -0
- package/src/lib/server/reservation-availability.d.ts +21 -0
- package/src/lib/server/reservation-availability.js +64 -10
- package/src/lib/server/reservation-availability.js.map +1 -1
- package/src/lib/server/reservation-walk-in.d.ts +28 -0
- package/src/lib/server/reservation-walk-in.js +166 -0
- package/src/lib/server/reservation-walk-in.js.map +1 -0
- package/src/lib/server/reserve.js +13 -10
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/reviews.js +3 -1
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +5 -2
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +26 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/fake-firestore.js +72 -0
- package/src/lib/testing/fake-firestore.js.map +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
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 { PluginUserEraser } from '@aglyn/aglyn/plugin-manager/plugin-user-erasure';
|
|
18
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
19
|
+
export interface PosHardwareUserEraserDeps {
|
|
20
|
+
firestore(): Firestore;
|
|
21
|
+
now?: () => number;
|
|
22
|
+
}
|
|
23
|
+
export declare function createPosHardwareUserEraser(deps: PosHardwareUserEraserDeps): PluginUserEraser;
|
|
24
|
+
/** The eraser over the platform's own Firestore, as the declarations register it. */
|
|
25
|
+
export declare const posHardwareUserEraser: PluginUserEraser;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
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
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
18
|
+
import { POS_HARDWARE_ORDERS_COLLECTION, POS_HARDWARE_SALES_COLLECTION, posHardwareBuyerErasedPatch, posHardwareSaleRecord } from "../model/pos-hardware.js";
|
|
19
|
+
const LABEL = '[pos-hardware] user eraser';
|
|
20
|
+
export function createPosHardwareUserEraser(deps) {
|
|
21
|
+
return async ({ uid, orgIds })=>{
|
|
22
|
+
var _deps_now;
|
|
23
|
+
if (!uid) return {
|
|
24
|
+
hardwareOrdersRedacted: 0,
|
|
25
|
+
formerWorkspacesReached: true
|
|
26
|
+
};
|
|
27
|
+
const db = deps.firestore();
|
|
28
|
+
const now = (_deps_now = deps.now) != null ? _deps_now : Date.now;
|
|
29
|
+
let refs;
|
|
30
|
+
let formerWorkspacesReached = true;
|
|
31
|
+
try {
|
|
32
|
+
const placed = await db.collectionGroup(POS_HARDWARE_ORDERS_COLLECTION).where('createdBy', '==', uid).get();
|
|
33
|
+
refs = placed.docs.map((doc)=>doc.ref);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`${LABEL}: the collection-group query was refused; reading current workspaces only`, error);
|
|
36
|
+
formerWorkspacesReached = false;
|
|
37
|
+
refs = [];
|
|
38
|
+
for (const orgId of orgIds){
|
|
39
|
+
const placed = await db.collection('orgs').doc(orgId).collection(POS_HARDWARE_ORDERS_COLLECTION).where('createdBy', '==', uid).get();
|
|
40
|
+
refs.push(...placed.docs.map((doc)=>doc.ref));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
let hardwareOrdersRedacted = 0;
|
|
44
|
+
for (const ref of refs){
|
|
45
|
+
const redacted = await db.runTransaction(async (transaction)=>{
|
|
46
|
+
const snapshot = await transaction.get(ref);
|
|
47
|
+
const order = snapshot.data();
|
|
48
|
+
// Gone, or already someone else's: nothing of this person is on it.
|
|
49
|
+
if (!snapshot.exists || !order || order.createdBy !== uid) return false;
|
|
50
|
+
const patch = posHardwareBuyerErasedPatch(order, now());
|
|
51
|
+
transaction.update(ref, patch);
|
|
52
|
+
transaction.set(db.collection(POS_HARDWARE_SALES_COLLECTION).doc(ref.id), posHardwareSaleRecord(ref.id, _extends({}, order, patch)));
|
|
53
|
+
return true;
|
|
54
|
+
});
|
|
55
|
+
if (redacted) hardwareOrdersRedacted += 1;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
hardwareOrdersRedacted,
|
|
59
|
+
formerWorkspacesReached
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** The eraser over the platform's own Firestore, as the declarations register it. */ export const posHardwareUserEraser = (request)=>createPosHardwareUserEraser({
|
|
64
|
+
firestore: ()=>firebaseAdmin.app().firestore()
|
|
65
|
+
})(request);
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=pos-hardware-user-eraser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-hardware-user-eraser.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 type {\n PluginUserEraser,\n PluginUserErasureReport,\n} from '@aglyn/aglyn/plugin-manager/plugin-user-erasure'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport {\n POS_HARDWARE_ORDERS_COLLECTION,\n POS_HARDWARE_SALES_COLLECTION,\n posHardwareBuyerErasedPatch,\n posHardwareSaleRecord,\n type PosHardwareOrder,\n} from '../model/pos-hardware'\n\n/*==========================================\n * COMMERCE'S SHARE OF AN ACCOUNT ERASURE: HARDWARE ORDERS (AGL-4038).\n *\n * A hardware order is the workspace's purchase and the platform's sale, so\n * an account erasure keeps it and every amount on it, and takes the BUYER\n * off it: who started it, the email, the name, the phone, the street and the\n * tracking (`posHardwareBuyerErasedPatch`). The country, state and postal\n * code stay, because sales tax is sourced by them. The retained sale record\n * (`posHardwareSales/{orderId}`) never held the person, and is rewritten\n * from the redacted order in the same transaction so the two agree.\n *\n * Found by a COLLECTION-GROUP query on `createdBy`, so an order placed in a\n * workspace the person has since left is reached too — the support-thread\n * redaction's shape (AGL-1971). It needs the `hardwareOrders.createdBy`\n * collection-group override in `cloud/firebase-firestore.indexes.json`; where\n * that index is not yet deployed the query is refused, and the eraser falls\n * back to the workspaces the person belonged to when the erasure began and\n * says so (`formerWorkspacesReached: false`), rather than reporting a clean\n * pass over orders it could not see.\n *\n * Idempotent: a redacted order no longer carries the uid, so a second run\n * finds nothing to do.\n *=========================================*/\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport interface PosHardwareUserEraserDeps {\n firestore(): Firestore\n now?: () => number\n}\n\nconst LABEL = '[pos-hardware] user eraser'\n\nexport function createPosHardwareUserEraser(deps: PosHardwareUserEraserDeps): PluginUserEraser {\n return async ({ uid, orgIds }): Promise<PluginUserErasureReport> => {\n if (!uid) return { hardwareOrdersRedacted: 0, formerWorkspacesReached: true }\n const db = deps.firestore()\n const now = deps.now ?? Date.now\n\n let refs: FirebaseFirestore.DocumentReference[]\n let formerWorkspacesReached = true\n try {\n const placed = await db.collectionGroup(POS_HARDWARE_ORDERS_COLLECTION).where('createdBy', '==', uid).get()\n refs = placed.docs.map((doc) => doc.ref)\n } catch (error) {\n console.error(`${LABEL}: the collection-group query was refused; reading current workspaces only`, error)\n formerWorkspacesReached = false\n refs = []\n for (const orgId of orgIds) {\n const placed = await db\n .collection('orgs')\n .doc(orgId)\n .collection(POS_HARDWARE_ORDERS_COLLECTION)\n .where('createdBy', '==', uid)\n .get()\n refs.push(...placed.docs.map((doc) => doc.ref))\n }\n }\n\n let hardwareOrdersRedacted = 0\n for (const ref of refs) {\n const redacted = await db.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(ref)\n const order = snapshot.data() as PosHardwareOrder | undefined\n // Gone, or already someone else's: nothing of this person is on it.\n if (!snapshot.exists || !order || order.createdBy !== uid) return false\n const patch = posHardwareBuyerErasedPatch(order, now())\n transaction.update(ref, patch)\n transaction.set(\n db.collection(POS_HARDWARE_SALES_COLLECTION).doc(ref.id),\n posHardwareSaleRecord(ref.id, { ...order, ...patch } as PosHardwareOrder),\n )\n return true\n })\n if (redacted) hardwareOrdersRedacted += 1\n }\n return { hardwareOrdersRedacted, formerWorkspacesReached }\n }\n}\n\n/** The eraser over the platform's own Firestore, as the declarations register it. */\nexport const posHardwareUserEraser: PluginUserEraser = (request) =>\n createPosHardwareUserEraser({ firestore: () => firebaseAdmin.app().firestore() })(request)\n"],"names":["firebaseAdmin","POS_HARDWARE_ORDERS_COLLECTION","POS_HARDWARE_SALES_COLLECTION","posHardwareBuyerErasedPatch","posHardwareSaleRecord","LABEL","createPosHardwareUserEraser","deps","uid","orgIds","hardwareOrdersRedacted","formerWorkspacesReached","db","firestore","now","Date","refs","placed","collectionGroup","where","get","docs","map","doc","ref","error","console","orgId","collection","push","redacted","runTransaction","transaction","snapshot","order","data","exists","createdBy","patch","update","set","id","posHardwareUserEraser","request","app"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAMD,SAASA,aAAa,QAAQ,iDAAgD;AAC9E,SACEC,8BAA8B,EAC9BC,6BAA6B,EAC7BC,2BAA2B,EAC3BC,qBAAqB,QAEhB,2BAAuB;AAiC9B,MAAMC,QAAQ;AAEd,OAAO,SAASC,4BAA4BC,IAA+B;IACzE,OAAO,OAAO,EAAEC,GAAG,EAAEC,MAAM,EAAE;YAGfF;QAFZ,IAAI,CAACC,KAAK,OAAO;YAAEE,wBAAwB;YAAGC,yBAAyB;QAAK;QAC5E,MAAMC,KAAKL,KAAKM,SAAS;QACzB,MAAMC,OAAMP,YAAAA,KAAKO,GAAG,YAARP,YAAYQ,KAAKD,GAAG;QAEhC,IAAIE;QACJ,IAAIL,0BAA0B;QAC9B,IAAI;YACF,MAAMM,SAAS,MAAML,GAAGM,eAAe,CAACjB,gCAAgCkB,KAAK,CAAC,aAAa,MAAMX,KAAKY,GAAG;YACzGJ,OAAOC,OAAOI,IAAI,CAACC,GAAG,CAAC,CAACC,MAAQA,IAAIC,GAAG;QACzC,EAAE,OAAOC,OAAO;YACdC,QAAQD,KAAK,CAAC,GAAGpB,MAAM,yEAAyE,CAAC,EAAEoB;YACnGd,0BAA0B;YAC1BK,OAAO,EAAE;YACT,KAAK,MAAMW,SAASlB,OAAQ;gBAC1B,MAAMQ,SAAS,MAAML,GAClBgB,UAAU,CAAC,QACXL,GAAG,CAACI,OACJC,UAAU,CAAC3B,gCACXkB,KAAK,CAAC,aAAa,MAAMX,KACzBY,GAAG;gBACNJ,KAAKa,IAAI,IAAIZ,OAAOI,IAAI,CAACC,GAAG,CAAC,CAACC,MAAQA,IAAIC,GAAG;YAC/C;QACF;QAEA,IAAId,yBAAyB;QAC7B,KAAK,MAAMc,OAAOR,KAAM;YACtB,MAAMc,WAAW,MAAMlB,GAAGmB,cAAc,CAAC,OAAOC;gBAC9C,MAAMC,WAAW,MAAMD,YAAYZ,GAAG,CAACI;gBACvC,MAAMU,QAAQD,SAASE,IAAI;gBAC3B,oEAAoE;gBACpE,IAAI,CAACF,SAASG,MAAM,IAAI,CAACF,SAASA,MAAMG,SAAS,KAAK7B,KAAK,OAAO;gBAClE,MAAM8B,QAAQnC,4BAA4B+B,OAAOpB;gBACjDkB,YAAYO,MAAM,CAACf,KAAKc;gBACxBN,YAAYQ,GAAG,CACb5B,GAAGgB,UAAU,CAAC1B,+BAA+BqB,GAAG,CAACC,IAAIiB,EAAE,GACvDrC,sBAAsBoB,IAAIiB,EAAE,EAAE,aAAKP,OAAUI;gBAE/C,OAAO;YACT;YACA,IAAIR,UAAUpB,0BAA0B;QAC1C;QACA,OAAO;YAAEA;YAAwBC;QAAwB;IAC3D;AACF;AAEA,mFAAmF,GACnF,OAAO,MAAM+B,wBAA0C,CAACC,UACtDrC,4BAA4B;QAAEO,WAAW,IAAMb,cAAc4C,GAAG,GAAG/B,SAAS;IAAG,GAAG8B,SAAQ"}
|
|
@@ -0,0 +1,31 @@
|
|
|
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 { BillingWebhookHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
import { type PosHardwareOrder } from '../model/pos-hardware';
|
|
19
|
+
/** The order a paid session describes, or `null` when it is not readable. */
|
|
20
|
+
export declare function posHardwareOrderFromSession(session: any, now?: number): {
|
|
21
|
+
orderId: string;
|
|
22
|
+
order: PosHardwareOrder;
|
|
23
|
+
} | null;
|
|
24
|
+
/**
|
|
25
|
+
* Writes the order once, with its retained sale record. `created` is false
|
|
26
|
+
* when a delivery already did, so the caller tells people only the first time.
|
|
27
|
+
*/
|
|
28
|
+
export declare function recordPosHardwareOrder(orderId: string, order: PosHardwareOrder): Promise<{
|
|
29
|
+
created: boolean;
|
|
30
|
+
}>;
|
|
31
|
+
export declare const posHardwareBillingWebhookHandler: BillingWebhookHandler;
|
|
@@ -0,0 +1,176 @@
|
|
|
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
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
18
|
+
import { decodePosHardwareLines, isPosHardwareShippingMethod, POS_HARDWARE_CURRENCY, POS_HARDWARE_METADATA_TYPE, POS_HARDWARE_ORDER_ID, POS_HARDWARE_SHIPPING_LABELS, posHardwareSaleRecord } from "../model/pos-hardware.js";
|
|
19
|
+
import { hardwareOrdersRef, posHardwareSaleRef } from "./pos-hardware.js";
|
|
20
|
+
import { raisePosHardwareOrderAlert, sendPosHardwareEmail } from "./pos-hardware-notify.js";
|
|
21
|
+
/*==========================================
|
|
22
|
+
* A PAID HARDWARE CHECKOUT BECOMES AN ORDER (AGL-4038).
|
|
23
|
+
*
|
|
24
|
+
* The platform's Stripe webhook fans every event out to the plugins' handlers
|
|
25
|
+
* (`registerBillingWebhookHandler`); this one self-selects on a
|
|
26
|
+
* `checkout.session.completed` whose metadata says it is a POS hardware
|
|
27
|
+
* order, and writes `orgs/{orgId}/hardwareOrders/{orderId}` and, in the same
|
|
28
|
+
* transaction, the retained sale record `posHardwareSales/{orderId}` that
|
|
29
|
+
* outlives the workspace (see `PosHardwareSaleRecord`).
|
|
30
|
+
*
|
|
31
|
+
* IDEMPOTENT BY THE ORDER ID. The id was fixed when the session was made
|
|
32
|
+
* (from the merchant's attempt key), and the write is a transactional
|
|
33
|
+
* create: a redelivered event finds the document and does nothing, so a
|
|
34
|
+
* replay never makes a second order, a second email or a second alert.
|
|
35
|
+
*
|
|
36
|
+
* NOTHING ON THE WIRE IS TRUSTED FOR WHAT IT NAMES. The lines are re-read
|
|
37
|
+
* against the catalog; the amounts are Stripe's own totals for the session;
|
|
38
|
+
* an unreadable session is logged and ignored rather than half-recorded.
|
|
39
|
+
*=========================================*/ function addressFrom(raw) {
|
|
40
|
+
var _raw_line1, _raw_city, _raw_postal_code, _raw_country, _raw_line2, _raw_state;
|
|
41
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
42
|
+
const line1 = String((_raw_line1 = raw.line1) != null ? _raw_line1 : '').trim();
|
|
43
|
+
const city = String((_raw_city = raw.city) != null ? _raw_city : '').trim();
|
|
44
|
+
const postalCode = String((_raw_postal_code = raw.postal_code) != null ? _raw_postal_code : '').trim();
|
|
45
|
+
const country = String((_raw_country = raw.country) != null ? _raw_country : '').trim().toUpperCase();
|
|
46
|
+
if (!line1 || !city || !postalCode || !country) return null;
|
|
47
|
+
const line2 = String((_raw_line2 = raw.line2) != null ? _raw_line2 : '').trim();
|
|
48
|
+
const state = String((_raw_state = raw.state) != null ? _raw_state : '').trim();
|
|
49
|
+
return _extends({
|
|
50
|
+
line1: line1.slice(0, 200)
|
|
51
|
+
}, line2 ? {
|
|
52
|
+
line2: line2.slice(0, 200)
|
|
53
|
+
} : {}, {
|
|
54
|
+
city: city.slice(0, 100)
|
|
55
|
+
}, state ? {
|
|
56
|
+
state: state.slice(0, 100)
|
|
57
|
+
} : {}, {
|
|
58
|
+
postalCode: postalCode.slice(0, 20),
|
|
59
|
+
country: country.slice(0, 2)
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const cents = (value)=>{
|
|
63
|
+
const number = Number(value);
|
|
64
|
+
return Number.isSafeInteger(number) && number >= 0 ? number : 0;
|
|
65
|
+
};
|
|
66
|
+
/** The order a paid session describes, or `null` when it is not readable. */ export function posHardwareOrderFromSession(session, now = Date.now()) {
|
|
67
|
+
var _ref, _metadata_orderId, _metadata_orgId, _metadata_hostId, _ref1, _ref2, _ref3, _metadata_uid, _ref4, _ref5, _ref6, _ref7, _ref8, _ref9, _ref10;
|
|
68
|
+
var _session_collected_information, _session_shipping_cost, _session_total_details;
|
|
69
|
+
const metadata = (_ref = session == null ? void 0 : session.metadata) != null ? _ref : {};
|
|
70
|
+
if (metadata.type !== POS_HARDWARE_METADATA_TYPE) return null;
|
|
71
|
+
const orderId = String((_metadata_orderId = metadata.orderId) != null ? _metadata_orderId : '');
|
|
72
|
+
const orgId = String((_metadata_orgId = metadata.orgId) != null ? _metadata_orgId : '');
|
|
73
|
+
const hostId = String((_metadata_hostId = metadata.hostId) != null ? _metadata_hostId : '');
|
|
74
|
+
if (!POS_HARDWARE_ORDER_ID.test(orderId) || !orgId || !hostId) return null;
|
|
75
|
+
const lines = decodePosHardwareLines(metadata.lines);
|
|
76
|
+
if (!lines) return null;
|
|
77
|
+
const method = isPosHardwareShippingMethod(metadata.shippingMethod) ? metadata.shippingMethod : 'standard';
|
|
78
|
+
// Where the newer API versions keep the shipping details, then the older.
|
|
79
|
+
const shippingDetails = (_ref1 = (_ref2 = session == null ? void 0 : (_session_collected_information = session.collected_information) == null ? void 0 : _session_collected_information.shipping_details) != null ? _ref2 : session == null ? void 0 : session.shipping_details) != null ? _ref1 : null;
|
|
80
|
+
const customer = (_ref3 = session == null ? void 0 : session.customer_details) != null ? _ref3 : {};
|
|
81
|
+
const subtotalCents = lines.reduce((sum, line)=>sum + line.lineCents, 0);
|
|
82
|
+
const shippingCents = (session == null ? void 0 : (_session_shipping_cost = session.shipping_cost) == null ? void 0 : _session_shipping_cost.amount_total) != null ? cents(session.shipping_cost.amount_total) : cents(metadata.shippingCents);
|
|
83
|
+
const taxCents = cents(session == null ? void 0 : (_session_total_details = session.total_details) == null ? void 0 : _session_total_details.amount_tax);
|
|
84
|
+
const totalCents = (session == null ? void 0 : session.amount_total) != null ? cents(session.amount_total) : subtotalCents + shippingCents + taxCents;
|
|
85
|
+
const paymentIntent = session == null ? void 0 : session.payment_intent;
|
|
86
|
+
return {
|
|
87
|
+
orderId,
|
|
88
|
+
order: {
|
|
89
|
+
orgId,
|
|
90
|
+
hostId,
|
|
91
|
+
createdBy: String((_metadata_uid = metadata.uid) != null ? _metadata_uid : ''),
|
|
92
|
+
contactEmail: String((_ref4 = customer == null ? void 0 : customer.email) != null ? _ref4 : '').trim() || null,
|
|
93
|
+
lines,
|
|
94
|
+
subtotalCents,
|
|
95
|
+
shipping: {
|
|
96
|
+
method,
|
|
97
|
+
label: POS_HARDWARE_SHIPPING_LABELS[method],
|
|
98
|
+
cents: shippingCents,
|
|
99
|
+
name: String((_ref5 = (_ref6 = shippingDetails == null ? void 0 : shippingDetails.name) != null ? _ref6 : customer == null ? void 0 : customer.name) != null ? _ref5 : '').trim() || null,
|
|
100
|
+
phone: String((_ref7 = customer == null ? void 0 : customer.phone) != null ? _ref7 : '').trim() || null,
|
|
101
|
+
address: addressFrom(shippingDetails == null ? void 0 : shippingDetails.address)
|
|
102
|
+
},
|
|
103
|
+
taxCents,
|
|
104
|
+
totalCents,
|
|
105
|
+
currency: String((_ref8 = session == null ? void 0 : session.currency) != null ? _ref8 : POS_HARDWARE_CURRENCY),
|
|
106
|
+
stripeCheckoutSessionId: String((_ref9 = session == null ? void 0 : session.id) != null ? _ref9 : ''),
|
|
107
|
+
stripePaymentIntentId: (typeof paymentIntent === 'string' ? paymentIntent : String((_ref10 = paymentIntent == null ? void 0 : paymentIntent.id) != null ? _ref10 : '')) || null,
|
|
108
|
+
stripeHardwareOrderId: null,
|
|
109
|
+
tracking: null,
|
|
110
|
+
status: 'paid',
|
|
111
|
+
livemode: (session == null ? void 0 : session.livemode) === true,
|
|
112
|
+
createdAtMs: now,
|
|
113
|
+
updatedAtMs: now,
|
|
114
|
+
paidAtMs: now
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Writes the order once, with its retained sale record. `created` is false
|
|
120
|
+
* when a delivery already did, so the caller tells people only the first time.
|
|
121
|
+
*/ export async function recordPosHardwareOrder(orderId, order) {
|
|
122
|
+
const ref = hardwareOrdersRef(order.orgId).doc(orderId);
|
|
123
|
+
const saleRef = posHardwareSaleRef(orderId);
|
|
124
|
+
return firebaseAdmin.app().firestore().runTransaction(async (transaction)=>{
|
|
125
|
+
const existing = await transaction.get(ref);
|
|
126
|
+
if (existing.exists) return {
|
|
127
|
+
created: false
|
|
128
|
+
};
|
|
129
|
+
transaction.create(ref, order);
|
|
130
|
+
transaction.set(saleRef, posHardwareSaleRecord(orderId, order));
|
|
131
|
+
return {
|
|
132
|
+
created: true
|
|
133
|
+
};
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
export const posHardwareBillingWebhookHandler = async ({ type, object })=>{
|
|
137
|
+
var _ref;
|
|
138
|
+
var _object_metadata, _object_metadata1;
|
|
139
|
+
if (type !== 'checkout.session.completed') return;
|
|
140
|
+
if ((object == null ? void 0 : (_object_metadata = object.metadata) == null ? void 0 : _object_metadata.type) !== POS_HARDWARE_METADATA_TYPE) return;
|
|
141
|
+
const hostId = String((_ref = object == null ? void 0 : (_object_metadata1 = object.metadata) == null ? void 0 : _object_metadata1.hostId) != null ? _ref : '');
|
|
142
|
+
// A session that is not paid holds nothing to fulfil. Card is the only
|
|
143
|
+
// method offered, so payment is settled when the session completes.
|
|
144
|
+
if ((object == null ? void 0 : object.mode) !== 'payment' || (object == null ? void 0 : object.payment_status) !== 'paid') {
|
|
145
|
+
return _extends({
|
|
146
|
+
claimed: true
|
|
147
|
+
}, hostId ? {
|
|
148
|
+
hostId
|
|
149
|
+
} : {});
|
|
150
|
+
}
|
|
151
|
+
const parsed = posHardwareOrderFromSession(object);
|
|
152
|
+
if (!parsed) {
|
|
153
|
+
var _ref1;
|
|
154
|
+
console.error('[pos-hardware] a paid hardware session could not be read', String((_ref1 = object == null ? void 0 : object.id) != null ? _ref1 : ''));
|
|
155
|
+
return _extends({
|
|
156
|
+
claimed: true
|
|
157
|
+
}, hostId ? {
|
|
158
|
+
hostId
|
|
159
|
+
} : {});
|
|
160
|
+
}
|
|
161
|
+
// Errors here propagate on purpose: the route answers 500 and Stripe
|
|
162
|
+
// redelivers, and the create above makes the retry safe.
|
|
163
|
+
const { created } = await recordPosHardwareOrder(parsed.orderId, parsed.order);
|
|
164
|
+
if (created) {
|
|
165
|
+
await Promise.all([
|
|
166
|
+
sendPosHardwareEmail('received', parsed.orderId, parsed.order),
|
|
167
|
+
raisePosHardwareOrderAlert(parsed.orderId, parsed.order)
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
claimed: true,
|
|
172
|
+
hostId: parsed.order.hostId
|
|
173
|
+
};
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
//# sourceMappingURL=pos-hardware-webhook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-hardware-webhook.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 type { BillingWebhookHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n decodePosHardwareLines,\n isPosHardwareShippingMethod,\n POS_HARDWARE_CURRENCY,\n POS_HARDWARE_METADATA_TYPE,\n POS_HARDWARE_ORDER_ID,\n POS_HARDWARE_SHIPPING_LABELS,\n posHardwareSaleRecord,\n type PosHardwareAddress,\n type PosHardwareOrder,\n} from '../model/pos-hardware'\nimport { hardwareOrdersRef, posHardwareSaleRef } from './pos-hardware'\nimport { raisePosHardwareOrderAlert, sendPosHardwareEmail } from './pos-hardware-notify'\n\n/*==========================================\n * A PAID HARDWARE CHECKOUT BECOMES AN ORDER (AGL-4038).\n *\n * The platform's Stripe webhook fans every event out to the plugins' handlers\n * (`registerBillingWebhookHandler`); this one self-selects on a\n * `checkout.session.completed` whose metadata says it is a POS hardware\n * order, and writes `orgs/{orgId}/hardwareOrders/{orderId}` and, in the same\n * transaction, the retained sale record `posHardwareSales/{orderId}` that\n * outlives the workspace (see `PosHardwareSaleRecord`).\n *\n * IDEMPOTENT BY THE ORDER ID. The id was fixed when the session was made\n * (from the merchant's attempt key), and the write is a transactional\n * create: a redelivered event finds the document and does nothing, so a\n * replay never makes a second order, a second email or a second alert.\n *\n * NOTHING ON THE WIRE IS TRUSTED FOR WHAT IT NAMES. The lines are re-read\n * against the catalog; the amounts are Stripe's own totals for the session;\n * an unreadable session is logged and ignored rather than half-recorded.\n *=========================================*/\n\nfunction addressFrom(raw: any): PosHardwareAddress | null {\n if (!raw || typeof raw !== 'object') return null\n const line1 = String(raw.line1 ?? '').trim()\n const city = String(raw.city ?? '').trim()\n const postalCode = String(raw.postal_code ?? '').trim()\n const country = String(raw.country ?? '').trim().toUpperCase()\n if (!line1 || !city || !postalCode || !country) return null\n const line2 = String(raw.line2 ?? '').trim()\n const state = String(raw.state ?? '').trim()\n return {\n line1: line1.slice(0, 200),\n ...(line2 ? { line2: line2.slice(0, 200) } : {}),\n city: city.slice(0, 100),\n ...(state ? { state: state.slice(0, 100) } : {}),\n postalCode: postalCode.slice(0, 20),\n country: country.slice(0, 2),\n }\n}\n\nconst cents = (value: unknown): number => {\n const number = Number(value)\n return Number.isSafeInteger(number) && number >= 0 ? number : 0\n}\n\n/** The order a paid session describes, or `null` when it is not readable. */\nexport function posHardwareOrderFromSession(\n session: any,\n now = Date.now(),\n): { orderId: string; order: PosHardwareOrder } | null {\n const metadata = session?.metadata ?? {}\n if (metadata.type !== POS_HARDWARE_METADATA_TYPE) return null\n const orderId = String(metadata.orderId ?? '')\n const orgId = String(metadata.orgId ?? '')\n const hostId = String(metadata.hostId ?? '')\n if (!POS_HARDWARE_ORDER_ID.test(orderId) || !orgId || !hostId) return null\n const lines = decodePosHardwareLines(metadata.lines)\n if (!lines) return null\n const method = isPosHardwareShippingMethod(metadata.shippingMethod) ? metadata.shippingMethod : 'standard'\n // Where the newer API versions keep the shipping details, then the older.\n const shippingDetails =\n session?.collected_information?.shipping_details ?? session?.shipping_details ?? null\n const customer = session?.customer_details ?? {}\n const subtotalCents = lines.reduce((sum, line) => sum + line.lineCents, 0)\n const shippingCents =\n session?.shipping_cost?.amount_total != null\n ? cents(session.shipping_cost.amount_total)\n : cents(metadata.shippingCents)\n const taxCents = cents(session?.total_details?.amount_tax)\n const totalCents =\n session?.amount_total != null ? cents(session.amount_total) : subtotalCents + shippingCents + taxCents\n const paymentIntent = session?.payment_intent\n return {\n orderId,\n order: {\n orgId,\n hostId,\n createdBy: String(metadata.uid ?? ''),\n contactEmail: String(customer?.email ?? '').trim() || null,\n lines,\n subtotalCents,\n shipping: {\n method,\n label: POS_HARDWARE_SHIPPING_LABELS[method],\n cents: shippingCents,\n name: String(shippingDetails?.name ?? customer?.name ?? '').trim() || null,\n phone: String(customer?.phone ?? '').trim() || null,\n address: addressFrom(shippingDetails?.address),\n },\n taxCents,\n totalCents,\n currency: String(session?.currency ?? POS_HARDWARE_CURRENCY),\n stripeCheckoutSessionId: String(session?.id ?? ''),\n stripePaymentIntentId:\n (typeof paymentIntent === 'string' ? paymentIntent : String(paymentIntent?.id ?? '')) || null,\n stripeHardwareOrderId: null,\n tracking: null,\n status: 'paid',\n livemode: session?.livemode === true,\n createdAtMs: now,\n updatedAtMs: now,\n paidAtMs: now,\n },\n }\n}\n\n/**\n * Writes the order once, with its retained sale record. `created` is false\n * when a delivery already did, so the caller tells people only the first time.\n */\nexport async function recordPosHardwareOrder(\n orderId: string,\n order: PosHardwareOrder,\n): Promise<{ created: boolean }> {\n const ref = hardwareOrdersRef(order.orgId).doc(orderId)\n const saleRef = posHardwareSaleRef(orderId)\n return firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction: any) => {\n const existing = await transaction.get(ref)\n if (existing.exists) return { created: false }\n transaction.create(ref, order)\n transaction.set(saleRef, posHardwareSaleRecord(orderId, order))\n return { created: true }\n })\n}\n\nexport const posHardwareBillingWebhookHandler: BillingWebhookHandler = async ({ type, object }) => {\n if (type !== 'checkout.session.completed') return\n if (object?.metadata?.type !== POS_HARDWARE_METADATA_TYPE) return\n const hostId = String(object?.metadata?.hostId ?? '')\n // A session that is not paid holds nothing to fulfil. Card is the only\n // method offered, so payment is settled when the session completes.\n if (object?.mode !== 'payment' || object?.payment_status !== 'paid') {\n return { claimed: true, ...(hostId ? { hostId } : {}) }\n }\n const parsed = posHardwareOrderFromSession(object)\n if (!parsed) {\n console.error('[pos-hardware] a paid hardware session could not be read', String(object?.id ?? ''))\n return { claimed: true, ...(hostId ? { hostId } : {}) }\n }\n // Errors here propagate on purpose: the route answers 500 and Stripe\n // redelivers, and the create above makes the retry safe.\n const { created } = await recordPosHardwareOrder(parsed.orderId, parsed.order)\n if (created) {\n await Promise.all([\n sendPosHardwareEmail('received', parsed.orderId, parsed.order),\n raisePosHardwareOrderAlert(parsed.orderId, parsed.order),\n ])\n }\n return { claimed: true, hostId: parsed.order.hostId }\n}\n"],"names":["firebaseAdmin","decodePosHardwareLines","isPosHardwareShippingMethod","POS_HARDWARE_CURRENCY","POS_HARDWARE_METADATA_TYPE","POS_HARDWARE_ORDER_ID","POS_HARDWARE_SHIPPING_LABELS","posHardwareSaleRecord","hardwareOrdersRef","posHardwareSaleRef","raisePosHardwareOrderAlert","sendPosHardwareEmail","addressFrom","raw","line1","String","trim","city","postalCode","postal_code","country","toUpperCase","line2","state","slice","cents","value","number","Number","isSafeInteger","posHardwareOrderFromSession","session","now","Date","metadata","shippingDetails","type","orderId","orgId","hostId","test","lines","method","shippingMethod","collected_information","shipping_details","customer","customer_details","subtotalCents","reduce","sum","line","lineCents","shippingCents","shipping_cost","amount_total","taxCents","total_details","amount_tax","totalCents","paymentIntent","payment_intent","order","createdBy","uid","contactEmail","email","shipping","label","name","phone","address","currency","stripeCheckoutSessionId","id","stripePaymentIntentId","stripeHardwareOrderId","tracking","status","livemode","createdAtMs","updatedAtMs","paidAtMs","recordPosHardwareOrder","ref","doc","saleRef","app","firestore","runTransaction","transaction","existing","get","exists","created","create","set","posHardwareBillingWebhookHandler","object","mode","payment_status","claimed","parsed","console","error","Promise","all"],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
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 { type PosHardwareConfig, type PosHardwareOrder } from '../model/pos-hardware';
|
|
19
|
+
/** The shop's config from this deployment, or `null` while it is hidden. */
|
|
20
|
+
export declare function posHardwareConfig(env?: Record<string, string | undefined>): PosHardwareConfig | null;
|
|
21
|
+
/** The order's id, derived from the attempt so a repeated press is one order. */
|
|
22
|
+
export declare function posHardwareOrderId(orgId: string, hostId: string, attemptKey: string): string;
|
|
23
|
+
/** The number a person reads: `HW-` and six characters of the id. */
|
|
24
|
+
export declare function posHardwareOrderNumber(orderId: string): string;
|
|
25
|
+
export declare function hardwareOrdersRef(orgId: string): FirebaseFirestore.CollectionReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
26
|
+
/**
|
|
27
|
+
* `posHardwareSales/{orderId}`, the retained sale record beside each order
|
|
28
|
+
* (see `PosHardwareSaleRecord`). Top-level on purpose: the workspace erasure
|
|
29
|
+
* cannot reach it.
|
|
30
|
+
*/
|
|
31
|
+
export declare function posHardwareSaleRef(orderId: string): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
32
|
+
/** An order as the merchant's card shows it: no Stripe ids, no staff notes. */
|
|
33
|
+
export declare function merchantHardwareOrderView(id: string, order: PosHardwareOrder): {
|
|
34
|
+
id: string;
|
|
35
|
+
number: string;
|
|
36
|
+
status: import("../model/pos-hardware").PosHardwareOrderStatus;
|
|
37
|
+
lines: {
|
|
38
|
+
sku: import("../model/pos-hardware").PosHardwareSkuId;
|
|
39
|
+
name: string;
|
|
40
|
+
quantity: number;
|
|
41
|
+
unitCents: number;
|
|
42
|
+
lineCents: number;
|
|
43
|
+
}[];
|
|
44
|
+
subtotalCents: number;
|
|
45
|
+
shippingCents: number;
|
|
46
|
+
shippingLabel: string;
|
|
47
|
+
taxCents: number;
|
|
48
|
+
totalCents: number;
|
|
49
|
+
tracking: import("../model/pos-hardware").PosHardwareTracking;
|
|
50
|
+
createdAtMs: number;
|
|
51
|
+
updatedAtMs: number;
|
|
52
|
+
};
|
|
53
|
+
export type MerchantHardwareOrderView = ReturnType<typeof merchantHardwareOrderView>;
|
|
54
|
+
/** The POS settings page of a site, as an absolute console URL. */
|
|
55
|
+
export declare function posSettingsUrl(hostId: string): Promise<string>;
|
|
56
|
+
/** `base` with `query` added before any `#fragment`, then `#hash`. */
|
|
57
|
+
export declare function withQueryAndHash(base: string, query: string, hash: string): string;
|
|
58
|
+
export declare const posHardwareHandler: PluginApiHandler;
|
|
@@ -0,0 +1,234 @@
|
|
|
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 { createHash } from "crypto";
|
|
18
|
+
import { firebaseAdmin, readOrgBilling } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { hostConsoleUrl } from "@aglyn/tenant-data-admin/server/host-console-link";
|
|
20
|
+
import { encodePosHardwareLines, isPosHardwareShippingMethod, POS_HARDWARE_ANCHOR, parsePosHardwareConfig, POS_HARDWARE_CURRENCY, POS_HARDWARE_METADATA_TYPE, POS_HARDWARE_ORDERS_COLLECTION, POS_HARDWARE_SALES_COLLECTION, POS_HARDWARE_SHIP_COUNTRIES, POS_HARDWARE_SHIPPING_LABELS, posHardwareOffers, posHardwareSku, pricePosHardwareCart } from "../model/pos-hardware.js";
|
|
21
|
+
import { authorizePosStaff, posIdempotencyKey, posRequestBody } from "./pos-auth.js";
|
|
22
|
+
import { posStripe, posTerminalAvailable } from "./pos-stripe.js";
|
|
23
|
+
/*==========================================
|
|
24
|
+
* `POST /api/commerce/pos-hardware` (AGL-4038): the merchant's half of the
|
|
25
|
+
* POS hardware shop, behind the Hardware card on Products → Point of sale.
|
|
26
|
+
*
|
|
27
|
+
* catalog what is on offer and at what price, or `available: false`
|
|
28
|
+
* checkout a Stripe Checkout Session for the basket, on the PLATFORM
|
|
29
|
+
* orders this site's hardware orders, newest first
|
|
30
|
+
*
|
|
31
|
+
* Gated like the register (`authorizePosStaff`: an admin or editor of the
|
|
32
|
+
* site with `managePos`, on a plan with POS), because the readers it sells
|
|
33
|
+
* are the register's.
|
|
34
|
+
*
|
|
35
|
+
* THE SALE IS THE PLATFORM'S. The session is created with the platform key
|
|
36
|
+
* and NO connected account, `transfer_data` or application fee: the merchant
|
|
37
|
+
* is the buyer here, and the platform is the seller of record that places
|
|
38
|
+
* the order with Stripe. The order document is written by the webhook once
|
|
39
|
+
* Stripe says the session is paid (`pos-hardware-webhook.ts`), never here.
|
|
40
|
+
*=========================================*/ /** The shop's config from this deployment, or `null` while it is hidden. */ export function posHardwareConfig(env = process.env) {
|
|
41
|
+
// A reader the platform cannot yet take payments on is not for sale: the
|
|
42
|
+
// shop opens only where the register offers card readers at all.
|
|
43
|
+
if (!posTerminalAvailable()) return null;
|
|
44
|
+
return parsePosHardwareConfig(env);
|
|
45
|
+
}
|
|
46
|
+
/** The order's id, derived from the attempt so a repeated press is one order. */ export function posHardwareOrderId(orgId, hostId, attemptKey) {
|
|
47
|
+
return `hw_${createHash('sha256').update(`${orgId}:${hostId}:${attemptKey}`).digest('hex').slice(0, 20)}`;
|
|
48
|
+
}
|
|
49
|
+
/** The number a person reads: `HW-` and six characters of the id. */ export function posHardwareOrderNumber(orderId) {
|
|
50
|
+
return `HW-${orderId.replace(/^hw_/, '').slice(0, 6).toUpperCase()}`;
|
|
51
|
+
}
|
|
52
|
+
export function hardwareOrdersRef(orgId) {
|
|
53
|
+
return firebaseAdmin.app().firestore().collection('orgs').doc(orgId).collection(POS_HARDWARE_ORDERS_COLLECTION);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* `posHardwareSales/{orderId}`, the retained sale record beside each order
|
|
57
|
+
* (see `PosHardwareSaleRecord`). Top-level on purpose: the workspace erasure
|
|
58
|
+
* cannot reach it.
|
|
59
|
+
*/ export function posHardwareSaleRef(orderId) {
|
|
60
|
+
return firebaseAdmin.app().firestore().collection(POS_HARDWARE_SALES_COLLECTION).doc(orderId);
|
|
61
|
+
}
|
|
62
|
+
/** An order as the merchant's card shows it: no Stripe ids, no staff notes. */ export function merchantHardwareOrderView(id, order) {
|
|
63
|
+
var _order_lines, _order_subtotalCents, _ref, _ref1, _order_taxCents, _order_totalCents, _order_tracking, _order_createdAtMs, _ref2, _order_updatedAtMs;
|
|
64
|
+
var _order_shipping, _order_shipping1;
|
|
65
|
+
return {
|
|
66
|
+
id,
|
|
67
|
+
number: posHardwareOrderNumber(id),
|
|
68
|
+
status: order.status,
|
|
69
|
+
lines: ((_order_lines = order.lines) != null ? _order_lines : []).map((line)=>({
|
|
70
|
+
sku: line.sku,
|
|
71
|
+
name: line.name,
|
|
72
|
+
quantity: line.quantity,
|
|
73
|
+
unitCents: line.unitCents,
|
|
74
|
+
lineCents: line.lineCents
|
|
75
|
+
})),
|
|
76
|
+
subtotalCents: (_order_subtotalCents = order.subtotalCents) != null ? _order_subtotalCents : 0,
|
|
77
|
+
shippingCents: (_ref = (_order_shipping = order.shipping) == null ? void 0 : _order_shipping.cents) != null ? _ref : 0,
|
|
78
|
+
shippingLabel: (_ref1 = (_order_shipping1 = order.shipping) == null ? void 0 : _order_shipping1.label) != null ? _ref1 : '',
|
|
79
|
+
taxCents: (_order_taxCents = order.taxCents) != null ? _order_taxCents : 0,
|
|
80
|
+
totalCents: (_order_totalCents = order.totalCents) != null ? _order_totalCents : 0,
|
|
81
|
+
tracking: (_order_tracking = order.tracking) != null ? _order_tracking : null,
|
|
82
|
+
createdAtMs: (_order_createdAtMs = order.createdAtMs) != null ? _order_createdAtMs : 0,
|
|
83
|
+
updatedAtMs: (_ref2 = (_order_updatedAtMs = order.updatedAtMs) != null ? _order_updatedAtMs : order.createdAtMs) != null ? _ref2 : 0
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** The POS settings page of a site, as an absolute console URL. */ export async function posSettingsUrl(hostId) {
|
|
87
|
+
return hostConsoleUrl(hostId, `/${hostId}/products/pos`);
|
|
88
|
+
}
|
|
89
|
+
/** `base` with `query` added before any `#fragment`, then `#hash`. */ export function withQueryAndHash(base, query, hash) {
|
|
90
|
+
const [path] = base.split('#');
|
|
91
|
+
const joiner = path.includes('?') ? '&' : '?';
|
|
92
|
+
return `${path}${joiner}${query}#${hash}`;
|
|
93
|
+
}
|
|
94
|
+
export const posHardwareHandler = async (req, res)=>{
|
|
95
|
+
var _body_hostId, _body_action;
|
|
96
|
+
if (req.method !== 'POST') return res.status(405).json({
|
|
97
|
+
error: 'Method not allowed'
|
|
98
|
+
});
|
|
99
|
+
const body = posRequestBody(req);
|
|
100
|
+
const hostId = String((_body_hostId = body['hostId']) != null ? _body_hostId : '');
|
|
101
|
+
const gate = await authorizePosStaff(req, hostId);
|
|
102
|
+
if ('error' in gate) return res.status(gate.status).json({
|
|
103
|
+
error: gate.error
|
|
104
|
+
});
|
|
105
|
+
const staff = gate.staff;
|
|
106
|
+
const config = posHardwareConfig();
|
|
107
|
+
const action = String((_body_action = body['action']) != null ? _body_action : '');
|
|
108
|
+
try {
|
|
109
|
+
switch(action){
|
|
110
|
+
case 'catalog':
|
|
111
|
+
if (!config) return res.status(200).json({
|
|
112
|
+
available: false
|
|
113
|
+
});
|
|
114
|
+
return res.status(200).json({
|
|
115
|
+
available: true,
|
|
116
|
+
offers: posHardwareOffers(config),
|
|
117
|
+
shipping: config.shipping.map((option)=>({
|
|
118
|
+
method: option.method,
|
|
119
|
+
label: POS_HARDWARE_SHIPPING_LABELS[option.method],
|
|
120
|
+
cents: option.cents
|
|
121
|
+
})),
|
|
122
|
+
automaticTax: config.automaticTax,
|
|
123
|
+
shipCountries: POS_HARDWARE_SHIP_COUNTRIES
|
|
124
|
+
});
|
|
125
|
+
case 'orders':
|
|
126
|
+
{
|
|
127
|
+
const snapshot = await hardwareOrdersRef(staff.orgId).where('hostId', '==', staff.hostId).orderBy('createdAtMs', 'desc').limit(50).get();
|
|
128
|
+
return res.status(200).json({
|
|
129
|
+
orders: snapshot.docs.map((doc)=>merchantHardwareOrderView(String(doc.id), doc.data()))
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
case 'checkout':
|
|
133
|
+
{
|
|
134
|
+
var _billing_stripeCustomerId, _ref;
|
|
135
|
+
var _created_body;
|
|
136
|
+
if (!config) return res.status(404).json({
|
|
137
|
+
error: 'The hardware shop is not available.'
|
|
138
|
+
});
|
|
139
|
+
const attemptKey = posIdempotencyKey(req);
|
|
140
|
+
if (!attemptKey) return res.status(400).json({
|
|
141
|
+
error: 'Missing Idempotency-Key'
|
|
142
|
+
});
|
|
143
|
+
if (!staff.orgId) return res.status(409).json({
|
|
144
|
+
error: 'This site has no workspace.'
|
|
145
|
+
});
|
|
146
|
+
const priced = pricePosHardwareCart(config, body['lines']);
|
|
147
|
+
if ('error' in priced) return res.status(400).json({
|
|
148
|
+
error: priced.error
|
|
149
|
+
});
|
|
150
|
+
const method = body['shippingMethod'];
|
|
151
|
+
const shipping = isPosHardwareShippingMethod(method) ? config.shipping.find((option)=>option.method === method) : undefined;
|
|
152
|
+
if (!shipping) return res.status(400).json({
|
|
153
|
+
error: 'Choose a shipping speed.'
|
|
154
|
+
});
|
|
155
|
+
const orderId = posHardwareOrderId(staff.orgId, staff.hostId, attemptKey);
|
|
156
|
+
const billing = await readOrgBilling(staff.orgId).catch(()=>({}));
|
|
157
|
+
const customer = String((_billing_stripeCustomerId = billing.stripeCustomerId) != null ? _billing_stripeCustomerId : '').trim();
|
|
158
|
+
const base = await posSettingsUrl(staff.hostId);
|
|
159
|
+
const params = _extends({
|
|
160
|
+
mode: 'payment',
|
|
161
|
+
client_reference_id: orderId,
|
|
162
|
+
success_url: withQueryAndHash(base, 'hardwareOrder=placed', POS_HARDWARE_ANCHOR),
|
|
163
|
+
cancel_url: withQueryAndHash(base, 'hardwareOrder=canceled', POS_HARDWARE_ANCHOR),
|
|
164
|
+
'payment_method_types[0]': 'card',
|
|
165
|
+
'shipping_address_collection[allowed_countries][0]': POS_HARDWARE_SHIP_COUNTRIES[0],
|
|
166
|
+
'phone_number_collection[enabled]': true,
|
|
167
|
+
'shipping_options[0][shipping_rate_data][type]': 'fixed_amount',
|
|
168
|
+
'shipping_options[0][shipping_rate_data][display_name]': POS_HARDWARE_SHIPPING_LABELS[shipping.method],
|
|
169
|
+
'shipping_options[0][shipping_rate_data][fixed_amount][amount]': shipping.cents,
|
|
170
|
+
'shipping_options[0][shipping_rate_data][fixed_amount][currency]': POS_HARDWARE_CURRENCY,
|
|
171
|
+
'metadata[type]': POS_HARDWARE_METADATA_TYPE,
|
|
172
|
+
'metadata[orderId]': orderId,
|
|
173
|
+
'metadata[orgId]': staff.orgId,
|
|
174
|
+
'metadata[hostId]': staff.hostId,
|
|
175
|
+
'metadata[uid]': staff.uid,
|
|
176
|
+
'metadata[lines]': encodePosHardwareLines(priced.lines),
|
|
177
|
+
'metadata[shippingMethod]': shipping.method,
|
|
178
|
+
'metadata[shippingCents]': shipping.cents,
|
|
179
|
+
'metadata[subtotalCents]': priced.subtotalCents,
|
|
180
|
+
'payment_intent_data[description]': `Card reader order ${posHardwareOrderNumber(orderId)}`,
|
|
181
|
+
'payment_intent_data[metadata][type]': POS_HARDWARE_METADATA_TYPE,
|
|
182
|
+
'payment_intent_data[metadata][orderId]': orderId,
|
|
183
|
+
'payment_intent_data[metadata][orgId]': staff.orgId
|
|
184
|
+
}, customer ? {
|
|
185
|
+
customer
|
|
186
|
+
} : {}, config.automaticTax ? _extends({
|
|
187
|
+
'automatic_tax[enabled]': true,
|
|
188
|
+
'shipping_options[0][shipping_rate_data][tax_behavior]': 'exclusive'
|
|
189
|
+
}, customer ? {
|
|
190
|
+
'customer_update[shipping]': 'auto'
|
|
191
|
+
} : {}) : {});
|
|
192
|
+
priced.lines.forEach((line, index)=>{
|
|
193
|
+
const sku = posHardwareSku(line.sku);
|
|
194
|
+
params[`line_items[${index}][quantity]`] = line.quantity;
|
|
195
|
+
// A literal, not POS_HARDWARE_CURRENCY: check:facilitator-charge-shape
|
|
196
|
+
// reads Stripe Tax beside a usd line item only when it says so (AGL-1956).
|
|
197
|
+
params[`line_items[${index}][price_data][currency]`] = 'usd';
|
|
198
|
+
params[`line_items[${index}][price_data][unit_amount]`] = line.unitCents;
|
|
199
|
+
params[`line_items[${index}][price_data][product_data][name]`] = line.name;
|
|
200
|
+
if (sku) params[`line_items[${index}][price_data][product_data][description]`] = sku.use;
|
|
201
|
+
params[`line_items[${index}][price_data][product_data][metadata][sku]`] = line.sku;
|
|
202
|
+
if (config.automaticTax) params[`line_items[${index}][price_data][tax_behavior]`] = 'exclusive';
|
|
203
|
+
});
|
|
204
|
+
const created = await posStripe('POST', 'checkout/sessions', {
|
|
205
|
+
params,
|
|
206
|
+
idempotencyKey: `pos-hw-checkout-${orderId}`
|
|
207
|
+
});
|
|
208
|
+
const url = String((_ref = (_created_body = created.body) == null ? void 0 : _created_body.url) != null ? _ref : '');
|
|
209
|
+
if (!created.ok || !url) {
|
|
210
|
+
var _created_body_error, _created_body1;
|
|
211
|
+
console.error('[pos-hardware] checkout refused', created.status, (_created_body1 = created.body) == null ? void 0 : (_created_body_error = _created_body1.error) == null ? void 0 : _created_body_error.code);
|
|
212
|
+
return res.status(502).json({
|
|
213
|
+
error: 'Checkout could not be started. Try again in a moment.'
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return res.status(200).json({
|
|
217
|
+
url,
|
|
218
|
+
orderId
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
default:
|
|
222
|
+
return res.status(400).json({
|
|
223
|
+
error: 'Unknown action'
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('[pos-hardware]', action, error);
|
|
228
|
+
return res.status(500).json({
|
|
229
|
+
error: 'The hardware shop could not answer'
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
//# sourceMappingURL=pos-hardware.js.map
|