@aglyn/plugins-commerce 1.0.0-beta.231 → 1.0.0-beta.232
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 +13 -13
- package/src/lib/components/console/catalog-organization-card.component.js +1 -0
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +4 -1
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +56 -26
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-zones.d.ts +23 -0
- package/src/lib/components/console/pos-zones.js +31 -0
- package/src/lib/components/console/pos-zones.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +10 -64
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +11 -20
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
- package/src/lib/declarations.server.js +21 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +44 -1
- package/src/lib/model/commerce-orders.js +2 -1
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce.d.ts +4 -1
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/product-write.d.ts +186 -0
- package/src/lib/model/product-write.js +116 -0
- package/src/lib/model/product-write.js.map +1 -0
- package/src/lib/plugin.js +3 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/channel-order-updates.d.ts +50 -0
- package/src/lib/server/channel-order-updates.js +290 -0
- package/src/lib/server/channel-order-updates.js.map +1 -0
- package/src/lib/server/channel-orders.d.ts +85 -0
- package/src/lib/server/channel-orders.js +543 -0
- package/src/lib/server/channel-orders.js.map +1 -0
- package/src/lib/server/product-catalog.js +5 -1
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product-drafts.d.ts +5 -1
- package/src/lib/server/product-drafts.js +3 -3
- package/src/lib/server/product-drafts.js.map +1 -1
- package/src/lib/server/product-writer.d.ts +19 -0
- package/src/lib/server/product-writer.js +261 -0
- package/src/lib/server/product-writer.js.map +1 -0
- package/src/lib/server/products-write.d.ts +61 -0
- package/src/lib/server/products-write.js +375 -0
- package/src/lib/server/products-write.js.map +1 -0
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +16 -3
- package/src/lib/testing/memory-firestore.js +11 -3
- package/src/lib/testing/memory-firestore.js.map +1 -1
- package/src/lib/transfer/products.server.d.ts +5 -0
- package/src/lib/transfer/products.server.js +2 -2
- package/src/lib/transfer/products.server.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/channel-orders.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 { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport type {\n PluginChannelOrder,\n PluginChannelOrderCancelOutcome,\n PluginChannelOrderFee,\n PluginChannelOrderOutcome,\n PluginChannelOrders,\n PluginChannelOrderShortfall,\n} from '@aglyn/aglyn/plugin-manager/plugin-channel-orders'\nimport { notifyHostManagers } from '@aglyn/tenant-data-admin'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport { createHash } from 'node:crypto'\nimport * as CommerceModel from '../model'\nimport { ORDER_CANCELLED_EVENT, ORDER_PAID_EVENT } from '../model/order-events'\nimport { alertLowStockCrossing } from './low-stock'\nimport { stageOrderEvent } from './order-events'\nimport { capRestockLines, resolveTrackedRestockLines, saleReleaseCaps } from './restock-flag'\nimport { completeChannelOrder, recordChannelOrderRefund } from './channel-order-updates'\n\n/**\n * ORDERS SOLD ON ANOTHER CHANNEL (AGL-3638): commerce's implementation of\n * core's `core.channel-orders`, which a marketplace plugin records each\n * order it brings in through.\n *\n * ## One transaction, so two channels cannot sell the last unit twice\n *\n * The order, its number, every product's count, every count's ledger row and\n * the `order.paid` event are ONE Firestore transaction. Each product is read\n * with `transaction.get`, which serializes this write with every other sale\n * of that product (the AGL-2320 reasoning): a storefront checkout landing at\n * the same moment decrements from the count this write left, never from the\n * one it read. The marketplace plugin then pushes the new count to every\n * other channel.\n *\n * A marketplace order is already sold and paid when it arrives, so a shelf\n * that cannot cover it does not refuse it: the units the shelf held are\n * taken, the order is recorded whole, and the merchant is told which units\n * are short — the same loud oversell `reserve-stock.ts` raises.\n *\n * ## Once per outside order\n *\n * The order's id is derived from the channel and its order id\n * ({@link channelOrderDocId}), and the transaction refuses to write over an\n * order that exists, so a retried import answers `already` and moves nothing.\n *\n * ## No money moves here\n *\n * The buyer paid the channel. `totals.feeCents` — Aglyn's own take — is 0,\n * the channel's charges are recorded on `channelSource.fees` for the\n * merchant's books, the tax is the channel's to remit, and refunds happen on\n * the channel. No buyer email is sent: a marketplace forbids contacting its\n * buyers outside it, and none of them hands over a real address.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nconst DOC_ID = /^[A-Za-z0-9_-]{1,200}$/\nconst CHANNEL_ID = /^[a-z0-9][a-z0-9-]{0,39}$/\nconst MAX_LINES = 250\nconst MAX_UNITS = 10_000\nconst MAX_CENTS = 100_000_000_00\n\n/**\n * The order document's id for one outside order: the channel, then a hash of\n * the channel's own id. Deterministic, so a second import of the same order\n * finds the first; hashed, because a channel's id may hold characters a\n * document id may not.\n */\nexport function channelOrderDocId(channelId: string, externalOrderId: string): string {\n const digest = createHash('sha256').update(`${channelId}\\u0000${externalOrderId}`).digest('hex').slice(0, 24)\n return `${channelId}-${digest}`\n}\n\nconst whole = (value: unknown, max: number): number | null => {\n const number = Number(value)\n return Number.isInteger(number) && number >= 0 && number <= max ? number : null\n}\n\nconst clip = (value: unknown, max: number): string => String(value ?? '').trim().slice(0, max)\n\n/** What a seller refuses before reading anything, in the merchant's words; `null` when the order is well formed. */\nexport function channelOrderProblem(order: PluginChannelOrder): string | null {\n if (!DOC_ID.test(String(order?.hostId ?? '')) || /^__.*__$/.test(order.hostId)) return 'The order names no site.'\n if (!CHANNEL_ID.test(String(order.channel?.id ?? ''))) return 'The order names no channel.'\n if (!clip(order.externalOrderId, 200)) return 'The order has no id on its channel.'\n if (!/^[A-Za-z]{3}$/.test(String(order.currency ?? ''))) return 'The order names no currency.'\n const lines = Array.isArray(order.lines) ? order.lines : []\n if (!lines.length) return 'The order has no items.'\n if (lines.length > MAX_LINES) return `The order has more than ${MAX_LINES} items.`\n for (const line of lines) {\n if (whole(line?.quantity, MAX_UNITS) === null || Number(line.quantity) < 1) return 'An item of the order has no quantity.'\n if (whole(line?.unitPriceCents, MAX_CENTS) === null) return 'An item of the order has no price.'\n if (!clip(line?.externalLineId, 200)) return 'An item of the order has no id on its channel.'\n }\n for (const amount of [order.shippingCents, order.taxCents, order.discountCents, order.totalCents]) {\n if (whole(amount, MAX_CENTS) === null) return 'The order’s totals are not whole amounts.'\n }\n return null\n}\n\nconst cleanFees = (fees: readonly PluginChannelOrderFee[] | null | undefined) => {\n if (!Array.isArray(fees)) return { fees: null, totalCents: null }\n const kept = fees\n .map((fee) => ({ label: clip(fee?.label, 80) || 'Fee', amountCents: whole(fee?.amountCents, MAX_CENTS) }))\n .filter((fee): fee is { label: string; amountCents: number } => fee.amountCents !== null)\n .slice(0, 20)\n return { fees: kept, totalCents: kept.reduce((sum, fee) => sum + fee.amountCents, 0) }\n}\n\nconst address = (value: PluginChannelOrder['shippingAddress']): CommerceModel.OrderAddress | null => {\n if (!value) return null\n const out: CommerceModel.OrderAddress = {}\n for (const key of ['name', 'line1', 'line2', 'city', 'state', 'postalCode', 'country', 'phone'] as const) {\n const text = clip(value[key], 200)\n if (text) out[key] = key === 'country' ? text.toUpperCase() : text\n }\n return Object.keys(out).length ? out : null\n}\n\ninterface Located {\n productId: string\n variantId: string\n}\n\n/** The live variant whose SKU is `sku`, matched without regard to case; `null` when none or several products claim it. */\nasync function locateSku(firestore: Firestore, hostId: string, sku: string): Promise<Located | null> {\n const wanted = sku.trim().toLowerCase()\n if (!wanted) return null\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('products')\n .where('skus', 'array-contains', wanted)\n .limit(5)\n .get()\n const found: Located[] = []\n for (const doc of snapshot.docs) {\n if (doc.get('deletedAt')) continue\n const variants = Array.isArray(doc.get('variants')) ? (doc.get('variants') as Array<Record<string, unknown>>) : []\n const variant = variants.find((entry) => String(entry?.['sku'] ?? '').trim().toLowerCase() === wanted && entry?.['id'])\n if (variant) found.push({ productId: doc.id, variantId: String(variant['id']) })\n }\n // Two products sharing a SKU is ambiguous; taking either's stock would be a guess.\n return found.length === 1 ? found[0] : null\n}\n\nexport interface ChannelOrderDeps {\n firestore: () => Firestore\n now: () => number\n}\n\nconst defaultDeps = (): ChannelOrderDeps => ({ firestore: () => firebaseAdmin.app().firestore(), now: Date.now })\n\n/** Records one outside order; see the module header. */\nexport async function importChannelOrder(\n order: PluginChannelOrder,\n deps: ChannelOrderDeps = defaultDeps(),\n): Promise<PluginChannelOrderOutcome> {\n const problem = channelOrderProblem(order)\n if (problem) return { outcome: 'refused', reason: problem }\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(order.hostId)\n const orderId = channelOrderDocId(order.channel.id, clip(order.externalOrderId, 200))\n const orderRef = hostRef.collection('orders').doc(orderId)\n const channelLabel = clip(order.channel.label, 60) || order.channel.id\n\n // Already recorded: answered before any product is looked up.\n const existing = await orderRef.get()\n if (existing.exists) return alreadyOutcome(orderId, existing.data() ?? {})\n\n const store = await hostRef.collection('settings').doc('store').get()\n const storeCurrency = clip(store.get('currency'), 3).toUpperCase() || 'USD'\n const currency = order.currency.toUpperCase()\n if (currency !== storeCurrency) {\n return {\n outcome: 'refused',\n reason: `This order is in ${currency}, and the store sells in ${storeCurrency}. Ship it from ${channelLabel}.`,\n }\n }\n\n // Each line's product, found before the transaction (a query cannot run\n // inside one) and checked again inside it, where the stock is decided.\n const located: Array<Located | null> = []\n for (const line of order.lines) {\n const productId = clip(line.productId, 200)\n const variantId = clip(line.variantId, 200)\n if (productId && variantId && DOC_ID.test(productId) && !/^__.*__$/.test(productId)) {\n located.push({ productId, variantId })\n } else {\n located.push(line.sku ? await locateSku(firestore, order.hostId, line.sku) : null)\n }\n }\n\n const nowMs = deps.now()\n const placedAtMs = Number.isFinite(order.placedAtMs) && order.placedAtMs > 0 ? Math.min(order.placedAtMs, nowMs) : nowMs\n const productIds = [...new Set(located.filter((entry): entry is Located => entry !== null).map((entry) => entry.productId))]\n const crossings: Array<{ before: CommerceModel.HostProduct; after: CommerceModel.HostProduct }> = []\n\n const result = await firestore.runTransaction(async (transaction) => {\n const counterRef = hostRef.collection('counters').doc('orders')\n const [again, counter, ...productSnaps] = await Promise.all([\n transaction.get(orderRef),\n transaction.get(counterRef),\n ...productIds.map((productId) => transaction.get(hostRef.collection('products').doc(productId))),\n ])\n if (again.exists) return { kind: 'already' as const, data: again.data() ?? {} }\n const products = new Map<string, { before: CommerceModel.HostProduct; current: CommerceModel.HostProduct }>()\n productSnaps.forEach((snapshot, index) => {\n if (!snapshot.exists || snapshot.get('deletedAt')) return\n const lifted = CommerceModel.liftLegacyProduct(snapshot.data() as never)\n products.set(productIds[index], { before: lifted, current: lifted })\n })\n\n const lineItems: CommerceModel.OrderLineItem[] = []\n const shortfalls: PluginChannelOrderShortfall[] = []\n const unmatched: number[] = []\n const ledger: CommerceModel.InventoryAdjustment[] = []\n order.lines.forEach((line, lineIndex) => {\n const quantity = Number(line.quantity)\n const place = located[lineIndex]\n const held = place ? products.get(place.productId) : undefined\n const variant = held?.current.variants.find((entry) => entry.id === place?.variantId)\n if (!place || !held || !variant) {\n unmatched.push(lineIndex)\n lineItems.push({\n productId: '',\n name: clip(line.name, 200) || clip(line.sku, 120) || 'Item',\n ...(line.sku ? { sku: clip(line.sku, 120) } : {}),\n productType: 'physical',\n quantity,\n unitAmountCents: Number(line.unitPriceCents),\n })\n return\n }\n const product = held.current\n const label = Object.values(variant.options ?? {}).filter(Boolean).join(' / ')\n lineItems.push({\n productId: place.productId,\n variantId: variant.id,\n name: product.name,\n ...(label ? { variantLabel: label } : {}),\n ...(variant.sku ? { sku: variant.sku } : line.sku ? { sku: clip(line.sku, 120) } : {}),\n productType: product.type ?? 'physical',\n quantity,\n unitAmountCents: Number(line.unitPriceCents),\n })\n if (variant.inventory == null) return\n const requested = -quantity\n const applied = CommerceModel.appliedVariantInventoryDelta(product, variant.id, requested)\n const variants = CommerceModel.adjustVariantInventory(product, variant.id, requested)\n held.current = { ...product, variants }\n ledger.push({\n productId: place.productId,\n variantId: variant.id,\n delta: requested,\n ...(applied !== requested ? { appliedDelta: applied } : {}),\n reason: 'sale',\n orderId,\n source: channelLabel,\n atMs: nowMs,\n })\n const short = applied - requested\n if (short > 0 && product.oversellPolicy !== 'backorder') {\n shortfalls.push({ lineIndex, sku: variant.sku ?? line.sku ?? null, short })\n }\n })\n\n const number = Number(counter.get('next') ?? 1)\n transaction.set(counterRef, { next: number + 1 }, { merge: true })\n for (const [productId, held] of products) {\n if (held.current === held.before) continue\n transaction.set(\n hostRef.collection('products').doc(productId),\n { variants: held.current.variants, ...CommerceModel.productStockFields(held.current), updatedAtMs: nowMs },\n { merge: true },\n )\n crossings.push({ before: held.before, after: held.current })\n }\n for (const row of ledger) transaction.set(hostRef.collection('inventoryAdjustments').doc(createResourceUid()), row)\n\n const itemsCents = lineItems.reduce((sum, line) => sum + line.unitAmountCents * line.quantity, 0)\n const fees = cleanFees(order.fees)\n const lines = order.lines.map((line, lineIndex) => ({ lineIndex, externalLineId: clip(line.externalLineId, 200) }))\n const externalRef = clip(order.externalRef, 120) || clip(order.externalOrderId, 120)\n const shipTo = address(order.shippingAddress)\n const timeline: CommerceModel.OrderTimelineEvent[] = [\n { atMs: nowMs, event: 'paid', detail: `Sold on ${channelLabel}, order ${externalRef}` },\n ...(unmatched.length\n ? [\n {\n atMs: nowMs,\n event: 'line-unmatched',\n detail:\n `${unmatched.length} ${unmatched.length === 1 ? 'item matches' : 'items match'} no product's SKU, ` +\n 'so no stock was taken for ' +\n `${unmatched.length === 1 ? 'it' : 'them'}.`,\n },\n ]\n : []),\n ...(shortfalls.length\n ? [\n {\n atMs: nowMs,\n event: 'oversold',\n detail: `${shortfalls.reduce((sum, entry) => sum + entry.short, 0)} sold beyond what was in stock.`,\n },\n ]\n : []),\n ]\n const data = CommerceModel.withOrderListFields(orderId, {\n number,\n status: 'paid' as const,\n channel: 'marketplace' as const,\n channelSource: {\n channelId: order.channel.id,\n channelLabel,\n externalOrderId: clip(order.externalOrderId, 200),\n externalRef,\n lines,\n currency,\n taxRemittedByChannel: true as const,\n fees: fees.fees,\n feesTotalCents: fees.totalCents,\n ...(order.handoff === 'courier' ? { handoff: 'courier' as const } : {}),\n },\n lineItems,\n totals: {\n itemsCents,\n shippingCents: Number(order.shippingCents),\n taxCents: Number(order.taxCents),\n discountCents: Number(order.discountCents),\n totalCents: Number(order.totalCents),\n // Aglyn takes nothing from a sale another channel was paid for.\n feeCents: 0,\n },\n timeline,\n livemode: order.testMode !== true,\n customerName: clip(order.customerName, 200) || shipTo?.name || null,\n customerEmail: null,\n ...(shipTo ? { shippingAddress: shipTo } : {}),\n amountCents: Number(order.totalCents),\n feeCents: 0,\n createdAtMs: placedAtMs,\n importedAtMs: nowMs,\n createdAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n })\n transaction.set(orderRef, data)\n // The event carries the order as stored, less the sentinel only Firestore can read.\n const { createdAt: _sentinel, ...eventOrder } = data\n stageOrderEvent(transaction, ORDER_PAID_EVENT, { hostId: order.hostId, orderId, key: 'paid', order: eventOrder })\n return { kind: 'created' as const, number, lines, shortfalls, unmatched }\n })\n\n if (result.kind === 'already') return alreadyOutcome(orderId, result.data)\n const displayRef = CommerceModel.formatOrderNumber({ number: result.number }, orderId)\n for (const crossing of crossings) alertLowStockCrossing(order.hostId, crossing.before, crossing.after)\n const totalLabel = `${(Number(order.totalCents) / 100).toFixed(2)} ${currency}`\n const units = order.lines.reduce((sum, line) => sum + Number(line.quantity), 0)\n void notifyHostManagers(order.hostId, {\n type: 'content.order',\n title: `New ${channelLabel} order on {site} — ${totalLabel}`,\n body:\n `Order ${displayRef} came in from ${channelLabel} (their order ${clip(order.externalRef, 120) || order.externalOrderId}): ` +\n `${units} item${units === 1 ? '' : 's'}, ${totalLabel}. ` +\n (order.handoff === 'courier'\n ? `Have it ready for the ${channelLabel} courier.`\n : `Ship it from the order here and the tracking goes back to ${channelLabel}.`),\n link: `/${order.hostId}/products`,\n })\n if (result.shortfalls.length) {\n const short = result.shortfalls.reduce((sum, entry) => sum + entry.short, 0)\n void notifyHostManagers(order.hostId, {\n type: 'content.lowStock',\n title: `Oversold on ${channelLabel} — order ${displayRef}`,\n body:\n `${channelLabel} sold ${short} unit${short === 1 ? '' : 's'} more than {site} had in stock ` +\n `(order ${displayRef}). Restock ${short === 1 ? 'it' : 'them'}, or cancel the items on ${channelLabel}.`,\n link: `/${order.hostId}/products`,\n })\n }\n return {\n outcome: 'created',\n recordId: orderId,\n displayRef,\n lines: result.lines,\n shortfalls: result.shortfalls,\n unmatched: result.unmatched,\n }\n}\n\nfunction alreadyOutcome(recordId: string, data: Record<string, unknown>): PluginChannelOrderOutcome {\n const source = data['channelSource'] as CommerceModel.OrderChannelSource | undefined\n return {\n outcome: 'already',\n recordId,\n displayRef: CommerceModel.formatOrderNumber({ number: Number(data['number']) || undefined }, recordId),\n lines: Array.isArray(source?.lines) ? source.lines : [],\n }\n}\n\n/**\n * The channel canceled an order it sent: the order is cancelled here, and\n * the units its sale took go back on the shelf — bounded by its own ledger,\n * exactly as the console's cancel bounds them. Refuses once anything shipped.\n */\nexport async function cancelChannelOrder(\n request: { hostId: string; recordId: string; reason: string },\n deps: ChannelOrderDeps = defaultDeps(),\n): Promise<PluginChannelOrderCancelOutcome> {\n if (!DOC_ID.test(String(request?.hostId ?? '')) || !DOC_ID.test(String(request?.recordId ?? ''))) {\n return { outcome: 'no_such_record' }\n }\n if (/^__.*__$/.test(request.hostId) || /^__.*__$/.test(request.recordId)) return { outcome: 'no_such_record' }\n const firestore = deps.firestore()\n const hostRef = firestore.collection('hosts').doc(request.hostId)\n const orderRef = hostRef.collection('orders').doc(request.recordId)\n const nowMs = deps.now()\n const outcome = await firestore.runTransaction(async (transaction): Promise<PluginChannelOrderCancelOutcome> => {\n const snapshot = await transaction.get(orderRef)\n if (!snapshot.exists) return { outcome: 'no_such_record' }\n const order = CommerceModel.liftLegacyOrder((snapshot.data() ?? {}) as never)\n if (order.channel !== 'marketplace') return { outcome: 'not_cancellable', status: order.status }\n if (order.status === 'cancelled') return { outcome: 'already' }\n if (order.status !== 'paid' || (order.fulfillments ?? []).length) {\n return { outcome: 'not_cancellable', status: order.status }\n }\n const rows = await transaction.get(hostRef.collection('inventoryAdjustments').where('orderId', '==', request.recordId))\n const caps = saleReleaseCaps(rows.docs.filter((row) => row.get('reason') === 'sale'))\n const { lines, products } = await resolveTrackedRestockLines(hostRef as never, order, (ref) => transaction.get(ref))\n const byProduct = new Map<string, CommerceModel.OrderRestockLine[]>()\n for (const line of capRestockLines(lines, caps)) {\n byProduct.set(line.productId, [...(byProduct.get(line.productId) ?? []), line])\n }\n let units = 0\n for (const [productId, productLines] of byProduct) {\n const product = products.get(productId)\n if (!product) continue\n let variants = product.variants\n for (const line of productLines) {\n variants = CommerceModel.adjustVariantInventory({ variants }, line.variantId, line.quantity)\n units += line.quantity\n }\n transaction.update(hostRef.collection('products').doc(productId), {\n variants,\n ...CommerceModel.productStockFields({ ...product, variants }),\n updatedAtMs: nowMs,\n })\n for (const line of productLines) {\n transaction.set(hostRef.collection('inventoryAdjustments').doc(createResourceUid()), {\n productId,\n variantId: line.variantId,\n delta: line.quantity,\n reason: 'cancellation',\n orderId: request.recordId,\n atMs: nowMs,\n } satisfies CommerceModel.InventoryAdjustment)\n }\n }\n const reason = clip(request.reason, 200) || 'Canceled on its channel'\n const patch = {\n status: 'cancelled' as const,\n updatedAtMs: nowMs,\n timeline: CommerceModel.appendOrderEvent(\n order,\n 'cancelled',\n units > 0 ? `${reason}. ${units} ${units === 1 ? 'unit' : 'units'} returned to stock.` : reason,\n nowMs,\n ),\n }\n transaction.update(orderRef, patch)\n stageOrderEvent(transaction, ORDER_CANCELLED_EVENT, {\n hostId: request.hostId,\n orderId: request.recordId,\n key: 'cancelled',\n order: { ...(snapshot.data() ?? {}), ...patch },\n })\n return { outcome: 'cancelled', restockedUnits: units }\n })\n return outcome\n}\n\n/** The channel's charges on a sale, recorded once it says; a later answer replaces an earlier one. */\nexport async function recordChannelOrderFees(\n request: { hostId: string; recordId: string; fees: PluginChannelOrderFee[] },\n deps: ChannelOrderDeps = defaultDeps(),\n): Promise<'recorded' | 'no_such_record'> {\n if (!DOC_ID.test(String(request?.hostId ?? '')) || !DOC_ID.test(String(request?.recordId ?? ''))) return 'no_such_record'\n if (/^__.*__$/.test(request.hostId) || /^__.*__$/.test(request.recordId)) return 'no_such_record'\n const firestore = deps.firestore()\n const orderRef = firestore.collection('hosts').doc(request.hostId).collection('orders').doc(request.recordId)\n const fees = cleanFees(request.fees)\n return firestore.runTransaction(async (transaction) => {\n const snapshot = await transaction.get(orderRef)\n const source = snapshot.get('channelSource') as CommerceModel.OrderChannelSource | undefined\n if (!snapshot.exists || snapshot.get('channel') !== 'marketplace' || !source) return 'no_such_record' as const\n transaction.update(orderRef, {\n channelSource: { ...source, fees: fees.fees, feesTotalCents: fees.totalCents },\n updatedAtMs: deps.now(),\n })\n return 'recorded' as const\n })\n}\n\nexport const commerceChannelOrders: PluginChannelOrders = {\n importOrder: (order) => importChannelOrder(order),\n cancelOrder: (request) => cancelChannelOrder(request),\n recordFees: (request) => recordChannelOrderFees(request),\n // A courier's pickup and a channel's refund or adjustment (AGL-3644).\n completeOrder: (request) => completeChannelOrder(request),\n recordRefund: (request) => recordChannelOrderRefund(request),\n}\n"],"names":["createResourceUid","notifyHostManagers","firebaseAdmin","createHash","CommerceModel","ORDER_CANCELLED_EVENT","ORDER_PAID_EVENT","alertLowStockCrossing","stageOrderEvent","capRestockLines","resolveTrackedRestockLines","saleReleaseCaps","completeChannelOrder","recordChannelOrderRefund","DOC_ID","CHANNEL_ID","MAX_LINES","MAX_UNITS","MAX_CENTS","channelOrderDocId","channelId","externalOrderId","digest","update","slice","whole","value","max","number","Number","isInteger","clip","String","trim","channelOrderProblem","order","test","hostId","channel","id","currency","lines","Array","isArray","length","line","quantity","unitPriceCents","externalLineId","amount","shippingCents","taxCents","discountCents","totalCents","cleanFees","fees","kept","map","fee","label","amountCents","filter","reduce","sum","address","out","key","text","toUpperCase","Object","keys","locateSku","firestore","sku","wanted","toLowerCase","snapshot","collection","doc","where","limit","get","found","docs","variants","variant","find","entry","push","productId","variantId","defaultDeps","app","now","Date","importChannelOrder","deps","existing","problem","outcome","reason","hostRef","orderId","orderRef","channelLabel","exists","alreadyOutcome","data","store","storeCurrency","located","nowMs","placedAtMs","isFinite","Math","min","productIds","Set","crossings","result","runTransaction","transaction","again","counter","counterRef","productSnaps","Promise","all","kind","products","Map","forEach","index","lifted","liftLegacyProduct","set","before","current","lineItems","shortfalls","unmatched","ledger","lineIndex","product","place","held","undefined","name","productType","unitAmountCents","values","options","Boolean","join","variantLabel","type","inventory","requested","applied","appliedVariantInventoryDelta","adjustVariantInventory","delta","appliedDelta","source","atMs","short","oversellPolicy","next","merge","productStockFields","updatedAtMs","after","row","itemsCents","externalRef","shipTo","shippingAddress","timeline","event","detail","withOrderListFields","status","channelSource","taxRemittedByChannel","feesTotalCents","handoff","totals","feeCents","livemode","testMode","customerName","customerEmail","createdAtMs","importedAtMs","createdAt","FieldValue","serverTimestamp","_sentinel","eventOrder","displayRef","formatOrderNumber","crossing","totalLabel","toFixed","units","title","body","link","recordId","cancelChannelOrder","request","liftLegacyOrder","fulfillments","rows","caps","ref","byProduct","productLines","patch","appendOrderEvent","restockedUnits","recordChannelOrderFees","commerceChannelOrders","importOrder","cancelOrder","recordFees","completeOrder","recordRefund"],"mappings":"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"}
|
|
@@ -153,12 +153,16 @@ function optionValue(options, pattern) {
|
|
|
153
153
|
const imageUrl = variantImage != null ? variantImage : media[0];
|
|
154
154
|
const weightGrams = Number(variant.weightGrams) > 0 ? Math.round(Number(variant.weightGrams)) : undefined;
|
|
155
155
|
const gtin = CommerceModel.normalizeGtin(variant.barcode) || (hasVariants ? '' : (_ref = facts == null ? void 0 : facts.gtin) != null ? _ref : '');
|
|
156
|
+
const sku = str(variant.sku).trim().slice(0, 120);
|
|
156
157
|
const offer = _extends({
|
|
157
158
|
id: offerId(productId, variant.id, hasVariants),
|
|
158
159
|
groupId: productId,
|
|
159
160
|
hasVariants,
|
|
160
161
|
productId,
|
|
161
|
-
variantId: variant.id
|
|
162
|
+
variantId: variant.id
|
|
163
|
+
}, sku ? {
|
|
164
|
+
sku
|
|
165
|
+
} : {}, {
|
|
162
166
|
productName: product.name.trim(),
|
|
163
167
|
title: choices.length ? `${product.name.trim()} - ${choices.join(' / ')}` : product.name.trim(),
|
|
164
168
|
description,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-catalog.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 {\n CatalogOffer,\n CatalogPage,\n CatalogShippingPrice,\n CatalogStore,\n PluginProductCatalog,\n} from '@aglyn/aglyn/plugin-manager/plugin-product-catalog'\nimport { liveCustomDomain } from '@aglyn/aglyn/app-utils/host-naming'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport * as CommerceModel from '../model'\n\n/**\n * THE STORE'S CATALOG, AS OTHER PLUGINS READ IT (AGL-3637): core's\n * `core.product-catalog` contract, answered from this plugin's products.\n *\n * A shopping-channel feed, an ad catalog and a marketplace sync read every\n * offer the store sells through here and never through `hosts/{id}/products`.\n * What counts as listed, how a variant becomes an offer, what the store's\n * currency is and what shipping costs are this plugin's decisions, made once:\n *\n * - Listed is `status: 'active'` and not soft-deleted. Drafts, archived and\n * deleted products are never offered.\n * - A product with one configuration is one offer under the product's own\n * id — the id the site's analytics events already send — and a product\n * with several is one offer per variant, `{productId}_{variantId}`.\n * - A variant on sale (`compareAtPriceUsd` above `priceUsd`) offers the\n * compare-at price as its price and its own as the sale price.\n * - Shipping is priced per country the store's zones name, for one unit of\n * the offer, through the same resolver checkout charges with; local pickup\n * is not shipping and a `*` zone names no country.\n *\n * Walked in document-id order, so a page boundary is stable while products\n * are added and edited, and a product is never split across two pages.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype DocumentData = FirebaseFirestore.DocumentData\n\nconst MAX_PRODUCTS_PER_READ = 200\n/** Google's ceiling; the longest any channel accepts for every channel's id. */\nconst MAX_OFFER_ID = 50\n\nconst firestore = (): Firestore => firebaseAdmin.app().firestore()\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** Plain text from a description that may hold markup. */\nexport function plainDescription(value: unknown): string {\n return str(value)\n .replace(/<\\s*(br|\\/p|\\/div|\\/li|\\/h[1-6])\\s*\\/?>/gi, '\\n')\n .replace(/<[^>]*>/g, ' ')\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/'/gi, \"'\")\n .replace(/[ \\t\\f\\v]+/g, ' ')\n .replace(/\\s*\\n\\s*/g, '\\n')\n .trim()\n}\n\nconst toMinor = (amount: unknown): number => {\n const number = Number(amount)\n return Number.isFinite(number) && number > 0 ? Math.round(number * 100) : 0\n}\n\n/** The origin the store's pages answer on: its live custom domain, else its subdomain. */\nexport function storeOrigin(host: DocumentData | undefined): string | null {\n const domain = liveCustomDomain(host as Parameters<typeof liveCustomDomain>[0])\n if (domain) return `https://${domain}`\n const subdomain = str(host?.['subdomain']).trim()\n return subdomain ? `https://${subdomain}.${TENANT_APEX}` : null\n}\n\n/** An absolute URL for a stored media reference, or undefined. */\nfunction absoluteImage(url: unknown, origin: string | null): string | undefined {\n const value = str(url).trim()\n if (!value) return undefined\n if (/^https?:\\/\\//i.test(value)) return value\n if (value.startsWith('//')) return `https:${value}`\n if (value.startsWith('/') && origin) return `${origin}${value}`\n return undefined\n}\n\n/** A stable offer id within the longest any channel accepts. */\nexport function offerId(productId: string, variantId: string, hasVariants: boolean): string {\n if (!hasVariants) return productId.slice(0, MAX_OFFER_ID)\n const full = `${productId}_${variantId}`\n if (full.length <= MAX_OFFER_ID) return full\n // Deterministic, so the id is the same on every read.\n let hash = 0\n for (const char of full) hash = (Math.imul(hash, 31) + char.charCodeAt(0)) | 0\n const suffix = (hash >>> 0).toString(36)\n return `${productId.slice(0, MAX_OFFER_ID - suffix.length - 1)}_${suffix}`\n}\n\nconst COLOR_OPTION = /^(colou?r|shade|finish)$/i\nconst SIZE_OPTION = /^(size|fit)$/i\n\nfunction optionValue(options: Record<string, string>, pattern: RegExp): string | undefined {\n for (const [name, value] of Object.entries(options)) {\n if (pattern.test(name.trim()) && value) return value\n }\n return undefined\n}\n\n/** The category path `Parent > Child` for a product's first category. */\nfunction categoryPath(\n categoryIds: readonly string[] | undefined,\n categories: ReadonlyMap<string, CommerceModel.ProductCategory>,\n): string | undefined {\n const first = (categoryIds ?? []).find((id) => categories.has(id))\n if (!first) return undefined\n const names: string[] = []\n const seen = new Set<string>()\n let at: string | null | undefined = first\n while (at && !seen.has(at) && categories.has(at) && names.length < 5) {\n seen.add(at)\n const category: CommerceModel.ProductCategory = categories.get(at)!\n if (category.name) names.unshift(category.name)\n at = category.parentId\n }\n return names.length ? names.join(' > ') : undefined\n}\n\n/** The specific countries the store's shipping zones name, in order, upper case. */\nfunction shippingCountries(settings: CommerceModel.ShippingSettings | undefined): string[] {\n const countries = new Set<string>()\n for (const zone of settings?.zones ?? []) {\n for (const code of zone.countries ?? []) {\n const country = String(code ?? '').trim().toUpperCase()\n if (/^[A-Z]{2}$/.test(country)) countries.add(country)\n }\n }\n return [...countries].slice(0, 100)\n}\n\n/** The countries the store's zones price by a live carrier quote, upper case. */\nexport function carrierPricedCountries(\n settings: CommerceModel.ShippingSettings | undefined,\n countries: readonly string[],\n): string[] {\n if (!CommerceModel.hasCarrierRates(settings)) return []\n return countries.filter((country) => CommerceModel.carrierRatesFor(settings, country).length > 0)\n}\n\n/** The packed size of one unit, when every side is known. */\nfunction dimensionsOf(product: CommerceModel.HostProduct): CatalogOffer['dimensionsCm'] {\n const facts = CommerceModel.normalizeProductShippingFacts(product.shipping)\n if (!facts?.lengthCm || !facts.widthCm || !facts.heightCm) return undefined\n return { length: facts.lengthCm, width: facts.widthCm, height: facts.heightCm }\n}\n\n/** What one unit of an offer costs to ship to each country, the cheapest rate each. */\nexport function offerShipping(\n settings: CommerceModel.ShippingSettings | undefined,\n countries: readonly string[],\n unit: { priceMinor: number; weightGrams?: number },\n): CatalogShippingPrice[] {\n const prices: CatalogShippingPrice[] = []\n for (const country of countries) {\n const [cheapest] = CommerceModel.resolveShippingRates(settings, country, {\n subtotalCents: unit.priceMinor,\n totalGrams: unit.weightGrams ?? 0,\n }).filter((rate) => rate.rateId !== CommerceModel.LOCAL_PICKUP_RATE_ID)\n if (cheapest) prices.push({ country, service: cheapest.name, priceMinor: cheapest.amountCents })\n }\n return prices\n}\n\ninterface PageContext {\n origin: string | null\n categories: ReadonlyMap<string, CommerceModel.ProductCategory>\n shipping: CommerceModel.ShippingSettings | undefined\n countries: readonly string[]\n}\n\n/** The offers of one stored product; empty for a product that is not listed. */\nexport function productOffers(productId: string, raw: DocumentData, context: PageContext): CatalogOffer[] {\n if (!raw || raw['deletedAt']) return []\n const product = CommerceModel.liftLegacyProduct(raw as CommerceModel.HostProduct)\n if (product.status !== 'active' || !str(product.name).trim() || !str(product.slug)) return []\n const variants = (product.variants ?? []).filter((variant) => variant && str(variant.id))\n if (!variants.length) return []\n const hasVariants = variants.length > 1\n const facts = CommerceModel.normalizeProductChannelFacts(product.channel)\n const media = [...(product.mediaUrls ?? []), ...(product.imageUrl ? [product.imageUrl] : [])]\n .map((url) => absoluteImage(url, context.origin))\n .filter((url): url is string => Boolean(url))\n const description = plainDescription(product.description)\n const productType = categoryPath(product.categoryIds, context.categories)\n const subscriptionOnly = Boolean(product.subscription) && product.subscriptionOptional !== true\n const updatedAtMs = Number(product.updatedAtMs ?? product.createdAtMs) || undefined\n const dimensionsCm = (product.type ?? 'physical') === 'physical' ? dimensionsOf(product) : undefined\n return variants.map((variant) => {\n const options = Object.fromEntries(\n Object.entries(variant.options ?? {}).filter(([, value]) => typeof value === 'string' && value),\n ) as Record<string, string>\n const choices = (product.options ?? [])\n .map((option) => options[option.name])\n .filter((value): value is string => Boolean(value))\n const ownPrice = toMinor(variant.priceUsd)\n const compareAt = toMinor(variant.compareAtPriceUsd)\n const onSale = compareAt > ownPrice && ownPrice > 0\n const priceMinor = onSale ? compareAt : ownPrice\n const tracked = CommerceModel.stockTrackingApplies(product) && variant.inventory != null\n const quantity = tracked ? Math.max(0, Math.trunc(Number(variant.inventory) || 0)) : null\n const availability: CatalogOffer['availability'] =\n quantity === null || quantity > 0\n ? 'in_stock'\n : product.oversellPolicy === 'backorder'\n ? 'backorder'\n : 'out_of_stock'\n const variantImage = absoluteImage(variant.imageUrl, context.origin)\n const imageUrl = variantImage ?? media[0]\n const weightGrams = Number(variant.weightGrams) > 0 ? Math.round(Number(variant.weightGrams)) : undefined\n const gtin = CommerceModel.normalizeGtin(variant.barcode) || (hasVariants ? '' : facts?.gtin ?? '')\n const offer: CatalogOffer = {\n id: offerId(productId, variant.id, hasVariants),\n groupId: productId,\n hasVariants,\n productId,\n variantId: variant.id,\n productName: product.name.trim(),\n title: choices.length ? `${product.name.trim()} - ${choices.join(' / ')}` : product.name.trim(),\n description,\n path: `/products/${product.slug}`,\n ...(imageUrl ? { imageUrl } : {}),\n additionalImageUrls: media.filter((url) => url !== imageUrl).slice(0, 10),\n priceMinor,\n ...(onSale ? { salePriceMinor: ownPrice } : {}),\n availability,\n quantity,\n kind: product.type ?? 'physical',\n subscriptionOnly,\n ...(facts?.condition ? { condition: facts.condition } : {}),\n ...(facts?.brand ? { brand: facts.brand } : {}),\n ...(gtin.length >= 8 ? { gtin } : {}),\n ...(facts?.mpn ? { mpn: facts.mpn } : {}),\n ...(facts?.googleProductCategory ? { googleProductCategory: facts.googleProductCategory } : {}),\n ...(productType ? { productType } : {}),\n options,\n ...(optionValue(options, COLOR_OPTION) ? { color: optionValue(options, COLOR_OPTION) } : {}),\n ...(optionValue(options, SIZE_OPTION) ? { size: optionValue(options, SIZE_OPTION) } : {}),\n ...(weightGrams ? { weightGrams } : {}),\n ...(dimensionsCm ? { dimensionsCm } : {}),\n shipping:\n (product.type ?? 'physical') === 'physical'\n ? offerShipping(context.shipping, context.countries, {\n priceMinor: onSale ? ownPrice : priceMinor,\n ...(weightGrams ? { weightGrams } : {}),\n })\n : [],\n ...(updatedAtMs ? { updatedAtMs } : {}),\n }\n return offer\n })\n}\n\nasync function readStore(hostId: string) {\n const hostRef = firestore().collection('hosts').doc(hostId)\n const [host, store] = await Promise.all([hostRef.get(), hostRef.collection('settings').doc('store').get()])\n return { host, store }\n}\n\nasync function readCategories(hostId: string): Promise<Map<string, CommerceModel.ProductCategory>> {\n const snapshot = await firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('productCategories')\n .limit(500)\n .get()\n return new Map(\n snapshot.docs.map((doc) => [doc.id, doc.data() as CommerceModel.ProductCategory] as const),\n )\n}\n\nconst isDocumentId = (value: string) =>\n Boolean(value) && value.length <= 200 && !value.includes('/') && !/^__.*__$/.test(value)\n\nexport const productCatalog: PluginProductCatalog = {\n async store(hostId: string): Promise<CatalogStore | null> {\n if (!isDocumentId(hostId)) return null\n const { host, store } = await readStore(hostId)\n if (!host.exists) return null\n const data = host.data() ?? {}\n const settings = store.data() ?? {}\n const origin = storeOrigin(data)\n const shipping = settings['shipping'] as CommerceModel.ShippingSettings | undefined\n const name =\n str(data['displayName']).trim() ||\n (origin ? origin.replace(/^https?:\\/\\//, '') : '') ||\n 'Store'\n return {\n hostId,\n name,\n origin,\n currency: str(settings['currency']).trim().toUpperCase() || 'USD',\n productPagesServed: Boolean(str(settings['pdpScreenId'])),\n carrierPricedCountries: carrierPricedCountries(shipping, shippingCountries(shipping)),\n }\n },\n\n async page({ hostId, cursor, limit }): Promise<CatalogPage> {\n if (!isDocumentId(hostId)) return { offers: [], nextCursor: null }\n const want = Math.max(1, Math.min(Math.trunc(limit) || 1, 1000))\n const [{ host, store }, categories] = await Promise.all([readStore(hostId), readCategories(hostId)])\n if (!host.exists) return { offers: [], nextCursor: null }\n const shipping = store.get('shipping') as CommerceModel.ShippingSettings | undefined\n const context: PageContext = {\n origin: storeOrigin(host.data()),\n categories,\n shipping,\n countries: shippingCountries(shipping),\n }\n const products = firestore().collection('hosts').doc(hostId).collection('products')\n const idPath = firebaseAdmin.firestore.FieldPath.documentId()\n const offers: CatalogOffer[] = []\n let after = cursor && isDocumentId(cursor) ? cursor : null\n // Reads products until the page holds `want` offers or the catalog ends.\n // Drafts and archived products are read and skipped, so a store of many\n // drafts may take more than one read for a page.\n for (;;) {\n let query = products.orderBy(idPath).limit(MAX_PRODUCTS_PER_READ)\n if (after) query = query.startAfter(after)\n const snapshot = await query.get()\n for (const doc of snapshot.docs) {\n offers.push(...productOffers(doc.id, doc.data(), context))\n after = doc.id\n if (offers.length >= want) {\n const last = snapshot.docs[snapshot.docs.length - 1]\n // The page is full: continue after this product, unless it was the\n // last of a short read, which is the end of the catalog.\n const ended = doc.id === last.id && snapshot.size < MAX_PRODUCTS_PER_READ\n return { offers, nextCursor: ended ? null : doc.id }\n }\n }\n if (snapshot.size < MAX_PRODUCTS_PER_READ) return { offers, nextCursor: null }\n }\n },\n}\n"],"names":["liveCustomDomain","TENANT_APEX","firebaseAdmin","CommerceModel","MAX_PRODUCTS_PER_READ","MAX_OFFER_ID","firestore","app","str","value","plainDescription","replace","trim","toMinor","amount","number","Number","isFinite","Math","round","storeOrigin","host","domain","subdomain","absoluteImage","url","origin","undefined","test","startsWith","offerId","productId","variantId","hasVariants","slice","full","length","hash","char","imul","charCodeAt","suffix","toString","COLOR_OPTION","SIZE_OPTION","optionValue","options","pattern","name","Object","entries","categoryPath","categoryIds","categories","first","find","id","has","names","seen","Set","at","add","category","get","unshift","parentId","join","shippingCountries","settings","countries","zone","zones","code","country","String","toUpperCase","carrierPricedCountries","hasCarrierRates","filter","carrierRatesFor","dimensionsOf","product","facts","normalizeProductShippingFacts","shipping","lengthCm","widthCm","heightCm","width","height","offerShipping","unit","prices","cheapest","resolveShippingRates","subtotalCents","priceMinor","totalGrams","weightGrams","rate","rateId","LOCAL_PICKUP_RATE_ID","push","service","amountCents","productOffers","raw","context","liftLegacyProduct","status","slug","variants","variant","normalizeProductChannelFacts","channel","media","mediaUrls","imageUrl","map","Boolean","description","productType","subscriptionOnly","subscription","subscriptionOptional","updatedAtMs","createdAtMs","dimensionsCm","type","fromEntries","choices","option","ownPrice","priceUsd","compareAt","compareAtPriceUsd","onSale","tracked","stockTrackingApplies","inventory","quantity","max","trunc","availability","oversellPolicy","variantImage","gtin","normalizeGtin","barcode","offer","groupId","productName","title","path","additionalImageUrls","salePriceMinor","kind","condition","brand","mpn","googleProductCategory","color","size","readStore","hostId","hostRef","collection","doc","store","Promise","all","readCategories","snapshot","limit","Map","docs","data","isDocumentId","includes","productCatalog","exists","currency","productPagesServed","page","cursor","offers","nextCursor","want","min","products","idPath","FieldPath","documentId","after","query","orderBy","startAfter","last","ended"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-catalog.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 {\n CatalogOffer,\n CatalogPage,\n CatalogShippingPrice,\n CatalogStore,\n PluginProductCatalog,\n} from '@aglyn/aglyn/plugin-manager/plugin-product-catalog'\nimport { liveCustomDomain } from '@aglyn/aglyn/app-utils/host-naming'\nimport { TENANT_APEX } from '@aglyn/aglyn/app-utils/tenant-apex'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin/server/firebase-admin'\nimport * as CommerceModel from '../model'\n\n/**\n * THE STORE'S CATALOG, AS OTHER PLUGINS READ IT (AGL-3637): core's\n * `core.product-catalog` contract, answered from this plugin's products.\n *\n * A shopping-channel feed, an ad catalog and a marketplace sync read every\n * offer the store sells through here and never through `hosts/{id}/products`.\n * What counts as listed, how a variant becomes an offer, what the store's\n * currency is and what shipping costs are this plugin's decisions, made once:\n *\n * - Listed is `status: 'active'` and not soft-deleted. Drafts, archived and\n * deleted products are never offered.\n * - A product with one configuration is one offer under the product's own\n * id — the id the site's analytics events already send — and a product\n * with several is one offer per variant, `{productId}_{variantId}`.\n * - A variant on sale (`compareAtPriceUsd` above `priceUsd`) offers the\n * compare-at price as its price and its own as the sale price.\n * - Shipping is priced per country the store's zones name, for one unit of\n * the offer, through the same resolver checkout charges with; local pickup\n * is not shipping and a `*` zone names no country.\n *\n * Walked in document-id order, so a page boundary is stable while products\n * are added and edited, and a product is never split across two pages.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\ntype DocumentData = FirebaseFirestore.DocumentData\n\nconst MAX_PRODUCTS_PER_READ = 200\n/** Google's ceiling; the longest any channel accepts for every channel's id. */\nconst MAX_OFFER_ID = 50\n\nconst firestore = (): Firestore => firebaseAdmin.app().firestore()\n\nconst str = (value: unknown): string => (typeof value === 'string' ? value : '')\n\n/** Plain text from a description that may hold markup. */\nexport function plainDescription(value: unknown): string {\n return str(value)\n .replace(/<\\s*(br|\\/p|\\/div|\\/li|\\/h[1-6])\\s*\\/?>/gi, '\\n')\n .replace(/<[^>]*>/g, ' ')\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/'/gi, \"'\")\n .replace(/[ \\t\\f\\v]+/g, ' ')\n .replace(/\\s*\\n\\s*/g, '\\n')\n .trim()\n}\n\nconst toMinor = (amount: unknown): number => {\n const number = Number(amount)\n return Number.isFinite(number) && number > 0 ? Math.round(number * 100) : 0\n}\n\n/** The origin the store's pages answer on: its live custom domain, else its subdomain. */\nexport function storeOrigin(host: DocumentData | undefined): string | null {\n const domain = liveCustomDomain(host as Parameters<typeof liveCustomDomain>[0])\n if (domain) return `https://${domain}`\n const subdomain = str(host?.['subdomain']).trim()\n return subdomain ? `https://${subdomain}.${TENANT_APEX}` : null\n}\n\n/** An absolute URL for a stored media reference, or undefined. */\nfunction absoluteImage(url: unknown, origin: string | null): string | undefined {\n const value = str(url).trim()\n if (!value) return undefined\n if (/^https?:\\/\\//i.test(value)) return value\n if (value.startsWith('//')) return `https:${value}`\n if (value.startsWith('/') && origin) return `${origin}${value}`\n return undefined\n}\n\n/** A stable offer id within the longest any channel accepts. */\nexport function offerId(productId: string, variantId: string, hasVariants: boolean): string {\n if (!hasVariants) return productId.slice(0, MAX_OFFER_ID)\n const full = `${productId}_${variantId}`\n if (full.length <= MAX_OFFER_ID) return full\n // Deterministic, so the id is the same on every read.\n let hash = 0\n for (const char of full) hash = (Math.imul(hash, 31) + char.charCodeAt(0)) | 0\n const suffix = (hash >>> 0).toString(36)\n return `${productId.slice(0, MAX_OFFER_ID - suffix.length - 1)}_${suffix}`\n}\n\nconst COLOR_OPTION = /^(colou?r|shade|finish)$/i\nconst SIZE_OPTION = /^(size|fit)$/i\n\nfunction optionValue(options: Record<string, string>, pattern: RegExp): string | undefined {\n for (const [name, value] of Object.entries(options)) {\n if (pattern.test(name.trim()) && value) return value\n }\n return undefined\n}\n\n/** The category path `Parent > Child` for a product's first category. */\nfunction categoryPath(\n categoryIds: readonly string[] | undefined,\n categories: ReadonlyMap<string, CommerceModel.ProductCategory>,\n): string | undefined {\n const first = (categoryIds ?? []).find((id) => categories.has(id))\n if (!first) return undefined\n const names: string[] = []\n const seen = new Set<string>()\n let at: string | null | undefined = first\n while (at && !seen.has(at) && categories.has(at) && names.length < 5) {\n seen.add(at)\n const category: CommerceModel.ProductCategory = categories.get(at)!\n if (category.name) names.unshift(category.name)\n at = category.parentId\n }\n return names.length ? names.join(' > ') : undefined\n}\n\n/** The specific countries the store's shipping zones name, in order, upper case. */\nfunction shippingCountries(settings: CommerceModel.ShippingSettings | undefined): string[] {\n const countries = new Set<string>()\n for (const zone of settings?.zones ?? []) {\n for (const code of zone.countries ?? []) {\n const country = String(code ?? '').trim().toUpperCase()\n if (/^[A-Z]{2}$/.test(country)) countries.add(country)\n }\n }\n return [...countries].slice(0, 100)\n}\n\n/** The countries the store's zones price by a live carrier quote, upper case. */\nexport function carrierPricedCountries(\n settings: CommerceModel.ShippingSettings | undefined,\n countries: readonly string[],\n): string[] {\n if (!CommerceModel.hasCarrierRates(settings)) return []\n return countries.filter((country) => CommerceModel.carrierRatesFor(settings, country).length > 0)\n}\n\n/** The packed size of one unit, when every side is known. */\nfunction dimensionsOf(product: CommerceModel.HostProduct): CatalogOffer['dimensionsCm'] {\n const facts = CommerceModel.normalizeProductShippingFacts(product.shipping)\n if (!facts?.lengthCm || !facts.widthCm || !facts.heightCm) return undefined\n return { length: facts.lengthCm, width: facts.widthCm, height: facts.heightCm }\n}\n\n/** What one unit of an offer costs to ship to each country, the cheapest rate each. */\nexport function offerShipping(\n settings: CommerceModel.ShippingSettings | undefined,\n countries: readonly string[],\n unit: { priceMinor: number; weightGrams?: number },\n): CatalogShippingPrice[] {\n const prices: CatalogShippingPrice[] = []\n for (const country of countries) {\n const [cheapest] = CommerceModel.resolveShippingRates(settings, country, {\n subtotalCents: unit.priceMinor,\n totalGrams: unit.weightGrams ?? 0,\n }).filter((rate) => rate.rateId !== CommerceModel.LOCAL_PICKUP_RATE_ID)\n if (cheapest) prices.push({ country, service: cheapest.name, priceMinor: cheapest.amountCents })\n }\n return prices\n}\n\ninterface PageContext {\n origin: string | null\n categories: ReadonlyMap<string, CommerceModel.ProductCategory>\n shipping: CommerceModel.ShippingSettings | undefined\n countries: readonly string[]\n}\n\n/** The offers of one stored product; empty for a product that is not listed. */\nexport function productOffers(productId: string, raw: DocumentData, context: PageContext): CatalogOffer[] {\n if (!raw || raw['deletedAt']) return []\n const product = CommerceModel.liftLegacyProduct(raw as CommerceModel.HostProduct)\n if (product.status !== 'active' || !str(product.name).trim() || !str(product.slug)) return []\n const variants = (product.variants ?? []).filter((variant) => variant && str(variant.id))\n if (!variants.length) return []\n const hasVariants = variants.length > 1\n const facts = CommerceModel.normalizeProductChannelFacts(product.channel)\n const media = [...(product.mediaUrls ?? []), ...(product.imageUrl ? [product.imageUrl] : [])]\n .map((url) => absoluteImage(url, context.origin))\n .filter((url): url is string => Boolean(url))\n const description = plainDescription(product.description)\n const productType = categoryPath(product.categoryIds, context.categories)\n const subscriptionOnly = Boolean(product.subscription) && product.subscriptionOptional !== true\n const updatedAtMs = Number(product.updatedAtMs ?? product.createdAtMs) || undefined\n const dimensionsCm = (product.type ?? 'physical') === 'physical' ? dimensionsOf(product) : undefined\n return variants.map((variant) => {\n const options = Object.fromEntries(\n Object.entries(variant.options ?? {}).filter(([, value]) => typeof value === 'string' && value),\n ) as Record<string, string>\n const choices = (product.options ?? [])\n .map((option) => options[option.name])\n .filter((value): value is string => Boolean(value))\n const ownPrice = toMinor(variant.priceUsd)\n const compareAt = toMinor(variant.compareAtPriceUsd)\n const onSale = compareAt > ownPrice && ownPrice > 0\n const priceMinor = onSale ? compareAt : ownPrice\n const tracked = CommerceModel.stockTrackingApplies(product) && variant.inventory != null\n const quantity = tracked ? Math.max(0, Math.trunc(Number(variant.inventory) || 0)) : null\n const availability: CatalogOffer['availability'] =\n quantity === null || quantity > 0\n ? 'in_stock'\n : product.oversellPolicy === 'backorder'\n ? 'backorder'\n : 'out_of_stock'\n const variantImage = absoluteImage(variant.imageUrl, context.origin)\n const imageUrl = variantImage ?? media[0]\n const weightGrams = Number(variant.weightGrams) > 0 ? Math.round(Number(variant.weightGrams)) : undefined\n const gtin = CommerceModel.normalizeGtin(variant.barcode) || (hasVariants ? '' : facts?.gtin ?? '')\n const sku = str(variant.sku).trim().slice(0, 120)\n const offer: CatalogOffer = {\n id: offerId(productId, variant.id, hasVariants),\n groupId: productId,\n hasVariants,\n productId,\n variantId: variant.id,\n ...(sku ? { sku } : {}),\n productName: product.name.trim(),\n title: choices.length ? `${product.name.trim()} - ${choices.join(' / ')}` : product.name.trim(),\n description,\n path: `/products/${product.slug}`,\n ...(imageUrl ? { imageUrl } : {}),\n additionalImageUrls: media.filter((url) => url !== imageUrl).slice(0, 10),\n priceMinor,\n ...(onSale ? { salePriceMinor: ownPrice } : {}),\n availability,\n quantity,\n kind: product.type ?? 'physical',\n subscriptionOnly,\n ...(facts?.condition ? { condition: facts.condition } : {}),\n ...(facts?.brand ? { brand: facts.brand } : {}),\n ...(gtin.length >= 8 ? { gtin } : {}),\n ...(facts?.mpn ? { mpn: facts.mpn } : {}),\n ...(facts?.googleProductCategory ? { googleProductCategory: facts.googleProductCategory } : {}),\n ...(productType ? { productType } : {}),\n options,\n ...(optionValue(options, COLOR_OPTION) ? { color: optionValue(options, COLOR_OPTION) } : {}),\n ...(optionValue(options, SIZE_OPTION) ? { size: optionValue(options, SIZE_OPTION) } : {}),\n ...(weightGrams ? { weightGrams } : {}),\n ...(dimensionsCm ? { dimensionsCm } : {}),\n shipping:\n (product.type ?? 'physical') === 'physical'\n ? offerShipping(context.shipping, context.countries, {\n priceMinor: onSale ? ownPrice : priceMinor,\n ...(weightGrams ? { weightGrams } : {}),\n })\n : [],\n ...(updatedAtMs ? { updatedAtMs } : {}),\n }\n return offer\n })\n}\n\nasync function readStore(hostId: string) {\n const hostRef = firestore().collection('hosts').doc(hostId)\n const [host, store] = await Promise.all([hostRef.get(), hostRef.collection('settings').doc('store').get()])\n return { host, store }\n}\n\nasync function readCategories(hostId: string): Promise<Map<string, CommerceModel.ProductCategory>> {\n const snapshot = await firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('productCategories')\n .limit(500)\n .get()\n return new Map(\n snapshot.docs.map((doc) => [doc.id, doc.data() as CommerceModel.ProductCategory] as const),\n )\n}\n\nconst isDocumentId = (value: string) =>\n Boolean(value) && value.length <= 200 && !value.includes('/') && !/^__.*__$/.test(value)\n\nexport const productCatalog: PluginProductCatalog = {\n async store(hostId: string): Promise<CatalogStore | null> {\n if (!isDocumentId(hostId)) return null\n const { host, store } = await readStore(hostId)\n if (!host.exists) return null\n const data = host.data() ?? {}\n const settings = store.data() ?? {}\n const origin = storeOrigin(data)\n const shipping = settings['shipping'] as CommerceModel.ShippingSettings | undefined\n const name =\n str(data['displayName']).trim() ||\n (origin ? origin.replace(/^https?:\\/\\//, '') : '') ||\n 'Store'\n return {\n hostId,\n name,\n origin,\n currency: str(settings['currency']).trim().toUpperCase() || 'USD',\n productPagesServed: Boolean(str(settings['pdpScreenId'])),\n carrierPricedCountries: carrierPricedCountries(shipping, shippingCountries(shipping)),\n }\n },\n\n async page({ hostId, cursor, limit }): Promise<CatalogPage> {\n if (!isDocumentId(hostId)) return { offers: [], nextCursor: null }\n const want = Math.max(1, Math.min(Math.trunc(limit) || 1, 1000))\n const [{ host, store }, categories] = await Promise.all([readStore(hostId), readCategories(hostId)])\n if (!host.exists) return { offers: [], nextCursor: null }\n const shipping = store.get('shipping') as CommerceModel.ShippingSettings | undefined\n const context: PageContext = {\n origin: storeOrigin(host.data()),\n categories,\n shipping,\n countries: shippingCountries(shipping),\n }\n const products = firestore().collection('hosts').doc(hostId).collection('products')\n const idPath = firebaseAdmin.firestore.FieldPath.documentId()\n const offers: CatalogOffer[] = []\n let after = cursor && isDocumentId(cursor) ? cursor : null\n // Reads products until the page holds `want` offers or the catalog ends.\n // Drafts and archived products are read and skipped, so a store of many\n // drafts may take more than one read for a page.\n for (;;) {\n let query = products.orderBy(idPath).limit(MAX_PRODUCTS_PER_READ)\n if (after) query = query.startAfter(after)\n const snapshot = await query.get()\n for (const doc of snapshot.docs) {\n offers.push(...productOffers(doc.id, doc.data(), context))\n after = doc.id\n if (offers.length >= want) {\n const last = snapshot.docs[snapshot.docs.length - 1]\n // The page is full: continue after this product, unless it was the\n // last of a short read, which is the end of the catalog.\n const ended = doc.id === last.id && snapshot.size < MAX_PRODUCTS_PER_READ\n return { offers, nextCursor: ended ? null : doc.id }\n }\n }\n if (snapshot.size < MAX_PRODUCTS_PER_READ) return { offers, nextCursor: null }\n }\n },\n}\n"],"names":["liveCustomDomain","TENANT_APEX","firebaseAdmin","CommerceModel","MAX_PRODUCTS_PER_READ","MAX_OFFER_ID","firestore","app","str","value","plainDescription","replace","trim","toMinor","amount","number","Number","isFinite","Math","round","storeOrigin","host","domain","subdomain","absoluteImage","url","origin","undefined","test","startsWith","offerId","productId","variantId","hasVariants","slice","full","length","hash","char","imul","charCodeAt","suffix","toString","COLOR_OPTION","SIZE_OPTION","optionValue","options","pattern","name","Object","entries","categoryPath","categoryIds","categories","first","find","id","has","names","seen","Set","at","add","category","get","unshift","parentId","join","shippingCountries","settings","countries","zone","zones","code","country","String","toUpperCase","carrierPricedCountries","hasCarrierRates","filter","carrierRatesFor","dimensionsOf","product","facts","normalizeProductShippingFacts","shipping","lengthCm","widthCm","heightCm","width","height","offerShipping","unit","prices","cheapest","resolveShippingRates","subtotalCents","priceMinor","totalGrams","weightGrams","rate","rateId","LOCAL_PICKUP_RATE_ID","push","service","amountCents","productOffers","raw","context","liftLegacyProduct","status","slug","variants","variant","normalizeProductChannelFacts","channel","media","mediaUrls","imageUrl","map","Boolean","description","productType","subscriptionOnly","subscription","subscriptionOptional","updatedAtMs","createdAtMs","dimensionsCm","type","fromEntries","choices","option","ownPrice","priceUsd","compareAt","compareAtPriceUsd","onSale","tracked","stockTrackingApplies","inventory","quantity","max","trunc","availability","oversellPolicy","variantImage","gtin","normalizeGtin","barcode","sku","offer","groupId","productName","title","path","additionalImageUrls","salePriceMinor","kind","condition","brand","mpn","googleProductCategory","color","size","readStore","hostId","hostRef","collection","doc","store","Promise","all","readCategories","snapshot","limit","Map","docs","data","isDocumentId","includes","productCatalog","exists","currency","productPagesServed","page","cursor","offers","nextCursor","want","min","products","idPath","FieldPath","documentId","after","query","orderBy","startAfter","last","ended"],"mappings":"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"}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type PluginDraftCheck, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
|
|
17
|
+
import { type PluginDraftCheck, type PluginDraftContext, type PluginDraftRefusal, type PluginResourceDraftWriter } from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts';
|
|
18
18
|
import { type HostProduct, type ProductType, type ProposedProduct, type UnpricedProductDraft } from '../model/commerce';
|
|
19
19
|
/**
|
|
20
20
|
* A PRODUCT ANOTHER PLUGIN ASKS FOR (AGL-3616).
|
|
@@ -105,6 +105,10 @@ export declare function readProductDraftContent(content: Readonly<Record<string,
|
|
|
105
105
|
export declare function productDraftDocument(read: ProductDraftRead, slug: string, nowMs: number): UnpricedProductDraft | HostProduct;
|
|
106
106
|
/** Whether content is a product this plugin would store, with what it says about it. Pure. */
|
|
107
107
|
export declare function checkProductDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck;
|
|
108
|
+
export declare function roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null;
|
|
109
|
+
export declare function planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null;
|
|
110
|
+
/** The allowance, counted as the resources route and the importer count a product create. */
|
|
111
|
+
export declare function roomRefusal(org: PluginDraftContext['org'], used: number): PluginDraftRefusal | null;
|
|
108
112
|
export interface ProductDraftWriterDeps {
|
|
109
113
|
/** The Admin SDK handle; specs hand in a double. */
|
|
110
114
|
firestore?: () => Firestore;
|
|
@@ -219,7 +219,7 @@ function recordOf(product) {
|
|
|
219
219
|
facts: factsOf(data)
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
|
-
function roleRefusal(host, uid) {
|
|
222
|
+
export function roleRefusal(host, uid) {
|
|
223
223
|
var _host_get;
|
|
224
224
|
const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[uid];
|
|
225
225
|
return hostRoleCanWrite(role) ? null : {
|
|
@@ -227,13 +227,13 @@ function roleRefusal(host, uid) {
|
|
|
227
227
|
error: PRODUCT_DRAFT_ROLE_REFUSAL
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
|
-
function planRefusal(org) {
|
|
230
|
+
export function planRefusal(org) {
|
|
231
231
|
return checkEntitlement(org, 'commerce') ? null : {
|
|
232
232
|
status: 403,
|
|
233
233
|
error: PRODUCT_DRAFT_PLAN_REFUSAL
|
|
234
234
|
};
|
|
235
235
|
}
|
|
236
|
-
/** The allowance, counted as the resources route and the importer count a product create. */ function roomRefusal(org, used) {
|
|
236
|
+
/** The allowance, counted as the resources route and the importer count a product create. */ export function roomRefusal(org, used) {
|
|
237
237
|
const quota = checkQuota(org, 'productsPerHost', used);
|
|
238
238
|
return quota.allowed ? null : {
|
|
239
239
|
status: 403,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-drafts.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 { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftContext,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n COMMERCE_MAX_OPTION_VALUES,\n COMMERCE_MAX_OPTIONS,\n COMMERCE_MAX_PRICE_USD,\n COMMERCE_MAX_VARIANTS,\n commerceSlug,\n type HostProduct,\n productCollectionIds,\n productPriceMissing,\n productSearchFields,\n type ProductType,\n type ProposedProduct,\n type SmartCollectionRules,\n unpricedProductDraft,\n type UnpricedProductDraft,\n validateProduct,\n} from '../model/commerce'\n\n/**\n * A PRODUCT ANOTHER PLUGIN ASKS FOR (AGL-3616).\n *\n * The draft writer this plugin registers for the `product` resource on the\n * core's resource-drafts seam. A plugin that sets a store up from a brief\n * asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is the one the products card makes from an accepted AI\n * proposal (`unpricedProductDraft`): a `draft` — off the storefront, and\n * refused by every sale door — with one variant per combination of its\n * options, no photo, and the search, stock and smart-collection keys every\n * product write derives. Born live (`deletedAt: null`), as the product\n * resource stamps it, so the products table lists it.\n * - THE PRICE is left empty unless the content states one. An unpriced\n * product is marked for the merchant to price: the editor will not save it\n * and the card will not activate it until every variant has a price.\n * - THE ROOM is the `productsPerHost` allowance, counted the way the\n * resources route and the importer count it, inside the transaction that\n * creates. THE PLAN is the `commerce` feature, and THE ROLE a member who\n * may write the site's content — refused in the resources route's words.\n * - THE CHECK is `validateProduct`, the rule every product write is held to,\n * with a placeholder price standing in where none was stated.\n *\n * Nothing is published, priced or sold. The writer touches the one new\n * product document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const PRODUCT_DRAFT_RESOURCE = 'product'\n\n/** The longest product name the proposals and the editor keep. */\nexport const PRODUCT_DRAFT_NAME_MAX = 120\n/** The longest description a draft keeps. */\nexport const PRODUCT_DRAFT_DESCRIPTION_MAX = 5_000\n/** The most tags a draft keeps, and the longest one. */\nexport const PRODUCT_DRAFT_TAGS_MAX = 20\nexport const PRODUCT_DRAFT_TAG_MAX = 40\n/** The most categories a draft is filed under. */\nexport const PRODUCT_DRAFT_CATEGORIES_MAX = 10\n/** The longest search-listing title and description a draft keeps. */\nexport const PRODUCT_DRAFT_SEO_TITLE_MAX = 120\nexport const PRODUCT_DRAFT_SEO_DESCRIPTION_MAX = 320\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const PRODUCT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n/** The resources route's refusal for a plan without the feature. */\nexport const PRODUCT_DRAFT_PLAN_REFUSAL = 'This feature is not included in your plan — see Billing'\n\n/** The resources route's refusal at the plan's allowance. */\nexport function productDraftLimitRefusal(limit: number): string {\n return `Your plan includes ${limit} ${limit === 1 ? 'product' : 'products'} — ` +\n 'upgrade in Billing for more'\n}\n\n/**\n * What a caller sends as `content`: a proposed product, as the AI products\n * job proposes one, with an optional stated price for every variant and the\n * ids of categories the site already has. A photo is never taken: the\n * merchant adds pictures in the editor.\n */\nexport interface ProductDraftContent {\n name: string\n type?: ProductType\n description?: string\n tags?: string[]\n options?: Array<{ name: string; values: string[] }>\n seoTitle?: string\n seoDescription?: string\n /** One price for every variant, in dollars; absent leaves them unpriced. */\n priceUsd?: number\n /** Ids of the site's product categories; one the site lacks is dropped. */\n categoryIds?: string[]\n}\n\nconst PRODUCT_TYPES: readonly ProductType[] = ['physical', 'digital', 'service']\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\nfunction strings(label: string, value: unknown, problems: string[]): string[] {\n if (value === undefined) return []\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {\n problems.push(`${label} are a list of words`)\n return []\n }\n return [...new Set(value.map(line).filter(Boolean))]\n}\n\n/** A draft as the writer will store it, before its slug and keys are settled. */\nexport interface ProductDraftRead {\n proposal: ProposedProduct\n /** The price stated for every variant, or `null` for the merchant to set. */\n priceUsd: number | null\n categoryIds: string[]\n}\n\nexport type ProductDraftContentRead =\n | { ok: true; value: ProductDraftRead }\n | { ok: false; problems: string[] }\n\n/** The content as the writer reads it, or every problem that stops it. */\nexport function readProductDraftContent(\n content: Readonly<Record<string, unknown>>,\n): ProductDraftContentRead {\n const problems: string[] = []\n const name = line(content['name'])\n if (!name) problems.push('The product needs a name')\n else if (name.length > PRODUCT_DRAFT_NAME_MAX) {\n problems.push(`The name is longer than ${PRODUCT_DRAFT_NAME_MAX} characters`)\n }\n const rawType = content['type'] ?? 'physical'\n if (!(PRODUCT_TYPES as readonly unknown[]).includes(rawType)) {\n problems.push('The type is physical, digital or service')\n }\n const rawDescription = content['description']\n const description = typeof rawDescription === 'string' ? rawDescription.trim() : ''\n if (rawDescription !== undefined && typeof rawDescription !== 'string') {\n problems.push('The description is text')\n } else if (description.length > PRODUCT_DRAFT_DESCRIPTION_MAX) {\n problems.push(`The description is longer than ${PRODUCT_DRAFT_DESCRIPTION_MAX} characters`)\n }\n const tags = strings('Tags', content['tags'], problems)\n if (tags.length > PRODUCT_DRAFT_TAGS_MAX) problems.push(`A product keeps at most ${PRODUCT_DRAFT_TAGS_MAX} tags`)\n if (tags.some((tag) => tag.length > PRODUCT_DRAFT_TAG_MAX)) {\n problems.push(`A tag is longer than ${PRODUCT_DRAFT_TAG_MAX} characters`)\n }\n const categoryIds = strings('Categories', content['categoryIds'], problems)\n if (categoryIds.length > PRODUCT_DRAFT_CATEGORIES_MAX) {\n problems.push(`A product is filed under at most ${PRODUCT_DRAFT_CATEGORIES_MAX} categories`)\n }\n const seoTitle = line(content['seoTitle'])\n const seoDescription = line(content['seoDescription'])\n if (seoTitle.length > PRODUCT_DRAFT_SEO_TITLE_MAX) {\n problems.push(`The search title is longer than ${PRODUCT_DRAFT_SEO_TITLE_MAX} characters`)\n }\n if (seoDescription.length > PRODUCT_DRAFT_SEO_DESCRIPTION_MAX) {\n problems.push(`The search description is longer than ${PRODUCT_DRAFT_SEO_DESCRIPTION_MAX} characters`)\n }\n\n // Options are held here before the draft expands them: a matrix past the\n // variant ceiling is refused, never built.\n const rawOptions = content['options'] ?? []\n const options: ProposedProduct['options'] = []\n if (!Array.isArray(rawOptions)) {\n problems.push('Options are a list of a name and its values')\n } else {\n if (rawOptions.length > COMMERCE_MAX_OPTIONS) {\n problems.push(`At most ${COMMERCE_MAX_OPTIONS} options per product`)\n }\n for (const option of rawOptions) {\n const optionName = isRecord(option) ? line(option['name']) : ''\n const values = isRecord(option) ? strings(`The values of \"${optionName}\"`, option['values'], problems) : []\n if (!optionName || !values.length) {\n problems.push('Each option needs a name and at least one value')\n continue\n }\n if (values.length > COMMERCE_MAX_OPTION_VALUES) {\n problems.push(`Option \"${optionName}\" has too many values`)\n continue\n }\n options.push({ name: optionName, values })\n }\n if (new Set(options.map((option) => option.name)).size !== options.length) {\n problems.push('Each option needs its own name')\n }\n const combinations = options.reduce((count, option) => count * option.values.length, 1)\n if (combinations > COMMERCE_MAX_VARIANTS) {\n problems.push(`Those options make ${combinations} variants; a product holds at most ${COMMERCE_MAX_VARIANTS}`)\n }\n }\n\n // No price unless one is stated: the merchant sets it otherwise.\n const rawPrice = content['priceUsd']\n let priceUsd: number | null = null\n if (rawPrice !== undefined && rawPrice !== null) {\n if (\n typeof rawPrice !== 'number' ||\n !Number.isFinite(rawPrice) ||\n rawPrice < 0 ||\n rawPrice > COMMERCE_MAX_PRICE_USD ||\n Math.round(rawPrice * 100) !== rawPrice * 100\n ) {\n problems.push(`A stated price is dollars and cents from 0 to ${COMMERCE_MAX_PRICE_USD}`)\n } else {\n priceUsd = rawPrice\n }\n }\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n value: {\n proposal: {\n name,\n type: rawType as ProductType,\n description,\n tags,\n options,\n seoTitle,\n seoDescription,\n },\n priceUsd,\n categoryIds,\n },\n }\n}\n\n/** The product document a read builds, under `slug`, at `nowMs`. */\nexport function productDraftDocument(\n read: ProductDraftRead,\n slug: string,\n nowMs: number,\n): UnpricedProductDraft | HostProduct {\n const draft = unpricedProductDraft(read.proposal, new Set(), nowMs)\n const priced =\n read.priceUsd === null\n ? draft\n : ({\n ...draft,\n variants: draft.variants.map((variant) => ({ ...variant, priceUsd: read.priceUsd as number })),\n } as HostProduct)\n return {\n ...priced,\n // The search keys travel with every create; recomputed for the price.\n ...productSearchFields(priced as HostProduct),\n slug,\n ...(read.categoryIds.length ? { categoryIds: read.categoryIds } : {}),\n }\n}\n\n/** What the owner's product rule says about a draft, a placeholder price standing in for none. */\nfunction productProblem(product: UnpricedProductDraft | HostProduct): string | null {\n return validateProduct({\n ...product,\n variants: product.variants.map((variant) => ({\n ...variant,\n priceUsd: (variant as { priceUsd?: number }).priceUsd ?? 0,\n })),\n } as HostProduct)\n}\n\nfunction factsOf(product: Partial<HostProduct>) {\n return {\n status: product.status ?? 'draft',\n slug: product.slug ?? '',\n type: product.type ?? 'physical',\n variants: product.variants?.length ?? 0,\n priceMissing: productPriceMissing({ variants: product.variants ?? [] }),\n }\n}\n\n/** Whether content is a product this plugin would store, with what it says about it. Pure. */\nexport function checkProductDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readProductDraftContent(content)\n if (read.ok === false) return read\n const slug = commerceSlug(read.value.proposal.name) || 'product'\n const product = productDraftDocument(read.value, slug, 0)\n const problem = productProblem(product)\n return problem ? { ok: false, problems: [problem] } : { ok: true, facts: factsOf(product as HostProduct) }\n}\n\nfunction recordOf(product: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (product.data() ?? {}) as Partial<HostProduct>\n return { id: product.id, name: String(data.name ?? ''), versionId: null, facts: factsOf(data) }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: PRODUCT_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'commerce') ? null : { status: 403, error: PRODUCT_DRAFT_PLAN_REFUSAL }\n}\n\n/** The allowance, counted as the resources route and the importer count a product create. */\nfunction roomRefusal(org: PluginDraftContext['org'], used: number): PluginDraftRefusal | null {\n const quota = checkQuota(org as never, 'productsPerHost', used)\n return quota.allowed ? null : { status: 403, error: productDraftLimitRefusal(quota.limit) }\n}\n\n/** Candidates asked of the store at once, as the console's slug ledger probes. */\nconst SLUG_PROBE = 10\n\n/**\n * The first of `base`, `base-2`, `base-3`, … that no product holds, live or\n * deleted — a soft-deleted product still answers its slug on the storefront\n * (`productSlugLedger`, AGL-3321). Asked inside the transaction.\n */\nasync function freeSlug(\n products: FirebaseFirestore.CollectionReference,\n base: string,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<string> {\n const candidate = (suffix: number) => (suffix < 2 ? base : `${base}-${suffix}`)\n for (let first = 1; ; first += SLUG_PROBE) {\n const batch = Array.from({ length: SLUG_PROBE }, (_, offset) => candidate(first + offset))\n const held = new Set(\n (await read(products.where('slug', 'in', batch))).docs.map((doc) => String(doc.get('slug') ?? '')),\n )\n const free = batch.find((slug) => !held.has(slug))\n if (free) return free\n }\n}\n\nexport interface ProductDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createProductDraftWriter(deps: ProductDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n const early = roleRefusal(host, context.uid) ?? planRefusal(context.org)\n if (early) return early\n const used = (await hostRef.collection('products').count().get()).data().count\n return roomRefusal(context.org, Number(used) || 0)\n },\n\n check: (content) => checkProductDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const product = await firestore().collection('hosts').doc(hostId).collection('products').doc(id).get()\n return product.exists ? recordOf(product) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = line(request.name)\n const read = readProductDraftContent(asked ? { ...request.content, name: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const products = hostRef.collection('products')\n const productRef = products.doc(request.id)\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(productRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n // Asked again under the same id: the draft it already wrote.\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n const used = (await tx.get(products.count())).data().count\n const room = roomRefusal(request.org, Number(used) || 0)\n if (room) return { ok: false, ...room }\n const base = commerceSlug(read.value.proposal.name) || 'product'\n const [slug, categories, smart] = await Promise.all([\n freeSlug(products, base, (query) => tx.get(query)),\n // A category the site does not have is dropped, not stored.\n read.value.categoryIds.length\n ? Promise.all(read.value.categoryIds.map((id) => tx.get(hostRef.collection('productCategories').doc(id))))\n : Promise.resolve([]),\n tx.get(hostRef.collection('collections').where('kind', '==', 'catalog').where('mode', '==', 'smart')),\n ])\n const categoryIds = categories.filter((category) => category.exists).map((category) => category.id)\n const nowMs = request.now.getTime()\n const product = productDraftDocument({ ...read.value, categoryIds }, slug, nowMs)\n const problem = productProblem(product)\n if (problem) return { ok: false, status: 400, error: problem }\n const rules: SmartCollectionRules[] = smart.docs.map((doc) => ({\n id: doc.id,\n rules: doc.get('rules') ?? [],\n matchAll: doc.get('matchAll'),\n }))\n const priced = read.value.priceUsd !== null\n tx.create(productRef, {\n ...product,\n collectionIds: productCollectionIds(product as HostProduct, rules),\n // The legacy flat price every create path still writes, only where\n // there is one: an unpriced draft is not a free one.\n ...(priced ? { priceUsd: read.value.priceUsd } : {}),\n deletedAt: null,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: product.name,\n versionId: null,\n facts: factsOf(product as HostProduct),\n }\n })\n },\n }\n}\n\nexport const productDraftWriter = createProductDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerProductDraftWriter(): void {\n registerPluginResourceDraftWriter(PRODUCT_DRAFT_RESOURCE, productDraftWriter, { pluginId: BUNDLE_ID })\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","checkQuota","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","COMMERCE_MAX_OPTION_VALUES","COMMERCE_MAX_OPTIONS","COMMERCE_MAX_PRICE_USD","COMMERCE_MAX_VARIANTS","commerceSlug","productCollectionIds","productPriceMissing","productSearchFields","unpricedProductDraft","validateProduct","PRODUCT_DRAFT_RESOURCE","PRODUCT_DRAFT_NAME_MAX","PRODUCT_DRAFT_DESCRIPTION_MAX","PRODUCT_DRAFT_TAGS_MAX","PRODUCT_DRAFT_TAG_MAX","PRODUCT_DRAFT_CATEGORIES_MAX","PRODUCT_DRAFT_SEO_TITLE_MAX","PRODUCT_DRAFT_SEO_DESCRIPTION_MAX","PRODUCT_DRAFT_ROLE_REFUSAL","PRODUCT_DRAFT_PLAN_REFUSAL","productDraftLimitRefusal","limit","PRODUCT_TYPES","isRecord","value","Array","isArray","line","replace","trim","strings","label","problems","undefined","some","entry","push","Set","map","filter","Boolean","readProductDraftContent","content","name","length","rawType","includes","rawDescription","description","tags","tag","categoryIds","seoTitle","seoDescription","rawOptions","options","option","optionName","values","size","combinations","reduce","count","rawPrice","priceUsd","Number","isFinite","Math","round","ok","proposal","type","productDraftDocument","read","slug","nowMs","draft","priced","variants","variant","productProblem","product","factsOf","status","priceMissing","checkProductDraftContent","problem","facts","recordOf","data","id","String","versionId","roleRefusal","host","uid","role","get","error","planRefusal","org","roomRefusal","used","quota","allowed","SLUG_PROBE","freeSlug","products","base","candidate","suffix","first","batch","from","_","offset","held","where","docs","doc","free","find","has","createProductDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","hostId","exists","early","check","write","request","asked","db","productRef","runTransaction","tx","existing","Promise","all","replayed","room","categories","smart","query","resolve","category","now","getTime","rules","matchAll","create","collectionIds","deletedAt","createdAt","updatedAt","createdBy","productDraftWriter","registerProductDraftWriter","pluginId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-drafts.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 { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftContext,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n COMMERCE_MAX_OPTION_VALUES,\n COMMERCE_MAX_OPTIONS,\n COMMERCE_MAX_PRICE_USD,\n COMMERCE_MAX_VARIANTS,\n commerceSlug,\n type HostProduct,\n productCollectionIds,\n productPriceMissing,\n productSearchFields,\n type ProductType,\n type ProposedProduct,\n type SmartCollectionRules,\n unpricedProductDraft,\n type UnpricedProductDraft,\n validateProduct,\n} from '../model/commerce'\n\n/**\n * A PRODUCT ANOTHER PLUGIN ASKS FOR (AGL-3616).\n *\n * The draft writer this plugin registers for the `product` resource on the\n * core's resource-drafts seam. A plugin that sets a store up from a brief\n * asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is the one the products card makes from an accepted AI\n * proposal (`unpricedProductDraft`): a `draft` — off the storefront, and\n * refused by every sale door — with one variant per combination of its\n * options, no photo, and the search, stock and smart-collection keys every\n * product write derives. Born live (`deletedAt: null`), as the product\n * resource stamps it, so the products table lists it.\n * - THE PRICE is left empty unless the content states one. An unpriced\n * product is marked for the merchant to price: the editor will not save it\n * and the card will not activate it until every variant has a price.\n * - THE ROOM is the `productsPerHost` allowance, counted the way the\n * resources route and the importer count it, inside the transaction that\n * creates. THE PLAN is the `commerce` feature, and THE ROLE a member who\n * may write the site's content — refused in the resources route's words.\n * - THE CHECK is `validateProduct`, the rule every product write is held to,\n * with a placeholder price standing in where none was stated.\n *\n * Nothing is published, priced or sold. The writer touches the one new\n * product document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const PRODUCT_DRAFT_RESOURCE = 'product'\n\n/** The longest product name the proposals and the editor keep. */\nexport const PRODUCT_DRAFT_NAME_MAX = 120\n/** The longest description a draft keeps. */\nexport const PRODUCT_DRAFT_DESCRIPTION_MAX = 5_000\n/** The most tags a draft keeps, and the longest one. */\nexport const PRODUCT_DRAFT_TAGS_MAX = 20\nexport const PRODUCT_DRAFT_TAG_MAX = 40\n/** The most categories a draft is filed under. */\nexport const PRODUCT_DRAFT_CATEGORIES_MAX = 10\n/** The longest search-listing title and description a draft keeps. */\nexport const PRODUCT_DRAFT_SEO_TITLE_MAX = 120\nexport const PRODUCT_DRAFT_SEO_DESCRIPTION_MAX = 320\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const PRODUCT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n/** The resources route's refusal for a plan without the feature. */\nexport const PRODUCT_DRAFT_PLAN_REFUSAL = 'This feature is not included in your plan — see Billing'\n\n/** The resources route's refusal at the plan's allowance. */\nexport function productDraftLimitRefusal(limit: number): string {\n return `Your plan includes ${limit} ${limit === 1 ? 'product' : 'products'} — ` +\n 'upgrade in Billing for more'\n}\n\n/**\n * What a caller sends as `content`: a proposed product, as the AI products\n * job proposes one, with an optional stated price for every variant and the\n * ids of categories the site already has. A photo is never taken: the\n * merchant adds pictures in the editor.\n */\nexport interface ProductDraftContent {\n name: string\n type?: ProductType\n description?: string\n tags?: string[]\n options?: Array<{ name: string; values: string[] }>\n seoTitle?: string\n seoDescription?: string\n /** One price for every variant, in dollars; absent leaves them unpriced. */\n priceUsd?: number\n /** Ids of the site's product categories; one the site lacks is dropped. */\n categoryIds?: string[]\n}\n\nconst PRODUCT_TYPES: readonly ProductType[] = ['physical', 'digital', 'service']\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\nfunction strings(label: string, value: unknown, problems: string[]): string[] {\n if (value === undefined) return []\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {\n problems.push(`${label} are a list of words`)\n return []\n }\n return [...new Set(value.map(line).filter(Boolean))]\n}\n\n/** A draft as the writer will store it, before its slug and keys are settled. */\nexport interface ProductDraftRead {\n proposal: ProposedProduct\n /** The price stated for every variant, or `null` for the merchant to set. */\n priceUsd: number | null\n categoryIds: string[]\n}\n\nexport type ProductDraftContentRead =\n | { ok: true; value: ProductDraftRead }\n | { ok: false; problems: string[] }\n\n/** The content as the writer reads it, or every problem that stops it. */\nexport function readProductDraftContent(\n content: Readonly<Record<string, unknown>>,\n): ProductDraftContentRead {\n const problems: string[] = []\n const name = line(content['name'])\n if (!name) problems.push('The product needs a name')\n else if (name.length > PRODUCT_DRAFT_NAME_MAX) {\n problems.push(`The name is longer than ${PRODUCT_DRAFT_NAME_MAX} characters`)\n }\n const rawType = content['type'] ?? 'physical'\n if (!(PRODUCT_TYPES as readonly unknown[]).includes(rawType)) {\n problems.push('The type is physical, digital or service')\n }\n const rawDescription = content['description']\n const description = typeof rawDescription === 'string' ? rawDescription.trim() : ''\n if (rawDescription !== undefined && typeof rawDescription !== 'string') {\n problems.push('The description is text')\n } else if (description.length > PRODUCT_DRAFT_DESCRIPTION_MAX) {\n problems.push(`The description is longer than ${PRODUCT_DRAFT_DESCRIPTION_MAX} characters`)\n }\n const tags = strings('Tags', content['tags'], problems)\n if (tags.length > PRODUCT_DRAFT_TAGS_MAX) problems.push(`A product keeps at most ${PRODUCT_DRAFT_TAGS_MAX} tags`)\n if (tags.some((tag) => tag.length > PRODUCT_DRAFT_TAG_MAX)) {\n problems.push(`A tag is longer than ${PRODUCT_DRAFT_TAG_MAX} characters`)\n }\n const categoryIds = strings('Categories', content['categoryIds'], problems)\n if (categoryIds.length > PRODUCT_DRAFT_CATEGORIES_MAX) {\n problems.push(`A product is filed under at most ${PRODUCT_DRAFT_CATEGORIES_MAX} categories`)\n }\n const seoTitle = line(content['seoTitle'])\n const seoDescription = line(content['seoDescription'])\n if (seoTitle.length > PRODUCT_DRAFT_SEO_TITLE_MAX) {\n problems.push(`The search title is longer than ${PRODUCT_DRAFT_SEO_TITLE_MAX} characters`)\n }\n if (seoDescription.length > PRODUCT_DRAFT_SEO_DESCRIPTION_MAX) {\n problems.push(`The search description is longer than ${PRODUCT_DRAFT_SEO_DESCRIPTION_MAX} characters`)\n }\n\n // Options are held here before the draft expands them: a matrix past the\n // variant ceiling is refused, never built.\n const rawOptions = content['options'] ?? []\n const options: ProposedProduct['options'] = []\n if (!Array.isArray(rawOptions)) {\n problems.push('Options are a list of a name and its values')\n } else {\n if (rawOptions.length > COMMERCE_MAX_OPTIONS) {\n problems.push(`At most ${COMMERCE_MAX_OPTIONS} options per product`)\n }\n for (const option of rawOptions) {\n const optionName = isRecord(option) ? line(option['name']) : ''\n const values = isRecord(option) ? strings(`The values of \"${optionName}\"`, option['values'], problems) : []\n if (!optionName || !values.length) {\n problems.push('Each option needs a name and at least one value')\n continue\n }\n if (values.length > COMMERCE_MAX_OPTION_VALUES) {\n problems.push(`Option \"${optionName}\" has too many values`)\n continue\n }\n options.push({ name: optionName, values })\n }\n if (new Set(options.map((option) => option.name)).size !== options.length) {\n problems.push('Each option needs its own name')\n }\n const combinations = options.reduce((count, option) => count * option.values.length, 1)\n if (combinations > COMMERCE_MAX_VARIANTS) {\n problems.push(`Those options make ${combinations} variants; a product holds at most ${COMMERCE_MAX_VARIANTS}`)\n }\n }\n\n // No price unless one is stated: the merchant sets it otherwise.\n const rawPrice = content['priceUsd']\n let priceUsd: number | null = null\n if (rawPrice !== undefined && rawPrice !== null) {\n if (\n typeof rawPrice !== 'number' ||\n !Number.isFinite(rawPrice) ||\n rawPrice < 0 ||\n rawPrice > COMMERCE_MAX_PRICE_USD ||\n Math.round(rawPrice * 100) !== rawPrice * 100\n ) {\n problems.push(`A stated price is dollars and cents from 0 to ${COMMERCE_MAX_PRICE_USD}`)\n } else {\n priceUsd = rawPrice\n }\n }\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n value: {\n proposal: {\n name,\n type: rawType as ProductType,\n description,\n tags,\n options,\n seoTitle,\n seoDescription,\n },\n priceUsd,\n categoryIds,\n },\n }\n}\n\n/** The product document a read builds, under `slug`, at `nowMs`. */\nexport function productDraftDocument(\n read: ProductDraftRead,\n slug: string,\n nowMs: number,\n): UnpricedProductDraft | HostProduct {\n const draft = unpricedProductDraft(read.proposal, new Set(), nowMs)\n const priced =\n read.priceUsd === null\n ? draft\n : ({\n ...draft,\n variants: draft.variants.map((variant) => ({ ...variant, priceUsd: read.priceUsd as number })),\n } as HostProduct)\n return {\n ...priced,\n // The search keys travel with every create; recomputed for the price.\n ...productSearchFields(priced as HostProduct),\n slug,\n ...(read.categoryIds.length ? { categoryIds: read.categoryIds } : {}),\n }\n}\n\n/** What the owner's product rule says about a draft, a placeholder price standing in for none. */\nfunction productProblem(product: UnpricedProductDraft | HostProduct): string | null {\n return validateProduct({\n ...product,\n variants: product.variants.map((variant) => ({\n ...variant,\n priceUsd: (variant as { priceUsd?: number }).priceUsd ?? 0,\n })),\n } as HostProduct)\n}\n\nfunction factsOf(product: Partial<HostProduct>) {\n return {\n status: product.status ?? 'draft',\n slug: product.slug ?? '',\n type: product.type ?? 'physical',\n variants: product.variants?.length ?? 0,\n priceMissing: productPriceMissing({ variants: product.variants ?? [] }),\n }\n}\n\n/** Whether content is a product this plugin would store, with what it says about it. Pure. */\nexport function checkProductDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readProductDraftContent(content)\n if (read.ok === false) return read\n const slug = commerceSlug(read.value.proposal.name) || 'product'\n const product = productDraftDocument(read.value, slug, 0)\n const problem = productProblem(product)\n return problem ? { ok: false, problems: [problem] } : { ok: true, facts: factsOf(product as HostProduct) }\n}\n\nfunction recordOf(product: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (product.data() ?? {}) as Partial<HostProduct>\n return { id: product.id, name: String(data.name ?? ''), versionId: null, facts: factsOf(data) }\n}\n\nexport function roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: PRODUCT_DRAFT_ROLE_REFUSAL }\n}\n\nexport function planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'commerce') ? null : { status: 403, error: PRODUCT_DRAFT_PLAN_REFUSAL }\n}\n\n/** The allowance, counted as the resources route and the importer count a product create. */\nexport function roomRefusal(org: PluginDraftContext['org'], used: number): PluginDraftRefusal | null {\n const quota = checkQuota(org as never, 'productsPerHost', used)\n return quota.allowed ? null : { status: 403, error: productDraftLimitRefusal(quota.limit) }\n}\n\n/** Candidates asked of the store at once, as the console's slug ledger probes. */\nconst SLUG_PROBE = 10\n\n/**\n * The first of `base`, `base-2`, `base-3`, … that no product holds, live or\n * deleted — a soft-deleted product still answers its slug on the storefront\n * (`productSlugLedger`, AGL-3321). Asked inside the transaction.\n */\nasync function freeSlug(\n products: FirebaseFirestore.CollectionReference,\n base: string,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<string> {\n const candidate = (suffix: number) => (suffix < 2 ? base : `${base}-${suffix}`)\n for (let first = 1; ; first += SLUG_PROBE) {\n const batch = Array.from({ length: SLUG_PROBE }, (_, offset) => candidate(first + offset))\n const held = new Set(\n (await read(products.where('slug', 'in', batch))).docs.map((doc) => String(doc.get('slug') ?? '')),\n )\n const free = batch.find((slug) => !held.has(slug))\n if (free) return free\n }\n}\n\nexport interface ProductDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createProductDraftWriter(deps: ProductDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n const early = roleRefusal(host, context.uid) ?? planRefusal(context.org)\n if (early) return early\n const used = (await hostRef.collection('products').count().get()).data().count\n return roomRefusal(context.org, Number(used) || 0)\n },\n\n check: (content) => checkProductDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const product = await firestore().collection('hosts').doc(hostId).collection('products').doc(id).get()\n return product.exists ? recordOf(product) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = line(request.name)\n const read = readProductDraftContent(asked ? { ...request.content, name: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const products = hostRef.collection('products')\n const productRef = products.doc(request.id)\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(productRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n // Asked again under the same id: the draft it already wrote.\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n const used = (await tx.get(products.count())).data().count\n const room = roomRefusal(request.org, Number(used) || 0)\n if (room) return { ok: false, ...room }\n const base = commerceSlug(read.value.proposal.name) || 'product'\n const [slug, categories, smart] = await Promise.all([\n freeSlug(products, base, (query) => tx.get(query)),\n // A category the site does not have is dropped, not stored.\n read.value.categoryIds.length\n ? Promise.all(read.value.categoryIds.map((id) => tx.get(hostRef.collection('productCategories').doc(id))))\n : Promise.resolve([]),\n tx.get(hostRef.collection('collections').where('kind', '==', 'catalog').where('mode', '==', 'smart')),\n ])\n const categoryIds = categories.filter((category) => category.exists).map((category) => category.id)\n const nowMs = request.now.getTime()\n const product = productDraftDocument({ ...read.value, categoryIds }, slug, nowMs)\n const problem = productProblem(product)\n if (problem) return { ok: false, status: 400, error: problem }\n const rules: SmartCollectionRules[] = smart.docs.map((doc) => ({\n id: doc.id,\n rules: doc.get('rules') ?? [],\n matchAll: doc.get('matchAll'),\n }))\n const priced = read.value.priceUsd !== null\n tx.create(productRef, {\n ...product,\n collectionIds: productCollectionIds(product as HostProduct, rules),\n // The legacy flat price every create path still writes, only where\n // there is one: an unpriced draft is not a free one.\n ...(priced ? { priceUsd: read.value.priceUsd } : {}),\n deletedAt: null,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: product.name,\n versionId: null,\n facts: factsOf(product as HostProduct),\n }\n })\n },\n }\n}\n\nexport const productDraftWriter = createProductDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerProductDraftWriter(): void {\n registerPluginResourceDraftWriter(PRODUCT_DRAFT_RESOURCE, productDraftWriter, { pluginId: BUNDLE_ID })\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","checkQuota","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","COMMERCE_MAX_OPTION_VALUES","COMMERCE_MAX_OPTIONS","COMMERCE_MAX_PRICE_USD","COMMERCE_MAX_VARIANTS","commerceSlug","productCollectionIds","productPriceMissing","productSearchFields","unpricedProductDraft","validateProduct","PRODUCT_DRAFT_RESOURCE","PRODUCT_DRAFT_NAME_MAX","PRODUCT_DRAFT_DESCRIPTION_MAX","PRODUCT_DRAFT_TAGS_MAX","PRODUCT_DRAFT_TAG_MAX","PRODUCT_DRAFT_CATEGORIES_MAX","PRODUCT_DRAFT_SEO_TITLE_MAX","PRODUCT_DRAFT_SEO_DESCRIPTION_MAX","PRODUCT_DRAFT_ROLE_REFUSAL","PRODUCT_DRAFT_PLAN_REFUSAL","productDraftLimitRefusal","limit","PRODUCT_TYPES","isRecord","value","Array","isArray","line","replace","trim","strings","label","problems","undefined","some","entry","push","Set","map","filter","Boolean","readProductDraftContent","content","name","length","rawType","includes","rawDescription","description","tags","tag","categoryIds","seoTitle","seoDescription","rawOptions","options","option","optionName","values","size","combinations","reduce","count","rawPrice","priceUsd","Number","isFinite","Math","round","ok","proposal","type","productDraftDocument","read","slug","nowMs","draft","priced","variants","variant","productProblem","product","factsOf","status","priceMissing","checkProductDraftContent","problem","facts","recordOf","data","id","String","versionId","roleRefusal","host","uid","role","get","error","planRefusal","org","roomRefusal","used","quota","allowed","SLUG_PROBE","freeSlug","products","base","candidate","suffix","first","batch","from","_","offset","held","where","docs","doc","free","find","has","createProductDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","hostId","exists","early","check","write","request","asked","db","productRef","runTransaction","tx","existing","Promise","all","replayed","room","categories","smart","query","resolve","category","now","getTime","rules","matchAll","create","collectionIds","deletedAt","createdAt","updatedAt","createdBy","productDraftWriter","registerProductDraftWriter","pluginId"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { PluginProductWriter, SourcedProductWrite, SourcedProductWriteOutcome } from '@aglyn/aglyn/plugin-manager/plugin-product-writer';
|
|
18
|
+
export declare function upsertSourcedProduct(write: SourcedProductWrite, firestore?: FirebaseFirestore.Firestore): Promise<SourcedProductWriteOutcome>;
|
|
19
|
+
export declare const commerceProductWriter: PluginProductWriter;
|