@aglyn/plugins-commerce 1.0.0-beta.233 → 1.0.0-beta.235

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 (135) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/cart.js +3 -2
  3. package/src/lib/components/cart.js.map +1 -1
  4. package/src/lib/components/console/commerce-analytics-card.component.js +3 -6
  5. package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
  6. package/src/lib/components/console/commerce-glance-card.component.js +9 -3
  7. package/src/lib/components/console/commerce-glance-card.component.js.map +1 -1
  8. package/src/lib/components/console/customer-notifications-card.component.js +3 -1
  9. package/src/lib/components/console/customer-notifications-card.component.js.map +1 -1
  10. package/src/lib/components/console/discounts-card.component.js +2 -3
  11. package/src/lib/components/console/discounts-card.component.js.map +1 -1
  12. package/src/lib/components/console/host-coupons-card.component.js +3 -4
  13. package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
  14. package/src/lib/components/console/local-delivery-settings-card.component.js +2 -2
  15. package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -1
  16. package/src/lib/components/console/locations-card.component.js +1 -1
  17. package/src/lib/components/console/locations-card.component.js.map +1 -1
  18. package/src/lib/components/console/member-posts-card.component.js +2 -2
  19. package/src/lib/components/console/member-posts-card.component.js.map +1 -1
  20. package/src/lib/components/console/order-detail-dialog.component.js +35 -30
  21. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  22. package/src/lib/components/console/order-fulfillment-panel.component.d.ts +11 -1
  23. package/src/lib/components/console/order-fulfillment-panel.component.js +75 -4
  24. package/src/lib/components/console/order-fulfillment-panel.component.js.map +1 -1
  25. package/src/lib/components/console/order-zones.d.ts +45 -1
  26. package/src/lib/components/console/order-zones.js +8 -0
  27. package/src/lib/components/console/order-zones.js.map +1 -1
  28. package/src/lib/components/console/pickup-delivery-queue-card.component.js +15 -1
  29. package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -1
  30. package/src/lib/components/console/pos-page.component.js +3 -2
  31. package/src/lib/components/console/pos-page.component.js.map +1 -1
  32. package/src/lib/components/console/product-editor-dialog.component.d.ts +2 -0
  33. package/src/lib/components/console/product-editor-dialog.component.js +30 -12
  34. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  35. package/src/lib/components/console/products-hub-card.component.js +13 -4
  36. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  37. package/src/lib/components/console/products-hub-zone.component.d.ts +2 -1
  38. package/src/lib/components/console/products-hub-zone.component.js +1 -1
  39. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  40. package/src/lib/components/console/reviews-moderation-card.component.js +1 -2
  41. package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -1
  42. package/src/lib/components/console/shipping-settings-card.component.js +2 -2
  43. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
  44. package/src/lib/components/console/shippingeasy-card.component.js +1 -1
  45. package/src/lib/components/console/shippingeasy-card.component.js.map +1 -1
  46. package/src/lib/components/console/shipstation-card.component.js +1 -2
  47. package/src/lib/components/console/shipstation-card.component.js.map +1 -1
  48. package/src/lib/components/console/store-settings-card.component.js +3 -1
  49. package/src/lib/components/console/store-settings-card.component.js.map +1 -1
  50. package/src/lib/components/console/tax-settings-card.component.js +4 -1
  51. package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
  52. package/src/lib/components/order-status.js +24 -1
  53. package/src/lib/components/order-status.js.map +1 -1
  54. package/src/lib/components/product-detail.js +30 -16
  55. package/src/lib/components/product-detail.js.map +1 -1
  56. package/src/lib/components/product-grid.d.ts +7 -0
  57. package/src/lib/components/product-grid.js +50 -7
  58. package/src/lib/components/product-grid.js.map +1 -1
  59. package/src/lib/components/related-products.js +1 -1
  60. package/src/lib/components/related-products.js.map +1 -1
  61. package/src/lib/components/wishlist.js +1 -1
  62. package/src/lib/components/wishlist.js.map +1 -1
  63. package/src/lib/declarations.server.js +11 -0
  64. package/src/lib/declarations.server.js.map +1 -1
  65. package/src/lib/model/commerce-orders.d.ts +8 -0
  66. package/src/lib/model/commerce-orders.js.map +1 -1
  67. package/src/lib/model/commerce.d.ts +21 -0
  68. package/src/lib/model/commerce.js +39 -1
  69. package/src/lib/model/commerce.js.map +1 -1
  70. package/src/lib/model/index.d.ts +1 -0
  71. package/src/lib/model/index.js +1 -0
  72. package/src/lib/model/index.js.map +1 -1
  73. package/src/lib/model/order-annotations.d.ts +54 -0
  74. package/src/lib/model/order-annotations.js +76 -0
  75. package/src/lib/model/order-annotations.js.map +1 -0
  76. package/src/lib/model/order-events.d.ts +2 -0
  77. package/src/lib/model/order-events.js.map +1 -1
  78. package/src/lib/model/order-fulfillment.d.ts +29 -0
  79. package/src/lib/model/order-fulfillment.js +63 -0
  80. package/src/lib/model/order-fulfillment.js.map +1 -1
  81. package/src/lib/model/order-local-fulfillment.d.ts +36 -0
  82. package/src/lib/model/order-local-fulfillment.js +19 -1
  83. package/src/lib/model/order-local-fulfillment.js.map +1 -1
  84. package/src/lib/model/order-status-view.d.ts +11 -0
  85. package/src/lib/model/order-status-view.js +20 -3
  86. package/src/lib/model/order-status-view.js.map +1 -1
  87. package/src/lib/model/product-write.js +8 -4
  88. package/src/lib/model/product-write.js.map +1 -1
  89. package/src/lib/server/api-v1/openapi.js +1 -0
  90. package/src/lib/server/api-v1/openapi.js.map +1 -1
  91. package/src/lib/server/api-v1/order-view.d.ts +1 -0
  92. package/src/lib/server/api-v1/order-view.js +10 -0
  93. package/src/lib/server/api-v1/order-view.js.map +1 -1
  94. package/src/lib/server/cart-checkout.js +9 -1
  95. package/src/lib/server/cart-checkout.js.map +1 -1
  96. package/src/lib/server/catalog.d.ts +6 -0
  97. package/src/lib/server/catalog.js +3 -1
  98. package/src/lib/server/catalog.js.map +1 -1
  99. package/src/lib/server/checkout.js +11 -1
  100. package/src/lib/server/checkout.js.map +1 -1
  101. package/src/lib/server/fulfill-order.d.ts +12 -0
  102. package/src/lib/server/fulfill-order.js +13 -0
  103. package/src/lib/server/fulfill-order.js.map +1 -1
  104. package/src/lib/server/local-delivery-records.d.ts +30 -0
  105. package/src/lib/server/local-delivery-records.js +284 -0
  106. package/src/lib/server/local-delivery-records.js.map +1 -0
  107. package/src/lib/server/order-annotations.d.ts +28 -0
  108. package/src/lib/server/order-annotations.js +143 -0
  109. package/src/lib/server/order-annotations.js.map +1 -0
  110. package/src/lib/server/order-events.js +2 -1
  111. package/src/lib/server/order-events.js.map +1 -1
  112. package/src/lib/server/order-notifications.js +39 -8
  113. package/src/lib/server/order-notifications.js.map +1 -1
  114. package/src/lib/server/pos-payment.js +9 -3
  115. package/src/lib/server/pos-payment.js.map +1 -1
  116. package/src/lib/server/product-catalog.js +6 -2
  117. package/src/lib/server/product-catalog.js.map +1 -1
  118. package/src/lib/server/product-drafts.d.ts +36 -9
  119. package/src/lib/server/product-drafts.js +24 -5
  120. package/src/lib/server/product-drafts.js.map +1 -1
  121. package/src/lib/server/product.d.ts +6 -1
  122. package/src/lib/server/product.js +1 -1
  123. package/src/lib/server/product.js.map +1 -1
  124. package/src/lib/server/products-write.d.ts +29 -0
  125. package/src/lib/server/products-write.js +7 -7
  126. package/src/lib/server/products-write.js.map +1 -1
  127. package/src/lib/server/shipment-records.js +2 -0
  128. package/src/lib/server/shipment-records.js.map +1 -1
  129. package/src/lib/server/site-page-resolver.d.ts +13 -0
  130. package/src/lib/server/site-page-resolver.js +138 -1
  131. package/src/lib/server/site-page-resolver.js.map +1 -1
  132. package/src/lib/server.js +5 -0
  133. package/src/lib/server.js.map +1 -1
  134. package/src/lib/utils/use-storefront-purchase-event.js +8 -2
  135. package/src/lib/utils/use-storefront-purchase-event.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/commerce-glance-card.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'use client'\n\nimport {\n formatOrderNumber,\n isLowStock,\n liftLegacyProduct,\n orderIsTestMode,\n} from '../../model'\nimport { checkEntitlement, pluginDocsHelp } from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Alert, Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { collection, limit, orderBy, query, where } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useConsoleHostRoute,\n useFirestore,\n useOrgPlan,\n} from '@aglyn/tenant-feature-instance'\nimport { useFirestoreCollection } from '@aglyn/tenant-feature-instance'\n\nconst THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000\n\n/**\n * How many orders the 30-day figures may be computed from, and how many\n * products the low-stock count may scan.\n *\n * Both tiles are aggregates, so neither can be paged: a sum over page one is\n * not a sum. What bounds them instead is the window itself — the orders query\n * asks for thirty days rather than for a count, so a store reads what it sold\n * rather than a fixed slab, and only a store selling more than this in a month\n * meets the ceiling at all.\n *\n * A ceiling that bites makes every figure under it an UNDERSTATEMENT, which is\n * why `truncated` is rendered rather than logged. Revenue quietly short is the\n * one failure a reader cannot detect from the number itself.\n */\nconst GLANCE_ORDER_CEILING = 250\nconst GLANCE_PRODUCT_CEILING = 250\n\n/**\n * Commerce at a glance (AGL-353): 30-day revenue, orders, AOV, low-stock\n * count and the five latest orders. Renders nothing for hosts without a\n * catalog so non-commerce dashboards stay clean.\n *\n * The figures are the `commerceAnalytics` entitlement — `/pricing` sells\n * \"Commerce analytics\" as Pro-and-up, and `/product/commerce` calls it \"a\n * built-in commerce analytics dashboard\". Until AGL-1938 the flag was\n * inert: its ONLY occurrence outside `plan-entitlements.ts` was its own\n * type declaration, and this card carried no check, so every commerce org\n * — Starter included — got the Pro surface for free. The store link and\n * card shell stay ungated; only the measured numbers are the paid part.\n */\nexport function CommerceGlanceCard(props: { hostId: string }) {\n const { hostId } = props\n // Console routes are /[orgSlug]/hosts/[subdomain]/… (AGL-673); this\n // component only has a host doc id.\n const consoleRoute = useConsoleHostRoute(hostId)\n const { org, ready: orgReady } = useOrgPlan(hostId)\n const entitled = checkEntitlement(org as never, 'commerceAnalytics')\n const firestore = useFirestore()\n /*\n * Anchored once per mount so the query identity is stable. Recomputing it\n * per render would rebuild the listener on every pass, which re-reads the\n * whole window each time.\n */\n const since = useMemo(() => Date.now() - THIRTY_DAYS_MS, [])\n /**\n * The thirty days the tiles claim, asked for as thirty days.\n *\n * `createdAtMs` is the field every order writer in the plugin stamps — cart,\n * buy-now, draft, POS cash, POS card and subscription cycle — and the one\n * `reconcile-stock` already walks the collection by. `createdAt` is not\n * interchangeable: the orders collection group is indexed on `createdAtMs`\n * alone, and a range on a field a document lacks drops that document rather\n * than mis-placing it.\n *\n * The range and the order are the same field, so this needs no composite\n * index beyond the `createdAtMs` override already declared.\n */\n const { data: orderDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'orders'),\n where('createdAtMs', '>=', since),\n orderBy('createdAtMs', 'desc'),\n limit(GLANCE_ORDER_CEILING + 1),\n ),\n [firestore, hostId, since],\n { idField: '$id' },\n )\n const { data: productDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'products'),\n GLANCE_PRODUCT_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const orderWindow = useMemo(\n () => ceilingedWindow<any>(orderDocs ?? undefined, GLANCE_ORDER_CEILING),\n [orderDocs],\n )\n const productWindow = useMemo(\n () =>\n ceilingedWindow<any>(productDocs ?? undefined, GLANCE_PRODUCT_CEILING),\n [productDocs],\n )\n\n const summary = useMemo(() => {\n // The query returns the window already ordered newest-first; re-sorting a\n // ceilinged read would only restate the order it arrived in.\n const orders = orderWindow.rows\n // ONE DEFINITION OF 30-DAY REVENUE, NOT TWO.\n //\n // This card and `commerce-analytics-card` read the same orders collection\n // over the same window and disagreed three ways, so one dashboard showed a\n // store two different revenues:\n //\n // - `pending` orders, which have taken no money, were counted here as\n // revenue and excluded there.\n // - `cancelled` was tested with the American spelling, which never\n // matches the persisted `OrderStatus`, so every cancelled order was\n // booked. The value is a PERSISTED enum and is not the place to apply\n // the American-spelling rule; the comparison follows the data.\n // - A refund was all-or-nothing: a fully-refunded order was dropped\n // whole while a 99%-refunded one counted in full.\n //\n // The filter and the per-order figure now match that card line for line,\n // so the two cannot drift again without someone editing both.\n const recentWindow = orders.filter(\n (order) =>\n order.status !== 'pending' &&\n order.status !== 'cancelled' &&\n // A rehearsal is not revenue — the same exclusion the\n // analytics card applies, from the same helper.\n !orderIsTestMode(order),\n )\n // Net of what went back, rather than dropping the order. The legacy flat\n // `amountCents` is the fallback, matching every other money reader\n // (AGL-1747): this card had the modern read but not the fallback, so a\n // Commerce Starter order (AGL-90) rendered as $0.00.\n const orderNetCents = (order: any) =>\n Number(order.totals?.totalCents ?? order.amountCents ?? 0) -\n Number(order.refundedCents ?? 0)\n const revenueCents = recentWindow.reduce(\n (sum, order) => sum + orderNetCents(order),\n 0,\n )\n const lowStock = productWindow.rows.filter((product: any) => {\n try {\n return isLowStock(liftLegacyProduct(product))\n } catch {\n return false\n }\n }).length\n return {\n latest: orders.slice(0, 5),\n orders30d: recentWindow.length,\n revenueCents,\n aovCents: recentWindow.length\n ? Math.round(revenueCents / recentWindow.length)\n : 0,\n lowStock,\n }\n }, [orderWindow, productWindow])\n\n // No catalog and nothing sold in the window — this host doesn't sell; stay\n // invisible.\n if (!(productWindow.rows.length || orderWindow.rows.length)) return null\n\n const money = (cents: number) => `$${(cents / 100).toFixed(2)}`\n\n const openStoreAction = (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={consoleRoute.base ? `${consoleRoute.base}/products` : undefined}\n size=\"small\"\n color=\"primary\"\n >\n {'Open store'}\n </Button>\n )\n\n if (!orgReady) {\n // `checkEntitlement(undefined)` resolves the FREE tier rather than\n // \"unknown\" (see `useOrgPlan`), and this card's \"no\" is an upsell —\n // never accuse a paying org of being unentitled while the plan doc is\n // still in flight.\n return (\n <CardDisplay\n header={'Commerce'}\n help={pluginDocsHelp('commerce')}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: openStoreAction }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Checking your plan…'}\n </Typography>\n </CardDisplay>\n )\n }\n\n if (!entitled) {\n return (\n <CardDisplay\n header={'Commerce'}\n help={pluginDocsHelp('commerce')}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: openStoreAction }}\n >\n <Alert\n severity=\"info\"\n action={\n consoleRoute.orgSlug ? (\n <AppLink\n componentVariant=\"button\"\n color=\"inherit\"\n size=\"small\"\n href={`/${consoleRoute.orgSlug}/billing`}\n >\n {'Upgrade'}\n </AppLink>\n ) : undefined\n }\n >\n {'Commerce analytics — revenue, orders and average order value — ' +\n 'is a Pro feature. Every order is still being recorded, so the ' +\n 'numbers are waiting the moment you upgrade.'}\n </Alert>\n </CardDisplay>\n )\n }\n\n return (\n <CardDisplay\n header={'Commerce'}\n help={pluginDocsHelp('commerce')}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: openStoreAction }}\n >\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={3}>\n <Stack>\n <Typography variant=\"h6\">{money(summary.revenueCents)}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Revenue · 30d'}\n </Typography>\n </Stack>\n <Stack>\n <Typography variant=\"h6\">{summary.orders30d}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Orders · 30d'}\n </Typography>\n </Stack>\n <Stack>\n <Typography variant=\"h6\">{money(summary.aovCents)}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Avg order'}\n </Typography>\n </Stack>\n {summary.lowStock ? (\n <Stack>\n <Typography variant=\"h6\" color=\"warning.main\">\n {summary.lowStock}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Low stock'}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n {orderWindow.truncated || productWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {orderWindow.truncated\n ? `Counted from the ${GLANCE_ORDER_CEILING} most recent orders of the last 30 days. Open the store for the full figures.`\n : `Low stock counted across ${GLANCE_PRODUCT_CEILING} products. Open the store for the full catalog.`}\n </Typography>\n ) : null}\n {summary.latest.length ? (\n <>\n <Divider />\n <Stack spacing={0.5}>\n {summary.latest.map((order: any) => (\n <Stack\n key={order.$id}\n direction=\"row\"\n spacing={1}\n sx={{\n justifyContent: 'space-between',\n alignItems: 'center',\n }}\n >\n <Typography variant=\"body2\" noWrap sx={{ minWidth: 0 }}>\n {/* `HostOrder`'s own fields. An order with no number yet\n (a checkout still settling) falls back to its id. */}\n {order.number != null\n ? formatOrderNumber(order)\n : order.$id.slice(0, 8)}\n {order.customerEmail ? ` · ${order.customerEmail}` : ''}\n </Typography>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\">\n {money(\n Number(\n order.totals?.totalCents ?? order.amountCents ?? 0,\n ),\n )}\n </Typography>\n <Chip size=\"small\" label={order.status ?? 'paid'} />\n </Stack>\n </Stack>\n ))}\n </Stack>\n </>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nCommerceGlanceCard.displayName = 'CommerceGlanceCard'\n\nexport default CommerceGlanceCard\n"],"names":["formatOrderNumber","isLowStock","liftLegacyProduct","orderIsTestMode","checkEntitlement","pluginDocsHelp","AppLink","CardDisplay","Alert","Button","Chip","Divider","Stack","Typography","collection","limit","orderBy","query","where","useMemo","ceilingedWindow","collectionCeiling","useConsoleHostRoute","useFirestore","useOrgPlan","useFirestoreCollection","THIRTY_DAYS_MS","GLANCE_ORDER_CEILING","GLANCE_PRODUCT_CEILING","CommerceGlanceCard","props","hostId","consoleRoute","org","ready","orgReady","entitled","firestore","since","Date","now","data","orderDocs","idField","productDocs","orderWindow","undefined","productWindow","summary","orders","rows","recentWindow","filter","order","status","orderNetCents","Number","totals","totalCents","amountCents","refundedCents","revenueCents","reduce","sum","lowStock","product","length","latest","slice","orders30d","aovCents","Math","round","money","cents","toFixed","openStoreAction","component","componentVariant","nativeButton","href","base","size","color","header","help","contentGutterX","contentGutterY","HeaderProps","action","variant","severity","orgSlug","spacing","direction","truncated","map","sx","justifyContent","alignItems","noWrap","minWidth","number","$id","customerEmail","label","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/commerce-glance-card.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'use client'\n\nimport {\n formatOrderNumber,\n isLowStock,\n liftLegacyProduct,\n orderIsTestMode,\n} from '../../model'\nimport { checkEntitlement, pluginDocsHelp } from '@aglyn/aglyn'\nimport { AppLink, CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { Alert, Button, Chip, Divider, Stack, Typography } from '@mui/material'\nimport { collection, limit, orderBy, query, where } from 'firebase/firestore'\nimport { useMemo } from 'react'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useConsoleHostRoute,\n useFirestore,\n useOrgPlan,\n} from '@aglyn/tenant-feature-instance'\nimport { useFirestoreCollection } from '@aglyn/tenant-feature-instance'\n\nconst THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000\n\n/**\n * How many orders the 30-day figures may be computed from, and how many\n * products the low-stock count may scan.\n *\n * Both tiles are aggregates, so neither can be paged: a sum over page one is\n * not a sum. What bounds them instead is the window itself — the orders query\n * asks for thirty days rather than for a count, so a store reads what it sold\n * rather than a fixed slab, and only a store selling more than this in a month\n * meets the ceiling at all.\n *\n * A ceiling that bites makes every figure under it an UNDERSTATEMENT, which is\n * why `truncated` is rendered rather than logged. Revenue quietly short is the\n * one failure a reader cannot detect from the number itself.\n */\nconst GLANCE_ORDER_CEILING = 250\nconst GLANCE_PRODUCT_CEILING = 250\n\n/**\n * Commerce at a glance (AGL-353): 30-day revenue, orders, AOV, low-stock\n * count and the five latest orders. Renders nothing for hosts without a\n * catalog so non-commerce dashboards stay clean.\n *\n * The figures are the `commerceAnalytics` entitlement — `/pricing` sells\n * \"Commerce analytics\" as Pro-and-up, and `/product/commerce` calls it \"a\n * built-in commerce analytics dashboard\". Until AGL-1938 the flag was\n * inert: its ONLY occurrence outside `plan-entitlements.ts` was its own\n * type declaration, and this card carried no check, so every commerce org\n * — Starter included — got the Pro surface for free. The store link and\n * card shell stay ungated; only the measured numbers are the paid part.\n */\nexport function CommerceGlanceCard(props: { hostId: string }) {\n const { hostId } = props\n // Console routes are /[orgSlug]/hosts/[subdomain]/… (AGL-673); this\n // component only has a host doc id.\n const consoleRoute = useConsoleHostRoute(hostId)\n const { org, ready: orgReady } = useOrgPlan(hostId)\n const entitled = checkEntitlement(org as never, 'commerceAnalytics')\n const firestore = useFirestore()\n /*\n * Anchored once per mount so the query identity is stable. Recomputing it\n * per render would rebuild the listener on every pass, which re-reads the\n * whole window each time.\n */\n const since = useMemo(() => Date.now() - THIRTY_DAYS_MS, [])\n /**\n * The thirty days the tiles claim, asked for as thirty days.\n *\n * `createdAtMs` is the field every order writer in the plugin stamps — cart,\n * buy-now, draft, POS cash, POS card and subscription cycle — and the one\n * `reconcile-stock` already walks the collection by. `createdAt` is not\n * interchangeable: the orders collection group is indexed on `createdAtMs`\n * alone, and a range on a field a document lacks drops that document rather\n * than mis-placing it.\n *\n * The range and the order are the same field, so this needs no composite\n * index beyond the `createdAtMs` override already declared.\n */\n const { data: orderDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'orders'),\n where('createdAtMs', '>=', since),\n orderBy('createdAtMs', 'desc'),\n limit(GLANCE_ORDER_CEILING + 1),\n ),\n [firestore, hostId, since],\n { idField: '$id' },\n )\n const { data: productDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'products'),\n GLANCE_PRODUCT_CEILING,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const orderWindow = useMemo(\n () => ceilingedWindow<any>(orderDocs ?? undefined, GLANCE_ORDER_CEILING),\n [orderDocs],\n )\n const productWindow = useMemo(\n () =>\n ceilingedWindow<any>(productDocs ?? undefined, GLANCE_PRODUCT_CEILING),\n [productDocs],\n )\n\n const summary = useMemo(() => {\n // The query returns the window already ordered newest-first; re-sorting a\n // ceilinged read would only restate the order it arrived in.\n const orders = orderWindow.rows\n // ONE DEFINITION OF 30-DAY REVENUE, NOT TWO.\n //\n // This card and `commerce-analytics-card` read the same orders collection\n // over the same window and disagreed three ways, so one dashboard showed a\n // store two different revenues:\n //\n // - `pending` orders, which have taken no money, were counted here as\n // revenue and excluded there.\n // - `cancelled` was tested with the American spelling, which never\n // matches the persisted `OrderStatus`, so every cancelled order was\n // booked. The value is a PERSISTED enum and is not the place to apply\n // the American-spelling rule; the comparison follows the data.\n // - A refund was all-or-nothing: a fully-refunded order was dropped\n // whole while a 99%-refunded one counted in full.\n //\n // The filter and the per-order figure now match that card line for line,\n // so the two cannot drift again without someone editing both.\n const recentWindow = orders.filter(\n (order) =>\n order.status !== 'pending' &&\n order.status !== 'cancelled' &&\n // A rehearsal is not revenue — the same exclusion the\n // analytics card applies, from the same helper.\n !orderIsTestMode(order),\n )\n // Net of what went back, rather than dropping the order. The legacy flat\n // `amountCents` is the fallback, matching every other money reader\n // (AGL-1747): this card had the modern read but not the fallback, so a\n // Commerce Starter order (AGL-90) rendered as $0.00.\n const orderNetCents = (order: any) =>\n Number(order.totals?.totalCents ?? order.amountCents ?? 0) -\n Number(order.refundedCents ?? 0)\n const revenueCents = recentWindow.reduce(\n (sum, order) => sum + orderNetCents(order),\n 0,\n )\n const lowStock = productWindow.rows.filter((product: any) => {\n try {\n return isLowStock(liftLegacyProduct(product))\n } catch {\n return false\n }\n }).length\n return {\n latest: orders.slice(0, 5),\n orders30d: recentWindow.length,\n revenueCents,\n aovCents: recentWindow.length\n ? Math.round(revenueCents / recentWindow.length)\n : 0,\n lowStock,\n }\n }, [orderWindow, productWindow])\n\n // No catalog and nothing sold in the window — this host doesn't sell; stay\n // invisible.\n if (!(productWindow.rows.length || orderWindow.rows.length)) return null\n\n const money = (cents: number) => `$${(cents / 100).toFixed(2)}`\n\n const openStoreAction = (\n <Button\n component={AppLink as any}\n {...({ componentVariant: 'naked', nativeButton: false } as any)}\n href={consoleRoute.base ? `${consoleRoute.base}/products` : undefined}\n size=\"small\"\n color=\"primary\"\n >\n {'Open store'}\n </Button>\n )\n\n if (!orgReady) {\n // `checkEntitlement(undefined)` resolves the FREE tier rather than\n // \"unknown\" (see `useOrgPlan`), and this card's \"no\" is an upsell —\n // never accuse a paying org of being unentitled while the plan doc is\n // still in flight.\n return (\n <CardDisplay\n header={'Commerce'}\n help={pluginDocsHelp('commerce', { anchor: '#commerce-glance' })}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: openStoreAction }}\n >\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Checking your plan…'}\n </Typography>\n </CardDisplay>\n )\n }\n\n if (!entitled) {\n return (\n <CardDisplay\n header={'Commerce'}\n help={pluginDocsHelp('commerce', { anchor: '#commerce-glance' })}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: openStoreAction }}\n >\n <Alert\n severity=\"info\"\n action={\n consoleRoute.orgSlug ? (\n <AppLink\n componentVariant=\"button\"\n color=\"inherit\"\n size=\"small\"\n href={`/${consoleRoute.orgSlug}/billing`}\n >\n {'Upgrade'}\n </AppLink>\n ) : undefined\n }\n >\n {'Commerce analytics — revenue, orders and average order value — ' +\n 'is a Pro feature. Every order is still being recorded, so the ' +\n 'numbers are waiting the moment you upgrade.'}\n </Alert>\n </CardDisplay>\n )\n }\n\n return (\n <CardDisplay\n header={'Commerce'}\n help={pluginDocsHelp('commerce', { anchor: '#commerce-glance' })}\n contentGutterX\n contentGutterY\n HeaderProps={{ action: openStoreAction }}\n >\n <Stack spacing={1.5}>\n <Stack direction=\"row\" spacing={3}>\n <Stack>\n <Typography variant=\"h6\">{money(summary.revenueCents)}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Revenue · 30d'}\n </Typography>\n </Stack>\n <Stack>\n <Typography variant=\"h6\">{summary.orders30d}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Orders · 30d'}\n </Typography>\n </Stack>\n <Stack>\n <Typography variant=\"h6\">{money(summary.aovCents)}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Avg order'}\n </Typography>\n </Stack>\n {summary.lowStock ? (\n <Stack>\n <Typography variant=\"h6\" color=\"warning.main\">\n {summary.lowStock}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Low stock'}\n </Typography>\n </Stack>\n ) : null}\n </Stack>\n {orderWindow.truncated || productWindow.truncated ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {orderWindow.truncated\n ? `Counted from the ${GLANCE_ORDER_CEILING} most recent orders of the last 30 days. Open the store for the full figures.`\n : `Low stock counted across ${GLANCE_PRODUCT_CEILING} products. Open the store for the full catalog.`}\n </Typography>\n ) : null}\n {summary.latest.length ? (\n <>\n <Divider />\n <Stack spacing={0.5}>\n {summary.latest.map((order: any) => (\n <Stack\n key={order.$id}\n direction=\"row\"\n spacing={1}\n sx={{\n justifyContent: 'space-between',\n alignItems: 'center',\n }}\n >\n <Typography variant=\"body2\" noWrap sx={{ minWidth: 0 }}>\n {/* `HostOrder`'s own fields. An order with no number yet\n (a checkout still settling) falls back to its id. */}\n {order.number != null\n ? formatOrderNumber(order)\n : order.$id.slice(0, 8)}\n {order.customerEmail ? ` · ${order.customerEmail}` : ''}\n </Typography>\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\">\n {money(\n Number(\n order.totals?.totalCents ?? order.amountCents ?? 0,\n ),\n )}\n </Typography>\n <Chip size=\"small\" label={order.status ?? 'paid'} />\n </Stack>\n </Stack>\n ))}\n </Stack>\n </>\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nCommerceGlanceCard.displayName = 'CommerceGlanceCard'\n\nexport default CommerceGlanceCard\n"],"names":["formatOrderNumber","isLowStock","liftLegacyProduct","orderIsTestMode","checkEntitlement","pluginDocsHelp","AppLink","CardDisplay","Alert","Button","Chip","Divider","Stack","Typography","collection","limit","orderBy","query","where","useMemo","ceilingedWindow","collectionCeiling","useConsoleHostRoute","useFirestore","useOrgPlan","useFirestoreCollection","THIRTY_DAYS_MS","GLANCE_ORDER_CEILING","GLANCE_PRODUCT_CEILING","CommerceGlanceCard","props","hostId","consoleRoute","org","ready","orgReady","entitled","firestore","since","Date","now","data","orderDocs","idField","productDocs","orderWindow","undefined","productWindow","summary","orders","rows","recentWindow","filter","order","status","orderNetCents","Number","totals","totalCents","amountCents","refundedCents","revenueCents","reduce","sum","lowStock","product","length","latest","slice","orders30d","aovCents","Math","round","money","cents","toFixed","openStoreAction","component","componentVariant","nativeButton","href","base","size","color","header","help","anchor","contentGutterX","contentGutterY","HeaderProps","action","variant","severity","orgSlug","spacing","direction","truncated","map","sx","justifyContent","alignItems","noWrap","minWidth","number","$id","customerEmail","label","displayName"],"mappings":"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"}
@@ -83,7 +83,9 @@ import * as CommerceModel from "../../model/index.js";
83
83
  ]);
