@aglyn/plugins-commerce 1.0.0-beta.201 → 1.0.0-beta.203

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 (41) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/console/gift-cards-card.component.js +34 -2
  3. package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
  4. package/src/lib/model/commerce-gift-cards.d.ts +30 -0
  5. package/src/lib/model/commerce-gift-cards.js +39 -1
  6. package/src/lib/model/commerce-gift-cards.js.map +1 -1
  7. package/src/lib/server/billing-webhook.js +180 -106
  8. package/src/lib/server/billing-webhook.js.map +1 -1
  9. package/src/lib/server/cart-checkout.js +46 -8
  10. package/src/lib/server/cart-checkout.js.map +1 -1
  11. package/src/lib/server/checkout.js +23 -5
  12. package/src/lib/server/checkout.js.map +1 -1
  13. package/src/lib/server/download.js +12 -1
  14. package/src/lib/server/download.js.map +1 -1
  15. package/src/lib/server/draft-order.js +5 -2
  16. package/src/lib/server/draft-order.js.map +1 -1
  17. package/src/lib/server/feed.js +12 -2
  18. package/src/lib/server/feed.js.map +1 -1
  19. package/src/lib/server/gift-card-risk.d.ts +112 -0
  20. package/src/lib/server/gift-card-risk.js +220 -0
  21. package/src/lib/server/gift-card-risk.js.map +1 -0
  22. package/src/lib/server/gift-cards.js +37 -2
  23. package/src/lib/server/gift-cards.js.map +1 -1
  24. package/src/lib/server/paid-media-delivery.d.ts +26 -4
  25. package/src/lib/server/paid-media-delivery.js +36 -9
  26. package/src/lib/server/paid-media-delivery.js.map +1 -1
  27. package/src/lib/server/recurring-charges.d.ts +44 -0
  28. package/src/lib/server/recurring-charges.js +65 -0
  29. package/src/lib/server/recurring-charges.js.map +1 -0
  30. package/src/lib/server/refund.js +24 -0
  31. package/src/lib/server/refund.js.map +1 -1
  32. package/src/lib/server/reserve.js +14 -5
  33. package/src/lib/server/reserve.js.map +1 -1
  34. package/src/lib/server/stream.js +12 -1
  35. package/src/lib/server/stream.js.map +1 -1
  36. package/src/lib/server/subscription-portal.js +13 -5
  37. package/src/lib/server/subscription-portal.js.map +1 -1
  38. package/src/lib/server/supplier-outbox.js +22 -7
  39. package/src/lib/server/supplier-outbox.js.map +1 -1
  40. package/src/lib/server.js +15 -3
  41. package/src/lib/server.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aglyn/plugins-commerce",
3
- "version": "1.0.0-beta.201",
3
+ "version": "1.0.0-beta.203",
4
4
  "license": "Apache-2.0",
5
5
  "homepage": "https://aglyn.com",
6
6
  "repository": {
@@ -25,18 +25,18 @@
25
25
  "./package.json": "./package.json"
26
26
  },
