@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
package/src/lib/server.js CHANGED
@@ -20,7 +20,7 @@
20
20
  * pulls in firebase-admin + Stripe, so it is NOT re-exported from the client
21
21
  * barrel — apps import `@aglyn/plugins-commerce/server` only from their
22
22
  * (server-only) API dispatcher registration.
23
- */ import { registerBillingWebhookHandler, registerOrderFulfilmentService, registerPluginApiRoute, registerPluginJob, registerSitePageEnricher, registerSitePageResolver, registerPluginPermissions, registerPluginConfigSchema } from "@aglyn/aglyn/server";
23
+ */ import { registerBillingWebhookHandler, registerPluginApiRoute, registerPluginJob, registerSitePageEnricher, registerSitePageResolver, registerPluginPermissions, registerPluginConfigSchema } from "@aglyn/aglyn/server";
24
24
  import { isEmailConfigured } from "@aglyn/shared-util-email";
25
25
  import { firebaseAdmin } from "@aglyn/tenant-data-admin";
26
26
  import { BUNDLE_ID } from "./constants/bundle-common.js";
@@ -70,7 +70,7 @@ import { connectHandler } from "./server/connect.js";
70
70
  import { cancelOrderHandler } from "./server/cancel-order.js";
71
71
  import { collectionMembershipHandler } from "./server/collection-membership.js";
72
72
  import { draftOrderHandler } from "./server/draft-order.js";
73
- import { fulfillOrderHandler, recordOrderShipment } from "./server/fulfill-order.js";
73
+ import { fulfillOrderHandler } from "./server/fulfill-order.js";
74
74
  import { giftCardsHandler } from "./server/gift-cards.js";
75
75
  import { memberPostHandler } from "./server/member-post.js";
76
76
  import { orderAnalyticsHandler } from "./server/order-analytics.js";