84
84
  return /*#__PURE__*/ _jsx(CardDisplay, {
85
85
  header: 'Customer notifications',
86
- help: pluginDocsHelp('commerce'),
86
+ help: pluginDocsHelp('orderNotifications', {
87
+ anchor: '#customer-emails'
88
+ }),
87
89
  contentGutterX: true,
88
90
  contentGutterY: true,
89
91
  children: /*#__PURE__*/ _jsxs(Stack, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/customer-notifications-card.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'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n useFirestore,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { FormControlLabel, Stack, Switch, Typography } from '@mui/material'\nimport { doc, setDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport * as CommerceModel from '../../model'\n\nexport interface CustomerNotificationsCardProps {\n hostId: string\n}\n\n/**\n * Which order messages the store sends its buyers (AGL-3610), on\n * `hosts/{hostId}/settings/store.buyerNotifications`. Every moment is ON\n * unless switched off here — transaction messages are on by default for every\n * store — and each switch writes only its own key, so this card can never\n * overwrite the store settings card's fields beside it.\n *\n * The \"Also send as texts\" switch appears only when the platform can send\n * texts (the receipt route's channel answer), so the card never offers a\n * channel that does not exist.\n */\nexport function CustomerNotificationsCard(props: CustomerNotificationsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { data: store } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'settings', 'store'),\n [firestore, hostId],\n )\n const [smsAvailable, setSmsAvailable] = useState(false)\n const [saving, setSaving] = useState<string | null>(null)\n\n useEffect(() => {\n let live = true\n void authorizedFetch(\n user,\n `/api/commerce/order-receipt-send?hostId=${encodeURIComponent(hostId)}`,\n )\n .then((response) => (response.ok ? response.json() : {}))\n .then((payload: any) => {\n if (live) setSmsAvailable(payload?.sms === true)\n })\n .catch(() => undefined)\n return () => {\n live = false\n }\n }, [hostId, user])\n\n const settings = store?.buyerNotifications\n const handleToggle = useCallback(\n async (key: CommerceModel.BuyerNotificationEvent | 'texts', on: boolean) => {\n setSaving(key)\n try {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'settings', 'store'),\n { buyerNotifications: { [key]: on } },\n { merge: true },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('That setting could not be saved. Try again.', {\n variant: 'error',\n })\n } finally {\n setSaving(null)\n }\n },\n [enqueueSnackbar, firestore, hostId],\n )\n\n return (\n <CardDisplay\n header={'Customer notifications'}\n help={pluginDocsHelp('commerce')}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The emails your customers get about their orders. Each one links ' +\n 'to a private order status page with shipments and tracking. ' +\n 'Change their wording and colors under Emails.'}\n </Typography>\n {CommerceModel.BUYER_NOTIFICATION_EVENTS.map((event) => {\n const label = CommerceModel.BUYER_NOTIFICATION_LABELS[event]\n return (\n <FormControlLabel\n key={event}\n control={\n <Switch\n size=\"small\"\n checked={CommerceModel.buyerNotificationEnabled(settings, event)}\n disabled={saving === event}\n onChange={(change) => handleToggle(event, change.target.checked)}\n />\n }\n label={\n <Stack>\n <Typography variant=\"body2\">{label.label}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {label.description}\n </Typography>\n </Stack>\n }\n />\n )\n })}\n {smsAvailable ? (\n <FormControlLabel\n control={\n <Switch\n size=\"small\"\n checked={CommerceModel.buyerNotificationEnabled(settings, 'texts')}\n disabled={saving === 'texts'}\n onChange={(change) => handleToggle('texts', change.target.checked)}\n />\n }\n label={\n <Stack>\n <Typography variant=\"body2\">{'Also send as texts'}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'When an order has the customer’s phone number for updates. ' +\n 'Customers can reply STOP to opt out.'}\n </Typography>\n </Stack>\n }\n />\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nCustomerNotificationsCard.displayName = 'CustomerNotificationsCard'\n\nexport default CustomerNotificationsCard\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","authorizedFetch","useFirestore","useFirestoreDoc","useUser","FormControlLabel","Stack","Switch","Typography","doc","setDoc","useCallback","useEffect","useState","CommerceModel","CustomerNotificationsCard","props","hostId","firestore","data","user","enqueueSnackbar","store","smsAvailable","setSmsAvailable","saving","setSaving","live","encodeURIComponent","then","response","ok","json","payload","sms","catch","undefined","settings","buyerNotifications","handleToggle","key","on","merge","error","console","variant","header","help","contentGutterX","contentGutterY","spacing","color","BUYER_NOTIFICATION_EVENTS","map","event","label","BUYER_NOTIFICATION_LABELS","control","size","checked","buyerNotificationEnabled","disabled","onChange","change","target","description","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/customer-notifications-card.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'use client'\n\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport {\n useFirestore,\n useFirestoreDoc,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { FormControlLabel, Stack, Switch, Typography } from '@mui/material'\nimport { doc, setDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport * as CommerceModel from '../../model'\n\nexport interface CustomerNotificationsCardProps {\n hostId: string\n}\n\n/**\n * Which order messages the store sends its buyers (AGL-3610), on\n * `hosts/{hostId}/settings/store.buyerNotifications`. Every moment is ON\n * unless switched off here — transaction messages are on by default for every\n * store — and each switch writes only its own key, so this card can never\n * overwrite the store settings card's fields beside it.\n *\n * The \"Also send as texts\" switch appears only when the platform can send\n * texts (the receipt route's channel answer), so the card never offers a\n * channel that does not exist.\n */\nexport function CustomerNotificationsCard(props: CustomerNotificationsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { data: store } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'settings', 'store'),\n [firestore, hostId],\n )\n const [smsAvailable, setSmsAvailable] = useState(false)\n const [saving, setSaving] = useState<string | null>(null)\n\n useEffect(() => {\n let live = true\n void authorizedFetch(\n user,\n `/api/commerce/order-receipt-send?hostId=${encodeURIComponent(hostId)}`,\n )\n .then((response) => (response.ok ? response.json() : {}))\n .then((payload: any) => {\n if (live) setSmsAvailable(payload?.sms === true)\n })\n .catch(() => undefined)\n return () => {\n live = false\n }\n }, [hostId, user])\n\n const settings = store?.buyerNotifications\n const handleToggle = useCallback(\n async (key: CommerceModel.BuyerNotificationEvent | 'texts', on: boolean) => {\n setSaving(key)\n try {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'settings', 'store'),\n { buyerNotifications: { [key]: on } },\n { merge: true },\n )\n } catch (error) {\n console.error(error)\n enqueueSnackbar('That setting could not be saved. Try again.', {\n variant: 'error',\n })\n } finally {\n setSaving(null)\n }\n },\n [enqueueSnackbar, firestore, hostId],\n )\n\n return (\n <CardDisplay\n header={'Customer notifications'}\n help={pluginDocsHelp('orderNotifications', { anchor: '#customer-emails' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'The emails your customers get about their orders. Each one links ' +\n 'to a private order status page with shipments and tracking. ' +\n 'Change their wording and colors under Emails.'}\n </Typography>\n {CommerceModel.BUYER_NOTIFICATION_EVENTS.map((event) => {\n const label = CommerceModel.BUYER_NOTIFICATION_LABELS[event]\n return (\n <FormControlLabel\n key={event}\n control={\n <Switch\n size=\"small\"\n checked={CommerceModel.buyerNotificationEnabled(settings, event)}\n disabled={saving === event}\n onChange={(change) => handleToggle(event, change.target.checked)}\n />\n }\n label={\n <Stack>\n <Typography variant=\"body2\">{label.label}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {label.description}\n </Typography>\n </Stack>\n }\n />\n )\n })}\n {smsAvailable ? (\n <FormControlLabel\n control={\n <Switch\n size=\"small\"\n checked={CommerceModel.buyerNotificationEnabled(settings, 'texts')}\n disabled={saving === 'texts'}\n onChange={(change) => handleToggle('texts', change.target.checked)}\n />\n }\n label={\n <Stack>\n <Typography variant=\"body2\">{'Also send as texts'}</Typography>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'When an order has the customer’s phone number for updates. ' +\n 'Customers can reply STOP to opt out.'}\n </Typography>\n </Stack>\n }\n />\n ) : null}\n </Stack>\n </CardDisplay>\n )\n}\nCustomerNotificationsCard.displayName = 'CustomerNotificationsCard'\n\nexport default CustomerNotificationsCard\n"],"names":["pluginDocsHelp","CardDisplay","useSnackbar","authorizedFetch","useFirestore","useFirestoreDoc","useUser","FormControlLabel","Stack","Switch","Typography","doc","setDoc","useCallback","useEffect","useState","CommerceModel","CustomerNotificationsCard","props","hostId","firestore","data","user","enqueueSnackbar","store","smsAvailable","setSmsAvailable","saving","setSaving","live","encodeURIComponent","then","response","ok","json","payload","sms","catch","undefined","settings","buyerNotifications","handleToggle","key","on","merge","error","console","variant","header","help","anchor","contentGutterX","contentGutterY","spacing","color","BUYER_NOTIFICATION_EVENTS","map","event","label","BUYER_NOTIFICATION_LABELS","control","size","checked","buyerNotificationEnabled","disabled","onChange","change","target","description","displayName"],"mappings":"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"}
@@ -167,9 +167,8 @@ import { COMMERCE_DISCOUNTS_TRANSFER } from "../../transfer/transfer-keys.js";
167
167
  hostId: hostId
168
168
  })
