@aglyn/plugins-commerce 1.0.0-beta.196 → 1.0.0-beta.198
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/plugins-commerce",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.198",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,18 +25,18 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/aglyn": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-ui-jsx": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-ui-next": "1.0.0-beta.
|
|
32
|
-
"@aglyn/shared-ui-snackstack": "1.0.0-beta.
|
|
33
|
-
"@aglyn/shared-util-email": "1.0.0-beta.
|
|
34
|
-
"@aglyn/shared-util-http": "1.0.0-beta.
|
|
35
|
-
"@aglyn/shared-util-timestamp": "1.0.0-beta.
|
|
36
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
37
|
-
"@aglyn/tenant-data-admin": "1.0.0-beta.
|
|
38
|
-
"@aglyn/tenant-feature-instance": "1.0.0-beta.
|
|
39
|
-
"@aglyn/tenant-runtime": "1.0.0-beta.
|
|
28
|
+
"@aglyn/aglyn": "1.0.0-beta.198",
|
|
29
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.198",
|
|
30
|
+
"@aglyn/shared-ui-jsx": "1.0.0-beta.198",
|
|
31
|
+
"@aglyn/shared-ui-next": "1.0.0-beta.198",
|
|
32
|
+
"@aglyn/shared-ui-snackstack": "1.0.0-beta.198",
|
|
33
|
+
"@aglyn/shared-util-email": "1.0.0-beta.198",
|
|
34
|
+
"@aglyn/shared-util-http": "1.0.0-beta.198",
|
|
35
|
+
"@aglyn/shared-util-timestamp": "1.0.0-beta.198",
|
|
36
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.198",
|
|
37
|
+
"@aglyn/tenant-data-admin": "1.0.0-beta.198",
|
|
38
|
+
"@aglyn/tenant-feature-instance": "1.0.0-beta.198",
|
|
39
|
+
"@aglyn/tenant-runtime": "1.0.0-beta.198",
|
|
40
40
|
"@stripe/react-stripe-js": "^3.10.0",
|
|
41
41
|
"@stripe/stripe-js": "^5.10.0",
|
|
42
42
|
"@swc/helpers": "0.5.23",
|
|
@@ -587,7 +587,7 @@ const ORDER_CHANNEL_OPTIONS = Object.keys(CommerceModel.ORDER_CHANNEL_LABELS).ma
|
|
|
587
587
|
return /*#__PURE__*/ _jsxs(CardDisplay, {
|
|
588
588
|
header: 'Orders',
|
|
589
589
|
help: pluginDocsHelp('commerce', {
|
|
590
|
-
anchor: '#orders',
|
|
590
|
+
anchor: '#orders-screen',
|
|
591
591
|
excerpt: 'Every order with its channel, status and net total. A refunded ' + 'order shows what is left, with the gross beneath it.'
|
|
592
592
|
}),
|
|
593
593
|
contentGutterX: true,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/host-orders-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n inMemoryListField,\n type ListFilterClause,\n upsertListFilterClause,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-rows-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n checkEntitlement,\n ORDERS_CUSTOMER_PARAM,\n ORDERS_ORDER_PARAM,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport {\n Alert,\n AlertTitle,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, getDoc, limit, orderBy, query } from 'firebase/firestore'\nimport { useSearchParams } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useOrgPlan,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { useFirestoreCollection } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\nimport CommerceStatTile from './commerce-stat-tile.component'\nimport OrderDetailDialog, {\n DISPUTE_COLOR,\n} from './order-detail-dialog.component'\n\n/**\n * How far back the table, its filters and its export reach.\n *\n * This window cannot be paged the way a plain list can. The Filters panel,\n * the search and the CSV export all run over what was read, so a page of ten\n * would quietly narrow every one of them — a status filter would search a\n * tenth of the orders and report confidently on what it found. Bounding the read and saying where the\n * bound falls keeps the filters honest about their own scope.\n */\nconst ORDERS_WINDOW = 200\n\n/** A name map for the filter menu; it renders no product rows of its own. */\nconst PRODUCT_NAME_WINDOW = 100\n\n/*\n * What the orders grid's Filters panel offers. The card holds its whole\n * window, so the panel and the search answer over every order it read.\n *\n * Status and Channel are the order's own enumerations; Disputes is apart\n * from Status on purpose, because the two are orthogonal: an OPEN dispute\n * sits on an order that is still `paid`, and a lost one on `refunded`\n * beside every ordinary refund. Product is the ids of every line item, so\n * a cart, POS or draft order is found by any product it holds. The date is\n * the field the query orders by, which every order writer stamps.\n */\nconst ORDER_FILTER_FIELDS = [\n inMemoryListField('orderLabel', 'text'),\n inMemoryListField('customerEmail', 'text'),\n inMemoryListField('channelKey', 'select'),\n inMemoryListField('statusKey', 'select'),\n inMemoryListField('createdAtMs', 'date'),\n { ...inMemoryListField('productIds', 'select'), tokensPath: 'productIds', verbatimTokens: true },\n inMemoryListField('disputeKey', 'select'),\n]\nconst ORDER_FILTER_HEADERS: Readonly<Record<string, string>> = {\n orderLabel: 'Order',\n customerEmail: 'Customer',\n channelKey: 'Channel',\n statusKey: 'Status',\n createdAtMs: 'Date',\n productIds: 'Product',\n disputeKey: 'Disputes',\n}\nconst ORDER_STATUS_OPTIONS = (\n Object.keys(CommerceModel.ORDER_STATUS_LABELS) as (keyof typeof CommerceModel.ORDER_STATUS_LABELS)[]\n).map((status) => ({ value: status, label: CommerceModel.ORDER_STATUS_LABELS[status] }))\nconst ORDER_CHANNEL_OPTIONS = (\n Object.keys(CommerceModel.ORDER_CHANNEL_LABELS) as (keyof typeof CommerceModel.ORDER_CHANNEL_LABELS)[]\n).map((channel) => ({ value: channel, label: CommerceModel.ORDER_CHANNEL_LABELS[channel] }))\n/*\n * `lost` is the badge's tone, not the raw dispute status: a dispute that was\n * WON also closes with money untouched, and listing it as charged back would\n * tell a merchant they lost a case they won.\n */\nconst ORDER_DISPUTE_OPTIONS = [\n { value: 'open', label: 'Open dispute' },\n { value: 'lost', label: 'Charged back' },\n]\n/** What the quick search reads on an order row. */\nconst ORDER_SEARCH_FIELDS = ['orderLabel', 'customerEmail', 'itemName'] as const\n/** The filter-only columns never show. */\nconst ORDER_HIDDEN_COLUMNS = hiddenFilterVisibility(ORDER_FILTER_FIELDS, [\n 'orderLabel',\n 'customerEmail',\n 'channelKey',\n 'statusKey',\n 'createdAtMs',\n])\n\n/** The clause the dispute banner's \"Show them\" sets. */\nconst OPEN_DISPUTE_CLAUSE: ListFilterClause = {\n field: 'disputeKey',\n op: 'equals',\n value: 'open',\n}\n\nexport interface HostOrdersCardProps {\n hostId: string\n}\n\n/**\n * Orders console (AGL-287): filterable list over webhook-written order\n * docs with a detail dialog (timeline, fulfill, refund, cancel, notes,\n * packing slip) and draft orders that send the buyer a payment link.\n */\nexport function HostOrdersCard(props: HostOrdersCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n /**\n * The most recent orders, in the order the table shows them.\n *\n * `createdAtMs` is the field every order writer in the plugin stamps and the\n * one `reconcile-stock` already walks the collection by. Ordering on it is\n * what makes this window the RECENT orders: an unordered cap is answered in\n * document-id order, and orders are keyed by generated ids, so it returns a\n * pseudo-random sample that a client sort then dresses as newest-first.\n */\n const { data: orderDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'orders'),\n orderBy('createdAtMs', 'desc'),\n limit(ORDERS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { data: productDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'products'),\n PRODUCT_NAME_WINDOW,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const orderWindow = useMemo(\n () => ceilingedWindow<any>(orderDocs ?? undefined, ORDERS_WINDOW),\n [orderDocs],\n )\n const productWindow = useMemo(\n () => ceilingedWindow<any>(productDocs ?? undefined, PRODUCT_NAME_WINDOW),\n [productDocs],\n )\n const productNames = useMemo(() => {\n const map: Record<string, string> = {}\n for (const product of productWindow.rows) {\n map[product.$id] = product.name ?? product.$id\n }\n return map\n }, [productWindow])\n\n /**\n * The money tiles are the `commerceAnalytics` surface (AGL-1938,\n * AGL-2056), and this is the third place they appear. Rendering them\n * ungated here would undo both of those passes and hand a Starter org\n * the Pro figures one tab away from the upgrade prompt that refuses\n * them. The TABLE below is not gated — a list of your own orders is not\n * a paid feature.\n */\n const { org, ready: orgReady } = useOrgPlan(hostId)\n const showStats =\n orgReady && checkEntitlement(org as never, 'commerceAnalytics')\n\n // The query returns the window already newest-first; re-sorting it here\n // would only restate the order it arrived in.\n const orders = orderWindow.rows\n\n /*\n * The grid's clauses (AGL-96, AGL-3317), held here so two things outside\n * the grid can set one: the CRM's contact page links with the buyer's\n * address to answer \"what has this customer ordered\" (AGL-2622), seeded as\n * a Customer clause matched as a substring so a domain finds every buyer at\n * a company; and the dispute banner's \"Show them\".\n */\n const searchParams = useSearchParams()\n const [clauses, setClauses] = useState<ListFilterClause[]>(() => {\n const customer = searchParams?.get(ORDERS_CUSTOMER_PARAM)?.trim()\n return customer\n ? [{ field: 'customerEmail', op: 'contains', value: customer }]\n : []\n })\n const [selectedId, setSelectedId] = useState<string | null>(null)\n /*\n * An order named in the URL opens in its dialog on arrival (AGL-2622). A\n * contact's timeline names the order that made the person a customer,\n * and the address it links to is this list with `?order={id}`. The\n * window is the newest two hundred, and an order from last year is not\n * in it, so the document is read once by id and held beside the window\n * for the dialog; an order that IS in the window is found there first\n * and the read is skipped. Once per id — the ref keeps a re-render from\n * reopening a dialog the merchant has since closed — and the landing read\n * is judged against the ref rather than an effect cleanup, because the\n * `setSelectedId` above re-renders before the read lands and a cleanup\n * keyed on any dep would cancel the answer to the question just asked.\n */\n const seededOrderId = searchParams?.get(ORDERS_ORDER_PARAM) ?? null\n const [seededOrder, setSeededOrder] = useState<any | null>(null)\n const seededOpened = useRef<string | null>(null)\n useEffect(() => {\n if (!seededOrderId || seededOpened.current === seededOrderId) return\n if (orderDocs === undefined) return\n seededOpened.current = seededOrderId\n setSelectedId(seededOrderId)\n if (orderDocs.some((order: any) => order.$id === seededOrderId)) return\n void getDoc(doc(firestore, 'hosts', hostId, 'orders', seededOrderId))\n .then((snapshot) => {\n if (seededOpened.current !== snapshot.id || !snapshot.exists()) return\n setSeededOrder({ ...snapshot.data(), $id: snapshot.id })\n })\n .catch(() => undefined)\n }, [seededOrderId, orderDocs, firestore, hostId])\n const [draft, setDraft] = useState<{\n productId: string\n variantId: string\n quantity: string\n email: string\n /** Destination the merchant declared, once they have been asked. */\n shipTo?: string\n /**\n * Revealed only when the server refuses for want of a destination\n * (AGL-1792). A merchant whose rates are the same everywhere — or who\n * configured none — is never asked and never sees this field.\n */\n shipCountries?: string[]\n busy?: boolean\n } | null>(null)\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n /**\n * Each order with the values the grid shows and filters by. `liftLegacyOrder`\n * names a status and channel on a row that predates them, and Product\n * reads the line items first (AGL-1747): the flat `productId` is written\n * only by the two buy-now Stripe paths.\n */\n const orderRows = useMemo(\n () =>\n orders.map((order: any) => {\n const lifted = CommerceModel.liftLegacyOrder(order)\n const dispute = CommerceModel.describeOrderDispute(lifted)\n const productIds = [\n ...new Set(\n [\n ...(lifted.lineItems ?? []).map((line) => line.productId),\n order.productId,\n ].filter((id): id is string => typeof id === 'string' && id !== ''),\n ),\n ]\n return {\n ...order,\n orderLabel: CommerceModel.formatOrderNumber(lifted, order.$id),\n itemName:\n lifted.lineItems?.[0]?.name ??\n productNames[order.productId] ??\n order.productId ??\n '',\n statusKey: lifted.status,\n channelKey: lifted.channel ?? 'online',\n netCents: CommerceModel.orderNetCents(lifted),\n disputeBadge: dispute,\n productIds,\n disputeKey: CommerceModel.orderHasOpenDispute(lifted)\n ? 'open'\n : dispute?.tone === 'lost'\n ? 'lost'\n : '',\n }\n }),\n [orders, productNames],\n )\n const productOptions = useMemo(\n () =>\n productWindow.rows.map((product: any) => ({\n value: String(product.$id),\n label: String(product.name ?? product.$id),\n })),\n [productWindow],\n )\n const filterOptions = useMemo(\n () => ({\n statusKey: ORDER_STATUS_OPTIONS,\n channelKey: ORDER_CHANNEL_OPTIONS,\n productIds: productOptions,\n disputeKey: ORDER_DISPUTE_OPTIONS,\n }),\n [productOptions],\n )\n const listFilter = useListRowsFilter({\n rows: orderRows,\n fields: ORDER_FILTER_FIELDS,\n options: filterOptions,\n headers: ORDER_FILTER_HEADERS,\n search: ORDER_SEARCH_FIELDS,\n clauses,\n onChange: setClauses,\n })\n const visibleOrders = listFilter.rows\n const showingOpenDisputes = clauses.some(\n (clause) =>\n clause.field === OPEN_DISPUTE_CLAUSE.field &&\n clause.op === OPEN_DISPUTE_CLAUSE.op &&\n clause.value === OPEN_DISPUTE_CLAUSE.value,\n )\n\n /**\n * Raised over EVERY loaded order, not the visible ones (AGL-1796). A\n * merchant filtered to \"delivered\" still has a deadline running on a `paid`\n * order, and the evidence window is days long.\n */\n const openDisputes = useMemo(\n () =>\n CommerceModel.summariseOpenDisputes(\n orders.map((order: any) => CommerceModel.liftLegacyOrder(order)),\n ),\n [orders],\n )\n /**\n * 30-day money summary with the prior 30 days behind it (AGL-2136). Over\n * the LOADED window, like every other figure on this card — the query is\n * `limit(200)`, so a store past 200 orders in 60 days is summarising a\n * slice. That is the same bound the analytics card has always had, and\n * making this one silently different would be worse than sharing it.\n */\n const summary = useMemo(\n () => CommerceModel.summarizeOrderWindow(orders, { nowMs: Date.now() }),\n [orders],\n )\n\n const handleExportCsv = useCallback(() => {\n // The rows themselves are built by the pure model helper (AGL-1747) so the\n // column-by-column arithmetic is unit-testable without a Firestore mock.\n const csv = CommerceModel.buildOrdersCsv(visibleOrders, productNames)\n const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = 'orders.csv'\n anchor.click()\n URL.revokeObjectURL(url)\n }, [visibleOrders, productNames])\n\n /**\n * Idempotency key for ONE draft attempt (AGL-1697).\n *\n * Minted lazily on the first create click, NOT per call — two clicks (or a\n * click retried after a lost response) must present ONE key so the server\n * replays the original payment link instead of minting a second order.\n * Retired whenever a content field changes: an edited draft is a different\n * order, and replaying the old one against it would hand the merchant a\n * link priced for fields they no longer see. Answering the shipping-country\n * ask also retires it, which is safe — that refusal is served before the\n * server takes the claim, so the first key was never spent.\n */\n const draftAttemptKey = useRef('')\n useEffect(() => {\n draftAttemptKey.current = ''\n }, [\n draft?.productId,\n draft?.variantId,\n draft?.quantity,\n draft?.email,\n draft?.shipTo,\n ])\n\n const handleDraftCreate = useCallback(async () => {\n if (!draft?.productId) return\n setDraft((prev) => (prev ? { ...prev, busy: true } : prev))\n try {\n if (!draftAttemptKey.current) {\n draftAttemptKey.current =\n globalThis.crypto?.randomUUID?.() ??\n `${Date.now()}-${Math.random().toString(36).slice(2)}`\n }\n const response = await authorizedFetch(\n user,\n '/api/commerce/draft-order',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n // Stable across a retry of THIS attempt (AGL-1697), so a\n // double-click cannot mint two live payment links.\n 'Idempotency-Key': draftAttemptKey.current,\n },\n body: JSON.stringify({\n hostId,\n productId: draft.productId,\n variantId: draft.variantId || undefined,\n quantity: Number(draft.quantity) || 1,\n email: draft.email || undefined,\n // A request, never an instruction: the server resolves the\n // rates for this country AND restricts the payment link's\n // collectable addresses to it, so declaring one cannot buy a\n // cheaper zone's rate than the address the buyer then enters\n // (AGL-1721).\n ...(draft.shipTo ? { shippingCountry: draft.shipTo } : {}),\n }),\n },\n )\n const payload = await response.json()\n if (!response.ok) {\n // The merchant's rates differ by destination, so the server will not\n // price this order until it knows one (AGL-1792). Reveal the field and\n // let them answer; a store whose rates are the same everywhere, or\n // which configured none, never sends this and never shows it.\n if (payload?.needsShippingCountry) {\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n shipCountries: (\n payload.shippingCountries as string[] | undefined\n )?.length\n ? (payload.shippingCountries as string[])\n : [...CommerceModel.CHECKOUT_SHIPPING_COUNTRIES],\n }\n : prev,\n )\n }\n return void enqueueSnackbar(payload?.error ?? 'Draft order failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n await navigator.clipboard.writeText(payload.url).catch(() => undefined)\n enqueueSnackbar('Draft created — payment link copied', {\n variant: 'success',\n persist: false,\n })\n setDraft(null)\n } finally {\n setDraft((prev) => (prev ? { ...prev, busy: false } : prev))\n }\n }, [draft, user, hostId, enqueueSnackbar])\n\n const selectedOrder =\n (orderDocs ?? []).find((order: any) => order.$id === selectedId) ??\n (seededOrder && seededOrder.$id === selectedId ? seededOrder : null)\n\n /**\n * Shared by the two triggers (AGL-1805) so the empty state and the toolbar\n * cannot drift into opening the dialog with different starting fields.\n */\n const openDraft = useCallback(\n () => setDraft({ productId: '', variantId: '', quantity: '1', email: '' }),\n [],\n )\n\n /** What the draft dialog can actually offer. */\n const selectableProducts = useMemo(\n () => productWindow.rows.filter((product: any) => !product.deletedAt),\n [productWindow],\n )\n\n /*\n * The six columns `/product/commerce` advertises, in the order the mockup\n * shows them — every fact scannable on its own, Channel included.\n */\n const orderColumns = useMemo<GridColDef[]>(\n () => [\n {\n field: 'orderLabel',\n headerName: 'Order',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }: any) => {\n const extraItems = Math.max(0, (row.lineItems?.length ?? 0) - 1)\n return (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.orderLabel}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {extraItems ? `${row.itemName} +${extraItems} more` : row.itemName}\n </Typography>\n </Stack>\n )\n },\n },\n {\n field: 'customerEmail',\n headerName: 'Customer',\n flex: 1,\n minWidth: 180,\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\" noWrap>\n {row.customerEmail || '—'}\n </Typography>\n ),\n },\n {\n field: 'channelKey',\n headerName: 'Channel',\n width: 120,\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\">\n {CommerceModel.orderChannelLabel(row.channelKey)}\n </Typography>\n ),\n },\n {\n field: 'netCents',\n headerName: 'Total',\n type: 'number',\n width: 150,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: any) => (\n <Stack sx={{ alignItems: 'flex-end', minWidth: 0, width: 1 }}>\n <Typography variant=\"body2\">\n {`$${(row.netCents / 100).toFixed(2)}`}\n </Typography>\n {row.refundedCents ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`$${((row.totals?.totalCents ?? row.amountCents ?? 0) / 100).toFixed(2)} less refunds`}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'statusKey',\n headerName: 'Status',\n width: 230,\n renderCell: ({ row }: any) => {\n // A lost chargeback leaves `status: 'refunded'` (AGL-1787), so the\n // status pill cannot tell the two apart either — which is why the\n // dispute chip sits beside it rather than being folded into it.\n const dispute: ReturnType<typeof CommerceModel.describeOrderDispute> =\n row.disputeBadge\n return (\n <Stack sx={{ minWidth: 0 }}>\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center' }}>\n <Chip\n label={\n CommerceModel.ORDER_STATUS_LABELS[\n row.statusKey as CommerceModel.OrderStatus\n ] ?? row.statusKey\n }\n size=\"small\"\n color={\n CommerceModel.ORDER_STATUS_COLOR[\n row.statusKey as CommerceModel.OrderStatus\n ] ?? 'default'\n }\n variant=\"outlined\"\n />\n {dispute ? (\n <Tooltip title={dispute.detail}>\n <Chip\n label={dispute.label}\n size=\"small\"\n color={DISPUTE_COLOR[dispute.tone]}\n variant=\"filled\"\n />\n </Tooltip>\n ) : null}\n </Stack>\n {/*\n The deadline on the row itself, while the case is open — the\n one fact on this screen that expires.\n */}\n {dispute?.evidenceDaysLeft !== undefined ? (\n <Typography\n variant=\"caption\"\n component=\"div\"\n color={dispute.evidenceDaysLeft < 0 ? 'error' : 'warning.main'}\n >\n {dispute.evidenceDaysLeft < 0\n ? 'Evidence deadline passed'\n : `Evidence due in ${dispute.evidenceDaysLeft} day${\n dispute.evidenceDaysLeft === 1 ? '' : 's'\n }`}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'createdAtMs',\n headerName: 'Date',\n width: 130,\n renderCell: ({ row }: any) => {\n const createdAt =\n row.createdAt?.toDate?.() ??\n (row.createdAtMs ? new Date(row.createdAtMs) : null)\n return (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {createdAt ? createdAt.toLocaleDateString() : '—'}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {createdAt ? createdAt.toLocaleTimeString() : ''}\n </Typography>\n </Stack>\n )\n },\n },\n ],\n [],\n )\n\n return (\n <CardDisplay\n header={'Orders'}\n help={pluginDocsHelp('commerce', {\n anchor: '#orders',\n excerpt:\n 'Every order with its channel, status and net total. A refunded ' +\n 'order shows what is left, with the gross beneath it.',\n })}\n contentGutterX\n contentGutterY\n >\n {orders.length === 0 ? (\n /*\n * An invitation, not a report (AGL-1805). The \"Draft order\" button\n * used to live in the other arm of this ternary, so the one state a\n * draft order exists for — no sales yet, invoice the customer you\n * already have — was the only state that could not reach it, and\n * every other route into the dialog needs an order to exist first.\n *\n * The filters stay behind deliberately: they belong to a list with\n * rows in it. Export CSV likewise — a header-only file is not\n * something a merchant on day one is looking for.\n */\n <Stack spacing={1} sx={{ alignItems: 'flex-start' }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {\n 'No orders yet. Storefront sales, POS sales and draft orders ' +\n 'all appear here as they come in.'\n }\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {\n 'Selling to someone directly? Draft their order and send them ' +\n 'a payment link.'\n }\n </Typography>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openDraft}\n >\n {'Draft order'}\n </Button>\n </Stack>\n ) : (\n <Stack spacing={1}>\n {showStats ? (\n <Stack\n direction=\"row\"\n spacing={3}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n <CommerceStatTile\n label=\"Revenue · 30d\"\n value={`$${(summary.revenueCents / 100).toFixed(2)}`}\n deltaPct={summary.revenueDeltaPct}\n deltaCaption=\"vs the previous 30 days\"\n />\n <CommerceStatTile\n label=\"Orders · 30d\"\n value={String(summary.orders)}\n deltaPct={summary.ordersDeltaPct}\n deltaCaption=\"vs the previous 30 days\"\n />\n <CommerceStatTile\n label=\"Avg order value\"\n value={`$${(summary.aovCents / 100).toFixed(2)}`}\n deltaPct={summary.aovDeltaPct}\n deltaCaption=\"vs the previous 30 days\"\n />\n </Stack>\n ) : null}\n {openDisputes.count > 0 ? (\n <Alert\n severity={openDisputes.overdue ? 'error' : 'warning'}\n action={\n showingOpenDisputes ? undefined : (\n <Button\n size=\"small\"\n color=\"inherit\"\n onClick={() =>\n setClauses(\n upsertListFilterClause(\n clauses,\n OPEN_DISPUTE_CLAUSE.field,\n OPEN_DISPUTE_CLAUSE,\n ),\n )\n }\n >\n {'Show them'}\n </Button>\n )\n }\n >\n <AlertTitle>\n {openDisputes.count === 1\n ? 'A shopper has disputed a charge with their bank'\n : `${openDisputes.count} charges are disputed with the shopper’s bank`}\n </AlertTitle>\n {openDisputes.soonestDaysLeft === undefined\n ? 'Answer in the Stripe dashboard while the case is open — an unanswered dispute is decided for the shopper.'\n : openDisputes.soonestDaysLeft < 0\n ? 'The evidence deadline has passed on at least one of these. Stripe decides an unanswered dispute for the shopper.'\n : `Evidence is due to Stripe in ${openDisputes.soonestDaysLeft} day${\n openDisputes.soonestDaysLeft === 1 ? '' : 's'\n } on the tightest of these. An unanswered dispute is decided for the shopper.`}\n </Alert>\n ) : null}\n {/*\n The buttons stay beside the grid: Export CSV writes the model's\n own order columns (`buildOrdersCsv`) for the rows the filters\n leave, which the grid's own export cannot.\n */}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', justifyContent: 'flex-end' }}\n >\n <Button size=\"small\" onClick={handleExportCsv}>\n {'Export CSV'}\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openDraft}\n >\n {'Draft order'}\n </Button>\n </Stack>\n <ListFilterChips {...listFilter.chipsProps} />\n <ListTable\n aria-label=\"Orders\"\n rows={visibleOrders}\n columns={listFilter.filterColumns(orderColumns)}\n onOpen={(id) => setSelectedId(id)}\n {...listFilter.gridProps}\n initialState={{\n columns: { columnVisibilityModel: ORDER_HIDDEN_COLUMNS },\n }}\n noRowsLabel=\"No orders match these filters\"\n />\n {orderWindow.truncated ? (\n /*\n * Where the filters, the search and the export stop. All run over\n * what was read, so a store past this window would otherwise see\n * a status filter return nothing and read it as having no such\n * orders.\n */\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Showing the ${ORDERS_WINDOW} most recent orders. Filters and Export CSV cover these.`}\n </Typography>\n ) : null}\n </Stack>\n )}\n {selectedOrder ? (\n <OrderDetailDialog\n hostId={hostId}\n order={selectedOrder}\n onClose={() => setSelectedId(null)}\n />\n ) : null}\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'Draft order'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Product\"\n value={draft?.productId ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, productId: event.target.value, variantId: '' }\n : prev,\n )\n }\n size=\"small\"\n select\n sx={{ mt: 1 }}\n /*\n * The second dead end behind the first (AGL-1805): a store with\n * no orders may have no products either, and a draft order is\n * composed from one — so \"Create & copy link\" stays disabled. Say\n * why, rather than opening an empty menu onto nothing.\n */\n helperText={\n selectableProducts.length === 0\n ? 'Add a product to this store first — a draft order is built from one.'\n : undefined\n }\n >\n {selectableProducts.map((product: any) => (\n <MenuItem key={product.$id} value={product.$id}>\n {product.name ?? product.$id}\n </MenuItem>\n ))}\n </TextField>\n {(() => {\n const product = productWindow.rows.find(\n (item: any) => item.$id === draft?.productId,\n )\n const variants = product\n ? CommerceModel.liftLegacyProduct(product).variants\n : []\n return variants.length > 1 ? (\n <TextField\n label=\"Variant\"\n value={draft?.variantId ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, variantId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n >\n {variants.map((variant) => (\n <MenuItem key={variant.id} value={variant.id}>\n {`${Object.values(variant.options ?? {}).join(' / ') || 'Default'} — $${variant.priceUsd}`}\n </MenuItem>\n ))}\n </TextField>\n ) : null\n })()}\n <TextField\n label=\"Quantity\"\n value={draft?.quantity ?? '1'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n quantity: event.target.value.replace(/[^0-9]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n <TextField\n label=\"Buyer email (optional)\"\n value={draft?.email ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, email: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n {draft?.shipCountries?.length ? (\n <TextField\n label=\"Ships to\"\n value={draft?.shipTo ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, shipTo: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n helperText={\n 'This store’s shipping rates differ by destination, so the ' +\n 'payment link has to be priced for one.'\n }\n >\n {draft.shipCountries.map((code) => (\n <MenuItem key={code} value={code}>\n {CommerceModel.CHECKOUT_SHIPPING_COUNTRY_NAMES[code] ?? code}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n !draft?.productId ||\n draft?.busy ||\n // Once asked, the answer is required: retrying without one is\n // refused again, so the button would only look broken.\n Boolean(draft?.shipCountries?.length && !draft?.shipTo)\n }\n onClick={handleDraftCreate}\n >\n {'Create & copy link'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nHostOrdersCard.displayName = 'HostOrdersCard'\n\nexport default HostOrdersCard\n"],"names":["CommerceModel","CardDisplay","ListFilterChips","ListTable","hiddenFilterVisibility","inMemoryListField","upsertListFilterClause","useListRowsFilter","useSnackbar","checkEntitlement","ORDERS_CUSTOMER_PARAM","ORDERS_ORDER_PARAM","pluginDocsHelp","Alert","AlertTitle","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Tooltip","Typography","collection","doc","getDoc","limit","orderBy","query","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","ceilingedWindow","collectionCeiling","useFirestore","useOrgPlan","useUser","useFirestoreCollection","authorizedFetch","CommerceStatTile","OrderDetailDialog","DISPUTE_COLOR","ORDERS_WINDOW","PRODUCT_NAME_WINDOW","ORDER_FILTER_FIELDS","tokensPath","verbatimTokens","ORDER_FILTER_HEADERS","orderLabel","customerEmail","channelKey","statusKey","createdAtMs","productIds","disputeKey","ORDER_STATUS_OPTIONS","Object","keys","ORDER_STATUS_LABELS","map","status","value","label","ORDER_CHANNEL_OPTIONS","ORDER_CHANNEL_LABELS","channel","ORDER_DISPUTE_OPTIONS","ORDER_SEARCH_FIELDS","ORDER_HIDDEN_COLUMNS","OPEN_DISPUTE_CLAUSE","field","op","HostOrdersCard","props","draft","hostId","firestore","data","orderDocs","idField","productDocs","orderWindow","undefined","productWindow","productNames","product","rows","$id","name","org","ready","orgReady","showStats","orders","searchParams","clauses","setClauses","customer","get","trim","selectedId","setSelectedId","seededOrderId","seededOrder","setSeededOrder","seededOpened","current","some","order","then","snapshot","id","exists","catch","setDraft","user","enqueueSnackbar","orderRows","lifted","liftLegacyOrder","dispute","describeOrderDispute","Set","lineItems","line","productId","filter","formatOrderNumber","itemName","netCents","orderNetCents","disputeBadge","orderHasOpenDispute","tone","productOptions","String","filterOptions","listFilter","fields","options","headers","search","onChange","visibleOrders","showingOpenDisputes","clause","openDisputes","summariseOpenDisputes","summary","summarizeOrderWindow","nowMs","Date","now","handleExportCsv","csv","buildOrdersCsv","url","URL","createObjectURL","Blob","type","anchor","document","createElement","href","download","click","revokeObjectURL","draftAttemptKey","variantId","quantity","email","shipTo","handleDraftCreate","prev","busy","globalThis","crypto","randomUUID","Math","random","toString","slice","response","method","body","JSON","stringify","Number","shippingCountry","payload","json","ok","needsShippingCountry","shipCountries","shippingCountries","length","CHECKOUT_SHIPPING_COUNTRIES","error","variant","allowDuplicate","navigator","clipboard","writeText","persist","selectedOrder","find","openDraft","selectableProducts","deletedAt","orderColumns","headerName","flex","minWidth","renderCell","row","extraItems","max","sx","noWrap","color","width","orderChannelLabel","align","headerAlign","alignItems","toFixed","refundedCents","totals","totalCents","amountCents","direction","spacing","size","ORDER_STATUS_COLOR","title","detail","evidenceDaysLeft","component","createdAt","toDate","toLocaleDateString","toLocaleTimeString","header","help","excerpt","contentGutterX","contentGutterY","onClick","flexWrap","rowGap","revenueCents","deltaPct","revenueDeltaPct","deltaCaption","ordersDeltaPct","aovCents","aovDeltaPct","count","severity","overdue","action","soonestDaysLeft","justifyContent","chipsProps","aria-label","columns","filterColumns","onOpen","gridProps","initialState","columnVisibilityModel","noRowsLabel","truncated","onClose","open","Boolean","maxWidth","fullWidth","display","flexDirection","gap","event","target","select","mt","helperText","item","variants","liftLegacyProduct","values","join","priceUsd","replace","slotProps","htmlInput","inputMode","code","CHECKOUT_SHIPPING_COUNTRY_NAMES","disabled","displayName"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/host-orders-card.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport ListFilterChips from '@aglyn/shared-ui-jsx/components/list-filter-chips.component'\nimport { ListTable } from '@aglyn/shared-ui-jsx/components/list-table.component'\nimport { hiddenFilterVisibility } from '@aglyn/shared-ui-jsx/const/list-filter'\nimport {\n inMemoryListField,\n type ListFilterClause,\n upsertListFilterClause,\n} from '@aglyn/shared-ui-jsx/const/list-grid-filter'\nimport { useListRowsFilter } from '@aglyn/shared-ui-jsx/hooks/use-list-rows-filter'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n checkEntitlement,\n ORDERS_CUSTOMER_PARAM,\n ORDERS_ORDER_PARAM,\n pluginDocsHelp,\n} from '@aglyn/aglyn'\nimport {\n Alert,\n AlertTitle,\n Button,\n Chip,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n MenuItem,\n Stack,\n TextField,\n Tooltip,\n Typography,\n} from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport { collection, doc, getDoc, limit, orderBy, query } from 'firebase/firestore'\nimport { useSearchParams } from 'next/navigation'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport {\n ceilingedWindow,\n collectionCeiling,\n useFirestore,\n useOrgPlan,\n useUser,\n} from '@aglyn/tenant-feature-instance'\nimport { useFirestoreCollection } from '@aglyn/tenant-feature-instance'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\nimport CommerceStatTile from './commerce-stat-tile.component'\nimport OrderDetailDialog, {\n DISPUTE_COLOR,\n} from './order-detail-dialog.component'\n\n/**\n * How far back the table, its filters and its export reach.\n *\n * This window cannot be paged the way a plain list can. The Filters panel,\n * the search and the CSV export all run over what was read, so a page of ten\n * would quietly narrow every one of them — a status filter would search a\n * tenth of the orders and report confidently on what it found. Bounding the read and saying where the\n * bound falls keeps the filters honest about their own scope.\n */\nconst ORDERS_WINDOW = 200\n\n/** A name map for the filter menu; it renders no product rows of its own. */\nconst PRODUCT_NAME_WINDOW = 100\n\n/*\n * What the orders grid's Filters panel offers. The card holds its whole\n * window, so the panel and the search answer over every order it read.\n *\n * Status and Channel are the order's own enumerations; Disputes is apart\n * from Status on purpose, because the two are orthogonal: an OPEN dispute\n * sits on an order that is still `paid`, and a lost one on `refunded`\n * beside every ordinary refund. Product is the ids of every line item, so\n * a cart, POS or draft order is found by any product it holds. The date is\n * the field the query orders by, which every order writer stamps.\n */\nconst ORDER_FILTER_FIELDS = [\n inMemoryListField('orderLabel', 'text'),\n inMemoryListField('customerEmail', 'text'),\n inMemoryListField('channelKey', 'select'),\n inMemoryListField('statusKey', 'select'),\n inMemoryListField('createdAtMs', 'date'),\n { ...inMemoryListField('productIds', 'select'), tokensPath: 'productIds', verbatimTokens: true },\n inMemoryListField('disputeKey', 'select'),\n]\nconst ORDER_FILTER_HEADERS: Readonly<Record<string, string>> = {\n orderLabel: 'Order',\n customerEmail: 'Customer',\n channelKey: 'Channel',\n statusKey: 'Status',\n createdAtMs: 'Date',\n productIds: 'Product',\n disputeKey: 'Disputes',\n}\nconst ORDER_STATUS_OPTIONS = (\n Object.keys(CommerceModel.ORDER_STATUS_LABELS) as (keyof typeof CommerceModel.ORDER_STATUS_LABELS)[]\n).map((status) => ({ value: status, label: CommerceModel.ORDER_STATUS_LABELS[status] }))\nconst ORDER_CHANNEL_OPTIONS = (\n Object.keys(CommerceModel.ORDER_CHANNEL_LABELS) as (keyof typeof CommerceModel.ORDER_CHANNEL_LABELS)[]\n).map((channel) => ({ value: channel, label: CommerceModel.ORDER_CHANNEL_LABELS[channel] }))\n/*\n * `lost` is the badge's tone, not the raw dispute status: a dispute that was\n * WON also closes with money untouched, and listing it as charged back would\n * tell a merchant they lost a case they won.\n */\nconst ORDER_DISPUTE_OPTIONS = [\n { value: 'open', label: 'Open dispute' },\n { value: 'lost', label: 'Charged back' },\n]\n/** What the quick search reads on an order row. */\nconst ORDER_SEARCH_FIELDS = ['orderLabel', 'customerEmail', 'itemName'] as const\n/** The filter-only columns never show. */\nconst ORDER_HIDDEN_COLUMNS = hiddenFilterVisibility(ORDER_FILTER_FIELDS, [\n 'orderLabel',\n 'customerEmail',\n 'channelKey',\n 'statusKey',\n 'createdAtMs',\n])\n\n/** The clause the dispute banner's \"Show them\" sets. */\nconst OPEN_DISPUTE_CLAUSE: ListFilterClause = {\n field: 'disputeKey',\n op: 'equals',\n value: 'open',\n}\n\nexport interface HostOrdersCardProps {\n hostId: string\n}\n\n/**\n * Orders console (AGL-287): filterable list over webhook-written order\n * docs with a detail dialog (timeline, fulfill, refund, cancel, notes,\n * packing slip) and draft orders that send the buyer a payment link.\n */\nexport function HostOrdersCard(props: HostOrdersCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n /**\n * The most recent orders, in the order the table shows them.\n *\n * `createdAtMs` is the field every order writer in the plugin stamps and the\n * one `reconcile-stock` already walks the collection by. Ordering on it is\n * what makes this window the RECENT orders: an unordered cap is answered in\n * document-id order, and orders are keyed by generated ids, so it returns a\n * pseudo-random sample that a client sort then dresses as newest-first.\n */\n const { data: orderDocs } = useFirestoreCollection<any>(\n () =>\n query(\n collection(firestore, 'hosts', hostId, 'orders'),\n orderBy('createdAtMs', 'desc'),\n limit(ORDERS_WINDOW + 1),\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const { data: productDocs } = useFirestoreCollection<any>(\n () =>\n collectionCeiling(\n collection(firestore, 'hosts', hostId, 'products'),\n PRODUCT_NAME_WINDOW,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const orderWindow = useMemo(\n () => ceilingedWindow<any>(orderDocs ?? undefined, ORDERS_WINDOW),\n [orderDocs],\n )\n const productWindow = useMemo(\n () => ceilingedWindow<any>(productDocs ?? undefined, PRODUCT_NAME_WINDOW),\n [productDocs],\n )\n const productNames = useMemo(() => {\n const map: Record<string, string> = {}\n for (const product of productWindow.rows) {\n map[product.$id] = product.name ?? product.$id\n }\n return map\n }, [productWindow])\n\n /**\n * The money tiles are the `commerceAnalytics` surface (AGL-1938,\n * AGL-2056), and this is the third place they appear. Rendering them\n * ungated here would undo both of those passes and hand a Starter org\n * the Pro figures one tab away from the upgrade prompt that refuses\n * them. The TABLE below is not gated — a list of your own orders is not\n * a paid feature.\n */\n const { org, ready: orgReady } = useOrgPlan(hostId)\n const showStats =\n orgReady && checkEntitlement(org as never, 'commerceAnalytics')\n\n // The query returns the window already newest-first; re-sorting it here\n // would only restate the order it arrived in.\n const orders = orderWindow.rows\n\n /*\n * The grid's clauses (AGL-96, AGL-3317), held here so two things outside\n * the grid can set one: the CRM's contact page links with the buyer's\n * address to answer \"what has this customer ordered\" (AGL-2622), seeded as\n * a Customer clause matched as a substring so a domain finds every buyer at\n * a company; and the dispute banner's \"Show them\".\n */\n const searchParams = useSearchParams()\n const [clauses, setClauses] = useState<ListFilterClause[]>(() => {\n const customer = searchParams?.get(ORDERS_CUSTOMER_PARAM)?.trim()\n return customer\n ? [{ field: 'customerEmail', op: 'contains', value: customer }]\n : []\n })\n const [selectedId, setSelectedId] = useState<string | null>(null)\n /*\n * An order named in the URL opens in its dialog on arrival (AGL-2622). A\n * contact's timeline names the order that made the person a customer,\n * and the address it links to is this list with `?order={id}`. The\n * window is the newest two hundred, and an order from last year is not\n * in it, so the document is read once by id and held beside the window\n * for the dialog; an order that IS in the window is found there first\n * and the read is skipped. Once per id — the ref keeps a re-render from\n * reopening a dialog the merchant has since closed — and the landing read\n * is judged against the ref rather than an effect cleanup, because the\n * `setSelectedId` above re-renders before the read lands and a cleanup\n * keyed on any dep would cancel the answer to the question just asked.\n */\n const seededOrderId = searchParams?.get(ORDERS_ORDER_PARAM) ?? null\n const [seededOrder, setSeededOrder] = useState<any | null>(null)\n const seededOpened = useRef<string | null>(null)\n useEffect(() => {\n if (!seededOrderId || seededOpened.current === seededOrderId) return\n if (orderDocs === undefined) return\n seededOpened.current = seededOrderId\n setSelectedId(seededOrderId)\n if (orderDocs.some((order: any) => order.$id === seededOrderId)) return\n void getDoc(doc(firestore, 'hosts', hostId, 'orders', seededOrderId))\n .then((snapshot) => {\n if (seededOpened.current !== snapshot.id || !snapshot.exists()) return\n setSeededOrder({ ...snapshot.data(), $id: snapshot.id })\n })\n .catch(() => undefined)\n }, [seededOrderId, orderDocs, firestore, hostId])\n const [draft, setDraft] = useState<{\n productId: string\n variantId: string\n quantity: string\n email: string\n /** Destination the merchant declared, once they have been asked. */\n shipTo?: string\n /**\n * Revealed only when the server refuses for want of a destination\n * (AGL-1792). A merchant whose rates are the same everywhere — or who\n * configured none — is never asked and never sees this field.\n */\n shipCountries?: string[]\n busy?: boolean\n } | null>(null)\n const { data: user } = useUser()\n const { enqueueSnackbar } = useSnackbar()\n /**\n * Each order with the values the grid shows and filters by. `liftLegacyOrder`\n * names a status and channel on a row that predates them, and Product\n * reads the line items first (AGL-1747): the flat `productId` is written\n * only by the two buy-now Stripe paths.\n */\n const orderRows = useMemo(\n () =>\n orders.map((order: any) => {\n const lifted = CommerceModel.liftLegacyOrder(order)\n const dispute = CommerceModel.describeOrderDispute(lifted)\n const productIds = [\n ...new Set(\n [\n ...(lifted.lineItems ?? []).map((line) => line.productId),\n order.productId,\n ].filter((id): id is string => typeof id === 'string' && id !== ''),\n ),\n ]\n return {\n ...order,\n orderLabel: CommerceModel.formatOrderNumber(lifted, order.$id),\n itemName:\n lifted.lineItems?.[0]?.name ??\n productNames[order.productId] ??\n order.productId ??\n '',\n statusKey: lifted.status,\n channelKey: lifted.channel ?? 'online',\n netCents: CommerceModel.orderNetCents(lifted),\n disputeBadge: dispute,\n productIds,\n disputeKey: CommerceModel.orderHasOpenDispute(lifted)\n ? 'open'\n : dispute?.tone === 'lost'\n ? 'lost'\n : '',\n }\n }),\n [orders, productNames],\n )\n const productOptions = useMemo(\n () =>\n productWindow.rows.map((product: any) => ({\n value: String(product.$id),\n label: String(product.name ?? product.$id),\n })),\n [productWindow],\n )\n const filterOptions = useMemo(\n () => ({\n statusKey: ORDER_STATUS_OPTIONS,\n channelKey: ORDER_CHANNEL_OPTIONS,\n productIds: productOptions,\n disputeKey: ORDER_DISPUTE_OPTIONS,\n }),\n [productOptions],\n )\n const listFilter = useListRowsFilter({\n rows: orderRows,\n fields: ORDER_FILTER_FIELDS,\n options: filterOptions,\n headers: ORDER_FILTER_HEADERS,\n search: ORDER_SEARCH_FIELDS,\n clauses,\n onChange: setClauses,\n })\n const visibleOrders = listFilter.rows\n const showingOpenDisputes = clauses.some(\n (clause) =>\n clause.field === OPEN_DISPUTE_CLAUSE.field &&\n clause.op === OPEN_DISPUTE_CLAUSE.op &&\n clause.value === OPEN_DISPUTE_CLAUSE.value,\n )\n\n /**\n * Raised over EVERY loaded order, not the visible ones (AGL-1796). A\n * merchant filtered to \"delivered\" still has a deadline running on a `paid`\n * order, and the evidence window is days long.\n */\n const openDisputes = useMemo(\n () =>\n CommerceModel.summariseOpenDisputes(\n orders.map((order: any) => CommerceModel.liftLegacyOrder(order)),\n ),\n [orders],\n )\n /**\n * 30-day money summary with the prior 30 days behind it (AGL-2136). Over\n * the LOADED window, like every other figure on this card — the query is\n * `limit(200)`, so a store past 200 orders in 60 days is summarising a\n * slice. That is the same bound the analytics card has always had, and\n * making this one silently different would be worse than sharing it.\n */\n const summary = useMemo(\n () => CommerceModel.summarizeOrderWindow(orders, { nowMs: Date.now() }),\n [orders],\n )\n\n const handleExportCsv = useCallback(() => {\n // The rows themselves are built by the pure model helper (AGL-1747) so the\n // column-by-column arithmetic is unit-testable without a Firestore mock.\n const csv = CommerceModel.buildOrdersCsv(visibleOrders, productNames)\n const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))\n const anchor = document.createElement('a')\n anchor.href = url\n anchor.download = 'orders.csv'\n anchor.click()\n URL.revokeObjectURL(url)\n }, [visibleOrders, productNames])\n\n /**\n * Idempotency key for ONE draft attempt (AGL-1697).\n *\n * Minted lazily on the first create click, NOT per call — two clicks (or a\n * click retried after a lost response) must present ONE key so the server\n * replays the original payment link instead of minting a second order.\n * Retired whenever a content field changes: an edited draft is a different\n * order, and replaying the old one against it would hand the merchant a\n * link priced for fields they no longer see. Answering the shipping-country\n * ask also retires it, which is safe — that refusal is served before the\n * server takes the claim, so the first key was never spent.\n */\n const draftAttemptKey = useRef('')\n useEffect(() => {\n draftAttemptKey.current = ''\n }, [\n draft?.productId,\n draft?.variantId,\n draft?.quantity,\n draft?.email,\n draft?.shipTo,\n ])\n\n const handleDraftCreate = useCallback(async () => {\n if (!draft?.productId) return\n setDraft((prev) => (prev ? { ...prev, busy: true } : prev))\n try {\n if (!draftAttemptKey.current) {\n draftAttemptKey.current =\n globalThis.crypto?.randomUUID?.() ??\n `${Date.now()}-${Math.random().toString(36).slice(2)}`\n }\n const response = await authorizedFetch(\n user,\n '/api/commerce/draft-order',\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n // Stable across a retry of THIS attempt (AGL-1697), so a\n // double-click cannot mint two live payment links.\n 'Idempotency-Key': draftAttemptKey.current,\n },\n body: JSON.stringify({\n hostId,\n productId: draft.productId,\n variantId: draft.variantId || undefined,\n quantity: Number(draft.quantity) || 1,\n email: draft.email || undefined,\n // A request, never an instruction: the server resolves the\n // rates for this country AND restricts the payment link's\n // collectable addresses to it, so declaring one cannot buy a\n // cheaper zone's rate than the address the buyer then enters\n // (AGL-1721).\n ...(draft.shipTo ? { shippingCountry: draft.shipTo } : {}),\n }),\n },\n )\n const payload = await response.json()\n if (!response.ok) {\n // The merchant's rates differ by destination, so the server will not\n // price this order until it knows one (AGL-1792). Reveal the field and\n // let them answer; a store whose rates are the same everywhere, or\n // which configured none, never sends this and never shows it.\n if (payload?.needsShippingCountry) {\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n shipCountries: (\n payload.shippingCountries as string[] | undefined\n )?.length\n ? (payload.shippingCountries as string[])\n : [...CommerceModel.CHECKOUT_SHIPPING_COUNTRIES],\n }\n : prev,\n )\n }\n return void enqueueSnackbar(payload?.error ?? 'Draft order failed', {\n variant: 'error',\n allowDuplicate: true,\n })\n }\n await navigator.clipboard.writeText(payload.url).catch(() => undefined)\n enqueueSnackbar('Draft created — payment link copied', {\n variant: 'success',\n persist: false,\n })\n setDraft(null)\n } finally {\n setDraft((prev) => (prev ? { ...prev, busy: false } : prev))\n }\n }, [draft, user, hostId, enqueueSnackbar])\n\n const selectedOrder =\n (orderDocs ?? []).find((order: any) => order.$id === selectedId) ??\n (seededOrder && seededOrder.$id === selectedId ? seededOrder : null)\n\n /**\n * Shared by the two triggers (AGL-1805) so the empty state and the toolbar\n * cannot drift into opening the dialog with different starting fields.\n */\n const openDraft = useCallback(\n () => setDraft({ productId: '', variantId: '', quantity: '1', email: '' }),\n [],\n )\n\n /** What the draft dialog can actually offer. */\n const selectableProducts = useMemo(\n () => productWindow.rows.filter((product: any) => !product.deletedAt),\n [productWindow],\n )\n\n /*\n * The six columns `/product/commerce` advertises, in the order the mockup\n * shows them — every fact scannable on its own, Channel included.\n */\n const orderColumns = useMemo<GridColDef[]>(\n () => [\n {\n field: 'orderLabel',\n headerName: 'Order',\n flex: 1,\n minWidth: 160,\n renderCell: ({ row }: any) => {\n const extraItems = Math.max(0, (row.lineItems?.length ?? 0) - 1)\n return (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {row.orderLabel}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {extraItems ? `${row.itemName} +${extraItems} more` : row.itemName}\n </Typography>\n </Stack>\n )\n },\n },\n {\n field: 'customerEmail',\n headerName: 'Customer',\n flex: 1,\n minWidth: 180,\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\" noWrap>\n {row.customerEmail || '—'}\n </Typography>\n ),\n },\n {\n field: 'channelKey',\n headerName: 'Channel',\n width: 120,\n renderCell: ({ row }: any) => (\n <Typography variant=\"body2\">\n {CommerceModel.orderChannelLabel(row.channelKey)}\n </Typography>\n ),\n },\n {\n field: 'netCents',\n headerName: 'Total',\n type: 'number',\n width: 150,\n align: 'right',\n headerAlign: 'right',\n renderCell: ({ row }: any) => (\n <Stack sx={{ alignItems: 'flex-end', minWidth: 0, width: 1 }}>\n <Typography variant=\"body2\">\n {`$${(row.netCents / 100).toFixed(2)}`}\n </Typography>\n {row.refundedCents ? (\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {`$${((row.totals?.totalCents ?? row.amountCents ?? 0) / 100).toFixed(2)} less refunds`}\n </Typography>\n ) : null}\n </Stack>\n ),\n },\n {\n field: 'statusKey',\n headerName: 'Status',\n width: 230,\n renderCell: ({ row }: any) => {\n // A lost chargeback leaves `status: 'refunded'` (AGL-1787), so the\n // status pill cannot tell the two apart either — which is why the\n // dispute chip sits beside it rather than being folded into it.\n const dispute: ReturnType<typeof CommerceModel.describeOrderDispute> =\n row.disputeBadge\n return (\n <Stack sx={{ minWidth: 0 }}>\n <Stack direction=\"row\" spacing={0.5} sx={{ alignItems: 'center' }}>\n <Chip\n label={\n CommerceModel.ORDER_STATUS_LABELS[\n row.statusKey as CommerceModel.OrderStatus\n ] ?? row.statusKey\n }\n size=\"small\"\n color={\n CommerceModel.ORDER_STATUS_COLOR[\n row.statusKey as CommerceModel.OrderStatus\n ] ?? 'default'\n }\n variant=\"outlined\"\n />\n {dispute ? (\n <Tooltip title={dispute.detail}>\n <Chip\n label={dispute.label}\n size=\"small\"\n color={DISPUTE_COLOR[dispute.tone]}\n variant=\"filled\"\n />\n </Tooltip>\n ) : null}\n </Stack>\n {/*\n The deadline on the row itself, while the case is open — the\n one fact on this screen that expires.\n */}\n {dispute?.evidenceDaysLeft !== undefined ? (\n <Typography\n variant=\"caption\"\n component=\"div\"\n color={dispute.evidenceDaysLeft < 0 ? 'error' : 'warning.main'}\n >\n {dispute.evidenceDaysLeft < 0\n ? 'Evidence deadline passed'\n : `Evidence due in ${dispute.evidenceDaysLeft} day${\n dispute.evidenceDaysLeft === 1 ? '' : 's'\n }`}\n </Typography>\n ) : null}\n </Stack>\n )\n },\n },\n {\n field: 'createdAtMs',\n headerName: 'Date',\n width: 130,\n renderCell: ({ row }: any) => {\n const createdAt =\n row.createdAt?.toDate?.() ??\n (row.createdAtMs ? new Date(row.createdAtMs) : null)\n return (\n <Stack sx={{ minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {createdAt ? createdAt.toLocaleDateString() : '—'}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {createdAt ? createdAt.toLocaleTimeString() : ''}\n </Typography>\n </Stack>\n )\n },\n },\n ],\n [],\n )\n\n return (\n <CardDisplay\n header={'Orders'}\n help={pluginDocsHelp('commerce', {\n anchor: '#orders-screen',\n excerpt:\n 'Every order with its channel, status and net total. A refunded ' +\n 'order shows what is left, with the gross beneath it.',\n })}\n contentGutterX\n contentGutterY\n >\n {orders.length === 0 ? (\n /*\n * An invitation, not a report (AGL-1805). The \"Draft order\" button\n * used to live in the other arm of this ternary, so the one state a\n * draft order exists for — no sales yet, invoice the customer you\n * already have — was the only state that could not reach it, and\n * every other route into the dialog needs an order to exist first.\n *\n * The filters stay behind deliberately: they belong to a list with\n * rows in it. Export CSV likewise — a header-only file is not\n * something a merchant on day one is looking for.\n */\n <Stack spacing={1} sx={{ alignItems: 'flex-start' }}>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {\n 'No orders yet. Storefront sales, POS sales and draft orders ' +\n 'all appear here as they come in.'\n }\n </Typography>\n <Typography variant=\"body2\" color=\"text.secondary\">\n {\n 'Selling to someone directly? Draft their order and send them ' +\n 'a payment link.'\n }\n </Typography>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openDraft}\n >\n {'Draft order'}\n </Button>\n </Stack>\n ) : (\n <Stack spacing={1}>\n {showStats ? (\n <Stack\n direction=\"row\"\n spacing={3}\n sx={{ flexWrap: 'wrap', rowGap: 1 }}\n >\n <CommerceStatTile\n label=\"Revenue · 30d\"\n value={`$${(summary.revenueCents / 100).toFixed(2)}`}\n deltaPct={summary.revenueDeltaPct}\n deltaCaption=\"vs the previous 30 days\"\n />\n <CommerceStatTile\n label=\"Orders · 30d\"\n value={String(summary.orders)}\n deltaPct={summary.ordersDeltaPct}\n deltaCaption=\"vs the previous 30 days\"\n />\n <CommerceStatTile\n label=\"Avg order value\"\n value={`$${(summary.aovCents / 100).toFixed(2)}`}\n deltaPct={summary.aovDeltaPct}\n deltaCaption=\"vs the previous 30 days\"\n />\n </Stack>\n ) : null}\n {openDisputes.count > 0 ? (\n <Alert\n severity={openDisputes.overdue ? 'error' : 'warning'}\n action={\n showingOpenDisputes ? undefined : (\n <Button\n size=\"small\"\n color=\"inherit\"\n onClick={() =>\n setClauses(\n upsertListFilterClause(\n clauses,\n OPEN_DISPUTE_CLAUSE.field,\n OPEN_DISPUTE_CLAUSE,\n ),\n )\n }\n >\n {'Show them'}\n </Button>\n )\n }\n >\n <AlertTitle>\n {openDisputes.count === 1\n ? 'A shopper has disputed a charge with their bank'\n : `${openDisputes.count} charges are disputed with the shopper’s bank`}\n </AlertTitle>\n {openDisputes.soonestDaysLeft === undefined\n ? 'Answer in the Stripe dashboard while the case is open — an unanswered dispute is decided for the shopper.'\n : openDisputes.soonestDaysLeft < 0\n ? 'The evidence deadline has passed on at least one of these. Stripe decides an unanswered dispute for the shopper.'\n : `Evidence is due to Stripe in ${openDisputes.soonestDaysLeft} day${\n openDisputes.soonestDaysLeft === 1 ? '' : 's'\n } on the tightest of these. An unanswered dispute is decided for the shopper.`}\n </Alert>\n ) : null}\n {/*\n The buttons stay beside the grid: Export CSV writes the model's\n own order columns (`buildOrdersCsv`) for the rows the filters\n leave, which the grid's own export cannot.\n */}\n <Stack\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center', justifyContent: 'flex-end' }}\n >\n <Button size=\"small\" onClick={handleExportCsv}>\n {'Export CSV'}\n </Button>\n <Button\n size=\"small\"\n variant=\"contained\"\n color=\"primary\"\n onClick={openDraft}\n >\n {'Draft order'}\n </Button>\n </Stack>\n <ListFilterChips {...listFilter.chipsProps} />\n <ListTable\n aria-label=\"Orders\"\n rows={visibleOrders}\n columns={listFilter.filterColumns(orderColumns)}\n onOpen={(id) => setSelectedId(id)}\n {...listFilter.gridProps}\n initialState={{\n columns: { columnVisibilityModel: ORDER_HIDDEN_COLUMNS },\n }}\n noRowsLabel=\"No orders match these filters\"\n />\n {orderWindow.truncated ? (\n /*\n * Where the filters, the search and the export stop. All run over\n * what was read, so a store past this window would otherwise see\n * a status filter return nothing and read it as having no such\n * orders.\n */\n <Typography variant=\"caption\" color=\"text.secondary\">\n {`Showing the ${ORDERS_WINDOW} most recent orders. Filters and Export CSV cover these.`}\n </Typography>\n ) : null}\n </Stack>\n )}\n {selectedOrder ? (\n <OrderDetailDialog\n hostId={hostId}\n order={selectedOrder}\n onClose={() => setSelectedId(null)}\n />\n ) : null}\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{'Draft order'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Product\"\n value={draft?.productId ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? { ...prev, productId: event.target.value, variantId: '' }\n : prev,\n )\n }\n size=\"small\"\n select\n sx={{ mt: 1 }}\n /*\n * The second dead end behind the first (AGL-1805): a store with\n * no orders may have no products either, and a draft order is\n * composed from one — so \"Create & copy link\" stays disabled. Say\n * why, rather than opening an empty menu onto nothing.\n */\n helperText={\n selectableProducts.length === 0\n ? 'Add a product to this store first — a draft order is built from one.'\n : undefined\n }\n >\n {selectableProducts.map((product: any) => (\n <MenuItem key={product.$id} value={product.$id}>\n {product.name ?? product.$id}\n </MenuItem>\n ))}\n </TextField>\n {(() => {\n const product = productWindow.rows.find(\n (item: any) => item.$id === draft?.productId,\n )\n const variants = product\n ? CommerceModel.liftLegacyProduct(product).variants\n : []\n return variants.length > 1 ? (\n <TextField\n label=\"Variant\"\n value={draft?.variantId ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, variantId: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n >\n {variants.map((variant) => (\n <MenuItem key={variant.id} value={variant.id}>\n {`${Object.values(variant.options ?? {}).join(' / ') || 'Default'} — $${variant.priceUsd}`}\n </MenuItem>\n ))}\n </TextField>\n ) : null\n })()}\n <TextField\n label=\"Quantity\"\n value={draft?.quantity ?? '1'}\n onChange={(event) =>\n setDraft((prev) =>\n prev\n ? {\n ...prev,\n quantity: event.target.value.replace(/[^0-9]/g, ''),\n }\n : prev,\n )\n }\n size=\"small\"\n slotProps={{ htmlInput: { inputMode: 'numeric' } }}\n />\n <TextField\n label=\"Buyer email (optional)\"\n value={draft?.email ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, email: event.target.value } : prev,\n )\n }\n size=\"small\"\n />\n {draft?.shipCountries?.length ? (\n <TextField\n label=\"Ships to\"\n value={draft?.shipTo ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, shipTo: event.target.value } : prev,\n )\n }\n size=\"small\"\n select\n helperText={\n 'This store’s shipping rates differ by destination, so the ' +\n 'payment link has to be priced for one.'\n }\n >\n {draft.shipCountries.map((code) => (\n <MenuItem key={code} value={code}>\n {CommerceModel.CHECKOUT_SHIPPING_COUNTRY_NAMES[code] ?? code}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={\n !draft?.productId ||\n draft?.busy ||\n // Once asked, the answer is required: retrying without one is\n // refused again, so the button would only look broken.\n Boolean(draft?.shipCountries?.length && !draft?.shipTo)\n }\n onClick={handleDraftCreate}\n >\n {'Create & copy link'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n )\n}\nHostOrdersCard.displayName = 'HostOrdersCard'\n\nexport default HostOrdersCard\n"],"names":["CommerceModel","CardDisplay","ListFilterChips","ListTable","hiddenFilterVisibility","inMemoryListField","upsertListFilterClause","useListRowsFilter","useSnackbar","checkEntitlement","ORDERS_CUSTOMER_PARAM","ORDERS_ORDER_PARAM","pluginDocsHelp","Alert","AlertTitle","Button","Chip","Dialog","DialogActions","DialogContent","DialogTitle","MenuItem","Stack","TextField","Tooltip","Typography","collection","doc","getDoc","limit","orderBy","query","useSearchParams","useCallback","useEffect","useMemo","useRef","useState","ceilingedWindow","collectionCeiling","useFirestore","useOrgPlan","useUser","useFirestoreCollection","authorizedFetch","CommerceStatTile","OrderDetailDialog","DISPUTE_COLOR","ORDERS_WINDOW","PRODUCT_NAME_WINDOW","ORDER_FILTER_FIELDS","tokensPath","verbatimTokens","ORDER_FILTER_HEADERS","orderLabel","customerEmail","channelKey","statusKey","createdAtMs","productIds","disputeKey","ORDER_STATUS_OPTIONS","Object","keys","ORDER_STATUS_LABELS","map","status","value","label","ORDER_CHANNEL_OPTIONS","ORDER_CHANNEL_LABELS","channel","ORDER_DISPUTE_OPTIONS","ORDER_SEARCH_FIELDS","ORDER_HIDDEN_COLUMNS","OPEN_DISPUTE_CLAUSE","field","op","HostOrdersCard","props","draft","hostId","firestore","data","orderDocs","idField","productDocs","orderWindow","undefined","productWindow","productNames","product","rows","$id","name","org","ready","orgReady","showStats","orders","searchParams","clauses","setClauses","customer","get","trim","selectedId","setSelectedId","seededOrderId","seededOrder","setSeededOrder","seededOpened","current","some","order","then","snapshot","id","exists","catch","setDraft","user","enqueueSnackbar","orderRows","lifted","liftLegacyOrder","dispute","describeOrderDispute","Set","lineItems","line","productId","filter","formatOrderNumber","itemName","netCents","orderNetCents","disputeBadge","orderHasOpenDispute","tone","productOptions","String","filterOptions","listFilter","fields","options","headers","search","onChange","visibleOrders","showingOpenDisputes","clause","openDisputes","summariseOpenDisputes","summary","summarizeOrderWindow","nowMs","Date","now","handleExportCsv","csv","buildOrdersCsv","url","URL","createObjectURL","Blob","type","anchor","document","createElement","href","download","click","revokeObjectURL","draftAttemptKey","variantId","quantity","email","shipTo","handleDraftCreate","prev","busy","globalThis","crypto","randomUUID","Math","random","toString","slice","response","method","body","JSON","stringify","Number","shippingCountry","payload","json","ok","needsShippingCountry","shipCountries","shippingCountries","length","CHECKOUT_SHIPPING_COUNTRIES","error","variant","allowDuplicate","navigator","clipboard","writeText","persist","selectedOrder","find","openDraft","selectableProducts","deletedAt","orderColumns","headerName","flex","minWidth","renderCell","row","extraItems","max","sx","noWrap","color","width","orderChannelLabel","align","headerAlign","alignItems","toFixed","refundedCents","totals","totalCents","amountCents","direction","spacing","size","ORDER_STATUS_COLOR","title","detail","evidenceDaysLeft","component","createdAt","toDate","toLocaleDateString","toLocaleTimeString","header","help","excerpt","contentGutterX","contentGutterY","onClick","flexWrap","rowGap","revenueCents","deltaPct","revenueDeltaPct","deltaCaption","ordersDeltaPct","aovCents","aovDeltaPct","count","severity","overdue","action","soonestDaysLeft","justifyContent","chipsProps","aria-label","columns","filterColumns","onOpen","gridProps","initialState","columnVisibilityModel","noRowsLabel","truncated","onClose","open","Boolean","maxWidth","fullWidth","display","flexDirection","gap","event","target","select","mt","helperText","item","variants","liftLegacyProduct","values","join","priceUsd","replace","slotProps","htmlInput","inputMode","code","CHECKOUT_SHIPPING_COUNTRY_NAMES","disabled","displayName"],"mappings":"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"}
|
|
@@ -62,6 +62,8 @@ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
|
62
62
|
source: 'newsletter'
|
|
63
63
|
}, options.marketingConsent ? {
|
|
64
64
|
marketingConsent: true
|
|
65
|
+
} : {}, options.disclosedConsentGroup ? {
|
|
66
|
+
disclosedConsentGroup: options.disclosedConsentGroup
|
|
65
67
|
} : {}));
|
|
66
68
|
} catch (error) {
|
|
67
69
|
console.error('list enrollment failed', error);
|
|
@@ -222,6 +224,12 @@ import { NO_CLIENT_ADDRESS_BUCKET, readClientIp } from "@aglyn/aglyn/app-utils/r
|
|
|
222
224
|
const hostId = String((_body_hostId = body.hostId) != null ? _body_hostId : '');
|
|
223
225
|
const email = String((_body_email = body.email) != null ? _body_email : '').trim().toLowerCase();
|
|
224
226
|
const listId = String((_body_listId = body.listId) != null ? _body_listId : '').trim();
|
|
227
|
+
/*
|
|
228
|
+
* The key of the consent-group sentence the signup rendered, when it
|
|
229
|
+
* rendered one (AGL-3320). The opt-in reaches the site's whole consent
|
|
230
|
+
* group only when this is the group's current key; a signup that shows no
|
|
231
|
+
* sentence sends none and subscribes the person to this site alone.
|
|
232
|
+
*/ const disclosedConsentGroup = typeof body.__consentGroup === 'string' ? body.__consentGroup.trim().slice(0, 64) : '';
|
|
225
233
|
if (!hostId || !EMAIL_PATTERN.test(email)) {
|
|
226
234
|
return res.status(400).json({
|
|
227
235
|
error: 'Enter a valid email'
|
|
@@ -284,7 +292,9 @@ import { NO_CLIENT_ADDRESS_BUCKET, readClientIp } from "@aglyn/aglyn/app-utils/r
|
|
|
284
292
|
// subscribes stays a customer.
|
|
285
293
|
lifecycleFloor: 'subscriber',
|
|
286
294
|
marketingConsent: true
|
|
287
|
-
},
|
|
295
|
+
}, disclosedConsentGroup ? {
|
|
296
|
+
disclosedConsentGroup
|
|
297
|
+
} : {}, campaignTouch ? {
|
|
288
298
|
detail: {
|
|
289
299
|
campaignTouch
|
|
290
300
|
}
|
|
@@ -315,12 +325,14 @@ import { NO_CLIENT_ADDRESS_BUCKET, readClientIp } from "@aglyn/aglyn/app-utils/r
|
|
|
315
325
|
* (`docs/specs/email-overhaul.md` §1d). This is a DECLARED opt-in and
|
|
316
326
|
* not an inference from an act: the request this handler serves is
|
|
317
327
|
* "subscribe me", which is the checkbox.
|
|
318
|
-
*/ await enrollInList({
|
|
328
|
+
*/ await enrollInList(_extends({
|
|
319
329
|
hostId,
|
|
320
330
|
listId,
|
|
321
331
|
email,
|
|
322
332
|
marketingConsent: true
|
|
323
|
-
}
|
|
333
|
+
}, disclosedConsentGroup ? {
|
|
334
|
+
disclosedConsentGroup
|
|
335
|
+
} : {}));
|
|
324
336
|
}
|
|
325
337
|
/*
|
|
326
338
|
* The caller is told which of the two things happened, so the signup form
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/newsletter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\nimport {\n mergeEmailTopics,\n normalizeEmailTopic,\n topicRequiresDoubleOptIn,\n EMAIL_TOPICS_COLLECTION,\n type EmailTopic,\n} from '@aglyn/aglyn/app-utils/email-topics'\nimport {\n enrollListMember,\n firebaseAdmin,\n hostSendingIdentity,\n meterHostEmail,\n consentGroupForSite,\n orgDataCollectionForHost,\n recordPendingTopicConfirmation,\n resolveCampaignTouch,\n resolveOrgIdForHost,\n siteRequiresDoubleOptIn,\n} from '@aglyn/tenant-data-admin'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\nimport { buildConfirmUrl } from '@aglyn/tenant-data-admin/server/email-unsubscribe-link'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * The stream a newsletter signup joins.\n *\n * The built-in id, not the campaign default: somebody typing their address\n * into a footer box is asking for the newsletter, and attributing it to\n * \"Promotions and offers\" would confirm them for a stream they did not name.\n */\nconst NEWSLETTER_TOPIC_ID = 'newsletter'\n\n/**\n * Enrolls one address into an org list's members (AGL-2499).\n *\n * The document id comes from `enrollListMember`, which is the only writer of\n * that collection — the workflow `enrollList` step reaches it too, and when\n * the two derived their own ids the same person subscribing by both routes\n * became two members of one list.\n *\n * Best-effort and silent on any problem: list enrollment rides along with\n * the newsletter signup, which must still succeed (and still upsert the\n * contact) even when the list id is stale, was mistyped into the besigner\n * prop, or belongs to an org this host cannot resolve.\n */\nasync function enrollInList(options: {\n hostId: string\n listId: string\n email: string\n name?: string\n /** The opt-in the signup itself carries — see the call site. */\n marketingConsent?: boolean\n}): Promise<void> {\n if (!isDocumentId(options.listId)) return\n try {\n const contactsRef = await orgDataCollectionForHost(options.hostId, 'contacts')\n const listRef = contactsRef.parent?.collection('lists').doc(options.listId)\n if (!listRef) return\n const listSnapshot = await listRef.get()\n // A stale/mistyped id must not silently CREATE a list — campaign-send's\n // `list` audience would then read a list nobody set up.\n if (!listSnapshot.exists) return\n await enrollListMember({\n listRef,\n group: await consentGroupForSite(options.hostId),\n email: options.email,\n ...(options.name ? { name: options.name } : {}),\n source: 'newsletter',\n ...(options.marketingConsent ? { marketingConsent: true } : {}),\n })\n } catch (error) {\n console.error('list enrollment failed', error)\n }\n}\n\n/**\n * DOUBLE OPT-IN, when this site asks for one\n * (`docs/specs/email-competitive-gaps.md` P8).\n *\n * A footer signup is where a confirmation belongs — ActiveCampaign's shape,\n * where forms are the thing that default to it — and it is the only capture\n * path in the product where the person is present, expecting a reply, and has\n * just typed the address themselves.\n *\n * ## The confirmation message is TRANSACTIONAL\n *\n * It carries no marketing context, so it passes no frequency cap and adds no\n * unsubscribe header: somebody who just asked to subscribe is owed the answer\n * to what they asked, and gating it behind a marketing ceiling would drop\n * exactly the message that lets them out of the quarantine. The suppression\n * lists are not consulted here either — `recordPendingTopicConfirmation`\n * refuses an address that left this stream before any message is composed,\n * which is the check that matters: a signup form must not become a way to\n * mail somebody who unsubscribed by asking them again.\n *\n * A hard-bounced address is deliberately NOT refused here, and the asymmetry\n * with the opt-out above is the point. An opt-out is a stated intention; a\n * bounce is a measurement of a mailbox at a moment, and refusing to send at\n * submission time would settle it on evidence that cannot be older than the\n * question being asked. The confirmation goes out, and if it is delivered and\n * clicked, `confirmTopicSubscription` lifts the platform bounce — the round\n * trip is what proves the record stale. A complaint or an unsubscribe is\n * never lifted by that path.\n *\n * ## Best-effort, like the enrollment beside it\n *\n * A failure to send leaves the address pending and unmailable, which is the\n * safe direction: they signed up, nothing reaches them, and signing up again\n * re-sends. The signup itself still succeeds — the contact is captured with\n * its consent record either way, and losing the capture because a confirmation\n * bounced would be a worse trade for everybody.\n *\n * @returns whether the address was put in the quarantine, so the caller can\n * decide whether to enroll them on a list yet.\n */\nasync function requestConfirmation(options: {\n hostId: string\n email: string\n topics: EmailTopic[]\n /** The host document the setting was read from — see {@link loadHostGate}. */\n host: { cname?: unknown; subdomain?: unknown }\n}): Promise<boolean> {\n const topic = options.topics.find(\n (candidate) => candidate.id === NEWSLETTER_TOPIC_ID,\n )\n const { result } = await recordPendingTopicConfirmation(\n options.hostId,\n options.email,\n NEWSLETTER_TOPIC_ID,\n )\n if (result !== 'pending') return false\n try {\n const siteBase =\n hostPublicOrigin({\n cname: String(options.host.cname ?? ''),\n subdomain: String(options.host.subdomain ?? ''),\n }) ?? ''\n const url = buildConfirmUrl({\n siteBase,\n hostId: options.hostId,\n email: options.email,\n topicId: NEWSLETTER_TOPIC_ID,\n })\n // No origin or no signing secret means no link, and a confirmation\n // message with nothing to click is worse than none: the person believes\n // they have subscribed and nothing ever arrives. Said out loud rather\n // than shipped quietly, exactly as the unsubscribe seam does.\n if (!url) {\n console.warn(\n '[newsletter] confirmation not sent — no public origin or no ' +\n 'EMAIL_UNSUBSCRIBE_SECRET',\n )\n return true\n }\n const stream = topic?.name ?? 'our newsletter'\n const result = await sendEmail({\n to: options.email,\n subject: `Confirm your subscription`,\n text:\n `Please confirm that you want to receive ${stream} at this ` +\n `address:\\n\\n${url}\\n\\nThe link works for three days. If you did ` +\n 'not sign up, ignore this message — nothing will be sent.',\n sendingIdentity: await hostSendingIdentity(options.hostId),\n audience: 'tenant',\n context: 'newsletter confirmation',\n })\n /*\n * Counted against the site, as TRANSACTIONAL.\n *\n * It is a message this site sent, so it costs what a message costs and\n * the meter has to see it. `'transactional'` is the honest class: the\n * person just typed their address into a form and is waiting for the\n * answer, which is why the message carries no marketing context and no\n * unsubscribe header either. Only a delivery is counted, so a send that\n * never left is not billed as one.\n */\n if (result.sent) await meterHostEmail(options.hostId)\n } catch (error) {\n console.error('confirmation send failed', error)\n }\n return true\n}\n\n/**\n * The site's topic catalog, for the one question the signup asks of it.\n *\n * Fails soft to the built-ins, like the preference page's read: a site whose\n * org cannot be resolved still gets the four defaults, so the decision is\n * made against a catalog rather than against nothing.\n */\nasync function loadTopics(hostId: string): Promise<EmailTopic[]> {\n try {\n const orgId = await resolveOrgIdForHost(hostId)\n if (!orgId) return mergeEmailTopics(null)\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(EMAIL_TOPICS_COLLECTION)\n .get()\n return mergeEmailTopics(\n (snapshot?.docs ?? [])\n .map((doc: any) => normalizeEmailTopic(doc.id, doc.data()))\n .filter((topic: EmailTopic | null): topic is EmailTopic => !!topic),\n )\n } catch (error) {\n console.error('[newsletter] topic catalog read failed', error)\n return mergeEmailTopics(null)\n }\n}\n\n/**\n * Everything the confirmation decision needs, in one round trip.\n *\n * Three reads — the host document, the org lookup and the topic catalog — and\n * the first is issued IN PARALLEL with the other two rather than after them,\n * because none of the three depends on another's answer. A signup is a person\n * waiting for a form to come back, and serialized reads on that path are\n * latency the person feels for a decision that is usually \"no\".\n *\n * The host document is returned rather than discarded: the confirmation link\n * needs the site's public origin, which is on that same document, and reading\n * it twice would be a read per signup to recover something already in hand.\n *\n * Fails soft on the site flag, which `siteRequiresDoubleOptIn` already does —\n * a default a failed read could switch ON would quarantine every new signup\n * on a site whose owner never asked for confirmations.\n */\nasync function loadHostGate(hostId: string): Promise<{\n topics: EmailTopic[]\n siteDefault: boolean\n host: { cname?: unknown; subdomain?: unknown }\n}> {\n const [topics, siteDefault, snapshot] = await Promise.all([\n loadTopics(hostId),\n siteRequiresDoubleOptIn(hostId),\n firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .get()\n .catch(() => null),\n ])\n return {\n topics,\n siteDefault,\n host: {\n cname: snapshot?.get('cname'),\n subdomain: snapshot?.get('subdomain'),\n },\n }\n}\n\n// Best-effort per-instance flood damper.\nconst attemptsByIp = new Map<string, number[]>()\nimport {\n NO_CLIENT_ADDRESS_BUCKET,\n readClientIp,\n} from '@aglyn/aglyn/app-utils/request-ip'\n\n/**\n * Newsletter opt-in (AGL-301): footer signups and checkout opt-ins land\n * in the contacts CRM with an explicit consent timestamp, feeding the\n * email-campaign audiences.\n */\nexport const newsletterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const email = String(body.email ?? '')\n .trim()\n .toLowerCase()\n const listId = String(body.listId ?? '').trim()\n if (!hostId || !EMAIL_PATTERN.test(email)) {\n return res.status(400).json({ error: 'Enter a valid email' })\n }\n // Keeps counting under the no-address bucket rather than being skipped: an\n // unauthenticated signup endpoint that stops counting is an open write into\n // the contacts CRM.\n const ip =\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n NO_CLIENT_ADDRESS_BUCKET\n const now = Date.now()\n const attempts = (attemptsByIp.get(ip) ?? []).filter(\n (at) => now - at < 60_000,\n )\n attempts.push(now)\n attemptsByIp.set(ip, attempts)\n if (attempts.length > 10) {\n return res.status(429).json({ error: 'Too many attempts' })\n }\n try {\n // A newsletter signup is an identify moment: the visitor was anonymous\n // while they browsed and this request is the first thing that names them.\n // Both channels are asked once, here, and the later touch is credited.\n const campaignTouch = await resolveCampaignTouch({\n hostId,\n wire: body.campaignTouch,\n email,\n atMs: now,\n })\n /*\n * Reported to whichever plugin keeps people, not written by this one\n * (AGL-3080). A newsletter signup is commerce meeting somebody; what a\n * person record is, and whether this workspace keeps any, belongs to the\n * plugin that models them.\n *\n * Still awaited, because the confirmation below is deliberately after it.\n */\n await recordCapturedContact({\n /*\n * Not resolved here. The record system keys a person on the SITE they\n * were met on, so this costs the capture nothing — and resolving it\n * would put a Firestore read in front of the writer, which is the one\n * thing `recordCapturedContact` asks a door not to do.\n */\n orgId: '',\n hostId,\n identity: { email },\n interaction: {\n source: 'newsletter',\n refId: `newsletter-${now}`,\n summary: 'Subscribed to the newsletter',\n },\n // A TOUCH (AGL-3232): the opt-in lands on the lead the site is\n // working for this address when it holds one, and on the contact\n // otherwise — the consent stays with the one record the person is.\n surface: 'touch',\n // The stage the name is for (AGL-2612): somebody who asked to hear\n // from the site and nothing more yet. A floor, so a customer who\n // subscribes stays a customer.\n lifecycleFloor: 'subscriber',\n marketingConsent: true,\n // Where the visitor ARRIVED from — a fact about this visit and not\n // about the person, which is what `detail` carries.\n ...(campaignTouch ? { detail: { campaignTouch } } : {}),\n })\n /*\n * The confirmation, when this site asks for one.\n *\n * AFTER the contact upsert and never instead of it. The person typed\n * their address and ticked a box, and that consent record is a fact\n * whether or not they go on to click a link — withholding the capture\n * until they do would lose the record of what they actually did. What the\n * confirmation gates is the SEND, on the person's own topic entry, which\n * is where `filterTopicSendable` reads it.\n */\n const { topics, siteDefault, host } = await loadHostGate(hostId)\n const confirming =\n topicRequiresDoubleOptIn(\n topics.find((topic) => topic.id === NEWSLETTER_TOPIC_ID),\n siteDefault,\n ) && (await requestConfirmation({ hostId, email, topics, host }))\n if (listId) {\n /*\n * The same basis the contact upsert records, on the membership too.\n *\n * A list membership had no consent field at all, so `audience: 'list'`\n * gave the send-time join nothing to read even for the one audience\n * whose members literally asked for a newsletter\n * (`docs/specs/email-overhaul.md` §1d). This is a DECLARED opt-in and\n * not an inference from an act: the request this handler serves is\n * \"subscribe me\", which is the checkbox.\n */\n await enrollInList({ hostId, listId, email, marketingConsent: true })\n }\n /*\n * The caller is told which of the two things happened, so the signup form\n * can say \"check your email\" rather than \"you're subscribed\".\n *\n * A form that reported success identically either way would be the site\n * telling somebody they are on the list while the send path refuses them,\n * and the only way they could find out is by noticing that nothing\n * arrives.\n */\n return res.status(200).json({ ok: true, confirmationRequired: confirming })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Signup failed' })\n }\n}\n"],"names":["hostPublicOrigin","mergeEmailTopics","normalizeEmailTopic","topicRequiresDoubleOptIn","EMAIL_TOPICS_COLLECTION","enrollListMember","firebaseAdmin","hostSendingIdentity","meterHostEmail","consentGroupForSite","orgDataCollectionForHost","recordPendingTopicConfirmation","resolveCampaignTouch","resolveOrgIdForHost","siteRequiresDoubleOptIn","recordCapturedContact","buildConfirmUrl","sendEmail","isDocumentId","EMAIL_PATTERN","NEWSLETTER_TOPIC_ID","enrollInList","options","listId","contactsRef","hostId","listRef","parent","collection","doc","listSnapshot","get","exists","group","email","name","source","marketingConsent","error","console","requestConfirmation","topic","topics","find","candidate","id","result","siteBase","cname","String","host","subdomain","url","topicId","warn","stream","to","subject","text","sendingIdentity","audience","context","sent","loadTopics","orgId","snapshot","app","firestore","docs","map","data","filter","loadHostGate","siteDefault","Promise","all","catch","attemptsByIp","Map","NO_CLIENT_ADDRESS_BUCKET","readClientIp","newsletterHandler","req","res","body","method","status","json","JSON","parse","trim","toLowerCase","test","ip","headers","remoteAddress","socket","now","Date","attempts","at","push","set","length","campaignTouch","wire","atMs","identity","interaction","refId","summary","surface","lifecycleFloor","detail","confirming","ok","confirmationRequired"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/newsletter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { hostPublicOrigin } from '@aglyn/aglyn/app-utils/host-naming'\nimport {\n mergeEmailTopics,\n normalizeEmailTopic,\n topicRequiresDoubleOptIn,\n EMAIL_TOPICS_COLLECTION,\n type EmailTopic,\n} from '@aglyn/aglyn/app-utils/email-topics'\nimport {\n enrollListMember,\n firebaseAdmin,\n hostSendingIdentity,\n meterHostEmail,\n consentGroupForSite,\n orgDataCollectionForHost,\n recordPendingTopicConfirmation,\n resolveCampaignTouch,\n resolveOrgIdForHost,\n siteRequiresDoubleOptIn,\n} from '@aglyn/tenant-data-admin'\nimport recordCapturedContact from '@aglyn/aglyn/plugin-manager/record-captured-contact'\nimport { buildConfirmUrl } from '@aglyn/tenant-data-admin/server/email-unsubscribe-link'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\n/**\n * The stream a newsletter signup joins.\n *\n * The built-in id, not the campaign default: somebody typing their address\n * into a footer box is asking for the newsletter, and attributing it to\n * \"Promotions and offers\" would confirm them for a stream they did not name.\n */\nconst NEWSLETTER_TOPIC_ID = 'newsletter'\n\n/**\n * Enrolls one address into an org list's members (AGL-2499).\n *\n * The document id comes from `enrollListMember`, which is the only writer of\n * that collection — the workflow `enrollList` step reaches it too, and when\n * the two derived their own ids the same person subscribing by both routes\n * became two members of one list.\n *\n * Best-effort and silent on any problem: list enrollment rides along with\n * the newsletter signup, which must still succeed (and still upsert the\n * contact) even when the list id is stale, was mistyped into the besigner\n * prop, or belongs to an org this host cannot resolve.\n */\nasync function enrollInList(options: {\n hostId: string\n listId: string\n email: string\n name?: string\n /** The opt-in the signup itself carries — see the call site. */\n marketingConsent?: boolean\n /**\n * The disclosure key the signup rendered beside its opt-in, as the contact\n * capture received it: the membership pools over the site's consent group\n * only when it is the group's current key (AGL-3320).\n */\n disclosedConsentGroup?: string\n}): Promise<void> {\n if (!isDocumentId(options.listId)) return\n try {\n const contactsRef = await orgDataCollectionForHost(options.hostId, 'contacts')\n const listRef = contactsRef.parent?.collection('lists').doc(options.listId)\n if (!listRef) return\n const listSnapshot = await listRef.get()\n // A stale/mistyped id must not silently CREATE a list — campaign-send's\n // `list` audience would then read a list nobody set up.\n if (!listSnapshot.exists) return\n await enrollListMember({\n listRef,\n group: await consentGroupForSite(options.hostId),\n email: options.email,\n ...(options.name ? { name: options.name } : {}),\n source: 'newsletter',\n ...(options.marketingConsent ? { marketingConsent: true } : {}),\n ...(options.disclosedConsentGroup\n ? { disclosedConsentGroup: options.disclosedConsentGroup }\n : {}),\n })\n } catch (error) {\n console.error('list enrollment failed', error)\n }\n}\n\n/**\n * DOUBLE OPT-IN, when this site asks for one\n * (`docs/specs/email-competitive-gaps.md` P8).\n *\n * A footer signup is where a confirmation belongs — ActiveCampaign's shape,\n * where forms are the thing that default to it — and it is the only capture\n * path in the product where the person is present, expecting a reply, and has\n * just typed the address themselves.\n *\n * ## The confirmation message is TRANSACTIONAL\n *\n * It carries no marketing context, so it passes no frequency cap and adds no\n * unsubscribe header: somebody who just asked to subscribe is owed the answer\n * to what they asked, and gating it behind a marketing ceiling would drop\n * exactly the message that lets them out of the quarantine. The suppression\n * lists are not consulted here either — `recordPendingTopicConfirmation`\n * refuses an address that left this stream before any message is composed,\n * which is the check that matters: a signup form must not become a way to\n * mail somebody who unsubscribed by asking them again.\n *\n * A hard-bounced address is deliberately NOT refused here, and the asymmetry\n * with the opt-out above is the point. An opt-out is a stated intention; a\n * bounce is a measurement of a mailbox at a moment, and refusing to send at\n * submission time would settle it on evidence that cannot be older than the\n * question being asked. The confirmation goes out, and if it is delivered and\n * clicked, `confirmTopicSubscription` lifts the platform bounce — the round\n * trip is what proves the record stale. A complaint or an unsubscribe is\n * never lifted by that path.\n *\n * ## Best-effort, like the enrollment beside it\n *\n * A failure to send leaves the address pending and unmailable, which is the\n * safe direction: they signed up, nothing reaches them, and signing up again\n * re-sends. The signup itself still succeeds — the contact is captured with\n * its consent record either way, and losing the capture because a confirmation\n * bounced would be a worse trade for everybody.\n *\n * @returns whether the address was put in the quarantine, so the caller can\n * decide whether to enroll them on a list yet.\n */\nasync function requestConfirmation(options: {\n hostId: string\n email: string\n topics: EmailTopic[]\n /** The host document the setting was read from — see {@link loadHostGate}. */\n host: { cname?: unknown; subdomain?: unknown }\n}): Promise<boolean> {\n const topic = options.topics.find(\n (candidate) => candidate.id === NEWSLETTER_TOPIC_ID,\n )\n const { result } = await recordPendingTopicConfirmation(\n options.hostId,\n options.email,\n NEWSLETTER_TOPIC_ID,\n )\n if (result !== 'pending') return false\n try {\n const siteBase =\n hostPublicOrigin({\n cname: String(options.host.cname ?? ''),\n subdomain: String(options.host.subdomain ?? ''),\n }) ?? ''\n const url = buildConfirmUrl({\n siteBase,\n hostId: options.hostId,\n email: options.email,\n topicId: NEWSLETTER_TOPIC_ID,\n })\n // No origin or no signing secret means no link, and a confirmation\n // message with nothing to click is worse than none: the person believes\n // they have subscribed and nothing ever arrives. Said out loud rather\n // than shipped quietly, exactly as the unsubscribe seam does.\n if (!url) {\n console.warn(\n '[newsletter] confirmation not sent — no public origin or no ' +\n 'EMAIL_UNSUBSCRIBE_SECRET',\n )\n return true\n }\n const stream = topic?.name ?? 'our newsletter'\n const result = await sendEmail({\n to: options.email,\n subject: `Confirm your subscription`,\n text:\n `Please confirm that you want to receive ${stream} at this ` +\n `address:\\n\\n${url}\\n\\nThe link works for three days. If you did ` +\n 'not sign up, ignore this message — nothing will be sent.',\n sendingIdentity: await hostSendingIdentity(options.hostId),\n audience: 'tenant',\n context: 'newsletter confirmation',\n })\n /*\n * Counted against the site, as TRANSACTIONAL.\n *\n * It is a message this site sent, so it costs what a message costs and\n * the meter has to see it. `'transactional'` is the honest class: the\n * person just typed their address into a form and is waiting for the\n * answer, which is why the message carries no marketing context and no\n * unsubscribe header either. Only a delivery is counted, so a send that\n * never left is not billed as one.\n */\n if (result.sent) await meterHostEmail(options.hostId)\n } catch (error) {\n console.error('confirmation send failed', error)\n }\n return true\n}\n\n/**\n * The site's topic catalog, for the one question the signup asks of it.\n *\n * Fails soft to the built-ins, like the preference page's read: a site whose\n * org cannot be resolved still gets the four defaults, so the decision is\n * made against a catalog rather than against nothing.\n */\nasync function loadTopics(hostId: string): Promise<EmailTopic[]> {\n try {\n const orgId = await resolveOrgIdForHost(hostId)\n if (!orgId) return mergeEmailTopics(null)\n const snapshot = await firebaseAdmin\n .app()\n .firestore()\n .collection('orgs')\n .doc(orgId)\n .collection(EMAIL_TOPICS_COLLECTION)\n .get()\n return mergeEmailTopics(\n (snapshot?.docs ?? [])\n .map((doc: any) => normalizeEmailTopic(doc.id, doc.data()))\n .filter((topic: EmailTopic | null): topic is EmailTopic => !!topic),\n )\n } catch (error) {\n console.error('[newsletter] topic catalog read failed', error)\n return mergeEmailTopics(null)\n }\n}\n\n/**\n * Everything the confirmation decision needs, in one round trip.\n *\n * Three reads — the host document, the org lookup and the topic catalog — and\n * the first is issued IN PARALLEL with the other two rather than after them,\n * because none of the three depends on another's answer. A signup is a person\n * waiting for a form to come back, and serialized reads on that path are\n * latency the person feels for a decision that is usually \"no\".\n *\n * The host document is returned rather than discarded: the confirmation link\n * needs the site's public origin, which is on that same document, and reading\n * it twice would be a read per signup to recover something already in hand.\n *\n * Fails soft on the site flag, which `siteRequiresDoubleOptIn` already does —\n * a default a failed read could switch ON would quarantine every new signup\n * on a site whose owner never asked for confirmations.\n */\nasync function loadHostGate(hostId: string): Promise<{\n topics: EmailTopic[]\n siteDefault: boolean\n host: { cname?: unknown; subdomain?: unknown }\n}> {\n const [topics, siteDefault, snapshot] = await Promise.all([\n loadTopics(hostId),\n siteRequiresDoubleOptIn(hostId),\n firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .get()\n .catch(() => null),\n ])\n return {\n topics,\n siteDefault,\n host: {\n cname: snapshot?.get('cname'),\n subdomain: snapshot?.get('subdomain'),\n },\n }\n}\n\n// Best-effort per-instance flood damper.\nconst attemptsByIp = new Map<string, number[]>()\nimport {\n NO_CLIENT_ADDRESS_BUCKET,\n readClientIp,\n} from '@aglyn/aglyn/app-utils/request-ip'\n\n/**\n * Newsletter opt-in (AGL-301): footer signups and checkout opt-ins land\n * in the contacts CRM with an explicit consent timestamp, feeding the\n * email-campaign audiences.\n */\nexport const newsletterHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const email = String(body.email ?? '')\n .trim()\n .toLowerCase()\n const listId = String(body.listId ?? '').trim()\n /*\n * The key of the consent-group sentence the signup rendered, when it\n * rendered one (AGL-3320). The opt-in reaches the site's whole consent\n * group only when this is the group's current key; a signup that shows no\n * sentence sends none and subscribes the person to this site alone.\n */\n const disclosedConsentGroup =\n typeof body.__consentGroup === 'string'\n ? body.__consentGroup.trim().slice(0, 64)\n : ''\n if (!hostId || !EMAIL_PATTERN.test(email)) {\n return res.status(400).json({ error: 'Enter a valid email' })\n }\n // Keeps counting under the no-address bucket rather than being skipped: an\n // unauthenticated signup endpoint that stops counting is an open write into\n // the contacts CRM.\n const ip =\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n NO_CLIENT_ADDRESS_BUCKET\n const now = Date.now()\n const attempts = (attemptsByIp.get(ip) ?? []).filter(\n (at) => now - at < 60_000,\n )\n attempts.push(now)\n attemptsByIp.set(ip, attempts)\n if (attempts.length > 10) {\n return res.status(429).json({ error: 'Too many attempts' })\n }\n try {\n // A newsletter signup is an identify moment: the visitor was anonymous\n // while they browsed and this request is the first thing that names them.\n // Both channels are asked once, here, and the later touch is credited.\n const campaignTouch = await resolveCampaignTouch({\n hostId,\n wire: body.campaignTouch,\n email,\n atMs: now,\n })\n /*\n * Reported to whichever plugin keeps people, not written by this one\n * (AGL-3080). A newsletter signup is commerce meeting somebody; what a\n * person record is, and whether this workspace keeps any, belongs to the\n * plugin that models them.\n *\n * Still awaited, because the confirmation below is deliberately after it.\n */\n await recordCapturedContact({\n /*\n * Not resolved here. The record system keys a person on the SITE they\n * were met on, so this costs the capture nothing — and resolving it\n * would put a Firestore read in front of the writer, which is the one\n * thing `recordCapturedContact` asks a door not to do.\n */\n orgId: '',\n hostId,\n identity: { email },\n interaction: {\n source: 'newsletter',\n refId: `newsletter-${now}`,\n summary: 'Subscribed to the newsletter',\n },\n // A TOUCH (AGL-3232): the opt-in lands on the lead the site is\n // working for this address when it holds one, and on the contact\n // otherwise — the consent stays with the one record the person is.\n surface: 'touch',\n // The stage the name is for (AGL-2612): somebody who asked to hear\n // from the site and nothing more yet. A floor, so a customer who\n // subscribes stays a customer.\n lifecycleFloor: 'subscriber',\n marketingConsent: true,\n ...(disclosedConsentGroup ? { disclosedConsentGroup } : {}),\n // Where the visitor ARRIVED from — a fact about this visit and not\n // about the person, which is what `detail` carries.\n ...(campaignTouch ? { detail: { campaignTouch } } : {}),\n })\n /*\n * The confirmation, when this site asks for one.\n *\n * AFTER the contact upsert and never instead of it. The person typed\n * their address and ticked a box, and that consent record is a fact\n * whether or not they go on to click a link — withholding the capture\n * until they do would lose the record of what they actually did. What the\n * confirmation gates is the SEND, on the person's own topic entry, which\n * is where `filterTopicSendable` reads it.\n */\n const { topics, siteDefault, host } = await loadHostGate(hostId)\n const confirming =\n topicRequiresDoubleOptIn(\n topics.find((topic) => topic.id === NEWSLETTER_TOPIC_ID),\n siteDefault,\n ) && (await requestConfirmation({ hostId, email, topics, host }))\n if (listId) {\n /*\n * The same basis the contact upsert records, on the membership too.\n *\n * A list membership had no consent field at all, so `audience: 'list'`\n * gave the send-time join nothing to read even for the one audience\n * whose members literally asked for a newsletter\n * (`docs/specs/email-overhaul.md` §1d). This is a DECLARED opt-in and\n * not an inference from an act: the request this handler serves is\n * \"subscribe me\", which is the checkbox.\n */\n await enrollInList({\n hostId,\n listId,\n email,\n marketingConsent: true,\n ...(disclosedConsentGroup ? { disclosedConsentGroup } : {}),\n })\n }\n /*\n * The caller is told which of the two things happened, so the signup form\n * can say \"check your email\" rather than \"you're subscribed\".\n *\n * A form that reported success identically either way would be the site\n * telling somebody they are on the list while the send path refuses them,\n * and the only way they could find out is by noticing that nothing\n * arrives.\n */\n return res.status(200).json({ ok: true, confirmationRequired: confirming })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Signup failed' })\n }\n}\n"],"names":["hostPublicOrigin","mergeEmailTopics","normalizeEmailTopic","topicRequiresDoubleOptIn","EMAIL_TOPICS_COLLECTION","enrollListMember","firebaseAdmin","hostSendingIdentity","meterHostEmail","consentGroupForSite","orgDataCollectionForHost","recordPendingTopicConfirmation","resolveCampaignTouch","resolveOrgIdForHost","siteRequiresDoubleOptIn","recordCapturedContact","buildConfirmUrl","sendEmail","isDocumentId","EMAIL_PATTERN","NEWSLETTER_TOPIC_ID","enrollInList","options","listId","contactsRef","hostId","listRef","parent","collection","doc","listSnapshot","get","exists","group","email","name","source","marketingConsent","disclosedConsentGroup","error","console","requestConfirmation","topic","topics","find","candidate","id","result","siteBase","cname","String","host","subdomain","url","topicId","warn","stream","to","subject","text","sendingIdentity","audience","context","sent","loadTopics","orgId","snapshot","app","firestore","docs","map","data","filter","loadHostGate","siteDefault","Promise","all","catch","attemptsByIp","Map","NO_CLIENT_ADDRESS_BUCKET","readClientIp","newsletterHandler","req","res","body","method","status","json","JSON","parse","trim","toLowerCase","__consentGroup","slice","test","ip","headers","remoteAddress","socket","now","Date","attempts","at","push","set","length","campaignTouch","wire","atMs","identity","interaction","refId","summary","surface","lifecycleFloor","detail","confirming","ok","confirmationRequired"],"mappings":"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"}
|