27
27
  "dependencies": {
28
- "@aglyn/aglyn": "1.0.0-beta.201",
29
- "@aglyn/shared-data-mdi": "1.0.0-beta.201",
30
- "@aglyn/shared-ui-jsx": "1.0.0-beta.201",
31
- "@aglyn/shared-ui-next": "1.0.0-beta.201",
32
- "@aglyn/shared-ui-snackstack": "1.0.0-beta.201",
33
- "@aglyn/shared-util-email": "1.0.0-beta.201",
34
- "@aglyn/shared-util-http": "1.0.0-beta.201",
35
- "@aglyn/shared-util-timestamp": "1.0.0-beta.201",
36
- "@aglyn/shared-util-tools": "1.0.0-beta.201",
37
- "@aglyn/tenant-data-admin": "1.0.0-beta.201",
38
- "@aglyn/tenant-feature-instance": "1.0.0-beta.201",
39
- "@aglyn/tenant-runtime": "1.0.0-beta.201",
28
+ "@aglyn/aglyn": "1.0.0-beta.203",
29
+ "@aglyn/shared-data-mdi": "1.0.0-beta.203",
30
+ "@aglyn/shared-ui-jsx": "1.0.0-beta.203",
31
+ "@aglyn/shared-ui-next": "1.0.0-beta.203",
32
+ "@aglyn/shared-ui-snackstack": "1.0.0-beta.203",
33
+ "@aglyn/shared-util-email": "1.0.0-beta.203",
34
+ "@aglyn/shared-util-http": "1.0.0-beta.203",
35
+ "@aglyn/shared-util-timestamp": "1.0.0-beta.203",
36
+ "@aglyn/shared-util-tools": "1.0.0-beta.203",
37
+ "@aglyn/tenant-data-admin": "1.0.0-beta.203",
38
+ "@aglyn/tenant-feature-instance": "1.0.0-beta.203",
39
+ "@aglyn/tenant-runtime": "1.0.0-beta.203",
40
40
  "@stripe/react-stripe-js": "^3.10.0",
41
41
  "@stripe/stripe-js": "^5.10.0",
42
42
  "@swc/helpers": "0.5.23",
@@ -15,7 +15,7 @@
15
15
  * limitations under the License.
16
16
  */ 'use client';
17
17
  import { _ as _extends } from "@swc/helpers/_/_extends";
18
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
19
19
  import { CardDisplay, useConfirmationContext } from "@aglyn/shared-ui-jsx";
20
20
  import { ListPagination } from "@aglyn/shared-ui-jsx/components/list-pagination.component";
21
21
  import { ListQueryNotices, listQueryRefusals } from "@aglyn/shared-ui-jsx/components/list-query-notices.component";
@@ -235,6 +235,22 @@ const giftCardsHelp = pluginDocsHelp('commerce', {
235
235
  confirm,
236
236
  post
237
237
  ]);
238
+ // A card frozen by a fraud signal on its purchase (AGL-3363).
239
+ const handleRelease = useCallback(async (card)=>{
240
+ const confirmed = await confirm({
241
+ title: `Release ${card.$id}?`,
242
+ description: 'The payment that bought this card was questioned by the card ' + 'issuer, held for review, or disputed. Release it only if you are ' + 'satisfied the payment is genuine: the holder can then spend the ' + `${usd(card.balanceCents)} left on it.`,
243
+ confirmationText: 'Release gift card'
244
+ }).then(()=>true).catch(()=>false);
245
+ if (!confirmed) return;
246
+ await post({
247
+ action: 'release',
248
+ code: card.$id
249
+ }, ()=>`${card.$id} released`);
250
+ }, [
251
+ confirm,
252
+ post
253
+ ]);
238
254
  return /*#__PURE__*/ _jsx(EntitlementGatedCard, {
239
255
  help: giftCardsHelp,
240
256
  hostId: hostId,
@@ -366,10 +382,26 @@ const giftCardsHelp = pluginDocsHelp('commerce', {
366
382
  variant: "outlined",
367
383
  label: 'Issued by hand'
368
384
  }),
385
+ !card.voidedAtMs && Number(card.frozenAtMs) > 0 ? /*#__PURE__*/ _jsxs(_Fragment, {
386
+ children: [
387
+ /*#__PURE__*/ _jsx(Chip, {
388
+ size: "small",
389
+ color: "warning",
390
+ variant: "outlined",
391
+ label: 'On hold: payment questioned'
392
+ }),
393
+ /*#__PURE__*/ _jsx(Button, {
394
+ size: "small",
395
+ disabled: busy,
396
+ onClick: ()=>handleRelease(card),
397
+ children: 'Release'
398
+ })
399
+ ]
400
+ }) : null,
369
401
  card.voidedAtMs ? /*#__PURE__*/ _jsx(Chip, {
370
402
  size: "small",
371
403
  variant: "outlined",
372
- label: 'Voided'
404
+ label: card.voidedReason === 'refund' ? 'Voided: order refunded' : card.voidedReason === 'dispute-lost' ? 'Voided: chargeback lost' : 'Voided'
373
405
  }) : Number((_card_balanceCents1 = card.balanceCents) != null ? _card_balanceCents1 : 0) > 0 ? /*#__PURE__*/ _jsx(Button, {
374
406
  size: "small",
375
407
  color: "error",
@@ -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 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 ? (\n <Chip size=\"small\" variant=\"outlined\" label={'Voided'} />\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","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","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'\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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"}
@@ -59,13 +59,43 @@ export interface GiftCardHold {
59
59
  cents: number;
60
60
  expiresAtMs: number;
61
61
  }
62
+ /**
63
+ * Why a card cannot be redeemed for now (AGL-3363): the payment that bought
64
+ * it drew an issuer's early fraud warning, a Radar review, or a dispute; or
65
+ * part of it was refunded outside the console and the refund could not be
66
+ * matched to the order's lines (`refund-review`).
67
+ */
68
+ export type GiftCardFreezeReason = 'early-fraud-warning' | 'radar-review' | 'dispute' | 'refund-review';
69
+ /** Why a card's balance was taken back rather than spent. */
70
+ export type GiftCardVoidReason = 'refund' | 'dispute-lost';
62
71
  /** `hosts/{hostId}/giftCards/{code}` doc, as far as redemption cares. */
63
72
  export interface HostGiftCard {
64
73
  balanceCents?: number;
65
74
  /** Session id → hold. Absent on every card issued before AGL-2449. */
66
75
  holds?: Record<string, GiftCardHold>;
67
76
  voidedAtMs?: number;
77
+ voidedReason?: GiftCardVoidReason;
78
+ /** Set while the purchase is under a fraud signal; nothing can redeem it. */
79
+ frozenAtMs?: number;
80
+ frozenReason?: GiftCardFreezeReason;
68
81
  }
82
+ export declare const GIFT_CARD_PURCHASE_LIMITS: {
83
+ /** Gift-card value one checkout may buy. */
84
+ readonly perOrderCents: 500000;
85
+ /** …while the workspace is young (`isYoungWorkspace`). */
86
+ readonly perOrderCentsYoung: 50000;
87
+ /** Checkouts buying gift cards, per visitor address per day. */
88
+ readonly perVisitorDaily: 5;
89
+ /** …while the workspace is young. */
90
+ readonly perVisitorDailyYoung: 2;
91
+ };
92
+ /** The refusal a checkout gives past the per-order ceiling, or null. */
93
+ export declare function giftCardPurchaseRefusal(input: {
94
+ giftCardCents: number;
95
+ young: boolean;
96
+ }): string | null;
97
+ /** Is the card frozen by a fraud signal on the payment that bought it? */
98
+ export declare function isGiftCardFrozen(card: HostGiftCard | undefined): boolean;
69
99
  /**
70
100
  * Stripe Checkout Sessions expire 24h after creation, so a hold outlives any
71
101
  * session that can still be paid. See the module docblock for why a shorter
@@ -52,7 +52,42 @@
52
52
  * shorter. A hold that lapsed while its session was still payable would reopen
53
53
  * the very window this closes — the shopper pays a session whose hold has
54
54
  * evaporated and the balance is spent twice again.
55
- */ /** One in-flight claim on a card's balance, keyed by Stripe session id. */ /**
55
+ */ /** One in-flight claim on a card's balance, keyed by Stripe session id. */ /*==========================================
56
+ * GIFT CARDS ARE A CASH-OUT (AGL-3363).
57
+ *
58
+ * Buying store credit with a stolen card and spending it before the
59
+ * chargeback lands is the standard way to turn card data into goods. Three
60
+ * rules, all shared by every door that sells or redeems a card:
61
+ *
62
+ * 1. A PURCHASE HAS A CEILING. One checkout may buy at most
63
+ * {@link GIFT_CARD_PURCHASE_LIMITS} of gift cards — lower while the
64
+ * workspace is young, when a stolen-card storefront is likeliest — and
65
+ * one visitor may buy gift cards only so many times a day. A real shop's
66
+ * holiday order ($100 cards for a team) sits well inside both.
67
+ * 2. A SIGNAL FREEZES. An early fraud warning, a Radar review or a dispute
68
+ * on the purchase freezes every card it issued (`frozenAtMs`): its balance
69
+ * stays, and nothing can redeem it until the merchant releases it or the
70
+ * dispute is won.
71
+ * 3. A REVERSAL VOIDS. A refund of the purchase, or a lost dispute, zeroes
72
+ * what is left on its cards — the same zeroing the console's Void button
73
+ * does, never a delete (AGL-1767).
74
+ *=========================================*/ export const GIFT_CARD_PURCHASE_LIMITS = {
75
+ /** Gift-card value one checkout may buy. */ perOrderCents: 500000,
76
+ /** …while the workspace is young (`isYoungWorkspace`). */ perOrderCentsYoung: 50000,
77
+ /** Checkouts buying gift cards, per visitor address per day. */ perVisitorDaily: 5,
78
+ /** …while the workspace is young. */ perVisitorDailyYoung: 2
79
+ };
80
+ /** The refusal a checkout gives past the per-order ceiling, or null. */ export function giftCardPurchaseRefusal(input) {
81
+ const ceiling = input.young ? GIFT_CARD_PURCHASE_LIMITS.perOrderCentsYoung : GIFT_CARD_PURCHASE_LIMITS.perOrderCents;
82
+ if (!(input.giftCardCents > ceiling)) return null;
83
+ // The ceiling is not named: a shopper who is testing it must not be told
84
+ // where it is. A real buyer of a large gift order is sent to the shop.
85
+ return 'This order has more in gift cards than can be bought in one online ' + 'checkout. Please contact the shop to complete it.';
86
+ }
87
+ /** Is the card frozen by a fraud signal on the payment that bought it? */ export function isGiftCardFrozen(card) {
88
+ return Number(card == null ? void 0 : card.frozenAtMs) > 0;
89
+ }
90
+ /**
56
91
  * Stripe Checkout Sessions expire 24h after creation, so a hold outlives any
57
92
  * session that can still be paid. See the module docblock for why a shorter
58
93
  * TTL is not the safer choice it looks like.
@@ -89,6 +124,9 @@
89
124
  * on a card voided mid-flight, where the console zeroes `balanceCents` while
90
125
  * holds stand) reads as empty rather than as a debt to be collected.
91
126
  */ export function giftCardAvailableCents(card, nowMs) {
127
+ // A frozen card offers nothing to a NEW checkout (AGL-3363). Settlement of
128
+ // a hold placed before the freeze is left alone: that shopper has paid.
129
+ if (isGiftCardFrozen(card)) return 0;
92
130
  const held = Object.values(pruneGiftCardHolds(card == null ? void 0 : card.holds, nowMs)).reduce((sum, hold)=>sum + hold.cents, 0);
93
131
  return Math.max(0, cents(card == null ? void 0 : card.balanceCents) - held);
94
132
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-gift-cards.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Gift-card hold arithmetic (AGL-2449).\n *\n * A gift card is CASH, and until this module existed it was the one limited\n * resource in the order path with no reserve at all. `cart-checkout.ts` read\n * `balanceCents` with a plain `.get()`, minted a Stripe coupon for that much,\n * and the webhook decremented the card minutes later with a bare\n * `FieldValue.increment(-N)`. The increment is atomic, so nothing was ever\n * LOST — but nothing ever CHECKED either, so two checkouts entering the same\n * code both read $50, both got $50 off, and the card settled at `-$50`. The\n * merchant shipped $100 of goods against a $50 card, and the outstanding\n * liability total went negative — the exact corruption `redeemExistingOrRecord`\n * already reasons about for a DELETED card, arriving through a live one.\n *\n * The stock fix (AGL-2320) could not be copied here: stock is decremented after\n * the money has moved and therefore cannot refuse, so it locks and reports the\n * shortfall. A gift card is applied BEFORE Stripe is contacted, so this door can\n * still say no — and refusing is the only thing that actually keeps the money.\n *\n * So the balance is HELD at checkout and SETTLED at the webhook, both in one\n * transaction against one document. Holds live in a map on the card itself\n * keyed by Stripe session id, which is what keeps this to a single-document\n * transaction: no fan-out, no second collection, no index.\n *\n * ## Why holds expire rather than being swept\n *\n * An abandoned checkout must not stand a customer's money off indefinitely, and\n * a sweep job is the wrong instrument: `process-abandoned.ts` is the only\n * existing sweep and it returns 501 when SMTP is unconfigured, so a\n * release that depended on it would not run at all for a self-hosted store with\n * no mail transport. Instead every hold carries `expiresAtMs` and EVERY read\n * prunes, so the release is a consequence of the next read rather than of a job\n * that may never run. A card nobody touches again holds nothing that matters,\n * because nobody is reading it either.\n *\n * The TTL matches Stripe's Checkout Session lifetime rather than something\n * shorter. A hold that lapsed while its session was still payable would reopen\n * the very window this closes — the shopper pays a session whose hold has\n * evaporated and the balance is spent twice again.\n */\n\n/** One in-flight claim on a card's balance, keyed by Stripe session id. */\nexport interface GiftCardHold {\n cents: number\n expiresAtMs: number\n}\n\n/** `hosts/{hostId}/giftCards/{code}` doc, as far as redemption cares. */\nexport interface HostGiftCard {\n balanceCents?: number\n /** Session id → hold. Absent on every card issued before AGL-2449. */\n holds?: Record<string, GiftCardHold>\n voidedAtMs?: number\n}\n\n/**\n * Stripe Checkout Sessions expire 24h after creation, so a hold outlives any\n * session that can still be paid. See the module docblock for why a shorter\n * TTL is not the safer choice it looks like.\n */\nexport const GIFT_CARD_HOLD_TTL_MS = 24 * 60 * 60 * 1000\n\n/** One money figure off an untrusted document, as whole non-negative cents. */\nfunction cents(value: unknown): number {\n const number = Math.round(Number(value))\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/**\n * The card's holds with the lapsed ones dropped.\n *\n * Total, not a filter over a trusted shape: a hold whose `expiresAtMs` is\n * absent or non-numeric is treated as EXPIRED rather than eternal, so a\n * malformed row releases the money instead of stranding it. The one direction\n * that cannot be wrong is the one that keeps a customer from spending their own\n * balance forever.\n */\nexport function pruneGiftCardHolds(\n holds: Record<string, GiftCardHold> | undefined,\n nowMs: number,\n): Record<string, GiftCardHold> {\n const live: Record<string, GiftCardHold> = {}\n for (const [sessionId, hold] of Object.entries(holds ?? {})) {\n const expiresAtMs = Number(hold?.expiresAtMs)\n if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) continue\n const amount = cents(hold?.cents)\n if (amount > 0) live[sessionId] = { cents: amount, expiresAtMs }\n }\n return live\n}\n\n/**\n * What a NEW checkout may apply: the balance less every hold still standing.\n *\n * Never negative. A card whose holds already exceed its balance (possible only\n * on a card voided mid-flight, where the console zeroes `balanceCents` while\n * holds stand) reads as empty rather than as a debt to be collected.\n */\nexport function giftCardAvailableCents(\n card: HostGiftCard | undefined,\n nowMs: number,\n): number {\n const held = Object.values(pruneGiftCardHolds(card?.holds, nowMs)).reduce(\n (sum, hold) => sum + hold.cents,\n 0,\n )\n return Math.max(0, cents(card?.balanceCents) - held)\n}\n\n/**\n * What settling `sessionId` should actually take off the card.\n *\n * Capped at the live balance, not at the hold, and that is the point of the\n * function. The hold is what this session RESERVED; the balance is what the\n * card can still give up. They diverge when the card was voided or hand-adjusted\n * between the hold and the payment, and in that window paying out the hold would\n * drive the balance negative — reintroducing the corrupted liability aggregate\n * from the other side.\n *\n * A session with no hold settles ZERO. That is the redelivery case (the first\n * delivery consumed it) and the pre-AGL-2449 case (a session that predates\n * holds), and both must be no-ops rather than a second decrement.\n */\nexport function giftCardSettlementCents(\n card: HostGiftCard | undefined,\n sessionId: string,\n // Deliberately unused, and kept so this reads as the sibling of\n // `giftCardAvailableCents` at every call site. The asymmetry IS the rule\n // below: availability is a question about now, settlement is not.\n _nowMs: number,\n): number {\n // NOT pruned: a hold that lapsed while its session sat unpaid is still owed\n // once that session is paid. Expiry governs what a NEW checkout may claim,\n // never whether a completed payment is honoured — dropping it here would take\n // the discount from the shopper and give nothing back to the merchant.\n const hold = (card?.holds ?? {})[sessionId]\n return Math.min(cents(hold?.cents), cents(card?.balanceCents))\n}\n"],"names":["GIFT_CARD_HOLD_TTL_MS","cents","value","number","Math","round","Number","isFinite","pruneGiftCardHolds","holds","nowMs","live","sessionId","hold","Object","entries","expiresAtMs","amount","giftCardAvailableCents","card","held","values","reduce","sum","max","balanceCents","giftCardSettlementCents","_nowMs","min"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,yEAAyE,GAczE;;;;CAIC,GACD,OAAO,MAAMA,wBAAwB,KAAK,KAAK,KAAK,KAAI;AAExD,6EAA6E,GAC7E,SAASC,MAAMC,KAAc;IAC3B,MAAMC,SAASC,KAAKC,KAAK,CAACC,OAAOJ;IACjC,OAAOI,OAAOC,QAAQ,CAACJ,WAAWA,SAAS,IAAIA,SAAS;AAC1D;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASK,mBACdC,KAA+C,EAC/CC,KAAa;IAEb,MAAMC,OAAqC,CAAC;IAC5C,KAAK,MAAM,CAACC,WAAWC,KAAK,IAAIC,OAAOC,OAAO,CAACN,gBAAAA,QAAS,CAAC,GAAI;QAC3D,MAAMO,cAAcV,OAAOO,wBAAAA,KAAMG,WAAW;QAC5C,IAAI,CAACV,OAAOC,QAAQ,CAACS,gBAAgBA,eAAeN,OAAO;QAC3D,MAAMO,SAAShB,MAAMY,wBAAAA,KAAMZ,KAAK;QAChC,IAAIgB,SAAS,GAAGN,IAAI,CAACC,UAAU,GAAG;YAAEX,OAAOgB;YAAQD;QAAY;IACjE;IACA,OAAOL;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASO,uBACdC,IAA8B,EAC9BT,KAAa;IAEb,MAAMU,OAAON,OAAOO,MAAM,CAACb,mBAAmBW,wBAAAA,KAAMV,KAAK,EAAEC,QAAQY,MAAM,CACvE,CAACC,KAAKV,OAASU,MAAMV,KAAKZ,KAAK,EAC/B;IAEF,OAAOG,KAAKoB,GAAG,CAAC,GAAGvB,MAAMkB,wBAAAA,KAAMM,YAAY,IAAIL;AACjD;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASM,wBACdP,IAA8B,EAC9BP,SAAiB,EACjB,gEAAgE;AAChE,yEAAyE;AACzE,kEAAkE;AAClEe,MAAc;;IAEd,4EAA4E;IAC5E,2EAA2E;IAC3E,8EAA8E;IAC9E,uEAAuE;IACvE,MAAMd,OAAO,SAACM,wBAAAA,KAAMV,KAAK,mBAAI,CAAC,EAAE,CAACG,UAAU;IAC3C,OAAOR,KAAKwB,GAAG,CAAC3B,MAAMY,wBAAAA,KAAMZ,KAAK,GAAGA,MAAMkB,wBAAAA,KAAMM,YAAY;AAC9D"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-gift-cards.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Gift-card hold arithmetic (AGL-2449).\n *\n * A gift card is CASH, and until this module existed it was the one limited\n * resource in the order path with no reserve at all. `cart-checkout.ts` read\n * `balanceCents` with a plain `.get()`, minted a Stripe coupon for that much,\n * and the webhook decremented the card minutes later with a bare\n * `FieldValue.increment(-N)`. The increment is atomic, so nothing was ever\n * LOST — but nothing ever CHECKED either, so two checkouts entering the same\n * code both read $50, both got $50 off, and the card settled at `-$50`. The\n * merchant shipped $100 of goods against a $50 card, and the outstanding\n * liability total went negative — the exact corruption `redeemExistingOrRecord`\n * already reasons about for a DELETED card, arriving through a live one.\n *\n * The stock fix (AGL-2320) could not be copied here: stock is decremented after\n * the money has moved and therefore cannot refuse, so it locks and reports the\n * shortfall. A gift card is applied BEFORE Stripe is contacted, so this door can\n * still say no — and refusing is the only thing that actually keeps the money.\n *\n * So the balance is HELD at checkout and SETTLED at the webhook, both in one\n * transaction against one document. Holds live in a map on the card itself\n * keyed by Stripe session id, which is what keeps this to a single-document\n * transaction: no fan-out, no second collection, no index.\n *\n * ## Why holds expire rather than being swept\n *\n * An abandoned checkout must not stand a customer's money off indefinitely, and\n * a sweep job is the wrong instrument: `process-abandoned.ts` is the only\n * existing sweep and it returns 501 when SMTP is unconfigured, so a\n * release that depended on it would not run at all for a self-hosted store with\n * no mail transport. Instead every hold carries `expiresAtMs` and EVERY read\n * prunes, so the release is a consequence of the next read rather than of a job\n * that may never run. A card nobody touches again holds nothing that matters,\n * because nobody is reading it either.\n *\n * The TTL matches Stripe's Checkout Session lifetime rather than something\n * shorter. A hold that lapsed while its session was still payable would reopen\n * the very window this closes — the shopper pays a session whose hold has\n * evaporated and the balance is spent twice again.\n */\n\n/** One in-flight claim on a card's balance, keyed by Stripe session id. */\nexport interface GiftCardHold {\n cents: number\n expiresAtMs: number\n}\n\n/**\n * Why a card cannot be redeemed for now (AGL-3363): the payment that bought\n * it drew an issuer's early fraud warning, a Radar review, or a dispute; or\n * part of it was refunded outside the console and the refund could not be\n * matched to the order's lines (`refund-review`).\n */\nexport type GiftCardFreezeReason =\n | 'early-fraud-warning'\n | 'radar-review'\n | 'dispute'\n | 'refund-review'\n\n/** Why a card's balance was taken back rather than spent. */\nexport type GiftCardVoidReason = 'refund' | 'dispute-lost'\n\n/** `hosts/{hostId}/giftCards/{code}` doc, as far as redemption cares. */\nexport interface HostGiftCard {\n balanceCents?: number\n /** Session id → hold. Absent on every card issued before AGL-2449. */\n holds?: Record<string, GiftCardHold>\n voidedAtMs?: number\n voidedReason?: GiftCardVoidReason\n /** Set while the purchase is under a fraud signal; nothing can redeem it. */\n frozenAtMs?: number\n frozenReason?: GiftCardFreezeReason\n}\n\n/*==========================================\n * GIFT CARDS ARE A CASH-OUT (AGL-3363).\n *\n * Buying store credit with a stolen card and spending it before the\n * chargeback lands is the standard way to turn card data into goods. Three\n * rules, all shared by every door that sells or redeems a card:\n *\n * 1. A PURCHASE HAS A CEILING. One checkout may buy at most\n * {@link GIFT_CARD_PURCHASE_LIMITS} of gift cards — lower while the\n * workspace is young, when a stolen-card storefront is likeliest — and\n * one visitor may buy gift cards only so many times a day. A real shop's\n * holiday order ($100 cards for a team) sits well inside both.\n * 2. A SIGNAL FREEZES. An early fraud warning, a Radar review or a dispute\n * on the purchase freezes every card it issued (`frozenAtMs`): its balance\n * stays, and nothing can redeem it until the merchant releases it or the\n * dispute is won.\n * 3. A REVERSAL VOIDS. A refund of the purchase, or a lost dispute, zeroes\n * what is left on its cards — the same zeroing the console's Void button\n * does, never a delete (AGL-1767).\n *=========================================*/\n\nexport const GIFT_CARD_PURCHASE_LIMITS = {\n /** Gift-card value one checkout may buy. */\n perOrderCents: 500_000,\n /** …while the workspace is young (`isYoungWorkspace`). */\n perOrderCentsYoung: 50_000,\n /** Checkouts buying gift cards, per visitor address per day. */\n perVisitorDaily: 5,\n /** …while the workspace is young. */\n perVisitorDailyYoung: 2,\n} as const\n\n/** The refusal a checkout gives past the per-order ceiling, or null. */\nexport function giftCardPurchaseRefusal(input: {\n giftCardCents: number\n young: boolean\n}): string | null {\n const ceiling = input.young\n ? GIFT_CARD_PURCHASE_LIMITS.perOrderCentsYoung\n : GIFT_CARD_PURCHASE_LIMITS.perOrderCents\n if (!(input.giftCardCents > ceiling)) return null\n // The ceiling is not named: a shopper who is testing it must not be told\n // where it is. A real buyer of a large gift order is sent to the shop.\n return (\n 'This order has more in gift cards than can be bought in one online ' +\n 'checkout. Please contact the shop to complete it.'\n )\n}\n\n/** Is the card frozen by a fraud signal on the payment that bought it? */\nexport function isGiftCardFrozen(card: HostGiftCard | undefined): boolean {\n return Number(card?.frozenAtMs) > 0\n}\n\n/**\n * Stripe Checkout Sessions expire 24h after creation, so a hold outlives any\n * session that can still be paid. See the module docblock for why a shorter\n * TTL is not the safer choice it looks like.\n */\nexport const GIFT_CARD_HOLD_TTL_MS = 24 * 60 * 60 * 1000\n\n/** One money figure off an untrusted document, as whole non-negative cents. */\nfunction cents(value: unknown): number {\n const number = Math.round(Number(value))\n return Number.isFinite(number) && number > 0 ? number : 0\n}\n\n/**\n * The card's holds with the lapsed ones dropped.\n *\n * Total, not a filter over a trusted shape: a hold whose `expiresAtMs` is\n * absent or non-numeric is treated as EXPIRED rather than eternal, so a\n * malformed row releases the money instead of stranding it. The one direction\n * that cannot be wrong is the one that keeps a customer from spending their own\n * balance forever.\n */\nexport function pruneGiftCardHolds(\n holds: Record<string, GiftCardHold> | undefined,\n nowMs: number,\n): Record<string, GiftCardHold> {\n const live: Record<string, GiftCardHold> = {}\n for (const [sessionId, hold] of Object.entries(holds ?? {})) {\n const expiresAtMs = Number(hold?.expiresAtMs)\n if (!Number.isFinite(expiresAtMs) || expiresAtMs <= nowMs) continue\n const amount = cents(hold?.cents)\n if (amount > 0) live[sessionId] = { cents: amount, expiresAtMs }\n }\n return live\n}\n\n/**\n * What a NEW checkout may apply: the balance less every hold still standing.\n *\n * Never negative. A card whose holds already exceed its balance (possible only\n * on a card voided mid-flight, where the console zeroes `balanceCents` while\n * holds stand) reads as empty rather than as a debt to be collected.\n */\nexport function giftCardAvailableCents(\n card: HostGiftCard | undefined,\n nowMs: number,\n): number {\n // A frozen card offers nothing to a NEW checkout (AGL-3363). Settlement of\n // a hold placed before the freeze is left alone: that shopper has paid.\n if (isGiftCardFrozen(card)) return 0\n const held = Object.values(pruneGiftCardHolds(card?.holds, nowMs)).reduce(\n (sum, hold) => sum + hold.cents,\n 0,\n )\n return Math.max(0, cents(card?.balanceCents) - held)\n}\n\n/**\n * What settling `sessionId` should actually take off the card.\n *\n * Capped at the live balance, not at the hold, and that is the point of the\n * function. The hold is what this session RESERVED; the balance is what the\n * card can still give up. They diverge when the card was voided or hand-adjusted\n * between the hold and the payment, and in that window paying out the hold would\n * drive the balance negative — reintroducing the corrupted liability aggregate\n * from the other side.\n *\n * A session with no hold settles ZERO. That is the redelivery case (the first\n * delivery consumed it) and the pre-AGL-2449 case (a session that predates\n * holds), and both must be no-ops rather than a second decrement.\n */\nexport function giftCardSettlementCents(\n card: HostGiftCard | undefined,\n sessionId: string,\n // Deliberately unused, and kept so this reads as the sibling of\n // `giftCardAvailableCents` at every call site. The asymmetry IS the rule\n // below: availability is a question about now, settlement is not.\n _nowMs: number,\n): number {\n // NOT pruned: a hold that lapsed while its session sat unpaid is still owed\n // once that session is paid. Expiry governs what a NEW checkout may claim,\n // never whether a completed payment is honoured — dropping it here would take\n // the discount from the shopper and give nothing back to the merchant.\n const hold = (card?.holds ?? {})[sessionId]\n return Math.min(cents(hold?.cents), cents(card?.balanceCents))\n}\n"],"names":["GIFT_CARD_PURCHASE_LIMITS","perOrderCents","perOrderCentsYoung","perVisitorDaily","perVisitorDailyYoung","giftCardPurchaseRefusal","input","ceiling","young","giftCardCents","isGiftCardFrozen","card","Number","frozenAtMs","GIFT_CARD_HOLD_TTL_MS","cents","value","number","Math","round","isFinite","pruneGiftCardHolds","holds","nowMs","live","sessionId","hold","Object","entries","expiresAtMs","amount","giftCardAvailableCents","held","values","reduce","sum","max","balanceCents","giftCardSettlementCents","_nowMs","min"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuCC,GAED,yEAAyE,GAiCzE;;;;;;;;;;;;;;;;;;;2CAmB2C,GAE3C,OAAO,MAAMA,4BAA4B;IACvC,0CAA0C,GAC1CC,eAAe;IACf,wDAAwD,GACxDC,oBAAoB;IACpB,8DAA8D,GAC9DC,iBAAiB;IACjB,mCAAmC,GACnCC,sBAAsB;AACxB,EAAU;AAEV,sEAAsE,GACtE,OAAO,SAASC,wBAAwBC,KAGvC;IACC,MAAMC,UAAUD,MAAME,KAAK,GACvBR,0BAA0BE,kBAAkB,GAC5CF,0BAA0BC,aAAa;IAC3C,IAAI,CAAEK,CAAAA,MAAMG,aAAa,GAAGF,OAAM,GAAI,OAAO;IAC7C,yEAAyE;IACzE,uEAAuE;IACvE,OACE,wEACA;AAEJ;AAEA,wEAAwE,GACxE,OAAO,SAASG,iBAAiBC,IAA8B;IAC7D,OAAOC,OAAOD,wBAAAA,KAAME,UAAU,IAAI;AACpC;AAEA;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB,KAAK,KAAK,KAAK,KAAI;AAExD,6EAA6E,GAC7E,SAASC,MAAMC,KAAc;IAC3B,MAAMC,SAASC,KAAKC,KAAK,CAACP,OAAOI;IACjC,OAAOJ,OAAOQ,QAAQ,CAACH,WAAWA,SAAS,IAAIA,SAAS;AAC1D;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASI,mBACdC,KAA+C,EAC/CC,KAAa;IAEb,MAAMC,OAAqC,CAAC;IAC5C,KAAK,MAAM,CAACC,WAAWC,KAAK,IAAIC,OAAOC,OAAO,CAACN,gBAAAA,QAAS,CAAC,GAAI;QAC3D,MAAMO,cAAcjB,OAAOc,wBAAAA,KAAMG,WAAW;QAC5C,IAAI,CAACjB,OAAOQ,QAAQ,CAACS,gBAAgBA,eAAeN,OAAO;QAC3D,MAAMO,SAASf,MAAMW,wBAAAA,KAAMX,KAAK;QAChC,IAAIe,SAAS,GAAGN,IAAI,CAACC,UAAU,GAAG;YAAEV,OAAOe;YAAQD;QAAY;IACjE;IACA,OAAOL;AACT;AAEA;;;;;;CAMC,GACD,OAAO,SAASO,uBACdpB,IAA8B,EAC9BY,KAAa;IAEb,2EAA2E;IAC3E,wEAAwE;IACxE,IAAIb,iBAAiBC,OAAO,OAAO;IACnC,MAAMqB,OAAOL,OAAOM,MAAM,CAACZ,mBAAmBV,wBAAAA,KAAMW,KAAK,EAAEC,QAAQW,MAAM,CACvE,CAACC,KAAKT,OAASS,MAAMT,KAAKX,KAAK,EAC/B;IAEF,OAAOG,KAAKkB,GAAG,CAAC,GAAGrB,MAAMJ,wBAAAA,KAAM0B,YAAY,IAAIL;AACjD;AAEA;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASM,wBACd3B,IAA8B,EAC9Bc,SAAiB,EACjB,gEAAgE;AAChE,yEAAyE;AACzE,kEAAkE;AAClEc,MAAc;;IAEd,4EAA4E;IAC5E,2EAA2E;IAC3E,8EAA8E;IAC9E,uEAAuE;IACvE,MAAMb,OAAO,SAACf,wBAAAA,KAAMW,KAAK,mBAAI,CAAC,EAAE,CAACG,UAAU;IAC3C,OAAOP,KAAKsB,GAAG,CAACzB,MAAMW,wBAAAA,KAAMX,KAAK,GAAGA,MAAMJ,wBAAAA,KAAM0B,YAAY;AAC9D"}