@aglyn/plugins-commerce 1.0.0-beta.278 → 1.0.0-beta.283
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,366 @@
|
|
|
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 { pluginRequestFromWeb } from "@aglyn/aglyn/server";
|
|
18
|
+
import { emailUnverifiedResponse, firebaseAdmin, isImpersonationSession } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { addAdminAudit } from "@aglyn/tenant-data-admin/server/admin-audit-write";
|
|
20
|
+
import { invalidIdTokenResponse } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
21
|
+
import { isPosHardwareOrderStatus, nextPosHardwareStatus, POS_HARDWARE_ORDER_ID, POS_HARDWARE_ORDERS_COLLECTION, POS_HARDWARE_STATUS_STAMP, posHardwareRefundable, posHardwareSaleRecord } from "../model/pos-hardware.js";
|
|
22
|
+
import { fulfillmentTrackingUrl } from "../model/tracking-url.js";
|
|
23
|
+
import { hardwareOrdersRef, posHardwareOrderNumber, posHardwareSaleRef } from "./pos-hardware.js";
|
|
24
|
+
import { sendPosHardwareEmail } from "./pos-hardware-notify.js";
|
|
25
|
+
import { posStripe } from "./pos-stripe.js";
|
|
26
|
+
/*==========================================
|
|
27
|
+
* `/api/commerce/admin/hardware-orders` (AGL-4038): the staff end of the POS
|
|
28
|
+
* hardware shop, for the Hardware orders staff page.
|
|
29
|
+
*
|
|
30
|
+
* GET every workspace's orders, newest first, optionally one status
|
|
31
|
+
* POST update record Stripe's order id and the tracking, or move the
|
|
32
|
+
* order one step on (Ordered → Shipped → Delivered →
|
|
33
|
+
* Registered); Shipped needs a tracking number
|
|
34
|
+
* cancel cancel and refund in full, before it ships
|
|
35
|
+
* refund refund in full, before it ships
|
|
36
|
+
*
|
|
37
|
+
* Staff only. Reading is open to every staff role; every write is `super` or
|
|
38
|
+
* `billing`, because each one changes what a customer is told or returns
|
|
39
|
+
* their money. Every write is an `adminAudit` row, and each one rewrites the
|
|
40
|
+
* order's retained sale record (`posHardwareSales/{orderId}`) from the order
|
|
41
|
+
* as it now stands, in the same transaction.
|
|
42
|
+
*=========================================*/ /** One page of the staff list. */ const PAGE = 25;
|
|
43
|
+
/** A page cursor is the path of the last order the previous page showed. */ const CURSOR = /^orgs\/[^/]+\/hardwareOrders\/[^/]+$/;
|
|
44
|
+
const WRITE_ROLES = [
|
|
45
|
+
'super',
|
|
46
|
+
'billing'
|
|
47
|
+
];
|
|
48
|
+
const json = (body, status = 200)=>Response.json(body, {
|
|
49
|
+
status,
|
|
50
|
+
headers: {
|
|
51
|
+
'Cache-Control': 'no-store'
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
/** An order as the staff page lists it. */ function staffView(id, order) {
|
|
55
|
+
var _order_contactEmail, _order_lines, _order_subtotalCents, _order_shipping, _order_taxCents, _order_totalCents, _order_stripeCheckoutSessionId, _order_stripePaymentIntentId, _order_stripeHardwareOrderId, _order_tracking, _order_createdAtMs, _order_updatedAtMs;
|
|
56
|
+
return {
|
|
57
|
+
id,
|
|
58
|
+
number: posHardwareOrderNumber(id),
|
|
59
|
+
orgId: order.orgId,
|
|
60
|
+
hostId: order.hostId,
|
|
61
|
+
status: order.status,
|
|
62
|
+
contactEmail: (_order_contactEmail = order.contactEmail) != null ? _order_contactEmail : null,
|
|
63
|
+
lines: (_order_lines = order.lines) != null ? _order_lines : [],
|
|
64
|
+
subtotalCents: (_order_subtotalCents = order.subtotalCents) != null ? _order_subtotalCents : 0,
|
|
65
|
+
shipping: (_order_shipping = order.shipping) != null ? _order_shipping : null,
|
|
66
|
+
taxCents: (_order_taxCents = order.taxCents) != null ? _order_taxCents : 0,
|
|
67
|
+
totalCents: (_order_totalCents = order.totalCents) != null ? _order_totalCents : 0,
|
|
68
|
+
stripeCheckoutSessionId: (_order_stripeCheckoutSessionId = order.stripeCheckoutSessionId) != null ? _order_stripeCheckoutSessionId : null,
|
|
69
|
+
stripePaymentIntentId: (_order_stripePaymentIntentId = order.stripePaymentIntentId) != null ? _order_stripePaymentIntentId : null,
|
|
70
|
+
stripeHardwareOrderId: (_order_stripeHardwareOrderId = order.stripeHardwareOrderId) != null ? _order_stripeHardwareOrderId : null,
|
|
71
|
+
tracking: (_order_tracking = order.tracking) != null ? _order_tracking : null,
|
|
72
|
+
livemode: order.livemode === true,
|
|
73
|
+
refundPending: Boolean(order.refundRequestedAtMs),
|
|
74
|
+
createdAtMs: (_order_createdAtMs = order.createdAtMs) != null ? _order_createdAtMs : 0,
|
|
75
|
+
updatedAtMs: (_order_updatedAtMs = order.updatedAtMs) != null ? _order_updatedAtMs : 0
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
let Refusal = class Refusal extends Error {
|
|
79
|
+
constructor(message, status){
|
|
80
|
+
super(message), this.status = status;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/** A tracking entry from the request, or `undefined` when none was sent. */ function trackingFrom(raw) {
|
|
84
|
+
var _ref, _ref1, _ref2;
|
|
85
|
+
if (raw === undefined) return undefined;
|
|
86
|
+
if (raw === null) return null;
|
|
87
|
+
const carrier = String((_ref = raw == null ? void 0 : raw.carrier) != null ? _ref : '').trim().slice(0, 60);
|
|
88
|
+
const number = String((_ref1 = raw == null ? void 0 : raw.number) != null ? _ref1 : '').trim().slice(0, 80);
|
|
89
|
+
const url = String((_ref2 = raw == null ? void 0 : raw.url) != null ? _ref2 : '').trim().slice(0, 500);
|
|
90
|
+
if (!carrier || !number) throw new Refusal('Enter the carrier and the tracking number.', 400);
|
|
91
|
+
return {
|
|
92
|
+
carrier,
|
|
93
|
+
number,
|
|
94
|
+
// An https link typed by staff wins; otherwise one is built for a known
|
|
95
|
+
// carrier, and an unknown one has none.
|
|
96
|
+
url: fulfillmentTrackingUrl({
|
|
97
|
+
carrier,
|
|
98
|
+
trackingNumber: number,
|
|
99
|
+
trackingUrl: url || null
|
|
100
|
+
})
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async function handler(request) {
|
|
104
|
+
var _headers_authorization;
|
|
105
|
+
const { method, query, body, headers: rawHeaders } = await pluginRequestFromWeb(request);
|
|
106
|
+
const headers = rawHeaders;
|
|
107
|
+
const authorization = (_headers_authorization = headers.authorization) != null ? _headers_authorization : '';
|
|
108
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
109
|
+
if (!idToken) return json({
|
|
110
|
+
error: 'Unauthenticated'
|
|
111
|
+
}, 401);
|
|
112
|
+
try {
|
|
113
|
+
var _decoded_staffRole, _input_action, _input_orgId, _input_orderId;
|
|
114
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
115
|
+
if (!decoded.email_verified && !isImpersonationSession(decoded)) return emailUnverifiedResponse();
|
|
116
|
+
if (!decoded['staff']) return json({
|
|
117
|
+
error: 'Staff only'
|
|
118
|
+
}, 403);
|
|
119
|
+
// A missing role claim reads as the least one, never as super.
|
|
120
|
+
const role = String((_decoded_staffRole = decoded['staffRole']) != null ? _decoded_staffRole : 'support');
|
|
121
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
122
|
+
if (method === 'GET') {
|
|
123
|
+
var _ref, _ref1, _ref2;
|
|
124
|
+
var _shown_;
|
|
125
|
+
const requested = String((_ref = query == null ? void 0 : query['status']) != null ? _ref : '');
|
|
126
|
+
let ordersQuery = firestore.collectionGroup(POS_HARDWARE_ORDERS_COLLECTION);
|
|
127
|
+
if (requested) {
|
|
128
|
+
if (!isPosHardwareOrderStatus(requested)) return json({
|
|
129
|
+
error: 'Unknown status'
|
|
130
|
+
}, 400);
|
|
131
|
+
ordersQuery = ordersQuery.where('status', '==', requested);
|
|
132
|
+
}
|
|
133
|
+
ordersQuery = ordersQuery.orderBy('createdAtMs', 'desc');
|
|
134
|
+
const after = String((_ref1 = query == null ? void 0 : query['after']) != null ? _ref1 : '');
|
|
135
|
+
if (after) {
|
|
136
|
+
if (!CURSOR.test(after)) return json({
|
|
137
|
+
error: 'Unknown page'
|
|
138
|
+
}, 400);
|
|
139
|
+
const cursor = await firestore.doc(after).get();
|
|
140
|
+
// An order deleted since the page was read starts the list over
|
|
141
|
+
// rather than answering an empty page.
|
|
142
|
+
if (cursor.exists) ordersQuery = ordersQuery.startAfter(cursor);
|
|
143
|
+
}
|
|
144
|
+
const snapshot = await ordersQuery.limit(PAGE + 1).get();
|
|
145
|
+
const docs = snapshot.docs;
|
|
146
|
+
const shown = docs.slice(0, PAGE);
|
|
147
|
+
return json({
|
|
148
|
+
orders: shown.map((doc)=>staffView(doc.id, doc.data())),
|
|
149
|
+
next: docs.length > PAGE ? (_ref2 = (_shown_ = shown[shown.length - 1]) == null ? void 0 : _shown_.ref.path) != null ? _ref2 : null : null,
|
|
150
|
+
pageSize: PAGE,
|
|
151
|
+
canWrite: WRITE_ROLES.includes(role)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (method !== 'POST') return json({
|
|
155
|
+
error: 'Method not allowed'
|
|
156
|
+
}, 405);
|
|
157
|
+
if (!WRITE_ROLES.includes(role)) {
|
|
158
|
+
return json({
|
|
159
|
+
error: 'Changing a hardware order requires the super or billing staff role.'
|
|
160
|
+
}, 403);
|
|
161
|
+
}
|
|
162
|
+
const input = body != null ? body : {};
|
|
163
|
+
const action = String((_input_action = input['action']) != null ? _input_action : '');
|
|
164
|
+
const orgId = String((_input_orgId = input['orgId']) != null ? _input_orgId : '');
|
|
165
|
+
const orderId = String((_input_orderId = input['orderId']) != null ? _input_orderId : '');
|
|
166
|
+
if (!orgId || !/^[A-Za-z0-9_-]{1,128}$/.test(orgId) || !POS_HARDWARE_ORDER_ID.test(orderId)) {
|
|
167
|
+
return json({
|
|
168
|
+
error: 'orgId or orderId is missing or malformed'
|
|
169
|
+
}, 400);
|
|
170
|
+
}
|
|
171
|
+
const ref = hardwareOrdersRef(orgId).doc(orderId);
|
|
172
|
+
const actor = {
|
|
173
|
+
actorUid: decoded.uid,
|
|
174
|
+
actorEmail: decoded.email ? String(decoded.email) : null
|
|
175
|
+
};
|
|
176
|
+
if (action === 'update') return await updateOrder(firestore, ref, orgId, orderId, input, actor);
|
|
177
|
+
if (action === 'cancel' || action === 'refund') {
|
|
178
|
+
return await refundOrder(firestore, ref, orgId, orderId, action, actor);
|
|
179
|
+
}
|
|
180
|
+
return json({
|
|
181
|
+
error: 'Unknown action'
|
|
182
|
+
}, 400);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof Refusal) return json({
|
|
185
|
+
error: error.message
|
|
186
|
+
}, error.status);
|
|
187
|
+
const unauthenticated = invalidIdTokenResponse(error);
|
|
188
|
+
if (unauthenticated) return unauthenticated;
|
|
189
|
+
console.error('[pos-hardware-admin]', error);
|
|
190
|
+
return json({
|
|
191
|
+
error: 'Hardware orders failed'
|
|
192
|
+
}, 500);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function updateOrder(firestore, ref, orgId, orderId, input, actor) {
|
|
196
|
+
var _outcome_before_stripeHardwareOrderId, _outcome_before_tracking, _outcome_after_stripeHardwareOrderId, _outcome_after_tracking;
|
|
197
|
+
const tracking = trackingFrom(input['tracking']);
|
|
198
|
+
const hardwareOrderIdRaw = input['stripeHardwareOrderId'];
|
|
199
|
+
const stripeHardwareOrderId = hardwareOrderIdRaw === undefined ? undefined : String(hardwareOrderIdRaw != null ? hardwareOrderIdRaw : '').trim().slice(0, 120) || null;
|
|
200
|
+
const advanceTo = input['status'] === undefined ? undefined : String(input['status']);
|
|
201
|
+
if (advanceTo !== undefined && !isPosHardwareOrderStatus(advanceTo)) {
|
|
202
|
+
return json({
|
|
203
|
+
error: 'Unknown status'
|
|
204
|
+
}, 400);
|
|
205
|
+
}
|
|
206
|
+
const outcome = await firestore.runTransaction(async (transaction)=>{
|
|
207
|
+
const snapshot = await transaction.get(ref);
|
|
208
|
+
if (!snapshot.exists) throw new Refusal('No such order', 404);
|
|
209
|
+
const before = snapshot.data();
|
|
210
|
+
if (before.status === 'canceled' || before.status === 'refunded') {
|
|
211
|
+
throw new Refusal('This order was canceled or refunded and can no longer change.', 409);
|
|
212
|
+
}
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
const patch = {
|
|
215
|
+
updatedAtMs: now
|
|
216
|
+
};
|
|
217
|
+
if (stripeHardwareOrderId !== undefined) patch['stripeHardwareOrderId'] = stripeHardwareOrderId;
|
|
218
|
+
if (tracking !== undefined) patch['tracking'] = tracking;
|
|
219
|
+
let reached = null;
|
|
220
|
+
if (advanceTo !== undefined && advanceTo !== before.status) {
|
|
221
|
+
if (nextPosHardwareStatus(before.status) !== advanceTo) {
|
|
222
|
+
throw new Refusal(`An order that is ${before.status} cannot move to ${advanceTo}.`, 409);
|
|
223
|
+
}
|
|
224
|
+
if (before.refundRequestedAtMs) throw new Refusal('A refund is in progress for this order.', 409);
|
|
225
|
+
const trackingAfter = tracking !== undefined ? tracking : before.tracking;
|
|
226
|
+
if (advanceTo === 'shipped' && !(trackingAfter == null ? void 0 : trackingAfter.number)) {
|
|
227
|
+
throw new Refusal('Add the tracking number before marking the order shipped.', 400);
|
|
228
|
+
}
|
|
229
|
+
patch['status'] = advanceTo;
|
|
230
|
+
patch[POS_HARDWARE_STATUS_STAMP[advanceTo]] = now;
|
|
231
|
+
reached = advanceTo;
|
|
232
|
+
}
|
|
233
|
+
const after = _extends({}, before, patch);
|
|
234
|
+
transaction.update(ref, patch);
|
|
235
|
+
transaction.set(posHardwareSaleRef(orderId), posHardwareSaleRecord(orderId, after));
|
|
236
|
+
return {
|
|
237
|
+
before,
|
|
238
|
+
after,
|
|
239
|
+
reached
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
await addAdminAudit(firestore, _extends({
|
|
243
|
+
action: 'pos-hardware-order-update'
|
|
244
|
+
}, actor, {
|
|
245
|
+
scope: 'posHardwareOrder',
|
|
246
|
+
target: `orgs/${orgId}/${POS_HARDWARE_ORDERS_COLLECTION}/${orderId}`,
|
|
247
|
+
orgId,
|
|
248
|
+
before: {
|
|
249
|
+
status: outcome.before.status,
|
|
250
|
+
stripeHardwareOrderId: (_outcome_before_stripeHardwareOrderId = outcome.before.stripeHardwareOrderId) != null ? _outcome_before_stripeHardwareOrderId : null,
|
|
251
|
+
tracking: (_outcome_before_tracking = outcome.before.tracking) != null ? _outcome_before_tracking : null
|
|
252
|
+
},
|
|
253
|
+
after: {
|
|
254
|
+
status: outcome.after.status,
|
|
255
|
+
stripeHardwareOrderId: (_outcome_after_stripeHardwareOrderId = outcome.after.stripeHardwareOrderId) != null ? _outcome_after_stripeHardwareOrderId : null,
|
|
256
|
+
tracking: (_outcome_after_tracking = outcome.after.tracking) != null ? _outcome_after_tracking : null
|
|
257
|
+
},
|
|
258
|
+
at: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
259
|
+
}));
|
|
260
|
+
// Told once per step: the transaction above lets only one caller reach it.
|
|
261
|
+
if (outcome.reached === 'shipped') await sendPosHardwareEmail('shipped', orderId, outcome.after);
|
|
262
|
+
if (outcome.reached === 'delivered') await sendPosHardwareEmail('delivered', orderId, outcome.after);
|
|
263
|
+
return json({
|
|
264
|
+
ok: true,
|
|
265
|
+
order: staffView(orderId, outcome.after)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
async function refundOrder(firestore, ref, orgId, orderId, action, actor) {
|
|
269
|
+
var _ref, _refund_body_amount;
|
|
270
|
+
var _refund_body;
|
|
271
|
+
const finalStatus = action === 'cancel' ? 'canceled' : 'refunded';
|
|
272
|
+
// Claim the refund under the lock first, so the order cannot be marked
|
|
273
|
+
// shipped while the money is on its way back, and two presses refund once.
|
|
274
|
+
const claim = await firestore.runTransaction(async (transaction)=>{
|
|
275
|
+
const snapshot = await transaction.get(ref);
|
|
276
|
+
if (!snapshot.exists) throw new Refusal('No such order', 404);
|
|
277
|
+
const order = snapshot.data();
|
|
278
|
+
if (order.status === finalStatus) return {
|
|
279
|
+
order,
|
|
280
|
+
already: true
|
|
281
|
+
};
|
|
282
|
+
if (!posHardwareRefundable(order.status)) {
|
|
283
|
+
throw new Refusal('This order has shipped, so it can no longer be canceled or refunded here. Arrange a return with Stripe instead.', 409);
|
|
284
|
+
}
|
|
285
|
+
if (!order.stripePaymentIntentId) throw new Refusal('This order has no payment to refund.', 409);
|
|
286
|
+
transaction.update(ref, {
|
|
287
|
+
refundRequestedAtMs: Date.now()
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
order,
|
|
291
|
+
already: false
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
if (claim.already) return json({
|
|
295
|
+
ok: true,
|
|
296
|
+
order: staffView(orderId, claim.order)
|
|
297
|
+
});
|
|
298
|
+
const refund = await posStripe('POST', 'refunds', {
|
|
299
|
+
params: {
|
|
300
|
+
payment_intent: claim.order.stripePaymentIntentId,
|
|
301
|
+
'metadata[type]': 'pos-hardware-order',
|
|
302
|
+
'metadata[orderId]': orderId,
|
|
303
|
+
'metadata[orgId]': orgId
|
|
304
|
+
},
|
|
305
|
+
// One refund per order, whichever of the two acts asked for it.
|
|
306
|
+
idempotencyKey: `pos-hw-refund-${orderId}`
|
|
307
|
+
});
|
|
308
|
+
if (!refund.ok || !((_refund_body = refund.body) == null ? void 0 : _refund_body.id)) {
|
|
309
|
+
var _ref1;
|
|
310
|
+
var _refund_body_error, _refund_body1, _refund_body_error1, _refund_body2;
|
|
311
|
+
await ref.update({
|
|
312
|
+
refundRequestedAtMs: firebaseAdmin.firestore.FieldValue.delete()
|
|
313
|
+
}).catch(()=>undefined);
|
|
314
|
+
console.error('[pos-hardware-admin] refund refused', orderId, refund.status, (_refund_body1 = refund.body) == null ? void 0 : (_refund_body_error = _refund_body1.error) == null ? void 0 : _refund_body_error.code);
|
|
315
|
+
return json({
|
|
316
|
+
error: String((_ref1 = (_refund_body2 = refund.body) == null ? void 0 : (_refund_body_error1 = _refund_body2.error) == null ? void 0 : _refund_body_error1.message) != null ? _ref1 : '') || 'Stripe refused the refund.'
|
|
317
|
+
}, 502);
|
|
318
|
+
}
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
const patch = {
|
|
321
|
+
status: finalStatus,
|
|
322
|
+
[POS_HARDWARE_STATUS_STAMP[finalStatus]]: now,
|
|
323
|
+
updatedAtMs: now,
|
|
324
|
+
refundId: String(refund.body.id),
|
|
325
|
+
refundedCents: Number((_ref = (_refund_body_amount = refund.body.amount) != null ? _refund_body_amount : claim.order.totalCents) != null ? _ref : 0),
|
|
326
|
+
refundRequestedAtMs: firebaseAdmin.firestore.FieldValue.delete()
|
|
327
|
+
};
|
|
328
|
+
const settled = _extends({}, claim.order);
|
|
329
|
+
delete settled.refundRequestedAtMs;
|
|
330
|
+
const after = _extends({}, settled, {
|
|
331
|
+
status: finalStatus,
|
|
332
|
+
[POS_HARDWARE_STATUS_STAMP[finalStatus]]: now,
|
|
333
|
+
updatedAtMs: now,
|
|
334
|
+
refundId: patch.refundId,
|
|
335
|
+
refundedCents: patch.refundedCents
|
|
336
|
+
});
|
|
337
|
+
// The order and its retained sale record move together: the record is
|
|
338
|
+
// what the refund is accounted from once the workspace is gone.
|
|
339
|
+
await firestore.runTransaction(async (transaction)=>{
|
|
340
|
+
transaction.update(ref, patch);
|
|
341
|
+
transaction.set(posHardwareSaleRef(orderId), posHardwareSaleRecord(orderId, after));
|
|
342
|
+
});
|
|
343
|
+
await addAdminAudit(firestore, _extends({
|
|
344
|
+
action: `pos-hardware-order-${action}`
|
|
345
|
+
}, actor, {
|
|
346
|
+
scope: 'posHardwareOrder',
|
|
347
|
+
target: `orgs/${orgId}/${POS_HARDWARE_ORDERS_COLLECTION}/${orderId}`,
|
|
348
|
+
orgId,
|
|
349
|
+
before: {
|
|
350
|
+
status: claim.order.status
|
|
351
|
+
},
|
|
352
|
+
after: {
|
|
353
|
+
status: finalStatus,
|
|
354
|
+
refundId: patch.refundId,
|
|
355
|
+
refundedCents: patch.refundedCents
|
|
356
|
+
},
|
|
357
|
+
at: firebaseAdmin.firestore.FieldValue.serverTimestamp()
|
|
358
|
+
}));
|
|
359
|
+
return json({
|
|
360
|
+
ok: true,
|
|
361
|
+
order: staffView(orderId, after)
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
export { handler as posHardwareAdminOrders };
|
|
365
|
+
|
|
366
|
+
//# sourceMappingURL=pos-hardware-admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-hardware-admin.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 { pluginRequestFromWeb } from '@aglyn/aglyn/server'\nimport {\n emailUnverifiedResponse,\n firebaseAdmin,\n isImpersonationSession,\n} from '@aglyn/tenant-data-admin'\nimport { addAdminAudit } from '@aglyn/tenant-data-admin/server/admin-audit-write'\nimport { invalidIdTokenResponse } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport {\n isPosHardwareOrderStatus,\n nextPosHardwareStatus,\n POS_HARDWARE_ORDER_ID,\n POS_HARDWARE_ORDERS_COLLECTION,\n POS_HARDWARE_STATUS_STAMP,\n posHardwareRefundable,\n posHardwareSaleRecord,\n type PosHardwareOrder,\n type PosHardwareOrderStatus,\n type PosHardwareTracking,\n} from '../model/pos-hardware'\nimport { fulfillmentTrackingUrl } from '../model/tracking-url'\nimport { hardwareOrdersRef, posHardwareOrderNumber, posHardwareSaleRef } from './pos-hardware'\nimport { sendPosHardwareEmail } from './pos-hardware-notify'\nimport { posStripe } from './pos-stripe'\n\n/*==========================================\n * `/api/commerce/admin/hardware-orders` (AGL-4038): the staff end of the POS\n * hardware shop, for the Hardware orders staff page.\n *\n * GET every workspace's orders, newest first, optionally one status\n * POST update record Stripe's order id and the tracking, or move the\n * order one step on (Ordered → Shipped → Delivered →\n * Registered); Shipped needs a tracking number\n * cancel cancel and refund in full, before it ships\n * refund refund in full, before it ships\n *\n * Staff only. Reading is open to every staff role; every write is `super` or\n * `billing`, because each one changes what a customer is told or returns\n * their money. Every write is an `adminAudit` row, and each one rewrites the\n * order's retained sale record (`posHardwareSales/{orderId}`) from the order\n * as it now stands, in the same transaction.\n *=========================================*/\n\n/** One page of the staff list. */\nconst PAGE = 25\n/** A page cursor is the path of the last order the previous page showed. */\nconst CURSOR = /^orgs\\/[^/]+\\/hardwareOrders\\/[^/]+$/\nconst WRITE_ROLES = ['super', 'billing']\n\nconst json = (body: unknown, status = 200) =>\n Response.json(body, { status, headers: { 'Cache-Control': 'no-store' } })\n\n/** An order as the staff page lists it. */\nfunction staffView(id: string, order: PosHardwareOrder) {\n return {\n id,\n number: posHardwareOrderNumber(id),\n orgId: order.orgId,\n hostId: order.hostId,\n status: order.status,\n contactEmail: order.contactEmail ?? null,\n lines: order.lines ?? [],\n subtotalCents: order.subtotalCents ?? 0,\n shipping: order.shipping ?? null,\n taxCents: order.taxCents ?? 0,\n totalCents: order.totalCents ?? 0,\n stripeCheckoutSessionId: order.stripeCheckoutSessionId ?? null,\n stripePaymentIntentId: order.stripePaymentIntentId ?? null,\n stripeHardwareOrderId: order.stripeHardwareOrderId ?? null,\n tracking: order.tracking ?? null,\n livemode: order.livemode === true,\n refundPending: Boolean((order as { refundRequestedAtMs?: number }).refundRequestedAtMs),\n createdAtMs: order.createdAtMs ?? 0,\n updatedAtMs: order.updatedAtMs ?? 0,\n }\n}\n\nexport type StaffHardwareOrderView = ReturnType<typeof staffView>\n\nclass Refusal extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message)\n }\n}\n\n/** A tracking entry from the request, or `undefined` when none was sent. */\nfunction trackingFrom(raw: unknown): PosHardwareTracking | null | undefined {\n if (raw === undefined) return undefined\n if (raw === null) return null\n const carrier = String((raw as { carrier?: unknown })?.carrier ?? '').trim().slice(0, 60)\n const number = String((raw as { number?: unknown })?.number ?? '').trim().slice(0, 80)\n const url = String((raw as { url?: unknown })?.url ?? '').trim().slice(0, 500)\n if (!carrier || !number) throw new Refusal('Enter the carrier and the tracking number.', 400)\n return {\n carrier,\n number,\n // An https link typed by staff wins; otherwise one is built for a known\n // carrier, and an unknown one has none.\n url: fulfillmentTrackingUrl({ carrier, trackingNumber: number, trackingUrl: url || null }),\n }\n}\n\nasync function handler(request: Request): Promise<Response> {\n const { method, query, body, headers: rawHeaders } = await pluginRequestFromWeb(request)\n const headers = rawHeaders as Partial<Record<string, string>>\n const authorization = headers.authorization ?? ''\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return json({ error: 'Unauthenticated' }, 401)\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n if (!decoded.email_verified && !isImpersonationSession(decoded)) return emailUnverifiedResponse()\n if (!decoded['staff']) return json({ error: 'Staff only' }, 403)\n // A missing role claim reads as the least one, never as super.\n const role = String(decoded['staffRole'] ?? 'support')\n const firestore = firebaseAdmin.app().firestore()\n\n if (method === 'GET') {\n const requested = String(query?.['status'] ?? '')\n let ordersQuery: any = firestore.collectionGroup(POS_HARDWARE_ORDERS_COLLECTION)\n if (requested) {\n if (!isPosHardwareOrderStatus(requested)) return json({ error: 'Unknown status' }, 400)\n ordersQuery = ordersQuery.where('status', '==', requested)\n }\n ordersQuery = ordersQuery.orderBy('createdAtMs', 'desc')\n const after = String(query?.['after'] ?? '')\n if (after) {\n if (!CURSOR.test(after)) return json({ error: 'Unknown page' }, 400)\n const cursor = await firestore.doc(after).get()\n // An order deleted since the page was read starts the list over\n // rather than answering an empty page.\n if (cursor.exists) ordersQuery = ordersQuery.startAfter(cursor)\n }\n const snapshot = await ordersQuery.limit(PAGE + 1).get()\n const docs = snapshot.docs as Array<{ id: string; ref: { path: string }; data: () => PosHardwareOrder }>\n const shown = docs.slice(0, PAGE)\n return json({\n orders: shown.map((doc) => staffView(doc.id, doc.data())),\n next: docs.length > PAGE ? (shown[shown.length - 1]?.ref.path ?? null) : null,\n pageSize: PAGE,\n canWrite: WRITE_ROLES.includes(role),\n })\n }\n\n if (method !== 'POST') return json({ error: 'Method not allowed' }, 405)\n if (!WRITE_ROLES.includes(role)) {\n return json({ error: 'Changing a hardware order requires the super or billing staff role.' }, 403)\n }\n const input = (body ?? {}) as Record<string, unknown>\n const action = String(input['action'] ?? '')\n const orgId = String(input['orgId'] ?? '')\n const orderId = String(input['orderId'] ?? '')\n if (!orgId || !/^[A-Za-z0-9_-]{1,128}$/.test(orgId) || !POS_HARDWARE_ORDER_ID.test(orderId)) {\n return json({ error: 'orgId or orderId is missing or malformed' }, 400)\n }\n const ref = hardwareOrdersRef(orgId).doc(orderId)\n const actor = { actorUid: decoded.uid, actorEmail: decoded.email ? String(decoded.email) : null }\n\n if (action === 'update') return await updateOrder(firestore, ref, orgId, orderId, input, actor)\n if (action === 'cancel' || action === 'refund') {\n return await refundOrder(firestore, ref, orgId, orderId, action, actor)\n }\n return json({ error: 'Unknown action' }, 400)\n } catch (error) {\n if (error instanceof Refusal) return json({ error: error.message }, error.status)\n const unauthenticated = invalidIdTokenResponse(error)\n if (unauthenticated) return unauthenticated\n console.error('[pos-hardware-admin]', error)\n return json({ error: 'Hardware orders failed' }, 500)\n }\n}\n\ntype Actor = { actorUid: string; actorEmail: string | null }\n\nasync function updateOrder(\n firestore: any,\n ref: any,\n orgId: string,\n orderId: string,\n input: Record<string, unknown>,\n actor: Actor,\n): Promise<Response> {\n const tracking = trackingFrom(input['tracking'])\n const hardwareOrderIdRaw = input['stripeHardwareOrderId']\n const stripeHardwareOrderId =\n hardwareOrderIdRaw === undefined ? undefined : String(hardwareOrderIdRaw ?? '').trim().slice(0, 120) || null\n const advanceTo = input['status'] === undefined ? undefined : String(input['status'])\n if (advanceTo !== undefined && !isPosHardwareOrderStatus(advanceTo)) {\n return json({ error: 'Unknown status' }, 400)\n }\n\n const outcome = await firestore.runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) throw new Refusal('No such order', 404)\n const before = snapshot.data() as PosHardwareOrder & { refundRequestedAtMs?: number }\n if (before.status === 'canceled' || before.status === 'refunded') {\n throw new Refusal('This order was canceled or refunded and can no longer change.', 409)\n }\n const now = Date.now()\n const patch: Record<string, unknown> = { updatedAtMs: now }\n if (stripeHardwareOrderId !== undefined) patch['stripeHardwareOrderId'] = stripeHardwareOrderId\n if (tracking !== undefined) patch['tracking'] = tracking\n let reached: PosHardwareOrderStatus | null = null\n if (advanceTo !== undefined && advanceTo !== before.status) {\n if (nextPosHardwareStatus(before.status) !== advanceTo) {\n throw new Refusal(`An order that is ${before.status} cannot move to ${advanceTo}.`, 409)\n }\n if (before.refundRequestedAtMs) throw new Refusal('A refund is in progress for this order.', 409)\n const trackingAfter = tracking !== undefined ? tracking : before.tracking\n if (advanceTo === 'shipped' && !trackingAfter?.number) {\n throw new Refusal('Add the tracking number before marking the order shipped.', 400)\n }\n patch['status'] = advanceTo\n patch[POS_HARDWARE_STATUS_STAMP[advanceTo]] = now\n reached = advanceTo\n }\n const after = { ...before, ...patch } as PosHardwareOrder\n transaction.update(ref, patch)\n transaction.set(posHardwareSaleRef(orderId), posHardwareSaleRecord(orderId, after))\n return { before, after, reached }\n })\n\n await addAdminAudit(firestore, {\n action: 'pos-hardware-order-update',\n ...actor,\n scope: 'posHardwareOrder',\n target: `orgs/${orgId}/${POS_HARDWARE_ORDERS_COLLECTION}/${orderId}`,\n orgId,\n before: {\n status: outcome.before.status,\n stripeHardwareOrderId: outcome.before.stripeHardwareOrderId ?? null,\n tracking: outcome.before.tracking ?? null,\n },\n after: {\n status: outcome.after.status,\n stripeHardwareOrderId: outcome.after.stripeHardwareOrderId ?? null,\n tracking: outcome.after.tracking ?? null,\n },\n at: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n // Told once per step: the transaction above lets only one caller reach it.\n if (outcome.reached === 'shipped') await sendPosHardwareEmail('shipped', orderId, outcome.after)\n if (outcome.reached === 'delivered') await sendPosHardwareEmail('delivered', orderId, outcome.after)\n return json({ ok: true, order: staffView(orderId, outcome.after) })\n}\n\nasync function refundOrder(\n firestore: any,\n ref: any,\n orgId: string,\n orderId: string,\n action: 'cancel' | 'refund',\n actor: Actor,\n): Promise<Response> {\n const finalStatus: PosHardwareOrderStatus = action === 'cancel' ? 'canceled' : 'refunded'\n // Claim the refund under the lock first, so the order cannot be marked\n // shipped while the money is on its way back, and two presses refund once.\n const claim = await firestore.runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) throw new Refusal('No such order', 404)\n const order = snapshot.data() as PosHardwareOrder & { refundRequestedAtMs?: number }\n if (order.status === finalStatus) return { order, already: true }\n if (!posHardwareRefundable(order.status)) {\n throw new Refusal(\n 'This order has shipped, so it can no longer be canceled or refunded here. Arrange a return with Stripe instead.',\n 409,\n )\n }\n if (!order.stripePaymentIntentId) throw new Refusal('This order has no payment to refund.', 409)\n transaction.update(ref, { refundRequestedAtMs: Date.now() })\n return { order, already: false }\n })\n if (claim.already) return json({ ok: true, order: staffView(orderId, claim.order) })\n\n const refund = await posStripe('POST', 'refunds', {\n params: {\n payment_intent: claim.order.stripePaymentIntentId,\n 'metadata[type]': 'pos-hardware-order',\n 'metadata[orderId]': orderId,\n 'metadata[orgId]': orgId,\n },\n // One refund per order, whichever of the two acts asked for it.\n idempotencyKey: `pos-hw-refund-${orderId}`,\n })\n if (!refund.ok || !refund.body?.id) {\n await ref.update({ refundRequestedAtMs: firebaseAdmin.firestore.FieldValue.delete() }).catch(() => undefined)\n console.error('[pos-hardware-admin] refund refused', orderId, refund.status, refund.body?.error?.code)\n return json({ error: String(refund.body?.error?.message ?? '') || 'Stripe refused the refund.' }, 502)\n }\n const now = Date.now()\n const patch = {\n status: finalStatus,\n [POS_HARDWARE_STATUS_STAMP[finalStatus]]: now,\n updatedAtMs: now,\n refundId: String(refund.body.id),\n refundedCents: Number(refund.body.amount ?? claim.order.totalCents ?? 0),\n refundRequestedAtMs: firebaseAdmin.firestore.FieldValue.delete(),\n }\n const settled = { ...claim.order }\n delete settled.refundRequestedAtMs\n const after: PosHardwareOrder = {\n ...settled,\n status: finalStatus,\n [POS_HARDWARE_STATUS_STAMP[finalStatus]]: now,\n updatedAtMs: now,\n refundId: patch.refundId,\n refundedCents: patch.refundedCents,\n }\n // The order and its retained sale record move together: the record is\n // what the refund is accounted from once the workspace is gone.\n await firestore.runTransaction(async (transaction: any) => {\n transaction.update(ref, patch)\n transaction.set(posHardwareSaleRef(orderId), posHardwareSaleRecord(orderId, after))\n })\n await addAdminAudit(firestore, {\n action: `pos-hardware-order-${action}`,\n ...actor,\n scope: 'posHardwareOrder',\n target: `orgs/${orgId}/${POS_HARDWARE_ORDERS_COLLECTION}/${orderId}`,\n orgId,\n before: { status: claim.order.status },\n after: { status: finalStatus, refundId: patch.refundId, refundedCents: patch.refundedCents },\n at: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n return json({ ok: true, order: staffView(orderId, after) })\n}\n\nexport { handler as posHardwareAdminOrders }\n"],"names":["pluginRequestFromWeb","emailUnverifiedResponse","firebaseAdmin","isImpersonationSession","addAdminAudit","invalidIdTokenResponse","isPosHardwareOrderStatus","nextPosHardwareStatus","POS_HARDWARE_ORDER_ID","POS_HARDWARE_ORDERS_COLLECTION","POS_HARDWARE_STATUS_STAMP","posHardwareRefundable","posHardwareSaleRecord","fulfillmentTrackingUrl","hardwareOrdersRef","posHardwareOrderNumber","posHardwareSaleRef","sendPosHardwareEmail","posStripe","PAGE","CURSOR","WRITE_ROLES","json","body","status","Response","headers","staffView","id","order","number","orgId","hostId","contactEmail","lines","subtotalCents","shipping","taxCents","totalCents","stripeCheckoutSessionId","stripePaymentIntentId","stripeHardwareOrderId","tracking","livemode","refundPending","Boolean","refundRequestedAtMs","createdAtMs","updatedAtMs","Refusal","Error","message","trackingFrom","raw","undefined","carrier","String","trim","slice","url","trackingNumber","trackingUrl","handler","request","method","query","rawHeaders","authorization","idToken","startsWith","length","error","decoded","input","app","auth","verifyIdToken","email_verified","role","firestore","shown","requested","ordersQuery","collectionGroup","where","orderBy","after","test","cursor","doc","get","exists","startAfter","snapshot","limit","docs","orders","map","data","next","ref","path","pageSize","canWrite","includes","action","orderId","actor","actorUid","uid","actorEmail","email","updateOrder","refundOrder","unauthenticated","console","outcome","hardwareOrderIdRaw","advanceTo","runTransaction","transaction","before","now","Date","patch","reached","trackingAfter","update","set","scope","target","at","FieldValue","serverTimestamp","ok","refund","finalStatus","claim","already","params","payment_intent","idempotencyKey","delete","catch","code","refundId","refundedCents","Number","amount","settled","posHardwareAdminOrders"],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { POS_HARDWARE_EMAIL_KEYS } from '@aglyn/shared-util-email';
|
|
18
|
+
import { type PosHardwareOrder } from '../model/pos-hardware';
|
|
19
|
+
export type PosHardwareEmailKind = keyof typeof POS_HARDWARE_EMAIL_KEYS;
|
|
20
|
+
/** The merge values for one order's emails. Exported for the render spec. */
|
|
21
|
+
export declare function posHardwareEmailMerge(orderId: string, order: PosHardwareOrder, names: {
|
|
22
|
+
site: string;
|
|
23
|
+
org: string;
|
|
24
|
+
}): Promise<Record<string, string>>;
|
|
25
|
+
export declare function sendPosHardwareEmail(kind: PosHardwareEmailKind, orderId: string, order: PosHardwareOrder): Promise<void>;
|
|
26
|
+
/** "New hardware order", once per order. */
|
|
27
|
+
export declare function raisePosHardwareOrderAlert(orderId: string, order: PosHardwareOrder): Promise<void>;
|
|
@@ -0,0 +1,142 @@
|
|
|
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 { formatOperatorAlertAmount } from "@aglyn/aglyn/app-utils/operator-alerts";
|
|
18
|
+
import { isAccountBanLockdownReason, normalizeOrgLockdown } from "@aglyn/aglyn/app-utils/lockdown";
|
|
19
|
+
import { POS_HARDWARE_EMAIL_KEYS, sendEmail } from "@aglyn/shared-util-email";
|
|
20
|
+
import { firebaseAdmin, hostDisplayName, meterPlatformEmail, raiseOperatorAlert } from "@aglyn/tenant-data-admin";
|
|
21
|
+
import { filterSuppressedEmails } from "@aglyn/tenant-data-admin/server/email-suppression";
|
|
22
|
+
import { orgSystemEmailBrand, renderSystemEmailContent } from "@aglyn/tenant-data-admin/server/render-system-email";
|
|
23
|
+
import { COMMERCE_POS_HARDWARE_ORDER } from "../constants/operator-alerts.js";
|
|
24
|
+
import { carrierLabelFor } from "../model/tracking-url.js";
|
|
25
|
+
import { formatPosHardwareCents, POS_HARDWARE_ANCHOR } from "../model/pos-hardware.js";
|
|
26
|
+
import { posHardwareOrderNumber, posSettingsUrl } from "./pos-hardware.js";
|
|
27
|
+
/** The lines as the emails list them, one per item. */ function itemsText(order) {
|
|
28
|
+
var _order_lines;
|
|
29
|
+
return ((_order_lines = order.lines) != null ? _order_lines : []).map((line)=>`${line.name} × ${line.quantity}`).join('\n');
|
|
30
|
+
}
|
|
31
|
+
function shipToText(order) {
|
|
32
|
+
var _order_shipping, _order_shipping1;
|
|
33
|
+
const address = (_order_shipping = order.shipping) == null ? void 0 : _order_shipping.address;
|
|
34
|
+
const cityLine = address ? [
|
|
35
|
+
address.city,
|
|
36
|
+
[
|
|
37
|
+
address.state,
|
|
38
|
+
address.postalCode
|
|
39
|
+
].filter(Boolean).join(' ')
|
|
40
|
+
].filter(Boolean).join(', ') : '';
|
|
41
|
+
return [
|
|
42
|
+
(_order_shipping1 = order.shipping) == null ? void 0 : _order_shipping1.name,
|
|
43
|
+
address == null ? void 0 : address.line1,
|
|
44
|
+
address == null ? void 0 : address.line2,
|
|
45
|
+
cityLine
|
|
46
|
+
].filter(Boolean).join('\n');
|
|
47
|
+
}
|
|
48
|
+
async function siteAndOrgNames(order) {
|
|
49
|
+
var _ref;
|
|
50
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
51
|
+
const [host, org] = await Promise.all([
|
|
52
|
+
firestore.collection('hosts').doc(order.hostId).get().catch(()=>null),
|
|
53
|
+
firestore.collection('orgs').doc(order.orgId).get().catch(()=>null)
|
|
54
|
+
]);
|
|
55
|
+
const lock = normalizeOrgLockdown(org == null ? void 0 : org.data());
|
|
56
|
+
return {
|
|
57
|
+
site: (host == null ? void 0 : host.exists) ? hostDisplayName(host.data(), order.hostId) : 'your site',
|
|
58
|
+
org: String((_ref = org == null ? void 0 : org.get('name')) != null ? _ref : '').trim() || 'your workspace',
|
|
59
|
+
ban: Boolean(lock && isAccountBanLockdownReason(lock.reason))
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/** The fallback subject and first line, when no design renders. */ const FALLBACK = {
|
|
63
|
+
received: (number)=>`We have your card reader order ${number}`,
|
|
64
|
+
shipped: (number)=>`Your card reader order ${number} has shipped`,
|
|
65
|
+
delivered: (number)=>`Your card reader order ${number} was delivered`
|
|
66
|
+
};
|
|
67
|
+
/** The merge values for one order's emails. Exported for the render spec. */ export async function posHardwareEmailMerge(orderId, order, names) {
|
|
68
|
+
var _order_totalCents, _ref, _ref1, _ref2;
|
|
69
|
+
var _order_shipping, _order_tracking, _order_tracking1, _order_tracking2;
|
|
70
|
+
const base = await posSettingsUrl(order.hostId);
|
|
71
|
+
const [path] = base.split('#');
|
|
72
|
+
const ordersUrl = `${path}#${POS_HARDWARE_ANCHOR}`;
|
|
73
|
+
return {
|
|
74
|
+
'org.name': names.org,
|
|
75
|
+
'site.name': names.site,
|
|
76
|
+
'order.number': posHardwareOrderNumber(orderId),
|
|
77
|
+
'order.items': itemsText(order),
|
|
78
|
+
'order.total': formatPosHardwareCents((_order_totalCents = order.totalCents) != null ? _order_totalCents : 0),
|
|
79
|
+
'order.shipTo': shipToText(order),
|
|
80
|
+
'order.shippingMethod': (_ref = (_order_shipping = order.shipping) == null ? void 0 : _order_shipping.label) != null ? _ref : '',
|
|
81
|
+
'order.carrier': carrierLabelFor((_order_tracking = order.tracking) == null ? void 0 : _order_tracking.carrier) || 'the carrier',
|
|
82
|
+
'order.trackingNumber': (_ref1 = (_order_tracking1 = order.tracking) == null ? void 0 : _order_tracking1.number) != null ? _ref1 : '',
|
|
83
|
+
// An unknown carrier has no tracker to link; the card shows the number.
|
|
84
|
+
'order.trackingUrl': (_ref2 = (_order_tracking2 = order.tracking) == null ? void 0 : _order_tracking2.url) != null ? _ref2 : ordersUrl,
|
|
85
|
+
'order.ordersUrl': ordersUrl,
|
|
86
|
+
'order.setupUrl': `${path}#pos-devices`
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export async function sendPosHardwareEmail(kind, orderId, order) {
|
|
90
|
+
try {
|
|
91
|
+
var _order_contactEmail;
|
|
92
|
+
const to = String((_order_contactEmail = order.contactEmail) != null ? _order_contactEmail : '').trim();
|
|
93
|
+
if (!to) return;
|
|
94
|
+
const names = await siteAndOrgNames(order);
|
|
95
|
+
// A ban is a ban (AGL-3420): after its notice the account is sent nothing.
|
|
96
|
+
if (names.ban) return;
|
|
97
|
+
const [recipient] = await filterSuppressedEmails([
|
|
98
|
+
to
|
|
99
|
+
]);
|
|
100
|
+
if (!recipient) return;
|
|
101
|
+
const merge = await posHardwareEmailMerge(orderId, order, names);
|
|
102
|
+
const subject = FALLBACK[kind](merge['order.number']);
|
|
103
|
+
const brand = await orgSystemEmailBrand(order.orgId);
|
|
104
|
+
const content = await renderSystemEmailContent(POS_HARDWARE_EMAIL_KEYS[kind], merge, brand, {
|
|
105
|
+
subject,
|
|
106
|
+
text: `${subject}\n\n${merge['order.items']}\n\n${merge['order.ordersUrl']}`
|
|
107
|
+
});
|
|
108
|
+
const result = await sendEmail(_extends({
|
|
109
|
+
to: recipient
|
|
110
|
+
}, content, brand.fromName ? {
|
|
111
|
+
fromName: brand.fromName
|
|
112
|
+
} : {}, {
|
|
113
|
+
context: POS_HARDWARE_EMAIL_KEYS[kind]
|
|
114
|
+
}));
|
|
115
|
+
if (result.sent) await meterPlatformEmail(1);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('[pos-hardware] email failed', kind, orderId, error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** "New hardware order", once per order. */ export async function raisePosHardwareOrderAlert(orderId, order) {
|
|
121
|
+
try {
|
|
122
|
+
var _order_totalCents, _order_lines, _ref;
|
|
123
|
+
var _order_shipping;
|
|
124
|
+
const names = await siteAndOrgNames(order);
|
|
125
|
+
await raiseOperatorAlert(COMMERCE_POS_HARDWARE_ORDER, {
|
|
126
|
+
dedupeKey: orderId,
|
|
127
|
+
orgId: order.orgId,
|
|
128
|
+
hostId: order.hostId,
|
|
129
|
+
context: {
|
|
130
|
+
orderNumber: posHardwareOrderNumber(orderId),
|
|
131
|
+
amount: formatOperatorAlertAmount((_order_totalCents = order.totalCents) != null ? _order_totalCents : 0),
|
|
132
|
+
site: `${names.site} (${order.hostId})`,
|
|
133
|
+
items: ((_order_lines = order.lines) != null ? _order_lines : []).map((line)=>`${line.quantity} × ${line.name}`).join(', '),
|
|
134
|
+
shipping: (_ref = (_order_shipping = order.shipping) == null ? void 0 : _order_shipping.label) != null ? _ref : 'shipping'
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.error('[pos-hardware] operator alert failed', orderId, error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
//# sourceMappingURL=pos-hardware-notify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/pos-hardware-notify.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 { formatOperatorAlertAmount } from '@aglyn/aglyn/app-utils/operator-alerts'\nimport {\n isAccountBanLockdownReason,\n normalizeOrgLockdown,\n} from '@aglyn/aglyn/app-utils/lockdown'\nimport { POS_HARDWARE_EMAIL_KEYS, sendEmail } from '@aglyn/shared-util-email'\nimport {\n firebaseAdmin,\n hostDisplayName,\n meterPlatformEmail,\n raiseOperatorAlert,\n} from '@aglyn/tenant-data-admin'\nimport { filterSuppressedEmails } from '@aglyn/tenant-data-admin/server/email-suppression'\nimport {\n orgSystemEmailBrand,\n renderSystemEmailContent,\n} from '@aglyn/tenant-data-admin/server/render-system-email'\nimport { COMMERCE_POS_HARDWARE_ORDER } from '../constants/operator-alerts'\nimport { carrierLabelFor } from '../model/tracking-url'\nimport { formatPosHardwareCents, POS_HARDWARE_ANCHOR, type PosHardwareOrder } from '../model/pos-hardware'\nimport { posHardwareOrderNumber, posSettingsUrl } from './pos-hardware'\n\n/*==========================================\n * TELLING PEOPLE ABOUT A HARDWARE ORDER (AGL-4038).\n *\n * The buyer gets a system email at each step they wait on: received, shipped\n * (with the carrier and tracking link) and delivered (with the way to set the\n * reader up). Staff get the \"New hardware order\" operator alert, because in\n * phase 1 a person places every order with Stripe.\n *\n * Never throws. The order is already recorded when any of these run, and a\n * lost email must not undo it or turn a webhook into a redelivery.\n *=========================================*/\n\nexport type PosHardwareEmailKind = keyof typeof POS_HARDWARE_EMAIL_KEYS\n\n/** The lines as the emails list them, one per item. */\nfunction itemsText(order: PosHardwareOrder): string {\n return (order.lines ?? []).map((line) => `${line.name} × ${line.quantity}`).join('\\n')\n}\n\nfunction shipToText(order: PosHardwareOrder): string {\n const address = order.shipping?.address\n const cityLine = address\n ? [address.city, [address.state, address.postalCode].filter(Boolean).join(' ')].filter(Boolean).join(', ')\n : ''\n return [order.shipping?.name, address?.line1, address?.line2, cityLine].filter(Boolean).join('\\n')\n}\n\nasync function siteAndOrgNames(order: PosHardwareOrder): Promise<{ site: string; org: string; ban: boolean }> {\n const firestore = firebaseAdmin.app().firestore()\n const [host, org] = await Promise.all([\n firestore.collection('hosts').doc(order.hostId).get().catch(() => null),\n firestore.collection('orgs').doc(order.orgId).get().catch(() => null),\n ])\n const lock = normalizeOrgLockdown(org?.data() as never)\n return {\n site: host?.exists ? hostDisplayName(host.data() as Record<string, unknown>, order.hostId) : 'your site',\n org: String(org?.get('name') ?? '').trim() || 'your workspace',\n ban: Boolean(lock && isAccountBanLockdownReason(lock.reason)),\n }\n}\n\n/** The fallback subject and first line, when no design renders. */\nconst FALLBACK: Record<PosHardwareEmailKind, (number: string) => string> = {\n received: (number) => `We have your card reader order ${number}`,\n shipped: (number) => `Your card reader order ${number} has shipped`,\n delivered: (number) => `Your card reader order ${number} was delivered`,\n}\n\n/** The merge values for one order's emails. Exported for the render spec. */\nexport async function posHardwareEmailMerge(\n orderId: string,\n order: PosHardwareOrder,\n names: { site: string; org: string },\n): Promise<Record<string, string>> {\n const base = await posSettingsUrl(order.hostId)\n const [path] = base.split('#')\n const ordersUrl = `${path}#${POS_HARDWARE_ANCHOR}`\n return {\n 'org.name': names.org,\n 'site.name': names.site,\n 'order.number': posHardwareOrderNumber(orderId),\n 'order.items': itemsText(order),\n 'order.total': formatPosHardwareCents(order.totalCents ?? 0),\n 'order.shipTo': shipToText(order),\n 'order.shippingMethod': order.shipping?.label ?? '',\n 'order.carrier': carrierLabelFor(order.tracking?.carrier) || 'the carrier',\n 'order.trackingNumber': order.tracking?.number ?? '',\n // An unknown carrier has no tracker to link; the card shows the number.\n 'order.trackingUrl': order.tracking?.url ?? ordersUrl,\n 'order.ordersUrl': ordersUrl,\n 'order.setupUrl': `${path}#pos-devices`,\n }\n}\n\nexport async function sendPosHardwareEmail(\n kind: PosHardwareEmailKind,\n orderId: string,\n order: PosHardwareOrder,\n): Promise<void> {\n try {\n const to = String(order.contactEmail ?? '').trim()\n if (!to) return\n const names = await siteAndOrgNames(order)\n // A ban is a ban (AGL-3420): after its notice the account is sent nothing.\n if (names.ban) return\n const [recipient] = await filterSuppressedEmails([to])\n if (!recipient) return\n const merge = await posHardwareEmailMerge(orderId, order, names)\n const subject = FALLBACK[kind](merge['order.number'])\n const brand = await orgSystemEmailBrand(order.orgId)\n const content = await renderSystemEmailContent(POS_HARDWARE_EMAIL_KEYS[kind], merge, brand, {\n subject,\n text: `${subject}\\n\\n${merge['order.items']}\\n\\n${merge['order.ordersUrl']}`,\n })\n const result = await sendEmail({\n to: recipient,\n ...content,\n ...(brand.fromName ? { fromName: brand.fromName } : {}),\n context: POS_HARDWARE_EMAIL_KEYS[kind],\n })\n if (result.sent) await meterPlatformEmail(1)\n } catch (error) {\n console.error('[pos-hardware] email failed', kind, orderId, error)\n }\n}\n\n/** \"New hardware order\", once per order. */\nexport async function raisePosHardwareOrderAlert(orderId: string, order: PosHardwareOrder): Promise<void> {\n try {\n const names = await siteAndOrgNames(order)\n await raiseOperatorAlert(COMMERCE_POS_HARDWARE_ORDER, {\n dedupeKey: orderId,\n orgId: order.orgId,\n hostId: order.hostId,\n context: {\n orderNumber: posHardwareOrderNumber(orderId),\n amount: formatOperatorAlertAmount(order.totalCents ?? 0),\n site: `${names.site} (${order.hostId})`,\n items: (order.lines ?? []).map((line) => `${line.quantity} × ${line.name}`).join(', '),\n shipping: order.shipping?.label ?? 'shipping',\n },\n })\n } catch (error) {\n console.error('[pos-hardware] operator alert failed', orderId, error)\n }\n}\n"],"names":["formatOperatorAlertAmount","isAccountBanLockdownReason","normalizeOrgLockdown","POS_HARDWARE_EMAIL_KEYS","sendEmail","firebaseAdmin","hostDisplayName","meterPlatformEmail","raiseOperatorAlert","filterSuppressedEmails","orgSystemEmailBrand","renderSystemEmailContent","COMMERCE_POS_HARDWARE_ORDER","carrierLabelFor","formatPosHardwareCents","POS_HARDWARE_ANCHOR","posHardwareOrderNumber","posSettingsUrl","itemsText","order","lines","map","line","name","quantity","join","shipToText","address","shipping","cityLine","city","state","postalCode","filter","Boolean","line1","line2","siteAndOrgNames","firestore","app","host","org","Promise","all","collection","doc","hostId","get","catch","orgId","lock","data","site","exists","String","trim","ban","reason","FALLBACK","received","number","shipped","delivered","posHardwareEmailMerge","orderId","names","base","path","split","ordersUrl","totalCents","label","tracking","carrier","url","sendPosHardwareEmail","kind","to","contactEmail","recipient","merge","subject","brand","content","text","result","fromName","context","sent","error","console","raisePosHardwareOrderAlert","dedupeKey","orderNumber","amount","items"],"mappings":"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"}
|