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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +4 -2
  2. package/package.json +14 -13
  3. package/src/lib/components/console/catalog-organization-card.component.js +9 -0
  4. package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
  5. package/src/lib/components/console/discounts-card.component.js +8 -0
  6. package/src/lib/components/console/discounts-card.component.js.map +1 -1
  7. package/src/lib/components/console/gift-cards-card.component.js +8 -0
  8. package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
  9. package/src/lib/components/console/host-coupons-card.component.js +8 -0
  10. package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
  11. package/src/lib/components/console/host-orders-card.component.js +34 -70
  12. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  13. package/src/lib/components/console/product-zones.d.ts +7 -6
  14. package/src/lib/components/console/product-zones.js +5 -4
  15. package/src/lib/components/console/product-zones.js.map +1 -1
  16. package/src/lib/components/console/products-hub-card.component.js +86 -284
  17. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  18. package/src/lib/declarations.console-server.d.ts +38 -0
  19. package/src/lib/declarations.console-server.js +67 -0
  20. package/src/lib/declarations.console-server.js.map +1 -0
  21. package/src/lib/model/commerce-gift-cards.d.ts +26 -0
  22. package/src/lib/model/commerce-gift-cards.js +42 -0
  23. package/src/lib/model/commerce-gift-cards.js.map +1 -1
  24. package/src/lib/model/commerce-orders.d.ts +0 -57
  25. package/src/lib/model/commerce-orders.js +0 -80
  26. package/src/lib/model/commerce-orders.js.map +1 -1
  27. package/src/lib/model/index.d.ts +0 -1
  28. package/src/lib/model/index.js +0 -1
  29. package/src/lib/model/index.js.map +1 -1
  30. package/src/lib/plugin.js +5 -0
  31. package/src/lib/plugin.js.map +1 -1
  32. package/src/lib/server/api-v1/openapi.d.ts +27 -0
  33. package/src/lib/server/api-v1/openapi.js +219 -0
  34. package/src/lib/server/api-v1/openapi.js.map +1 -0
  35. package/src/lib/server/api-v1/orders-and-products.d.ts +19 -0
  36. package/src/lib/server/api-v1/orders-and-products.js +419 -0
  37. package/src/lib/server/api-v1/orders-and-products.js.map +1 -0
  38. package/src/lib/server/billing-webhook.js +10 -13
  39. package/src/lib/server/billing-webhook.js.map +1 -1
  40. package/src/lib/server/download.js +4 -1
  41. package/src/lib/server/download.js.map +1 -1
  42. package/src/lib/server/fulfill-order.d.ts +52 -11
  43. package/src/lib/server/fulfill-order.js +6 -9
  44. package/src/lib/server/fulfill-order.js.map +1 -1
  45. package/src/lib/server/gift-card-issue.d.ts +57 -0
  46. package/src/lib/server/gift-card-issue.js +155 -0
  47. package/src/lib/server/gift-card-issue.js.map +1 -0
  48. package/src/lib/server/gift-cards.js +25 -84
  49. package/src/lib/server/gift-cards.js.map +1 -1
  50. package/src/lib/server/membership-register.js +9 -8
  51. package/src/lib/server/membership-register.js.map +1 -1
  52. package/src/lib/server/newsletter.js +7 -5
  53. package/src/lib/server/newsletter.js.map +1 -1
  54. package/src/lib/server/paid-media-delivery.d.ts +21 -0
  55. package/src/lib/server/paid-media-delivery.js +23 -5
  56. package/src/lib/server/paid-media-delivery.js.map +1 -1
  57. package/src/lib/server/paid-media-serve-count.d.ts +28 -0
  58. package/src/lib/server/paid-media-serve-count.js +46 -0
  59. package/src/lib/server/paid-media-serve-count.js.map +1 -0
  60. package/src/lib/server/person-eraser.d.ts +45 -0
  61. package/src/lib/server/person-eraser.js +57 -0
  62. package/src/lib/server/person-eraser.js.map +1 -0
  63. package/src/lib/server/refund.js +15 -15
  64. package/src/lib/server/refund.js.map +1 -1
  65. package/src/lib/server/restock-flag.d.ts +2 -2
  66. package/src/lib/server/restock-flag.js +1 -1
  67. package/src/lib/server/restock-flag.js.map +1 -1
  68. package/src/lib/server/stream.js +4 -1
  69. package/src/lib/server/stream.js.map +1 -1
  70. package/src/lib/server.js +5 -21
  71. package/src/lib/server.js.map +1 -1
  72. package/src/lib/testing/stand-in-person-records.d.ts +43 -0
  73. package/src/lib/testing/stand-in-person-records.js +63 -0
  74. package/src/lib/testing/stand-in-person-records.js.map +1 -0
  75. package/src/lib/transfer/gift-card-confirm-step.component.d.ts +22 -0
  76. package/src/lib/transfer/gift-card-confirm-step.component.js +259 -0
  77. package/src/lib/transfer/gift-card-confirm-step.component.js.map +1 -0
  78. package/src/lib/transfer/gift-cards-plan.d.ts +29 -0
  79. package/src/lib/transfer/gift-cards-plan.js +40 -0
  80. package/src/lib/transfer/gift-cards-plan.js.map +1 -0
  81. package/src/lib/transfer/gift-cards.server.d.ts +34 -0
  82. package/src/lib/transfer/gift-cards.server.js +296 -0
  83. package/src/lib/transfer/gift-cards.server.js.map +1 -0
  84. package/src/lib/transfer/list-filter.d.ts +29 -0
  85. package/src/lib/transfer/list-filter.js +112 -0
  86. package/src/lib/transfer/list-filter.js.map +1 -0
  87. package/src/lib/transfer/product-import-results.d.ts +29 -0
  88. package/src/lib/transfer/product-import-results.js +49 -0
  89. package/src/lib/transfer/product-import-results.js.map +1 -0
  90. package/src/lib/transfer/product-import-step.component.d.ts +30 -0
  91. package/src/lib/transfer/product-import-step.component.js +99 -0
  92. package/src/lib/transfer/product-import-step.component.js.map +1 -0
  93. package/src/lib/transfer/product-transfer.d.ts +211 -0
  94. package/src/lib/transfer/product-transfer.js +1594 -0
  95. package/src/lib/transfer/product-transfer.js.map +1 -0
  96. package/src/lib/transfer/products.server.d.ts +44 -0
  97. package/src/lib/transfer/products.server.js +550 -0
  98. package/src/lib/transfer/products.server.js.map +1 -0
  99. package/src/lib/transfer/records-transfer.d.ts +182 -0
  100. package/src/lib/transfer/records-transfer.js +1153 -0
  101. package/src/lib/transfer/records-transfer.js.map +1 -0
  102. package/src/lib/transfer/records.server.d.ts +68 -0
  103. package/src/lib/transfer/records.server.js +455 -0
  104. package/src/lib/transfer/records.server.js.map +1 -0
  105. package/src/lib/transfer/register-transfer-resources.d.ts +17 -0
  106. package/src/lib/transfer/register-transfer-resources.js +171 -0
  107. package/src/lib/transfer/register-transfer-resources.js.map +1 -0
  108. package/src/lib/transfer/register-transfer-ui.d.ts +23 -0
  109. package/src/lib/transfer/register-transfer-ui.js +78 -0
  110. package/src/lib/transfer/register-transfer-ui.js.map +1 -0
  111. package/src/lib/transfer/server-common.d.ts +76 -0
  112. package/src/lib/transfer/server-common.js +165 -0
  113. package/src/lib/transfer/server-common.js.map +1 -0
  114. package/src/lib/transfer/transfer-header-actions.component.d.ts +39 -0
  115. package/src/lib/transfer/transfer-header-actions.component.js +79 -0
  116. package/src/lib/transfer/transfer-header-actions.component.js.map +1 -0
  117. package/src/lib/transfer/transfer-keys.d.ts +31 -0
  118. package/src/lib/transfer/transfer-keys.js +56 -0
  119. package/src/lib/transfer/transfer-keys.js.map +1 -0
  120. package/src/lib/model/commerce-io.d.ts +0 -36
  121. package/src/lib/model/commerce-io.js +0 -245
  122. package/src/lib/model/commerce-io.js.map +0 -1
  123. package/src/lib/server/contact-refund.d.ts +0 -117
  124. package/src/lib/server/contact-refund.js +0 -196
  125. package/src/lib/server/contact-refund.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/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'\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 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","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","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('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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"}
