@aglyn/plugins-commerce 1.0.0-beta.221 → 1.0.0-beta.223

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.
Files changed (125) hide show
  1. package/README.md +4 -2
  2. package/package.json +14 -13
  3. package/src/lib/components/console/catalog-organization-card.component.js +9 -0
  4. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  5. package/src/lib/components/console/discounts-card.component.js +8 -0
  6. package/src/lib/components/console/discounts-card.component.js.map +1 -1
  7. package/src/lib/components/console/gift-cards-card.component.js +8 -0
  8. package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
  9. package/src/lib/components/console/host-coupons-card.component.js +8 -0
  10. package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
  11. package/src/lib/components/console/host-orders-card.component.js +34 -70
  12. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  13. package/src/lib/components/console/product-zones.d.ts +7 -6
  14. package/src/lib/components/console/product-zones.js +5 -4
  15. package/src/lib/components/console/product-zones.js.map +1 -1
  16. package/src/lib/components/console/products-hub-card.component.js +86 -284
  17. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  18. package/src/lib/declarations.console-server.d.ts +38 -0
  19. package/src/lib/declarations.console-server.js +67 -0
  20. package/src/lib/declarations.console-server.js.map +1 -0
  21. package/src/lib/model/commerce-gift-cards.d.ts +26 -0
  22. package/src/lib/model/commerce-gift-cards.js +42 -0
  23. package/src/lib/model/commerce-gift-cards.js.map +1 -1
  24. package/src/lib/model/commerce-orders.d.ts +0 -57
  25. package/src/lib/model/commerce-orders.js +0 -80
  26. package/src/lib/model/commerce-orders.js.map +1 -1
  27. package/src/lib/model/index.d.ts +0 -1
  28. package/src/lib/model/index.js +0 -1
  29. package/src/lib/model/index.js.map +1 -1
  30. package/src/lib/plugin.js +5 -0
  31. package/src/lib/plugin.js.map +1 -1
  32. package/src/lib/server/api-v1/openapi.d.ts +27 -0
  33. package/src/lib/server/api-v1/openapi.js +219 -0
  34. package/src/lib/server/api-v1/openapi.js.map +1 -0
  35. package/src/lib/server/api-v1/orders-and-products.d.ts +19 -0
  36. package/src/lib/server/api-v1/orders-and-products.js +419 -0
  37. package/src/lib/server/api-v1/orders-and-products.js.map +1 -0
  38. package/src/lib/server/billing-webhook.js +10 -13
  39. package/src/lib/server/billing-webhook.js.map +1 -1
  40. package/src/lib/server/download.js +4 -1
  41. package/src/lib/server/download.js.map +1 -1
  42. package/src/lib/server/fulfill-order.d.ts +52 -11
  43. package/src/lib/server/fulfill-order.js +6 -9
  44. package/src/lib/server/fulfill-order.js.map +1 -1
  45. package/src/lib/server/gift-card-issue.d.ts +57 -0
  46. package/src/lib/server/gift-card-issue.js +155 -0
  47. package/src/lib/server/gift-card-issue.js.map +1 -0
  48. package/src/lib/server/gift-cards.js +25 -84
  49. package/src/lib/server/gift-cards.js.map +1 -1
  50. package/src/lib/server/membership-register.js +9 -8
  51. package/src/lib/server/membership-register.js.map +1 -1
  52. package/src/lib/server/newsletter.js +7 -5
  53. package/src/lib/server/newsletter.js.map +1 -1
  54. package/src/lib/server/paid-media-delivery.d.ts +21 -0
  55. package/src/lib/server/paid-media-delivery.js +23 -5
  56. package/src/lib/server/paid-media-delivery.js.map +1 -1
  57. package/src/lib/server/paid-media-serve-count.d.ts +28 -0
  58. package/src/lib/server/paid-media-serve-count.js +46 -0
  59. package/src/lib/server/paid-media-serve-count.js.map +1 -0
  60. package/src/lib/server/person-eraser.d.ts +45 -0
  61. package/src/lib/server/person-eraser.js +57 -0
  62. package/src/lib/server/person-eraser.js.map +1 -0
  63. package/src/lib/server/refund.js +15 -15
  64. package/src/lib/server/refund.js.map +1 -1
  65. package/src/lib/server/restock-flag.d.ts +2 -2
  66. package/src/lib/server/restock-flag.js +1 -1
  67. package/src/lib/server/restock-flag.js.map +1 -1
  68. package/src/lib/server/stream.js +4 -1
  69. package/src/lib/server/stream.js.map +1 -1
  70. package/src/lib/server.js +5 -21
  71. package/src/lib/server.js.map +1 -1
  72. package/src/lib/testing/stand-in-person-records.d.ts +43 -0
  73. package/src/lib/testing/stand-in-person-records.js +63 -0
  74. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  75. package/src/lib/transfer/gift-card-confirm-step.component.d.ts +22 -0
  76. package/src/lib/transfer/gift-card-confirm-step.component.js +259 -0
  77. package/src/lib/transfer/gift-card-confirm-step.component.js.map +1 -0
  78. package/src/lib/transfer/gift-cards-plan.d.ts +29 -0
  79. package/src/lib/transfer/gift-cards-plan.js +40 -0
  80. package/src/lib/transfer/gift-cards-plan.js.map +1 -0
  81. package/src/lib/transfer/gift-cards.server.d.ts +34 -0
  82. package/src/lib/transfer/gift-cards.server.js +296 -0
  83. package/src/lib/transfer/gift-cards.server.js.map +1 -0
  84. package/src/lib/transfer/list-filter.d.ts +29 -0
  85. package/src/lib/transfer/list-filter.js +112 -0
  86. package/src/lib/transfer/list-filter.js.map +1 -0
  87. package/src/lib/transfer/product-import-results.d.ts +29 -0
  88. package/src/lib/transfer/product-import-results.js +49 -0
  89. package/src/lib/transfer/product-import-results.js.map +1 -0
  90. package/src/lib/transfer/product-import-step.component.d.ts +30 -0
  91. package/src/lib/transfer/product-import-step.component.js +99 -0
  92. package/src/lib/transfer/product-import-step.component.js.map +1 -0
  93. package/src/lib/transfer/product-transfer.d.ts +211 -0
  94. package/src/lib/transfer/product-transfer.js +1594 -0
  95. package/src/lib/transfer/product-transfer.js.map +1 -0
  96. package/src/lib/transfer/products.server.d.ts +44 -0
  97. package/src/lib/transfer/products.server.js +550 -0
  98. package/src/lib/transfer/products.server.js.map +1 -0
  99. package/src/lib/transfer/records-transfer.d.ts +182 -0
  100. package/src/lib/transfer/records-transfer.js +1153 -0
  101. package/src/lib/transfer/records-transfer.js.map +1 -0
  102. package/src/lib/transfer/records.server.d.ts +68 -0
  103. package/src/lib/transfer/records.server.js +455 -0
  104. package/src/lib/transfer/records.server.js.map +1 -0
  105. package/src/lib/transfer/register-transfer-resources.d.ts +17 -0
  106. package/src/lib/transfer/register-transfer-resources.js +171 -0
  107. package/src/lib/transfer/register-transfer-resources.js.map +1 -0
  108. package/src/lib/transfer/register-transfer-ui.d.ts +23 -0
  109. package/src/lib/transfer/register-transfer-ui.js +78 -0
  110. package/src/lib/transfer/register-transfer-ui.js.map +1 -0
  111. package/src/lib/transfer/server-common.d.ts +76 -0
  112. package/src/lib/transfer/server-common.js +165 -0
  113. package/src/lib/transfer/server-common.js.map +1 -0
  114. package/src/lib/transfer/transfer-header-actions.component.d.ts +39 -0
  115. package/src/lib/transfer/transfer-header-actions.component.js +79 -0
  116. package/src/lib/transfer/transfer-header-actions.component.js.map +1 -0
  117. package/src/lib/transfer/transfer-keys.d.ts +31 -0
  118. package/src/lib/transfer/transfer-keys.js +56 -0
  119. package/src/lib/transfer/transfer-keys.js.map +1 -0
  120. package/src/lib/model/commerce-io.d.ts +0 -36
  121. package/src/lib/model/commerce-io.js +0 -245
  122. package/src/lib/model/commerce-io.js.map +0 -1
  123. package/src/lib/server/contact-refund.d.ts +0 -117
  124. package/src/lib/server/contact-refund.js +0 -196
  125. package/src/lib/server/contact-refund.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-gift-cards.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\n/**\n * Gift-card hold arithmetic (AGL-2449).\n *\n * A gift card is CASH, and until this module existed it was the one limited\n * resource in the order path with no reserve at all. `cart-checkout.ts` read\n * `balanceCents` with a plain `.get()`, minted a Stripe coupon for that much,\n * and the webhook decremented the card minutes later with a bare\n * `FieldValue.increment(-N)`. The increment is atomic, so nothing was ever\n * LOST — but nothing ever CHECKED either, so two checkouts entering the same\n * code both read $50, both got $50 off, and the card settled at `-$50`. The\n * merchant shipped $100 of goods against a $50 card, and the outstanding\n * liability total went negative — the exact corruption `redeemExistingOrRecord`\n * already reasons about for a DELETED card, arriving through a live one.\n *\n * The stock fix (AGL-2320) could not be copied here: stock is decremented after\n * the money has moved and therefore cannot refuse, so it locks and reports the\n * shortfall. A gift card is applied BEFORE Stripe is contacted, so this door can\n * still say no — and refusing is the only thing that actually keeps the money.\n *\n * So the balance is HELD at checkout and SETTLED at the webhook, both in one\n * transaction against one document. Holds live in a map on the card itself\n * keyed by Stripe session id, which is what keeps this to a single-document\n * transaction: no fan-out, no second collection, no index.\n *\n * ## Why holds expire rather than being swept\n *\n * An abandoned checkout must not stand a customer's money off indefinitely, and\n * a sweep job is the wrong instrument: `process-abandoned.ts` is the only\n * existing sweep and it returns 501 when SMTP is unconfigured, so a\n * release that depended on it would not run at all for a self-hosted store with\n * no mail transport. Instead every hold carries `expiresAtMs` and EVERY read\n * prunes, so the release is a consequence of the next read rather than of a job\n * that may never run. A card nobody touches again holds nothing that matters,\n * because nobody is reading it either.\n *\n * The TTL matches Stripe's Checkout Session lifetime rather than something\n * shorter. A hold that lapsed while its session was still payable would reopen\n * the very window this closes — the shopper pays a session whose hold has\n * evaporated and the balance is spent twice again.\n */\n\n/** One in-flight claim on a card's balance, keyed by Stripe session id. */\nexport interface GiftCardHold {\n cents: number\n expiresAtMs: number\n}\n\n/**\n * Why a card cannot be redeemed for now (AGL-3363): the payment that bought\n * it drew an issuer's early fraud warning, a Radar review, or a dispute; or\n * part of it was refunded outside the console and the refund could not be\n * matched to the order's lines (`refund-review`).\n */\nexport type GiftCardFreezeReason =\n | 'early-fraud-warning'\n | 'radar-review'\n | 'dispute'\n | 'refund-review'\n\n/** Why a card's balance was taken back rather than spent. */\nexport type GiftCardVoidReason = 'refund' | 'dispute-lost'\n\n/** `hosts/{hostId}/giftCards/{code}` doc, as far as redemption cares. */\nexport interface HostGiftCard {\n balanceCents?: number\n /** Session id → hold. Absent on every card issued before AGL-2449. */\n holds?: Record<string, GiftCardHold>\n voidedAtMs?: number\n voidedReason?: GiftCardVoidReason\n /** Set while the purchase is under a fraud signal; nothing can redeem it. */\n frozenAtMs?: number\n frozenReason?: GiftCardFreezeReason\n}\n\n/*==========================================\n * GIFT CARDS ARE A CASH-OUT (AGL-3363).\n *\n * Buying store credit with a stolen card and spending it before the\n * chargeback lands is the standard way to turn card data into goods. Three\n * rules, all shared by every door that sells or redeems a card:\n *\n * 1. A PURCHASE HAS A CEILING. One checkout may buy at most\n * {@link GIFT_CARD_PURCHASE_LIMITS} of gift cards — lower while the\n * workspace is young, when a stolen-card storefront is likeliest — and\n * one visitor may buy gift cards only so many times a day. A real shop's\n * holiday order ($100 cards for a team) sits well inside both.\n * 2. A SIGNAL FREEZES. An early fraud warning, a Radar review or a dispute\n * on the purchase freezes every card it issued (`frozenAtMs`): its balance\n * stays, and nothing can redeem it until the merchant releases it or the\n * dispute is won.\n * 3. A REVERSAL VOIDS. A refund of the purchase, or a lost dispute, zeroes\n * what is left on its cards — the same zeroing the console's Void button\n * does, never a delete (AGL-1767).\n *=========================================*/\n\nexport const GIFT_CARD_PURCHASE_LIMITS = {\n /** Gift-card value one checkout may buy. */\n perOrderCents: 500_000,\n /** …while the workspace is young (`isYoungWorkspace`). */\n perOrderCentsYoung: 50_000,\n /** Checkouts buying gift cards, per visitor address per day. */\n perVisitorDaily: 5,\n /** …while the workspace is young. */\n perVisitorDailyYoung: 2,\n} as const\n\n/** The refusal a checkout gives past the per-order ceiling, or null. */\nexport function giftCardPurchaseRefusal(input: {\n giftCardCents: number\n young: boolean\n}): string | null {\n const ceiling = input.young\n ? GIFT_CARD_PURCHASE_LIMITS.perOrderCentsYoung\n : GIFT_CARD_PURCHASE_LIMITS.perOrderCents\n if (!(input.giftCardCents > ceiling)) return null\n // The ceiling is not named: a shopper who is testing it must not be told\n // where it is. A real buyer of a large gift order is sent to the shop.\n return (\n 'This order has more in gift cards than can be bought in one online ' +\n 'checkout. Please contact the shop to complete it.'\n )\n}\n\n/** Is the card frozen by a fraud signal on the payment that bought it? */\nexport function isGiftCardFrozen(card: HostGiftCard | undefined): boolean {\n return Number(card?.frozenAtMs) > 0\n}\n\n/**\n * Stripe Checkout Sessions expire 24h after creation, so a hold outlives any\n * session that can still be paid. See the module docblock for why a shorter\n * TTL is not the safer choice it looks like.\n */\nexport const GIFT_CARD_HOLD_TTL_MS = 24 * 60 * 60 * 1000\n\n/** One money figure off an untrusted document, as whole non-negative cents. */\nfunction cents(value: unknown): number {\n const number = Math.round(Number(value))\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/**\n * The card's holds with the lapsed ones dropped.\n *\n * Total, not a filter over a trusted shape: a hold whose `expiresAtMs` is\n * absent or non-numeric is treated as EXPIRED rather than eternal, so a\n * malformed row releases the money instead of stranding it. The one direction\n * that cannot be wrong is the one that keeps a customer from spending their own\n * balance forever.\n */\nexport function pruneGiftCardHolds(\n holds: Record<string, GiftCardHold> | undefined,\n nowMs: number,\n): Record<string, GiftCardHold> {\n const live: Record<string, GiftCardHold> = {}\n for (const [sessionId, hold] of Object.entries(holds ?? {})) {\n const expiresAtMs = Number(hold?.expiresAtMs)\n if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) continue\n const amount = cents(hold?.cents)\n if (amount > 0) live[sessionId] = { cents: amount, expiresAtMs }\n }\n return live\n}\n\n/**\n * What a NEW checkout may apply: the balance less every hold still standing.\n *\n * Never negative. A card whose holds already exceed its balance (possible only\n * on a card voided mid-flight, where the console zeroes `balanceCents` while\n * holds stand) reads as empty rather than as a debt to be collected.\n */\nexport function giftCardAvailableCents(\n card: HostGiftCard | undefined,\n nowMs: number,\n): number {\n // A frozen card offers nothing to a NEW checkout (AGL-3363). Settlement of\n // a hold placed before the freeze is left alone: that shopper has paid.\n if (isGiftCardFrozen(card)) return 0\n const held = Object.values(pruneGiftCardHolds(card?.holds, nowMs)).reduce(\n (sum, hold) => sum + hold.cents,\n 0,\n )\n return Math.max(0, cents(card?.balanceCents) - held)\n}\n\n/**\n * What settling `sessionId` should actually take off the card.\n *\n * Capped at the live balance, not at the hold, and that is the point of the\n * function. The hold is what this session RESERVED; the balance is what the\n * card can still give up. They diverge when the card was voided or hand-adjusted\n * between the hold and the payment, and in that window paying out the hold would\n * drive the balance negative — reintroducing the corrupted liability aggregate\n * from the other side.\n *\n * A session with no hold settles ZERO. That is the redelivery case (the first\n * delivery consumed it) and the pre-AGL-2449 case (a session that predates\n * holds), and both must be no-ops rather than a second decrement.\n */\nexport function giftCardSettlementCents(\n card: HostGiftCard | undefined,\n sessionId: string,\n // Deliberately unused, and kept so this reads as the sibling of\n // `giftCardAvailableCents` at every call site. The asymmetry IS the rule\n // below: availability is a question about now, settlement is not.\n _nowMs: number,\n): number {\n // NOT pruned: a hold that lapsed while its session sat unpaid is still owed\n // once that session is paid. Expiry governs what a NEW checkout may claim,\n // never whether a completed payment is honoured — dropping it here would take\n // the discount from the shopper and give nothing back to the merchant.\n const hold = (card?.holds ?? {})[sessionId]\n return Math.min(cents(hold?.cents), cents(card?.balanceCents))\n}\n"],"names":["GIFT_CARD_PURCHASE_LIMITS","perOrderCents","perOrderCentsYoung","perVisitorDaily","perVisitorDailyYoung","giftCardPurchaseRefusal","input","ceiling","young","giftCardCents","isGiftCardFrozen","card","Number","frozenAtMs","GIFT_CARD_HOLD_TTL_MS","cents","value","number","Math","round","isFinite","pruneGiftCardHolds","holds","nowMs","live","sessionId","hold","Object","entries","expiresAtMs","amount","giftCardAvailableCents","held","values","reduce","sum","max","balanceCents","giftCardSettlementCents","_nowMs","min"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,yEAAyE,GAiCzE;;;;;;;;;;;;;;;;;;;2CAmB2C,GAE3C,OAAO,MAAMA,4BAA4B;IACvC,0CAA0C,GAC1CC,eAAe;IACf,wDAAwD,GACxDC,oBAAoB;IACpB,8DAA8D,GAC9DC,iBAAiB;IACjB,mCAAmC,GACnCC,sBAAsB;AACxB,EAAU;AAEV,sEAAsE,GACtE,OAAO,SAASC,wBAAwBC,KAGvC;IACC,MAAMC,UAAUD,MAAME,KAAK,GACvBR,0BAA0BE,kBAAkB,GAC5CF,0BAA0BC,aAAa;IAC3C,IAAI,CAAEK,CAAAA,MAAMG,aAAa,GAAGF,OAAM,GAAI,OAAO;IAC7C,yEAAyE;IACzE,uEAAuE;IACvE,OACE,wEACA;AAEJ;AAEA,wEAAwE,GACxE,OAAO,SAASG,iBAAiBC,IAA8B;IAC7D,OAAOC,OAAOD,wBAAAA,KAAME,UAAU,IAAI;AACpC;AAEA;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB,KAAK,KAAK,KAAK,KAAI;AAExD,6EAA6E,GAC7E,SAASC,MAAMC,KAAc;IAC3B,MAAMC,SAASC,KAAKC,KAAK,CAACP,OAAOI;IACjC,OAAOJ,OAAOQ,QAAQ,CAACH,WAAWA,SAAS,IAAIA,SAAS;AAC1D;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASI,mBACdC,KAA+C,EAC/CC,KAAa;IAEb,MAAMC,OAAqC,CAAC;IAC5C,KAAK,MAAM,CAACC,WAAWC,KAAK,IAAIC,OAAOC,OAAO,CAACN,gBAAAA,QAAS,CAAC,GAAI;QAC3D,MAAMO,cAAcjB,OAAOc,wBAAAA,KAAMG,WAAW;QAC5C,IAAI,CAACjB,OAAOQ,QAAQ,CAACS,gBAAgBA,eAAeN,OAAO;QAC3D,MAAMO,SAASf,MAAMW,wBAAAA,KAAMX,KAAK;QAChC,IAAIe,SAAS,GAAGN,IAAI,CAACC,UAAU,GAAG;YAAEV,OAAOe;YAAQD;QAAY;IACjE;IACA,OAAOL;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASO,uBACdpB,IAA8B,EAC9BY,KAAa;IAEb,2EAA2E;IAC3E,wEAAwE;IACxE,IAAIb,iBAAiBC,OAAO,OAAO;IACnC,MAAMqB,OAAOL,OAAOM,MAAM,CAACZ,mBAAmBV,wBAAAA,KAAMW,KAAK,EAAEC,QAAQW,MAAM,CACvE,CAACC,KAAKT,OAASS,MAAMT,KAAKX,KAAK,EAC/B;IAEF,OAAOG,KAAKkB,GAAG,CAAC,GAAGrB,MAAMJ,wBAAAA,KAAM0B,YAAY,IAAIL;AACjD;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASM,wBACd3B,IAA8B,EAC9Bc,SAAiB,EACjB,gEAAgE;AAChE,yEAAyE;AACzE,kEAAkE;AAClEc,MAAc;;IAEd,4EAA4E;IAC5E,2EAA2E;IAC3E,8EAA8E;IAC9E,uEAAuE;IACvE,MAAMb,OAAO,SAACf,wBAAAA,KAAMW,KAAK,mBAAI,CAAC,EAAE,CAACG,UAAU;IAC3C,OAAOP,KAAKsB,GAAG,CAACzB,MAAMW,wBAAAA,KAAMX,KAAK,GAAGA,MAAMJ,wBAAAA,KAAM0B,YAAY;AAC9D"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-gift-cards.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\n/**\n * Gift-card hold arithmetic (AGL-2449).\n *\n * A gift card is CASH, and until this module existed it was the one limited\n * resource in the order path with no reserve at all. `cart-checkout.ts` read\n * `balanceCents` with a plain `.get()`, minted a Stripe coupon for that much,\n * and the webhook decremented the card minutes later with a bare\n * `FieldValue.increment(-N)`. The increment is atomic, so nothing was ever\n * LOST — but nothing ever CHECKED either, so two checkouts entering the same\n * code both read $50, both got $50 off, and the card settled at `-$50`. The\n * merchant shipped $100 of goods against a $50 card, and the outstanding\n * liability total went negative — the exact corruption `redeemExistingOrRecord`\n * already reasons about for a DELETED card, arriving through a live one.\n *\n * The stock fix (AGL-2320) could not be copied here: stock is decremented after\n * the money has moved and therefore cannot refuse, so it locks and reports the\n * shortfall. A gift card is applied BEFORE Stripe is contacted, so this door can\n * still say no — and refusing is the only thing that actually keeps the money.\n *\n * So the balance is HELD at checkout and SETTLED at the webhook, both in one\n * transaction against one document. Holds live in a map on the card itself\n * keyed by Stripe session id, which is what keeps this to a single-document\n * transaction: no fan-out, no second collection, no index.\n *\n * ## Why holds expire rather than being swept\n *\n * An abandoned checkout must not stand a customer's money off indefinitely, and\n * a sweep job is the wrong instrument: `process-abandoned.ts` is the only\n * existing sweep and it returns 501 when SMTP is unconfigured, so a\n * release that depended on it would not run at all for a self-hosted store with\n * no mail transport. Instead every hold carries `expiresAtMs` and EVERY read\n * prunes, so the release is a consequence of the next read rather than of a job\n * that may never run. A card nobody touches again holds nothing that matters,\n * because nobody is reading it either.\n *\n * The TTL matches Stripe's Checkout Session lifetime rather than something\n * shorter. A hold that lapsed while its session was still payable would reopen\n * the very window this closes — the shopper pays a session whose hold has\n * evaporated and the balance is spent twice again.\n */\n\n/** One in-flight claim on a card's balance, keyed by Stripe session id. */\nexport interface GiftCardHold {\n cents: number\n expiresAtMs: number\n}\n\n/**\n * Why a card cannot be redeemed for now (AGL-3363): the payment that bought\n * it drew an issuer's early fraud warning, a Radar review, or a dispute; or\n * part of it was refunded outside the console and the refund could not be\n * matched to the order's lines (`refund-review`).\n */\nexport type GiftCardFreezeReason =\n | 'early-fraud-warning'\n | 'radar-review'\n | 'dispute'\n | 'refund-review'\n\n/** Why a card's balance was taken back rather than spent. */\nexport type GiftCardVoidReason = 'refund' | 'dispute-lost'\n\n/** `hosts/{hostId}/giftCards/{code}` doc, as far as redemption cares. */\nexport interface HostGiftCard {\n balanceCents?: number\n /** Session id → hold. Absent on every card issued before AGL-2449. */\n holds?: Record<string, GiftCardHold>\n voidedAtMs?: number\n voidedReason?: GiftCardVoidReason\n /** Set while the purchase is under a fraud signal; nothing can redeem it. */\n frozenAtMs?: number\n frozenReason?: GiftCardFreezeReason\n}\n\n/*==========================================\n * GIFT CARDS ARE A CASH-OUT (AGL-3363).\n *\n * Buying store credit with a stolen card and spending it before the\n * chargeback lands is the standard way to turn card data into goods. Three\n * rules, all shared by every door that sells or redeems a card:\n *\n * 1. A PURCHASE HAS A CEILING. One checkout may buy at most\n * {@link GIFT_CARD_PURCHASE_LIMITS} of gift cards — lower while the\n * workspace is young, when a stolen-card storefront is likeliest — and\n * one visitor may buy gift cards only so many times a day. A real shop's\n * holiday order ($100 cards for a team) sits well inside both.\n * 2. A SIGNAL FREEZES. An early fraud warning, a Radar review or a dispute\n * on the purchase freezes every card it issued (`frozenAtMs`): its balance\n * stays, and nothing can redeem it until the merchant releases it or the\n * dispute is won.\n * 3. A REVERSAL VOIDS. A refund of the purchase, or a lost dispute, zeroes\n * what is left on its cards — the same zeroing the console's Void button\n * does, never a delete (AGL-1767).\n *=========================================*/\n\nexport const GIFT_CARD_PURCHASE_LIMITS = {\n /** Gift-card value one checkout may buy. */\n perOrderCents: 500_000,\n /** …while the workspace is young (`isYoungWorkspace`). */\n perOrderCentsYoung: 50_000,\n /** Checkouts buying gift cards, per visitor address per day. */\n perVisitorDaily: 5,\n /** …while the workspace is young. */\n perVisitorDailyYoung: 2,\n} as const\n\n/** The refusal a checkout gives past the per-order ceiling, or null. */\nexport function giftCardPurchaseRefusal(input: {\n giftCardCents: number\n young: boolean\n}): string | null {\n const ceiling = input.young\n ? GIFT_CARD_PURCHASE_LIMITS.perOrderCentsYoung\n : GIFT_CARD_PURCHASE_LIMITS.perOrderCents\n if (!(input.giftCardCents > ceiling)) return null\n // The ceiling is not named: a shopper who is testing it must not be told\n // where it is. A real buyer of a large gift order is sent to the shop.\n return (\n 'This order has more in gift cards than can be bought in one online ' +\n 'checkout. Please contact the shop to complete it.'\n )\n}\n\n/** Is the card frozen by a fraud signal on the payment that bought it? */\nexport function isGiftCardFrozen(card: HostGiftCard | undefined): boolean {\n return Number(card?.frozenAtMs) > 0\n}\n\n/**\n * Stripe Checkout Sessions expire 24h after creation, so a hold outlives any\n * session that can still be paid. See the module docblock for why a shorter\n * TTL is not the safer choice it looks like.\n */\nexport const GIFT_CARD_HOLD_TTL_MS = 24 * 60 * 60 * 1000\n\n/** One money figure off an untrusted document, as whole non-negative cents. */\nfunction cents(value: unknown): number {\n const number = Math.round(Number(value))\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/**\n * The card's holds with the lapsed ones dropped.\n *\n * Total, not a filter over a trusted shape: a hold whose `expiresAtMs` is\n * absent or non-numeric is treated as EXPIRED rather than eternal, so a\n * malformed row releases the money instead of stranding it. The one direction\n * that cannot be wrong is the one that keeps a customer from spending their own\n * balance forever.\n */\nexport function pruneGiftCardHolds(\n holds: Record<string, GiftCardHold> | undefined,\n nowMs: number,\n): Record<string, GiftCardHold> {\n const live: Record<string, GiftCardHold> = {}\n for (const [sessionId, hold] of Object.entries(holds ?? {})) {\n const expiresAtMs = Number(hold?.expiresAtMs)\n if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) continue\n const amount = cents(hold?.cents)\n if (amount > 0) live[sessionId] = { cents: amount, expiresAtMs }\n }\n return live\n}\n\n/**\n * What a NEW checkout may apply: the balance less every hold still standing.\n *\n * Never negative. A card whose holds already exceed its balance (possible only\n * on a card voided mid-flight, where the console zeroes `balanceCents` while\n * holds stand) reads as empty rather than as a debt to be collected.\n */\nexport function giftCardAvailableCents(\n card: HostGiftCard | undefined,\n nowMs: number,\n): number {\n // A frozen card offers nothing to a NEW checkout (AGL-3363). Settlement of\n // a hold placed before the freeze is left alone: that shopper has paid.\n if (isGiftCardFrozen(card)) return 0\n const held = Object.values(pruneGiftCardHolds(card?.holds, nowMs)).reduce(\n (sum, hold) => sum + hold.cents,\n 0,\n )\n return Math.max(0, cents(card?.balanceCents) - held)\n}\n\n/**\n * What settling `sessionId` should actually take off the card.\n *\n * Capped at the live balance, not at the hold, and that is the point of the\n * function. The hold is what this session RESERVED; the balance is what the\n * card can still give up. They diverge when the card was voided or hand-adjusted\n * between the hold and the payment, and in that window paying out the hold would\n * drive the balance negative — reintroducing the corrupted liability aggregate\n * from the other side.\n *\n * A session with no hold settles ZERO. That is the redelivery case (the first\n * delivery consumed it) and the pre-AGL-2449 case (a session that predates\n * holds), and both must be no-ops rather than a second decrement.\n */\nexport function giftCardSettlementCents(\n card: HostGiftCard | undefined,\n sessionId: string,\n // Deliberately unused, and kept so this reads as the sibling of\n // `giftCardAvailableCents` at every call site. The asymmetry IS the rule\n // below: availability is a question about now, settlement is not.\n _nowMs: number,\n): number {\n // NOT pruned: a hold that lapsed while its session sat unpaid is still owed\n // once that session is paid. Expiry governs what a NEW checkout may claim,\n // never whether a completed payment is honoured — dropping it here would take\n // the discount from the shopper and give nothing back to the merchant.\n const hold = (card?.holds ?? {})[sessionId]\n return Math.min(cents(hold?.cents), cents(card?.balanceCents))\n}\n\n/*==========================================\n * ISSUING A CARD (AGL-2226, AGL-3551).\n *\n * A card is issued, never written: by hand from the Gift cards card, or one\n * card per row from an import, both through `issueGiftCard`\n * (`server/gift-card-issue.ts`). These are the rules both doors share.\n *=========================================*/\n\n/** The most one card may be issued for, by hand or from a file. */\nexport const GIFT_CARD_ISSUE_MAX_CENTS = 100_000\n\n/** The currency every card is held in: the store charges in US dollars. */\nexport const GIFT_CARD_CURRENCY = 'USD'\n\n/**\n * A code as the store keeps it: upper case, with no spaces — a code another\n * platform printed in groups (`ABCD EFGH 1234`) is the same code joined.\n */\nexport function giftCardCodeOf(value: unknown): string {\n return String(value ?? '')\n .trim()\n .replace(/\\s+/g, '')\n .toUpperCase()\n}\n\n/** The shortest code a card may carry: anything shorter can be guessed at a checkout. */\nexport const GIFT_CARD_CODE_MIN = 8\nexport const GIFT_CARD_CODE_MAX = 40\n\n/** Why a code cannot be a card's code, or `null`. A code is the card's document id and spends it. */\nexport function giftCardCodeProblem(code: string): string | null {\n if (code.length < GIFT_CARD_CODE_MIN || code.length > GIFT_CARD_CODE_MAX || !/^[A-Z0-9][A-Z0-9_-]*$/.test(code)) {\n return (\n `A gift card code is ${GIFT_CARD_CODE_MIN} to ${GIFT_CARD_CODE_MAX} letters, digits, ` +\n 'hyphens or underscores. Leave the code blank and a new one is made.'\n )\n }\n return null\n}\n\n/** Why a card cannot be issued for this many cents, or `null`. */\nexport function giftCardAmountProblem(amountCents: number): string | null {\n if (!Number.isInteger(amountCents) || amountCents <= 0) return 'A gift card is issued for more than $0.'\n if (amountCents > GIFT_CARD_ISSUE_MAX_CENTS) {\n return `A gift card may be issued for at most $${(GIFT_CARD_ISSUE_MAX_CENTS / 100).toLocaleString('en-US')}.`\n }\n return null\n}\n\n/**\n * Whether anybody has spent from a card: a redemption settled on it\n * (`lastUsedAtMs`, a balance below what it was issued for), or a checkout\n * holding part of it — expired holds included, because a lapsed hold is\n * still settled when its session is paid (`giftCardSettlementCents`).\n */\nexport function giftCardRedeemed(\n card: (HostGiftCard & { initialCents?: number; lastUsedAtMs?: number }) | undefined,\n): boolean {\n if (!card) return false\n if (Number(card.lastUsedAtMs) > 0) return true\n if (Object.keys(card.holds ?? {}).length) return true\n const initial = Number(card.initialCents)\n return Number.isFinite(initial) && cents(card.balanceCents) < initial\n}\n"],"names":["GIFT_CARD_PURCHASE_LIMITS","perOrderCents","perOrderCentsYoung","perVisitorDaily","perVisitorDailyYoung","giftCardPurchaseRefusal","input","ceiling","young","giftCardCents","isGiftCardFrozen","card","Number","frozenAtMs","GIFT_CARD_HOLD_TTL_MS","cents","value","number","Math","round","isFinite","pruneGiftCardHolds","holds","nowMs","live","sessionId","hold","Object","entries","expiresAtMs","amount","giftCardAvailableCents","held","values","reduce","sum","max","balanceCents","giftCardSettlementCents","_nowMs","min","GIFT_CARD_ISSUE_MAX_CENTS","GIFT_CARD_CURRENCY","giftCardCodeOf","String","trim","replace","toUpperCase","GIFT_CARD_CODE_MIN","GIFT_CARD_CODE_MAX","giftCardCodeProblem","code","length","test","giftCardAmountProblem","amountCents","isInteger","toLocaleString","giftCardRedeemed","lastUsedAtMs","keys","initial","initialCents"],"mappings":"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"}
@@ -808,17 +808,6 @@ export declare function formatOrderNumber(order: Pick<HostOrder, 'number'>, docI
808
808
  * into the v1 shape for display; already-shaped orders pass through.
809
809
  */
810
810
  export declare function liftLegacyOrder(raw: Partial<HostOrder>): HostOrder;
811
- /**
812
- * An orders-console row: the stored doc plus the Firestore id the collection
813
- * hook attaches. `createdAt` is typed structurally rather than against the
814
- * Firestore SDK — this module is pure and installs no client.
815
- */
816
- export interface OrderExportRow extends Partial<HostOrder> {
817
- $id?: string;
818
- createdAt?: {
819
- toDate?: () => Date;
820
- } | null;
821
- }
822
811
  /**
823
812
  * Does this order contain `productId`?
824
813
  *
@@ -997,51 +986,5 @@ export declare function summarizeOrderWindow(orders: readonly (Partial<HostOrder
997
986
  nowMs: number;
998
987
  days?: number;
999
988
  }): OrderWindowSummary;
1000
- /**
1001
- * Header for {@link buildOrdersCsv}. The original seven columns (AGL-96) keep
1002
- * their names AND their positions, so a saved import mapping — by name or by
1003
- * index — still resolves; the four reconciliation columns are appended.
1004
- */
1005
- export declare const ORDERS_CSV_HEADER: string;
1006
- /**
1007
- * The orders console's `orders.csv` (AGL-1747).
1008
- *
1009
- * ## What was wrong
1010
- *
1011
- * The export read `amountCents`, `feeCents` and `productId` — the legacy
1012
- * Commerce Starter flat fields (AGL-90) — and nothing else. Checked writer by
1013
- * writer, only the two buy-now-shaped Stripe paths write them:
1014
- *
1015
- * | Order path | `amountCents` | `productId` |
1016
- * | -- | -- | -- |
1017
- * | buy-now (`commerce-order`, `billing-webhook.ts`) | yes | yes |
1018
- * | cart (`commerce-cart`, `billing-webhook.ts`) | yes | **no** |
1019
- * | POS cash and POS card (`pos-order.ts`) | **no** | **no** |
1020
- * | draft (`draft-order.ts`, paid by `commerce-draft`) | **no** | **no** |
1021
- *
1022
- * So every POS and draft order exported as `$0.00` and every cart order
1023
- * exported with a blank product, each beside a plausible date, email and order
1024
- * id — nothing in the file signals that the number is missing rather than
1025
- * genuinely zero. The console screen was right the whole time: the table row
1026
- * beside the export button, the detail dialog, the analytics card and lifetime
1027
- * purchases all read `totals` with the flat field only as a fallback. The
1028
- * export was the sole reader that had the precedence backwards, and a CSV is
1029
- * the artefact that reaches a bookkeeper.
1030
- *
1031
- * ## What "correct" means
1032
- *
1033
- * Per AGL-1711's lesson, the useful test is not "do the parts sum" but "does
1034
- * each column match what was actually charged" — a total-only assertion passes
1035
- * against a row whose every component is wrong. The spec therefore pins each
1036
- * cell of a worked example individually.
1037
- *
1038
- * ## Refunds
1039
- *
1040
- * `amountUsd` stays gross, which is what it has always meant, and the appended
1041
- * `refundedUsd`/`netUsd` carry the rest. A refunded order previously exported
1042
- * at its gross with nothing beside it to say so — the same class of error, in
1043
- * that the file understated nothing but overstated revenue.
1044
- */
1045
- export declare function buildOrdersCsv(orders: OrderExportRow[], productNames?: Record<string, string>): string;
1046
989
  /** Appends a timeline event immutably (webhook + console share this). */
1047
990
  export declare function appendOrderEvent(order: Pick<HostOrder, 'timeline'>, event: string, detail?: string, atMs?: number): OrderTimelineEvent[];
@@ -926,86 +926,6 @@ const ORDER_WINDOW_DAY_MS = 24 * 60 * 60 * 1000;
926
926
  aovDeltaPct: deltaPct(aovCents, priorAovCents)
927
927
  };
928
928
  }
