@aglyn/plugins-commerce 1.0.0-beta.231 → 1.0.0-beta.232
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/console/catalog-organization-card.component.js +1 -0
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +4 -1
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +56 -26
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-zones.d.ts +23 -0
- package/src/lib/components/console/pos-zones.js +31 -0
- package/src/lib/components/console/pos-zones.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +10 -64
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +11 -20
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
- package/src/lib/declarations.server.js +21 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +44 -1
- package/src/lib/model/commerce-orders.js +2 -1
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce.d.ts +4 -1
- 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/product-write.d.ts +186 -0
- package/src/lib/model/product-write.js +116 -0
- package/src/lib/model/product-write.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/channel-order-updates.d.ts +50 -0
- package/src/lib/server/channel-order-updates.js +290 -0
- package/src/lib/server/channel-order-updates.js.map +1 -0
- package/src/lib/server/channel-orders.d.ts +85 -0
- package/src/lib/server/channel-orders.js +543 -0
- package/src/lib/server/channel-orders.js.map +1 -0
- package/src/lib/server/product-catalog.js +5 -1
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product-drafts.d.ts +5 -1
- package/src/lib/server/product-drafts.js +3 -3
- package/src/lib/server/product-drafts.js.map +1 -1
- package/src/lib/server/product-writer.d.ts +19 -0
- package/src/lib/server/product-writer.js +261 -0
- package/src/lib/server/product-writer.js.map +1 -0
- package/src/lib/server/products-write.d.ts +61 -0
- package/src/lib/server/products-write.js +375 -0
- package/src/lib/server/products-write.js.map +1 -0
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +16 -3
- package/src/lib/testing/memory-firestore.js +11 -3
- package/src/lib/testing/memory-firestore.js.map +1 -1
- package/src/lib/transfer/products.server.d.ts +5 -0
- package/src/lib/transfer/products.server.js +2 -2
- package/src/lib/transfer/products.server.js.map +1 -1
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
3
|
+
/**
|
|
4
|
+
* @license
|
|
5
|
+
* Copyright 2026 Aglyn LLC
|
|
6
|
+
*
|
|
7
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
* you may not use this file except in compliance with the License.
|
|
9
|
+
* You may obtain a copy of the License at
|
|
10
|
+
*
|
|
11
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
*
|
|
13
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
* See the License for the specific language governing permissions and
|
|
17
|
+
* limitations under the License.
|
|
18
|
+
*/ import { adjustVariantInventory, commerceSlug, productSearchFields, productStockFields } from "./commerce.js";
|
|
19
|
+
/**
|
|
20
|
+
* The fields a product save writes, before its collection membership and its
|
|
21
|
+
* timestamps: the product as edited, with
|
|
22
|
+
*
|
|
23
|
+
* - `$id` dropped (AGL-1374): the listener's synthetic key, never a field;
|
|
24
|
+
* - the seed's `skus` / `barcodes` dropped (AGL-3321): `productSearchFields`
|
|
25
|
+
* omits each when no variant has one, so a seeded copy would outlive the
|
|
26
|
+
* last SKU;
|
|
27
|
+
* - the trimmed name with the search keys derived from it, together, so the
|
|
28
|
+
* two cannot drift (AGL-2501);
|
|
29
|
+
* - a slug, the primary variant's price, the stock fields, the first image
|
|
30
|
+
* and `updatedAtMs`.
|
|
31
|
+
*/ export function productSaveFields(current, nowMs) {
|
|
32
|
+
var _ref, _ref1, _ref2;
|
|
33
|
+
var _current_mediaUrls;
|
|
34
|
+
const { $id: _syntheticId, skus: _seededSkus, barcodes: _seededBarcodes } = current, currentFields = _object_without_properties_loose(current, [
|
|
35
|
+
"$id",
|
|
36
|
+
"skus",
|
|
37
|
+
"barcodes"
|
|
38
|
+
]);
|
|
39
|
+
const primaryVariant = current.variants[0];
|
|
40
|
+
return _extends({}, currentFields, productSearchFields({
|
|
41
|
+
name: current.name.trim().slice(0, 120),
|
|
42
|
+
variants: current.variants
|
|
43
|
+
}), {
|
|
44
|
+
slug: current.slug || commerceSlug(current.name),
|
|
45
|
+
priceUsd: (_ref = primaryVariant == null ? void 0 : primaryVariant.priceUsd) != null ? _ref : 0
|
|
46
|
+
}, productStockFields({
|
|
47
|
+
variants: current.variants,
|
|
48
|
+
oversellPolicy: current.oversellPolicy
|
|
49
|
+
}), {
|
|
50
|
+
imageUrl: (_ref1 = (_ref2 = (_current_mediaUrls = current.mediaUrls) == null ? void 0 : _current_mediaUrls[0]) != null ? _ref2 : current.imageUrl) != null ? _ref1 : null,
|
|
51
|
+
updatedAtMs: nowMs
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** What the smart-collection rules are matched against: the saved name with the product's own facts. */ export function productMembershipInput(fields, current) {
|
|
55
|
+
return {
|
|
56
|
+
name: fields.name,
|
|
57
|
+
type: current.type,
|
|
58
|
+
tags: current.tags,
|
|
59
|
+
categoryIds: current.categoryIds,
|
|
60
|
+
variants: current.variants
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* An EDIT's replacing write, without its membership and server timestamp:
|
|
65
|
+
* live stock holds are not the editor's to write (AGL-2356), so the seed's
|
|
66
|
+
* are dropped and the stored ones carried; the product stays live
|
|
67
|
+
* (`deletedAt: null`, AGL-3321).
|
|
68
|
+
*/ export function productEditWrite(fields, liveHolds) {
|
|
69
|
+
const { stockHolds: _seededHolds } = fields, withoutHolds = _object_without_properties_loose(fields, [
|
|
70
|
+
"stockHolds"
|
|
71
|
+
]);
|
|
72
|
+
return _extends({}, withoutHolds, liveHolds ? {
|
|
73
|
+
stockHolds: liveHolds
|
|
74
|
+
} : {}, {
|
|
75
|
+
deletedAt: null
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** A smart collection as `hosts/{hostId}/collections` stores it, as the membership rules read it. */ export function smartCollectionRulesOf(id, data) {
|
|
79
|
+
var _data_rules;
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
rules: (_data_rules = data['rules']) != null ? _data_rules : [],
|
|
83
|
+
matchAll: data['matchAll']
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The product update and the ledger row one stock adjustment writes: the
|
|
88
|
+
* variants with the count moved, the stock fields that move with it
|
|
89
|
+
* (AGL-3321), and the same `inventoryAdjustments` row the sale webhook writes
|
|
90
|
+
* (AGL-281). A zero or non-numeric delta writes nothing.
|
|
91
|
+
*/ export function stockAdjustmentWrite(product, productId, adjustment, nowMs) {
|
|
92
|
+
const delta = Math.round(Number(adjustment.delta));
|
|
93
|
+
if (!delta) return null;
|
|
94
|
+
const variants = adjustVariantInventory(product, adjustment.variantId, delta, adjustment.locationId || undefined);
|
|
95
|
+
return {
|
|
96
|
+
update: _extends({
|
|
97
|
+
variants
|
|
98
|
+
}, productStockFields(_extends({}, product, {
|
|
99
|
+
variants
|
|
100
|
+
})), {
|
|
101
|
+
updatedAtMs: nowMs
|
|
102
|
+
}),
|
|
103
|
+
ledger: _extends({
|
|
104
|
+
productId,
|
|
105
|
+
variantId: adjustment.variantId,
|
|
106
|
+
delta,
|
|
107
|
+
reason: adjustment.reason
|
|
108
|
+
}, adjustment.locationId ? {
|
|
109
|
+
locationId: adjustment.locationId
|
|
110
|
+
} : {}, {
|
|
111
|
+
atMs: nowMs
|
|
112
|
+
})
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//# sourceMappingURL=product-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/product-write.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 {\n adjustVariantInventory,\n commerceSlug,\n type HostProduct,\n type InventoryAdjustment,\n type InventoryAdjustmentReason,\n productSearchFields,\n productStockFields,\n type SmartCollectionRules,\n} from './commerce'\n\n/*\n * WHAT A PRODUCT SAVE AND A STOCK ADJUSTMENT WRITE, worked out once.\n *\n * The console's product editor and its Adjust stock dialog compute these\n * fields in the browser before their Firestore writes; the native apps save\n * through `commerce/products/save` and `commerce/products/stock`, which run\n * this same code on the server. One computation, so a product saved from a\n * phone carries exactly the search keys, stock verdict, price, image and\n * smart-collection membership the console would have written.\n */\n\n/** A product as the editor holds it: the stored fields plus keys the listener or a seed may carry. */\nexport type EditedProduct = HostProduct & {\n $id?: string\n skus?: string[]\n barcodes?: string[]\n stockHolds?: Record<string, unknown>\n}\n\n/**\n * The fields a product save writes, before its collection membership and its\n * timestamps: the product as edited, with\n *\n * - `$id` dropped (AGL-1374): the listener's synthetic key, never a field;\n * - the seed's `skus` / `barcodes` dropped (AGL-3321): `productSearchFields`\n * omits each when no variant has one, so a seeded copy would outlive the\n * last SKU;\n * - the trimmed name with the search keys derived from it, together, so the\n * two cannot drift (AGL-2501);\n * - a slug, the primary variant's price, the stock fields, the first image\n * and `updatedAtMs`.\n */\nexport function productSaveFields(current: EditedProduct, nowMs: number) {\n const {\n $id: _syntheticId,\n skus: _seededSkus,\n barcodes: _seededBarcodes,\n ...currentFields\n } = current\n const primaryVariant = current.variants[0]\n return {\n ...currentFields,\n ...productSearchFields({\n name: current.name.trim().slice(0, 120),\n variants: current.variants,\n }),\n slug: current.slug || commerceSlug(current.name),\n priceUsd: primaryVariant?.priceUsd ?? 0,\n ...productStockFields({\n variants: current.variants,\n oversellPolicy: current.oversellPolicy,\n }),\n imageUrl: current.mediaUrls?.[0] ?? current.imageUrl ?? null,\n updatedAtMs: nowMs,\n }\n}\n\n/** What the smart-collection rules are matched against: the saved name with the product's own facts. */\nexport function productMembershipInput(\n fields: { name: string },\n current: Pick<HostProduct, 'type' | 'tags' | 'categoryIds' | 'variants'>,\n) {\n return {\n name: fields.name,\n type: current.type,\n tags: current.tags,\n categoryIds: current.categoryIds,\n variants: current.variants,\n }\n}\n\n/**\n * An EDIT's replacing write, without its membership and server timestamp:\n * live stock holds are not the editor's to write (AGL-2356), so the seed's\n * are dropped and the stored ones carried; the product stays live\n * (`deletedAt: null`, AGL-3321).\n */\nexport function productEditWrite(\n fields: ReturnType<typeof productSaveFields>,\n liveHolds: Record<string, unknown> | null | undefined,\n) {\n const { stockHolds: _seededHolds, ...withoutHolds } = fields as typeof fields & {\n stockHolds?: Record<string, unknown>\n }\n return {\n ...withoutHolds,\n ...(liveHolds ? { stockHolds: liveHolds } : {}),\n deletedAt: null,\n }\n}\n\n/** A smart collection as `hosts/{hostId}/collections` stores it, as the membership rules read it. */\nexport function smartCollectionRulesOf(id: string, data: Record<string, unknown>): SmartCollectionRules {\n return {\n id,\n rules: (data['rules'] as SmartCollectionRules['rules']) ?? [],\n matchAll: data['matchAll'] as boolean | undefined,\n }\n}\n\n/** One stock adjustment, as the Adjust stock dialog asks for it. */\nexport interface StockAdjustment {\n variantId: string\n delta: number\n reason: InventoryAdjustmentReason\n locationId?: string\n}\n\n/**\n * The product update and the ledger row one stock adjustment writes: the\n * variants with the count moved, the stock fields that move with it\n * (AGL-3321), and the same `inventoryAdjustments` row the sale webhook writes\n * (AGL-281). A zero or non-numeric delta writes nothing.\n */\nexport function stockAdjustmentWrite(\n product: HostProduct,\n productId: string,\n adjustment: StockAdjustment,\n nowMs: number,\n): { update: Record<string, unknown>; ledger: InventoryAdjustment } | null {\n const delta = Math.round(Number(adjustment.delta))\n if (!delta) return null\n const variants = adjustVariantInventory(product, adjustment.variantId, delta, adjustment.locationId || undefined)\n return {\n update: {\n variants,\n ...productStockFields({ ...product, variants }),\n updatedAtMs: nowMs,\n },\n ledger: {\n productId,\n variantId: adjustment.variantId,\n delta,\n reason: adjustment.reason,\n ...(adjustment.locationId ? { locationId: adjustment.locationId } : {}),\n atMs: nowMs,\n },\n }\n}\n"],"names":["adjustVariantInventory","commerceSlug","productSearchFields","productStockFields","productSaveFields","current","nowMs","$id","_syntheticId","skus","_seededSkus","barcodes","_seededBarcodes","currentFields","primaryVariant","variants","name","trim","slice","slug","priceUsd","oversellPolicy","imageUrl","mediaUrls","updatedAtMs","productMembershipInput","fields","type","tags","categoryIds","productEditWrite","liveHolds","stockHolds","_seededHolds","withoutHolds","deletedAt","smartCollectionRulesOf","id","data","rules","matchAll","stockAdjustmentWrite","product","productId","adjustment","delta","Math","round","Number","variantId","locationId","undefined","update","ledger","reason","atMs"],"mappings":";;AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,sBAAsB,EACtBC,YAAY,EAIZC,mBAAmB,EACnBC,kBAAkB,QAEb,gBAAY;AAqBnB;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,kBAAkBC,OAAsB,EAAEC,KAAa;cAoBzDD;QAAAA;IAnBZ,MAAM,EACJE,KAAKC,YAAY,EACjBC,MAAMC,WAAW,EACjBC,UAAUC,eAAe,EAE1B,GAAGP,SADCQ,iDACDR;;;;;IACJ,MAAMS,iBAAiBT,QAAQU,QAAQ,CAAC,EAAE;IAC1C,OAAO,aACFF,eACAX,oBAAoB;QACrBc,MAAMX,QAAQW,IAAI,CAACC,IAAI,GAAGC,KAAK,CAAC,GAAG;QACnCH,UAAUV,QAAQU,QAAQ;IAC5B;QACAI,MAAMd,QAAQc,IAAI,IAAIlB,aAAaI,QAAQW,IAAI;QAC/CI,QAAQ,UAAEN,kCAAAA,eAAgBM,QAAQ,mBAAI;OACnCjB,mBAAmB;QACpBY,UAAUV,QAAQU,QAAQ;QAC1BM,gBAAgBhB,QAAQgB,cAAc;IACxC;QACAC,QAAQ,GAAEjB,kBAAAA,qBAAAA,QAAQkB,SAAS,qBAAjBlB,kBAAmB,CAAC,EAAE,oBAAIA,QAAQiB,QAAQ,YAA1CjB,QAA8C;QACxDmB,aAAalB;;AAEjB;AAEA,sGAAsG,GACtG,OAAO,SAASmB,uBACdC,MAAwB,EACxBrB,OAAwE;IAExE,OAAO;QACLW,MAAMU,OAAOV,IAAI;QACjBW,MAAMtB,QAAQsB,IAAI;QAClBC,MAAMvB,QAAQuB,IAAI;QAClBC,aAAaxB,QAAQwB,WAAW;QAChCd,UAAUV,QAAQU,QAAQ;IAC5B;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASe,iBACdJ,MAA4C,EAC5CK,SAAqD;IAErD,MAAM,EAAEC,YAAYC,YAAY,EAAmB,GAAGP,QAAjBQ,gDAAiBR;;;IAGtD,OAAO,aACFQ,cACCH,YAAY;QAAEC,YAAYD;IAAU,IAAI,CAAC;QAC7CI,WAAW;;AAEf;AAEA,mGAAmG,GACnG,OAAO,SAASC,uBAAuBC,EAAU,EAAEC,IAA6B;QAGpEA;IAFV,OAAO;QACLD;QACAE,KAAK,GAAGD,cAAAA,IAAI,CAAC,QAAQ,YAAbA,cAAmD,EAAE;QAC7DE,UAAUF,IAAI,CAAC,WAAW;IAC5B;AACF;AAUA;;;;;CAKC,GACD,OAAO,SAASG,qBACdC,OAAoB,EACpBC,SAAiB,EACjBC,UAA2B,EAC3BtC,KAAa;IAEb,MAAMuC,QAAQC,KAAKC,KAAK,CAACC,OAAOJ,WAAWC,KAAK;IAChD,IAAI,CAACA,OAAO,OAAO;IACnB,MAAM9B,WAAWf,uBAAuB0C,SAASE,WAAWK,SAAS,EAAEJ,OAAOD,WAAWM,UAAU,IAAIC;IACvG,OAAO;QACLC,QAAQ;YACNrC;WACGZ,mBAAmB,aAAKuC;YAAS3B;;YACpCS,aAAalB;;QAEf+C,QAAQ;YACNV;YACAM,WAAWL,WAAWK,SAAS;YAC/BJ;YACAS,QAAQV,WAAWU,MAAM;WACrBV,WAAWM,UAAU,GAAG;YAAEA,YAAYN,WAAWM,UAAU;QAAC,IAAI,CAAC;YACrEK,MAAMjD;;IAEV;AACF"}
|
package/src/lib/plugin.js
CHANGED
|
@@ -30,6 +30,7 @@ import { registerCommerceZones } from "./components/console/product-zones.js";
|
|
|
30
30
|
import { registerCommerceOrderZones } from "./components/console/order-zones.js";
|
|
31
31
|
import { registerCommerceReturnZones } from "./components/console/return-zones.js";
|
|
32
32
|
import { registerCommerceStoreZones } from "./components/console/store-zones.js";
|
|
33
|
+
import { registerCommercePosZones } from "./components/console/pos-zones.js";
|
|
33
34
|
import { registerCommerceTransferUi } from "./transfer/register-transfer-ui.js";
|
|
34
35
|
import { COMMERCE_SEARCH_SOURCES } from "./model/commerce-search-sources.js";
|
|
35
36
|
import { COMMERCE_CONFIG_SCHEMA } from "./plugin-config.js";
|
|
@@ -58,6 +59,8 @@ import { COMMERCE_CONFIG_SCHEMA } from "./plugin-config.js";
|
|
|
58
59
|
registerCommerceReturnZones();
|
|
59
60
|
// The orders list's bulk actions and the store's Settings (AGL-3612).
|
|
60
61
|
registerCommerceStoreZones();
|
|
62
|
+
// The register's orders from other channels — a delivery app's (AGL-3644).
|
|
63
|
+
registerCommercePosZones();
|
|
61
64
|
// Its import and export resources' names and the products wizard's own
|
|
62
65
|
// step (AGL-3531); the server halves register from the console's server
|
|
63
66
|
// declarations.
|
package/src/lib/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiStorefrontOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nconst PosConsolePage = lazy(() => import('./components/console/pos-page.component'))\n/** The customer display (AGL-3608): loads only on the public page that shows it. */\nconst PosDisplayPage = lazy(() => import('./components/console/pos-display-page.component'))\nconst CommerceGlanceCard = lazy(\n () => import('./components/console/commerce-glance-card.component'),\n)\nconst NewestSiteUsersCard = lazy(\n () => import('./components/console/newest-site-users-card.component'),\n)\nconst SiteMemberPurchases = lazy(\n () => import('./components/console/site-member-purchases.component'),\n)\nimport { COMMERCE_CONSOLE_SECTIONS } from './components/commerce-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { COMMERCE_PERMISSIONS } from './model/plugin-permissions'\nimport { registerCommerceRecordLists } from './model/commerce-record-lists'\nimport { registerCommerceRecordRoutes } from './model/commerce-record-routes'\nimport { registerCommerceZones } from './components/console/product-zones'\nimport { registerCommerceOrderZones } from './components/console/order-zones'\nimport { registerCommerceReturnZones } from './components/console/return-zones'\nimport { registerCommerceStoreZones } from './components/console/store-zones'\nimport { registerCommerceTransferUi } from './transfer/register-transfer-ui'\nimport { COMMERCE_SEARCH_SOURCES } from './model/commerce-search-sources'\nimport { COMMERCE_CONFIG_SCHEMA } from './plugin-config'\n\n/** Code-split: the Products console page only loads when opened. */\nconst CommerceConsolePage = lazy(() => import('./components/commerce-console-page'))\n\n/**\n * Console half (AGL-395): registers the Products nav item in the\n * ConsoleExtension registry so the commerce console surface is owned by the\n * plugin, like events and email. The page body still lives in the console\n * app (`/[hostId]/products`): its product editor depends on the app's media\n * browser, which transitively pulls in the org/session context, so a full\n * body relocation needs a plugin-consumable media-picker context first —\n * tracked as follow-up. The nav item omits a `Component`, so the shell's\n * nav renders the link and the existing named route serves the page.\n */\nexport function registerCommerceConsole(): void {\n // Where a product is read, for surfaces outside this plugin (AGL-3080).\n registerCommerceRecordRoutes()\n registerCommerceRecordLists()\n // The zones its product editor, products hub and CSV import dialog host,\n // with the props each hands a widget (AGL-2916, AGL-3080).\n registerCommerceZones()\n // …and the order dialog's two (AGL-3611), where a shipping plugin's widget\n // buys a label and records the shipment through the dialog's own route.\n registerCommerceOrderZones()\n // …and the return dialog's (AGL-3611), where a shipping plugin's widget\n // buys the buyer a return label and attaches it through this plugin's route.\n registerCommerceReturnZones()\n // The orders list's bulk actions and the store's Settings (AGL-3612).\n registerCommerceStoreZones()\n // Its import and export resources' names and the products wizard's own\n // step (AGL-3531); the server halves register from the console's server\n // declarations.\n registerCommerceTransferUi()\n // Plugin-declared permissions (AGL-435): tier defaults ride every\n // resolved role set; custom roles override key-by-key.\n Aglyn.registerPluginPermissions(COMMERCE_PERMISSIONS)\n // Register discount ceiling (AGL-2161) — the generic settings card renders\n // it from this schema, so there is no bespoke UI to keep in sync.\n Aglyn.registerPluginConfigSchema(COMMERCE_CONFIG_SCHEMA)\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Commerce',\n searchSources: COMMERCE_SEARCH_SOURCES,\n // The screen a register turns toward its customer (AGL-3608), at\n // `/kiosk/commerce/pos-display`. No staff session: the device proves\n // itself with the display token it got by pairing, and\n // `server/pos-display.ts` refuses every read without one.\n publicPages: [\n { path: '/pos-display', title: 'Customer display', Component: PosDisplayPage },\n ],\n // Dashboard/analytics glance card (AGL-419): rendered through the\n // shell's 'commerceGlance' widget slot.\n widgets: [\n {\n slot: 'commerceGlance',\n widgetId: 'commerce-glance',\n title: 'Commerce',\n Component: CommerceGlanceCard,\n },\n // What a site user bought — lifetime total, orders and subscriptions —\n // in their drawer on the site's Users page: the orders and\n // subscriptions are this plugin's.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.siteMember,\n widgetId: 'commerce-site-member-purchases',\n title: 'Purchases',\n Component: SiteMemberPurchases,\n },\n ],\n navItems: [\n {\n label: 'Products',\n href: '/products',\n icon: { path: mdiStorefrontOutline.path },\n // Sections as ROUTES (AGL-2501): `/products/orders` and friends are\n // real URLs the shell resolves and gates, so the page mounts the one\n // being read instead of subscribing all six.\n sections: COMMERCE_CONSOLE_SECTIONS,\n header: {\n title: 'Products',\n icon: { path: mdiStorefrontOutline.path },\n // The overview, not `catalog`: this surface is the whole commerce\n // console — Catalog, Orders, Promotions and Reservations.\n docsTopic: 'commerce',\n },\n Component: CommerceConsolePage,\n },\n {\n // POS register (AGL-312/419): relocated from the app's /pos route —\n // the generic [pluginSlug] shell now serves it at the same URL.\n label: 'POS',\n href: '/pos',\n /*\n * WHO may open the register, declared on THIS nav item rather than on\n * the extension.\n *\n * A key here narrows one surface; a key on the extension would gate\n * every surface it registers, and Products is the sibling — a catalog\n * anyone who works on the site may open. The register is the one that\n * takes money and lists checked-in guests by name, so the two\n * genuinely have different answers and only this one is narrowed.\n *\n * `managePos` rather than a dotted catalog key because it is the key\n * the sale itself is refused by: `server/pos-order.ts` resolves the\n * member's permission map and returns 403 unless `managePos` holds.\n * The shell answers this declaration out of that same resolved map, so\n * the console offers a register exactly to the people the route will\n * accept a sale from — the gate and its server twin cannot disagree,\n * because they are reading one value.\n *\n * `pos-order.ts` stays the enforcement point regardless. This decides\n * whether the surface is built at all, which is the part a check\n * inside the page cannot do: by the time the page can refuse itself it\n * has mounted and its product, register, location and reservation\n * listeners are already open.\n */\n permission: 'managePos',\n icon: { path: mdiStorefrontOutline.path },\n header: { title: 'Point of Sale', docsTopic: 'pos' },\n Component: PosConsolePage,\n },\n ],\n })\n /*\n * The USER ACCOUNTS half, registered under its own id.\n *\n * `accounts` is a switch rather than a bundle: the member blocks and every\n * `membership/*` handler ship inside this package, which is why the id\n * carries no loader manifest entry of its own. A second extension is how a\n * card about visitor accounts is gated on the switch that decides whether\n * the site serves them — `listConsoleWidgets` filters by the workspace's\n * effective plugin ids, and `accounts` is default-OFF per site.\n *\n * A separate call rather than more widgets on the commerce extension: the\n * registry is keyed by `pluginId` and that key IS the gate, so folding this\n * card in above would have kept the dashboard defect it fixes — `Newest\n * site users` on a site whose /signin returns 404.\n */\n Aglyn.registerConsoleExtension({\n pluginId: Aglyn.ACCOUNTS_PLUGIN_ID,\n displayName: 'User Accounts',\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'accounts-newest-site-users',\n title: 'Newest site users',\n Component: NewestSiteUsersCard,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiStorefrontOutline","lazy","PosConsolePage","PosDisplayPage","CommerceGlanceCard","NewestSiteUsersCard","SiteMemberPurchases","COMMERCE_CONSOLE_SECTIONS","BUNDLE_ID","COMMERCE_PERMISSIONS","registerCommerceRecordLists","registerCommerceRecordRoutes","registerCommerceZones","registerCommerceOrderZones","registerCommerceReturnZones","registerCommerceStoreZones","registerCommerceTransferUi","COMMERCE_SEARCH_SOURCES","COMMERCE_CONFIG_SCHEMA","CommerceConsolePage","registerCommerceConsole","registerPluginPermissions","registerPluginConfigSchema","registerConsoleExtension","pluginId","displayName","searchSources","publicPages","path","title","Component","widgets","slot","widgetId","CONSOLE_WIDGET_SLOTS","siteMember","navItems","label","href","icon","sections","header","docsTopic","permission","ACCOUNTS_PLUGIN_ID","hostDashboard"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/plugin.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 * as Aglyn from '@aglyn/aglyn'\nimport { mdiStorefrontOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nconst PosConsolePage = lazy(() => import('./components/console/pos-page.component'))\n/** The customer display (AGL-3608): loads only on the public page that shows it. */\nconst PosDisplayPage = lazy(() => import('./components/console/pos-display-page.component'))\nconst CommerceGlanceCard = lazy(\n () => import('./components/console/commerce-glance-card.component'),\n)\nconst NewestSiteUsersCard = lazy(\n () => import('./components/console/newest-site-users-card.component'),\n)\nconst SiteMemberPurchases = lazy(\n () => import('./components/console/site-member-purchases.component'),\n)\nimport { COMMERCE_CONSOLE_SECTIONS } from './components/commerce-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { COMMERCE_PERMISSIONS } from './model/plugin-permissions'\nimport { registerCommerceRecordLists } from './model/commerce-record-lists'\nimport { registerCommerceRecordRoutes } from './model/commerce-record-routes'\nimport { registerCommerceZones } from './components/console/product-zones'\nimport { registerCommerceOrderZones } from './components/console/order-zones'\nimport { registerCommerceReturnZones } from './components/console/return-zones'\nimport { registerCommerceStoreZones } from './components/console/store-zones'\nimport { registerCommercePosZones } from './components/console/pos-zones'\nimport { registerCommerceTransferUi } from './transfer/register-transfer-ui'\nimport { COMMERCE_SEARCH_SOURCES } from './model/commerce-search-sources'\nimport { COMMERCE_CONFIG_SCHEMA } from './plugin-config'\n\n/** Code-split: the Products console page only loads when opened. */\nconst CommerceConsolePage = lazy(() => import('./components/commerce-console-page'))\n\n/**\n * Console half (AGL-395): registers the Products nav item in the\n * ConsoleExtension registry so the commerce console surface is owned by the\n * plugin, like events and email. The page body still lives in the console\n * app (`/[hostId]/products`): its product editor depends on the app's media\n * browser, which transitively pulls in the org/session context, so a full\n * body relocation needs a plugin-consumable media-picker context first —\n * tracked as follow-up. The nav item omits a `Component`, so the shell's\n * nav renders the link and the existing named route serves the page.\n */\nexport function registerCommerceConsole(): void {\n // Where a product is read, for surfaces outside this plugin (AGL-3080).\n registerCommerceRecordRoutes()\n registerCommerceRecordLists()\n // The zones its product editor, products hub and CSV import dialog host,\n // with the props each hands a widget (AGL-2916, AGL-3080).\n registerCommerceZones()\n // …and the order dialog's two (AGL-3611), where a shipping plugin's widget\n // buys a label and records the shipment through the dialog's own route.\n registerCommerceOrderZones()\n // …and the return dialog's (AGL-3611), where a shipping plugin's widget\n // buys the buyer a return label and attaches it through this plugin's route.\n registerCommerceReturnZones()\n // The orders list's bulk actions and the store's Settings (AGL-3612).\n registerCommerceStoreZones()\n // The register's orders from other channels — a delivery app's (AGL-3644).\n registerCommercePosZones()\n // Its import and export resources' names and the products wizard's own\n // step (AGL-3531); the server halves register from the console's server\n // declarations.\n registerCommerceTransferUi()\n // Plugin-declared permissions (AGL-435): tier defaults ride every\n // resolved role set; custom roles override key-by-key.\n Aglyn.registerPluginPermissions(COMMERCE_PERMISSIONS)\n // Register discount ceiling (AGL-2161) — the generic settings card renders\n // it from this schema, so there is no bespoke UI to keep in sync.\n Aglyn.registerPluginConfigSchema(COMMERCE_CONFIG_SCHEMA)\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Commerce',\n searchSources: COMMERCE_SEARCH_SOURCES,\n // The screen a register turns toward its customer (AGL-3608), at\n // `/kiosk/commerce/pos-display`. No staff session: the device proves\n // itself with the display token it got by pairing, and\n // `server/pos-display.ts` refuses every read without one.\n publicPages: [\n { path: '/pos-display', title: 'Customer display', Component: PosDisplayPage },\n ],\n // Dashboard/analytics glance card (AGL-419): rendered through the\n // shell's 'commerceGlance' widget slot.\n widgets: [\n {\n slot: 'commerceGlance',\n widgetId: 'commerce-glance',\n title: 'Commerce',\n Component: CommerceGlanceCard,\n },\n // What a site user bought — lifetime total, orders and subscriptions —\n // in their drawer on the site's Users page: the orders and\n // subscriptions are this plugin's.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.siteMember,\n widgetId: 'commerce-site-member-purchases',\n title: 'Purchases',\n Component: SiteMemberPurchases,\n },\n ],\n navItems: [\n {\n label: 'Products',\n href: '/products',\n icon: { path: mdiStorefrontOutline.path },\n // Sections as ROUTES (AGL-2501): `/products/orders` and friends are\n // real URLs the shell resolves and gates, so the page mounts the one\n // being read instead of subscribing all six.\n sections: COMMERCE_CONSOLE_SECTIONS,\n header: {\n title: 'Products',\n icon: { path: mdiStorefrontOutline.path },\n // The overview, not `catalog`: this surface is the whole commerce\n // console — Catalog, Orders, Promotions and Reservations.\n docsTopic: 'commerce',\n },\n Component: CommerceConsolePage,\n },\n {\n // POS register (AGL-312/419): relocated from the app's /pos route —\n // the generic [pluginSlug] shell now serves it at the same URL.\n label: 'POS',\n href: '/pos',\n /*\n * WHO may open the register, declared on THIS nav item rather than on\n * the extension.\n *\n * A key here narrows one surface; a key on the extension would gate\n * every surface it registers, and Products is the sibling — a catalog\n * anyone who works on the site may open. The register is the one that\n * takes money and lists checked-in guests by name, so the two\n * genuinely have different answers and only this one is narrowed.\n *\n * `managePos` rather than a dotted catalog key because it is the key\n * the sale itself is refused by: `server/pos-order.ts` resolves the\n * member's permission map and returns 403 unless `managePos` holds.\n * The shell answers this declaration out of that same resolved map, so\n * the console offers a register exactly to the people the route will\n * accept a sale from — the gate and its server twin cannot disagree,\n * because they are reading one value.\n *\n * `pos-order.ts` stays the enforcement point regardless. This decides\n * whether the surface is built at all, which is the part a check\n * inside the page cannot do: by the time the page can refuse itself it\n * has mounted and its product, register, location and reservation\n * listeners are already open.\n */\n permission: 'managePos',\n icon: { path: mdiStorefrontOutline.path },\n header: { title: 'Point of Sale', docsTopic: 'pos' },\n Component: PosConsolePage,\n },\n ],\n })\n /*\n * The USER ACCOUNTS half, registered under its own id.\n *\n * `accounts` is a switch rather than a bundle: the member blocks and every\n * `membership/*` handler ship inside this package, which is why the id\n * carries no loader manifest entry of its own. A second extension is how a\n * card about visitor accounts is gated on the switch that decides whether\n * the site serves them — `listConsoleWidgets` filters by the workspace's\n * effective plugin ids, and `accounts` is default-OFF per site.\n *\n * A separate call rather than more widgets on the commerce extension: the\n * registry is keyed by `pluginId` and that key IS the gate, so folding this\n * card in above would have kept the dashboard defect it fixes — `Newest\n * site users` on a site whose /signin returns 404.\n */\n Aglyn.registerConsoleExtension({\n pluginId: Aglyn.ACCOUNTS_PLUGIN_ID,\n displayName: 'User Accounts',\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'accounts-newest-site-users',\n title: 'Newest site users',\n Component: NewestSiteUsersCard,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiStorefrontOutline","lazy","PosConsolePage","PosDisplayPage","CommerceGlanceCard","NewestSiteUsersCard","SiteMemberPurchases","COMMERCE_CONSOLE_SECTIONS","BUNDLE_ID","COMMERCE_PERMISSIONS","registerCommerceRecordLists","registerCommerceRecordRoutes","registerCommerceZones","registerCommerceOrderZones","registerCommerceReturnZones","registerCommerceStoreZones","registerCommercePosZones","registerCommerceTransferUi","COMMERCE_SEARCH_SOURCES","COMMERCE_CONFIG_SCHEMA","CommerceConsolePage","registerCommerceConsole","registerPluginPermissions","registerPluginConfigSchema","registerConsoleExtension","pluginId","displayName","searchSources","publicPages","path","title","Component","widgets","slot","widgetId","CONSOLE_WIDGET_SLOTS","siteMember","navItems","label","href","icon","sections","header","docsTopic","permission","ACCOUNTS_PLUGIN_ID","hostDashboard"],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { PluginChannelOrderHandoff, PluginChannelOrderHandoffOutcome, PluginChannelOrderRefund, PluginChannelOrderRefundOutcome } from '@aglyn/aglyn/plugin-manager/plugin-channel-orders';
|
|
18
|
+
/**
|
|
19
|
+
* WHAT A CHANNEL SAYS AFTER ITS ORDER CAME IN (AGL-3644): commerce's half of
|
|
20
|
+
* `completeOrder` and `recordRefund` on core's `core.channel-orders`.
|
|
21
|
+
*
|
|
22
|
+
* A delivery app's order is handed to its own courier over the counter, and
|
|
23
|
+
* the app — not the store — refunds its buyer or takes an item off the order.
|
|
24
|
+
* So, for an order the channel sent (`channel: 'marketplace'`):
|
|
25
|
+
*
|
|
26
|
+
* - **A hand-off** closes the order as fulfilled with one fulfillment for
|
|
27
|
+
* every unit still open, carrying the channel as its carrier and no
|
|
28
|
+
* tracking: nothing was shipped from here. Keyed, so a second report of the
|
|
29
|
+
* same pickup writes nothing.
|
|
30
|
+
* - **A refund or adjustment** is RECORDED: `refundedCents` grows by the
|
|
31
|
+
* amount (never past the order's total), the order is `refunded` once the
|
|
32
|
+
* whole of it is, and the channel's id for it is kept so the same report
|
|
33
|
+
* twice records once. No money moves: the channel refunded its own buyer.
|
|
34
|
+
* Units the channel names as never having left go back on the shelf, at
|
|
35
|
+
* most what the order's own sale took less what was already put back —
|
|
36
|
+
* the ledger bound the console's cancel uses.
|
|
37
|
+
*
|
|
38
|
+
* Each is ONE transaction that reads the order (and, for a restock, each
|
|
39
|
+
* product) before it writes, and stages its order event in the same write.
|
|
40
|
+
*/
|
|
41
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
42
|
+
export interface ChannelOrderUpdateDeps {
|
|
43
|
+
firestore: () => Firestore;
|
|
44
|
+
now: () => number;
|
|
45
|
+
}
|
|
46
|
+
/** The channel's courier took the order: every open unit is fulfilled, with nothing shipped from here. */
|
|
47
|
+
export declare function completeChannelOrder(request: PluginChannelOrderHandoff, deps?: ChannelOrderUpdateDeps): Promise<PluginChannelOrderHandoffOutcome>;
|
|
48
|
+
/** The channel refunded its buyer or took items off the order: recorded once, with the named units back on the shelf. */
|
|
49
|
+
export declare function recordChannelOrderRefund(request: PluginChannelOrderRefund, deps?: ChannelOrderUpdateDeps): Promise<PluginChannelOrderRefundOutcome>;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,290 @@
|
|
|
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 { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
18
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin/server/firebase-admin";
|
|
19
|
+
import * as CommerceModel from "../model/index.js";
|
|
20
|
+
import { ORDER_FULFILLED_EVENT, ORDER_REFUNDED_EVENT } from "../model/order-events.js";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { fulfillmentEventView, stageOrderEvent } from "./order-events.js";
|
|
23
|
+
import { restockCapKey, saleReleaseCaps } from "./restock-flag.js";
|
|
24
|
+
const DOC_ID = /^[A-Za-z0-9_-]{1,200}$/;
|
|
25
|
+
const MAX_CENTS = 10000000000;
|
|
26
|
+
const HANDOFF_KEY = 'channel-handoff';
|
|
27
|
+
/** Refund ids kept on an order: far more than any channel sends for one. */ const MAX_REFUND_IDS = 100;
|
|
28
|
+
const restockKey = restockCapKey;
|
|
29
|
+
/** The hand-off's fulfillment id: one per order, so a second report finds the first. */ const handoffFulfillmentId = (orderId)=>`f-${createHash('sha256').update(`${orderId}:${HANDOFF_KEY}`).digest('hex').slice(0, 20)}`;
|
|
30
|
+
const defaultDeps = ()=>({
|
|
31
|
+
firestore: ()=>firebaseAdmin.app().firestore(),
|
|
32
|
+
now: Date.now
|
|
33
|
+
});
|
|
34
|
+
const clip = (value, max)=>String(value != null ? value : '').trim().slice(0, max);
|
|
35
|
+
const validIds = (hostId, recordId)=>DOC_ID.test(String(hostId != null ? hostId : '')) && DOC_ID.test(String(recordId != null ? recordId : '')) && !/^__.*__$/.test(String(hostId)) && !/^__.*__$/.test(String(recordId));
|
|
36
|
+
/** The channel's courier took the order: every open unit is fulfilled, with nothing shipped from here. */ export async function completeChannelOrder(request, deps = defaultDeps()) {
|
|
37
|
+
if (!validIds(request == null ? void 0 : request.hostId, request == null ? void 0 : request.recordId)) return {
|
|
38
|
+
outcome: 'no_such_record'
|
|
39
|
+
};
|
|
40
|
+
const firestore = deps.firestore();
|
|
41
|
+
const orderRef = firestore.collection('hosts').doc(request.hostId).collection('orders').doc(request.recordId);
|
|
42
|
+
const nowMs = deps.now();
|
|
43
|
+
const fulfillmentId = handoffFulfillmentId(request.recordId);
|
|
44
|
+
return firestore.runTransaction(async (transaction)=>{
|
|
45
|
+
var _snapshot_data, _order_fulfillments, _order_fulfillments1, _snapshot_data1;
|
|
46
|
+
const snapshot = await transaction.get(orderRef);
|
|
47
|
+
if (!snapshot.exists) return {
|
|
48
|
+
outcome: 'no_such_record'
|
|
49
|
+
};
|
|
50
|
+
const order = CommerceModel.liftLegacyOrder((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
|
|
51
|
+
if (order.channel !== 'marketplace' || !order.channelSource) return {
|
|
52
|
+
outcome: 'not_completable',
|
|
53
|
+
status: order.status
|
|
54
|
+
};
|
|
55
|
+
if (((_order_fulfillments = order.fulfillments) != null ? _order_fulfillments : []).some((entry)=>entry.id === fulfillmentId)) return {
|
|
56
|
+
outcome: 'already'
|
|
57
|
+
};
|
|
58
|
+
if (order.status === 'fulfilled' || order.status === 'delivered') return {
|
|
59
|
+
outcome: 'already'
|
|
60
|
+
};
|
|
61
|
+
if (!CommerceModel.canTransitionOrder(order.status, 'fulfilled')) {
|
|
62
|
+
return {
|
|
63
|
+
outcome: 'not_completable',
|
|
64
|
+
status: order.status
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
let lines = CommerceModel.remainingFulfillmentLines(order);
|
|
68
|
+
if (!lines.length) {
|
|
69
|
+
lines = CommerceModel.orderLineFulfillmentStates(order).filter((state)=>state.remainingQuantity > 0).map((state)=>({
|
|
70
|
+
lineItemId: state.lineItemId,
|
|
71
|
+
quantity: state.remainingQuantity
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
const carrier = clip(order.channelSource.channelLabel, 40) || 'Courier';
|
|
75
|
+
const fulfillment = {
|
|
76
|
+
id: fulfillmentId,
|
|
77
|
+
lineItemIds: lines.map((entry)=>entry.lineItemId),
|
|
78
|
+
lines,
|
|
79
|
+
carrier,
|
|
80
|
+
status: 'active',
|
|
81
|
+
// The channel tells its own buyer; nobody is emailed from here.
|
|
82
|
+
notify: false,
|
|
83
|
+
atMs: nowMs
|
|
84
|
+
};
|
|
85
|
+
const patch = {
|
|
86
|
+
status: 'fulfilled',
|
|
87
|
+
fulfillments: [
|
|
88
|
+
...(_order_fulfillments1 = order.fulfillments) != null ? _order_fulfillments1 : [],
|
|
89
|
+
fulfillment
|
|
90
|
+
],
|
|
91
|
+
updatedAtMs: nowMs,
|
|
92
|
+
timeline: CommerceModel.appendOrderEvent(order, 'fulfilled', clip(request.note, 200) || `Picked up by the ${carrier} courier`, nowMs)
|
|
93
|
+
};
|
|
94
|
+
transaction.update(orderRef, patch);
|
|
95
|
+
stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, {
|
|
96
|
+
hostId: request.hostId,
|
|
97
|
+
orderId: request.recordId,
|
|
98
|
+
key: fulfillment.id,
|
|
99
|
+
order: _extends({}, (_snapshot_data1 = snapshot.data()) != null ? _snapshot_data1 : {}, patch),
|
|
100
|
+
extra: {
|
|
101
|
+
fulfillment: fulfillmentEventView(order, fulfillment)
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
outcome: 'completed'
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* What the order's sale took off each shelf, less what has gone back since
|
|
111
|
+
* (a cancel's or an earlier adjustment's rows): the most a restock may still
|
|
112
|
+
* return, by product and variant.
|
|
113
|
+
*/ function remainingReleaseCaps(rows) {
|
|
114
|
+
const caps = saleReleaseCaps(rows.filter((row)=>row.get('reason') === 'sale'));
|
|
115
|
+
for (const row of rows){
|
|
116
|
+
var _ref, _row_get, _row_get1, _row_get2, _caps_get;
|
|
117
|
+
const reason = row.get('reason');
|
|
118
|
+
if (reason !== 'refund' && reason !== 'cancellation') continue;
|
|
119
|
+
const delta = Number((_ref = (_row_get = row.get('appliedDelta')) != null ? _row_get : row.get('delta')) != null ? _ref : 0);
|
|
120
|
+
if (!(delta > 0)) continue;
|
|
121
|
+
const key = restockKey(String((_row_get1 = row.get('productId')) != null ? _row_get1 : ''), String((_row_get2 = row.get('variantId')) != null ? _row_get2 : ''));
|
|
122
|
+
if (caps.has(key)) caps.set(key, Math.max(0, ((_caps_get = caps.get(key)) != null ? _caps_get : 0) - delta));
|
|
123
|
+
}
|
|
124
|
+
return caps;
|
|
125
|
+
}
|
|
126
|
+
/** The channel refunded its buyer or took items off the order: recorded once, with the named units back on the shelf. */ export async function recordChannelOrderRefund(request, deps = defaultDeps()) {
|
|
127
|
+
if (!validIds(request == null ? void 0 : request.hostId, request == null ? void 0 : request.recordId)) return {
|
|
128
|
+
outcome: 'no_such_record'
|
|
129
|
+
};
|
|
130
|
+
const refundId = clip(request.refundId, 200);
|
|
131
|
+
if (!refundId) return {
|
|
132
|
+
outcome: 'refused',
|
|
133
|
+
reason: 'The refund has no id on its channel.'
|
|
134
|
+
};
|
|
135
|
+
const amount = Number(request.amountCents);
|
|
136
|
+
if (!Number.isInteger(amount) || amount < 0 || amount > MAX_CENTS) {
|
|
137
|
+
return {
|
|
138
|
+
outcome: 'refused',
|
|
139
|
+
reason: 'The refund is not a whole amount.'
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const asked = (Array.isArray(request.restock) ? request.restock : []).map((entry)=>({
|
|
143
|
+
lineIndex: Number(entry == null ? void 0 : entry.lineIndex),
|
|
144
|
+
quantity: Number(entry == null ? void 0 : entry.quantity)
|
|
145
|
+
})).filter((entry)=>Number.isInteger(entry.lineIndex) && entry.lineIndex >= 0 && Number.isInteger(entry.quantity) && entry.quantity > 0);
|
|
146
|
+
if (amount === 0 && !asked.length) return {
|
|
147
|
+
outcome: 'refused',
|
|
148
|
+
reason: 'The refund moves no money and no stock.'
|
|
149
|
+
};
|
|
150
|
+
const firestore = deps.firestore();
|
|
151
|
+
const hostRef = firestore.collection('hosts').doc(request.hostId);
|
|
152
|
+
const orderRef = hostRef.collection('orders').doc(request.recordId);
|
|
153
|
+
const nowMs = deps.now();
|
|
154
|
+
return firestore.runTransaction(async (transaction)=>{
|
|
155
|
+
var _snapshot_data, _source_refundIds, _order_lineItems, _ref, _ref1, _order_refundedCents, _source_refundIds1;
|
|
156
|
+
var _order_totals;
|
|
157
|
+
const snapshot = await transaction.get(orderRef);
|
|
158
|
+
if (!snapshot.exists) return {
|
|
159
|
+
outcome: 'no_such_record'
|
|
160
|
+
};
|
|
161
|
+
const order = CommerceModel.liftLegacyOrder((_snapshot_data = snapshot.data()) != null ? _snapshot_data : {});
|
|
162
|
+
const source = order.channelSource;
|
|
163
|
+
if (order.channel !== 'marketplace' || !source) return {
|
|
164
|
+
outcome: 'no_such_record'
|
|
165
|
+
};
|
|
166
|
+
if (((_source_refundIds = source.refundIds) != null ? _source_refundIds : []).includes(refundId)) return {
|
|
167
|
+
outcome: 'already'
|
|
168
|
+
};
|
|
169
|
+
// Every read before any write: the ledger, then each product a restock touches.
|
|
170
|
+
const lineItems = (_order_lineItems = order.lineItems) != null ? _order_lineItems : [];
|
|
171
|
+
const wanted = new Map();
|
|
172
|
+
for (const entry of asked){
|
|
173
|
+
var _wanted_get;
|
|
174
|
+
const line = lineItems[entry.lineIndex];
|
|
175
|
+
if (!(line == null ? void 0 : line.productId) || !line.variantId || /^__.*__$/.test(line.productId)) continue;
|
|
176
|
+
wanted.set(entry.lineIndex, Math.min(Number(line.quantity) || 0, ((_wanted_get = wanted.get(entry.lineIndex)) != null ? _wanted_get : 0) + entry.quantity));
|
|
177
|
+
}
|
|
178
|
+
const rows = wanted.size ? (await transaction.get(hostRef.collection('inventoryAdjustments').where('orderId', '==', request.recordId))).docs : [];
|
|
179
|
+
const caps = remainingReleaseCaps(rows);
|
|
180
|
+
const productIds = [
|
|
181
|
+
...new Set([
|
|
182
|
+
...wanted.keys()
|
|
183
|
+
].map((index)=>lineItems[index].productId))
|
|
184
|
+
];
|
|
185
|
+
const productSnaps = await Promise.all(productIds.map((id)=>transaction.get(hostRef.collection('products').doc(id))));
|
|
186
|
+
const products = new Map();
|
|
187
|
+
productSnaps.forEach((product, index)=>{
|
|
188
|
+
if (product.exists && !product.get('deletedAt')) {
|
|
189
|
+
products.set(productIds[index], CommerceModel.liftLegacyProduct(product.data()));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
let restockedUnits = 0;
|
|
193
|
+
const restockedLines = [];
|
|
194
|
+
const ledger = [];
|
|
195
|
+
const changed = new Set();
|
|
196
|
+
for (const [lineIndex, quantity] of wanted){
|
|
197
|
+
var _caps_get;
|
|
198
|
+
const line = lineItems[lineIndex];
|
|
199
|
+
const product = products.get(line.productId);
|
|
200
|
+
const variantId = String(line.variantId);
|
|
201
|
+
const variant = product == null ? void 0 : product.variants.find((entry)=>entry.id === variantId);
|
|
202
|
+
if (!product || !variant || variant.inventory == null) continue;
|
|
203
|
+
const key = restockKey(line.productId, variantId);
|
|
204
|
+
// A pair the ledger never recorded a sale for gave up nothing to return.
|
|
205
|
+
const budget = (_caps_get = caps.get(key)) != null ? _caps_get : 0;
|
|
206
|
+
const back = Math.min(quantity, budget);
|
|
207
|
+
if (back <= 0) continue;
|
|
208
|
+
caps.set(key, budget - back);
|
|
209
|
+
products.set(line.productId, _extends({}, product, {
|
|
210
|
+
variants: CommerceModel.adjustVariantInventory(product, variantId, back)
|
|
211
|
+
}));
|
|
212
|
+
changed.add(line.productId);
|
|
213
|
+
restockedUnits += back;
|
|
214
|
+
restockedLines.push(lineIndex);
|
|
215
|
+
ledger.push({
|
|
216
|
+
productId: line.productId,
|
|
217
|
+
variantId,
|
|
218
|
+
delta: back,
|
|
219
|
+
reason: 'refund',
|
|
220
|
+
orderId: request.recordId,
|
|
221
|
+
source: source.channelLabel,
|
|
222
|
+
atMs: nowMs
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const total = Number((_ref = (_ref1 = (_order_totals = order.totals) == null ? void 0 : _order_totals.totalCents) != null ? _ref1 : order.amountCents) != null ? _ref : 0);
|
|
226
|
+
const before = Number((_order_refundedCents = order.refundedCents) != null ? _order_refundedCents : 0);
|
|
227
|
+
const recorded = Math.max(0, Math.min(amount, total - before));
|
|
228
|
+
const refundedCents = before + recorded;
|
|
229
|
+
const full = total > 0 && refundedCents >= total;
|
|
230
|
+
let status = order.status;
|
|
231
|
+
if (full && status !== 'refunded' && CommerceModel.canTransitionOrder(status, 'refunded')) status = 'refunded';
|
|
232
|
+
for (const productId of changed){
|
|
233
|
+
const product = products.get(productId);
|
|
234
|
+
transaction.update(hostRef.collection('products').doc(productId), _extends({
|
|
235
|
+
variants: product.variants
|
|
236
|
+
}, CommerceModel.productStockFields(product), {
|
|
237
|
+
updatedAtMs: nowMs
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
for (const row of ledger)transaction.set(hostRef.collection('inventoryAdjustments').doc(createResourceUid()), row);
|
|
241
|
+
const money = (cents)=>`${(cents / 100).toFixed(2)} ${source.currency}`;
|
|
242
|
+
const reason = clip(request.reason, 200) || `Refunded on ${source.channelLabel}`;
|
|
243
|
+
const parts = [
|
|
244
|
+
recorded > 0 ? `${reason}: ${money(recorded)} refunded by ${source.channelLabel}` : reason,
|
|
245
|
+
...recorded < amount ? [
|
|
246
|
+
`${money(amount - recorded)} more than the order had left was not recorded`
|
|
247
|
+
] : [],
|
|
248
|
+
...restockedUnits > 0 ? [
|
|
249
|
+
`${restockedUnits} ${restockedUnits === 1 ? 'unit' : 'units'} returned to stock`
|
|
250
|
+
] : []
|
|
251
|
+
];
|
|
252
|
+
const patch = {
|
|
253
|
+
status,
|
|
254
|
+
refundedCents,
|
|
255
|
+
channelSource: _extends({}, source, {
|
|
256
|
+
refundIds: [
|
|
257
|
+
...(_source_refundIds1 = source.refundIds) != null ? _source_refundIds1 : [],
|
|
258
|
+
refundId
|
|
259
|
+
].slice(-MAX_REFUND_IDS)
|
|
260
|
+
}),
|
|
261
|
+
updatedAtMs: nowMs,
|
|
262
|
+
timeline: CommerceModel.appendOrderEvent(order, 'refund', `${parts.join('. ')}.`, nowMs)
|
|
263
|
+
};
|
|
264
|
+
transaction.update(orderRef, patch);
|
|
265
|
+
if (recorded > 0) {
|
|
266
|
+
var _snapshot_data1;
|
|
267
|
+
stageOrderEvent(transaction, ORDER_REFUNDED_EVENT, {
|
|
268
|
+
hostId: request.hostId,
|
|
269
|
+
orderId: request.recordId,
|
|
270
|
+
key: `refund:channel:${refundId}`,
|
|
271
|
+
order: _extends({}, (_snapshot_data1 = snapshot.data()) != null ? _snapshot_data1 : {}, patch),
|
|
272
|
+
extra: {
|
|
273
|
+
refund: {
|
|
274
|
+
id: null,
|
|
275
|
+
amountCents: recorded,
|
|
276
|
+
lineItemIds: restockedLines,
|
|
277
|
+
full
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
outcome: 'recorded',
|
|
284
|
+
refundedCents,
|
|
285
|
+
restockedUnits
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
//# sourceMappingURL=channel-order-updates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/channel-order-updates.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport type {\n PluginChannelOrderHandoff,\n PluginChannelOrderHandoffOutcome,\n PluginChannelOrderRefund,\n PluginChannelOrderRefundOutcome,\n} from '@aglyn/aglyn/plugin-manager/plugin-channel-orders'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport * as CommerceModel from '../model'\nimport { ORDER_FULFILLED_EVENT, ORDER_REFUNDED_EVENT } from '../model/order-events'\nimport { createHash } from 'node:crypto'\nimport { fulfillmentEventView, stageOrderEvent } from './order-events'\nimport { restockCapKey, saleReleaseCaps } from './restock-flag'\n\n/**\n * WHAT A CHANNEL SAYS AFTER ITS ORDER CAME IN (AGL-3644): commerce's half of\n * `completeOrder` and `recordRefund` on core's `core.channel-orders`.\n *\n * A delivery app's order is handed to its own courier over the counter, and\n * the app — not the store — refunds its buyer or takes an item off the order.\n * So, for an order the channel sent (`channel: 'marketplace'`):\n *\n * - **A hand-off** closes the order as fulfilled with one fulfillment for\n * every unit still open, carrying the channel as its carrier and no\n * tracking: nothing was shipped from here. Keyed, so a second report of the\n * same pickup writes nothing.\n * - **A refund or adjustment** is RECORDED: `refundedCents` grows by the\n * amount (never past the order's total), the order is `refunded` once the\n * whole of it is, and the channel's id for it is kept so the same report\n * twice records once. No money moves: the channel refunded its own buyer.\n * Units the channel names as never having left go back on the shelf, at\n * most what the order's own sale took less what was already put back —\n * the ledger bound the console's cancel uses.\n *\n * Each is ONE transaction that reads the order (and, for a restock, each\n * product) before it writes, and stages its order event in the same write.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nconst DOC_ID = /^[A-Za-z0-9_-]{1,200}$/\nconst MAX_CENTS = 100_000_000_00\nconst HANDOFF_KEY = 'channel-handoff'\n/** Refund ids kept on an order: far more than any channel sends for one. */\nconst MAX_REFUND_IDS = 100\n\nconst restockKey = restockCapKey\n\n/** The hand-off's fulfillment id: one per order, so a second report finds the first. */\nconst handoffFulfillmentId = (orderId: string) =>\n `f-${createHash('sha256').update(`${orderId}:${HANDOFF_KEY}`).digest('hex').slice(0, 20)}`\n\nexport interface ChannelOrderUpdateDeps {\n firestore: () => Firestore\n now: () => number\n}\n\nconst defaultDeps = (): ChannelOrderUpdateDeps => ({ firestore: () => firebaseAdmin.app().firestore(), now: Date.now })\n\nconst clip = (value: unknown, max: number): string => String(value ?? '').trim().slice(0, max)\n\nconst validIds = (hostId: unknown, recordId: unknown): boolean =>\n DOC_ID.test(String(hostId ?? '')) &&\n DOC_ID.test(String(recordId ?? '')) &&\n !/^__.*__$/.test(String(hostId)) &&\n !/^__.*__$/.test(String(recordId))\n\n/** The channel's courier took the order: every open unit is fulfilled, with nothing shipped from here. */\nexport async function completeChannelOrder(\n request: PluginChannelOrderHandoff,\n deps: ChannelOrderUpdateDeps = defaultDeps(),\n): Promise<PluginChannelOrderHandoffOutcome> {\n if (!validIds(request?.hostId, request?.recordId)) return { outcome: 'no_such_record' }\n const firestore = deps.firestore()\n const orderRef = firestore.collection('hosts').doc(request.hostId).collection('orders').doc(request.recordId)\n const nowMs = deps.now()\n const fulfillmentId = handoffFulfillmentId(request.recordId)\n return firestore.runTransaction(async (transaction): Promise<PluginChannelOrderHandoffOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_record' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n if (order.channel !== 'marketplace' || !order.channelSource) return { outcome: 'not_completable', status: order.status }\n if ((order.fulfillments ?? []).some((entry) => entry.id === fulfillmentId)) return { outcome: 'already' }\n if (order.status === 'fulfilled' || order.status === 'delivered') return { outcome: 'already' }\n if (!CommerceModel.canTransitionOrder(order.status, 'fulfilled')) {\n return { outcome: 'not_completable', status: order.status }\n }\n let lines = CommerceModel.remainingFulfillmentLines(order)\n if (!lines.length) {\n lines = CommerceModel.orderLineFulfillmentStates(order)\n .filter((state) => state.remainingQuantity > 0)\n .map((state) => ({ lineItemId: state.lineItemId, quantity: state.remainingQuantity }))\n }\n const carrier = clip(order.channelSource.channelLabel, 40) || 'Courier'\n const fulfillment: CommerceModel.OrderFulfillment = {\n id: fulfillmentId,\n lineItemIds: lines.map((entry) => entry.lineItemId),\n lines,\n carrier,\n status: 'active',\n // The channel tells its own buyer; nobody is emailed from here.\n notify: false,\n atMs: nowMs,\n }\n const patch = {\n status: 'fulfilled' as const,\n fulfillments: [...(order.fulfillments ?? []), fulfillment],\n updatedAtMs: nowMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'fulfilled',\n clip(request.note, 200) || `Picked up by the ${carrier} courier`,\n nowMs,\n ),\n }\n transaction.update(orderRef, patch)\n stageOrderEvent(transaction, ORDER_FULFILLED_EVENT, {\n hostId: request.hostId,\n orderId: request.recordId,\n key: fulfillment.id,\n order: { ...(snapshot.data() ?? {}), ...patch },\n extra: { fulfillment: fulfillmentEventView(order, fulfillment) },\n })\n return { outcome: 'completed' }\n })\n}\n\n/**\n * What the order's sale took off each shelf, less what has gone back since\n * (a cancel's or an earlier adjustment's rows): the most a restock may still\n * return, by product and variant.\n */\nfunction remainingReleaseCaps(rows: readonly { get: (field: string) => unknown }[]): Map<string, number> {\n const caps = saleReleaseCaps(rows.filter((row) => row.get('reason') === 'sale'))\n for (const row of rows) {\n const reason = row.get('reason')\n if (reason !== 'refund' && reason !== 'cancellation') continue\n const delta = Number(row.get('appliedDelta') ?? row.get('delta') ?? 0)\n if (!(delta > 0)) continue\n const key = restockKey(String(row.get('productId') ?? ''), String(row.get('variantId') ?? ''))\n if (caps.has(key)) caps.set(key, Math.max(0, (caps.get(key) ?? 0) - delta))\n }\n return caps\n}\n\n/** The channel refunded its buyer or took items off the order: recorded once, with the named units back on the shelf. */\nexport async function recordChannelOrderRefund(\n request: PluginChannelOrderRefund,\n deps: ChannelOrderUpdateDeps = defaultDeps(),\n): Promise<PluginChannelOrderRefundOutcome> {\n if (!validIds(request?.hostId, request?.recordId)) return { outcome: 'no_such_record' }\n const refundId = clip(request.refundId, 200)\n if (!refundId) return { outcome: 'refused', reason: 'The refund has no id on its channel.' }\n const amount = Number(request.amountCents)\n if (!Number.isInteger(amount) || amount < 0 || amount > MAX_CENTS) {\n return { outcome: 'refused', reason: 'The refund is not a whole amount.' }\n }\n const asked = (Array.isArray(request.restock) ? request.restock : [])\n .map((entry) => ({ lineIndex: Number(entry?.lineIndex), quantity: Number(entry?.quantity) }))\n .filter((entry) => Number.isInteger(entry.lineIndex) && entry.lineIndex >= 0 && Number.isInteger(entry.quantity) && entry.quantity > 0)\n if (amount === 0 && !asked.length) return { outcome: 'refused', reason: 'The refund moves no money and no stock.' }\n\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(request.hostId)\n const orderRef = hostRef.collection('orders').doc(request.recordId)\n const nowMs = deps.now()\n return firestore.runTransaction(async (transaction): Promise<PluginChannelOrderRefundOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_record' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n const source = order.channelSource\n if (order.channel !== 'marketplace' || !source) return { outcome: 'no_such_record' }\n if ((source.refundIds ?? []).includes(refundId)) return { outcome: 'already' }\n\n // Every read before any write: the ledger, then each product a restock touches.\n const lineItems = order.lineItems ?? []\n const wanted = new Map<number, number>()\n for (const entry of asked) {\n const line = lineItems[entry.lineIndex]\n if (!line?.productId || !line.variantId || /^__.*__$/.test(line.productId)) continue\n wanted.set(entry.lineIndex, Math.min(Number(line.quantity) || 0, (wanted.get(entry.lineIndex) ?? 0) + entry.quantity))\n }\n const rows = wanted.size\n ? (await transaction.get(hostRef.collection('inventoryAdjustments').where('orderId', '==', request.recordId))).docs\n : []\n const caps = remainingReleaseCaps(rows)\n const productIds = [...new Set([...wanted.keys()].map((index) => lineItems[index].productId))]\n const productSnaps = await Promise.all(productIds.map((id) => transaction.get(hostRef.collection('products').doc(id))))\n const products = new Map<string, CommerceModel.HostProduct>()\n productSnaps.forEach((product, index) => {\n if (product.exists && !product.get('deletedAt')) {\n products.set(productIds[index], CommerceModel.liftLegacyProduct(product.data() as never))\n }\n })\n\n let restockedUnits = 0\n const restockedLines: number[] = []\n const ledger: CommerceModel.InventoryAdjustment[] = []\n const changed = new Set<string>()\n for (const [lineIndex, quantity] of wanted) {\n const line = lineItems[lineIndex]\n const product = products.get(line.productId)\n const variantId = String(line.variantId)\n const variant = product?.variants.find((entry) => entry.id === variantId)\n if (!product || !variant || variant.inventory == null) continue\n const key = restockKey(line.productId, variantId)\n // A pair the ledger never recorded a sale for gave up nothing to return.\n const budget = caps.get(key) ?? 0\n const back = Math.min(quantity, budget)\n if (back <= 0) continue\n caps.set(key, budget - back)\n products.set(line.productId, {\n ...product,\n variants: CommerceModel.adjustVariantInventory(product, variantId, back),\n })\n changed.add(line.productId)\n restockedUnits += back\n restockedLines.push(lineIndex)\n ledger.push({\n productId: line.productId,\n variantId,\n delta: back,\n reason: 'refund',\n orderId: request.recordId,\n source: source.channelLabel,\n atMs: nowMs,\n })\n }\n\n const total = Number(order.totals?.totalCents ?? order.amountCents ?? 0)\n const before = Number(order.refundedCents ?? 0)\n const recorded = Math.max(0, Math.min(amount, total - before))\n const refundedCents = before + recorded\n const full = total > 0 && refundedCents >= total\n let status = order.status\n if (full && status !== 'refunded' && CommerceModel.canTransitionOrder(status, 'refunded')) status = 'refunded'\n\n for (const productId of changed) {\n const product = products.get(productId) as CommerceModel.HostProduct\n transaction.update(hostRef.collection('products').doc(productId), {\n variants: product.variants,\n ...CommerceModel.productStockFields(product),\n updatedAtMs: nowMs,\n })\n }\n for (const row of ledger) transaction.set(hostRef.collection('inventoryAdjustments').doc(createResourceUid()), row)\n\n const money = (cents: number) => `${(cents / 100).toFixed(2)} ${source.currency}`\n const reason = clip(request.reason, 200) || `Refunded on ${source.channelLabel}`\n const parts = [\n recorded > 0 ? `${reason}: ${money(recorded)} refunded by ${source.channelLabel}` : reason,\n ...(recorded < amount ? [`${money(amount - recorded)} more than the order had left was not recorded`] : []),\n ...(restockedUnits > 0 ? [`${restockedUnits} ${restockedUnits === 1 ? 'unit' : 'units'} returned to stock`] : []),\n ]\n const patch = {\n status,\n refundedCents,\n channelSource: { ...source, refundIds: [...(source.refundIds ?? []), refundId].slice(-MAX_REFUND_IDS) },\n updatedAtMs: nowMs,\n timeline: CommerceModel.appendOrderEvent(order, 'refund', `${parts.join('. ')}.`, nowMs),\n }\n transaction.update(orderRef, patch)\n if (recorded > 0) {\n stageOrderEvent(transaction, ORDER_REFUNDED_EVENT, {\n hostId: request.hostId,\n orderId: request.recordId,\n key: `refund:channel:${refundId}`,\n order: { ...(snapshot.data() ?? {}), ...patch },\n extra: { refund: { id: null, amountCents: recorded, lineItemIds: restockedLines, full } },\n })\n }\n return { outcome: 'recorded', refundedCents, restockedUnits }\n })\n}\n"],"names":["createResourceUid","firebaseAdmin","CommerceModel","ORDER_FULFILLED_EVENT","ORDER_REFUNDED_EVENT","createHash","fulfillmentEventView","stageOrderEvent","restockCapKey","saleReleaseCaps","DOC_ID","MAX_CENTS","HANDOFF_KEY","MAX_REFUND_IDS","restockKey","handoffFulfillmentId","orderId","update","digest","slice","defaultDeps","firestore","app","now","Date","clip","value","max","String","trim","validIds","hostId","recordId","test","completeChannelOrder","request","deps","outcome","orderRef","collection","doc","nowMs","fulfillmentId","runTransaction","transaction","snapshot","order","get","exists","liftLegacyOrder","data","channel","channelSource","status","fulfillments","some","entry","id","canTransitionOrder","lines","remainingFulfillmentLines","length","orderLineFulfillmentStates","filter","state","remainingQuantity","map","lineItemId","quantity","carrier","channelLabel","fulfillment","lineItemIds","notify","atMs","patch","updatedAtMs","timeline","appendOrderEvent","note","key","extra","remainingReleaseCaps","rows","caps","row","reason","delta","Number","has","set","Math","recordChannelOrderRefund","refundId","amount","amountCents","isInteger","asked","Array","isArray","restock","lineIndex","hostRef","source","refundIds","includes","lineItems","wanted","Map","line","productId","variantId","min","size","where","docs","productIds","Set","keys","index","productSnaps","Promise","all","products","forEach","product","liftLegacyProduct","restockedUnits","restockedLines","ledger","changed","variant","variants","find","inventory","budget","back","adjustVariantInventory","add","push","total","totals","totalCents","before","refundedCents","recorded","full","productStockFields","money","cents","toFixed","currency","parts","join","refund"],"mappings":"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"}
|