@@ -29,6 +29,8 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
29
29
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
30
30
  import { GIFT_CARD_SEARCH_TOKENS_PATH } from "../../model/gift-card-search.js";
31
31
  import { EntitlementGatedCard } from "./entitlement-gate.component.js";
32
+ import TransferHeaderActions from "../../transfer/transfer-header-actions.component.js";
33
+ import { COMMERCE_GIFT_CARDS_TRANSFER } from "../../transfer/transfer-keys.js";
32
34
  const usd = (cents)=>`$${((cents != null ? cents : 0) / 100).toFixed(2)}`;
33
35
  /**
34
36
  * The card list's one query (AGL-3321): newest first, and the search an
@@ -259,6 +261,12 @@ const giftCardsHelp = pluginDocsHelp('commerce', {
259
261
  upsell: 'Gift cards let shoppers buy store credit for someone else, and let ' + 'you issue credit by hand for a goodwill gesture or a service ' + 'recovery. Balances apply automatically at checkout.',
260
262
  children: /*#__PURE__*/ _jsx(CardDisplay, {
261
263
  header: 'Gift cards',
264
+ HeaderProps: {
265
+ action: /*#__PURE__*/ _jsx(TransferHeaderActions, {
266
+ resource: COMMERCE_GIFT_CARDS_TRANSFER,
267
+ hostId: hostId
268
+ })
269
+ },
262
270
  help: giftCardsHelp,
263
271
  contentGutterX: true,
264
272
  contentGutterY: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/gift-cards-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 { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListQueryNotices,\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n count,\n getAggregateFromServer,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { GIFT_CARD_SEARCH_TOKENS_PATH } from '../../model/gift-card-search'\nimport { EntitlementGatedCard } from './entitlement-gate.component'\n\nexport interface GiftCardsCardProps {\n hostId: string\n}\n\nconst usd = (cents: number | undefined) => `$${((cents ?? 0) / 100).toFixed(2)}`\n\n/**\n * The card list's one query (AGL-3321): newest first, and the search an\n * `array-contains` on the tokens every card writer stamps from its code and\n * its recipient's address (`giftCardSearchTokens`), so a card on page forty\n * is found exactly as one on page one is. One composite:\n * `(searchTokens CONTAINS, createdAtMs DESC)`.\n */\nexport const GIFT_CARD_LIST_QUERY: ListQueryDeclaration = {\n fields: [],\n sorts: [{ path: 'createdAtMs', direction: 'desc' }],\n search: { tokensPath: GIFT_CARD_SEARCH_TOKENS_PATH },\n}\n\nconst giftCardsHelp = pluginDocsHelp('commerce', {\n anchor: '#gift-cards',\n title: 'Gift cards',\n excerpt:\n 'Every gift card your store has issued, what is left on it, and what ' +\n 'that adds up to in outstanding store credit.',\n})\n\n/**\n * Gift cards & store credit (AGL-2226).\n *\n * `hosts/{hostId}/giftCards` has existed since AGL-322 with no console at\n * either end: `billing-webhook.ts` mints a card when one is bought,\n * `cart-checkout.ts` decrements it at redemption, and nothing in the product\n * ever showed the merchant a code, a balance, or the total they owe.\n *\n * The total is the point. Outstanding gift-card balance is a real liability\n * on the merchant's books, and it was invisible — which is also why AGL-1767\n * (a redemption resurrecting a deleted card at a NEGATIVE balance,\n * understating that liability) could not have been noticed from the console.\n * The sum below deliberately floors each card at zero so a stray negative row\n * cannot flatter the number the way that bug did.\n *\n * Reads are client-side because the Firestore host catch-all already grants\n * them and the merchant wants the list live. The two operations that MOVE\n * MONEY — issue and void — go through `commerce/gift-cards`, because that same\n * catch-all would otherwise let any site admin write their own `balanceCents`\n * and redeem it at checkout.\n */\nexport function GiftCardsCard(props: GiftCardsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const [search, setSearch] = useState('')\n /*\n * Ordered by the server and paged by it, and the search is the QUERY's\n * (AGL-3321): it used to match the code and the address over the page in\n * front of the merchant, and said so.\n */\n const {\n rows: cards,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: collection(firestore, 'hosts', hostId, 'giftCards'),\n declaration: GIFT_CARD_LIST_QUERY,\n request: { clauses: [], search: search.trim() ? [search.trim()] : [] },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const [amount, setAmount] = useState('')\n const [recipient, setRecipient] = useState('')\n const [busy, setBusy] = useState(false)\n\n\n /*==========================================\n * OUTSTANDING LIABILITY IS A SERVER AGGREGATE, not a page total.\n *\n * These two numbers are what the merchant owes their customers, and they\n * were `reduce`d over whatever the window happened to hold — 300 cards\n * chosen by code. A shop with 400 gift cards under-reported its own\n * liability and had no way to know: the chip does not say \"of the cards on\n * screen\", and there is no reading of \"$X outstanding\" under which a\n * sample is the answer.\n *\n * `where('balanceCents','>',0)` IS the per-card floor the reduce applied\n * by hand. A negative balance is a data fault (AGL-1767), not credit the\n * merchant gets back; excluding it contributes zero, exactly as flooring\n * it did. Zero-balance cards contribute nothing either way and are what\n * `liveCount` was already counting.\n *\n * An aggregation is billed per 1,000 documents scanned rather than per\n * document, so this is cheaper than the read it replaces as well as\n * correct.\n *=========================================*/\n const [totalsEpoch, setTotalsEpoch] = useState(0)\n const [totals, setTotals] = useState<{\n outstandingCents: number\n liveCount: number\n issuedCount: number\n } | null>(null)\n useEffect(() => {\n let active = true\n const cardsRef = collection(firestore, 'hosts', hostId, 'giftCards')\n void Promise.all([\n getAggregateFromServer(query(cardsRef, where('balanceCents', '>', 0)), {\n outstandingCents: sum('balanceCents'),\n liveCount: count(),\n }),\n // \"Issued\" is every card ever, so this one carries no predicate.\n getAggregateFromServer(cardsRef, { issuedCount: count() }),\n ])\n .then(([balances, issued]) => {\n if (!active) return\n setTotals({\n outstandingCents: Number(balances.data().outstandingCents ?? 0),\n liveCount: Number(balances.data().liveCount ?? 0),\n issuedCount: Number(issued.data().issuedCount ?? 0),\n })\n })\n .catch(() => {\n // Held at null rather than zeroed. \"$0.00 outstanding\" is a\n // confident wrong number in the flattering direction; an absent one\n // renders as unknown and cannot be mistaken for a settled book.\n if (active) setTotals(null)\n })\n return () => {\n active = false\n }\n }, [firestore, hostId, totalsEpoch])\n\n const post = useCallback(\n async (payload: Record<string, unknown>, success: (body: any) => string) => {\n setBusy(true)\n try {\n const response = await authorizedFetch(\n user,\n '/api/commerce/gift-cards',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, ...payload }),\n },\n )\n const body = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(body?.error ?? 'Gift card action failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(success(body), { variant: 'success', persist: false })\n // Issuing, voiding or adjusting moves the liability. The list is a\n // live listener and updates itself; the aggregate is a one-shot read\n // and would otherwise keep reporting the balance from before.\n setTotalsEpoch((epoch) => epoch + 1)\n } finally {\n setBusy(false)\n }\n },\n [user, hostId, enqueueSnackbar],\n )\n\n const handleIssue = useCallback(async () => {\n const dollars = Number(amount)\n if (!Number.isFinite(dollars) || dollars <= 0) {\n return void enqueueSnackbar('Enter an amount above zero', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n await post(\n {\n action: 'issue',\n amountCents: Math.round(dollars * 100),\n recipientEmail: recipient.trim(),\n },\n (body) =>\n body.emailed\n ? `Issued ${body.code} — emailed to ${recipient.trim()}`\n : `Issued ${body.code}`,\n )\n setAmount('')\n setRecipient('')\n }, [amount, recipient, post, enqueueSnackbar])\n\n const handleVoid = useCallback(\n async (card: any) => {\n // This provider REJECTS on dismissal rather than resolving false, so\n // the `.then/.catch` pair is the read — awaiting the promise bare would\n // throw out of the handler on a cancel.\n const confirmed = await confirm({\n title: `Void ${card.$id}?`,\n description:\n `This zeroes the ${usd(card.balanceCents)} left on the card. The ` +\n 'holder will no longer be able to redeem it, and it cannot be undone.',\n confirmationText: 'Void gift card',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await post({ action: 'void', code: card.$id }, () => `${card.$id} voided`)\n },\n [confirm, post],\n )\n\n // A card frozen by a fraud signal on its purchase (AGL-3363).\n const handleRelease = useCallback(\n async (card: any) => {\n const confirmed = await confirm({\n title: `Release ${card.$id}?`,\n description:\n 'The payment that bought this card was questioned by the card ' +\n 'issuer, held for review, or disputed. Release it only if you are ' +\n 'satisfied the payment is genuine: the holder can then spend the ' +\n `${usd(card.balanceCents)} left on it.`,\n confirmationText: 'Release gift card',\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await post({ action: 'release', code: card.$id }, () => `${card.$id} released`)\n },\n [confirm, post],\n )\n\n return (\n <EntitlementGatedCard\n help={giftCardsHelp}\n hostId={hostId}\n feature=\"giftCards\"\n header={'Gift cards'}\n upsell={\n 'Gift cards let shoppers buy store credit for someone else, and let ' +\n 'you issue credit by hand for a goodwill gesture or a service ' +\n 'recovery. Balances apply automatically at checkout.'\n }\n >\n <CardDisplay\n header={'Gift cards'}\n help={giftCardsHelp}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n <Chip\n size=\"small\"\n color={totals?.outstandingCents ? 'warning' : 'default'}\n label={\n totals\n ? `${usd(totals.outstandingCents)} outstanding`\n : 'Outstanding unavailable'\n }\n />\n {totals ? (\n <Chip\n size=\"small\"\n label={`${totals.liveCount} with a balance`}\n />\n ) : null}\n {totals ? (\n <Chip size=\"small\" label={`${totals.issuedCount} issued`} />\n ) : null}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Outstanding balance is store credit your customers have already ' +\n 'paid for and not yet spent — it is a liability, not revenue.'}\n </Typography>\n\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n <TextField\n label=\"Issue amount (USD)\"\n value={amount}\n onChange={(event) => setAmount(event.target.value)}\n size=\"small\"\n type=\"number\"\n sx={{ maxWidth: 180 }}\n />\n <TextField\n label=\"Email it to (optional)\"\n value={recipient}\n onChange={(event) => setRecipient(event.target.value)}\n size=\"small\"\n sx={{ minWidth: 240 }}\n />\n <Button\n variant=\"contained\"\n size=\"small\"\n disabled={busy || !amount}\n onClick={handleIssue}\n >\n {'Issue card'}\n </Button>\n </Stack>\n\n <TextField\n label=\"Search by code or email\"\n value={search}\n onChange={(event) => {\n setSearch(event.target.value)\n setPage(0)\n }}\n size=\"small\"\n sx={{ maxWidth: 320 }}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, { fields: GIFT_CARD_LIST_QUERY.fields })}\n notices={plan.notices}\n />\n\n {cards.length ? (\n <Stack spacing={1}>\n {cards.map((card: any) => (\n <Stack\n key={card.$id}\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n <Typography variant=\"body2\" sx={{ fontFamily: 'monospace' }}>\n {card.$id}\n </Typography>\n <Chip\n size=\"small\"\n color={\n Number(card.balanceCents ?? 0) > 0 ? 'success' : 'default'\n }\n label={`${usd(card.balanceCents)} of ${usd(card.initialCents)}`}\n />\n {card.recipientEmail ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {card.recipientEmail}\n </Typography>\n ) : null}\n {card.orderId ? null : (\n <Chip size=\"small\" variant=\"outlined\" label={'Issued by hand'} />\n )}\n {!card.voidedAtMs && Number(card.frozenAtMs) > 0 ? (\n <>\n <Chip\n size=\"small\"\n color=\"warning\"\n variant=\"outlined\"\n label={'On hold: payment questioned'}\n />\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => handleRelease(card)}\n >\n {'Release'}\n </Button>\n </>\n ) : null}\n {card.voidedAtMs ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={\n card.voidedReason === 'refund'\n ? 'Voided: order refunded'\n : card.voidedReason === 'dispute-lost'\n ? 'Voided: chargeback lost'\n : 'Voided'\n }\n />\n ) : Number(card.balanceCents ?? 0) > 0 ? (\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy}\n onClick={() => handleVoid(card)}\n >\n {'Void'}\n </Button>\n ) : null}\n </Stack>\n ))}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {search.trim()\n ? 'No gift card matches that code or email.'\n : 'No gift cards yet. Sell a gift-card product, or issue one above.'}\n </Typography>\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={cards.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </Stack>\n </CardDisplay>\n </EntitlementGatedCard>\n )\n}\nGiftCardsCard.displayName = 'GiftCardsCard'\n\nexport default GiftCardsCard\n"],"names":["CardDisplay","useConfirmationContext","ListPagination","ListQueryNotices","listQueryRefusals","useSnackbar","Button","Chip","Stack","TextField","Typography","collection","count","getAggregateFromServer","query","sum","where","useCallback","useEffect","useState","useFirestore","useUser","useListQuery","pluginDocsHelp","authorizedFetch","GIFT_CARD_SEARCH_TOKENS_PATH","EntitlementGatedCard","usd","cents","toFixed","GIFT_CARD_LIST_QUERY","fields","sorts","path","direction","search","tokensPath","giftCardsHelp","anchor","title","excerpt","GiftCardsCard","props","hostId","firestore","data","user","enqueueSnackbar","confirm","setSearch","rows","cards","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","trim","deps","idField","amount","setAmount","recipient","setRecipient","busy","setBusy","totalsEpoch","setTotalsEpoch","totals","setTotals","active","cardsRef","Promise","all","outstandingCents","liveCount","issuedCount","then","balances","issued","Number","catch","post","payload","success","response","method","headers","body","JSON","stringify","json","ok","error","variant","allowDuplicate","persist","epoch","handleIssue","dollars","isFinite","action","amountCents","Math","round","recipientEmail","emailed","code","handleVoid","card","confirmed","$id","description","balanceCents","confirmationText","confirmationButtonProps","color","handleRelease","help","feature","header","upsell","contentGutterX","contentGutterY","spacing","useFlexGap","sx","flexWrap","size","label","value","onChange","event","target","type","maxWidth","minWidth","disabled","onClick","refused","notices","length","map","alignItems","fontFamily","initialCents","orderId","voidedAtMs","frozenAtMs","voidedReason","rowCount","onPageChange","onPageSizeChange","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/gift-cards-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 { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport {\n ListQueryNotices,\n listQueryRefusals,\n} from '@aglyn/shared-ui-jsx/components/list-query-notices.component'\nimport type { ListQueryDeclaration } from '@aglyn/shared-ui-jsx/const/list-query-plan'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Chip,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport {\n collection,\n count,\n getAggregateFromServer,\n query,\n sum,\n where,\n} from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { GIFT_CARD_SEARCH_TOKENS_PATH } from '../../model/gift-card-search'\nimport { EntitlementGatedCard } from './entitlement-gate.component'\nimport TransferHeaderActions from '../../transfer/transfer-header-actions.component'\nimport { COMMERCE_GIFT_CARDS_TRANSFER } from '../../transfer/transfer-keys'\n\nexport interface GiftCardsCardProps {\n hostId: string\n}\n\nconst usd = (cents: number | undefined) => `$${((cents ?? 0) / 100).toFixed(2)}`\n\n/**\n * The card list's one query (AGL-3321): newest first, and the search an\n * `array-contains` on the tokens every card writer stamps from its code and\n * its recipient's address (`giftCardSearchTokens`), so a card on page forty\n * is found exactly as one on page one is. One composite:\n * `(searchTokens CONTAINS, createdAtMs DESC)`.\n */\nexport const GIFT_CARD_LIST_QUERY: ListQueryDeclaration = {\n fields: [],\n sorts: [{ path: 'createdAtMs', direction: 'desc' }],\n search: { tokensPath: GIFT_CARD_SEARCH_TOKENS_PATH },\n}\n\nconst giftCardsHelp = pluginDocsHelp('commerce', {\n anchor: '#gift-cards',\n title: 'Gift cards',\n excerpt:\n 'Every gift card your store has issued, what is left on it, and what ' +\n 'that adds up to in outstanding store credit.',\n})\n\n/**\n * Gift cards & store credit (AGL-2226).\n *\n * `hosts/{hostId}/giftCards` has existed since AGL-322 with no console at\n * either end: `billing-webhook.ts` mints a card when one is bought,\n * `cart-checkout.ts` decrements it at redemption, and nothing in the product\n * ever showed the merchant a code, a balance, or the total they owe.\n *\n * The total is the point. Outstanding gift-card balance is a real liability\n * on the merchant's books, and it was invisible — which is also why AGL-1767\n * (a redemption resurrecting a deleted card at a NEGATIVE balance,\n * understating that liability) could not have been noticed from the console.\n * The sum below deliberately floors each card at zero so a stray negative row\n * cannot flatter the number the way that bug did.\n *\n * Reads are client-side because the Firestore host catch-all already grants\n * them and the merchant wants the list live. The two operations that MOVE\n * MONEY — issue and void — go through `commerce/gift-cards`, because that same\n * catch-all would otherwise let any site admin write their own `balanceCents`\n * and redeem it at checkout.\n */\nexport function GiftCardsCard(props: GiftCardsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const [search, setSearch] = useState('')\n /*\n * Ordered by the server and paged by it, and the search is the QUERY's\n * (AGL-3321): it used to match the code and the address over the page in\n * front of the merchant, and said so.\n */\n const {\n rows: cards,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n } = useListQuery<any>({\n collection: collection(firestore, 'hosts', hostId, 'giftCards'),\n declaration: GIFT_CARD_LIST_QUERY,\n request: { clauses: [], search: search.trim() ? [search.trim()] : [] },\n deps: [firestore, hostId],\n idField: '$id',\n })\n const [amount, setAmount] = useState('')\n const [recipient, setRecipient] = useState('')\n const [busy, setBusy] = useState(false)\n\n\n /*==========================================\n * OUTSTANDING LIABILITY IS A SERVER AGGREGATE, not a page total.\n *\n * These two numbers are what the merchant owes their customers, and they\n * were `reduce`d over whatever the window happened to hold — 300 cards\n * chosen by code. A shop with 400 gift cards under-reported its own\n * liability and had no way to know: the chip does not say \"of the cards on\n * screen\", and there is no reading of \"$X outstanding\" under which a\n * sample is the answer.\n *\n * `where('balanceCents','>',0)` IS the per-card floor the reduce applied\n * by hand. A negative balance is a data fault (AGL-1767), not credit the\n * merchant gets back; excluding it contributes zero, exactly as flooring\n * it did. Zero-balance cards contribute nothing either way and are what\n * `liveCount` was already counting.\n *\n * An aggregation is billed per 1,000 documents scanned rather than per\n * document, so this is cheaper than the read it replaces as well as\n * correct.\n *=========================================*/\n const [totalsEpoch, setTotalsEpoch] = useState(0)\n const [totals, setTotals] = useState<{\n outstandingCents: number\n liveCount: number\n issuedCount: number\n } | null>(null)\n useEffect(() => {\n let active = true\n const cardsRef = collection(firestore, 'hosts', hostId, 'giftCards')\n void Promise.all([\n getAggregateFromServer(query(cardsRef, where('balanceCents', '>', 0)), {\n outstandingCents: sum('balanceCents'),\n liveCount: count(),\n }),\n // \"Issued\" is every card ever, so this one carries no predicate.\n getAggregateFromServer(cardsRef, { issuedCount: count() }),\n ])\n .then(([balances, issued]) => {\n if (!active) return\n setTotals({\n outstandingCents: Number(balances.data().outstandingCents ?? 0),\n liveCount: Number(balances.data().liveCount ?? 0),\n issuedCount: Number(issued.data().issuedCount ?? 0),\n })\n })\n .catch(() => {\n // Held at null rather than zeroed. \"$0.00 outstanding\" is a\n // confident wrong number in the flattering direction; an absent one\n // renders as unknown and cannot be mistaken for a settled book.\n if (active) setTotals(null)\n })\n return () => {\n active = false\n }\n }, [firestore, hostId, totalsEpoch])\n\n const post = useCallback(\n async (payload: Record<string, unknown>, success: (body: any) => string) => {\n setBusy(true)\n try {\n const response = await authorizedFetch(\n user,\n '/api/commerce/gift-cards',\n {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ hostId, ...payload }),\n },\n )\n const body = await response.json().catch(() => ({}))\n if (!response.ok) {\n return void enqueueSnackbar(body?.error ?? 'Gift card action failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n enqueueSnackbar(success(body), { variant: 'success', persist: false })\n // Issuing, voiding or adjusting moves the liability. The list is a\n // live listener and updates itself; the aggregate is a one-shot read\n // and would otherwise keep reporting the balance from before.\n setTotalsEpoch((epoch) => epoch + 1)\n } finally {\n setBusy(false)\n }\n },\n [user, hostId, enqueueSnackbar],\n )\n\n const handleIssue = useCallback(async () => {\n const dollars = Number(amount)\n if (!Number.isFinite(dollars) || dollars <= 0) {\n return void enqueueSnackbar('Enter an amount above zero', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n await post(\n {\n action: 'issue',\n amountCents: Math.round(dollars * 100),\n recipientEmail: recipient.trim(),\n },\n (body) =>\n body.emailed\n ? `Issued ${body.code} — emailed to ${recipient.trim()}`\n : `Issued ${body.code}`,\n )\n setAmount('')\n setRecipient('')\n }, [amount, recipient, post, enqueueSnackbar])\n\n const handleVoid = useCallback(\n async (card: any) => {\n // This provider REJECTS on dismissal rather than resolving false, so\n // the `.then/.catch` pair is the read — awaiting the promise bare would\n // throw out of the handler on a cancel.\n const confirmed = await confirm({\n title: `Void ${card.$id}?`,\n description:\n `This zeroes the ${usd(card.balanceCents)} left on the card. The ` +\n 'holder will no longer be able to redeem it, and it cannot be undone.',\n confirmationText: 'Void gift card',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await post({ action: 'void', code: card.$id }, () => `${card.$id} voided`)\n },\n [confirm, post],\n )\n\n // A card frozen by a fraud signal on its purchase (AGL-3363).\n const handleRelease = useCallback(\n async (card: any) => {\n const confirmed = await confirm({\n title: `Release ${card.$id}?`,\n description:\n 'The payment that bought this card was questioned by the card ' +\n 'issuer, held for review, or disputed. Release it only if you are ' +\n 'satisfied the payment is genuine: the holder can then spend the ' +\n `${usd(card.balanceCents)} left on it.`,\n confirmationText: 'Release gift card',\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await post({ action: 'release', code: card.$id }, () => `${card.$id} released`)\n },\n [confirm, post],\n )\n\n return (\n <EntitlementGatedCard\n help={giftCardsHelp}\n hostId={hostId}\n feature=\"giftCards\"\n header={'Gift cards'}\n upsell={\n 'Gift cards let shoppers buy store credit for someone else, and let ' +\n 'you issue credit by hand for a goodwill gesture or a service ' +\n 'recovery. Balances apply automatically at checkout.'\n }\n >\n <CardDisplay\n header={'Gift cards'}\n HeaderProps={{\n action: (\n <TransferHeaderActions resource={COMMERCE_GIFT_CARDS_TRANSFER} hostId={hostId} />\n ),\n }}\n help={giftCardsHelp}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n <Chip\n size=\"small\"\n color={totals?.outstandingCents ? 'warning' : 'default'}\n label={\n totals\n ? `${usd(totals.outstandingCents)} outstanding`\n : 'Outstanding unavailable'\n }\n />\n {totals ? (\n <Chip\n size=\"small\"\n label={`${totals.liveCount} with a balance`}\n />\n ) : null}\n {totals ? (\n <Chip size=\"small\" label={`${totals.issuedCount} issued`} />\n ) : null}\n </Stack>\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Outstanding balance is store credit your customers have already ' +\n 'paid for and not yet spent — it is a liability, not revenue.'}\n </Typography>\n\n <Stack direction=\"row\" spacing={1} useFlexGap sx={{ flexWrap: 'wrap' }}>\n <TextField\n label=\"Issue amount (USD)\"\n value={amount}\n onChange={(event) => setAmount(event.target.value)}\n size=\"small\"\n type=\"number\"\n sx={{ maxWidth: 180 }}\n />\n <TextField\n label=\"Email it to (optional)\"\n value={recipient}\n onChange={(event) => setRecipient(event.target.value)}\n size=\"small\"\n sx={{ minWidth: 240 }}\n />\n <Button\n variant=\"contained\"\n size=\"small\"\n disabled={busy || !amount}\n onClick={handleIssue}\n >\n {'Issue card'}\n </Button>\n </Stack>\n\n <TextField\n label=\"Search by code or email\"\n value={search}\n onChange={(event) => {\n setSearch(event.target.value)\n setPage(0)\n }}\n size=\"small\"\n sx={{ maxWidth: 320 }}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, { fields: GIFT_CARD_LIST_QUERY.fields })}\n notices={plan.notices}\n />\n\n {cards.length ? (\n <Stack spacing={1}>\n {cards.map((card: any) => (\n <Stack\n key={card.$id}\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ flexWrap: 'wrap', alignItems: 'center' }}\n >\n <Typography variant=\"body2\" sx={{ fontFamily: 'monospace' }}>\n {card.$id}\n </Typography>\n <Chip\n size=\"small\"\n color={\n Number(card.balanceCents ?? 0) > 0 ? 'success' : 'default'\n }\n label={`${usd(card.balanceCents)} of ${usd(card.initialCents)}`}\n />\n {card.recipientEmail ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {card.recipientEmail}\n </Typography>\n ) : null}\n {card.orderId ? null : (\n <Chip size=\"small\" variant=\"outlined\" label={'Issued by hand'} />\n )}\n {!card.voidedAtMs && Number(card.frozenAtMs) > 0 ? (\n <>\n <Chip\n size=\"small\"\n color=\"warning\"\n variant=\"outlined\"\n label={'On hold: payment questioned'}\n />\n <Button\n size=\"small\"\n disabled={busy}\n onClick={() => handleRelease(card)}\n >\n {'Release'}\n </Button>\n </>\n ) : null}\n {card.voidedAtMs ? (\n <Chip\n size=\"small\"\n variant=\"outlined\"\n label={\n card.voidedReason === 'refund'\n ? 'Voided: order refunded'\n : card.voidedReason === 'dispute-lost'\n ? 'Voided: chargeback lost'\n : 'Voided'\n }\n />\n ) : Number(card.balanceCents ?? 0) > 0 ? (\n <Button\n size=\"small\"\n color=\"error\"\n disabled={busy}\n onClick={() => handleVoid(card)}\n >\n {'Void'}\n </Button>\n ) : null}\n </Stack>\n ))}\n </Stack>\n ) : (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {search.trim()\n ? 'No gift card matches that code or email.'\n : 'No gift cards yet. Sell a gift-card product, or issue one above.'}\n </Typography>\n )}\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={cards.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n </Stack>\n </CardDisplay>\n </EntitlementGatedCard>\n )\n}\nGiftCardsCard.displayName = 'GiftCardsCard'\n\nexport default GiftCardsCard\n"],"names":["CardDisplay","useConfirmationContext","ListPagination","ListQueryNotices","listQueryRefusals","useSnackbar","Button","Chip","Stack","TextField","Typography","collection","count","getAggregateFromServer","query","sum","where","useCallback","useEffect","useState","useFirestore","useUser","useListQuery","pluginDocsHelp","authorizedFetch","GIFT_CARD_SEARCH_TOKENS_PATH","EntitlementGatedCard","TransferHeaderActions","COMMERCE_GIFT_CARDS_TRANSFER","usd","cents","toFixed","GIFT_CARD_LIST_QUERY","fields","sorts","path","direction","search","tokensPath","giftCardsHelp","anchor","title","excerpt","GiftCardsCard","props","hostId","firestore","data","user","enqueueSnackbar","confirm","setSearch","rows","cards","hasMore","page","setPage","pageSize","setPageSize","plan","declaration","request","clauses","trim","deps","idField","amount","setAmount","recipient","setRecipient","busy","setBusy","totalsEpoch","setTotalsEpoch","totals","setTotals","active","cardsRef","Promise","all","outstandingCents","liveCount","issuedCount","then","balances","issued","Number","catch","post","payload","success","response","method","headers","body","JSON","stringify","json","ok","error","variant","allowDuplicate","persist","epoch","handleIssue","dollars","isFinite","action","amountCents","Math","round","recipientEmail","emailed","code","handleVoid","card","confirmed","$id","description","balanceCents","confirmationText","confirmationButtonProps","color","handleRelease","help","feature","header","upsell","HeaderProps","resource","contentGutterX","contentGutterY","spacing","useFlexGap","sx","flexWrap","size","label","value","onChange","event","target","type","maxWidth","minWidth","disabled","onClick","refused","notices","length","map","alignItems","fontFamily","initialCents","orderId","voidedAtMs","frozenAtMs","voidedReason","rowCount","onPageChange","onPageSizeChange","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,SAASA,WAAW,EAAEC,sBAAsB,QAAQ,uBAAsB;AAC1E,SAASC,cAAc,QAAQ,4DAA2D;AAC1F,SACEC,gBAAgB,EAChBC,iBAAiB,QACZ,+DAA8D;AAErE,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,MAAM,EACNC,IAAI,EACJC,KAAK,EACLC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SACEC,UAAU,EACVC,KAAK,EACLC,sBAAsB,EACtBC,KAAK,EACLC,GAAG,EACHC,KAAK,QACA,qBAAoB;AAC3B,SAASC,WAAW,EAAEC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AACxD,SAASC,YAAY,EAAEC,OAAO,QAAQ,iCAAgC;AACtE,SAASC,YAAY,QAAQ,sDAAqD;AAClF,SAASC,cAAc,QAAQ,eAAc;AAC7C,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,4BAA4B,QAAQ,kCAA8B;AAC3E,SAASC,oBAAoB,QAAQ,kCAA8B;AACnE,OAAOC,2BAA2B,sDAAkD;AACpF,SAASC,4BAA4B,QAAQ,kCAA8B;AAM3E,MAAMC,MAAM,CAACC,QAA8B,CAAC,CAAC,EAAE,AAAC,CAAA,CAACA,gBAAAA,QAAS,KAAK,GAAE,EAAGC,OAAO,CAAC,IAAI;AAEhF;;;;;;CAMC,GACD,OAAO,MAAMC,uBAA6C;IACxDC,QAAQ,EAAE;IACVC,OAAO;QAAC;YAAEC,MAAM;YAAeC,WAAW;QAAO;KAAE;IACnDC,QAAQ;QAAEC,YAAYb;IAA6B;AACrD,EAAC;AAED,MAAMc,gBAAgBhB,eAAe,YAAY;IAC/CiB,QAAQ;IACRC,OAAO;IACPC,SACE,yEACA;AACJ;AAEA;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC,cAAcC,KAAyB;IACrD,MAAM,EAAEC,MAAM,EAAE,GAAGD;IACnB,MAAME,YAAY1B;IAClB,MAAM,EAAE2B,MAAMC,IAAI,EAAE,GAAG3B;IACvB,MAAM,EAAE4B,eAAe,EAAE,GAAG5C;IAC5B,MAAM,EAAE6C,OAAO,EAAE,GAAGjD;IACpB,MAAM,CAACoC,QAAQc,UAAU,GAAGhC,SAAS;IACrC;;;;GAIC,GACD,MAAM,EACJiC,MAAMC,KAAK,EACXC,OAAO,EACPC,IAAI,EACJC,OAAO,EACPC,QAAQ,EACRC,WAAW,EACXC,IAAI,EACL,GAAGrC,aAAkB;QACpBX,YAAYA,WAAWmC,WAAW,SAASD,QAAQ;QACnDe,aAAa5B;QACb6B,SAAS;YAAEC,SAAS,EAAE;YAAEzB,QAAQA,OAAO0B,IAAI,KAAK;gBAAC1B,OAAO0B,IAAI;aAAG,GAAG,EAAE;QAAC;QACrEC,MAAM;YAAClB;YAAWD;SAAO;QACzBoB,SAAS;IACX;IACA,MAAM,CAACC,QAAQC,UAAU,GAAGhD,SAAS;IACrC,MAAM,CAACiD,WAAWC,aAAa,GAAGlD,SAAS;IAC3C,MAAM,CAACmD,MAAMC,QAAQ,GAAGpD,SAAS;IAGjC;;;;;;;;;;;;;;;;;;;6CAmB2C,GAC3C,MAAM,CAACqD,aAAaC,eAAe,GAAGtD,SAAS;IAC/C,MAAM,CAACuD,QAAQC,UAAU,GAAGxD,SAIlB;IACVD,UAAU;QACR,IAAI0D,SAAS;QACb,MAAMC,WAAWlE,WAAWmC,WAAW,SAASD,QAAQ;QACxD,KAAKiC,QAAQC,GAAG,CAAC;YACflE,uBAAuBC,MAAM+D,UAAU7D,MAAM,gBAAgB,KAAK,KAAK;gBACrEgE,kBAAkBjE,IAAI;gBACtBkE,WAAWrE;YACb;YACA,iEAAiE;YACjEC,uBAAuBgE,UAAU;gBAAEK,aAAatE;YAAQ;SACzD,EACEuE,IAAI,CAAC,CAAC,CAACC,UAAUC,OAAO;gBAGID,iCACPA,0BACEC;YAJtB,IAAI,CAACT,QAAQ;YACbD,UAAU;gBACRK,kBAAkBM,QAAOF,kCAAAA,SAASrC,IAAI,GAAGiC,gBAAgB,YAAhCI,kCAAoC;gBAC7DH,WAAWK,QAAOF,2BAAAA,SAASrC,IAAI,GAAGkC,SAAS,YAAzBG,2BAA6B;gBAC/CF,aAAaI,QAAOD,2BAAAA,OAAOtC,IAAI,GAAGmC,WAAW,YAAzBG,2BAA6B;YACnD;QACF,GACCE,KAAK,CAAC;YACL,4DAA4D;YAC5D,oEAAoE;YACpE,gEAAgE;YAChE,IAAIX,QAAQD,UAAU;QACxB;QACF,OAAO;YACLC,SAAS;QACX;IACF,GAAG;QAAC9B;QAAWD;QAAQ2B;KAAY;IAEnC,MAAMgB,OAAOvE,YACX,OAAOwE,SAAkCC;QACvCnB,QAAQ;QACR,IAAI;YACF,MAAMoB,WAAW,MAAMnE,gBACrBwB,MACA,4BACA;gBACE4C,QAAQ;gBACRC,SAAS;oBAAE,gBAAgB;gBAAmB;gBAC9CC,MAAMC,KAAKC,SAAS,CAAC;oBAAEnD;mBAAW4C;YACpC;YAEF,MAAMK,OAAO,MAAMH,SAASM,IAAI,GAAGV,KAAK,CAAC,IAAO,CAAA,CAAC,CAAA;YACjD,IAAI,CAACI,SAASO,EAAE,EAAE;;gBAChB,OAAO,KAAKjD,wBAAgB6C,wBAAAA,KAAMK,KAAK,mBAAI,2BAA2B;oBACpEC,SAAS;oBACTC,gBAAgB;gBAClB;YACF;YACApD,gBAAgByC,QAAQI,OAAO;gBAAEM,SAAS;gBAAWE,SAAS;YAAM;YACpE,mEAAmE;YACnE,qEAAqE;YACrE,8DAA8D;YAC9D7B,eAAe,CAAC8B,QAAUA,QAAQ;QACpC,SAAU;YACRhC,QAAQ;QACV;IACF,GACA;QAACvB;QAAMH;QAAQI;KAAgB;IAGjC,MAAMuD,cAAcvF,YAAY;QAC9B,MAAMwF,UAAUnB,OAAOpB;QACvB,IAAI,CAACoB,OAAOoB,QAAQ,CAACD,YAAYA,WAAW,GAAG;YAC7C,OAAO,KAAKxD,gBAAgB,8BAA8B;gBACxDmD,SAAS;gBACTC,gBAAgB;YAClB;QACF;QACA,MAAMb,KACJ;YACEmB,QAAQ;YACRC,aAAaC,KAAKC,KAAK,CAACL,UAAU;YAClCM,gBAAgB3C,UAAUL,IAAI;QAChC,GACA,CAAC+B,OACCA,KAAKkB,OAAO,GACR,CAAC,OAAO,EAAElB,KAAKmB,IAAI,CAAC,cAAc,EAAE7C,UAAUL,IAAI,IAAI,GACtD,CAAC,OAAO,EAAE+B,KAAKmB,IAAI,EAAE;QAE7B9C,UAAU;QACVE,aAAa;IACf,GAAG;QAACH;QAAQE;QAAWoB;QAAMvC;KAAgB;IAE7C,MAAMiE,aAAajG,YACjB,OAAOkG;QACL,qEAAqE;QACrE,wEAAwE;QACxE,wCAAwC;QACxC,MAAMC,YAAY,MAAMlE,QAAQ;YAC9BT,OAAO,CAAC,KAAK,EAAE0E,KAAKE,GAAG,CAAC,CAAC,CAAC;YAC1BC,aACE,CAAC,gBAAgB,EAAEzF,IAAIsF,KAAKI,YAAY,EAAE,uBAAuB,CAAC,GAClE;YACFC,kBAAkB;YAClBC,yBAAyB;gBAAEC,OAAO;YAAQ;QAC5C,GACGvC,IAAI,CAAC,IAAM,MACXI,KAAK,CAAC,IAAM;QACf,IAAI,CAAC6B,WAAW;QAChB,MAAM5B,KAAK;YAAEmB,QAAQ;YAAQM,MAAME,KAAKE,GAAG;QAAC,GAAG,IAAM,GAAGF,KAAKE,GAAG,CAAC,OAAO,CAAC;IAC3E,GACA;QAACnE;QAASsC;KAAK;IAGjB,8DAA8D;IAC9D,MAAMmC,gBAAgB1G,YACpB,OAAOkG;QACL,MAAMC,YAAY,MAAMlE,QAAQ;YAC9BT,OAAO,CAAC,QAAQ,EAAE0E,KAAKE,GAAG,CAAC,CAAC,CAAC;YAC7BC,aACE,kEACA,sEACA,qEACA,GAAGzF,IAAIsF,KAAKI,YAAY,EAAE,YAAY,CAAC;YACzCC,kBAAkB;QACpB,GACGrC,IAAI,CAAC,IAAM,MACXI,KAAK,CAAC,IAAM;QACf,IAAI,CAAC6B,WAAW;QAChB,MAAM5B,KAAK;YAAEmB,QAAQ;YAAWM,MAAME,KAAKE,GAAG;QAAC,GAAG,IAAM,GAAGF,KAAKE,GAAG,CAAC,SAAS,CAAC;IAChF,GACA;QAACnE;QAASsC;KAAK;IAGjB,qBACE,KAAC9D;QACCkG,MAAMrF;QACNM,QAAQA;QACRgF,SAAQ;QACRC,QAAQ;QACRC,QACE,wEACA,kEACA;kBAGF,cAAA,KAAC/H;YACC8H,QAAQ;YACRE,aAAa;gBACXrB,sBACE,KAAChF;oBAAsBsG,UAAUrG;oBAA8BiB,QAAQA;;YAE3E;YACA+E,MAAMrF;YACN2F,cAAc;YACdC,cAAc;sBAEd,cAAA,MAAC3H;gBAAM4H,SAAS;;kCACd,MAAC5H;wBAAM4B,WAAU;wBAAMgG,SAAS;wBAAGC,UAAU;wBAACC,IAAI;4BAAEC,UAAU;wBAAO;;0CACnE,KAAChI;gCACCiI,MAAK;gCACLd,OAAOhD,CAAAA,0BAAAA,OAAQM,gBAAgB,IAAG,YAAY;gCAC9CyD,OACE/D,SACI,GAAG7C,IAAI6C,OAAOM,gBAAgB,EAAE,YAAY,CAAC,GAC7C;;4BAGPN,uBACC,KAACnE;gCACCiI,MAAK;gCACLC,OAAO,GAAG/D,OAAOO,SAAS,CAAC,eAAe,CAAC;iCAE3C;4BACHP,uBACC,KAACnE;gCAAKiI,MAAK;gCAAQC,OAAO,GAAG/D,OAAOQ,WAAW,CAAC,OAAO,CAAC;iCACtD;;;kCAEN,KAACxE;wBAAW0F,SAAQ;wBAAUsB,OAAM;kCACjC,qEACC;;kCAGJ,MAAClH;wBAAM4B,WAAU;wBAAMgG,SAAS;wBAAGC,UAAU;wBAACC,IAAI;4BAAEC,UAAU;wBAAO;;0CACnE,KAAC9H;gCACCgI,OAAM;gCACNC,OAAOxE;gCACPyE,UAAU,CAACC,QAAUzE,UAAUyE,MAAMC,MAAM,CAACH,KAAK;gCACjDF,MAAK;gCACLM,MAAK;gCACLR,IAAI;oCAAES,UAAU;gCAAI;;0CAEtB,KAACtI;gCACCgI,OAAM;gCACNC,OAAOtE;gCACPuE,UAAU,CAACC,QAAUvE,aAAauE,MAAMC,MAAM,CAACH,KAAK;gCACpDF,MAAK;gCACLF,IAAI;oCAAEU,UAAU;gCAAI;;0CAEtB,KAAC1I;gCACC8F,SAAQ;gCACRoC,MAAK;gCACLS,UAAU3E,QAAQ,CAACJ;gCACnBgF,SAAS1C;0CAER;;;;kCAIL,KAAC/F;wBACCgI,OAAM;wBACNC,OAAOrG;wBACPsG,UAAU,CAACC;4BACTzF,UAAUyF,MAAMC,MAAM,CAACH,KAAK;4BAC5BlF,QAAQ;wBACV;wBACAgF,MAAK;wBACLF,IAAI;4BAAES,UAAU;wBAAI;;kCAEtB,KAAC5I;wBACCgJ,SAAS/I,kBAAkBuD,KAAKwF,OAAO,EAAE;4BAAElH,QAAQD,qBAAqBC,MAAM;wBAAC;wBAC/EmH,SAASzF,KAAKyF,OAAO;;oBAGtB/F,MAAMgG,MAAM,iBACX,KAAC7I;wBAAM4H,SAAS;kCACb/E,MAAMiG,GAAG,CAAC,CAACnC;gCAcGA,oBAyCAA;iDAtDb,MAAC3G;gCAEC4B,WAAU;gCACVgG,SAAS;gCACTC,UAAU;gCACVC,IAAI;oCAAEC,UAAU;oCAAQgB,YAAY;gCAAS;;kDAE7C,KAAC7I;wCAAW0F,SAAQ;wCAAQkC,IAAI;4CAAEkB,YAAY;wCAAY;kDACvDrC,KAAKE,GAAG;;kDAEX,KAAC9G;wCACCiI,MAAK;wCACLd,OACEpC,QAAO6B,qBAAAA,KAAKI,YAAY,YAAjBJ,qBAAqB,KAAK,IAAI,YAAY;wCAEnDsB,OAAO,GAAG5G,IAAIsF,KAAKI,YAAY,EAAE,IAAI,EAAE1F,IAAIsF,KAAKsC,YAAY,GAAG;;oCAEhEtC,KAAKJ,cAAc,iBAClB,KAACrG;wCAAW0F,SAAQ;wCAAQsB,OAAM;kDAC/BP,KAAKJ,cAAc;yCAEpB;oCACHI,KAAKuC,OAAO,GAAG,qBACd,KAACnJ;wCAAKiI,MAAK;wCAAQpC,SAAQ;wCAAWqC,OAAO;;oCAE9C,CAACtB,KAAKwC,UAAU,IAAIrE,OAAO6B,KAAKyC,UAAU,IAAI,kBAC7C;;0DACE,KAACrJ;gDACCiI,MAAK;gDACLd,OAAM;gDACNtB,SAAQ;gDACRqC,OAAO;;0DAET,KAACnI;gDACCkI,MAAK;gDACLS,UAAU3E;gDACV4E,SAAS,IAAMvB,cAAcR;0DAE5B;;;yCAGH;oCACHA,KAAKwC,UAAU,iBACd,KAACpJ;wCACCiI,MAAK;wCACLpC,SAAQ;wCACRqC,OACEtB,KAAK0C,YAAY,KAAK,WAClB,2BACA1C,KAAK0C,YAAY,KAAK,iBACpB,4BACA;yCAGRvE,QAAO6B,sBAAAA,KAAKI,YAAY,YAAjBJ,sBAAqB,KAAK,kBACnC,KAAC7G;wCACCkI,MAAK;wCACLd,OAAM;wCACNuB,UAAU3E;wCACV4E,SAAS,IAAMhC,WAAWC;kDAEzB;yCAED;;+BA9DCA,KAAKE,GAAG;;uCAmEnB,KAAC3G;wBAAW0F,SAAQ;wBAAQsB,OAAM;kCAC/BrF,OAAO0B,IAAI,KACR,6CACA;;kCAGR,KAAC7D;wBACCqD,MAAMA;wBACNE,UAAUA;wBACVqG,UAAUzG,MAAMgG,MAAM;wBACtB/F,SAASA;wBACTyG,cAAcvG;wBACdwG,kBAAkBtG;;;;;;AAM9B;AACAf,cAAcsH,WAAW,GAAG;AAE5B,eAAetH,cAAa"}
@@ -27,6 +27,8 @@ import { useCallback, useState } from "react";
27
27
  import { useFirestore } from "@aglyn/tenant-feature-instance";
28
28
  import { usePagedCollection } from "@aglyn/tenant-feature-instance";
29
29
  import { PLATFORM_BRAND_NAME, pluginDocsHelp } from "@aglyn/aglyn";
30
+ import TransferHeaderActions from "../../transfer/transfer-header-actions.component.js";
31
+ import { COMMERCE_COUPONS_TRANSFER } from "../../transfer/transfer-keys.js";
30
32
  /**
31
33
  * Coupon codes (AGL-96), extracted from the retired Commerce Starter
32
34
  * card when the products hub landed (AGL-279). Percent-off codes at
@@ -97,6 +99,12 @@ import { PLATFORM_BRAND_NAME, pluginDocsHelp } from "@aglyn/aglyn";
97
99
  ]);
98
100
  return /*#__PURE__*/ _jsxs(CardDisplay, {
99
101
  header: 'Coupons',
102
+ HeaderProps: {
103
+ action: /*#__PURE__*/ _jsx(TransferHeaderActions, {
104
+ resource: COMMERCE_COUPONS_TRANSFER,
105
+ hostId: hostId
106
+ })
107
+ },
100
108
  help: pluginDocsHelp('commerceEndToEnd', {
101
109
  anchor: '#4-what-checkout-does',
102
110
  excerpt: 'Coupons redeemable at your storefront checkout, separate from ' + `any coupon ${PLATFORM_BRAND_NAME} applies to your own subscription.`
@@ -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'\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 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","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","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 { 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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"}
@@ -29,12 +29,15 @@ import { useSnackbar } from "@aglyn/shared-ui-snackstack";
29
29
  import { checkEntitlement, pluginDocsHelp } from "@aglyn/aglyn";
30
30
  import { ORDERS_CUSTOMER_PARAM, ORDERS_ORDER_PARAM } from "../../model/commerce-record-routes.js";
31
31
  import { Alert, AlertTitle, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, MenuItem, Stack, TextField, Tooltip, Typography } from "@mui/material";
32
- import { collection, doc, getDoc, getDocs, limit, orderBy, query, startAfter, where } from "firebase/firestore";
32
+ import { collection, doc, getDoc, limit, orderBy, query, where } from "firebase/firestore";
33
33
  import { useSearchParams } from "next/navigation";
34
34
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
35
35
  import { ceilingedWindow, collectionCeiling, useFirestore, useOrgPlan, useUser } from "@aglyn/tenant-feature-instance";
36
36
  import { useFirestoreCollection } from "@aglyn/tenant-feature-instance";
37
- import { listQueryConstraints, useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
37
+ import { useListQuery } from "@aglyn/tenant-feature-instance/hooks/use-list-query";
38
+ import { useTransferLauncher } from "@aglyn/aglyn/app-utils/transfer-launcher-context";
39
+ import { commerceListFilter } from "../../transfer/list-filter.js";
40
+ import { COMMERCE_ORDERS_TRANSFER } from "../../transfer/transfer-keys.js";
38
41
  import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
39
42
  import { OPEN_DISPUTE_CLAUSE, ORDER_CHANNEL_OPTIONS, ORDER_DISPUTE_OPTIONS, ORDER_LIST_FIELDS, ORDER_LIST_HEADERS, ORDER_LIST_QUERY, ORDER_LIST_SELECT_FIELDS, ORDER_STATUS_OPTIONS, ordersCustomerClause } from "../../constants/orders-list-query.js";
40
43
  import CommerceStatTile from "./commerce-stat-tile.component.js";
@@ -57,11 +60,6 @@ import OrderDetailDialog, { DISPUTE_COLOR } from "./order-detail-dialog.componen
57
60
  * over the list, and share the analytics card's bound.
58
61
  */ const STATS_WINDOW_MS = 60 * 24 * 60 * 60 * 1000;
59
62
  const STATS_ORDER_CEILING = 500;
60
- /**
61
- * Export CSV walks the list's own query, page by page, up to this many
62
- * orders, and says so when the matches run past it.
63
- */ const ORDERS_EXPORT_CEILING = 5000;
64
- const ORDERS_EXPORT_PAGE = 500;
65
63
  /** The columns the grid draws; every other declared field is a hidden filter column. */ const ORDER_VISIBLE_COLUMNS = [
66
64
  'orderLabel',
67
65
  'customerEmail',
@@ -296,65 +294,28 @@ const ORDER_HIDDEN_COLUMNS = hiddenFilterVisibility(ORDER_LIST_FIELDS, ORDER_VIS
296
294
  statsWindow
297
295
  ]);
298
296
  /**
299
- * Export CSV writes what the QUERY matches — every clause and the search
300
- * the grid is showing — not the page on screen. It walks the list's own
301
- * plan in pages of `ORDERS_EXPORT_PAGE` up to `ORDERS_EXPORT_CEILING`, and
302
- * says so when the matches run past it.
303
- */ const [exporting, setExporting] = useState(false);
304
- const handleExportCsv = useCallback(async ()=>{
305
- setExporting(true);
306
- try {
307
- const matched = [];
308
- let cursor = null;
309
- let more = true;
310
- while(more && matched.length < ORDERS_EXPORT_CEILING){
311
- var _docs_;
312
- const take = Math.min(ORDERS_EXPORT_PAGE, ORDERS_EXPORT_CEILING - matched.length);
313
- // One past the page: whether more match is a fact, not a guess.
314
- const snapshot = await getDocs(query(ordersRef, ...listQueryConstraints(plan), ...cursor ? [
315
- startAfter(cursor)
316
- ] : [], limit(take + 1)));
317
- const docs = snapshot.docs.slice(0, take);
318
- for (const entry of docs)matched.push(_extends({}, entry.data(), {
319
- $id: entry.id
320
- }));
321
- more = snapshot.docs.length > take;
322
- cursor = (_docs_ = docs[docs.length - 1]) != null ? _docs_ : null;
323
- if (!cursor) more = false;
324
- }
325
- // The rows themselves are built by the pure model helper (AGL-1747) so
326
- // the column-by-column arithmetic is unit-testable without Firestore.
327
- const csv = CommerceModel.buildOrdersCsv(matched, productNames);
328
- const url = URL.createObjectURL(new Blob([
329
- csv
330
- ], {
331
- type: 'text/csv'
332
- }));
333
- const anchor = document.createElement('a');
334
- anchor.href = url;
335
- anchor.download = 'orders.csv';
336
- anchor.click();
337
- URL.revokeObjectURL(url);
338
- if (more) {
339
- enqueueSnackbar(`Exported the ${ORDERS_EXPORT_CEILING.toLocaleString('en-US')} newest matching orders. ` + 'Narrow the filters to export the rest.', {
340
- variant: 'warning',
341
- allowDuplicate: true
342
- });
297
+ * Export (AGL-3531) opens the console's export dialog on the
298
+ * `commerce.orders` resource: the fields the person picks, for what the
299
+ * QUERY matches — every clause and the search the grid is showing, not the
300
+ * page on screen — read and streamed on the server however many orders
301
+ * match. Orders are exported only; there is no Import.
302
+ */ const transfer = useTransferLauncher();
303
+ const openExport = useCallback(()=>{
304
+ transfer == null ? void 0 : transfer.openExport(_extends({
305
+ resource: COMMERCE_ORDERS_TRANSFER,
306
+ scope: 'host',
307
+ hostId
308
+ }, filtering ? {
309
+ filter: {
310
+ label: 'the orders the list’s filters and search find',
311
+ value: commerceListFilter(plan)
343
312
  }
344
- } catch (error) {
345
- console.error('Orders export failed', error);
346
- enqueueSnackbar('Export failed — try again', {
347
- variant: 'error',
348
- allowDuplicate: true
349
- });
350
- } finally{
351
- setExporting(false);
352
- }
313
+ } : {}));
353
314
  }, [
354
- ordersRef,
355
- plan,
356
- productNames,
357
- enqueueSnackbar
315
+ transfer,
316
+ hostId,
317
+ filtering,
318
+ plan
358
319
  ]);
359
320
  /**
360
321
  * Idempotency key for ONE draft attempt (AGL-1697).
@@ -646,7 +607,7 @@ const ORDER_HIDDEN_COLUMNS = hiddenFilterVisibility(ORDER_LIST_FIELDS, ORDER_VIS
646
607
  * every other route into the dialog needs an order to exist first.
647
608
  *
648
609
  * The filters stay behind deliberately: they belong to a list with
649
- * rows in it. Export CSV likewise — a header-only file is not
610
+ * rows in it. Export orders likewise — a header-only file is not
650
611
  * something a merchant on day one is looking for.
651
612
  */ /*#__PURE__*/ _jsxs(Stack, {
652
613
  spacing: 1,
@@ -731,12 +692,15 @@ const ORDER_HIDDEN_COLUMNS = hiddenFilterVisibility(ORDER_LIST_FIELDS, ORDER_VIS
731
692
  justifyContent: 'flex-end'
732
693
  },
733
694
  children: [
734
- /*#__PURE__*/ _jsx(Button, {
695
+ (transfer == null ? void 0 : transfer.can('export', {
696
+ resource: COMMERCE_ORDERS_TRANSFER,
697
+ scope: 'host',
698
+ hostId
699
+ })) ? /*#__PURE__*/ _jsx(Button, {
735
700
  size: "small",
736
- onClick: ()=>void handleExportCsv(),
737
- disabled: exporting,
738
- children: 'Export CSV'
739
- }),
701
+ onClick: openExport,
702
+ children: 'Export orders'
703
+ }) : null,
740
704
  /*#__PURE__*/ _jsx(Button, {
741
705
  size: "small",
742
706
  variant: "contained",