@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.
- package/README.md +4 -2
- package/package.json +14 -13
- package/src/lib/components/console/catalog-organization-card.component.js +9 -0
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/discounts-card.component.js +8 -0
- package/src/lib/components/console/discounts-card.component.js.map +1 -1
- package/src/lib/components/console/gift-cards-card.component.js +8 -0
- package/src/lib/components/console/gift-cards-card.component.js.map +1 -1
- package/src/lib/components/console/host-coupons-card.component.js +8 -0
- package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +34 -70
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/product-zones.d.ts +7 -6
- package/src/lib/components/console/product-zones.js +5 -4
- package/src/lib/components/console/product-zones.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +86 -284
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/declarations.console-server.d.ts +38 -0
- package/src/lib/declarations.console-server.js +67 -0
- package/src/lib/declarations.console-server.js.map +1 -0
- package/src/lib/model/commerce-gift-cards.d.ts +26 -0
- package/src/lib/model/commerce-gift-cards.js +42 -0
- package/src/lib/model/commerce-gift-cards.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +0 -57
- package/src/lib/model/commerce-orders.js +0 -80
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/index.d.ts +0 -1
- package/src/lib/model/index.js +0 -1
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/plugin.js +5 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/api-v1/openapi.d.ts +27 -0
- package/src/lib/server/api-v1/openapi.js +219 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -0
- package/src/lib/server/api-v1/orders-and-products.d.ts +19 -0
- package/src/lib/server/api-v1/orders-and-products.js +419 -0
- package/src/lib/server/api-v1/orders-and-products.js.map +1 -0
- package/src/lib/server/billing-webhook.js +10 -13
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/download.js +4 -1
- package/src/lib/server/download.js.map +1 -1
- package/src/lib/server/fulfill-order.d.ts +52 -11
- package/src/lib/server/fulfill-order.js +6 -9
- package/src/lib/server/fulfill-order.js.map +1 -1
- package/src/lib/server/gift-card-issue.d.ts +57 -0
- package/src/lib/server/gift-card-issue.js +155 -0
- package/src/lib/server/gift-card-issue.js.map +1 -0
- package/src/lib/server/gift-cards.js +25 -84
- package/src/lib/server/gift-cards.js.map +1 -1
- package/src/lib/server/membership-register.js +9 -8
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/newsletter.js +7 -5
- package/src/lib/server/newsletter.js.map +1 -1
- package/src/lib/server/paid-media-delivery.d.ts +21 -0
- package/src/lib/server/paid-media-delivery.js +23 -5
- package/src/lib/server/paid-media-delivery.js.map +1 -1
- package/src/lib/server/paid-media-serve-count.d.ts +28 -0
- package/src/lib/server/paid-media-serve-count.js +46 -0
- package/src/lib/server/paid-media-serve-count.js.map +1 -0
- package/src/lib/server/person-eraser.d.ts +45 -0
- package/src/lib/server/person-eraser.js +57 -0
- package/src/lib/server/person-eraser.js.map +1 -0
- package/src/lib/server/refund.js +15 -15
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/restock-flag.d.ts +2 -2
- package/src/lib/server/restock-flag.js +1 -1
- package/src/lib/server/restock-flag.js.map +1 -1
- package/src/lib/server/stream.js +4 -1
- package/src/lib/server/stream.js.map +1 -1
- package/src/lib/server.js +5 -21
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/stand-in-person-records.d.ts +43 -0
- package/src/lib/testing/stand-in-person-records.js +63 -0
- package/src/lib/testing/stand-in-person-records.js.map +1 -0
- package/src/lib/transfer/gift-card-confirm-step.component.d.ts +22 -0
- package/src/lib/transfer/gift-card-confirm-step.component.js +259 -0
- package/src/lib/transfer/gift-card-confirm-step.component.js.map +1 -0
- package/src/lib/transfer/gift-cards-plan.d.ts +29 -0
- package/src/lib/transfer/gift-cards-plan.js +40 -0
- package/src/lib/transfer/gift-cards-plan.js.map +1 -0
- package/src/lib/transfer/gift-cards.server.d.ts +34 -0
- package/src/lib/transfer/gift-cards.server.js +296 -0
- package/src/lib/transfer/gift-cards.server.js.map +1 -0
- package/src/lib/transfer/list-filter.d.ts +29 -0
- package/src/lib/transfer/list-filter.js +112 -0
- package/src/lib/transfer/list-filter.js.map +1 -0
- package/src/lib/transfer/product-import-results.d.ts +29 -0
- package/src/lib/transfer/product-import-results.js +49 -0
- package/src/lib/transfer/product-import-results.js.map +1 -0
- package/src/lib/transfer/product-import-step.component.d.ts +30 -0
- package/src/lib/transfer/product-import-step.component.js +99 -0
- package/src/lib/transfer/product-import-step.component.js.map +1 -0
- package/src/lib/transfer/product-transfer.d.ts +211 -0
- package/src/lib/transfer/product-transfer.js +1594 -0
- package/src/lib/transfer/product-transfer.js.map +1 -0
- package/src/lib/transfer/products.server.d.ts +44 -0
- package/src/lib/transfer/products.server.js +550 -0
- package/src/lib/transfer/products.server.js.map +1 -0
- package/src/lib/transfer/records-transfer.d.ts +182 -0
- package/src/lib/transfer/records-transfer.js +1153 -0
- package/src/lib/transfer/records-transfer.js.map +1 -0
- package/src/lib/transfer/records.server.d.ts +68 -0
- package/src/lib/transfer/records.server.js +455 -0
- package/src/lib/transfer/records.server.js.map +1 -0
- package/src/lib/transfer/register-transfer-resources.d.ts +17 -0
- package/src/lib/transfer/register-transfer-resources.js +171 -0
- package/src/lib/transfer/register-transfer-resources.js.map +1 -0
- package/src/lib/transfer/register-transfer-ui.d.ts +23 -0
- package/src/lib/transfer/register-transfer-ui.js +78 -0
- package/src/lib/transfer/register-transfer-ui.js.map +1 -0
- package/src/lib/transfer/server-common.d.ts +76 -0
- package/src/lib/transfer/server-common.js +165 -0
- package/src/lib/transfer/server-common.js.map +1 -0
- package/src/lib/transfer/transfer-header-actions.component.d.ts +39 -0
- package/src/lib/transfer/transfer-header-actions.component.js +79 -0
- package/src/lib/transfer/transfer-header-actions.component.js.map +1 -0
- package/src/lib/transfer/transfer-keys.d.ts +31 -0
- package/src/lib/transfer/transfer-keys.js +56 -0
- package/src/lib/transfer/transfer-keys.js.map +1 -0
- package/src/lib/model/commerce-io.d.ts +0 -36
- package/src/lib/model/commerce-io.js +0 -245
- package/src/lib/model/commerce-io.js.map +0 -1
- package/src/lib/server/contact-refund.d.ts +0 -117
- package/src/lib/server/contact-refund.js +0 -196
- package/src/lib/server/contact-refund.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/products-hub-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 {\n PRODUCT_LIST_BASE,\n PRODUCT_LIST_FIELDS,\n PRODUCT_LIST_HEADERS,\n PRODUCT_LIST_OPTIONS,\n PRODUCT_LIST_QUERY,\n PRODUCT_LIST_SELECT_FIELDS,\n productListRequestClauses,\n} from '../../constants/product-list-query'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\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 {\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Box,\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 addDoc,\n collection,\n doc,\n documentId,\n getCountFromServer,\n getDocs,\n limit,\n orderBy,\n query,\n type QueryDocumentSnapshot,\n startAfter,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { writeSiteWideChange } from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport {\n useFirestoreCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport {\n listQueryConstraints,\n useListQuery,\n} from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useHostResourceApi } from '@aglyn/tenant-feature-instance'\nimport { useOrgPlan } from '@aglyn/tenant-feature-instance'\nimport ProductEditorDialog from './product-editor-dialog.component'\nimport { productSlugLedger } from './product-slugs'\nimport { productCollectionFields, readSmartCollections } from './smart-collections'\nimport ProductsHubZone from './products-hub-zone.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { useConsoleWidgetSlot } from '@aglyn/aglyn/app-utils/console-widget-slot-context'\nimport {\n PRODUCT_IMPORT_ZONE,\n type ConsoleProductsHubZoneProps,\n} from './product-zones'\n\n/** Products read per request of the CSV export's walk. */\nconst EXPORT_PAGE = 500\n\n/**\n * The most products one CSV export writes (AGL-3321).\n *\n * The export walks the table's own query page by page, so it writes what the\n * filters and search match across the whole catalog rather than the rows on\n * screen. It stops here because the file is built in the browser, and says\n * so when it does.\n */\nconst EXPORT_CEILING = 10_000\n\n/**\n * How many of ONE product's license keys the dialog reads.\n *\n * Per product, not per store: the query carries the `productId` equality, so\n * this bounds a single pool rather than the site's whole key collection.\n */\nconst KEY_POOL_CEILING = 500\n\nexport interface ProductsHubCardProps {\n hostId: string\n}\n\ntype ProductRow = CommerceModel.HostProduct & { $id: string }\n\nconst STATUS_COLOR: Record<string, 'default' | 'success' | 'warning'> = {\n active: 'success',\n draft: 'warning',\n archived: 'default',\n}\n\n/** The grid's own columns; every other declared field is a hidden filter column. */\nconst PRODUCT_VISIBLE_COLUMNS = ['name', 'status', 'type', 'priceUsd', 'stock', 'variants']\n\n/**\n * Products hub v1 (AGL-279): the catalog manager replacing the Commerce\n * Starter card — a table over `hosts/{hostId}/products` whose search and\n * filters are its Firestore query (AGL-3321), full editor dialog, duplicate, archive/activate, soft delete (past\n * order rows keep resolving). Product cap (`productsPerHost`, AGL-278)\n * gated here on create/duplicate/import (AGL-471); server-side\n * enforcement of the client-write path rides AGL-473.\n */\nexport function ProductsHubCard(props: ProductsHubCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n // Product cap (AGL-471): per-plan `productsPerHost`, same pattern as\n // locations. Console-side gate; server enforcement rides AGL-473.\n const createHostResource = useHostResourceApi()\n const { org, ready: planReady } = useOrgPlan(hostId)\n const { confirm } = useConfirmationContext()\n /*\n * The grid's Filters panel and quick search, bound to the clauses the\n * query below serves (AGL-3321). Every clause and the search word go on\n * that query; see `constants/product-list-query.ts` for what is offered.\n */\n const gridFilter = useListGridFilter({\n selectFields: PRODUCT_LIST_SELECT_FIELDS,\n })\n const filtering =\n gridFilter.clauses.length > 0 ||\n gridFilter.searchWords.some((word) => word.trim())\n const [editing, setEditing] = useState<ProductRow | null>(null)\n const [creating, setCreating] = useState(false)\n const [adjusting, setAdjusting] = useState<{\n product: ProductRow\n variantId: string\n delta: string\n reason: CommerceModel.InventoryAdjustmentReason\n locationId: string\n } | null>(null)\n const [importing, setImporting] = useState<{\n text: string\n parsed: CommerceModel.ProductCsvImport | null\n } | null>(null)\n const [keysFor, setKeysFor] = useState<ProductRow | null>(null)\n const [keysText, setKeysText] = useState('')\n /**\n * What the import zone set for the import in the dialog, and what the last\n * import created with those options (AGL-2916), which the hub's zone hands\n * to its widgets.\n */\n const [importOptions, setImportOptions] = useState<Record<string, boolean>>({})\n const [lastImport, setLastImport] =\n useState<ConsoleProductsHubZoneProps['lastImport']>(null)\n const WidgetSlot = useConsoleWidgetSlot()\n\n const {\n rows: productDocs,\n data: productWindow,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n status: productsStatus,\n /**\n * The product rows the stock dialog is seeded from are unconfirmed by\n * the server (AGL-1358). A stock adjustment does not write the delta —\n * it recomputes the WHOLE `variants` array from the seeded product and\n * replaces it, so a cached seed silently reverts every sale, return and\n * adjustment the server has recorded since that snapshot, on every\n * variant and every location.\n */\n fromCache: productsFromCache,\n } = useListQuery<any>({\n /*\n * PAGED BY THE QUERY, and every filter ON it (AGL-3321).\n *\n * The table read a five-hundred-product window and put ONE predicate on\n * it — the search or the Status clause — then matched Status beside a\n * search, dropped soft-deleted products and sorted by name over the rows\n * it held, and paged that. Past five hundred products, a match past the\n * window answered \"no products match\", and a page came back short by\n * however many deleted products it had held.\n *\n * Now the plan puts the search word and every clause on one query under\n * the `deletedAt == null` scope, ordered by `nameLower`, and each page is\n * a page of that query — one page plus a probe row, widened as the reader\n * pages forward. Nothing is matched over the rows afterwards; a clause\n * the query cannot hold is refused by name (`ListQueryNotices` below).\n */\n collection: collection(firestore, 'hosts', hostId, 'products'),\n declaration: PRODUCT_LIST_QUERY,\n request: {\n clauses: productListRequestClauses(gridFilter.clauses),\n search: gridFilter.searchWords,\n base: PRODUCT_LIST_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n /*\n * The scope and the filters read MUTABLE fields — a delete sets\n * `deletedAt`, Archive moves `status`, a rename moves the tokens — so a\n * product can leave this query mid-session, and the SDK can cache that\n * departure as a tombstone at the product's own path (AGL-1196). A\n * disappearance is confirmed against the server before it is believed.\n */\n confirmDisappearances: true,\n })\n /*\n * License key pool (AGL-308) for the open dialog's product — asked FOR that\n * product, and ordered (AGL-2501).\n *\n * This read `limit(500)` over the site's whole `licenseKeys` collection with\n * no `orderBy`, then filtered by `productId` in the browser. Firestore\n * answers an unordered limit in DOCUMENT-ID order, so on a store past five\n * hundred keys the window was an arbitrary five hundred taken across every\n * product — and the \"N available\" line below counted what happened to be in\n * it. A product whose keys all hashed high showed a pool of zero while the\n * storefront went on delivering them.\n *\n * The equality moves into the query, so the window is this product's keys\n * rather than the store's. `orderBy(documentId())` orders on the document\n * NAME, which cannot be absent — every candidate FIELD here (`createdAtMs`,\n * `assignedAtMs`, `revokedAtMs`) is either optional by design or absent on\n * exactly the keys the counts are about. An equality plus an ordering on\n * `__name__` is served by the automatic single-field index, so this adds no\n * composite for anyone to deploy.\n */\n const { data: keyDocs } = useFirestoreCollection<any>(\n () =>\n keysFor\n ? query(\n collection(firestore, 'hosts', hostId, 'licenseKeys'),\n where('productId', '==', keysFor.$id),\n orderBy(documentId()),\n // One past the ceiling, so a pool larger than the window is a\n // fact rather than a guess from `length === 500`.\n limit(KEY_POOL_CEILING + 1),\n )\n : null,\n [firestore, hostId, keysFor?.$id],\n { idField: '$id' },\n )\n /** The key pool is larger than the window, so the counts below are partial. */\n const keyPoolTruncated = (keyDocs?.length ?? 0) > KEY_POOL_CEILING\n // Locations (AGL-286): the stock dialog buckets deltas when they exist.\n const { data: locationDocs } = useFirestoreCollection<any>(\n () => query(collection(firestore, 'hosts', hostId, 'locations'), limit(25)),\n [firestore, hostId],\n { idField: '$id' },\n )\n /*\n * The PAGE on screen, lifted from legacy shapes. It is what the table\n * draws, the reserved-stock clock watches and the zone hands its widgets,\n * and nothing narrows it: the query already answered every filter, and\n * soft-deleted products are outside its scope.\n */\n const products = useMemo<ProductRow[]>(\n () =>\n (productDocs ?? []).map((product: any) => ({\n ...CommerceModel.liftLegacyProduct(product),\n $id: product.$id,\n })),\n [productDocs],\n )\n\n /**\n * A CLOCK, because a hold lapses without anybody writing anything\n * (AGL-2356).\n *\n * The Firestore listener re-renders on document changes, and a reservation\n * expiring is not one — `expiresAtMs` simply passes. Without a tick the\n * \"reserved\" caption below would keep naming a hold that lapsed twenty\n * minutes ago, which is worse than not showing it at all: the merchant would\n * be told stock is spoken for while the storefront happily sells it.\n *\n * Only runs while something is actually held, so an ordinary catalog costs\n * no timer. A NEW hold arrives as a document change, which re-renders, which\n * re-arms this. It watches the page on screen, which is every row whose\n * caption it keeps true.\n */\n const [nowMs, setNowMs] = useState(() => Date.now())\n const anyHeld = useMemo(\n () =>\n products.some(\n (product) => CommerceModel.heldProductUnits(product, nowMs) > 0,\n ),\n [products, nowMs],\n )\n useEffect(() => {\n if (!anyHeld) return undefined\n const timer = setInterval(() => setNowMs(Date.now()), 60_000)\n return () => clearInterval(timer)\n }, [anyHeld])\n\n /**\n * The catalog HEAD-COUNT is a server aggregate, not the length of the\n * capped listener (AGL-1716, the AGL-1706 shape).\n *\n * The listener is `limit(500)` — correctly; a 25,000-product catalog does\n * not belong in a table. What it must not do is answer \"how many products\n * does this site have\", and it did: the length saturated at 500 and was\n * handed to `checkQuota(org, 'productsPerHost', …)` and to the batch check\n * the CSV importer runs. The bands are 2,500 / 10,000 / 25,000 above the\n * window, so on Pro and up the check compared 500 against thousands and\n * could never refuse — the card offered headroom and `api/hosts/resources`\n * then refused the create, which is the AGL-1716 shape exactly.\n *\n * The aggregate is deliberately UNFILTERED, which also closes a second,\n * quieter disagreement: `api/hosts/resources` enforces this quota with a\n * plain `collection('products').count()`, and `softDeletes` there governs\n * only the flat per-host cap on webhooks — so the server has always\n * counted soft-deleted products toward `productsPerHost` while this card\n * excluded them. The card now asks the enforcing route's question, in the\n * enforcing route's terms.\n *\n * The table pages its own query (AGL-3321) and answers a different\n * question — which products match — so it never stands in for this. A\n * one-shot goes stale where a listener refreshed for free, so the count is\n * re-read after any mutation that moves it.\n *\n * No counting RULE moves: `checkQuota` is untouched and `report-usage`\n * meters contacts, storage and API requests — never the catalog.\n */\n const [productCountEpoch, setProductCountEpoch] = useState(0)\n const [serverProductCount, setServerProductCount] = useState<number | null>(\n null,\n )\n useEffect(() => {\n let active = true\n void getCountFromServer(collection(firestore, 'hosts', hostId, 'products'))\n .then((snapshot) => {\n if (active) setServerProductCount(snapshot.data().count)\n })\n .catch(() => {\n // Falls back to the live-row count below — a LOWER bound, and this\n // card's prior behaviour. Deliberately not 0: `checkQuota` answers\n // from whatever it is handed, and 0 used is a confident wrong\n // number in the flattering direction.\n })\n return () => {\n active = false\n }\n }, [firestore, hostId, productCountEpoch])\n // The fallback, pending or denied: the live products the table's query has\n // read, probe row included. Each is a product the site has, so it can only\n // UNDERSTATE, never overstate, and nothing it gates fires on a count larger\n // than the truth (AGL-471).\n const loadedProductCount = productWindow?.length ?? 0\n const productCount = serverProductCount ?? loadedProductCount\n // Gate only once the org doc has loaded: an unresolved org reads as the\n // free tier's 0-product cap, which swallowed every Add/Duplicate click.\n // The resources API (AGL-473) stays the authoritative cap on create.\n //\n // `planReady` rather than `org` truthiness (AGL-1064): a host with no\n // owning org never produces one, and waiting on the value alone would\n // hold this open forever. The old fallback allowed UNLIMITED during the\n // window — safe only because the API re-checks; the controls now disable\n // instead, which does not lean on that backstop.\n // NULL means \"not known yet\", which a boolean cannot say — the old\n // fallback object claimed UNLIMITED and every consumer believed it.\n const productQuota = useMemo(\n () =>\n planReady\n ? Aglyn.checkQuota(org, 'productsPerHost', productCount)\n : null,\n [org, planReady, productCount],\n )\n\n const handleDuplicate = useCallback(\n (product: ProductRow) => async () => {\n // Plan unknown — the control is disabled; this guards the race.\n if (!productQuota) return\n if (!productQuota.allowed) {\n return void enqueueSnackbar(\n `Your plan includes ${productQuota.limit} products — upgrade for more`,\n { variant: 'info', persist: false },\n )\n }\n // The source's SKU and barcode keys are not copied: the copy's own are\n // derived below, and a copy of a product with none must have none.\n const { $id: _sourceId, skus: _skus, barcodes: _barcodes, ...copy } =\n product as ProductRow & { skus?: string[]; barcodes?: string[] }\n try {\n /*\n * The copy's slug is asked of the store, so a second Duplicate of\n * the same product does not mint a second `-copy` (AGL-3321), and\n * its search keys are the COPY's: spreading the source's would sort\n * and find it by the source's name.\n */\n const slug = await productSlugLedger(firestore, hostId).claim(\n CommerceModel.commerceSlug(`${product.slug}-copy`),\n )\n // Duplicate is a create — rides the quota-enforcing API (AGL-473).\n await createHostResource({\n hostId,\n resource: 'product',\n data: {\n ...copy,\n ...CommerceModel.productSearchFields({\n name: `${product.name} (copy)`,\n variants: product.variants,\n }),\n ...CommerceModel.productStockFields(product),\n // The COPY's smart collections: its name differs from the source's.\n ...(await productCollectionFields(firestore, hostId, {\n ...product,\n name: `${product.name} (copy)`,\n })),\n slug,\n status: 'draft',\n createdAtMs: Date.now(),\n updatedAtMs: Date.now(),\n },\n })\n // A create moves the count and the aggregate is a one-shot — the\n // listener refreshes the ROWS for free, the count has to be asked\n // again or the cap drifts stale for the rest of the session.\n setProductCountEpoch((epoch) => epoch + 1)\n enqueueSnackbar('Product duplicated as draft', {\n variant: 'success',\n persist: false,\n })\n } catch (error: any) {\n enqueueSnackbar(error?.message ?? 'Could not duplicate product', {\n variant: 'warning',\n persist: false,\n })\n }\n },\n [firestore, hostId, createHostResource, enqueueSnackbar, productQuota],\n )\n\n const handleStatus = useCallback(\n (product: ProductRow, status: CommerceModel.ProductStatus) => async () => {\n // A product with a variant nobody has priced (AGL-2916) stays off the\n // storefront until the editor has a price for each.\n if (status === 'active' && CommerceModel.productPriceMissing(product)) {\n return void enqueueSnackbar(\n `Set a price for every variant of ${product.name} before activating it.`,\n { variant: 'info', persist: false },\n )\n }\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(doc(firestore, 'hosts', hostId, 'products', product.$id), {\n status,\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n }),\n })\n },\n [firestore, hostId, user, enqueueSnackbar],\n )\n\n const handleDelete = useCallback(\n (product: ProductRow) => async () => {\n const confirmed = await confirm({\n title: 'Delete this product?',\n description:\n `\"${product.name}\" stops being purchasable; blocks referencing ` +\n 'it show a checkout error until repointed.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(doc(firestore, 'hosts', hostId, 'products', product.$id), {\n deletedAt: Timestamp.now(),\n }),\n })\n enqueueSnackbar('Product deleted', { variant: 'success', persist: false })\n },\n [confirm, firestore, hostId, user, enqueueSnackbar],\n )\n\n /*\n * CSV import/export (AGL-282): Shopify-dialect columns, dry-run first.\n *\n * The export writes what the table's filters and search MATCH, across the\n * whole catalog (AGL-3321): it walks the table's own query — the same plan,\n * so the same scope, predicates and order — a page of `EXPORT_PAGE` at a\n * time from a cursor, rather than writing the rows on screen. A walk that\n * reaches `EXPORT_CEILING` stops, asks once whether anything is left, and\n * says so when something is.\n */\n const [exporting, setExporting] = useState(false)\n const handleExport = useCallback(async () => {\n setExporting(true)\n try {\n const productsRef = collection(firestore, 'hosts', hostId, 'products')\n const constraints = listQueryConstraints(plan)\n const exported: ProductRow[] = []\n let cursor: QueryDocumentSnapshot | null = null\n let more = false\n for (;;) {\n const room = EXPORT_CEILING - exported.length\n const snapshot = await getDocs(\n query(\n productsRef,\n ...constraints,\n ...(cursor ? [startAfter(cursor)] : []),\n limit(room > 0 ? Math.min(EXPORT_PAGE, room) : 1),\n ),\n )\n if (room <= 0) {\n more = !snapshot.empty\n break\n }\n for (const product of snapshot.docs) {\n exported.push({\n ...CommerceModel.liftLegacyProduct(product.data() as any),\n $id: product.id,\n })\n }\n if (snapshot.docs.length < Math.min(EXPORT_PAGE, room)) break\n cursor = snapshot.docs[snapshot.docs.length - 1]\n }\n const csv = CommerceModel.productsToCsv(exported)\n const blob = new Blob([csv], { type: 'text/csv' })\n const anchor = document.createElement('a')\n anchor.href = URL.createObjectURL(blob)\n anchor.download = `products-${hostId}.csv`\n anchor.click()\n URL.revokeObjectURL(anchor.href)\n if (more) {\n enqueueSnackbar(\n `Exported the first ${EXPORT_CEILING.toLocaleString()} matching ` +\n 'products — the most one file holds. Filter the list to export the rest.',\n { variant: 'info', persist: false },\n )\n }\n } catch (error: any) {\n enqueueSnackbar(error?.message ?? 'Could not export the products', {\n variant: 'warning',\n persist: false,\n })\n } finally {\n setExporting(false)\n }\n }, [firestore, hostId, plan, enqueueSnackbar])\n\n const handleImportApply = useCallback(async () => {\n const parsed = importing?.parsed\n if (!parsed || parsed.products.length === 0) return\n // Batch-aware cap (AGL-471): the whole import must fit the plan.\n // Not startable until the plan is known (AGL-1064) — the old `org ?`\n // guard let an import begin against an unknown cap and leaned on the\n // API rejecting it partway through, leaving a half-imported catalog.\n if (!planReady) return\n const batchQuota = Aglyn.checkQuota(\n org,\n 'productsPerHost',\n productCount + parsed.products.length - 1,\n )\n if (!batchQuota.allowed) {\n return void enqueueSnackbar(\n `This import needs ${parsed.products.length} product slots — your ` +\n `plan allows ${batchQuota.limit}. See Billing to upgrade.`,\n { variant: 'info', persist: false },\n )\n }\n /*\n * The duplicate-slug check asks the STORE (AGL-3321): every slug the file\n * names, by `slug in […]`, and any suffixed one only when it is needed.\n * It asked the table's rows, which held five hundred products and now\n * hold one page, so a slug past them was handed out a second time.\n */\n const slugs = productSlugLedger(firestore, hostId)\n const created: string[] = []\n try {\n await slugs.ask(parsed.products.map((product) => product.slug))\n // One read of the host's smart collections for the whole file.\n const smartCollections = await readSmartCollections(firestore, hostId)\n // Each create rides the quota-enforcing API (AGL-473); the batch cap\n // above short-circuits before we start, so this loop stays bounded.\n for (const product of parsed.products) {\n const slug = await slugs.claim(product.slug)\n const { id } = await createHostResource({\n hostId,\n resource: 'product',\n data: {\n ...product,\n // Search keys travel with the name on the IMPORT path too — a\n // catalog arrives here in bulk, and the table orders, pages and\n // searches it by these keys alone.\n ...CommerceModel.productSearchFields(product),\n ...CommerceModel.productStockFields(product),\n // Its smart collections (AGL-3321).\n collectionIds: CommerceModel.productCollectionIds(product, smartCollections),\n slug,\n priceUsd: product.variants[0]?.priceUsd ?? 0,\n imageUrl: product.mediaUrls?.[0] ?? null,\n createdAtMs: Date.now(),\n updatedAtMs: Date.now(),\n },\n })\n created.push(id)\n }\n } catch (error: any) {\n return void enqueueSnackbar(error?.message ?? 'Import failed', {\n variant: 'warning',\n persist: false,\n })\n }\n setLastImport({\n key: `${Date.now().toString(36)}-${created.length}`,\n productIds: created,\n options: importOptions,\n })\n setImportOptions({})\n setImporting(null)\n setProductCountEpoch((epoch) => epoch + 1)\n enqueueSnackbar(`Imported ${parsed.products.length} products`, {\n variant: 'success',\n persist: false,\n })\n }, [\n importing,\n firestore,\n hostId,\n createHostResource,\n enqueueSnackbar,\n org,\n planReady,\n productCount,\n importOptions,\n ])\n\n const handleAdjustSave = useCallback(async () => {\n if (!adjusting) return\n const delta = Math.round(Number(adjusting.delta))\n if (!delta) return\n const variants = CommerceModel.adjustVariantInventory(\n adjusting.product,\n adjusting.variantId,\n delta,\n adjusting.locationId || undefined,\n )\n /**\n * Refuse the adjustment when the seed is unconfirmed (AGL-1358).\n *\n * `adjustVariantInventory` reads counts off the seeded product and\n * returns a whole new `variants` array, which is then written over the\n * stored one — so this is a full replace of live stock, not a delta, and\n * `merge` could not help. The refusal also has to cover the ADJUSTMENT\n * LOG, which is why the guard wraps both writes: a logged adjustment\n * whose stock write never happened is worse than neither, because the\n * history then disagrees with the count it is supposed to explain.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'stock',\n unreadable: productsStatus === 'error',\n fromCache: productsFromCache,\n },\n async () => {\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(\n doc(firestore, 'hosts', hostId, 'products', adjusting.product.$id),\n {\n variants,\n // The total and the In stock verdict move with the count (AGL-3321).\n ...CommerceModel.productStockFields({ ...adjusting.product, variants }),\n updatedAtMs: Date.now(),\n },\n ),\n })\n // Adjustment history (AGL-281): the same log the sale webhook writes.\n await addDoc(\n collection(firestore, 'hosts', hostId, 'inventoryAdjustments'),\n {\n productId: adjusting.product.$id,\n variantId: adjusting.variantId,\n delta,\n reason: adjusting.reason,\n ...(adjusting.locationId ? { locationId: adjusting.locationId } : {}),\n atMs: Date.now(),\n } satisfies CommerceModel.InventoryAdjustment,\n )\n },\n )\n // Keep the dialog open with the typed delta rather than failing silently.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setAdjusting(null)\n enqueueSnackbar('Stock adjusted', { variant: 'success', persist: false })\n }, [\n user,\n adjusting,\n firestore,\n hostId,\n enqueueSnackbar,\n productsFromCache,\n productsStatus,\n ])\n\n /** The hub's allowance check for a batch of `count` new products, for its zone. */\n const roomFor = useCallback(\n (count: number) =>\n planReady\n ? Aglyn.checkQuota(org, 'productsPerHost', productCount + count - 1)\n : null,\n [org, planReady, productCount],\n )\n const onCatalogCreated = useCallback(\n () => setProductCountEpoch((epoch) => epoch + 1),\n [],\n )\n const setImportOption = useCallback(\n (key: string, on: boolean) =>\n setImportOptions((current) => ({ ...current, [key]: on })),\n [],\n )\n\n const formatPrice = (product: ProductRow) => {\n const [min, max] = CommerceModel.productPriceRange(product)\n return min === max ? `$${min}` : `$${min}–$${max}`\n }\n const formatStock = (product: ProductRow) => {\n const total = CommerceModel.productInventory(product)\n if (total == null) return '—'\n return total > 0 ? String(total) : 'Sold out'\n }\n /**\n * RESERVED UNITS, NAMED (AGL-2356).\n *\n * Checkout now takes a hold on the units a live session is about to buy, and\n * that hold deliberately does NOT move `inventory` — the shelf count means\n * units on the shelf, and half the product reads it that way. The\n * consequence a merchant meets is that the storefront can refuse a sale of\n * the third unit while this column says `3`, and without this caption the\n * number is right and the behaviour looks broken.\n *\n * The house style of `stock-movements-card.component.tsx`: a trailing\n * `caption` in `text.secondary`, rendered ONLY when there is something to\n * say. Nothing is held on the overwhelming majority of products, so the\n * column reads exactly as it does today.\n */\n const formatHeld = (product: ProductRow) => {\n const held = CommerceModel.heldProductUnits(product, nowMs)\n return held > 0 ? ` (${held} reserved)` : ''\n }\n\n const productColumns = listFilterGridColumns(\n [\n {\n field: 'name',\n headerName: 'Product',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }: { row: ProductRow }) => (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`/${row.slug} · id: ${row.$id}`}\n </Typography>\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }: { row: ProductRow }) => (\n <Chip\n label={row.status}\n size=\"small\"\n color={STATUS_COLOR[row.status] ?? 'default'}\n variant=\"outlined\"\n />\n ),\n },\n { field: 'type', headerName: 'Type', width: 110 },\n {\n field: 'priceUsd',\n headerName: 'Price',\n width: 130,\n sortable: false,\n renderCell: ({ row }: { row: ProductRow }) =>\n CommerceModel.productPriceMissing(row) ? (\n <Chip label=\"Set a price\" size=\"small\" color=\"warning\" variant=\"outlined\" />\n ) : (\n formatPrice(row)\n ),\n },\n {\n field: 'stock',\n headerName: 'Stock',\n width: 150,\n sortable: false,\n renderCell: ({ row }: { row: ProductRow }) => (\n <span>\n {formatStock(row)}\n {formatHeld(row) ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"span\">\n {formatHeld(row)}\n </Typography>\n ) : null}\n </span>\n ),\n },\n {\n field: 'variants',\n headerName: 'Variants',\n width: 100,\n sortable: false,\n valueGetter: (_value: unknown, row: ProductRow) => row.variants.length,\n },\n listActionsColumn(\n (product: ProductRow) => (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ justifyContent: 'flex-end', whiteSpace: 'nowrap' }}\n >\n <Button size=\"small\" onClick={() => setEditing(product)}>\n {'Edit'}\n </Button>\n <Button size=\"small\" onClick={handleDuplicate(product)}>\n {'Duplicate'}\n </Button>\n {product.type === 'digital' ? (\n <Button size=\"small\" onClick={() => setKeysFor(product)}>\n {'Keys'}\n </Button>\n ) : null}\n {CommerceModel.productInventory(product) != null ? (\n <Button\n size=\"small\"\n onClick={() =>\n setAdjusting({\n product,\n variantId:\n product.variants.find(\n (variant) => variant.inventory != null,\n )?.id ?? product.variants[0].id,\n delta: '',\n reason: 'restock',\n locationId:\n (locationDocs ?? []).find(\n (location: any) => location.isDefault,\n )?.$id ??\n (locationDocs ?? [])[0]?.$id ??\n '',\n })\n }\n >\n {'Stock'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n onClick={handleStatus(\n product,\n product.status === 'archived' ? 'active' : 'archived',\n )}\n >\n {product.status === 'archived' ? 'Activate' : 'Archive'}\n </Button>\n <Button size=\"small\" color=\"error\" onClick={handleDelete(product)}>\n {'Delete'}\n </Button>\n </Stack>\n ),\n { width: 470 },\n ),\n ] as GridColDef[],\n PRODUCT_LIST_FIELDS,\n PRODUCT_LIST_OPTIONS,\n PRODUCT_LIST_HEADERS,\n )\n\n return (\n <CardDisplay\n /*\n * The header count is the SITE'S catalog, not the rows in hand.\n *\n * It was `products.length`, which is the filtered, ceilinged, now-paged\n * view — so it read 500 on a 25,000-product catalog and would read 10\n * once the table paged. `productCount` is the same server aggregate the\n * quota gate uses. Under a filter there is no honest number to put here:\n * the aggregate counts the whole catalog and the view counts matches, so\n * neither describes what the reader is looking at, and the footer's own\n * count line answers it instead.\n */\n header={\n filtering\n ? 'Products'\n : `Products${productCount ? ` (${productCount})` : ''}`\n }\n help={pluginDocsHelp('commerce', { anchor: '#products-hub' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <Box sx={{ flex: 1 }} />\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!productQuota}\n onClick={() => {\n if (!productQuota) return\n if (!productQuota.allowed) {\n return void enqueueSnackbar(\n `Your plan includes ${productQuota.limit} products — ` +\n 'upgrade for more',\n { variant: 'info', persist: false },\n )\n }\n setCreating(true)\n }}\n >\n {'Add product'}\n </Button>\n <Button\n size=\"small\"\n disabled={!planReady}\n onClick={() => {\n setImportOptions({})\n setImporting({ text: '', parsed: null })\n }}\n >\n {'Import'}\n </Button>\n <Button\n size=\"small\"\n // Nothing matches: there is nothing for the walk to write.\n disabled={exporting || (products.length === 0 && page === 0)}\n onClick={handleExport}\n >\n {exporting ? 'Exporting…' : 'Export'}\n </Button>\n </Stack>\n {/* The cap, standing rather than only on refusal (AGL-2113). The\n count is `productCount` — the site's products, the same number\n the gate above counts — not `products.length`, which is one page\n of the table's query (AGL-1716). */}\n <QuotaReadoutComponent\n ready={productQuota !== null}\n used={productCount}\n limit={productQuota?.limit ?? 0}\n noun=\"product\"\n />\n {/* The zone's widgets are handed the PAGE on screen — the products\n the reader has found and is looking at. Every write the zone makes\n asks the store for what it must not duplicate, never these rows. */}\n <ProductsHubZone\n hostId={hostId}\n products={products}\n roomFor={roomFor}\n lastImport={lastImport}\n onCreated={onCatalogCreated}\n />\n <ListFilterChips\n fields={PRODUCT_LIST_FIELDS}\n headers={PRODUCT_LIST_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={PRODUCT_LIST_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: PRODUCT_LIST_FIELDS,\n headers: PRODUCT_LIST_HEADERS,\n options: PRODUCT_LIST_OPTIONS,\n })}\n notices={plan.notices}\n />\n <ListTable\n aria-label=\"Products\"\n rows={products}\n columns={productColumns}\n loading={productsStatus === 'loading'}\n // One page of the query, turned by the footer below: the grid\n // neither slices, filters nor sorts it.\n hideFooter\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The one order the list offers, by name: the query's.\n sortingMode=\"server\"\n disableColumnSorting\n initialState={{\n columns: {\n columnVisibilityModel: hiddenFilterVisibility(\n PRODUCT_LIST_FIELDS,\n PRODUCT_VISIBLE_COLUMNS,\n ),\n },\n }}\n noRowsLabel={\n filtering ? 'No products match these filters' : 'No products yet'\n }\n noRowsDescription={\n filtering\n ? undefined\n : 'Build your catalog: add a product, then drop commerce ' +\n 'blocks on any page in Besigner.'\n }\n />\n {products.length === 0 && page === 0 && productsStatus !== 'loading' ? null : (\n /* The count line is the PAGE's. `hasMore` is a fact from the probe\n row; the site's total is `productCount` above, which counts the\n whole catalog rather than the matches, so it is not this. */\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={products.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n </Stack>\n <Dialog\n open={Boolean(keysFor)}\n onClose={() => setKeysFor(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{`License keys — ${keysFor?.name ?? ''}`}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n {(() => {\n // The query already asked for this product's keys, so no second\n // filter here: narrowing a window that was already narrowed is how\n // a reader comes to believe the count covers the pool.\n const productKeys = (keyDocs ?? []).slice(0, KEY_POOL_CEILING)\n const available = productKeys.filter(\n (key: any) => !key.assignedAtMs,\n )\n // RETIRED KEYS ARE COUNTED SEPARATELY (AGL-2454). A refund retires\n // the key rather than returning it to the pool — the buyer already\n // holds the string, so reissuing it would give two people one\n // working key — and without this line the merchant simply watches\n // \"available\" fall with nothing to explain where the key went.\n const retired = productKeys.filter((key: any) => key.revokedAtMs)\n const assigned =\n productKeys.length - available.length - retired.length\n return (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ mt: 1 }}>\n {`${available.length} available · ${assigned} assigned` +\n (retired.length\n ? ` · ${retired.length} retired (refunded or revoked — not reissued)`\n : '') +\n (keyPoolTruncated\n ? `, of the ${KEY_POOL_CEILING} keys read`\n : '') +\n '. Keys deliver automatically on purchase (receipt + account).'}\n </Typography>\n {available.slice(0, 8).map((key: any) => (\n <Stack\n key={key.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" sx={{ flex: 1, fontFamily: 'monospace' }} noWrap>\n {key.key}\n </Typography>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n updateDoc(\n doc(firestore, 'hosts', hostId, 'licenseKeys', key.$id),\n { revokedAtMs: Date.now(), assignedAtMs: Date.now() },\n )\n }\n >\n {'Revoke'}\n </Button>\n </Stack>\n ))}\n <TextField\n label=\"Add keys (one per line)\"\n value={keysText}\n onChange={(event) => setKeysText(event.target.value)}\n size=\"small\"\n multiline\n minRows={3}\n />\n </>\n )\n })()}\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setKeysFor(null)}>{'Close'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!keysText.trim()}\n onClick={async () => {\n const keys = keysText\n .split('\\n')\n .map((key) => key.trim())\n .filter(Boolean)\n .slice(0, 200)\n for (const key of keys) {\n await addDoc(\n collection(firestore, 'hosts', hostId, 'licenseKeys'),\n {\n productId: keysFor!.$id,\n key,\n assignedAtMs: null,\n createdAtMs: Date.now(),\n },\n )\n }\n setKeysText('')\n enqueueSnackbar(`Added ${keys.length} keys`, {\n variant: 'success',\n persist: false,\n })\n }}\n >\n {'Add keys'}\n </Button>\n </DialogActions>\n </Dialog>\n <Dialog\n open={Boolean(importing)}\n onClose={() => setImporting(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{'Import products (CSV)'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ mt: 1 }}>\n {'Shopify-compatible columns (Handle, Title, Option/Variant ' +\n 'columns, Image Src). Paste the file contents or choose a file.'}\n </Typography>\n <Button component=\"label\" size=\"small\" sx={{ alignSelf: 'flex-start' }}>\n {'Choose file'}\n <input\n type=\"file\"\n accept=\".csv,text/csv\"\n hidden\n onChange={async (event) => {\n const file = event.target.files?.[0]\n if (!file) return\n const text = await file.text()\n setImporting({ text, parsed: CommerceModel.parseProductsCsv(text) })\n }}\n />\n </Button>\n <TextField\n label=\"CSV\"\n value={importing?.text ?? ''}\n onChange={(event) =>\n setImporting({\n text: event.target.value,\n parsed: event.target.value.trim()\n ? CommerceModel.parseProductsCsv(event.target.value)\n : null,\n })\n }\n size=\"small\"\n multiline\n minRows={5}\n maxRows={10}\n />\n {importing?.parsed ? (\n <>\n <Typography variant=\"body2\">\n {`Ready to import ${importing.parsed.products.length} products` +\n (importing.parsed.errors.length\n ? ` — ${importing.parsed.errors.length} rows skipped:`\n : '')}\n </Typography>\n {importing.parsed.errors.slice(0, 5).map((error) => (\n <Typography\n key={error}\n variant=\"caption\"\n color=\"warning.main\"\n >\n {error}\n </Typography>\n ))}\n </>\n ) : null}\n {WidgetSlot && importing?.parsed?.products.length ? (\n <WidgetSlot\n slot={PRODUCT_IMPORT_ZONE.id}\n hostId={hostId}\n orgId={undefined}\n count={importing.parsed.products.length}\n options={importOptions}\n setOption={setImportOption}\n />\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setImporting(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!importing?.parsed?.products.length}\n onClick={handleImportApply}\n >\n {`Import${importing?.parsed?.products.length ? ` ${importing.parsed.products.length}` : ''}`}\n </Button>\n </DialogActions>\n </Dialog>\n <Dialog\n open={Boolean(adjusting)}\n onClose={() => setAdjusting(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{`Adjust stock — ${adjusting?.product.name ?? ''}`}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Variant\"\n value={adjusting?.variantId ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev ? { ...prev, variantId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n sx={{ mt: 1 }}\n >\n {(adjusting?.product.variants ?? [])\n .filter((variant) => variant.inventory != null)\n .map((variant) => (\n <MenuItem key={variant.id} value={variant.id}>\n {`${Object.values(variant.options ?? {}).join(' / ') || 'Default'} — ${variant.inventory} in stock${\n CommerceModel.heldVariantUnits(\n adjusting?.product,\n variant.id,\n nowMs,\n ) > 0\n ? `, ${CommerceModel.heldVariantUnits(\n adjusting?.product,\n variant.id,\n nowMs,\n )} reserved in checkout`\n : ''\n }`}\n </MenuItem>\n ))}\n </TextField>\n {(locationDocs?.length ?? 0) > 1 ? (\n <TextField\n label=\"Location\"\n value={adjusting?.locationId ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev ? { ...prev, locationId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n >\n {(locationDocs ?? []).map((location: any) => (\n <MenuItem key={location.$id} value={location.$id}>\n {location.name}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n label=\"Change\"\n placeholder=\"+10 or -3\"\n value={adjusting?.delta ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev\n ? {\n ...prev,\n delta: event.target.value.replace(/[^0-9+-]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Reason\"\n value={adjusting?.reason ?? 'restock'}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev\n ? {\n ...prev,\n reason: event.target\n .value as CommerceModel.InventoryAdjustmentReason,\n }\n : prev,\n )\n }\n size=\"small\"\n select\n >\n <MenuItem value=\"restock\">{'Restock'}</MenuItem>\n <MenuItem value=\"correction\">{'Correction'}</MenuItem>\n <MenuItem value=\"damage\">{'Damaged'}</MenuItem>\n <MenuItem value=\"refund\">{'Refund return'}</MenuItem>\n </TextField>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setAdjusting(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!Math.round(Number(adjusting?.delta))}\n onClick={handleAdjustSave}\n >\n {'Apply'}\n </Button>\n </DialogActions>\n </Dialog>\n <ProductEditorDialog\n key={editing?.$id ?? (creating ? 'new' : 'closed')}\n hostId={hostId}\n product={editing}\n // The editor is seeded from a row of THIS listener and replaces the\n // whole document, so the freshness verdict belongs here (AGL-1358) —\n // the dialog has no listener of its own to ask.\n seedFromCache={productsFromCache}\n seedUnreadable={productsStatus === 'error'}\n open={creating || editing !== null}\n onClose={() => {\n setEditing(null)\n setCreating(false)\n // The dialog reports no verdict, so a create and a cancel look\n // the same from here. Re-reading on both is one aggregate and\n // keeps the cap off the dialog's shoulders (AGL-1716).\n setProductCountEpoch((epoch) => epoch + 1)\n }}\n />\n </CardDisplay>\n )\n}\nProductsHubCard.displayName = 'ProductsHubCard'\n\nexport default ProductsHubCard\n"],"names":["Aglyn","CommerceModel","PRODUCT_LIST_BASE","PRODUCT_LIST_FIELDS","PRODUCT_LIST_HEADERS","PRODUCT_LIST_OPTIONS","PRODUCT_LIST_QUERY","PRODUCT_LIST_SELECT_FIELDS","productListRequestClauses","CardDisplay","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listActionsColumn","QuotaReadoutComponent","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","useSnackbar","Timestamp","Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Typography","addDoc","collection","doc","documentId","getCountFromServer","getDocs","limit","orderBy","query","startAfter","updateDoc","where","useCallback","useEffect","useMemo","useState","useFirestore","useUser","writeSiteWideChange","useFirestoreCollection","writeGuardedBySeed","listQueryConstraints","useListQuery","useHostResourceApi","useOrgPlan","ProductEditorDialog","productSlugLedger","productCollectionFields","readSmartCollections","ProductsHubZone","pluginDocsHelp","useConsoleWidgetSlot","PRODUCT_IMPORT_ZONE","EXPORT_PAGE","EXPORT_CEILING","KEY_POOL_CEILING","STATUS_COLOR","active","draft","archived","PRODUCT_VISIBLE_COLUMNS","ProductsHubCard","props","importing","hostId","firestore","data","user","enqueueSnackbar","createHostResource","org","ready","planReady","confirm","gridFilter","selectFields","filtering","clauses","length","searchWords","some","word","trim","editing","setEditing","creating","setCreating","adjusting","setAdjusting","setImporting","keysFor","setKeysFor","keysText","setKeysText","importOptions","setImportOptions","lastImport","setLastImport","WidgetSlot","rows","productDocs","productWindow","hasMore","page","setPage","pageSize","setPageSize","plan","status","productsStatus","fromCache","productsFromCache","declaration","request","search","base","deps","idField","confirmDisappearances","keyDocs","$id","keyPoolTruncated","locationDocs","products","map","product","liftLegacyProduct","nowMs","setNowMs","Date","now","anyHeld","heldProductUnits","undefined","timer","setInterval","clearInterval","productCountEpoch","setProductCountEpoch","serverProductCount","setServerProductCount","then","snapshot","count","catch","loadedProductCount","productCount","productQuota","checkQuota","handleDuplicate","allowed","variant","persist","_sourceId","skus","_skus","barcodes","_barcodes","copy","slug","claim","commerceSlug","resource","productSearchFields","name","variants","productStockFields","createdAtMs","updatedAtMs","epoch","error","message","handleStatus","productPriceMissing","write","batch","update","updatedAt","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","deletedAt","exporting","setExporting","handleExport","productsRef","constraints","exported","cursor","more","room","Math","min","empty","docs","push","id","csv","productsToCsv","blob","Blob","type","anchor","document","createElement","href","URL","createObjectURL","download","click","revokeObjectURL","toLocaleString","handleImportApply","parsed","batchQuota","slugs","created","ask","smartCollections","collectionIds","productCollectionIds","priceUsd","imageUrl","mediaUrls","key","toString","productIds","options","handleAdjustSave","delta","round","Number","adjustVariantInventory","variantId","locationId","verdict","subject","unreadable","productId","reason","atMs","ok","roomFor","onCatalogCreated","setImportOption","on","current","formatPrice","max","productPriceRange","formatStock","total","productInventory","String","formatHeld","held","productColumns","field","headerName","flex","minWidth","renderCell","row","sx","noWrap","width","label","size","sortable","span","component","valueGetter","_value","direction","spacing","justifyContent","whiteSpace","onClick","find","inventory","location","isDefault","header","help","contentGutterX","contentGutterY","xs","sm","disabled","text","used","noun","onCreated","fields","headers","onChange","setClauses","refused","notices","aria-label","columns","loading","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","sortingMode","disableColumnSorting","initialState","columnVisibilityModel","noRowsLabel","noRowsDescription","rowCount","onPageChange","onPageSizeChange","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","productKeys","slice","available","filter","assignedAtMs","retired","revokedAtMs","assigned","mt","alignItems","fontFamily","value","event","target","multiline","minRows","keys","split","alignSelf","input","accept","hidden","file","files","parseProductsCsv","maxRows","errors","slot","orgId","setOption","prev","select","Object","values","join","heldVariantUnits","placeholder","replace","seedFromCache","seedUnreadable","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/products-hub-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 { TransferResumeImport } from '@aglyn/aglyn/app-utils/transfer-resume-import'\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport {\n PRODUCT_LIST_BASE,\n PRODUCT_LIST_FIELDS,\n PRODUCT_LIST_HEADERS,\n PRODUCT_LIST_OPTIONS,\n PRODUCT_LIST_QUERY,\n PRODUCT_LIST_SELECT_FIELDS,\n productListRequestClauses,\n} from '../../constants/product-list-query'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\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 {\n ListTable,\n listActionsColumn,\n} from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport QuotaReadoutComponent from '@aglyn/shared-ui-jsx/components/quota-readout.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport { listFilterGridColumns } from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListGridFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-grid-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport { Timestamp } from '@aglyn/shared-util-timestamp'\nimport {\n Box,\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 addDoc,\n collection,\n doc,\n documentId,\n getCountFromServer,\n limit,\n orderBy,\n query,\n updateDoc,\n where,\n} from 'firebase/firestore'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport { writeSiteWideChange } from '@aglyn/tenant-feature-instance/hooks/helpers/site-wide-change'\nimport {\n useFirestoreCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { useListQuery } from '@aglyn/tenant-feature-instance/hooks/use-list-query'\nimport { useHostResourceApi } from '@aglyn/tenant-feature-instance'\nimport { useOrgPlan } from '@aglyn/tenant-feature-instance'\nimport ProductEditorDialog from './product-editor-dialog.component'\nimport { productSlugLedger } from './product-slugs'\nimport { productCollectionFields } from './smart-collections'\nimport ProductsHubZone from './products-hub-zone.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\nimport { type ConsoleProductsHubZoneProps } from './product-zones'\nimport { commerceListFilter } from '../../transfer/list-filter'\nimport { COMMERCE_PRODUCTS_TRANSFER } from '../../transfer/transfer-keys'\n\n/**\n * How many of ONE product's license keys the dialog reads.\n *\n * Per product, not per store: the query carries the `productId` equality, so\n * this bounds a single pool rather than the site's whole key collection.\n */\nconst KEY_POOL_CEILING = 500\n\nexport interface ProductsHubCardProps {\n hostId: string\n}\n\ntype ProductRow = CommerceModel.HostProduct & { $id: string }\n\nconst STATUS_COLOR: Record<string, 'default' | 'success' | 'warning'> = {\n active: 'success',\n draft: 'warning',\n archived: 'default',\n}\n\n/** The grid's own columns; every other declared field is a hidden filter column. */\nconst PRODUCT_VISIBLE_COLUMNS = ['name', 'status', 'type', 'priceUsd', 'stock', 'variants']\n\n/**\n * Products hub v1 (AGL-279): the catalog manager replacing the Commerce\n * Starter card — a table over `hosts/{hostId}/products` whose search and\n * filters are its Firestore query (AGL-3321), full editor dialog, duplicate, archive/activate, soft delete (past\n * order rows keep resolving). Product cap (`productsPerHost`, AGL-278)\n * gated here on create/duplicate/import (AGL-471); server-side\n * enforcement of the client-write path rides AGL-473.\n */\nexport function ProductsHubCard(props: ProductsHubCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n // Product cap (AGL-471): per-plan `productsPerHost`, same pattern as\n // locations. Console-side gate; server enforcement rides AGL-473.\n const createHostResource = useHostResourceApi()\n const { org, ready: planReady } = useOrgPlan(hostId)\n const { confirm } = useConfirmationContext()\n /*\n * The grid's Filters panel and quick search, bound to the clauses the\n * query below serves (AGL-3321). Every clause and the search word go on\n * that query; see `constants/product-list-query.ts` for what is offered.\n */\n const gridFilter = useListGridFilter({\n selectFields: PRODUCT_LIST_SELECT_FIELDS,\n })\n const filtering =\n gridFilter.clauses.length > 0 ||\n gridFilter.searchWords.some((word) => word.trim())\n const [editing, setEditing] = useState<ProductRow | null>(null)\n const [creating, setCreating] = useState(false)\n const [adjusting, setAdjusting] = useState<{\n product: ProductRow\n variantId: string\n delta: string\n reason: CommerceModel.InventoryAdjustmentReason\n locationId: string\n } | null>(null)\n const [keysFor, setKeysFor] = useState<ProductRow | null>(null)\n const [keysText, setKeysText] = useState('')\n /**\n * What the last import created, with the options its After import step\n * set (AGL-2916, AGL-3531), which the hub's zone hands to its widgets.\n */\n const [lastImport, setLastImport] =\n useState<ConsoleProductsHubZoneProps['lastImport']>(null)\n // The console's import wizard and export dialog; none outside the console.\n const transfer = Aglyn.useTransferLauncher()\n\n const {\n rows: productDocs,\n data: productWindow,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n plan,\n status: productsStatus,\n /**\n * The product rows the stock dialog is seeded from are unconfirmed by\n * the server (AGL-1358). A stock adjustment does not write the delta —\n * it recomputes the WHOLE `variants` array from the seeded product and\n * replaces it, so a cached seed silently reverts every sale, return and\n * adjustment the server has recorded since that snapshot, on every\n * variant and every location.\n */\n fromCache: productsFromCache,\n } = useListQuery<any>({\n /*\n * PAGED BY THE QUERY, and every filter ON it (AGL-3321).\n *\n * The table read a five-hundred-product window and put ONE predicate on\n * it — the search or the Status clause — then matched Status beside a\n * search, dropped soft-deleted products and sorted by name over the rows\n * it held, and paged that. Past five hundred products, a match past the\n * window answered \"no products match\", and a page came back short by\n * however many deleted products it had held.\n *\n * Now the plan puts the search word and every clause on one query under\n * the `deletedAt == null` scope, ordered by `nameLower`, and each page is\n * a page of that query — one page plus a probe row, widened as the reader\n * pages forward. Nothing is matched over the rows afterwards; a clause\n * the query cannot hold is refused by name (`ListQueryNotices` below).\n */\n collection: collection(firestore, 'hosts', hostId, 'products'),\n declaration: PRODUCT_LIST_QUERY,\n request: {\n clauses: productListRequestClauses(gridFilter.clauses),\n search: gridFilter.searchWords,\n base: PRODUCT_LIST_BASE,\n },\n deps: [firestore, hostId],\n idField: '$id',\n /*\n * The scope and the filters read MUTABLE fields — a delete sets\n * `deletedAt`, Archive moves `status`, a rename moves the tokens — so a\n * product can leave this query mid-session, and the SDK can cache that\n * departure as a tombstone at the product's own path (AGL-1196). A\n * disappearance is confirmed against the server before it is believed.\n */\n confirmDisappearances: true,\n })\n /*\n * License key pool (AGL-308) for the open dialog's product — asked FOR that\n * product, and ordered (AGL-2501).\n *\n * This read `limit(500)` over the site's whole `licenseKeys` collection with\n * no `orderBy`, then filtered by `productId` in the browser. Firestore\n * answers an unordered limit in DOCUMENT-ID order, so on a store past five\n * hundred keys the window was an arbitrary five hundred taken across every\n * product — and the \"N available\" line below counted what happened to be in\n * it. A product whose keys all hashed high showed a pool of zero while the\n * storefront went on delivering them.\n *\n * The equality moves into the query, so the window is this product's keys\n * rather than the store's. `orderBy(documentId())` orders on the document\n * NAME, which cannot be absent — every candidate FIELD here (`createdAtMs`,\n * `assignedAtMs`, `revokedAtMs`) is either optional by design or absent on\n * exactly the keys the counts are about. An equality plus an ordering on\n * `__name__` is served by the automatic single-field index, so this adds no\n * composite for anyone to deploy.\n */\n const { data: keyDocs } = useFirestoreCollection<any>(\n () =>\n keysFor\n ? query(\n collection(firestore, 'hosts', hostId, 'licenseKeys'),\n where('productId', '==', keysFor.$id),\n orderBy(documentId()),\n // One past the ceiling, so a pool larger than the window is a\n // fact rather than a guess from `length === 500`.\n limit(KEY_POOL_CEILING + 1),\n )\n : null,\n [firestore, hostId, keysFor?.$id],\n { idField: '$id' },\n )\n /** The key pool is larger than the window, so the counts below are partial. */\n const keyPoolTruncated = (keyDocs?.length ?? 0) > KEY_POOL_CEILING\n // Locations (AGL-286): the stock dialog buckets deltas when they exist.\n const { data: locationDocs } = useFirestoreCollection<any>(\n () => query(collection(firestore, 'hosts', hostId, 'locations'), limit(25)),\n [firestore, hostId],\n { idField: '$id' },\n )\n /*\n * The PAGE on screen, lifted from legacy shapes. It is what the table\n * draws, the reserved-stock clock watches and the zone hands its widgets,\n * and nothing narrows it: the query already answered every filter, and\n * soft-deleted products are outside its scope.\n */\n const products = useMemo<ProductRow[]>(\n () =>\n (productDocs ?? []).map((product: any) => ({\n ...CommerceModel.liftLegacyProduct(product),\n $id: product.$id,\n })),\n [productDocs],\n )\n\n /**\n * A CLOCK, because a hold lapses without anybody writing anything\n * (AGL-2356).\n *\n * The Firestore listener re-renders on document changes, and a reservation\n * expiring is not one — `expiresAtMs` simply passes. Without a tick the\n * \"reserved\" caption below would keep naming a hold that lapsed twenty\n * minutes ago, which is worse than not showing it at all: the merchant would\n * be told stock is spoken for while the storefront happily sells it.\n *\n * Only runs while something is actually held, so an ordinary catalog costs\n * no timer. A NEW hold arrives as a document change, which re-renders, which\n * re-arms this. It watches the page on screen, which is every row whose\n * caption it keeps true.\n */\n const [nowMs, setNowMs] = useState(() => Date.now())\n const anyHeld = useMemo(\n () =>\n products.some(\n (product) => CommerceModel.heldProductUnits(product, nowMs) > 0,\n ),\n [products, nowMs],\n )\n useEffect(() => {\n if (!anyHeld) return undefined\n const timer = setInterval(() => setNowMs(Date.now()), 60_000)\n return () => clearInterval(timer)\n }, [anyHeld])\n\n /**\n * The catalog HEAD-COUNT is a server aggregate, not the length of the\n * capped listener (AGL-1716, the AGL-1706 shape).\n *\n * The listener is `limit(500)` — correctly; a 25,000-product catalog does\n * not belong in a table. What it must not do is answer \"how many products\n * does this site have\", and it did: the length saturated at 500 and was\n * handed to `checkQuota(org, 'productsPerHost', …)` and to the batch check\n * the CSV importer runs. The bands are 2,500 / 10,000 / 25,000 above the\n * window, so on Pro and up the check compared 500 against thousands and\n * could never refuse — the card offered headroom and `api/hosts/resources`\n * then refused the create, which is the AGL-1716 shape exactly.\n *\n * The aggregate is deliberately UNFILTERED, which also closes a second,\n * quieter disagreement: `api/hosts/resources` enforces this quota with a\n * plain `collection('products').count()`, and `softDeletes` there governs\n * only the flat per-host cap on webhooks — so the server has always\n * counted soft-deleted products toward `productsPerHost` while this card\n * excluded them. The card now asks the enforcing route's question, in the\n * enforcing route's terms.\n *\n * The table pages its own query (AGL-3321) and answers a different\n * question — which products match — so it never stands in for this. A\n * one-shot goes stale where a listener refreshed for free, so the count is\n * re-read after any mutation that moves it.\n *\n * No counting RULE moves: `checkQuota` is untouched and `report-usage`\n * meters contacts, storage and API requests — never the catalog.\n */\n const [productCountEpoch, setProductCountEpoch] = useState(0)\n const [serverProductCount, setServerProductCount] = useState<number | null>(\n null,\n )\n useEffect(() => {\n let active = true\n void getCountFromServer(collection(firestore, 'hosts', hostId, 'products'))\n .then((snapshot) => {\n if (active) setServerProductCount(snapshot.data().count)\n })\n .catch(() => {\n // Falls back to the live-row count below — a LOWER bound, and this\n // card's prior behaviour. Deliberately not 0: `checkQuota` answers\n // from whatever it is handed, and 0 used is a confident wrong\n // number in the flattering direction.\n })\n return () => {\n active = false\n }\n }, [firestore, hostId, productCountEpoch])\n // The fallback, pending or denied: the live products the table's query has\n // read, probe row included. Each is a product the site has, so it can only\n // UNDERSTATE, never overstate, and nothing it gates fires on a count larger\n // than the truth (AGL-471).\n const loadedProductCount = productWindow?.length ?? 0\n const productCount = serverProductCount ?? loadedProductCount\n // Gate only once the org doc has loaded: an unresolved org reads as the\n // free tier's 0-product cap, which swallowed every Add/Duplicate click.\n // The resources API (AGL-473) stays the authoritative cap on create.\n //\n // `planReady` rather than `org` truthiness (AGL-1064): a host with no\n // owning org never produces one, and waiting on the value alone would\n // hold this open forever. The old fallback allowed UNLIMITED during the\n // window — safe only because the API re-checks; the controls now disable\n // instead, which does not lean on that backstop.\n // NULL means \"not known yet\", which a boolean cannot say — the old\n // fallback object claimed UNLIMITED and every consumer believed it.\n const productQuota = useMemo(\n () =>\n planReady\n ? Aglyn.checkQuota(org, 'productsPerHost', productCount)\n : null,\n [org, planReady, productCount],\n )\n\n const handleDuplicate = useCallback(\n (product: ProductRow) => async () => {\n // Plan unknown — the control is disabled; this guards the race.\n if (!productQuota) return\n if (!productQuota.allowed) {\n return void enqueueSnackbar(\n `Your plan includes ${productQuota.limit} products — upgrade for more`,\n { variant: 'info', persist: false },\n )\n }\n // The source's SKU and barcode keys are not copied: the copy's own are\n // derived below, and a copy of a product with none must have none.\n const { $id: _sourceId, skus: _skus, barcodes: _barcodes, ...copy } =\n product as ProductRow & { skus?: string[]; barcodes?: string[] }\n try {\n /*\n * The copy's slug is asked of the store, so a second Duplicate of\n * the same product does not mint a second `-copy` (AGL-3321), and\n * its search keys are the COPY's: spreading the source's would sort\n * and find it by the source's name.\n */\n const slug = await productSlugLedger(firestore, hostId).claim(\n CommerceModel.commerceSlug(`${product.slug}-copy`),\n )\n // Duplicate is a create — rides the quota-enforcing API (AGL-473).\n await createHostResource({\n hostId,\n resource: 'product',\n data: {\n ...copy,\n ...CommerceModel.productSearchFields({\n name: `${product.name} (copy)`,\n variants: product.variants,\n }),\n ...CommerceModel.productStockFields(product),\n // The COPY's smart collections: its name differs from the source's.\n ...(await productCollectionFields(firestore, hostId, {\n ...product,\n name: `${product.name} (copy)`,\n })),\n slug,\n status: 'draft',\n createdAtMs: Date.now(),\n updatedAtMs: Date.now(),\n },\n })\n // A create moves the count and the aggregate is a one-shot — the\n // listener refreshes the ROWS for free, the count has to be asked\n // again or the cap drifts stale for the rest of the session.\n setProductCountEpoch((epoch) => epoch + 1)\n enqueueSnackbar('Product duplicated as draft', {\n variant: 'success',\n persist: false,\n })\n } catch (error: any) {\n enqueueSnackbar(error?.message ?? 'Could not duplicate product', {\n variant: 'warning',\n persist: false,\n })\n }\n },\n [firestore, hostId, createHostResource, enqueueSnackbar, productQuota],\n )\n\n const handleStatus = useCallback(\n (product: ProductRow, status: CommerceModel.ProductStatus) => async () => {\n // A product with a variant nobody has priced (AGL-2916) stays off the\n // storefront until the editor has a price for each.\n if (status === 'active' && CommerceModel.productPriceMissing(product)) {\n return void enqueueSnackbar(\n `Set a price for every variant of ${product.name} before activating it.`,\n { variant: 'info', persist: false },\n )\n }\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(doc(firestore, 'hosts', hostId, 'products', product.$id), {\n status,\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n }),\n })\n },\n [firestore, hostId, user, enqueueSnackbar],\n )\n\n const handleDelete = useCallback(\n (product: ProductRow) => async () => {\n const confirmed = await confirm({\n title: 'Delete this product?',\n description:\n `\"${product.name}\" stops being purchasable; blocks referencing ` +\n 'it show a checkout error until repointed.',\n confirmationText: 'Delete',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(doc(firestore, 'hosts', hostId, 'products', product.$id), {\n deletedAt: Timestamp.now(),\n }),\n })\n enqueueSnackbar('Product deleted', { variant: 'success', persist: false })\n },\n [confirm, firestore, hostId, user, enqueueSnackbar],\n )\n\n /*\n * Import and export (AGL-3531) run on the console's transfer wizard and\n * dialog, on the `commerce.products` resource: a product matched by\n * handle, SKU or ID is updated, every step is a choice, and the export\n * writes what the table's query matches — its filters and search — on the\n * server, a row per variant, with a Shopify preset.\n *\n * When the wizard closes on its results, the products the import created\n * reach the hub's zone with the options its After import step set.\n */\n const openImport = useCallback(() => {\n transfer?.openImport({\n resource: COMMERCE_PRODUCTS_TRANSFER,\n scope: 'host',\n hostId,\n onFinished: () => {\n setProductCountEpoch((epoch) => epoch + 1)\n void (async () => {\n const { takeProductImport, createdProductIds } = await import(\n '../../transfer/product-import-results'\n )\n const finished = takeProductImport(hostId)\n if (!finished) return\n const productIds = await createdProductIds(user, finished)\n setLastImport({\n key: `${finished.jobId}-${productIds.length}`,\n productIds,\n options: finished.options,\n })\n })()\n },\n })\n }, [transfer, hostId, user])\n const openExport = useCallback(() => {\n transfer?.openExport({\n resource: COMMERCE_PRODUCTS_TRANSFER,\n scope: 'host',\n hostId,\n ...(filtering\n ? {\n filter: {\n label: 'the products the list’s filters and search find',\n value: commerceListFilter(plan),\n },\n }\n : {}),\n })\n }, [transfer, hostId, filtering, plan])\n\n const handleAdjustSave = useCallback(async () => {\n if (!adjusting) return\n const delta = Math.round(Number(adjusting.delta))\n if (!delta) return\n const variants = CommerceModel.adjustVariantInventory(\n adjusting.product,\n adjusting.variantId,\n delta,\n adjusting.locationId || undefined,\n )\n /**\n * Refuse the adjustment when the seed is unconfirmed (AGL-1358).\n *\n * `adjustVariantInventory` reads counts off the seeded product and\n * returns a whole new `variants` array, which is then written over the\n * stored one — so this is a full replace of live stock, not a delta, and\n * `merge` could not help. The refusal also has to cover the ADJUSTMENT\n * LOG, which is why the guard wraps both writes: a logged adjustment\n * whose stock write never happened is worse than neither, because the\n * history then disagrees with the count it is supposed to explain.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'stock',\n unreadable: productsStatus === 'error',\n fromCache: productsFromCache,\n },\n async () => {\n await writeSiteWideChange({\n firestore,\n user,\n hostId,\n write: (batch) =>\n batch.update(\n doc(firestore, 'hosts', hostId, 'products', adjusting.product.$id),\n {\n variants,\n // The total and the In stock verdict move with the count (AGL-3321).\n ...CommerceModel.productStockFields({ ...adjusting.product, variants }),\n updatedAtMs: Date.now(),\n },\n ),\n })\n // Adjustment history (AGL-281): the same log the sale webhook writes.\n await addDoc(\n collection(firestore, 'hosts', hostId, 'inventoryAdjustments'),\n {\n productId: adjusting.product.$id,\n variantId: adjusting.variantId,\n delta,\n reason: adjusting.reason,\n ...(adjusting.locationId ? { locationId: adjusting.locationId } : {}),\n atMs: Date.now(),\n } satisfies CommerceModel.InventoryAdjustment,\n )\n },\n )\n // Keep the dialog open with the typed delta rather than failing silently.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setAdjusting(null)\n enqueueSnackbar('Stock adjusted', { variant: 'success', persist: false })\n }, [\n user,\n adjusting,\n firestore,\n hostId,\n enqueueSnackbar,\n productsFromCache,\n productsStatus,\n ])\n\n /** The hub's allowance check for a batch of `count` new products, for its zone. */\n const roomFor = useCallback(\n (count: number) =>\n planReady\n ? Aglyn.checkQuota(org, 'productsPerHost', productCount + count - 1)\n : null,\n [org, planReady, productCount],\n )\n const onCatalogCreated = useCallback(\n () => setProductCountEpoch((epoch) => epoch + 1),\n [],\n )\n\n const formatPrice = (product: ProductRow) => {\n const [min, max] = CommerceModel.productPriceRange(product)\n return min === max ? `$${min}` : `$${min}–$${max}`\n }\n const formatStock = (product: ProductRow) => {\n const total = CommerceModel.productInventory(product)\n if (total == null) return '—'\n return total > 0 ? String(total) : 'Sold out'\n }\n /**\n * RESERVED UNITS, NAMED (AGL-2356).\n *\n * Checkout now takes a hold on the units a live session is about to buy, and\n * that hold deliberately does NOT move `inventory` — the shelf count means\n * units on the shelf, and half the product reads it that way. The\n * consequence a merchant meets is that the storefront can refuse a sale of\n * the third unit while this column says `3`, and without this caption the\n * number is right and the behaviour looks broken.\n *\n * The house style of `stock-movements-card.component.tsx`: a trailing\n * `caption` in `text.secondary`, rendered ONLY when there is something to\n * say. Nothing is held on the overwhelming majority of products, so the\n * column reads exactly as it does today.\n */\n const formatHeld = (product: ProductRow) => {\n const held = CommerceModel.heldProductUnits(product, nowMs)\n return held > 0 ? ` (${held} reserved)` : ''\n }\n\n const productColumns = listFilterGridColumns(\n [\n {\n field: 'name',\n headerName: 'Product',\n flex: 1,\n minWidth: 200,\n renderCell: ({ row }: { row: ProductRow }) => (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`/${row.slug} · id: ${row.$id}`}\n </Typography>\n </Stack>\n ),\n },\n {\n field: 'status',\n headerName: 'Status',\n width: 120,\n renderCell: ({ row }: { row: ProductRow }) => (\n <Chip\n label={row.status}\n size=\"small\"\n color={STATUS_COLOR[row.status] ?? 'default'}\n variant=\"outlined\"\n />\n ),\n },\n { field: 'type', headerName: 'Type', width: 110 },\n {\n field: 'priceUsd',\n headerName: 'Price',\n width: 130,\n sortable: false,\n renderCell: ({ row }: { row: ProductRow }) =>\n CommerceModel.productPriceMissing(row) ? (\n <Chip label=\"Set a price\" size=\"small\" color=\"warning\" variant=\"outlined\" />\n ) : (\n formatPrice(row)\n ),\n },\n {\n field: 'stock',\n headerName: 'Stock',\n width: 150,\n sortable: false,\n renderCell: ({ row }: { row: ProductRow }) => (\n <span>\n {formatStock(row)}\n {formatHeld(row) ? (\n <Typography variant=\"caption\" color=\"text.secondary\" component=\"span\">\n {formatHeld(row)}\n </Typography>\n ) : null}\n </span>\n ),\n },\n {\n field: 'variants',\n headerName: 'Variants',\n width: 100,\n sortable: false,\n valueGetter: (_value: unknown, row: ProductRow) => row.variants.length,\n },\n listActionsColumn(\n (product: ProductRow) => (\n <Stack\n direction=\"row\"\n spacing={0.5}\n sx={{ justifyContent: 'flex-end', whiteSpace: 'nowrap' }}\n >\n <Button size=\"small\" onClick={() => setEditing(product)}>\n {'Edit'}\n </Button>\n <Button size=\"small\" onClick={handleDuplicate(product)}>\n {'Duplicate'}\n </Button>\n {product.type === 'digital' ? (\n <Button size=\"small\" onClick={() => setKeysFor(product)}>\n {'Keys'}\n </Button>\n ) : null}\n {CommerceModel.productInventory(product) != null ? (\n <Button\n size=\"small\"\n onClick={() =>\n setAdjusting({\n product,\n variantId:\n product.variants.find(\n (variant) => variant.inventory != null,\n )?.id ?? product.variants[0].id,\n delta: '',\n reason: 'restock',\n locationId:\n (locationDocs ?? []).find(\n (location: any) => location.isDefault,\n )?.$id ??\n (locationDocs ?? [])[0]?.$id ??\n '',\n })\n }\n >\n {'Stock'}\n </Button>\n ) : null}\n <Button\n size=\"small\"\n onClick={handleStatus(\n product,\n product.status === 'archived' ? 'active' : 'archived',\n )}\n >\n {product.status === 'archived' ? 'Activate' : 'Archive'}\n </Button>\n <Button size=\"small\" color=\"error\" onClick={handleDelete(product)}>\n {'Delete'}\n </Button>\n </Stack>\n ),\n { width: 470 },\n ),\n ] as GridColDef[],\n PRODUCT_LIST_FIELDS,\n PRODUCT_LIST_OPTIONS,\n PRODUCT_LIST_HEADERS,\n )\n\n return (\n <CardDisplay\n /*\n * The header count is the SITE'S catalog, not the rows in hand.\n *\n * It was `products.length`, which is the filtered, ceilinged, now-paged\n * view — so it read 500 on a 25,000-product catalog and would read 10\n * once the table paged. `productCount` is the same server aggregate the\n * quota gate uses. Under a filter there is no honest number to put here:\n * the aggregate counts the whole catalog and the view counts matches, so\n * neither describes what the reader is looking at, and the footer's own\n * count line answers it instead.\n */\n header={\n filtering\n ? 'Products'\n : `Products${productCount ? ` (${productCount})` : ''}`\n }\n help={pluginDocsHelp('commerce', { anchor: '#products-hub' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={2}>\n <Stack direction={{ xs: 'column', sm: 'row' }} spacing={1}>\n <Box sx={{ flex: 1 }} />\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!productQuota}\n onClick={() => {\n if (!productQuota) return\n if (!productQuota.allowed) {\n return void enqueueSnackbar(\n `Your plan includes ${productQuota.limit} products — ` +\n 'upgrade for more',\n { variant: 'info', persist: false },\n )\n }\n setCreating(true)\n }}\n >\n {'Add product'}\n </Button>\n {/* Each only for whom the route takes it (AGL-3554); an import\n left unfinished, reopened where it stopped (AGL-3549). */}\n <TransferResumeImport\n target={{ resource: COMMERCE_PRODUCTS_TRANSFER, scope: 'host', hostId }}\n onFinished={() => setProductCountEpoch((epoch) => epoch + 1)}\n />\n {transfer?.can('import', { resource: COMMERCE_PRODUCTS_TRANSFER, scope: 'host', hostId }) ? (\n <Button size=\"small\" disabled={!planReady} onClick={openImport}>\n {'Import'}\n </Button>\n ) : null}\n {transfer?.can('export', { resource: COMMERCE_PRODUCTS_TRANSFER, scope: 'host', hostId }) ? (\n <Button\n size=\"small\"\n // Nothing matches: there is nothing for the export to write.\n disabled={products.length === 0 && page === 0}\n onClick={openExport}\n >\n {'Export'}\n </Button>\n ) : null}\n </Stack>\n {/* The cap, standing rather than only on refusal (AGL-2113). The\n count is `productCount` — the site's products, the same number\n the gate above counts — not `products.length`, which is one page\n of the table's query (AGL-1716). */}\n <QuotaReadoutComponent\n ready={productQuota !== null}\n used={productCount}\n limit={productQuota?.limit ?? 0}\n noun=\"product\"\n />\n {/* The zone's widgets are handed the PAGE on screen — the products\n the reader has found and is looking at. Every write the zone makes\n asks the store for what it must not duplicate, never these rows. */}\n <ProductsHubZone\n hostId={hostId}\n products={products}\n roomFor={roomFor}\n lastImport={lastImport}\n onCreated={onCatalogCreated}\n />\n <ListFilterChips\n fields={PRODUCT_LIST_FIELDS}\n headers={PRODUCT_LIST_HEADERS}\n clauses={gridFilter.clauses}\n onChange={gridFilter.setClauses}\n options={PRODUCT_LIST_OPTIONS}\n />\n <ListQueryNotices\n refused={listQueryRefusals(plan.refused, {\n fields: PRODUCT_LIST_FIELDS,\n headers: PRODUCT_LIST_HEADERS,\n options: PRODUCT_LIST_OPTIONS,\n })}\n notices={plan.notices}\n />\n <ListTable\n aria-label=\"Products\"\n rows={products}\n columns={productColumns}\n loading={productsStatus === 'loading'}\n // One page of the query, turned by the footer below: the grid\n // neither slices, filters nor sorts it.\n hideFooter\n filterMode=\"server\"\n filterModel={gridFilter.filterModel}\n onFilterModelChange={gridFilter.onFilterModelChange}\n quickFilter\n // The one order the list offers, by name: the query's.\n sortingMode=\"server\"\n disableColumnSorting\n initialState={{\n columns: {\n columnVisibilityModel: hiddenFilterVisibility(\n PRODUCT_LIST_FIELDS,\n PRODUCT_VISIBLE_COLUMNS,\n ),\n },\n }}\n noRowsLabel={\n filtering ? 'No products match these filters' : 'No products yet'\n }\n noRowsDescription={\n filtering\n ? undefined\n : 'Build your catalog: add a product, then drop commerce ' +\n 'blocks on any page in Besigner.'\n }\n />\n {products.length === 0 && page === 0 && productsStatus !== 'loading' ? null : (\n /* The count line is the PAGE's. `hasMore` is a fact from the probe\n row; the site's total is `productCount` above, which counts the\n whole catalog rather than the matches, so it is not this. */\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={products.length}\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n </Stack>\n <Dialog\n open={Boolean(keysFor)}\n onClose={() => setKeysFor(null)}\n maxWidth=\"sm\"\n fullWidth\n >\n <DialogTitle>{`License keys — ${keysFor?.name ?? ''}`}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n {(() => {\n // The query already asked for this product's keys, so no second\n // filter here: narrowing a window that was already narrowed is how\n // a reader comes to believe the count covers the pool.\n const productKeys = (keyDocs ?? []).slice(0, KEY_POOL_CEILING)\n const available = productKeys.filter(\n (key: any) => !key.assignedAtMs,\n )\n // RETIRED KEYS ARE COUNTED SEPARATELY (AGL-2454). A refund retires\n // the key rather than returning it to the pool — the buyer already\n // holds the string, so reissuing it would give two people one\n // working key — and without this line the merchant simply watches\n // \"available\" fall with nothing to explain where the key went.\n const retired = productKeys.filter((key: any) => key.revokedAtMs)\n const assigned =\n productKeys.length - available.length - retired.length\n return (\n <>\n <Typography variant=\"body2\" color=\"text.secondary\" sx={{ mt: 1 }}>\n {`${available.length} available · ${assigned} assigned` +\n (retired.length\n ? ` · ${retired.length} retired (refunded or revoked — not reissued)`\n : '') +\n (keyPoolTruncated\n ? `, of the ${KEY_POOL_CEILING} keys read`\n : '') +\n '. Keys deliver automatically on purchase (receipt + account).'}\n </Typography>\n {available.slice(0, 8).map((key: any) => (\n <Stack\n key={key.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Typography variant=\"caption\" sx={{ flex: 1, fontFamily: 'monospace' }} noWrap>\n {key.key}\n </Typography>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n updateDoc(\n doc(firestore, 'hosts', hostId, 'licenseKeys', key.$id),\n { revokedAtMs: Date.now(), assignedAtMs: Date.now() },\n )\n }\n >\n {'Revoke'}\n </Button>\n </Stack>\n ))}\n <TextField\n label=\"Add keys (one per line)\"\n value={keysText}\n onChange={(event) => setKeysText(event.target.value)}\n size=\"small\"\n multiline\n minRows={3}\n />\n </>\n )\n })()}\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setKeysFor(null)}>{'Close'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!keysText.trim()}\n onClick={async () => {\n const keys = keysText\n .split('\\n')\n .map((key) => key.trim())\n .filter(Boolean)\n .slice(0, 200)\n for (const key of keys) {\n await addDoc(\n collection(firestore, 'hosts', hostId, 'licenseKeys'),\n {\n productId: keysFor!.$id,\n key,\n assignedAtMs: null,\n createdAtMs: Date.now(),\n },\n )\n }\n setKeysText('')\n enqueueSnackbar(`Added ${keys.length} keys`, {\n variant: 'success',\n persist: false,\n })\n }}\n >\n {'Add keys'}\n </Button>\n </DialogActions>\n </Dialog>\n <Dialog\n open={Boolean(adjusting)}\n onClose={() => setAdjusting(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{`Adjust stock — ${adjusting?.product.name ?? ''}`}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Variant\"\n value={adjusting?.variantId ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev ? { ...prev, variantId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n sx={{ mt: 1 }}\n >\n {(adjusting?.product.variants ?? [])\n .filter((variant) => variant.inventory != null)\n .map((variant) => (\n <MenuItem key={variant.id} value={variant.id}>\n {`${Object.values(variant.options ?? {}).join(' / ') || 'Default'} — ${variant.inventory} in stock${\n CommerceModel.heldVariantUnits(\n adjusting?.product,\n variant.id,\n nowMs,\n ) > 0\n ? `, ${CommerceModel.heldVariantUnits(\n adjusting?.product,\n variant.id,\n nowMs,\n )} reserved in checkout`\n : ''\n }`}\n </MenuItem>\n ))}\n </TextField>\n {(locationDocs?.length ?? 0) > 1 ? (\n <TextField\n label=\"Location\"\n value={adjusting?.locationId ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev ? { ...prev, locationId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n >\n {(locationDocs ?? []).map((location: any) => (\n <MenuItem key={location.$id} value={location.$id}>\n {location.name}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n <TextField\n label=\"Change\"\n placeholder=\"+10 or -3\"\n value={adjusting?.delta ?? ''}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev\n ? {\n ...prev,\n delta: event.target.value.replace(/[^0-9+-]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n />\n <TextField\n label=\"Reason\"\n value={adjusting?.reason ?? 'restock'}\n onChange={(event) =>\n setAdjusting((prev) =>\n prev\n ? {\n ...prev,\n reason: event.target\n .value as CommerceModel.InventoryAdjustmentReason,\n }\n : prev,\n )\n }\n size=\"small\"\n select\n >\n <MenuItem value=\"restock\">{'Restock'}</MenuItem>\n <MenuItem value=\"correction\">{'Correction'}</MenuItem>\n <MenuItem value=\"damage\">{'Damaged'}</MenuItem>\n <MenuItem value=\"refund\">{'Refund return'}</MenuItem>\n </TextField>\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setAdjusting(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!Math.round(Number(adjusting?.delta))}\n onClick={handleAdjustSave}\n >\n {'Apply'}\n </Button>\n </DialogActions>\n </Dialog>\n <ProductEditorDialog\n key={editing?.$id ?? (creating ? 'new' : 'closed')}\n hostId={hostId}\n product={editing}\n // The editor is seeded from a row of THIS listener and replaces the\n // whole document, so the freshness verdict belongs here (AGL-1358) —\n // the dialog has no listener of its own to ask.\n seedFromCache={productsFromCache}\n seedUnreadable={productsStatus === 'error'}\n open={creating || editing !== null}\n onClose={() => {\n setEditing(null)\n setCreating(false)\n // The dialog reports no verdict, so a create and a cancel look\n // the same from here. Re-reading on both is one aggregate and\n // keeps the cap off the dialog's shoulders (AGL-1716).\n setProductCountEpoch((epoch) => epoch + 1)\n }}\n />\n </CardDisplay>\n )\n}\nProductsHubCard.displayName = 'ProductsHubCard'\n\nexport default ProductsHubCard\n"],"names":["TransferResumeImport","Aglyn","CommerceModel","PRODUCT_LIST_BASE","PRODUCT_LIST_FIELDS","PRODUCT_LIST_HEADERS","PRODUCT_LIST_OPTIONS","PRODUCT_LIST_QUERY","PRODUCT_LIST_SELECT_FIELDS","productListRequestClauses","CardDisplay","useConfirmationContext","ListFilterChips","ListPagination","ListQueryNotices","listQueryRefusals","ListTable","listActionsColumn","QuotaReadoutComponent","hiddenFilterVisibility","listFilterGridColumns","useListGridFilter","useSnackbar","Timestamp","Box","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Typography","addDoc","collection","doc","documentId","getCountFromServer","limit","orderBy","query","updateDoc","where","useCallback","useEffect","useMemo","useState","useFirestore","useUser","writeSiteWideChange","useFirestoreCollection","writeGuardedBySeed","useListQuery","useHostResourceApi","useOrgPlan","ProductEditorDialog","productSlugLedger","productCollectionFields","ProductsHubZone","pluginDocsHelp","commerceListFilter","COMMERCE_PRODUCTS_TRANSFER","KEY_POOL_CEILING","STATUS_COLOR","active","draft","archived","PRODUCT_VISIBLE_COLUMNS","ProductsHubCard","props","hostId","firestore","data","user","enqueueSnackbar","createHostResource","org","ready","planReady","confirm","gridFilter","selectFields","filtering","clauses","length","searchWords","some","word","trim","editing","setEditing","creating","setCreating","adjusting","setAdjusting","keysFor","setKeysFor","keysText","setKeysText","lastImport","setLastImport","transfer","useTransferLauncher","rows","productDocs","productWindow","hasMore","page","setPage","pageSize","setPageSize","plan","status","productsStatus","fromCache","productsFromCache","declaration","request","search","base","deps","idField","confirmDisappearances","keyDocs","$id","keyPoolTruncated","locationDocs","products","map","product","liftLegacyProduct","nowMs","setNowMs","Date","now","anyHeld","heldProductUnits","undefined","timer","setInterval","clearInterval","productCountEpoch","setProductCountEpoch","serverProductCount","setServerProductCount","then","snapshot","count","catch","loadedProductCount","productCount","productQuota","checkQuota","handleDuplicate","allowed","variant","persist","_sourceId","skus","_skus","barcodes","_barcodes","copy","slug","claim","commerceSlug","resource","productSearchFields","name","variants","productStockFields","createdAtMs","updatedAtMs","epoch","error","message","handleStatus","productPriceMissing","write","batch","update","updatedAt","handleDelete","confirmed","title","description","confirmationText","confirmationButtonProps","color","deletedAt","openImport","scope","onFinished","takeProductImport","createdProductIds","finished","productIds","key","jobId","options","openExport","filter","label","value","handleAdjustSave","delta","Math","round","Number","adjustVariantInventory","variantId","locationId","verdict","subject","unreadable","productId","reason","atMs","ok","roomFor","onCatalogCreated","formatPrice","min","max","productPriceRange","formatStock","total","productInventory","String","formatHeld","held","productColumns","field","headerName","flex","minWidth","renderCell","row","sx","noWrap","width","size","sortable","span","component","valueGetter","_value","direction","spacing","justifyContent","whiteSpace","onClick","type","find","inventory","id","location","isDefault","header","help","anchor","contentGutterX","contentGutterY","xs","sm","disabled","target","can","used","noun","onCreated","fields","headers","onChange","setClauses","refused","notices","aria-label","columns","loading","hideFooter","filterMode","filterModel","onFilterModelChange","quickFilter","sortingMode","disableColumnSorting","initialState","columnVisibilityModel","noRowsLabel","noRowsDescription","rowCount","onPageChange","onPageSizeChange","open","Boolean","onClose","maxWidth","fullWidth","display","flexDirection","gap","productKeys","slice","available","assignedAtMs","retired","revokedAtMs","assigned","mt","alignItems","fontFamily","event","multiline","minRows","keys","split","prev","select","Object","values","join","heldVariantUnits","placeholder","replace","seedFromCache","seedUnreadable","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The commerce plugin's CONSOLE-ONLY server declarations, named under
|
|
19
|
+
* `consoleServerDeclarations` in `plugins.config.json` and run at the
|
|
20
|
+
* console's boot. The tenant runtime registers none of it.
|
|
21
|
+
*
|
|
22
|
+
* It serves a site's products and orders on the customer REST API,
|
|
23
|
+
* `/v1/sites/{siteId}/products/…` and `/v1/sites/{siteId}/orders/…`: the
|
|
24
|
+
* console's router owns the pipeline in front — the key, the plan's API
|
|
25
|
+
* access, the quota, the rate limit, the error envelope — and the site's
|
|
26
|
+
* ownership, and hands every request under each resource here.
|
|
27
|
+
*
|
|
28
|
+
* No plan feature is named on the registrations, though both need
|
|
29
|
+
* `commerce`: the handlers ask the key's scope FIRST and the plan second
|
|
30
|
+
* (AGL-900, AGL-1928), so a key without the scope is told about the scope
|
|
31
|
+
* whatever plan its organization is on, and the router would ask in the
|
|
32
|
+
* opposite order.
|
|
33
|
+
*
|
|
34
|
+
* Light at boot: the handlers are imported with their first request and the
|
|
35
|
+
* descriptions when the document is first built. Registering again replaces
|
|
36
|
+
* this plugin's own entries.
|
|
37
|
+
*/
|
|
38
|
+
export declare function registerCommerceConsoleServerDeclarations(): void;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ // The registry's own module, not the data layer's barrel: boot needs the
|
|
17
|
+
// registry, not the whole server surface.
|
|
18
|
+
import { registerApiV1SiteResource } from "@aglyn/tenant-data-admin/server/api-v1-resources";
|
|
19
|
+
import { registerPluginPersonEraser } from "@aglyn/aglyn/plugin-manager/plugin-person-erasure";
|
|
20
|
+
import { BUNDLE_ID } from "./constants/bundle-common.js";
|
|
21
|
+
import { registerCommerceTransferResources } from "./transfer/register-transfer-resources.js";
|
|
22
|
+
/**
|
|
23
|
+
* The commerce plugin's CONSOLE-ONLY server declarations, named under
|
|
24
|
+
* `consoleServerDeclarations` in `plugins.config.json` and run at the
|
|
25
|
+
* console's boot. The tenant runtime registers none of it.
|
|
26
|
+
*
|
|
27
|
+
* It serves a site's products and orders on the customer REST API,
|
|
28
|
+
* `/v1/sites/{siteId}/products/…` and `/v1/sites/{siteId}/orders/…`: the
|
|
29
|
+
* console's router owns the pipeline in front — the key, the plan's API
|
|
30
|
+
* access, the quota, the rate limit, the error envelope — and the site's
|
|
31
|
+
* ownership, and hands every request under each resource here.
|
|
32
|
+
*
|
|
33
|
+
* No plan feature is named on the registrations, though both need
|
|
34
|
+
* `commerce`: the handlers ask the key's scope FIRST and the plan second
|
|
35
|
+
* (AGL-900, AGL-1928), so a key without the scope is told about the scope
|
|
36
|
+
* whatever plan its organization is on, and the router would ask in the
|
|
37
|
+
* opposite order.
|
|
38
|
+
*
|
|
39
|
+
* Light at boot: the handlers are imported with their first request and the
|
|
40
|
+
* descriptions when the document is first built. Registering again replaces
|
|
41
|
+
* this plugin's own entries.
|
|
42
|
+
*/ export function registerCommerceConsoleServerDeclarations() {
|
|
43
|
+
// Commerce's share of a person erasure (AGL-2623, AGL-3080): every order the
|
|
44
|
+
// person placed keeps its amounts, with the buyer taken off it. Required,
|
|
45
|
+
// and loaded with the first erasure.
|
|
46
|
+
registerPluginPersonEraser(async (request)=>(await import("./server/person-eraser.js")).commercePersonEraser(request), {
|
|
47
|
+
pluginId: BUNDLE_ID
|
|
48
|
+
});
|
|
49
|
+
registerApiV1SiteResource('products', {
|
|
50
|
+
handle: async (...args)=>(await import("./server/api-v1/orders-and-products.js")).handleProducts(...args),
|
|
51
|
+
describe: async ()=>(await import("./server/api-v1/openapi.js")).PRODUCTS_API_V1_DESCRIPTION
|
|
52
|
+
}, {
|
|
53
|
+
pluginId: BUNDLE_ID
|
|
54
|
+
});
|
|
55
|
+
registerApiV1SiteResource('orders', {
|
|
56
|
+
handle: async (...args)=>(await import("./server/api-v1/orders-and-products.js")).handleOrders(...args),
|
|
57
|
+
describe: async ()=>(await import("./server/api-v1/openapi.js")).ORDERS_API_V1_DESCRIPTION
|
|
58
|
+
}, {
|
|
59
|
+
pluginId: BUNDLE_ID
|
|
60
|
+
});
|
|
61
|
+
// Products, categories, orders, discounts, coupons and gift cards on the
|
|
62
|
+
// import wizard and the export dialog (AGL-3531): the transfer routes are
|
|
63
|
+
// the console's alone, so they register here.
|
|
64
|
+
registerCommerceTransferResources();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=declarations.console-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/declarations.console-server.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// The registry's own module, not the data layer's barrel: boot needs the\n// registry, not the whole server surface.\nimport { registerApiV1SiteResource } from '@aglyn/tenant-data-admin/server/api-v1-resources'\nimport { registerPluginPersonEraser } from '@aglyn/aglyn/plugin-manager/plugin-person-erasure'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerCommerceTransferResources } from './transfer/register-transfer-resources'\n\n/**\n * The commerce plugin's CONSOLE-ONLY server declarations, named under\n * `consoleServerDeclarations` in `plugins.config.json` and run at the\n * console's boot. The tenant runtime registers none of it.\n *\n * It serves a site's products and orders on the customer REST API,\n * `/v1/sites/{siteId}/products/…` and `/v1/sites/{siteId}/orders/…`: the\n * console's router owns the pipeline in front — the key, the plan's API\n * access, the quota, the rate limit, the error envelope — and the site's\n * ownership, and hands every request under each resource here.\n *\n * No plan feature is named on the registrations, though both need\n * `commerce`: the handlers ask the key's scope FIRST and the plan second\n * (AGL-900, AGL-1928), so a key without the scope is told about the scope\n * whatever plan its organization is on, and the router would ask in the\n * opposite order.\n *\n * Light at boot: the handlers are imported with their first request and the\n * descriptions when the document is first built. Registering again replaces\n * this plugin's own entries.\n */\nexport function registerCommerceConsoleServerDeclarations(): void {\n // Commerce's share of a person erasure (AGL-2623, AGL-3080): every order the\n // person placed keeps its amounts, with the buyer taken off it. Required,\n // and loaded with the first erasure.\n registerPluginPersonEraser(\n async (request) => (await import('./server/person-eraser')).commercePersonEraser(request),\n { pluginId: BUNDLE_ID },\n )\n registerApiV1SiteResource(\n 'products',\n {\n handle: async (...args) =>\n (await import('./server/api-v1/orders-and-products')).handleProducts(...args),\n describe: async () =>\n (await import('./server/api-v1/openapi')).PRODUCTS_API_V1_DESCRIPTION,\n },\n { pluginId: BUNDLE_ID },\n )\n registerApiV1SiteResource(\n 'orders',\n {\n handle: async (...args) =>\n (await import('./server/api-v1/orders-and-products')).handleOrders(...args),\n describe: async () =>\n (await import('./server/api-v1/openapi')).ORDERS_API_V1_DESCRIPTION,\n },\n { pluginId: BUNDLE_ID },\n )\n // Products, categories, orders, discounts, coupons and gift cards on the\n // import wizard and the export dialog (AGL-3531): the transfer routes are\n // the console's alone, so they register here.\n registerCommerceTransferResources()\n}\n"],"names":["registerApiV1SiteResource","registerPluginPersonEraser","BUNDLE_ID","registerCommerceTransferResources","registerCommerceConsoleServerDeclarations","request","commercePersonEraser","pluginId","handle","args","handleProducts","describe","PRODUCTS_API_V1_DESCRIPTION","handleOrders","ORDERS_API_V1_DESCRIPTION"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,yEAAyE;AACzE,0CAA0C;AAC1C,SAASA,yBAAyB,QAAQ,mDAAkD;AAC5F,SAASC,0BAA0B,QAAQ,oDAAmD;AAC9F,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,iCAAiC,QAAQ,4CAAwC;AAE1F;;;;;;;;;;;;;;;;;;;;CAoBC,GACD,OAAO,SAASC;IACd,6EAA6E;IAC7E,0EAA0E;IAC1E,qCAAqC;IACrCH,2BACE,OAAOI,UAAY,AAAC,CAAA,MAAM,MAAM,CAAC,4BAAwB,EAAGC,oBAAoB,CAACD,UACjF;QAAEE,UAAUL;IAAU;IAExBF,0BACE,YACA;QACEQ,QAAQ,OAAO,GAAGC,OAChB,AAAC,CAAA,MAAM,MAAM,CAAC,yCAAqC,EAAGC,cAAc,IAAID;QAC1EE,UAAU,UACR,AAAC,CAAA,MAAM,MAAM,CAAC,6BAAyB,EAAGC,2BAA2B;IACzE,GACA;QAAEL,UAAUL;IAAU;IAExBF,0BACE,UACA;QACEQ,QAAQ,OAAO,GAAGC,OAChB,AAAC,CAAA,MAAM,MAAM,CAAC,yCAAqC,EAAGI,YAAY,IAAIJ;QACxEE,UAAU,UACR,AAAC,CAAA,MAAM,MAAM,CAAC,6BAAyB,EAAGG,yBAAyB;IACvE,GACA;QAAEP,UAAUL;IAAU;IAExB,yEAAyE;IACzE,0EAA0E;IAC1E,8CAA8C;IAC9CC;AACF"}
|
|
@@ -135,3 +135,29 @@ export declare function giftCardAvailableCents(card: HostGiftCard | undefined, n
|
|
|
135
135
|
* holds), and both must be no-ops rather than a second decrement.
|
|
136
136
|
*/
|
|
137
137
|
export declare function giftCardSettlementCents(card: HostGiftCard | undefined, sessionId: string, _nowMs: number): number;
|
|
138
|
+
/** The most one card may be issued for, by hand or from a file. */
|
|
139
|
+
export declare const GIFT_CARD_ISSUE_MAX_CENTS = 100000;
|
|
140
|
+
/** The currency every card is held in: the store charges in US dollars. */
|
|
141
|
+
export declare const GIFT_CARD_CURRENCY = "USD";
|
|
142
|
+
/**
|
|
143
|
+
* A code as the store keeps it: upper case, with no spaces — a code another
|
|
144
|
+
* platform printed in groups (`ABCD EFGH 1234`) is the same code joined.
|
|
145
|
+
*/
|
|
146
|
+
export declare function giftCardCodeOf(value: unknown): string;
|
|
147
|
+
/** The shortest code a card may carry: anything shorter can be guessed at a checkout. */
|
|
148
|
+
export declare const GIFT_CARD_CODE_MIN = 8;
|
|
149
|
+
export declare const GIFT_CARD_CODE_MAX = 40;
|
|
150
|
+
/** Why a code cannot be a card's code, or `null`. A code is the card's document id and spends it. */
|
|
151
|
+
export declare function giftCardCodeProblem(code: string): string | null;
|
|
152
|
+
/** Why a card cannot be issued for this many cents, or `null`. */
|
|
153
|
+
export declare function giftCardAmountProblem(amountCents: number): string | null;
|
|
154
|
+
/**
|
|
155
|
+
* Whether anybody has spent from a card: a redemption settled on it
|
|
156
|
+
* (`lastUsedAtMs`, a balance below what it was issued for), or a checkout
|
|
157
|
+
* holding part of it — expired holds included, because a lapsed hold is
|
|
158
|
+
* still settled when its session is paid (`giftCardSettlementCents`).
|
|
159
|
+
*/
|
|
160
|
+
export declare function giftCardRedeemed(card: (HostGiftCard & {
|
|
161
|
+
initialCents?: number;
|
|
162
|
+
lastUsedAtMs?: number;
|
|
163
|
+
}) | undefined): boolean;
|
|
@@ -155,5 +155,47 @@ _nowMs) {
|
|
|
155
155
|
const hold = ((_ref = card == null ? void 0 : card.holds) != null ? _ref : {})[sessionId];
|
|
156
156
|
return Math.min(cents(hold == null ? void 0 : hold.cents), cents(card == null ? void 0 : card.balanceCents));
|
|
157
157
|
}
|
|
158
|
+
/*==========================================
|
|
159
|
+
* ISSUING A CARD (AGL-2226, AGL-3551).
|
|
160
|
+
*
|
|
161
|
+
* A card is issued, never written: by hand from the Gift cards card, or one
|
|
162
|
+
* card per row from an import, both through `issueGiftCard`
|
|
163
|
+
* (`server/gift-card-issue.ts`). These are the rules both doors share.
|
|
164
|
+
*=========================================*/ /** The most one card may be issued for, by hand or from a file. */ export const GIFT_CARD_ISSUE_MAX_CENTS = 100000;
|
|
165
|
+
/** The currency every card is held in: the store charges in US dollars. */ export const GIFT_CARD_CURRENCY = 'USD';
|
|
166
|
+
/**
|
|
167
|
+
* A code as the store keeps it: upper case, with no spaces — a code another
|
|
168
|
+
* platform printed in groups (`ABCD EFGH 1234`) is the same code joined.
|
|
169
|
+
*/ export function giftCardCodeOf(value) {
|
|
170
|
+
return String(value != null ? value : '').trim().replace(/\s+/g, '').toUpperCase();
|
|
171
|
+
}
|
|
172
|
+
/** The shortest code a card may carry: anything shorter can be guessed at a checkout. */ export const GIFT_CARD_CODE_MIN = 8;
|
|
173
|
+
export const GIFT_CARD_CODE_MAX = 40;
|
|
174
|
+
/** Why a code cannot be a card's code, or `null`. A code is the card's document id and spends it. */ export function giftCardCodeProblem(code) {
|
|
175
|
+
if (code.length < GIFT_CARD_CODE_MIN || code.length > GIFT_CARD_CODE_MAX || !/^[A-Z0-9][A-Z0-9_-]*$/.test(code)) {
|
|
176
|
+
return `A gift card code is ${GIFT_CARD_CODE_MIN} to ${GIFT_CARD_CODE_MAX} letters, digits, ` + 'hyphens or underscores. Leave the code blank and a new one is made.';
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
/** Why a card cannot be issued for this many cents, or `null`. */ export function giftCardAmountProblem(amountCents) {
|
|
181
|
+
if (!Number.isInteger(amountCents) || amountCents <= 0) return 'A gift card is issued for more than $0.';
|
|
182
|
+
if (amountCents > GIFT_CARD_ISSUE_MAX_CENTS) {
|
|
183
|
+
return `A gift card may be issued for at most $${(GIFT_CARD_ISSUE_MAX_CENTS / 100).toLocaleString('en-US')}.`;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Whether anybody has spent from a card: a redemption settled on it
|
|
189
|
+
* (`lastUsedAtMs`, a balance below what it was issued for), or a checkout
|
|
190
|
+
* holding part of it — expired holds included, because a lapsed hold is
|
|
191
|
+
* still settled when its session is paid (`giftCardSettlementCents`).
|
|
192
|
+
*/ export function giftCardRedeemed(card) {
|
|
193
|
+
var _card_holds;
|
|
194
|
+
if (!card) return false;
|
|
195
|
+
if (Number(card.lastUsedAtMs) > 0) return true;
|
|
196
|
+
if (Object.keys((_card_holds = card.holds) != null ? _card_holds : {}).length) return true;
|
|
197
|
+
const initial = Number(card.initialCents);
|
|
198
|
+
return Number.isFinite(initial) && cents(card.balanceCents) < initial;
|
|
199
|
+
}
|
|
158
200
|
|
|
159
201
|
//# sourceMappingURL=commerce-gift-cards.js.map
|