@@ -302,26 +302,10 @@ registerPluginJob({
302
302
  registerPluginApiRoute('commerce/draft-order', draftOrderHandler);
303
303
  // Fulfil + mark-delivered with the transition re-asked under the write
304
304
  // (AGL-1819) — the same stale-dialog hole cancel-order closes, minus the
305
- // stock release those two transitions never had.
305
+ // stock release those two transitions never had. The customer REST API's
306
+ // `PATCH /v1/sites/{id}/orders/{id}` records a shipment through the same
307
+ // transaction (`server/api-v1/orders-and-products.ts`).
306
308
  registerPluginApiRoute('commerce/fulfill-order', fulfillOrderHandler);
307
- // The same transaction, offered to the HOST APP as a capability (AGL-2461)
308
- // so `/v1` can record a shipment without importing this library — which
309
- // `eslint.config.mjs`'s `scope:app` boundary forbids, and which is why the
310
- // public REST API could take an order and never ship it.
311
- //
312
- // Registered HERE, in the consoleApi surface, beside the route it shares an
313
- // implementation with: `/v1` reaches it through the console's server plugin
314
- // loader, the same `ensureAll(['consoleApi'])` batch the plugin API
315
- // dispatcher runs. A registration at module scope would instead depend on
316
- // which entry point a process happened to import.
317
- //
318
- // NOTHING ABOUT AUTHORIZATION CROSSES THIS LINE. The capability is a
319
- // pre-authorized domain operation and `/v1` does its own gating (key scope,
320
- // plan entitlement, org-owns-host, plugin-enabled-for-site) before it calls.
321
- registerOrderFulfilmentService({
322
- pluginId: BUNDLE_ID,
323
- recordShipment: recordOrderShipment
324
- });
325
309
  // Issue / void store credit (AGL-2226). Server-side because the host
326
310
  // catch-all in the Firestore rules would otherwise let a client write
327
311
  // its own `balanceCents`, which checkout applies as amount-off.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/server.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/**\n * Server half of the commerce plugin (AGL-396): the site-facing storefront\n * API handlers, registered with the plugin API registry and served by the\n * tenant dispatcher at their unchanged `/api/commerce/*` URLs. This module\n * pulls in firebase-admin + Stripe, so it is NOT re-exported from the client\n * barrel — apps import `@aglyn/plugins-commerce/server` only from their\n * (server-only) API dispatcher registration.\n */\n\nimport {\n registerBillingWebhookHandler,\n registerOrderFulfilmentService,\n registerPluginApiRoute,\n registerPluginJob,\n registerSitePageEnricher,\n registerSitePageResolver,\n registerPluginPermissions,\n registerPluginConfigSchema,\n} from '@aglyn/aglyn/server'\nimport { isEmailConfigured } from '@aglyn/shared-util-email'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCommerceMediaPublishGuard } from './server/media-publish-guard'\nimport { registerCommerceRecurringCharges } from './server/recurring-charges'\nimport { registerProductCardReader } from './server/product-card'\nimport { registerCommerceTaxProfile } from './server/tax-profile'\nimport { registerTaxReturnSource } from '@aglyn/aglyn/plugin-manager/plugin-tax-return-sources'\nimport { commerceTaxReturnSource } from './server/tax-return-source'\nimport { taxSummaryHandler } from './server/tax-summary'\nimport { registerRevenueSource } from '@aglyn/aglyn/plugin-manager/plugin-revenue-sources'\nimport { commerceRevenueSource } from './server/revenue-source'\nimport { commerceBillingWebhookHandler } from './server/billing-webhook'\nimport { registerOrderFigureReaders } from './server/order-figures'\nimport { COMMERCE_PERMISSIONS } from './model/plugin-permissions'\nimport { COMMERCE_CONFIG_SCHEMA } from './plugin-config'\nimport { commerceSitePageEnricher } from './server/site-page-enricher'\nimport { commerceSitePageResolver } from './server/site-page-resolver'\nimport { cartCheckoutHandler } from './server/cart-checkout'\nimport { cartHandler } from './server/cart'\nimport { catalogHandler } from './server/catalog'\nimport { checkoutHandler } from './server/checkout'\nimport { downloadHandler } from './server/download'\nimport { feedHandler } from './server/feed'\nimport { newsletterHandler } from './server/newsletter'\nimport { notifyRestockHandler } from './server/notify-restock'\nimport { productHandler } from './server/product'\nimport { relatedHandler } from './server/related'\nimport { reservationAvailabilityHandler } from './server/reservation-availability'\nimport { gateHandler } from './server/gate'\nimport { memberFeedHandler } from './server/member-feed'\nimport { membershipAccountHandler } from './server/membership-account'\nimport { membershipAdminPasswordHandler } from './server/membership-admin-password'\nimport { membershipAdminRemoveHandler } from './server/membership-admin-remove'\nimport { membershipContentHandler } from './server/membership-content'\nimport { membershipLoginHandler } from './server/membership-login'\nimport { membershipLogoutHandler } from './server/membership-logout'\nimport { membershipRecoverHandler } from './server/membership-recover'\nimport { membershipRegisterHandler } from './server/membership-register'\nimport { membershipResetHandler } from './server/membership-reset'\nimport { membershipWishlistHandler } from './server/membership-wishlist'\nimport { reserveHandler } from './server/reserve'\nimport { streamHandler } from './server/stream'\nimport { subscriptionPortalHandler } from './server/subscription-portal'\nimport { reviewsHandler } from './server/reviews'\nimport { connectHandler } from './server/connect'\nimport { cancelOrderHandler } from './server/cancel-order'\nimport { collectionMembershipHandler } from './server/collection-membership'\nimport { draftOrderHandler } from './server/draft-order'\nimport {\n fulfillOrderHandler,\n recordOrderShipment,\n} from './server/fulfill-order'\nimport { giftCardsHandler } from './server/gift-cards'\nimport { memberPostHandler } from './server/member-post'\nimport { orderAnalyticsHandler } from './server/order-analytics'\nimport { posOrderHandler } from './server/pos-order'\nimport {\n processAbandonedHandler,\n scanAbandonedCheckouts,\n} from './server/process-abandoned'\nimport { processRestockHandler, scanRestockAlerts } from './server/process-restock'\nimport { scanStockDecrements } from './server/reconcile-stock'\nimport { refundHandler } from './server/refund'\nimport { scanSupplierDeliveries } from './server/supplier-outbox'\nimport { supplierUpdateHandler } from './server/supplier-update'\n\n/**\n * The two commerce beats (AGL-2227).\n *\n * `commerce/process-abandoned` and `commerce/process-restock` have existed\n * since AGL-323/326 as `x-cron-secret` HTTP doors, and `registerCommerceConsoleApi`\n * below has called them \"the scheduler-driven jobs\" in a comment that whole\n * time. **Nothing scheduled them.** Not `scheduled-crons.yml` (its table names\n * 11 paths; neither of these), not `vercel.json` (no `crons` key at all), not\n * `registerPluginJob` — the only two registrations in the repo were scheduled\n * publishing and the bookings hold-expiry.\n *\n * The cost was a Pro-tier entitlement (`abandonedCart`) that had never sent an\n * email, and a storefront \"notify me when it's back\" form writing into a queue\n * with no drain. AGL-1793 had already added the collection-group indexes these\n * two scans need, which is the clearest evidence they were always meant to run.\n *\n * Module scope, like the bookings job beside it: the runner route reaches jobs\n * through `ensureAll(['tenantApi'])`, and a registration inside a `register*`\n * function would depend on which entry point happened to be loaded.\n *\n * 15 minutes, not 1: `process-abandoned` will not remind a checkout younger\n * than an hour and `process-restock` only mails alerts whose product already\n * has stock, so a tighter beat buys nothing and costs two collection-group\n * scans a minute. Both are bounded (200 docs) and idempotent — each pass\n * stamps what it sent — so an overlapping or repeated beat cannot double-send.\n */\nconst RECOVERY_JOB_INTERVAL_MINUTES = 15\n\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'abandoned-checkout-recovery',\n intervalMinutes: RECOVERY_JOB_INTERVAL_MINUTES,\n description:\n 'Email one recovery reminder per stalled checkout (abandonedCart plans).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n // Quietly, not as an error: email is optional per deployment, and a beat\n // that logs every minute on a self-host without Resend buries everything\n // else in the log.\n if (!isEmailConfigured()) return\n const { sent } = await scanAbandonedCheckouts(gate)\n if (sent) console.info(`commerce: sent ${sent} abandoned-cart reminders`)\n },\n})\n\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'back-in-stock-alerts',\n intervalMinutes: RECOVERY_JOB_INTERVAL_MINUTES,\n description: 'Email shoppers whose requested product is in stock again.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n if (!isEmailConfigured()) return\n const { sent } = await scanRestockAlerts(gate)\n if (sent) console.info(`commerce: sent ${sent} back-in-stock alerts`)\n },\n})\n\n/**\n * The missing-decrement detector (AGL-2358).\n *\n * HOURLY, not on the 15-minute recovery beat: it is a detector for a rare\n * process death, not a queue drain, and nothing it finds gets less true for\n * waiting an hour. The cost of the beat is what sets the interval — one\n * collection-group read of the platform's most recent orders, plus two\n * queries per host that has any — and an hour keeps that off the same tick as\n * the two scans above.\n *\n * NOT gated on `isEmailConfigured()`, unlike its two neighbours. Those send\n * mail; this writes a console notification and a log line, both of which work\n * on a self-host with no mail provider at all — and a stock count that is\n * silently wrong is exactly what a self-hoster least wants suppressed by a\n * setting about email.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'stock-decrement-reconciliation',\n intervalMinutes: 60,\n description:\n 'Report paid orders whose stock decrement never landed (AGL-2358).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const scan = await scanStockDecrements(gate)\n if (scan.missingLines || scan.truncatedHosts) {\n console.warn(\n `commerce: ${scan.missingLines} order lines across ${scan.hosts} ` +\n `sites have no sale ledger row (${scan.reportedOrders} newly ` +\n `reported, ${scan.truncatedHosts} sites' ledger window truncated)`,\n )\n }\n },\n})\n\n/**\n * The dropship supplier outbox drain (AGL-2473).\n *\n * EVERY MINUTE, unlike the three above, and the interval is the point rather\n * than an oversight. This is the only one of the four that a BUYER is waiting\n * on: the row it drains is a paid order that has not been routed to whoever\n * ships it, and every minute it sits is a minute the parcel is not moving. The\n * first backoff step is also 60s, so a supplier that blipped is retried on the\n * next tick.\n *\n * The cost that set fifteen minutes for the recovery pair does not apply. Those\n * are collection-GROUP scans across every site on the platform; this is one\n * ordinary equality query against a top-level collection that is EMPTY in the\n * ordinary case — an empty result bills a single read, so the whole beat is\n * ~1,440 reads a day whether or not anyone is dropshipping.\n *\n * Not gated on `isEmailConfigured()`. The supplier notification is an HTTP POST\n * to the merchant's own supplier, and a self-host with no mail provider still\n * has dropship orders to route.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'supplier-webhook-delivery',\n intervalMinutes: 1,\n description:\n 'Deliver queued dropship supplier notifications, with backoff and a ' +\n 'dead letter that tells the merchant (AGL-2473).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const scan = await scanSupplierDeliveries(gate)\n if (scan.deadLettered) {\n console.warn(\n `commerce: ${scan.deadLettered} dropship orders could not be routed ` +\n `to their supplier and have been reported on the order`,\n )\n }\n },\n})\n\n/**\n * A visitor's payment door: it opens a Stripe Checkout Session for whoever\n * calls it, so the dispatcher holds it to the card-testing counters\n * (AGL-3363). Declared here, beside the registration, so the door cannot\n * exist without it.\n */\nconst CARD_PAYMENT_DOOR = { cardPayment: true } as const\n\n/** Registers the commerce plugin's storefront API routes. */\nexport function registerCommerceApi(): void {\n registerProductCardReader()\n // The merchant's tax rule, for every plugin that prices a charge here.\n registerCommerceTaxProfile()\n registerPluginPermissions(COMMERCE_PERMISSIONS)\n // Merchant-settable register discount ceiling (AGL-2161). Registered on\n // BOTH surfaces, like the permissions above: the POS route reads it\n // server-side, and the settings card renders it from the same schema.\n registerPluginConfigSchema(COMMERCE_CONFIG_SCHEMA)\n // PDP/PLP template pages (AGL-418): /products/* + /collections/*.\n registerSitePageResolver(commerceSitePageResolver)\n // Seeds product grids on ordinary screens (AGL-659) — the resolver above\n // only covers /products/* and /collections/*, so /products itself, and any\n // designed page with a grid on it, needed this to render server-side.\n registerSitePageEnricher(commerceSitePageEnricher)\n registerPluginApiRoute('commerce/cart-checkout', cartCheckoutHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/cart', cartHandler)\n registerPluginApiRoute('commerce/catalog', catalogHandler)\n registerPluginApiRoute('commerce/checkout', checkoutHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/download', downloadHandler)\n registerPluginApiRoute('commerce/feed', feedHandler)\n registerPluginApiRoute('commerce/newsletter', newsletterHandler)\n registerPluginApiRoute('commerce/notify-restock', notifyRestockHandler)\n // GA-safe order projection for the storefront `purchase` (AGL-1641).\n registerPluginApiRoute('commerce/order-analytics', orderAnalyticsHandler)\n registerPluginApiRoute('commerce/product', productHandler)\n registerPluginApiRoute('commerce/related', relatedHandler)\n registerPluginApiRoute('commerce/reservation-availability', reservationAvailabilityHandler)\n registerPluginApiRoute('commerce/reserve', reserveHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/gate', gateHandler)\n registerPluginApiRoute('commerce/member-feed', memberFeedHandler)\n registerPluginApiRoute('commerce/stream', streamHandler)\n registerPluginApiRoute('commerce/subscription-portal', subscriptionPortalHandler)\n registerPluginApiRoute('commerce/reviews', reviewsHandler)\n registerPluginApiRoute('membership/account', membershipAccountHandler)\n registerPluginApiRoute('membership/content', membershipContentHandler)\n registerPluginApiRoute('membership/login', membershipLoginHandler)\n registerPluginApiRoute('membership/logout', membershipLogoutHandler)\n // Password recovery pair (AGL-552): request + complete.\n registerPluginApiRoute('membership/recover', membershipRecoverHandler)\n registerPluginApiRoute('membership/register', membershipRegisterHandler)\n registerPluginApiRoute('membership/reset', membershipResetHandler)\n registerPluginApiRoute('membership/wishlist', membershipWishlistHandler)\n}\n\n/**\n * Registers the commerce plugin's console-side API routes (AGL-396):\n * merchant/staff operations (Connect onboarding, refunds, draft & POS\n * orders, member posts), the supplier tracking callback, and the HTTP doors\n * for the abandoned-cart / restock passes.\n *\n * Those last two are NOT what schedules them — the `registerPluginJob` calls\n * above are (AGL-2227). This comment used to call them \"the scheduler-driven\n * jobs\", which is how they stayed dark for months: it asserted the wiring\n * instead of having it.\n */\nexport function registerCommerceConsoleApi(): void {\n // What a product looks like to a surface that is not this plugin's — a\n // campaign email that features one asks here rather than importing the model.\n registerProductCardReader()\n // …and why a file somebody is SELLING may not be made public (AGL-3080).\n // The media library asks before it hands an asset its permanent CDN URL\n // back; what a product is, and which of its fields hold paid media, is\n // this plugin's to know.\n registerCommerceMediaPublishGuard()\n // …and which memberships a site sells, for a security lockdown to pause\n // (AGL-3364). The lockdown route runs on this surface.\n registerCommerceRecurringCharges()\n // …and the tax rule, for the webhook that confirms what they charged.\n registerCommerceTaxProfile()\n // …and its sales on the operator's own sales tax return (AGL-3080): the\n // tax a storefront checkout collected under the platform's registrations.\n // The staff return awaits this surface before it asks, and refuses to be\n // filed without this source — commerce declares `taxReturnSource`.\n registerTaxReturnSource(commerceTaxReturnSource, { pluginId: BUNDLE_ID })\n // …and the take its orders earn the operator, on the staff revenue report\n // (AGL-3080) — commerce declares `revenueSource`, so the report names this\n // source as unread rather than counting it as zero if it is missing.\n registerRevenueSource(commerceRevenueSource, { pluginId: BUNDLE_ID })\n // Stripe webhook sections (AGL-418): orders/carts/drafts/reservations/\n // subscriptions ride the platform webhook via the hook registry.\n registerBillingWebhookHandler(commerceBillingWebhookHandler)\n // Cancel + stock release in one transaction (AGL-1808). Server-side because\n // the release depends on the transition rule, and a client write could not\n // re-ask it under the same lock that flips the status.\n registerPluginApiRoute('commerce/cancel-order', cancelOrderHandler)\n registerPluginApiRoute('commerce/connect', connectHandler)\n registerPluginApiRoute('commerce/draft-order', draftOrderHandler)\n // Fulfil + mark-delivered with the transition re-asked under the write\n // (AGL-1819) — the same stale-dialog hole cancel-order closes, minus the\n // stock release those two transitions never had.\n registerPluginApiRoute('commerce/fulfill-order', fulfillOrderHandler)\n // The same transaction, offered to the HOST APP as a capability (AGL-2461)\n // so `/v1` can record a shipment without importing this library — which\n // `eslint.config.mjs`'s `scope:app` boundary forbids, and which is why the\n // public REST API could take an order and never ship it.\n //\n // Registered HERE, in the consoleApi surface, beside the route it shares an\n // implementation with: `/v1` reaches it through the console's server plugin\n // loader, the same `ensureAll(['consoleApi'])` batch the plugin API\n // dispatcher runs. A registration at module scope would instead depend on\n // which entry point a process happened to import.\n //\n // NOTHING ABOUT AUTHORIZATION CROSSES THIS LINE. The capability is a\n // pre-authorized domain operation and `/v1` does its own gating (key scope,\n // plan entitlement, org-owns-host, plugin-enabled-for-site) before it calls.\n registerOrderFulfilmentService({\n pluginId: BUNDLE_ID,\n recordShipment: recordOrderShipment,\n })\n // Issue / void store credit (AGL-2226). Server-side because the host\n // catch-all in the Firestore rules would otherwise let a client write\n // its own `balanceCents`, which checkout applies as amount-off.\n registerPluginApiRoute('commerce/gift-cards', giftCardsHandler)\n // The merchant's own storefront sales tax, by who owes it (AGL-2440): the\n // same rows and classifier the operator's return reads, fenced to one site.\n registerPluginApiRoute('commerce/tax-summary', { web: taxSummaryHandler })\n // A smart collection's rules changed or it was deleted: re-stamp which\n // products it holds (AGL-3321), the membership the storefront queries.\n registerPluginApiRoute('commerce/collection-membership', collectionMembershipHandler)\n // A site member's password help (AGL-914) and removal (AGL-3308), from the\n // Users page drawer and the Inbox. Console-auth, not the visitor cookie the\n // storefront's `membership/*` routes take, so they are served where the\n // console posts: the console's own dispatcher loads this surface only.\n registerPluginApiRoute(\n 'membership/admin-password',\n membershipAdminPasswordHandler,\n )\n registerPluginApiRoute('membership/admin-remove', membershipAdminRemoveHandler)\n registerPluginApiRoute('commerce/member-post', memberPostHandler)\n registerPluginApiRoute('commerce/pos-order', posOrderHandler)\n registerPluginApiRoute('commerce/process-abandoned', processAbandonedHandler)\n registerPluginApiRoute('commerce/process-restock', processRestockHandler)\n registerPluginApiRoute('commerce/refund', refundHandler)\n registerPluginApiRoute('commerce/supplier-update', supplierUpdateHandler)\n // The store's sales as figure tables (AGL-2915), for the AI plugin's\n // insights to read by id rather than by reading orders. The console runs\n // insight jobs, so the console surface registers them.\n registerOrderFigureReaders(() => firebaseAdmin.app().firestore())\n}\n\n// Shared with the (still app-side) membership/account route.\nexport { mintDownloadToken } from './server/download'\n\n// Site-member session primitives, shared with the (still app-side)\n// membership/* routes until those migrate too (AGL-396).\nexport * from './server/membership'\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { commercePlanEntitlements } from './plan-entitlements'\n"],"names":["registerBillingWebhookHandler","registerOrderFulfilmentService","registerPluginApiRoute","registerPluginJob","registerSitePageEnricher","registerSitePageResolver","registerPluginPermissions","registerPluginConfigSchema","isEmailConfigured","firebaseAdmin","BUNDLE_ID","registerCommerceMediaPublishGuard","registerCommerceRecurringCharges","registerProductCardReader","registerCommerceTaxProfile","registerTaxReturnSource","commerceTaxReturnSource","taxSummaryHandler","registerRevenueSource","commerceRevenueSource","commerceBillingWebhookHandler","registerOrderFigureReaders","COMMERCE_PERMISSIONS","COMMERCE_CONFIG_SCHEMA","commerceSitePageEnricher","commerceSitePageResolver","cartCheckoutHandler","cartHandler","catalogHandler","checkoutHandler","downloadHandler","feedHandler","newsletterHandler","notifyRestockHandler","productHandler","relatedHandler","reservationAvailabilityHandler","gateHandler","memberFeedHandler","membershipAccountHandler","membershipAdminPasswordHandler","membershipAdminRemoveHandler","membershipContentHandler","membershipLoginHandler","membershipLogoutHandler","membershipRecoverHandler","membershipRegisterHandler","membershipResetHandler","membershipWishlistHandler","reserveHandler","streamHandler","subscriptionPortalHandler","reviewsHandler","connectHandler","cancelOrderHandler","collectionMembershipHandler","draftOrderHandler","fulfillOrderHandler","recordOrderShipment","giftCardsHandler","memberPostHandler","orderAnalyticsHandler","posOrderHandler","processAbandonedHandler","scanAbandonedCheckouts","processRestockHandler","scanRestockAlerts","scanStockDecrements","refundHandler","scanSupplierDeliveries","supplierUpdateHandler","RECOVERY_JOB_INTERVAL_MINUTES","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","sent","console","info","scan","missingLines","truncatedHosts","warn","hosts","reportedOrders","deadLettered","CARD_PAYMENT_DOOR","cardPayment","registerCommerceApi","registerCommerceConsoleApi","recordShipment","web","app","firestore","mintDownloadToken"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/server.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/**\n * Server half of the commerce plugin (AGL-396): the site-facing storefront\n * API handlers, registered with the plugin API registry and served by the\n * tenant dispatcher at their unchanged `/api/commerce/*` URLs. This module\n * pulls in firebase-admin + Stripe, so it is NOT re-exported from the client\n * barrel — apps import `@aglyn/plugins-commerce/server` only from their\n * (server-only) API dispatcher registration.\n */\n\nimport {\n registerBillingWebhookHandler,\n registerPluginApiRoute,\n registerPluginJob,\n registerSitePageEnricher,\n registerSitePageResolver,\n registerPluginPermissions,\n registerPluginConfigSchema,\n} from '@aglyn/aglyn/server'\nimport { isEmailConfigured } from '@aglyn/shared-util-email'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCommerceMediaPublishGuard } from './server/media-publish-guard'\nimport { registerCommerceRecurringCharges } from './server/recurring-charges'\nimport { registerProductCardReader } from './server/product-card'\nimport { registerCommerceTaxProfile } from './server/tax-profile'\nimport { registerTaxReturnSource } from '@aglyn/aglyn/plugin-manager/plugin-tax-return-sources'\nimport { commerceTaxReturnSource } from './server/tax-return-source'\nimport { taxSummaryHandler } from './server/tax-summary'\nimport { registerRevenueSource } from '@aglyn/aglyn/plugin-manager/plugin-revenue-sources'\nimport { commerceRevenueSource } from './server/revenue-source'\nimport { commerceBillingWebhookHandler } from './server/billing-webhook'\nimport { registerOrderFigureReaders } from './server/order-figures'\nimport { COMMERCE_PERMISSIONS } from './model/plugin-permissions'\nimport { COMMERCE_CONFIG_SCHEMA } from './plugin-config'\nimport { commerceSitePageEnricher } from './server/site-page-enricher'\nimport { commerceSitePageResolver } from './server/site-page-resolver'\nimport { cartCheckoutHandler } from './server/cart-checkout'\nimport { cartHandler } from './server/cart'\nimport { catalogHandler } from './server/catalog'\nimport { checkoutHandler } from './server/checkout'\nimport { downloadHandler } from './server/download'\nimport { feedHandler } from './server/feed'\nimport { newsletterHandler } from './server/newsletter'\nimport { notifyRestockHandler } from './server/notify-restock'\nimport { productHandler } from './server/product'\nimport { relatedHandler } from './server/related'\nimport { reservationAvailabilityHandler } from './server/reservation-availability'\nimport { gateHandler } from './server/gate'\nimport { memberFeedHandler } from './server/member-feed'\nimport { membershipAccountHandler } from './server/membership-account'\nimport { membershipAdminPasswordHandler } from './server/membership-admin-password'\nimport { membershipAdminRemoveHandler } from './server/membership-admin-remove'\nimport { membershipContentHandler } from './server/membership-content'\nimport { membershipLoginHandler } from './server/membership-login'\nimport { membershipLogoutHandler } from './server/membership-logout'\nimport { membershipRecoverHandler } from './server/membership-recover'\nimport { membershipRegisterHandler } from './server/membership-register'\nimport { membershipResetHandler } from './server/membership-reset'\nimport { membershipWishlistHandler } from './server/membership-wishlist'\nimport { reserveHandler } from './server/reserve'\nimport { streamHandler } from './server/stream'\nimport { subscriptionPortalHandler } from './server/subscription-portal'\nimport { reviewsHandler } from './server/reviews'\nimport { connectHandler } from './server/connect'\nimport { cancelOrderHandler } from './server/cancel-order'\nimport { collectionMembershipHandler } from './server/collection-membership'\nimport { draftOrderHandler } from './server/draft-order'\nimport { fulfillOrderHandler } from './server/fulfill-order'\nimport { giftCardsHandler } from './server/gift-cards'\nimport { memberPostHandler } from './server/member-post'\nimport { orderAnalyticsHandler } from './server/order-analytics'\nimport { posOrderHandler } from './server/pos-order'\nimport {\n processAbandonedHandler,\n scanAbandonedCheckouts,\n} from './server/process-abandoned'\nimport { processRestockHandler, scanRestockAlerts } from './server/process-restock'\nimport { scanStockDecrements } from './server/reconcile-stock'\nimport { refundHandler } from './server/refund'\nimport { scanSupplierDeliveries } from './server/supplier-outbox'\nimport { supplierUpdateHandler } from './server/supplier-update'\n\n/**\n * The two commerce beats (AGL-2227).\n *\n * `commerce/process-abandoned` and `commerce/process-restock` have existed\n * since AGL-323/326 as `x-cron-secret` HTTP doors, and `registerCommerceConsoleApi`\n * below has called them \"the scheduler-driven jobs\" in a comment that whole\n * time. **Nothing scheduled them.** Not `scheduled-crons.yml` (its table names\n * 11 paths; neither of these), not `vercel.json` (no `crons` key at all), not\n * `registerPluginJob` — the only two registrations in the repo were scheduled\n * publishing and the bookings hold-expiry.\n *\n * The cost was a Pro-tier entitlement (`abandonedCart`) that had never sent an\n * email, and a storefront \"notify me when it's back\" form writing into a queue\n * with no drain. AGL-1793 had already added the collection-group indexes these\n * two scans need, which is the clearest evidence they were always meant to run.\n *\n * Module scope, like the bookings job beside it: the runner route reaches jobs\n * through `ensureAll(['tenantApi'])`, and a registration inside a `register*`\n * function would depend on which entry point happened to be loaded.\n *\n * 15 minutes, not 1: `process-abandoned` will not remind a checkout younger\n * than an hour and `process-restock` only mails alerts whose product already\n * has stock, so a tighter beat buys nothing and costs two collection-group\n * scans a minute. Both are bounded (200 docs) and idempotent — each pass\n * stamps what it sent — so an overlapping or repeated beat cannot double-send.\n */\nconst RECOVERY_JOB_INTERVAL_MINUTES = 15\n\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'abandoned-checkout-recovery',\n intervalMinutes: RECOVERY_JOB_INTERVAL_MINUTES,\n description:\n 'Email one recovery reminder per stalled checkout (abandonedCart plans).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n // Quietly, not as an error: email is optional per deployment, and a beat\n // that logs every minute on a self-host without Resend buries everything\n // else in the log.\n if (!isEmailConfigured()) return\n const { sent } = await scanAbandonedCheckouts(gate)\n if (sent) console.info(`commerce: sent ${sent} abandoned-cart reminders`)\n },\n})\n\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'back-in-stock-alerts',\n intervalMinutes: RECOVERY_JOB_INTERVAL_MINUTES,\n description: 'Email shoppers whose requested product is in stock again.',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n if (!isEmailConfigured()) return\n const { sent } = await scanRestockAlerts(gate)\n if (sent) console.info(`commerce: sent ${sent} back-in-stock alerts`)\n },\n})\n\n/**\n * The missing-decrement detector (AGL-2358).\n *\n * HOURLY, not on the 15-minute recovery beat: it is a detector for a rare\n * process death, not a queue drain, and nothing it finds gets less true for\n * waiting an hour. The cost of the beat is what sets the interval — one\n * collection-group read of the platform's most recent orders, plus two\n * queries per host that has any — and an hour keeps that off the same tick as\n * the two scans above.\n *\n * NOT gated on `isEmailConfigured()`, unlike its two neighbours. Those send\n * mail; this writes a console notification and a log line, both of which work\n * on a self-host with no mail provider at all — and a stock count that is\n * silently wrong is exactly what a self-hoster least wants suppressed by a\n * setting about email.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'stock-decrement-reconciliation',\n intervalMinutes: 60,\n description:\n 'Report paid orders whose stock decrement never landed (AGL-2358).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const scan = await scanStockDecrements(gate)\n if (scan.missingLines || scan.truncatedHosts) {\n console.warn(\n `commerce: ${scan.missingLines} order lines across ${scan.hosts} ` +\n `sites have no sale ledger row (${scan.reportedOrders} newly ` +\n `reported, ${scan.truncatedHosts} sites' ledger window truncated)`,\n )\n }\n },\n})\n\n/**\n * The dropship supplier outbox drain (AGL-2473).\n *\n * EVERY MINUTE, unlike the three above, and the interval is the point rather\n * than an oversight. This is the only one of the four that a BUYER is waiting\n * on: the row it drains is a paid order that has not been routed to whoever\n * ships it, and every minute it sits is a minute the parcel is not moving. The\n * first backoff step is also 60s, so a supplier that blipped is retried on the\n * next tick.\n *\n * The cost that set fifteen minutes for the recovery pair does not apply. Those\n * are collection-GROUP scans across every site on the platform; this is one\n * ordinary equality query against a top-level collection that is EMPTY in the\n * ordinary case — an empty result bills a single read, so the whole beat is\n * ~1,440 reads a day whether or not anyone is dropshipping.\n *\n * Not gated on `isEmailConfigured()`. The supplier notification is an HTTP POST\n * to the merchant's own supplier, and a self-host with no mail provider still\n * has dropship orders to route.\n */\nregisterPluginJob({\n pluginId: BUNDLE_ID,\n name: 'supplier-webhook-delivery',\n intervalMinutes: 1,\n description:\n 'Deliver queued dropship supplier notifications, with backoff and a ' +\n 'dead letter that tells the merchant (AGL-2473).',\n lockdown: { scope: 'per-host' },\n handler: async (gate) => {\n const scan = await scanSupplierDeliveries(gate)\n if (scan.deadLettered) {\n console.warn(\n `commerce: ${scan.deadLettered} dropship orders could not be routed ` +\n `to their supplier and have been reported on the order`,\n )\n }\n },\n})\n\n/**\n * A visitor's payment door: it opens a Stripe Checkout Session for whoever\n * calls it, so the dispatcher holds it to the card-testing counters\n * (AGL-3363). Declared here, beside the registration, so the door cannot\n * exist without it.\n */\nconst CARD_PAYMENT_DOOR = { cardPayment: true } as const\n\n/** Registers the commerce plugin's storefront API routes. */\nexport function registerCommerceApi(): void {\n registerProductCardReader()\n // The merchant's tax rule, for every plugin that prices a charge here.\n registerCommerceTaxProfile()\n registerPluginPermissions(COMMERCE_PERMISSIONS)\n // Merchant-settable register discount ceiling (AGL-2161). Registered on\n // BOTH surfaces, like the permissions above: the POS route reads it\n // server-side, and the settings card renders it from the same schema.\n registerPluginConfigSchema(COMMERCE_CONFIG_SCHEMA)\n // PDP/PLP template pages (AGL-418): /products/* + /collections/*.\n registerSitePageResolver(commerceSitePageResolver)\n // Seeds product grids on ordinary screens (AGL-659) — the resolver above\n // only covers /products/* and /collections/*, so /products itself, and any\n // designed page with a grid on it, needed this to render server-side.\n registerSitePageEnricher(commerceSitePageEnricher)\n registerPluginApiRoute('commerce/cart-checkout', cartCheckoutHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/cart', cartHandler)\n registerPluginApiRoute('commerce/catalog', catalogHandler)\n registerPluginApiRoute('commerce/checkout', checkoutHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/download', downloadHandler)\n registerPluginApiRoute('commerce/feed', feedHandler)\n registerPluginApiRoute('commerce/newsletter', newsletterHandler)\n registerPluginApiRoute('commerce/notify-restock', notifyRestockHandler)\n // GA-safe order projection for the storefront `purchase` (AGL-1641).\n registerPluginApiRoute('commerce/order-analytics', orderAnalyticsHandler)\n registerPluginApiRoute('commerce/product', productHandler)\n registerPluginApiRoute('commerce/related', relatedHandler)\n registerPluginApiRoute('commerce/reservation-availability', reservationAvailabilityHandler)\n registerPluginApiRoute('commerce/reserve', reserveHandler, CARD_PAYMENT_DOOR)\n registerPluginApiRoute('commerce/gate', gateHandler)\n registerPluginApiRoute('commerce/member-feed', memberFeedHandler)\n registerPluginApiRoute('commerce/stream', streamHandler)\n registerPluginApiRoute('commerce/subscription-portal', subscriptionPortalHandler)\n registerPluginApiRoute('commerce/reviews', reviewsHandler)\n registerPluginApiRoute('membership/account', membershipAccountHandler)\n registerPluginApiRoute('membership/content', membershipContentHandler)\n registerPluginApiRoute('membership/login', membershipLoginHandler)\n registerPluginApiRoute('membership/logout', membershipLogoutHandler)\n // Password recovery pair (AGL-552): request + complete.\n registerPluginApiRoute('membership/recover', membershipRecoverHandler)\n registerPluginApiRoute('membership/register', membershipRegisterHandler)\n registerPluginApiRoute('membership/reset', membershipResetHandler)\n registerPluginApiRoute('membership/wishlist', membershipWishlistHandler)\n}\n\n/**\n * Registers the commerce plugin's console-side API routes (AGL-396):\n * merchant/staff operations (Connect onboarding, refunds, draft & POS\n * orders, member posts), the supplier tracking callback, and the HTTP doors\n * for the abandoned-cart / restock passes.\n *\n * Those last two are NOT what schedules them — the `registerPluginJob` calls\n * above are (AGL-2227). This comment used to call them \"the scheduler-driven\n * jobs\", which is how they stayed dark for months: it asserted the wiring\n * instead of having it.\n */\nexport function registerCommerceConsoleApi(): void {\n // What a product looks like to a surface that is not this plugin's — a\n // campaign email that features one asks here rather than importing the model.\n registerProductCardReader()\n // …and why a file somebody is SELLING may not be made public (AGL-3080).\n // The media library asks before it hands an asset its permanent CDN URL\n // back; what a product is, and which of its fields hold paid media, is\n // this plugin's to know.\n registerCommerceMediaPublishGuard()\n // …and which memberships a site sells, for a security lockdown to pause\n // (AGL-3364). The lockdown route runs on this surface.\n registerCommerceRecurringCharges()\n // …and the tax rule, for the webhook that confirms what they charged.\n registerCommerceTaxProfile()\n // …and its sales on the operator's own sales tax return (AGL-3080): the\n // tax a storefront checkout collected under the platform's registrations.\n // The staff return awaits this surface before it asks, and refuses to be\n // filed without this source — commerce declares `taxReturnSource`.\n registerTaxReturnSource(commerceTaxReturnSource, { pluginId: BUNDLE_ID })\n // …and the take its orders earn the operator, on the staff revenue report\n // (AGL-3080) — commerce declares `revenueSource`, so the report names this\n // source as unread rather than counting it as zero if it is missing.\n registerRevenueSource(commerceRevenueSource, { pluginId: BUNDLE_ID })\n // Stripe webhook sections (AGL-418): orders/carts/drafts/reservations/\n // subscriptions ride the platform webhook via the hook registry.\n registerBillingWebhookHandler(commerceBillingWebhookHandler)\n // Cancel + stock release in one transaction (AGL-1808). Server-side because\n // the release depends on the transition rule, and a client write could not\n // re-ask it under the same lock that flips the status.\n registerPluginApiRoute('commerce/cancel-order', cancelOrderHandler)\n registerPluginApiRoute('commerce/connect', connectHandler)\n registerPluginApiRoute('commerce/draft-order', draftOrderHandler)\n // Fulfil + mark-delivered with the transition re-asked under the write\n // (AGL-1819) — the same stale-dialog hole cancel-order closes, minus the\n // stock release those two transitions never had. The customer REST API's\n // `PATCH /v1/sites/{id}/orders/{id}` records a shipment through the same\n // transaction (`server/api-v1/orders-and-products.ts`).\n registerPluginApiRoute('commerce/fulfill-order', fulfillOrderHandler)\n // Issue / void store credit (AGL-2226). Server-side because the host\n // catch-all in the Firestore rules would otherwise let a client write\n // its own `balanceCents`, which checkout applies as amount-off.\n registerPluginApiRoute('commerce/gift-cards', giftCardsHandler)\n // The merchant's own storefront sales tax, by who owes it (AGL-2440): the\n // same rows and classifier the operator's return reads, fenced to one site.\n registerPluginApiRoute('commerce/tax-summary', { web: taxSummaryHandler })\n // A smart collection's rules changed or it was deleted: re-stamp which\n // products it holds (AGL-3321), the membership the storefront queries.\n registerPluginApiRoute('commerce/collection-membership', collectionMembershipHandler)\n // A site member's password help (AGL-914) and removal (AGL-3308), from the\n // Users page drawer and the Inbox. Console-auth, not the visitor cookie the\n // storefront's `membership/*` routes take, so they are served where the\n // console posts: the console's own dispatcher loads this surface only.\n registerPluginApiRoute(\n 'membership/admin-password',\n membershipAdminPasswordHandler,\n )\n registerPluginApiRoute('membership/admin-remove', membershipAdminRemoveHandler)\n registerPluginApiRoute('commerce/member-post', memberPostHandler)\n registerPluginApiRoute('commerce/pos-order', posOrderHandler)\n registerPluginApiRoute('commerce/process-abandoned', processAbandonedHandler)\n registerPluginApiRoute('commerce/process-restock', processRestockHandler)\n registerPluginApiRoute('commerce/refund', refundHandler)\n registerPluginApiRoute('commerce/supplier-update', supplierUpdateHandler)\n // The store's sales as figure tables (AGL-2915), for the AI plugin's\n // insights to read by id rather than by reading orders. The console runs\n // insight jobs, so the console surface registers them.\n registerOrderFigureReaders(() => firebaseAdmin.app().firestore())\n}\n\n// Shared with the (still app-side) membership/account route.\nexport { mintDownloadToken } from './server/download'\n\n// Site-member session primitives, shared with the (still app-side)\n// membership/* routes until those migrate too (AGL-396).\nexport * from './server/membership'\n\n// Type-only (AGL-3080): the plugin's entitlement keys, declared by module\n// augmentation, for every program that loads this entry point.\nexport type { commercePlanEntitlements } from './plan-entitlements'\n"],"names":["registerBillingWebhookHandler","registerPluginApiRoute","registerPluginJob","registerSitePageEnricher","registerSitePageResolver","registerPluginPermissions","registerPluginConfigSchema","isEmailConfigured","firebaseAdmin","BUNDLE_ID","registerCommerceMediaPublishGuard","registerCommerceRecurringCharges","registerProductCardReader","registerCommerceTaxProfile","registerTaxReturnSource","commerceTaxReturnSource","taxSummaryHandler","registerRevenueSource","commerceRevenueSource","commerceBillingWebhookHandler","registerOrderFigureReaders","COMMERCE_PERMISSIONS","COMMERCE_CONFIG_SCHEMA","commerceSitePageEnricher","commerceSitePageResolver","cartCheckoutHandler","cartHandler","catalogHandler","checkoutHandler","downloadHandler","feedHandler","newsletterHandler","notifyRestockHandler","productHandler","relatedHandler","reservationAvailabilityHandler","gateHandler","memberFeedHandler","membershipAccountHandler","membershipAdminPasswordHandler","membershipAdminRemoveHandler","membershipContentHandler","membershipLoginHandler","membershipLogoutHandler","membershipRecoverHandler","membershipRegisterHandler","membershipResetHandler","membershipWishlistHandler","reserveHandler","streamHandler","subscriptionPortalHandler","reviewsHandler","connectHandler","cancelOrderHandler","collectionMembershipHandler","draftOrderHandler","fulfillOrderHandler","giftCardsHandler","memberPostHandler","orderAnalyticsHandler","posOrderHandler","processAbandonedHandler","scanAbandonedCheckouts","processRestockHandler","scanRestockAlerts","scanStockDecrements","refundHandler","scanSupplierDeliveries","supplierUpdateHandler","RECOVERY_JOB_INTERVAL_MINUTES","pluginId","name","intervalMinutes","description","lockdown","scope","handler","gate","sent","console","info","scan","missingLines","truncatedHosts","warn","hosts","reportedOrders","deadLettered","CARD_PAYMENT_DOOR","cardPayment","registerCommerceApi","registerCommerceConsoleApi","web","app","firestore","mintDownloadToken"],"mappings":"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"}
@@ -0,0 +1,43 @@
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 PluginPersonRefundRequest } from '@aglyn/aglyn/plugin-manager/plugin-person-records';
18
+ /**
19
+ * A plugin that keeps people, standing in for the one that does (AGL-3080).
20
+ *
21
+ * Commerce reports money it handed back — a refund, a dispute lost — through
22
+ * `plugin-person-records` and imports no record system, so its specs stand an
23
+ * owner up the way the loader would. One plugin may not import another, which
24
+ * is why this is here rather than borrowed from the CRM.
25
+ *
26
+ * What these specs certify is that the seller REPORTS the right reversal:
27
+ * once, with the amount this attempt reversed, and whether it closed the
28
+ * sale. What the report does to the customer's record — the gross left alone,
29
+ * the timeline line, the refusal to conjure a contact — is held by the CRM's
30
+ * own `person-refund.spec.ts`.
31
+ *
32
+ * Returns the reports, in the order the seller made them.
33
+ */
34
+ export declare function standInPersonRecords(): PluginPersonRefundRequest[];
35
+ /**
36
+ * The customer's ledger as the reports add up: what was handed back and how
37
+ * many sales were closed by it — `undefined` for none, as an untouched record
38
+ * reads — so a spec reads one figure rather than summing reports itself.
39
+ */
40
+ export declare function reportedRefundLedger(refunds: readonly PluginPersonRefundRequest[]): {
41
+ refundedCents: number | undefined;
42
+ refundedOrdersCount: number | undefined;
43
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { registerPluginPersonRecords } from "@aglyn/aglyn/plugin-manager/plugin-person-records";
17
+ /**
18
+ * A plugin that keeps people, standing in for the one that does (AGL-3080).
19
+ *
20
+ * Commerce reports money it handed back — a refund, a dispute lost — through
21
+ * `plugin-person-records` and imports no record system, so its specs stand an
22
+ * owner up the way the loader would. One plugin may not import another, which
23
+ * is why this is here rather than borrowed from the CRM.
24
+ *
25
+ * What these specs certify is that the seller REPORTS the right reversal:
26
+ * once, with the amount this attempt reversed, and whether it closed the
27
+ * sale. What the report does to the customer's record — the gross left alone,
28
+ * the timeline line, the refusal to conjure a contact — is held by the CRM's
29
+ * own `person-refund.spec.ts`.
30
+ *
31
+ * Returns the reports, in the order the seller made them.
32
+ */ export function standInPersonRecords() {
33
+ const refunds = [];
34
+ registerPluginPersonRecords({
35
+ async find () {
36
+ return null;
37
+ },
38
+ async read (request) {
39
+ return request.records.map(()=>null);
40
+ },
41
+ async recordRefund (request) {
42
+ refunds.push(request);
43
+ return 'recorded';
44
+ }
45
+ }, {
46
+ pluginId: 'record-system'
47
+ });
48
+ return refunds;
49
+ }
50
+ /**
51
+ * The customer's ledger as the reports add up: what was handed back and how
52
+ * many sales were closed by it — `undefined` for none, as an untouched record
53
+ * reads — so a spec reads one figure rather than summing reports itself.
54
+ */ export function reportedRefundLedger(refunds) {
55
+ const refundedCents = refunds.reduce((sum, refund)=>sum + refund.amountCents, 0);
56
+ const closed = refunds.filter((refund)=>refund.closedTheSale).length;
57
+ return {
58
+ refundedCents: refunds.length ? refundedCents : undefined,
59
+ refundedOrdersCount: closed ? closed : undefined
60
+ };
61
+ }
62
+
63
+ //# sourceMappingURL=stand-in-person-records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/testing/stand-in-person-records.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 registerPluginPersonRecords,\n type PluginPersonRefundRequest,\n} from '@aglyn/aglyn/plugin-manager/plugin-person-records'\n\n/**\n * A plugin that keeps people, standing in for the one that does (AGL-3080).\n *\n * Commerce reports money it handed back — a refund, a dispute lost — through\n * `plugin-person-records` and imports no record system, so its specs stand an\n * owner up the way the loader would. One plugin may not import another, which\n * is why this is here rather than borrowed from the CRM.\n *\n * What these specs certify is that the seller REPORTS the right reversal:\n * once, with the amount this attempt reversed, and whether it closed the\n * sale. What the report does to the customer's record — the gross left alone,\n * the timeline line, the refusal to conjure a contact — is held by the CRM's\n * own `person-refund.spec.ts`.\n *\n * Returns the reports, in the order the seller made them.\n */\nexport function standInPersonRecords(): PluginPersonRefundRequest[] {\n const refunds: PluginPersonRefundRequest[] = []\n registerPluginPersonRecords(\n {\n async find() {\n return null\n },\n async read(request) {\n return request.records.map(() => null)\n },\n async recordRefund(request) {\n refunds.push(request)\n return 'recorded'\n },\n },\n { pluginId: 'record-system' },\n )\n return refunds\n}\n\n/**\n * The customer's ledger as the reports add up: what was handed back and how\n * many sales were closed by it — `undefined` for none, as an untouched record\n * reads — so a spec reads one figure rather than summing reports itself.\n */\nexport function reportedRefundLedger(refunds: readonly PluginPersonRefundRequest[]): {\n refundedCents: number | undefined\n refundedOrdersCount: number | undefined\n} {\n const refundedCents = refunds.reduce((sum, refund) => sum + refund.amountCents, 0)\n const closed = refunds.filter((refund) => refund.closedTheSale).length\n return {\n refundedCents: refunds.length ? refundedCents : undefined,\n refundedOrdersCount: closed ? closed : undefined,\n }\n}\n"],"names":["registerPluginPersonRecords","standInPersonRecords","refunds","find","read","request","records","map","recordRefund","push","pluginId","reportedRefundLedger","refundedCents","reduce","sum","refund","amountCents","closed","filter","closedTheSale","length","undefined","refundedOrdersCount"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,2BAA2B,QAEtB,oDAAmD;AAE1D;;;;;;;;;;;;;;;CAeC,GACD,OAAO,SAASC;IACd,MAAMC,UAAuC,EAAE;IAC/CF,4BACE;QACE,MAAMG;YACJ,OAAO;QACT;QACA,MAAMC,MAAKC,OAAO;YAChB,OAAOA,QAAQC,OAAO,CAACC,GAAG,CAAC,IAAM;QACnC;QACA,MAAMC,cAAaH,OAAO;YACxBH,QAAQO,IAAI,CAACJ;YACb,OAAO;QACT;IACF,GACA;QAAEK,UAAU;IAAgB;IAE9B,OAAOR;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASS,qBAAqBT,OAA6C;IAIhF,MAAMU,gBAAgBV,QAAQW,MAAM,CAAC,CAACC,KAAKC,SAAWD,MAAMC,OAAOC,WAAW,EAAE;IAChF,MAAMC,SAASf,QAAQgB,MAAM,CAAC,CAACH,SAAWA,OAAOI,aAAa,EAAEC,MAAM;IACtE,OAAO;QACLR,eAAeV,QAAQkB,MAAM,GAAGR,gBAAgBS;QAChDC,qBAAqBL,SAASA,SAASI;IACzC;AACF"}
@@ -0,0 +1,22 @@
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 { TransferWizardStepProps } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources';
18
+ export declare function GiftCardConfirmStep(props: TransferWizardStepProps): import("react").JSX.Element;
19
+ export declare namespace GiftCardConfirmStep {
20
+ var displayName: string;
21
+ }
22
+ export default GiftCardConfirmStep;
@@ -0,0 +1,259 @@
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
+ */ 'use client';
17
+ import { _ as _extends } from "@swc/helpers/_/_extends";
18
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
19
+ import { ScrollTable } from "@aglyn/shared-ui-jsx/components/scroll-table.component";
20
+ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
21
+ import { useUser } from "@aglyn/tenant-feature-instance";
22
+ import { Alert, AlertTitle, Checkbox, Chip, FormControlLabel, Stack, TableBody, TableCell, TableHead, TablePagination, TableRow, TextField, Typography } from "@mui/material";
23
+ import { useEffect, useMemo, useState } from "react";
24
+ import { giftCardMoney, typedDollarsToCents } from "./records-transfer.js";
25
+ const PAGE = 25;
26
+ /** The plan route's largest page. */ const ROWS_PAGE = 200;
27
+ function readAnswer(value) {
28
+ var _answer_typed;
29
+ const answer = value && typeof value === 'object' ? value : {};
30
+ return {
31
+ typed: String((_answer_typed = answer.typed) != null ? _answer_typed : ''),
32
+ email: answer.email === true
33
+ };
34
+ }
35
+ export function GiftCardConfirmStep(props) {
36
+ const { orgId, jobId, value, setValue, setComplete } = props;
37
+ const { data: user } = useUser();
38
+ const answer = readAnswer(value);
39
+ const [cards, setCards] = useState(null);
40
+ const [error, setError] = useState(null);
41
+ const [page, setPage] = useState(0);
42
+ // Every card the dry run issues, page by page from the stored plan.
43
+ useEffect(()=>{
44
+ if (!user || !orgId || !jobId) return undefined;
45
+ let live = true;
46
+ void (async ()=>{
47
+ try {
48
+ const found = [];
49
+ let offset = 0;
50
+ while(offset !== null){
51
+ var _payload_error;
52
+ const response = await authorizedFetch(user, '/api/transfer/plan', {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json'
56
+ },
57
+ body: JSON.stringify({
58
+ orgId,
59
+ jobId,
60
+ action: 'rows',
61
+ offset,
62
+ limit: ROWS_PAGE,
63
+ verdicts: [
64
+ 'create'
65
+ ]
66
+ })
67
+ });
68
+ const payload = await response.json().catch(()=>({}));
69
+ if (!response.ok || !payload.rows) throw new Error((_payload_error = payload.error) != null ? _payload_error : 'The cards could not be read.');
70
+ for (const row of payload.rows.rows){
71
+ const card = row.giftCard;
72
+ if (card && !card.problem) found.push(card);
73
+ }
74
+ offset = payload.rows.next;
75
+ }
76
+ if (live) setCards(found);
77
+ } catch (reason) {
78
+ if (live) setError(reason instanceof Error ? reason.message : 'The cards could not be read.');
79
+ }
80
+ })();
81
+ return ()=>{
82
+ live = false;
83
+ };
84
+ }, [
85
+ user,
86
+ orgId,
87
+ jobId
88
+ ]);
89
+ const totalCents = useMemo(()=>(cards != null ? cards : []).reduce((sum, card)=>sum + card.amountCents, 0), [
90
+ cards
91
+ ]);
92
+ const typedCents = typedDollarsToCents(answer.typed);
93
+ const matches = cards !== null && typedCents === totalCents;
94
+ useEffect(()=>{
95
+ // Nothing to issue needs no total: the review says why each row is held.
96
+ setComplete(cards !== null && (cards.length === 0 || matches));
97
+ }, [
98
+ cards,
99
+ matches,
100
+ setComplete
101
+ ]);
102
+ const save = (next)=>{
103
+ const merged = _extends({}, answer, next);
104
+ const cents = typedDollarsToCents(merged.typed);
105
+ setValue(_extends({
106
+ typed: merged.typed,
107
+ email: merged.email
108
+ }, cents !== null && cents > 0 ? {
109
+ totalCents: cents
110
+ } : {}));
111
+ };
112
+ if (error) return /*#__PURE__*/ _jsx(Alert, {
113
+ severity: "error",
114
+ children: error
115
+ });
116
+ if (cards === null) {
117
+ return /*#__PURE__*/ _jsx(Typography, {
118
+ variant: "body2",
119
+ color: "text.secondary",
120
+ children: 'Reading the cards this import issues…'
121
+ });
122
+ }
123
+ if (!cards.length) {
124
+ return /*#__PURE__*/ _jsx(Alert, {
125
+ severity: "info",
126
+ children: 'No card in this file can be issued. The review lists every row with the reason.'
127
+ });
128
+ }
129
+ const madeCodes = cards.filter((card)=>card.newCode || card.madeCode).length;
130
+ const shown = cards.slice(page * PAGE, page * PAGE + PAGE);
131
+ return /*#__PURE__*/ _jsxs(Stack, {
132
+ spacing: 2,
133
+ children: [
134
+ /*#__PURE__*/ _jsxs(Alert, {
135
+ severity: "warning",
136
+ children: [
137
+ /*#__PURE__*/ _jsx(AlertTitle, {
138
+ children: `${cards.length.toLocaleString('en-US')} gift card${cards.length === 1 ? '' : 's'} worth ${giftCardMoney(totalCents)}`
139
+ }),
140
+ 'Each card is spendable money the moment it is issued. Check every code and value, then type the total ' + 'to issue them.'
141
+ ]
142
+ }),
143
+ /*#__PURE__*/ _jsxs(ScrollTable, {
144
+ size: "small",
145
+ "aria-label": "Gift cards this import issues",
146
+ children: [
147
+ /*#__PURE__*/ _jsx(TableHead, {
148
+ children: /*#__PURE__*/ _jsxs(TableRow, {
149
+ children: [
150
+ /*#__PURE__*/ _jsx(TableCell, {
151
+ children: "Code"
152
+ }),
153
+ /*#__PURE__*/ _jsx(TableCell, {
154
+ align: "right",
155
+ children: "Value"
156
+ }),
157
+ /*#__PURE__*/ _jsx(TableCell, {
158
+ children: "Recipient"
159
+ }),
160
+ /*#__PURE__*/ _jsx(TableCell, {
161
+ children: "Note"
162
+ })
163
+ ]
164
+ })
165
+ }),
166
+ /*#__PURE__*/ _jsx(TableBody, {
167
+ children: shown.map((card)=>{
168
+ var _card_recipientEmail, _card_note;
169
+ return /*#__PURE__*/ _jsxs(TableRow, {
170
+ children: [
171
+ /*#__PURE__*/ _jsx(TableCell, {
172
+ children: /*#__PURE__*/ _jsxs(Stack, {
173
+ direction: "row",
174
+ spacing: 1,
175
+ sx: {
176
+ alignItems: 'center'
177
+ },
178
+ children: [
179
+ /*#__PURE__*/ _jsx(Typography, {
180
+ variant: "body2",
181
+ sx: {
182
+ fontFamily: 'monospace'
183
+ },
184
+ children: card.code
185
+ }),
186
+ card.newCode ? /*#__PURE__*/ _jsx(Chip, {
187
+ size: "small",
188
+ label: "New code: the file's was taken"
189
+ }) : null,
190
+ card.madeCode ? /*#__PURE__*/ _jsx(Chip, {
191
+ size: "small",
192
+ label: "New code"
193
+ }) : null
194
+ ]
195
+ })
196
+ }),
197
+ /*#__PURE__*/ _jsx(TableCell, {
198
+ align: "right",
199
+ children: giftCardMoney(card.amountCents)
200
+ }),
201
+ /*#__PURE__*/ _jsx(TableCell, {
202
+ children: (_card_recipientEmail = card.recipientEmail) != null ? _card_recipientEmail : '—'
203
+ }),
204
+ /*#__PURE__*/ _jsx(TableCell, {
205
+ children: (_card_note = card.note) != null ? _card_note : '—'
206
+ })
207
+ ]
208
+ }, card.code);
209
+ })
210
+ })
211
+ ]
212
+ }),
213
+ cards.length > PAGE ? /*#__PURE__*/ _jsx(TablePagination, {
214
+ component: "div",
215
+ count: cards.length,
216
+ page: page,
217
+ onPageChange: (_event, next)=>setPage(next),
218
+ rowsPerPage: PAGE,
219
+ rowsPerPageOptions: [
220
+ PAGE
221
+ ]
222
+ }) : null,
223
+ /*#__PURE__*/ _jsx(TextField, {
224
+ label: "Type the total value to issue these cards",
225
+ value: answer.typed,
226
+ onChange: (event)=>save({
227
+ typed: event.target.value
228
+ }),
229
+ error: Boolean(answer.typed) && !matches,
230
+ helperText: answer.typed && !matches ? `That is not the total. These cards add up to ${giftCardMoney(totalCents)}.` : `Type ${giftCardMoney(totalCents)}.`,
231
+ autoComplete: "off",
232
+ slotProps: {
233
+ htmlInput: {
234
+ inputMode: 'decimal'
235
+ }
236
+ }
237
+ }),
238
+ /*#__PURE__*/ _jsx(FormControlLabel, {
239
+ control: /*#__PURE__*/ _jsx(Checkbox, {
240
+ checked: answer.email,
241
+ onChange: (event)=>save({
242
+ email: event.target.checked
243
+ })
244
+ }),
245
+ label: /*#__PURE__*/ _jsxs(Typography, {
246
+ variant: "body2",
247
+ children: [
248
+ 'Email each recipient their code as the card is issued.',
249
+ madeCodes ? ` ${madeCodes.toLocaleString('en-US')} card${madeCodes === 1 ? ' has' : 's have'} a new code nobody holds yet: email them, or share the codes yourself.` : ' Leave this off when the shoppers already hold these codes.'
250
+ ]
251
+ })
252
+ })
253
+ ]
254
+ });
255
+ }
256
+ GiftCardConfirmStep.displayName = 'GiftCardConfirmStep';
257
+ export default GiftCardConfirmStep;
258
+
259
+ //# sourceMappingURL=gift-card-confirm-step.component.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/transfer/gift-card-confirm-step.component.tsx"],"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'use client'\n\nimport type { PlannedTransferRow, TransferPlanRowsResponse } from '@aglyn/aglyn/data-transfer'\nimport type { TransferWizardStepProps } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { ScrollTable } from '@aglyn/shared-ui-jsx/components/scroll-table.component'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { useUser } from '@aglyn/tenant-feature-instance'\nimport {\n Alert,\n AlertTitle,\n Checkbox,\n Chip,\n FormControlLabel,\n Stack,\n TableBody,\n TableCell,\n TableHead,\n TablePagination,\n TableRow,\n TextField,\n Typography,\n} from '@mui/material'\nimport { useEffect, useMemo, useState } from 'react'\nimport { giftCardMoney, typedDollarsToCents, type GiftCardPlannedRow, type GiftCardRowPlan } from './records-transfer'\n\n/**\n * THE GIFT CARD IMPORT'S CONFIRM STEP (AGL-3551), after Conflicts: every\n * card the dry run will issue, with its code and value, and the total typed\n * back. The step is complete only when the typed total is exactly the total\n * of the cards listed; the server reads the same answer from `extras` and\n * issues nothing unless it is the total it counted itself.\n *\n * The answer also says whether each recipient is emailed their code. It is\n * off by default: cards moved from another platform carry codes shoppers\n * already hold.\n */\n\n/** The step's answer, as it rides in `extras`. */\ninterface ConfirmAnswer {\n typed: string\n totalCents?: number\n email: boolean\n}\n\nconst PAGE = 25\n/** The plan route's largest page. */\nconst ROWS_PAGE = 200\n\nfunction readAnswer(value: unknown): ConfirmAnswer {\n const answer = (value && typeof value === 'object' ? value : {}) as Partial<ConfirmAnswer>\n return { typed: String(answer.typed ?? ''), email: answer.email === true }\n}\n\nexport function GiftCardConfirmStep(props: TransferWizardStepProps) {\n const { orgId, jobId, value, setValue, setComplete } = props\n const { data: user } = useUser()\n const answer = readAnswer(value)\n const [cards, setCards] = useState<GiftCardRowPlan[] | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [page, setPage] = useState(0)\n\n // Every card the dry run issues, page by page from the stored plan.\n useEffect(() => {\n if (!user || !orgId || !jobId) return undefined\n let live = true\n void (async () => {\n try {\n const found: GiftCardRowPlan[] = []\n let offset: number | null = 0\n while (offset !== null) {\n const response = await authorizedFetch(user, '/api/transfer/plan', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ orgId, jobId, action: 'rows', offset, limit: ROWS_PAGE, verdicts: ['create'] }),\n })\n const payload = (await response.json().catch(() => ({}))) as Partial<TransferPlanRowsResponse> & { error?: string }\n if (!response.ok || !payload.rows) throw new Error(payload.error ?? 'The cards could not be read.')\n for (const row of payload.rows.rows as PlannedTransferRow[]) {\n const card = (row as GiftCardPlannedRow).giftCard\n if (card && !card.problem) found.push(card)\n }\n offset = payload.rows.next\n }\n if (live) setCards(found)\n } catch (reason) {\n if (live) setError(reason instanceof Error ? reason.message : 'The cards could not be read.')\n }\n })()\n return () => {\n live = false\n }\n }, [user, orgId, jobId])\n\n const totalCents = useMemo(() => (cards ?? []).reduce((sum, card) => sum + card.amountCents, 0), [cards])\n const typedCents = typedDollarsToCents(answer.typed)\n const matches = cards !== null && typedCents === totalCents\n\n useEffect(() => {\n // Nothing to issue needs no total: the review says why each row is held.\n setComplete(cards !== null && (cards.length === 0 || matches))\n }, [cards, matches, setComplete])\n\n const save = (next: Partial<ConfirmAnswer>) => {\n const merged = { ...answer, ...next }\n const cents = typedDollarsToCents(merged.typed)\n setValue({ typed: merged.typed, email: merged.email, ...(cents !== null && cents > 0 ? { totalCents: cents } : {}) })\n }\n\n if (error) return <Alert severity=\"error\">{error}</Alert>\n if (cards === null) {\n return (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Reading the cards this import issues…'}\n </Typography>\n )\n }\n if (!cards.length) {\n return (\n <Alert severity=\"info\">\n {'No card in this file can be issued. The review lists every row with the reason.'}\n </Alert>\n )\n }\n\n const madeCodes = cards.filter((card) => card.newCode || card.madeCode).length\n const shown = cards.slice(page * PAGE, page * PAGE + PAGE)\n return (\n <Stack spacing={2}>\n <Alert severity=\"warning\">\n <AlertTitle>\n {`${cards.length.toLocaleString('en-US')} gift card${cards.length === 1 ? '' : 's'} worth ${giftCardMoney(totalCents)}`}\n </AlertTitle>\n {'Each card is spendable money the moment it is issued. Check every code and value, then type the total ' +\n 'to issue them.'}\n </Alert>\n <ScrollTable size=\"small\" aria-label=\"Gift cards this import issues\">\n <TableHead>\n <TableRow>\n <TableCell>Code</TableCell>\n <TableCell align=\"right\">Value</TableCell>\n <TableCell>Recipient</TableCell>\n <TableCell>Note</TableCell>\n </TableRow>\n </TableHead>\n <TableBody>\n {shown.map((card) => (\n <TableRow key={card.code}>\n <TableCell>\n <Stack direction=\"row\" spacing={1} sx={{ alignItems: 'center' }}>\n <Typography variant=\"body2\" sx={{ fontFamily: 'monospace' }}>\n {card.code}\n </Typography>\n {card.newCode ? <Chip size=\"small\" label=\"New code: the file's was taken\" /> : null}\n {card.madeCode ? <Chip size=\"small\" label=\"New code\" /> : null}\n </Stack>\n </TableCell>\n <TableCell align=\"right\">{giftCardMoney(card.amountCents)}</TableCell>\n <TableCell>{card.recipientEmail ?? '—'}</TableCell>\n <TableCell>{card.note ?? '—'}</TableCell>\n </TableRow>\n ))}\n </TableBody>\n </ScrollTable>\n {cards.length > PAGE ? (\n <TablePagination\n component=\"div\"\n count={cards.length}\n page={page}\n onPageChange={(_event, next) => setPage(next)}\n rowsPerPage={PAGE}\n rowsPerPageOptions={[PAGE]}\n />\n ) : null}\n <TextField\n label=\"Type the total value to issue these cards\"\n value={answer.typed}\n onChange={(event) => save({ typed: event.target.value })}\n error={Boolean(answer.typed) && !matches}\n helperText={\n answer.typed && !matches\n ? `That is not the total. These cards add up to ${giftCardMoney(totalCents)}.`\n : `Type ${giftCardMoney(totalCents)}.`\n }\n autoComplete=\"off\"\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <FormControlLabel\n control={<Checkbox checked={answer.email} onChange={(event) => save({ email: event.target.checked })} />}\n label={\n <Typography variant=\"body2\">\n {'Email each recipient their code as the card is issued.'}\n {madeCodes\n ? ` ${madeCodes.toLocaleString('en-US')} card${madeCodes === 1 ? ' has' : 's have'} a new code nobody holds yet: email them, or share the codes yourself.`\n : ' Leave this off when the shoppers already hold these codes.'}\n </Typography>\n }\n />\n </Stack>\n )\n}\nGiftCardConfirmStep.displayName = 'GiftCardConfirmStep'\n\nexport default GiftCardConfirmStep\n"],"names":["ScrollTable","authorizedFetch","useUser","Alert","AlertTitle","Checkbox","Chip","FormControlLabel","Stack","TableBody","TableCell","TableHead","TablePagination","TableRow","TextField","Typography","useEffect","useMemo","useState","giftCardMoney","typedDollarsToCents","PAGE","ROWS_PAGE","readAnswer","value","answer","typed","String","email","GiftCardConfirmStep","props","orgId","jobId","setValue","setComplete","data","user","cards","setCards","error","setError","page","setPage","undefined","live","found","offset","payload","response","method","headers","body","JSON","stringify","action","limit","verdicts","json","catch","ok","rows","Error","row","card","giftCard","problem","push","next","reason","message","totalCents","reduce","sum","amountCents","typedCents","matches","length","save","merged","cents","severity","variant","color","madeCodes","filter","newCode","madeCode","shown","slice","spacing","toLocaleString","size","aria-label","align","map","direction","sx","alignItems","fontFamily","code","label","recipientEmail","note","component","count","onPageChange","_event","rowsPerPage","rowsPerPageOptions","onChange","event","target","Boolean","helperText","autoComplete","slotProps","htmlInput","inputMode","control","checked","displayName"],"mappings":"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"}