929
- /**
930
- * Header for {@link buildOrdersCsv}. The original seven columns (AGL-96) keep
931
- * their names AND their positions, so a saved import mapping — by name or by
932
- * index — still resolves; the four reconciliation columns are appended.
933
- */ export const ORDERS_CSV_HEADER = 'date,product,amountUsd,feeUsd,customerEmail,coupon,orderId,' + 'status,channel,refundedUsd,netUsd';
934
- /**
935
- * The orders console's `orders.csv` (AGL-1747).
936
- *
937
- * ## What was wrong
938
- *
939
- * The export read `amountCents`, `feeCents` and `productId` — the legacy
940
- * Commerce Starter flat fields (AGL-90) — and nothing else. Checked writer by
941
- * writer, only the two buy-now-shaped Stripe paths write them:
942
- *
943
- * | Order path | `amountCents` | `productId` |
944
- * | -- | -- | -- |
945
- * | buy-now (`commerce-order`, `billing-webhook.ts`) | yes | yes |
946
- * | cart (`commerce-cart`, `billing-webhook.ts`) | yes | **no** |
947
- * | POS cash and POS card (`pos-order.ts`) | **no** | **no** |
948
- * | draft (`draft-order.ts`, paid by `commerce-draft`) | **no** | **no** |
949
- *
950
- * So every POS and draft order exported as `$0.00` and every cart order
951
- * exported with a blank product, each beside a plausible date, email and order
952
- * id — nothing in the file signals that the number is missing rather than
953
- * genuinely zero. The console screen was right the whole time: the table row
954
- * beside the export button, the detail dialog, the analytics card and lifetime
955
- * purchases all read `totals` with the flat field only as a fallback. The
956
- * export was the sole reader that had the precedence backwards, and a CSV is
957
- * the artefact that reaches a bookkeeper.
958
- *
959
- * ## What "correct" means
960
- *
961
- * Per AGL-1711's lesson, the useful test is not "do the parts sum" but "does
962
- * each column match what was actually charged" — a total-only assertion passes
963
- * against a row whose every component is wrong. The spec therefore pins each
964
- * cell of a worked example individually.
965
- *
966
- * ## Refunds
967
- *
968
- * `amountUsd` stays gross, which is what it has always meant, and the appended
969
- * `refundedUsd`/`netUsd` carry the rest. A refunded order previously exported
970
- * at its gross with nothing beside it to say so — the same class of error, in
971
- * that the file understated nothing but overstated revenue.
972
- */ export function buildOrdersCsv(orders, productNames = {}) {
973
- const escape = (cell)=>/[",\n]/.test(cell) ? `"${cell.replace(/"/g, '""')}"` : cell;
974
- return [
975
- ORDERS_CSV_HEADER,
976
- ...orders.map((order)=>{
977
- var _order_lineItems, _ref, _ref1, _ref2, _ref3, _order_productId, _ref4, _ref5, _ref6, _ref7, _order_refundedCents, _order_customerEmail, _order_couponCode, _order_$id, _lifted_channel;
978
- var _order_createdAt_toDate, _order_createdAt, _lineItems_, _lifted_totals, _lifted_totals1;
979
- const lifted = liftLegacyOrder(order);
980
- // The STORED line items, not the lifted ones: `liftLegacyOrder` gives a
981
- // legacy flat row a synthetic line named "Product", which would shadow
982
- // the real name the `productNames` lookup still recovers for those rows.
983
- const lineItems = (_order_lineItems = order.lineItems) != null ? _order_lineItems : [];
984
- // A cart order has several lines and no single product; name the first
985
- // and count the rest, the way the table row renders it. `serverTimestamp`
986
- // reads back null until the write lands, so `createdAtMs` — which every
987
- // modern writer sets alongside it — covers the local snapshot.
988
- const date = (_ref = (_order_createdAt = order.createdAt) == null ? void 0 : (_order_createdAt_toDate = _order_createdAt.toDate) == null ? void 0 : _order_createdAt_toDate.call(_order_createdAt)) != null ? _ref : order.createdAtMs ? new Date(order.createdAtMs) : null;
989
- const product = (_ref1 = (_ref2 = (_ref3 = (_lineItems_ = lineItems[0]) == null ? void 0 : _lineItems_.name) != null ? _ref3 : productNames[(_order_productId = order.productId) != null ? _order_productId : '']) != null ? _ref2 : order.productId) != null ? _ref1 : '';
990
- const totalCents = Number((_ref4 = (_ref5 = (_lifted_totals = lifted.totals) == null ? void 0 : _lifted_totals.totalCents) != null ? _ref5 : order.amountCents) != null ? _ref4 : 0);
991
- const feeCents = Number((_ref6 = (_ref7 = (_lifted_totals1 = lifted.totals) == null ? void 0 : _lifted_totals1.feeCents) != null ? _ref7 : order.feeCents) != null ? _ref6 : 0);
992
- const refundedCents = Number((_order_refundedCents = order.refundedCents) != null ? _order_refundedCents : 0);
993
- return [
994
- date ? date.toISOString() : '',
995
- lineItems.length > 1 ? `${product} +${lineItems.length - 1} more` : product,
996
- usd(totalCents),
997
- usd(feeCents),
998
- (_order_customerEmail = order.customerEmail) != null ? _order_customerEmail : '',
999
- (_order_couponCode = order.couponCode) != null ? _order_couponCode : '',
1000
- (_order_$id = order.$id) != null ? _order_$id : '',
1001
- lifted.status,
1002
- (_lifted_channel = lifted.channel) != null ? _lifted_channel : 'online',
1003
- usd(refundedCents),
1004
- usd(totalCents - refundedCents)
1005
- ].map((cell)=>escape(String(cell))).join(',');
1006
- })
1007
- ].join('\n');
1008
- }
1009
929
  /** Appends a timeline event immutably (webhook + console share this). */ export function appendOrderEvent(order, event, detail, atMs = Date.now()) {
1010
930
  var _order_timeline;
1011
931
  return [