169
169
  },
170
- help: pluginDocsHelp('commerceEndToEnd', {
171
- anchor: '#4-what-checkout-does',
172
- excerpt: 'Discount codes your customers type at your own checkout — not ' + `the coupons ${Aglyn.PLATFORM_BRAND_NAME} issues against your subscription.`
170
+ help: pluginDocsHelp('commerce', {
171
+ anchor: '#discounts'
173
172
  }),
174
173
  contentGutterX: true,
175
174
  contentGutterY: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/discounts-card.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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteDoc,\n doc,\n setDoc,\n} from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collectionPage,\n usePagedCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport TransferHeaderActions from '../../transfer/transfer-header-actions.component'\nimport { COMMERCE_DISCOUNTS_TRANSFER } from '../../transfer/transfer-keys'\n\nexport interface DiscountsCardProps {\n hostId: string\n}\n\n/**\n * Discounts hub (AGL-305): code and automatic promotions with value,\n * min-subtotal, schedule, and usage-limit controls, resolved at\n * checkout/POS by the shared engine. Supersedes the simple coupons\n * card (which keeps working for existing codes).\n */\nexport function DiscountsCard(props: DiscountsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const {\n status: discountsStatus,\n /**\n * The row this dialog is seeded from is unconfirmed by the server\n * (AGL-1358). `redemptions` is a LIVE counter the checkout engine\n * increments, and editing a discount copies it out of the seed and\n * writes it straight back — so a cached seed rolls the count backwards\n * and hands out redemptions that were already spent.\n */\n fromCache: discountsFromCache,\n rows: discounts,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n /*\n * SERVER-PAGED, and ordered (AGL-2501).\n *\n * `limit(100)` alone is answered in DOCUMENT-ID order and the ids are\n * `createResourceUid()`, so the hundred were a pseudo-random sample of the\n * shop's promotions — and the hundred-and-first was UNREACHABLE, because\n * nothing rendered it and no control asked for more.\n *\n * The query can be paged here, unlike the cards carrying a duplicate-name\n * check: a discount's identity is its document id, `code` is an ordinary\n * field with no uniqueness test on this surface, and nothing on the card\n * is computed from rows that are off the page.\n *\n * `collectionPage` orders on the document NAME. `orderBy('code')` is the\n * tempting one and would be wrong twice: an AUTOMATIC promotion is stored\n * with `code: null`, so ordering on it would hide every automatic discount\n * the shop has, and a merchant reading \"no automatic promotions\" would be\n * reading a query artifact.\n */\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'discounts'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const [draft, setDraft] = useState<\n (Partial<CommerceModel.HostDiscount> & { id: string | null }) | null\n >(null)\n\n const handleSave = useCallback(async () => {\n if (!draft) return\n /**\n * `$id` comes off with `id` (AGL-1374).\n *\n * The editor is seeded `{ id: discount.$id, ...discount }`, and that\n * spread carries the listener's SYNTHETIC `$id` — `idField: '$id'`\n * stamps the document id onto the in-memory row, where nothing persists\n * it. Destructuring only `id` left `$id` in `data`, so every save wrote\n * it into the stored document as a real field. Nothing reads it; it is\n * the listener's bookkeeping leaking into storage, and once there no\n * reader can tell it from a field the card meant to write.\n */\n const seeded = draft as typeof draft & { $id?: string }\n const { id, $id: _syntheticId, ...data } = seeded\n const code = data.code?.trim().toUpperCase().replace(/[^A-Z0-9_-]/g, '')\n /**\n * Refuse an EDIT whose seed the server never confirmed (AGL-1358).\n *\n * `merge: true` protects nothing here — `{...data}` carries every field\n * of the seeded row, `redemptions` included, so it is written back\n * whether or not anyone touched it. Under `persistentLocalCache` that\n * seed can be arbitrarily old, and a redemption cap silently regains\n * uses that buyers already spent.\n *\n * Only the edit path is guarded: a NEW discount is built from blanks at\n * a fresh uid and can overwrite nothing.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'discount',\n unreadable: Boolean(id) && discountsStatus === 'error',\n fromCache: Boolean(id) && discountsFromCache,\n },\n async () => {\n await setDoc(\n doc(\n firestore,\n 'hosts',\n hostId,\n 'discounts',\n id ?? Aglyn.createResourceUid(),\n ),\n {\n ...data,\n ...(code ? { code } : { code: null }),\n enabled: data.enabled !== false,\n redemptions: data.redemptions ?? 0,\n },\n { merge: true },\n )\n },\n )\n // Keep the dialog open with what was typed rather than failing silently.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setDraft(null)\n enqueueSnackbar('Discount saved', { variant: 'success', persist: false })\n }, [\n draft,\n firestore,\n hostId,\n enqueueSnackbar,\n discountsFromCache,\n discountsStatus,\n ])\n\n const describe = (discount: any) => {\n const value =\n discount.kind === 'percent'\n ? `${discount.valuePct}% off`\n : discount.kind === 'fixed'\n ? `$${((discount.valueCents ?? 0) / 100).toFixed(2)} off`\n : 'Free shipping'\n const scopeCount = Array.isArray(discount.productIds)\n ? discount.productIds.length\n : 0\n const extras = [\n // THE SCOPE, SAID OUT LOUD. A discount can cover named\n // products rather than the catalog, and the card showed no sign of it —\n // so a merchant could not tell a store-wide discount from a scoped one,\n // and had no way to understand why a scoped one takes less off than the\n // basket total. \"Store-wide\" is stated rather than left blank, because a\n // silent absence is what a narrower scope would also look like.\n scopeCount > 0\n ? `${scopeCount} product${scopeCount === 1 ? '' : 's'} only`\n : 'store-wide',\n discount.minSubtotalCents\n ? `min $${(discount.minSubtotalCents / 100).toFixed(0)}`\n : '',\n // Held slots are NAMED, not folded into the used figure (AGL-2453). A\n // live checkout now consumes a slot for as long as it can still be paid,\n // so `redemptions` alone would show a cap of 100 sitting at 100 against\n // 97 orders with nothing to explain the other three.\n discount.maxRedemptions\n ? CommerceModel.promotionUsageLabel(discount, Date.now())\n : '',\n ]\n .filter(Boolean)\n .join(' · ')\n return `${value}${extras ? ` · ${extras}` : ''}`\n }\n\n return (\n <CardDisplay\n header={'Discounts'}\n HeaderProps={{\n action: <TransferHeaderActions resource={COMMERCE_DISCOUNTS_TRANSFER} hostId={hostId} />,\n }}\n help={pluginDocsHelp('commerceEndToEnd', {\n anchor: '#4-what-checkout-does',\n excerpt:\n 'Discount codes your customers type at your own checkout — not ' +\n `the coupons ${Aglyn.PLATFORM_BRAND_NAME} issues against your subscription.`,\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {discounts.length === 0 && page === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Codes buyers enter at checkout, or automatic promotions that ' +\n 'apply on their own.'}\n </Typography>\n ) : (\n discounts.map((discount: any) => (\n <Stack\n key={discount.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Chip\n label={discount.code || discount.name || 'Automatic'}\n size=\"small\"\n variant=\"outlined\"\n color={discount.code ? 'primary' : 'info'}\n />\n <Typography variant=\"body2\" sx={{ flex: 1 }} noWrap>\n {describe(discount)}\n {discount.enabled === false ? ' · disabled' : ''}\n </Typography>\n <Button\n size=\"small\"\n onClick={() => setDraft({ id: discount.$id, ...discount })}\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n deleteDoc(\n doc(firestore, 'hosts', hostId, 'discounts', discount.$id),\n )\n }\n >\n {'Delete'}\n </Button>\n </Stack>\n ))\n )}\n {discounts.length === 0 && page === 0 ? null : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={discounts.length}\n // No `count`: a server window does not know the collection's\n // total, and `hasMore` is the probe row's answer rather than a\n // comparison against the cap.\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setDraft({ id: null, kind: 'percent', valuePct: 10, enabled: true })\n }\n >\n {'Add discount'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit discount' : 'New discount'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Code (blank = automatic)\"\n value={draft?.code ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, code: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ mt: 1 }}\n placeholder=\"SAVE10\"\n />\n {!draft?.code ? (\n <TextField\n label=\"Promotion name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n placeholder=\"Summer sale\"\n />\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Type\"\n value={draft?.kind ?? 'percent'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, kind: event.target.value as CommerceModel.DiscountKind }\n : prev,\n )\n }\n size=\"small\"\n select\n sx={{ minWidth: 150 }}\n >\n <MenuItem value=\"percent\">{'Percent off'}</MenuItem>\n <MenuItem value=\"fixed\">{'Fixed amount off'}</MenuItem>\n <MenuItem value=\"free_shipping\">{'Free shipping'}</MenuItem>\n </TextField>\n {draft?.kind === 'percent' ? (\n <TextField\n label=\"%\"\n value={draft?.valuePct ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n valuePct: Math.min(\n 100,\n Math.max(0, Number(event.target.value) || 0),\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n sx={{ width: 90 }}\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n ) : null}\n {draft?.kind === 'fixed' ? (\n <TextField\n label=\"$ off\"\n value={\n draft?.valueCents != null ? draft.valueCents / 100 : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n valueCents: Math.round(\n (Number(event.target.value) || 0) * 100,\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n sx={{ width: 110 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n ) : null}\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Min subtotal ($)\"\n placeholder=\"None\"\n value={\n draft?.minSubtotalCents != null\n ? draft.minSubtotalCents / 100\n : ''\n }\n onChange={(event) => {\n const raw = event.target.value.trim()\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n minSubtotalCents:\n raw === ''\n ? undefined\n : Math.round((Number(raw) || 0) * 100),\n }\n : prev,\n )\n }}\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n label=\"Max uses\"\n placeholder=\"Unlimited\"\n value={draft?.maxRedemptions ?? ''}\n onChange={(event) => {\n const raw = event.target.value.trim()\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n maxRedemptions:\n raw === ''\n ? undefined\n : Math.max(1, Math.round(Number(raw))),\n }\n : prev,\n )\n }}\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Starts\"\n type=\"date\"\n value={\n draft?.startAtMs\n ? new Date(draft.startAtMs).toISOString().slice(0, 10)\n : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n startAtMs: event.target.value\n ? Date.parse(`${event.target.value}T00:00:00Z`)\n : undefined,\n }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n label=\"Ends\"\n type=\"date\"\n value={\n draft?.endAtMs\n ? new Date(draft.endAtMs).toISOString().slice(0, 10)\n : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n endAtMs: event.target.value\n ? Date.parse(`${event.target.value}T23:59:59Z`)\n : undefined,\n }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n (draft?.kind === 'percent' && !((draft?.valuePct ?? 0) > 0)) ||\n (draft?.kind === 'fixed' && !((draft?.valueCents ?? 0) > 0))\n }\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nDiscountsCard.displayName = 'DiscountsCard'\n\nexport default DiscountsCard\n"],"names":["Aglyn","CommerceModel","CardDisplay","useSnackbar","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Typography","collection","deleteDoc","doc","setDoc","useCallback","useState","useFirestore","collectionPage","usePagedCollection","writeGuardedBySeed","ListPagination","pluginDocsHelp","TransferHeaderActions","COMMERCE_DISCOUNTS_TRANSFER","DiscountsCard","props","hostId","firestore","enqueueSnackbar","status","discountsStatus","fromCache","discountsFromCache","rows","discounts","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","draft","setDraft","handleSave","data","seeded","id","$id","_syntheticId","code","trim","toUpperCase","replace","verdict","subject","unreadable","Boolean","createResourceUid","enabled","redemptions","merge","ok","message","variant","persist","describe","discount","value","kind","valuePct","valueCents","toFixed","scopeCount","Array","isArray","productIds","length","extras","minSubtotalCents","maxRedemptions","promotionUsageLabel","Date","now","filter","join","header","HeaderProps","action","resource","help","anchor","excerpt","PLATFORM_BRAND_NAME","contentGutterX","contentGutterY","spacing","color","map","direction","sx","alignItems","label","name","size","flex","noWrap","onClick","rowCount","onPageChange","onPageSizeChange","alignSelf","open","onClose","maxWidth","fullWidth","display","flexDirection","gap","onChange","event","prev","target","mt","placeholder","select","minWidth","Math","min","max","Number","width","slotProps","htmlInput","inputMode","round","raw","undefined","type","startAtMs","toISOString","slice","parse","inputLabel","shrink","endAtMs","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/discounts-card.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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n deleteDoc,\n doc,\n setDoc,\n} from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collectionPage,\n usePagedCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport TransferHeaderActions from '../../transfer/transfer-header-actions.component'\nimport { COMMERCE_DISCOUNTS_TRANSFER } from '../../transfer/transfer-keys'\n\nexport interface DiscountsCardProps {\n hostId: string\n}\n\n/**\n * Discounts hub (AGL-305): code and automatic promotions with value,\n * min-subtotal, schedule, and usage-limit controls, resolved at\n * checkout/POS by the shared engine. Supersedes the simple coupons\n * card (which keeps working for existing codes).\n */\nexport function DiscountsCard(props: DiscountsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const {\n status: discountsStatus,\n /**\n * The row this dialog is seeded from is unconfirmed by the server\n * (AGL-1358). `redemptions` is a LIVE counter the checkout engine\n * increments, and editing a discount copies it out of the seed and\n * writes it straight back — so a cached seed rolls the count backwards\n * and hands out redemptions that were already spent.\n */\n fromCache: discountsFromCache,\n rows: discounts,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n /*\n * SERVER-PAGED, and ordered (AGL-2501).\n *\n * `limit(100)` alone is answered in DOCUMENT-ID order and the ids are\n * `createResourceUid()`, so the hundred were a pseudo-random sample of the\n * shop's promotions — and the hundred-and-first was UNREACHABLE, because\n * nothing rendered it and no control asked for more.\n *\n * The query can be paged here, unlike the cards carrying a duplicate-name\n * check: a discount's identity is its document id, `code` is an ordinary\n * field with no uniqueness test on this surface, and nothing on the card\n * is computed from rows that are off the page.\n *\n * `collectionPage` orders on the document NAME. `orderBy('code')` is the\n * tempting one and would be wrong twice: an AUTOMATIC promotion is stored\n * with `code: null`, so ordering on it would hide every automatic discount\n * the shop has, and a merchant reading \"no automatic promotions\" would be\n * reading a query artifact.\n */\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'discounts'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const [draft, setDraft] = useState<\n (Partial<CommerceModel.HostDiscount> & { id: string | null }) | null\n >(null)\n\n const handleSave = useCallback(async () => {\n if (!draft) return\n /**\n * `$id` comes off with `id` (AGL-1374).\n *\n * The editor is seeded `{ id: discount.$id, ...discount }`, and that\n * spread carries the listener's SYNTHETIC `$id` — `idField: '$id'`\n * stamps the document id onto the in-memory row, where nothing persists\n * it. Destructuring only `id` left `$id` in `data`, so every save wrote\n * it into the stored document as a real field. Nothing reads it; it is\n * the listener's bookkeeping leaking into storage, and once there no\n * reader can tell it from a field the card meant to write.\n */\n const seeded = draft as typeof draft & { $id?: string }\n const { id, $id: _syntheticId, ...data } = seeded\n const code = data.code?.trim().toUpperCase().replace(/[^A-Z0-9_-]/g, '')\n /**\n * Refuse an EDIT whose seed the server never confirmed (AGL-1358).\n *\n * `merge: true` protects nothing here — `{...data}` carries every field\n * of the seeded row, `redemptions` included, so it is written back\n * whether or not anyone touched it. Under `persistentLocalCache` that\n * seed can be arbitrarily old, and a redemption cap silently regains\n * uses that buyers already spent.\n *\n * Only the edit path is guarded: a NEW discount is built from blanks at\n * a fresh uid and can overwrite nothing.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'discount',\n unreadable: Boolean(id) && discountsStatus === 'error',\n fromCache: Boolean(id) && discountsFromCache,\n },\n async () => {\n await setDoc(\n doc(\n firestore,\n 'hosts',\n hostId,\n 'discounts',\n id ?? Aglyn.createResourceUid(),\n ),\n {\n ...data,\n ...(code ? { code } : { code: null }),\n enabled: data.enabled !== false,\n redemptions: data.redemptions ?? 0,\n },\n { merge: true },\n )\n },\n )\n // Keep the dialog open with what was typed rather than failing silently.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setDraft(null)\n enqueueSnackbar('Discount saved', { variant: 'success', persist: false })\n }, [\n draft,\n firestore,\n hostId,\n enqueueSnackbar,\n discountsFromCache,\n discountsStatus,\n ])\n\n const describe = (discount: any) => {\n const value =\n discount.kind === 'percent'\n ? `${discount.valuePct}% off`\n : discount.kind === 'fixed'\n ? `$${((discount.valueCents ?? 0) / 100).toFixed(2)} off`\n : 'Free shipping'\n const scopeCount = Array.isArray(discount.productIds)\n ? discount.productIds.length\n : 0\n const extras = [\n // THE SCOPE, SAID OUT LOUD. A discount can cover named\n // products rather than the catalog, and the card showed no sign of it —\n // so a merchant could not tell a store-wide discount from a scoped one,\n // and had no way to understand why a scoped one takes less off than the\n // basket total. \"Store-wide\" is stated rather than left blank, because a\n // silent absence is what a narrower scope would also look like.\n scopeCount > 0\n ? `${scopeCount} product${scopeCount === 1 ? '' : 's'} only`\n : 'store-wide',\n discount.minSubtotalCents\n ? `min $${(discount.minSubtotalCents / 100).toFixed(0)}`\n : '',\n // Held slots are NAMED, not folded into the used figure (AGL-2453). A\n // live checkout now consumes a slot for as long as it can still be paid,\n // so `redemptions` alone would show a cap of 100 sitting at 100 against\n // 97 orders with nothing to explain the other three.\n discount.maxRedemptions\n ? CommerceModel.promotionUsageLabel(discount, Date.now())\n : '',\n ]\n .filter(Boolean)\n .join(' · ')\n return `${value}${extras ? ` · ${extras}` : ''}`\n }\n\n return (\n <CardDisplay\n header={'Discounts'}\n HeaderProps={{\n action: <TransferHeaderActions resource={COMMERCE_DISCOUNTS_TRANSFER} hostId={hostId} />,\n }}\n help={pluginDocsHelp('commerce', { anchor: '#discounts' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {discounts.length === 0 && page === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Codes buyers enter at checkout, or automatic promotions that ' +\n 'apply on their own.'}\n </Typography>\n ) : (\n discounts.map((discount: any) => (\n <Stack\n key={discount.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Chip\n label={discount.code || discount.name || 'Automatic'}\n size=\"small\"\n variant=\"outlined\"\n color={discount.code ? 'primary' : 'info'}\n />\n <Typography variant=\"body2\" sx={{ flex: 1 }} noWrap>\n {describe(discount)}\n {discount.enabled === false ? ' · disabled' : ''}\n </Typography>\n <Button\n size=\"small\"\n onClick={() => setDraft({ id: discount.$id, ...discount })}\n >\n {'Edit'}\n </Button>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n deleteDoc(\n doc(firestore, 'hosts', hostId, 'discounts', discount.$id),\n )\n }\n >\n {'Delete'}\n </Button>\n </Stack>\n ))\n )}\n {discounts.length === 0 && page === 0 ? null : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={discounts.length}\n // No `count`: a server window does not know the collection's\n // total, and `hasMore` is the probe row's answer rather than a\n // comparison against the cap.\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setDraft({ id: null, kind: 'percent', valuePct: 10, enabled: true })\n }\n >\n {'Add discount'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit discount' : 'New discount'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Code (blank = automatic)\"\n value={draft?.code ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, code: event.target.value } : prev,\n )\n }\n size=\"small\"\n sx={{ mt: 1 }}\n placeholder=\"SAVE10\"\n />\n {!draft?.code ? (\n <TextField\n label=\"Promotion name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n placeholder=\"Summer sale\"\n />\n ) : null}\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Type\"\n value={draft?.kind ?? 'percent'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, kind: event.target.value as CommerceModel.DiscountKind }\n : prev,\n )\n }\n size=\"small\"\n select\n sx={{ minWidth: 150 }}\n >\n <MenuItem value=\"percent\">{'Percent off'}</MenuItem>\n <MenuItem value=\"fixed\">{'Fixed amount off'}</MenuItem>\n <MenuItem value=\"free_shipping\">{'Free shipping'}</MenuItem>\n </TextField>\n {draft?.kind === 'percent' ? (\n <TextField\n label=\"%\"\n value={draft?.valuePct ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n valuePct: Math.min(\n 100,\n Math.max(0, Number(event.target.value) || 0),\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n sx={{ width: 90 }}\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n ) : null}\n {draft?.kind === 'fixed' ? (\n <TextField\n label=\"$ off\"\n value={\n draft?.valueCents != null ? draft.valueCents / 100 : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n valueCents: Math.round(\n (Number(event.target.value) || 0) * 100,\n ),\n }\n : prev,\n )\n }\n size=\"small\"\n sx={{ width: 110 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n ) : null}\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Min subtotal ($)\"\n placeholder=\"None\"\n value={\n draft?.minSubtotalCents != null\n ? draft.minSubtotalCents / 100\n : ''\n }\n onChange={(event) => {\n const raw = event.target.value.trim()\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n minSubtotalCents:\n raw === ''\n ? undefined\n : Math.round((Number(raw) || 0) * 100),\n }\n : prev,\n )\n }}\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n label=\"Max uses\"\n placeholder=\"Unlimited\"\n value={draft?.maxRedemptions ?? ''}\n onChange={(event) => {\n const raw = event.target.value.trim()\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n maxRedemptions:\n raw === ''\n ? undefined\n : Math.max(1, Math.round(Number(raw))),\n }\n : prev,\n )\n }}\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Starts\"\n type=\"date\"\n value={\n draft?.startAtMs\n ? new Date(draft.startAtMs).toISOString().slice(0, 10)\n : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n startAtMs: event.target.value\n ? Date.parse(`${event.target.value}T00:00:00Z`)\n : undefined,\n }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n <TextField\n label=\"Ends\"\n type=\"date\"\n value={\n draft?.endAtMs\n ? new Date(draft.endAtMs).toISOString().slice(0, 10)\n : ''\n }\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n endAtMs: event.target.value\n ? Date.parse(`${event.target.value}T23:59:59Z`)\n : undefined,\n }\n : prev,\n )\n }\n size=\"small\"\n fullWidth\n slotProps={{ inputLabel: { shrink: true } }}\n />\n </Stack>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n (draft?.kind === 'percent' && !((draft?.valuePct ?? 0) > 0)) ||\n (draft?.kind === 'fixed' && !((draft?.valueCents ?? 0) > 0))\n }\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nDiscountsCard.displayName = 'DiscountsCard'\n\nexport default DiscountsCard\n"],"names":["Aglyn","CommerceModel","CardDisplay","useSnackbar","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Typography","collection","deleteDoc","doc","setDoc","useCallback","useState","useFirestore","collectionPage","usePagedCollection","writeGuardedBySeed","ListPagination","pluginDocsHelp","TransferHeaderActions","COMMERCE_DISCOUNTS_TRANSFER","DiscountsCard","props","hostId","firestore","enqueueSnackbar","status","discountsStatus","fromCache","discountsFromCache","rows","discounts","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","draft","setDraft","handleSave","data","seeded","id","$id","_syntheticId","code","trim","toUpperCase","replace","verdict","subject","unreadable","Boolean","createResourceUid","enabled","redemptions","merge","ok","message","variant","persist","describe","discount","value","kind","valuePct","valueCents","toFixed","scopeCount","Array","isArray","productIds","length","extras","minSubtotalCents","maxRedemptions","promotionUsageLabel","Date","now","filter","join","header","HeaderProps","action","resource","help","anchor","contentGutterX","contentGutterY","spacing","color","map","direction","sx","alignItems","label","name","size","flex","noWrap","onClick","rowCount","onPageChange","onPageSizeChange","alignSelf","open","onClose","maxWidth","fullWidth","display","flexDirection","gap","onChange","event","prev","target","mt","placeholder","select","minWidth","Math","min","max","Number","width","slotProps","htmlInput","inputMode","round","raw","undefined","type","startAtMs","toISOString","slice","parse","inputLabel","shrink","endAtMs","disabled","displayName"],"mappings":"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"}
@@ -26,7 +26,7 @@ import { collection, doc, limit, query, setDoc, updateDoc } from "firebase/fires
26
26
  import { useCallback, useState } from "react";
27
27
  import { useFirestore } from "@aglyn/tenant-feature-instance";
28
28
  import { usePagedCollection } from "@aglyn/tenant-feature-instance";
29
- import { PLATFORM_BRAND_NAME, pluginDocsHelp } from "@aglyn/aglyn";
29
+ import { pluginDocsHelp } from "@aglyn/aglyn";
30
30
  import TransferHeaderActions from "../../transfer/transfer-header-actions.component.js";
31
31
  import { COMMERCE_COUPONS_TRANSFER } from "../../transfer/transfer-keys.js";
32
32
  /**
@@ -105,9 +105,8 @@ import { COMMERCE_COUPONS_TRANSFER } from "../../transfer/transfer-keys.js";
105
105
  hostId: hostId
106
106
  })
107
107
  },
108
- help: pluginDocsHelp('commerceEndToEnd', {
109
- anchor: '#4-what-checkout-does',
110
- excerpt: 'Coupons redeemable at your storefront checkout, separate from ' + `any coupon ${PLATFORM_BRAND_NAME} applies to your own subscription.`
108
+ help: pluginDocsHelp('commerce', {
109
+ anchor: '#coupons'
111
110
  }),
112
111
  contentGutterX: true,
113
112
  contentGutterY: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/host-coupons-card.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'use client'\n\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, doc, limit, query, setDoc, updateDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { usePagedCollection } from '@aglyn/tenant-feature-instance'\nimport { PLATFORM_BRAND_NAME, pluginDocsHelp } from '@aglyn/aglyn'\nimport TransferHeaderActions from '../../transfer/transfer-header-actions.component'\nimport { COMMERCE_COUPONS_TRANSFER } from '../../transfer/transfer-keys'\n\nexport interface HostCouponsCardProps {\n hostId: string\n}\n\n/**\n * Coupon codes (AGL-96), extracted from the retired Commerce Starter\n * card when the products hub landed (AGL-279). Percent-off codes at\n * `hosts/{hostId}/coupons/{CODE}`; the discounts engine (AGL-305)\n * supersedes these.\n */\nexport function HostCouponsCard(props: HostCouponsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n /*\n * A growing window rather than a hard hundred, which a shop with more\n * codes than that could not see past.\n *\n * No `orderBy`: a coupon's document ID is its CODE, and document-ID order\n * is what Firestore returns by default — so the window is already the\n * alphabetical front of the list, which is what the sort below wants. The\n * sort is kept because it makes that dependency explicit rather than\n * relying on the default holding.\n */\n const {\n rows: couponDocs,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n query(collection(firestore, 'hosts', hostId, 'coupons'), limit(pageLimit)),\n [firestore, hostId],\n { idField: '$id' },\n )\n const coupons = [...couponDocs].sort((a: any, b: any) =>\n String(a.$id).localeCompare(String(b.$id)),\n )\n const [couponDraft, setCouponDraft] = useState<{\n code: string\n percentOff: string\n maxRedemptions: string\n /** `YYYY-MM-DD` from a native date input, or '' for no expiry. */\n expiresOn: string\n } | null>(null)\n\n const handleCouponSave = useCallback(async () => {\n if (!couponDraft) return\n const code = couponDraft.code\n .trim()\n .toUpperCase()\n .replace(/[^A-Z0-9_-]/g, '')\n .slice(0, 40)\n const percentOff = Math.round(Number(couponDraft.percentOff))\n if (!code || !(percentOff > 0 && percentOff <= 100)) return\n // An unparseable value is dropped rather than written: a NaN here would\n // be a coupon that never expires or one that is born dead, depending on\n // how the server coerced it, and neither is what anyone typed.\n const expiresOn = couponDraft.expiresOn.trim()\n const parsedExpiry = expiresOn\n ? Date.parse(`${expiresOn}T23:59:59.999`)\n : NaN\n const expiresAtMs = Number.isFinite(parsedExpiry) ? parsedExpiry : null\n await setDoc(doc(firestore, 'hosts', hostId, 'coupons', code), {\n percentOff,\n enabled: true,\n redemptions: 0,\n ...(couponDraft.maxRedemptions.trim()\n ? {\n maxRedemptions: Math.max(\n 1,\n Math.round(Number(couponDraft.maxRedemptions)),\n ),\n }\n : {}),\n // THE FIELD THE CHECKOUTS ALREADY GATE ON.\n //\n // `checkout.ts` and `cart-checkout.ts` both refuse a coupon whose\n // `expiresAtMs` has passed, and nothing wrote it — so the gate read as\n // enforcement while every coupon ever created was immortal. A launch\n // code stays redeemable forever, which is a discount the merchant\n // believed they had withdrawn.\n //\n // The END of the chosen day, in the merchant's own timezone: \"expires\n // September 5th\" has to mean the 5th still works. The comparison on the\n // server is absolute epoch, so the local end-of-day is what carries that\n // meaning across.\n ...(expiresAtMs != null ? { expiresAtMs } : {}),\n createdAt: Timestamp.now(),\n })\n setCouponDraft(null)\n enqueueSnackbar(`Coupon ${code} created`, {\n variant: 'success',\n persist: false,\n })\n }, [couponDraft, firestore, hostId, enqueueSnackbar])\n\n const handleCouponToggle = useCallback(\n (coupon: any) => () =>\n updateDoc(doc(firestore, 'hosts', hostId, 'coupons', coupon.$id), {\n enabled: coupon.enabled === false,\n }),\n [firestore, hostId],\n )\n\n return (\n <CardDisplay\n header={'Coupons'}\n HeaderProps={{\n action: <TransferHeaderActions resource={COMMERCE_COUPONS_TRANSFER} hostId={hostId} />,\n }}\n help={pluginDocsHelp('commerceEndToEnd', {\n anchor: '#4-what-checkout-does',\n excerpt:\n 'Coupons redeemable at your storefront checkout, separate from ' +\n `any coupon ${PLATFORM_BRAND_NAME} applies to your own subscription.`,\n })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {coupons.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Percent-off codes buyers can enter at checkout.'}\n </Typography>\n ) : (\n coupons.map((coupon: any) => (\n <Stack\n key={coupon.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"body2\" sx={{ flex: 1 }} noWrap>\n {`${coupon.$id} · ${coupon.percentOff}% off · ` +\n // Held slots are named separately (AGL-2453) — see the\n // discounts card for why the used figure alone now misleads.\n CommerceModel.promotionUsageLabel(coupon, Date.now()) +\n // An expiry nobody can see is one the merchant cannot act\n // on, and an EXPIRED coupon still listed as live is the\n // question this card exists to answer.\n (coupon.expiresAtMs != null\n ? Number(coupon.expiresAtMs) < Date.now()\n ? ' · expired'\n : ` · expires ${new Date(\n Number(coupon.expiresAtMs),\n ).toLocaleDateString()}`\n : '') +\n (coupon.enabled === false ? ' · disabled' : '')}\n </Typography>\n <Button size=\"small\" onClick={handleCouponToggle(coupon)}>\n {coupon.enabled === false ? 'Enable' : 'Disable'}\n </Button>\n </Stack>\n ))\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={coupons.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setCouponDraft({\n code: '',\n percentOff: '',\n maxRedemptions: '',\n expiresOn: '',\n })\n }\n >\n {'Add coupon'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(couponDraft)}\n onClose={() => setCouponDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'New coupon'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Code\"\n value={couponDraft?.code ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, code: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n helperText=\"Letters/numbers; stored uppercase, e.g. LAUNCH20\"\n />\n <TextField\n label=\"Percent off\"\n type=\"number\"\n value={couponDraft?.percentOff ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, percentOff: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Max redemptions\"\n type=\"number\"\n placeholder=\"Blank = unlimited\"\n value={couponDraft?.maxRedemptions ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, maxRedemptions: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Expires\"\n type=\"date\"\n value={couponDraft?.expiresOn ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, expiresOn: event.target.value } : prev,\n )\n }\n size=\"small\"\n slotProps={{ inputLabel: { shrink: true } }}\n helperText=\"Blank = never expires. The code works through this day.\"\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setCouponDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n !couponDraft?.code.trim() ||\n !(Number(couponDraft?.percentOff) > 0) ||\n Number(couponDraft?.percentOff) > 100\n }\n onClick={handleCouponSave}\n >\n {'Create'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nHostCouponsCard.displayName = 'HostCouponsCard'\n\nexport default HostCouponsCard\n"],"names":["CommerceModel","CardDisplay","ListPagination","useSnackbar","Timestamp","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","collection","doc","limit","query","setDoc","updateDoc","useCallback","useState","useFirestore","usePagedCollection","PLATFORM_BRAND_NAME","pluginDocsHelp","TransferHeaderActions","COMMERCE_COUPONS_TRANSFER","HostCouponsCard","props","hostId","firestore","enqueueSnackbar","rows","couponDocs","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","coupons","sort","a","b","String","$id","localeCompare","couponDraft","setCouponDraft","handleCouponSave","code","trim","toUpperCase","replace","slice","percentOff","Math","round","Number","expiresOn","parsedExpiry","Date","parse","NaN","expiresAtMs","isFinite","enabled","redemptions","maxRedemptions","max","createdAt","now","variant","persist","handleCouponToggle","coupon","header","HeaderProps","action","resource","help","anchor","excerpt","contentGutterX","contentGutterY","spacing","length","color","map","direction","sx","alignItems","flex","noWrap","promotionUsageLabel","toLocaleDateString","size","onClick","rowCount","onPageChange","onPageSizeChange","alignSelf","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","label","value","onChange","event","prev","target","autoFocus","mt","helperText","type","placeholder","slotProps","inputLabel","shrink","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/host-coupons-card.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'use client'\n\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, doc, limit, query, setDoc, updateDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport { usePagedCollection } from '@aglyn/tenant-feature-instance'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport TransferHeaderActions from '../../transfer/transfer-header-actions.component'\nimport { COMMERCE_COUPONS_TRANSFER } from '../../transfer/transfer-keys'\n\nexport interface HostCouponsCardProps {\n hostId: string\n}\n\n/**\n * Coupon codes (AGL-96), extracted from the retired Commerce Starter\n * card when the products hub landed (AGL-279). Percent-off codes at\n * `hosts/{hostId}/coupons/{CODE}`; the discounts engine (AGL-305)\n * supersedes these.\n */\nexport function HostCouponsCard(props: HostCouponsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n /*\n * A growing window rather than a hard hundred, which a shop with more\n * codes than that could not see past.\n *\n * No `orderBy`: a coupon's document ID is its CODE, and document-ID order\n * is what Firestore returns by default — so the window is already the\n * alphabetical front of the list, which is what the sort below wants. The\n * sort is kept because it makes that dependency explicit rather than\n * relying on the default holding.\n */\n const {\n rows: couponDocs,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n (pageLimit) =>\n query(collection(firestore, 'hosts', hostId, 'coupons'), limit(pageLimit)),\n [firestore, hostId],\n { idField: '$id' },\n )\n const coupons = [...couponDocs].sort((a: any, b: any) =>\n String(a.$id).localeCompare(String(b.$id)),\n )\n const [couponDraft, setCouponDraft] = useState<{\n code: string\n percentOff: string\n maxRedemptions: string\n /** `YYYY-MM-DD` from a native date input, or '' for no expiry. */\n expiresOn: string\n } | null>(null)\n\n const handleCouponSave = useCallback(async () => {\n if (!couponDraft) return\n const code = couponDraft.code\n .trim()\n .toUpperCase()\n .replace(/[^A-Z0-9_-]/g, '')\n .slice(0, 40)\n const percentOff = Math.round(Number(couponDraft.percentOff))\n if (!code || !(percentOff > 0 && percentOff <= 100)) return\n // An unparseable value is dropped rather than written: a NaN here would\n // be a coupon that never expires or one that is born dead, depending on\n // how the server coerced it, and neither is what anyone typed.\n const expiresOn = couponDraft.expiresOn.trim()\n const parsedExpiry = expiresOn\n ? Date.parse(`${expiresOn}T23:59:59.999`)\n : NaN\n const expiresAtMs = Number.isFinite(parsedExpiry) ? parsedExpiry : null\n await setDoc(doc(firestore, 'hosts', hostId, 'coupons', code), {\n percentOff,\n enabled: true,\n redemptions: 0,\n ...(couponDraft.maxRedemptions.trim()\n ? {\n maxRedemptions: Math.max(\n 1,\n Math.round(Number(couponDraft.maxRedemptions)),\n ),\n }\n : {}),\n // THE FIELD THE CHECKOUTS ALREADY GATE ON.\n //\n // `checkout.ts` and `cart-checkout.ts` both refuse a coupon whose\n // `expiresAtMs` has passed, and nothing wrote it — so the gate read as\n // enforcement while every coupon ever created was immortal. A launch\n // code stays redeemable forever, which is a discount the merchant\n // believed they had withdrawn.\n //\n // The END of the chosen day, in the merchant's own timezone: \"expires\n // September 5th\" has to mean the 5th still works. The comparison on the\n // server is absolute epoch, so the local end-of-day is what carries that\n // meaning across.\n ...(expiresAtMs != null ? { expiresAtMs } : {}),\n createdAt: Timestamp.now(),\n })\n setCouponDraft(null)\n enqueueSnackbar(`Coupon ${code} created`, {\n variant: 'success',\n persist: false,\n })\n }, [couponDraft, firestore, hostId, enqueueSnackbar])\n\n const handleCouponToggle = useCallback(\n (coupon: any) => () =>\n updateDoc(doc(firestore, 'hosts', hostId, 'coupons', coupon.$id), {\n enabled: coupon.enabled === false,\n }),\n [firestore, hostId],\n )\n\n return (\n <CardDisplay\n header={'Coupons'}\n HeaderProps={{\n action: <TransferHeaderActions resource={COMMERCE_COUPONS_TRANSFER} hostId={hostId} />,\n }}\n help={pluginDocsHelp('commerce', { anchor: '#coupons' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {coupons.length === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Percent-off codes buyers can enter at checkout.'}\n </Typography>\n ) : (\n coupons.map((coupon: any) => (\n <Stack\n key={coupon.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"body2\" sx={{ flex: 1 }} noWrap>\n {`${coupon.$id} · ${coupon.percentOff}% off · ` +\n // Held slots are named separately (AGL-2453) — see the\n // discounts card for why the used figure alone now misleads.\n CommerceModel.promotionUsageLabel(coupon, Date.now()) +\n // An expiry nobody can see is one the merchant cannot act\n // on, and an EXPIRED coupon still listed as live is the\n // question this card exists to answer.\n (coupon.expiresAtMs != null\n ? Number(coupon.expiresAtMs) < Date.now()\n ? ' · expired'\n : ` · expires ${new Date(\n Number(coupon.expiresAtMs),\n ).toLocaleDateString()}`\n : '') +\n (coupon.enabled === false ? ' · disabled' : '')}\n </Typography>\n <Button size=\"small\" onClick={handleCouponToggle(coupon)}>\n {coupon.enabled === false ? 'Enable' : 'Disable'}\n </Button>\n </Stack>\n ))\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={coupons.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setCouponDraft({\n code: '',\n percentOff: '',\n maxRedemptions: '',\n expiresOn: '',\n })\n }\n >\n {'Add coupon'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(couponDraft)}\n onClose={() => setCouponDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'New coupon'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Code\"\n value={couponDraft?.code ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, code: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n helperText=\"Letters/numbers; stored uppercase, e.g. LAUNCH20\"\n />\n <TextField\n label=\"Percent off\"\n type=\"number\"\n value={couponDraft?.percentOff ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, percentOff: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Max redemptions\"\n type=\"number\"\n placeholder=\"Blank = unlimited\"\n value={couponDraft?.maxRedemptions ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, maxRedemptions: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Expires\"\n type=\"date\"\n value={couponDraft?.expiresOn ?? ''}\n onChange={(event) =>\n setCouponDraft((prev) =>\n prev ? { ...prev, expiresOn: event.target.value } : prev,\n )\n }\n size=\"small\"\n slotProps={{ inputLabel: { shrink: true } }}\n helperText=\"Blank = never expires. The code works through this day.\"\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setCouponDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n !couponDraft?.code.trim() ||\n !(Number(couponDraft?.percentOff) > 0) ||\n Number(couponDraft?.percentOff) > 100\n }\n onClick={handleCouponSave}\n >\n {'Create'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nHostCouponsCard.displayName = 'HostCouponsCard'\n\nexport default HostCouponsCard\n"],"names":["CommerceModel","CardDisplay","ListPagination","useSnackbar","Timestamp","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","collection","doc","limit","query","setDoc","updateDoc","useCallback","useState","useFirestore","usePagedCollection","pluginDocsHelp","TransferHeaderActions","COMMERCE_COUPONS_TRANSFER","HostCouponsCard","props","hostId","firestore","enqueueSnackbar","rows","couponDocs","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","coupons","sort","a","b","String","$id","localeCompare","couponDraft","setCouponDraft","handleCouponSave","code","trim","toUpperCase","replace","slice","percentOff","Math","round","Number","expiresOn","parsedExpiry","Date","parse","NaN","expiresAtMs","isFinite","enabled","redemptions","maxRedemptions","max","createdAt","now","variant","persist","handleCouponToggle","coupon","header","HeaderProps","action","resource","help","anchor","contentGutterX","contentGutterY","spacing","length","color","map","direction","sx","alignItems","flex","noWrap","promotionUsageLabel","toLocaleDateString","size","onClick","rowCount","onPageChange","onPageSizeChange","alignSelf","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","label","value","onChange","event","prev","target","autoFocus","mt","helperText","type","placeholder","slotProps","inputLabel","shrink","disabled","displayName"],"mappings":"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"}
@@ -210,8 +210,8 @@ const LEAD_TIMES = [
210
210
  });
211
211
  return /*#__PURE__*/ _jsx(CardDisplay, {
212
212
  header: 'Local delivery',
213
- help: pluginDocsHelp('commerce', {
214
- anchor: '#pickup-and-local-delivery'
213
+ help: pluginDocsHelp('pickupAndDelivery', {
214
+ anchor: '#set-up-local-delivery'
215
215
  }),
216
216
  HeaderProps: {
217
217
  action: /*#__PURE__*/ _jsx(Button, {