@aglyn/plugins-commerce 1.0.0-beta.264 → 1.0.0-beta.268
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 +14 -14
- package/src/lib/components/console/product-editor-dialog.component.js +10 -1
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +11 -10
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/server/billing-webhook.js +3 -1
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/pos-payment.js +9 -5
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-terminal.d.ts +12 -1
- package/src/lib/server/pos-terminal.js +82 -3
- package/src/lib/server/pos-terminal.js.map +1 -1
- package/src/lib/server/printers.js +33 -5
- package/src/lib/server/printers.js.map +1 -1
- package/src/lib/server/product-drafts.js +3 -2
- package/src/lib/server/product-drafts.js.map +1 -1
- package/src/lib/server/product-writer.js +3 -1
- package/src/lib/server/product-writer.js.map +1 -1
- package/src/lib/server/products-write.js +3 -2
- package/src/lib/server/products-write.js.map +1 -1
- package/src/lib/server/returns.js +3 -1
- package/src/lib/server/returns.js.map +1 -1
- package/src/lib/tenant-emails.js +264 -84
- package/src/lib/tenant-emails.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +5 -3
- package/src/lib/testing/memory-firestore.js +6 -3
- package/src/lib/testing/memory-firestore.js.map +1 -1
- package/src/lib/transfer/products.server.js +3 -2
- package/src/lib/transfer/products.server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/printers.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type { PluginApiHandler, PluginApiRequest } from '@aglyn/aglyn/server'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { resolveSiteTimeZone } from '@aglyn/aglyn/app-utils/collection-entry-date'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n normalizePrinterDeviceId,\n PRINTER_BRANDS,\n type PosPrinter,\n type PrinterBrand,\n type PrintReport,\n} from '../model/commerce-printers'\nimport { receiptDataFromOrder, type ReceiptData } from '../model/commerce-receipt'\nimport type { HostOrder } from '../model/commerce-orders'\nimport { enqueuePrintJob, printJobsRef, printersRef } from './print-queue'\nimport { printerPollUrl } from './printer-secret'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/**\n * `POST /api/commerce/printers` (AGL-3619): a register's cloud receipt\n * printers, from the Hardware card, and the jobs a manager sends them by hand\n * (a test page, a reprint, \"open drawer\").\n *\n * Every write to `printers` and `printJobs` is here or in the poll routes:\n * the Firestore rules deny both to every client, so a printer's identity, its\n * secret's version and its queue cannot be edited around this gate.\n */\n\n/** A counter seldom has more than two; the cap keeps a typo loop from minting dozens. */\nexport const MAX_PRINTERS_PER_REGISTER = 4\n\ninterface PrinterManager {\n uid: string\n hostId: string\n}\n\n/**\n * The register's gate (`pos-order.ts`): `admin` or `editor` on THIS site,\n * `managePos` on the member's permissions, and `pos` on the owning plan.\n */\nexport async function authorizePrinterManager(\n req: PluginApiRequest,\n hostId: string,\n): Promise<{ ok: true; manager: PrinterManager } | { ok: false; status: number; error: string }> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice(7) : ''\n if (!idToken) return { ok: false, status: 401, error: 'Unauthenticated' }\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(hostId)) return { ok: false, status: 400, error: 'Missing hostId' }\n let uid: string\n try {\n uid = (await firebaseAdmin.app().auth().verifyIdToken(idToken)).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return { ok: false, status: 401, error: 'Unauthenticated' }\n }\n const host = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get()\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n const role = (host.get('memberRoles') ?? {})[uid]\n if (role !== 'admin' && role !== 'editor') return { ok: false, status: 403, error: 'Not permitted' }\n const membership = await resolveOrgPermissions(uid, { hostId })\n if (!membership.permissions.managePos) return { ok: false, status: 403, error: 'Not permitted' }\n const owner = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(owner?.org as any, 'pos')) {\n return { ok: false, status: 403, error: 'POS requires the Pro plan or above' }\n }\n return { ok: true, manager: { uid, hostId } }\n}\n\nfunction requestBody(req: PluginApiRequest): Record<string, any> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body)\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? req.body : {}\n}\n\nconst text = (value: unknown, max: number): string => String(value ?? '').trim().slice(0, max)\n\n/** The editable settings in a create or update body, validated. */\nexport function printerSettingsFromBody(\n body: Record<string, any>,\n brand: PrinterBrand,\n): { ok: true; patch: Partial<PosPrinter> } | { ok: false; error: string } {\n const patch: Partial<PosPrinter> = {}\n if ('name' in body) {\n const name = text(body['name'], 60)\n if (!name) return { ok: false, error: 'Name the printer' }\n patch.name = name\n }\n if ('model' in body) patch.model = text(body['model'], 40)\n if ('deviceId' in body) {\n const deviceId = normalizePrinterDeviceId(brand, body['deviceId'])\n if (!deviceId) {\n return {\n ok: false,\n error:\n brand === 'star'\n ? 'Enter the printer’s MAC address, e.g. 00:11:62:12:34:56'\n : 'Enter the Server Direct Print ID: 1-30 letters, digits, dots, dashes or underscores',\n }\n }\n patch.deviceId = deviceId\n }\n if ('paperWidthMm' in body) patch.paperWidthMm = Number(body['paperWidthMm']) === 58 ? 58 : 80\n if ('autoPrintReceipts' in body) patch.autoPrintReceipts = body['autoPrintReceipts'] === true\n if ('kickDrawer' in body) patch.kickDrawer = body['kickDrawer'] === true\n if ('kitchenTickets' in body) patch.kitchenTickets = body['kitchenTickets'] === true\n if ('logoKey' in body) {\n const logoKey = text(body['logoKey'], 10)\n const valid =\n !logoKey ||\n (brand === 'star'\n ? /^\\d{1,3}$/.test(logoKey) && Number(logoKey) >= 1 && Number(logoKey) <= 255\n : /^\\d{2,3}\\s*,\\s*\\d{2,3}$/.test(logoKey))\n if (!valid) {\n return {\n ok: false,\n error:\n brand === 'star'\n ? 'The logo number is 1-255, as stored with Star Quick Setup Utility'\n : 'The logo key is two numbers, e.g. 48,48, as stored with Epson TM Utility',\n }\n }\n patch.logoKey = logoKey.replace(/\\s+/g, '')\n }\n return { ok: true, patch }\n}\n\n/** What the card shows a manager to paste into the printer's web configuration. */\nexport function printerCredentials(hostId: string, printerId: string, printer: PosPrinter) {\n return {\n printerId,\n brand: printer.brand,\n pollUrl: printerPollUrl(printer.brand, hostId, printerId, printer.secretVersion),\n deviceId: printer.deviceId,\n }\n}\n\n/** What a receipt needs from the site, the location and the register. */\nexport async function receiptContextFor(\n hostId: string,\n order: Pick<HostOrder, 'locationId'> & { registerId?: string },\n firestore: any = firebaseAdmin.app().firestore(),\n) {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [host, owner, location, register] = await Promise.all([\n hostRef.get(),\n getOrgForHost(hostId).catch(() => null),\n order.locationId ? hostRef.collection('locations').doc(order.locationId).get() : null,\n order.registerId ? hostRef.collection('registers').doc(order.registerId).get() : null,\n ])\n const storeLines = [location?.get?.('address')].filter(Boolean).map(String)\n return {\n storeName: String(host.get('displayName') ?? host.get('subdomain') ?? 'Receipt'),\n ...(storeLines.length ? { storeLines } : {}),\n ...(register?.exists ? { registerName: String(register.get('name') ?? '') } : {}),\n timeZone: resolveSiteTimeZone(owner?.org as any, host.data?.() ?? {}),\n }\n}\n\n/** The receipt an order prints, with the site's name, address and time zone. */\nexport async function orderReceipt(\n hostId: string,\n orderId: string,\n order: HostOrder & Record<string, any>,\n options: { banner?: string; firestore?: any } = {},\n): Promise<ReceiptData> {\n const context = await receiptContextFor(hostId, order, options.firestore)\n return receiptDataFromOrder(orderId, order, {\n ...context,\n ...(options.banner ? { banner: options.banner } : {}),\n })\n}\n\n/**\n * Queues work for a register's printers (AGL-3619): a receipt for every\n * printer set to auto-print, and a drawer kick on the FIRST printer set to\n * kick the drawer (a till has one drawer; two kicks would be one too many).\n * When the receipt printer is also the drawer printer, the kick rides the\n * receipt job so the drawer opens as the receipt starts.\n *\n * `kitchenTicket` prints on every printer set to print kitchen tickets, and\n * only when asked for: a Z-report or a paid-out slip is not an order to make.\n *\n * `receiptChoice` is what the customer said at the register: `none` (or a\n * receipt sent by email or text) prints no customer receipt; `print` prints\n * one even when no printer auto-prints, on the register's first printer.\n * Absent, the printers' own \"every sale\" setting decides.\n *\n * `idempotencyKey` names the cause (a sale's order id, a shift event's id): a\n * cause delivered twice queues its jobs once.\n */\nexport async function queueRegisterPrint(input: {\n hostId: string\n registerId: string\n receipt?: ReceiptData\n /** A shift report (AGL-3609): printed once, on the register's receipt printer. */\n report?: PrintReport\n kitchenTicket?: ReceiptData\n receiptChoice?: 'print' | 'none'\n openDrawer?: boolean\n orderId?: string\n reason: string\n idempotencyKey?: string\n createdBy?: string\n firestore?: any\n nowMs?: number\n}): Promise<{ jobIds: string[] }> {\n const firestore = input.firestore ?? firebaseAdmin.app().firestore()\n if (!input.registerId) return { jobIds: [] }\n const snapshot = await printersRef(firestore, input.hostId)\n .where('registerId', '==', input.registerId)\n .limit(MAX_PRINTERS_PER_REGISTER)\n .get()\n const printers = snapshot.docs\n .map((doc: any) => ({ id: doc.id, printer: doc.data() as PosPrinter }))\n .sort((a: any, b: any) => a.printer.createdAtMs - b.printer.createdAtMs || a.id.localeCompare(b.id))\n const drawerPrinter = input.openDrawer\n ? printers.find((entry: any) => entry.printer.kickDrawer)\n : undefined\n const jobIds: string[] = []\n const key = (suffix: string) =>\n input.idempotencyKey\n ? `${input.reason}-${input.idempotencyKey}-${suffix}`.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 120)\n : undefined\n let drawerHandled = false\n const receiptPrinters =\n input.receiptChoice === 'none'\n ? []\n : printers.filter((entry: any) => entry.printer.autoPrintReceipts)\n if (input.receiptChoice === 'print' && !receiptPrinters.length && printers.length) {\n receiptPrinters.push(printers[0])\n }\n if (input.receipt) {\n for (const entry of receiptPrinters) {\n const openDrawer = drawerPrinter?.id === entry.id\n drawerHandled ||= openDrawer\n const { jobId } = await enqueuePrintJob(\n firestore,\n input.hostId,\n entry.id,\n {\n kind: 'receipt',\n receipt: input.receipt,\n openDrawer,\n orderId: input.orderId,\n registerId: input.registerId,\n reason: input.reason,\n createdBy: input.createdBy,\n jobId: key(entry.id),\n },\n input.nowMs,\n )\n jobIds.push(jobId)\n }\n }\n if (input.report) {\n // One copy, on the printer that prints receipts — or the till's only one.\n const target = printers.find((entry: any) => entry.printer.autoPrintReceipts) ?? printers[0]\n if (target) {\n const { jobId } = await enqueuePrintJob(\n firestore,\n input.hostId,\n target.id,\n {\n kind: 'report',\n report: input.report,\n registerId: input.registerId,\n reason: input.reason,\n createdBy: input.createdBy,\n jobId: key(`${target.id}-report`),\n },\n input.nowMs,\n )\n jobIds.push(jobId)\n }\n }\n if (input.kitchenTicket) {\n for (const entry of printers) {\n if (!entry.printer.kitchenTickets) continue\n const { jobId } = await enqueuePrintJob(\n firestore,\n input.hostId,\n entry.id,\n {\n kind: 'kitchen',\n receipt: input.kitchenTicket,\n orderId: input.orderId,\n registerId: input.registerId,\n reason: input.reason,\n createdBy: input.createdBy,\n jobId: key(`${entry.id}-kitchen`),\n },\n input.nowMs,\n )\n jobIds.push(jobId)\n }\n }\n if (drawerPrinter && !drawerHandled) {\n const { jobId } = await enqueuePrintJob(\n firestore,\n input.hostId,\n drawerPrinter.id,\n {\n kind: 'drawer',\n orderId: input.orderId,\n registerId: input.registerId,\n reason: input.reason,\n createdBy: input.createdBy,\n jobId: key(`${drawerPrinter.id}-drawer`),\n },\n input.nowMs,\n )\n jobIds.push(jobId)\n }\n return { jobIds }\n}\n\nexport const printersHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = requestBody(req)\n const hostId = text(body['hostId'], 128)\n const gate = await authorizePrinterManager(req, hostId)\n if ('error' in gate) return res.status(gate.status).json({ error: gate.error })\n const { uid } = gate.manager\n const firestore = firebaseAdmin.app().firestore()\n const printers = printersRef(firestore, hostId)\n const action = text(body['action'], 20)\n const printerId = text(body['printerId'], 128)\n const loadPrinter = async () => {\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(printerId)) return null\n const snapshot = await printers.doc(printerId).get()\n return snapshot.exists ? { ref: snapshot.ref, printer: snapshot.data() as PosPrinter } : null\n }\n try {\n switch (action) {\n case 'create': {\n const brand = text(body['brand'], 10) as PrinterBrand\n if (!PRINTER_BRANDS.includes(brand)) return res.status(400).json({ error: 'Choose Star or Epson' })\n const registerId = text(body['registerId'], 128)\n const register = /^[A-Za-z0-9_-]{1,128}$/.test(registerId)\n ? await firestore.collection('hosts').doc(hostId).collection('registers').doc(registerId).get()\n : null\n if (!register?.exists) return res.status(404).json({ error: 'Unknown register' })\n const settings = printerSettingsFromBody(\n { name: '', deviceId: '', ...body },\n brand,\n )\n if ('error' in settings) return res.status(400).json({ error: settings.error })\n const siblings = await printers.where('registerId', '==', registerId).limit(MAX_PRINTERS_PER_REGISTER).get()\n if (siblings.size >= MAX_PRINTERS_PER_REGISTER) {\n return res.status(409).json({\n error: `A register can have up to ${MAX_PRINTERS_PER_REGISTER} printers.`,\n })\n }\n const duplicate = await printers.where('deviceId', '==', settings.patch.deviceId).limit(1).get()\n if (!duplicate.empty) {\n return res.status(409).json({ error: 'That printer is already added to this site.' })\n }\n const ref = printers.doc(createResourceUid())\n const printer: PosPrinter = {\n name: settings.patch.name!,\n brand,\n model: settings.patch.model ?? '',\n deviceId: settings.patch.deviceId!,\n registerId,\n paperWidthMm: settings.patch.paperWidthMm ?? 80,\n autoPrintReceipts: settings.patch.autoPrintReceipts ?? true,\n kickDrawer: settings.patch.kickDrawer ?? false,\n kitchenTickets: settings.patch.kitchenTickets ?? false,\n ...(settings.patch.logoKey ? { logoKey: settings.patch.logoKey } : {}),\n secretVersion: 1,\n createdAtMs: Date.now(),\n createdBy: uid,\n }\n await ref.set(printer)\n return res.status(200).json(printerCredentials(hostId, ref.id, printer))\n }\n case 'update': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(404).json({ error: 'Unknown printer' })\n const settings = printerSettingsFromBody(body, loaded.printer.brand)\n if ('error' in settings) return res.status(400).json({ error: settings.error })\n if (settings.patch.deviceId && settings.patch.deviceId !== loaded.printer.deviceId) {\n const duplicate = await printers.where('deviceId', '==', settings.patch.deviceId).limit(1).get()\n if (!duplicate.empty) {\n return res.status(409).json({ error: 'That printer is already added to this site.' })\n }\n }\n await loaded.ref.set({ ...settings.patch, updatedAtMs: Date.now() }, { merge: true })\n return res.status(200).json({ ok: true })\n }\n case 'remove': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(200).json({ ok: true })\n const active = await printJobsRef(firestore, hostId)\n .where('printerId', '==', printerId)\n .where('status', 'in', ['queued', 'printing'])\n .orderBy('createdAtMs', 'asc')\n .limit(50)\n .get()\n const batch = firestore.batch()\n for (const doc of active.docs) {\n batch.update(doc.ref, { status: 'canceled', finishedAtMs: Date.now() })\n }\n batch.delete(loaded.ref)\n await batch.commit()\n return res.status(200).json({ ok: true })\n }\n case 'credentials': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(404).json({ error: 'Unknown printer' })\n return res.status(200).json(printerCredentials(hostId, printerId, loaded.printer))\n }\n case 'regenerate': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(404).json({ error: 'Unknown printer' })\n const secretVersion = Math.max(1, Number(loaded.printer.secretVersion) || 1) + 1\n await loaded.ref.set({ secretVersion, updatedAtMs: Date.now() }, { merge: true })\n return res\n .status(200)\n .json(printerCredentials(hostId, printerId, { ...loaded.printer, secretVersion }))\n }\n case 'test':\n case 'drawer': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(404).json({ error: 'Unknown printer' })\n const context = action === 'test' ? await receiptContextFor(hostId, {}, firestore) : null\n const { jobId } = await enqueuePrintJob(firestore, hostId, printerId, {\n kind: action,\n registerId: loaded.printer.registerId,\n reason: action === 'test' ? 'test' : 'open_drawer',\n createdBy: uid,\n ...(context ? { storeName: context.storeName, timeZone: context.timeZone } : {}),\n ...(action === 'test' && body['openDrawer'] === true ? { openDrawer: true } : {}),\n })\n return res.status(200).json({ jobId })\n }\n case 'reprint': {\n const orderId = text(body['orderId'], 128)\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(orderId)) return res.status(400).json({ error: 'Missing orderId' })\n const orderSnapshot = await firestore.collection('hosts').doc(hostId).collection('orders').doc(orderId).get()\n if (!orderSnapshot.exists) return res.status(404).json({ error: 'Unknown order' })\n const order = orderSnapshot.data() as HostOrder & Record<string, any>\n let target = printerId ? await loadPrinter() : null\n if (!target && order['registerId']) {\n const candidates = await printers\n .where('registerId', '==', String(order['registerId']))\n .limit(MAX_PRINTERS_PER_REGISTER)\n .get()\n const doc = candidates.docs.find((d: any) => d.get('autoPrintReceipts')) ?? candidates.docs[0]\n if (doc) target = { ref: doc.ref, printer: doc.data() as PosPrinter }\n }\n if (!target) return res.status(404).json({ error: 'No printer for this register' })\n const receipt = await orderReceipt(hostId, orderId, order, { banner: 'REPRINT', firestore })\n const { jobId } = await enqueuePrintJob(firestore, hostId, target.ref.id, {\n kind: 'receipt',\n receipt,\n orderId,\n registerId: target.printer.registerId,\n reason: 'reprint',\n createdBy: uid,\n })\n return res.status(200).json({ jobId })\n }\n case 'cancel': {\n const jobId = text(body['jobId'], 128)\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(jobId)) return res.status(400).json({ error: 'Missing jobId' })\n const ref = printJobsRef(firestore, hostId).doc(jobId)\n const outcome = await firestore.runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) return 'missing'\n if (snapshot.get('status') !== 'queued') return 'started'\n transaction.update(ref, { status: 'canceled', finishedAtMs: Date.now() })\n return 'canceled'\n })\n if (outcome === 'missing') return res.status(404).json({ error: 'Unknown job' })\n if (outcome === 'started') {\n return res.status(409).json({ error: 'The printer already has it.' })\n }\n return res.status(200).json({ ok: true })\n }\n default:\n return res.status(400).json({ error: 'Unknown action' })\n }\n } catch (error) {\n console.error('[printers]', action, error)\n return res.status(500).json({ error: 'The printer could not be updated' })\n }\n}\n"],"names":["Aglyn","createResourceUid","resolveSiteTimeZone","firebaseAdmin","getOrgForHost","resolveOrgPermissions","normalizePrinterDeviceId","PRINTER_BRANDS","receiptDataFromOrder","enqueuePrintJob","printJobsRef","printersRef","printerPollUrl","isRefusedIdToken","MAX_PRINTERS_PER_REGISTER","authorizePrinterManager","req","hostId","host","authorization","String","headers","idToken","startsWith","slice","ok","status","error","test","uid","app","auth","verifyIdToken","firestore","collection","doc","get","exists","role","membership","permissions","managePos","owner","checkEntitlement","org","manager","requestBody","body","parsed","JSON","parse","text","value","max","trim","printerSettingsFromBody","brand","patch","name","model","deviceId","paperWidthMm","Number","autoPrintReceipts","kickDrawer","kitchenTickets","logoKey","valid","replace","printerCredentials","printerId","printer","pollUrl","secretVersion","receiptContextFor","order","register","location","hostRef","Promise","all","catch","locationId","registerId","storeLines","filter","Boolean","map","storeName","length","registerName","timeZone","data","orderReceipt","orderId","options","context","banner","queueRegisterPrint","input","jobIds","snapshot","where","limit","printers","docs","id","sort","a","b","createdAtMs","localeCompare","drawerPrinter","openDrawer","find","entry","undefined","key","suffix","idempotencyKey","reason","drawerHandled","receiptPrinters","receiptChoice","push","receipt","jobId","kind","createdBy","nowMs","report","target","kitchenTicket","printersHandler","res","method","json","gate","action","loadPrinter","ref","settings","includes","siblings","size","duplicate","empty","Date","now","set","loaded","updatedAtMs","merge","active","orderBy","batch","update","finishedAtMs","delete","commit","Math","orderSnapshot","candidates","d","outcome","runTransaction","transaction","console"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/printers.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type { PluginApiHandler, PluginApiRequest } from '@aglyn/aglyn/server'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { resolveSiteTimeZone } from '@aglyn/aglyn/app-utils/collection-entry-date'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolveOrgPermissions } from '@aglyn/tenant-runtime/org-permissions'\nimport {\n normalizePrinterDeviceId,\n PRINTER_BRANDS,\n type PosPrinter,\n type PrinterBrand,\n type PrintReport,\n} from '../model/commerce-printers'\nimport { receiptDataFromOrder, type ReceiptData } from '../model/commerce-receipt'\nimport type { HostOrder } from '../model/commerce-orders'\nimport { enqueuePrintJob, printJobsRef, printersRef } from './print-queue'\nimport { printerPollUrl } from './printer-secret'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\n\n/**\n * `POST /api/commerce/printers` (AGL-3619): a register's cloud receipt\n * printers, from the Hardware card, and the jobs a manager sends them by hand\n * (a test page, a reprint, \"open drawer\").\n *\n * Every write to `printers` and `printJobs` is here or in the poll routes:\n * the Firestore rules deny both to every client, so a printer's identity, its\n * secret's version and its queue cannot be edited around this gate.\n */\n\n/** A counter seldom has more than two; the cap keeps a typo loop from minting dozens. */\nexport const MAX_PRINTERS_PER_REGISTER = 4\n\ninterface PrinterManager {\n uid: string\n hostId: string\n}\n\n/**\n * The register's gate (`pos-order.ts`): `admin` or `editor` on THIS site,\n * `managePos` on the member's permissions, and `pos` on the owning plan.\n */\nexport async function authorizePrinterManager(\n req: PluginApiRequest,\n hostId: string,\n): Promise<{ ok: true; manager: PrinterManager } | { ok: false; status: number; error: string }> {\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice(7) : ''\n if (!idToken) return { ok: false, status: 401, error: 'Unauthenticated' }\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(hostId)) return { ok: false, status: 400, error: 'Missing hostId' }\n let uid: string\n try {\n uid = (await firebaseAdmin.app().auth().verifyIdToken(idToken)).uid\n } catch (error) {\n if (!isRefusedIdToken(error)) throw error\n return { ok: false, status: 401, error: 'Unauthenticated' }\n }\n const host = await firebaseAdmin.app().firestore().collection('hosts').doc(hostId).get()\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n const role = (host.get('memberRoles') ?? {})[uid]\n if (role !== 'admin' && role !== 'editor') return { ok: false, status: 403, error: 'Not permitted' }\n const membership = await resolveOrgPermissions(uid, { hostId })\n if (!membership.permissions.managePos) return { ok: false, status: 403, error: 'Not permitted' }\n const owner = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(owner?.org as any, 'pos')) {\n return { ok: false, status: 403, error: 'POS requires the Pro plan or above' }\n }\n return { ok: true, manager: { uid, hostId } }\n}\n\nfunction requestBody(req: PluginApiRequest): Record<string, any> {\n if (typeof req.body === 'string') {\n try {\n const parsed = JSON.parse(req.body)\n return parsed && typeof parsed === 'object' ? parsed : {}\n } catch {\n return {}\n }\n }\n return req.body && typeof req.body === 'object' ? req.body : {}\n}\n\nconst text = (value: unknown, max: number): string => String(value ?? '').trim().slice(0, max)\n\n/** The editable settings in a create or update body, validated. */\nexport function printerSettingsFromBody(\n body: Record<string, any>,\n brand: PrinterBrand,\n): { ok: true; patch: Partial<PosPrinter> } | { ok: false; error: string } {\n const patch: Partial<PosPrinter> = {}\n if ('name' in body) {\n const name = text(body['name'], 60)\n if (!name) return { ok: false, error: 'Name the printer' }\n patch.name = name\n }\n if ('model' in body) patch.model = text(body['model'], 40)\n if ('deviceId' in body) {\n const deviceId = normalizePrinterDeviceId(brand, body['deviceId'])\n if (!deviceId) {\n return {\n ok: false,\n error:\n brand === 'star'\n ? 'Enter the printer’s MAC address, e.g. 00:11:62:12:34:56'\n : 'Enter the Server Direct Print ID: 1-30 letters, digits, dots, dashes or underscores',\n }\n }\n patch.deviceId = deviceId\n }\n if ('paperWidthMm' in body) patch.paperWidthMm = Number(body['paperWidthMm']) === 58 ? 58 : 80\n if ('autoPrintReceipts' in body) patch.autoPrintReceipts = body['autoPrintReceipts'] === true\n if ('kickDrawer' in body) patch.kickDrawer = body['kickDrawer'] === true\n if ('kitchenTickets' in body) patch.kitchenTickets = body['kitchenTickets'] === true\n if ('logoKey' in body) {\n const logoKey = text(body['logoKey'], 10)\n const valid =\n !logoKey ||\n (brand === 'star'\n ? /^\\d{1,3}$/.test(logoKey) && Number(logoKey) >= 1 && Number(logoKey) <= 255\n : /^\\d{2,3}\\s*,\\s*\\d{2,3}$/.test(logoKey))\n if (!valid) {\n return {\n ok: false,\n error:\n brand === 'star'\n ? 'The logo number is 1-255, as stored with Star Quick Setup Utility'\n : 'The logo key is two numbers, e.g. 48,48, as stored with Epson TM Utility',\n }\n }\n patch.logoKey = logoKey.replace(/\\s+/g, '')\n }\n return { ok: true, patch }\n}\n\n/**\n * A printer's URL is signed with the deployment's token secret. Where that is\n * not configured nothing could ever poll, so a create or a regenerate is\n * refused BEFORE it writes: a signing failure leaves no printer the\n * manager cannot connect, and no version bump that strands a working one.\n */\nconst NO_SIGNING = 'Receipt printers are not available on this deployment.'\n\nfunction signedCredentials(\n hostId: string,\n printerId: string,\n printer: PosPrinter,\n): ReturnType<typeof printerCredentials> | null {\n try {\n return printerCredentials(hostId, printerId, printer)\n } catch (error) {\n console.error('[printers] the printer URL could not be signed', error)\n return null\n }\n}\n\n/** What the card shows a manager to paste into the printer's web configuration. */\nexport function printerCredentials(hostId: string, printerId: string, printer: PosPrinter) {\n return {\n printerId,\n brand: printer.brand,\n pollUrl: printerPollUrl(printer.brand, hostId, printerId, printer.secretVersion),\n deviceId: printer.deviceId,\n }\n}\n\n/** What a receipt needs from the site, the location and the register. */\nexport async function receiptContextFor(\n hostId: string,\n order: Pick<HostOrder, 'locationId'> & { registerId?: string },\n firestore: any = firebaseAdmin.app().firestore(),\n) {\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [host, owner, location, register] = await Promise.all([\n hostRef.get(),\n getOrgForHost(hostId).catch(() => null),\n order.locationId ? hostRef.collection('locations').doc(order.locationId).get() : null,\n order.registerId ? hostRef.collection('registers').doc(order.registerId).get() : null,\n ])\n const storeLines = [location?.get?.('address')].filter(Boolean).map(String)\n return {\n storeName: String(host.get('displayName') ?? host.get('subdomain') ?? 'Receipt'),\n ...(storeLines.length ? { storeLines } : {}),\n ...(register?.exists ? { registerName: String(register.get('name') ?? '') } : {}),\n timeZone: resolveSiteTimeZone(owner?.org as any, host.data?.() ?? {}),\n }\n}\n\n/** The receipt an order prints, with the site's name, address and time zone. */\nexport async function orderReceipt(\n hostId: string,\n orderId: string,\n order: HostOrder & Record<string, any>,\n options: { banner?: string; firestore?: any } = {},\n): Promise<ReceiptData> {\n const context = await receiptContextFor(hostId, order, options.firestore)\n return receiptDataFromOrder(orderId, order, {\n ...context,\n ...(options.banner ? { banner: options.banner } : {}),\n })\n}\n\n/**\n * Queues work for a register's printers (AGL-3619): a receipt for every\n * printer set to auto-print, and a drawer kick on the FIRST printer set to\n * kick the drawer (a till has one drawer; two kicks would be one too many).\n * When the receipt printer is also the drawer printer, the kick rides the\n * receipt job so the drawer opens as the receipt starts.\n *\n * `kitchenTicket` prints on every printer set to print kitchen tickets, and\n * only when asked for: a Z-report or a paid-out slip is not an order to make.\n *\n * `receiptChoice` is what the customer said at the register: `none` (or a\n * receipt sent by email or text) prints no customer receipt; `print` prints\n * one even when no printer auto-prints, on the register's first printer.\n * Absent, the printers' own \"every sale\" setting decides.\n *\n * `idempotencyKey` names the cause (a sale's order id, a shift event's id): a\n * cause delivered twice queues its jobs once.\n */\nexport async function queueRegisterPrint(input: {\n hostId: string\n registerId: string\n receipt?: ReceiptData\n /** A shift report (AGL-3609): printed once, on the register's receipt printer. */\n report?: PrintReport\n kitchenTicket?: ReceiptData\n receiptChoice?: 'print' | 'none'\n openDrawer?: boolean\n orderId?: string\n reason: string\n idempotencyKey?: string\n createdBy?: string\n firestore?: any\n nowMs?: number\n}): Promise<{ jobIds: string[] }> {\n const firestore = input.firestore ?? firebaseAdmin.app().firestore()\n if (!input.registerId) return { jobIds: [] }\n const snapshot = await printersRef(firestore, input.hostId)\n .where('registerId', '==', input.registerId)\n .limit(MAX_PRINTERS_PER_REGISTER)\n .get()\n const printers = snapshot.docs\n .map((doc: any) => ({ id: doc.id, printer: doc.data() as PosPrinter }))\n .sort((a: any, b: any) => a.printer.createdAtMs - b.printer.createdAtMs || a.id.localeCompare(b.id))\n const drawerPrinter = input.openDrawer\n ? printers.find((entry: any) => entry.printer.kickDrawer)\n : undefined\n const jobIds: string[] = []\n const key = (suffix: string) =>\n input.idempotencyKey\n ? `${input.reason}-${input.idempotencyKey}-${suffix}`.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 120)\n : undefined\n let drawerHandled = false\n const receiptPrinters =\n input.receiptChoice === 'none'\n ? []\n : printers.filter((entry: any) => entry.printer.autoPrintReceipts)\n if (input.receiptChoice === 'print' && !receiptPrinters.length && printers.length) {\n receiptPrinters.push(printers[0])\n }\n if (input.receipt) {\n for (const entry of receiptPrinters) {\n const openDrawer = drawerPrinter?.id === entry.id\n drawerHandled ||= openDrawer\n const { jobId } = await enqueuePrintJob(\n firestore,\n input.hostId,\n entry.id,\n {\n kind: 'receipt',\n receipt: input.receipt,\n openDrawer,\n orderId: input.orderId,\n registerId: input.registerId,\n reason: input.reason,\n createdBy: input.createdBy,\n jobId: key(entry.id),\n },\n input.nowMs,\n )\n jobIds.push(jobId)\n }\n }\n if (input.report) {\n // One copy, on the printer that prints receipts — or the till's only one.\n const target = printers.find((entry: any) => entry.printer.autoPrintReceipts) ?? printers[0]\n if (target) {\n const { jobId } = await enqueuePrintJob(\n firestore,\n input.hostId,\n target.id,\n {\n kind: 'report',\n report: input.report,\n registerId: input.registerId,\n reason: input.reason,\n createdBy: input.createdBy,\n jobId: key(`${target.id}-report`),\n },\n input.nowMs,\n )\n jobIds.push(jobId)\n }\n }\n if (input.kitchenTicket) {\n for (const entry of printers) {\n if (!entry.printer.kitchenTickets) continue\n const { jobId } = await enqueuePrintJob(\n firestore,\n input.hostId,\n entry.id,\n {\n kind: 'kitchen',\n receipt: input.kitchenTicket,\n orderId: input.orderId,\n registerId: input.registerId,\n reason: input.reason,\n createdBy: input.createdBy,\n jobId: key(`${entry.id}-kitchen`),\n },\n input.nowMs,\n )\n jobIds.push(jobId)\n }\n }\n if (drawerPrinter && !drawerHandled) {\n const { jobId } = await enqueuePrintJob(\n firestore,\n input.hostId,\n drawerPrinter.id,\n {\n kind: 'drawer',\n orderId: input.orderId,\n registerId: input.registerId,\n reason: input.reason,\n createdBy: input.createdBy,\n jobId: key(`${drawerPrinter.id}-drawer`),\n },\n input.nowMs,\n )\n jobIds.push(jobId)\n }\n return { jobIds }\n}\n\nexport const printersHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const body = requestBody(req)\n const hostId = text(body['hostId'], 128)\n const gate = await authorizePrinterManager(req, hostId)\n if ('error' in gate) return res.status(gate.status).json({ error: gate.error })\n const { uid } = gate.manager\n const firestore = firebaseAdmin.app().firestore()\n const printers = printersRef(firestore, hostId)\n const action = text(body['action'], 20)\n const printerId = text(body['printerId'], 128)\n const loadPrinter = async () => {\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(printerId)) return null\n const snapshot = await printers.doc(printerId).get()\n return snapshot.exists ? { ref: snapshot.ref, printer: snapshot.data() as PosPrinter } : null\n }\n try {\n switch (action) {\n case 'create': {\n const brand = text(body['brand'], 10) as PrinterBrand\n if (!PRINTER_BRANDS.includes(brand)) return res.status(400).json({ error: 'Choose Star or Epson' })\n const registerId = text(body['registerId'], 128)\n const register = /^[A-Za-z0-9_-]{1,128}$/.test(registerId)\n ? await firestore.collection('hosts').doc(hostId).collection('registers').doc(registerId).get()\n : null\n if (!register?.exists) return res.status(404).json({ error: 'Unknown register' })\n const settings = printerSettingsFromBody(\n { name: '', deviceId: '', ...body },\n brand,\n )\n if ('error' in settings) return res.status(400).json({ error: settings.error })\n const siblings = await printers.where('registerId', '==', registerId).limit(MAX_PRINTERS_PER_REGISTER).get()\n if (siblings.size >= MAX_PRINTERS_PER_REGISTER) {\n return res.status(409).json({\n error: `A register can have up to ${MAX_PRINTERS_PER_REGISTER} printers.`,\n })\n }\n const duplicate = await printers.where('deviceId', '==', settings.patch.deviceId).limit(1).get()\n if (!duplicate.empty) {\n return res.status(409).json({ error: 'That printer is already added to this site.' })\n }\n const ref = printers.doc(createResourceUid())\n const printer: PosPrinter = {\n name: settings.patch.name!,\n brand,\n model: settings.patch.model ?? '',\n deviceId: settings.patch.deviceId!,\n registerId,\n paperWidthMm: settings.patch.paperWidthMm ?? 80,\n autoPrintReceipts: settings.patch.autoPrintReceipts ?? true,\n kickDrawer: settings.patch.kickDrawer ?? false,\n kitchenTickets: settings.patch.kitchenTickets ?? false,\n ...(settings.patch.logoKey ? { logoKey: settings.patch.logoKey } : {}),\n secretVersion: 1,\n createdAtMs: Date.now(),\n createdBy: uid,\n }\n // Signed first: a deployment that cannot sign the URL adds no printer.\n const credentials = signedCredentials(hostId, ref.id, printer)\n if (!credentials) return res.status(503).json({ error: NO_SIGNING })\n await ref.set(printer)\n return res.status(200).json(credentials)\n }\n case 'update': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(404).json({ error: 'Unknown printer' })\n const settings = printerSettingsFromBody(body, loaded.printer.brand)\n if ('error' in settings) return res.status(400).json({ error: settings.error })\n if (settings.patch.deviceId && settings.patch.deviceId !== loaded.printer.deviceId) {\n const duplicate = await printers.where('deviceId', '==', settings.patch.deviceId).limit(1).get()\n if (!duplicate.empty) {\n return res.status(409).json({ error: 'That printer is already added to this site.' })\n }\n }\n await loaded.ref.set({ ...settings.patch, updatedAtMs: Date.now() }, { merge: true })\n return res.status(200).json({ ok: true })\n }\n case 'remove': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(200).json({ ok: true })\n const active = await printJobsRef(firestore, hostId)\n .where('printerId', '==', printerId)\n .where('status', 'in', ['queued', 'printing'])\n .orderBy('createdAtMs', 'asc')\n .limit(50)\n .get()\n const batch = firestore.batch()\n for (const doc of active.docs) {\n batch.update(doc.ref, { status: 'canceled', finishedAtMs: Date.now() })\n }\n batch.delete(loaded.ref)\n await batch.commit()\n return res.status(200).json({ ok: true })\n }\n case 'credentials': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(404).json({ error: 'Unknown printer' })\n const credentials = signedCredentials(hostId, printerId, loaded.printer)\n if (!credentials) return res.status(503).json({ error: NO_SIGNING })\n return res.status(200).json(credentials)\n }\n case 'regenerate': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(404).json({ error: 'Unknown printer' })\n const secretVersion = Math.max(1, Number(loaded.printer.secretVersion) || 1) + 1\n // Signed first: a failed signing must not bump the version and strand the printer's working URL.\n const credentials = signedCredentials(hostId, printerId, { ...loaded.printer, secretVersion })\n if (!credentials) return res.status(503).json({ error: NO_SIGNING })\n await loaded.ref.set({ secretVersion, updatedAtMs: Date.now() }, { merge: true })\n return res.status(200).json(credentials)\n }\n case 'test':\n case 'drawer': {\n const loaded = await loadPrinter()\n if (!loaded) return res.status(404).json({ error: 'Unknown printer' })\n const context = action === 'test' ? await receiptContextFor(hostId, {}, firestore) : null\n const { jobId } = await enqueuePrintJob(firestore, hostId, printerId, {\n kind: action,\n registerId: loaded.printer.registerId,\n reason: action === 'test' ? 'test' : 'open_drawer',\n createdBy: uid,\n ...(context ? { storeName: context.storeName, timeZone: context.timeZone } : {}),\n ...(action === 'test' && body['openDrawer'] === true ? { openDrawer: true } : {}),\n })\n return res.status(200).json({ jobId })\n }\n case 'reprint': {\n const orderId = text(body['orderId'], 128)\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(orderId)) return res.status(400).json({ error: 'Missing orderId' })\n const orderSnapshot = await firestore.collection('hosts').doc(hostId).collection('orders').doc(orderId).get()\n if (!orderSnapshot.exists) return res.status(404).json({ error: 'Unknown order' })\n const order = orderSnapshot.data() as HostOrder & Record<string, any>\n let target = printerId ? await loadPrinter() : null\n if (!target && order['registerId']) {\n const candidates = await printers\n .where('registerId', '==', String(order['registerId']))\n .limit(MAX_PRINTERS_PER_REGISTER)\n .get()\n const doc = candidates.docs.find((d: any) => d.get('autoPrintReceipts')) ?? candidates.docs[0]\n if (doc) target = { ref: doc.ref, printer: doc.data() as PosPrinter }\n }\n if (!target) return res.status(404).json({ error: 'No printer for this register' })\n const receipt = await orderReceipt(hostId, orderId, order, { banner: 'REPRINT', firestore })\n const { jobId } = await enqueuePrintJob(firestore, hostId, target.ref.id, {\n kind: 'receipt',\n receipt,\n orderId,\n registerId: target.printer.registerId,\n reason: 'reprint',\n createdBy: uid,\n })\n return res.status(200).json({ jobId })\n }\n case 'cancel': {\n const jobId = text(body['jobId'], 128)\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(jobId)) return res.status(400).json({ error: 'Missing jobId' })\n const ref = printJobsRef(firestore, hostId).doc(jobId)\n const outcome = await firestore.runTransaction(async (transaction: any) => {\n const snapshot = await transaction.get(ref)\n if (!snapshot.exists) return 'missing'\n if (snapshot.get('status') !== 'queued') return 'started'\n transaction.update(ref, { status: 'canceled', finishedAtMs: Date.now() })\n return 'canceled'\n })\n if (outcome === 'missing') return res.status(404).json({ error: 'Unknown job' })\n if (outcome === 'started') {\n return res.status(409).json({ error: 'The printer already has it.' })\n }\n return res.status(200).json({ ok: true })\n }\n default:\n return res.status(400).json({ error: 'Unknown action' })\n }\n } catch (error) {\n console.error('[printers]', action, error)\n return res.status(500).json({ error: 'The printer could not be updated' })\n }\n}\n"],"names":["Aglyn","createResourceUid","resolveSiteTimeZone","firebaseAdmin","getOrgForHost","resolveOrgPermissions","normalizePrinterDeviceId","PRINTER_BRANDS","receiptDataFromOrder","enqueuePrintJob","printJobsRef","printersRef","printerPollUrl","isRefusedIdToken","MAX_PRINTERS_PER_REGISTER","authorizePrinterManager","req","hostId","host","authorization","String","headers","idToken","startsWith","slice","ok","status","error","test","uid","app","auth","verifyIdToken","firestore","collection","doc","get","exists","role","membership","permissions","managePos","owner","checkEntitlement","org","manager","requestBody","body","parsed","JSON","parse","text","value","max","trim","printerSettingsFromBody","brand","patch","name","model","deviceId","paperWidthMm","Number","autoPrintReceipts","kickDrawer","kitchenTickets","logoKey","valid","replace","NO_SIGNING","signedCredentials","printerId","printer","printerCredentials","console","pollUrl","secretVersion","receiptContextFor","order","register","location","hostRef","Promise","all","catch","locationId","registerId","storeLines","filter","Boolean","map","storeName","length","registerName","timeZone","data","orderReceipt","orderId","options","context","banner","queueRegisterPrint","input","jobIds","snapshot","where","limit","printers","docs","id","sort","a","b","createdAtMs","localeCompare","drawerPrinter","openDrawer","find","entry","undefined","key","suffix","idempotencyKey","reason","drawerHandled","receiptPrinters","receiptChoice","push","receipt","jobId","kind","createdBy","nowMs","report","target","kitchenTicket","printersHandler","res","method","json","gate","action","loadPrinter","ref","settings","includes","siblings","size","duplicate","empty","Date","now","credentials","set","loaded","updatedAtMs","merge","active","orderBy","batch","update","finishedAtMs","delete","commit","Math","orderSnapshot","candidates","d","outcome","runTransaction","transaction"],"mappings":"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"}
|
|
@@ -16,6 +16,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
|
|
18
18
|
import { checkEntitlement, checkQuota } from "@aglyn/aglyn/app-utils/plan-entitlements";
|
|
19
|
+
import { countLiveDocuments } from "@aglyn/aglyn/app-utils/live-document-count";
|
|
19
20
|
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
20
21
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
21
22
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
@@ -292,7 +293,7 @@ export function createProductDraftWriter(deps = {}) {
|
|
|
292
293
|
};
|
|
293
294
|
const early = (_roleRefusal = roleRefusal(host, context.uid)) != null ? _roleRefusal : planRefusal(context.org);
|
|
294
295
|
if (early) return early;
|
|
295
|
-
const used =
|
|
296
|
+
const used = await countLiveDocuments(hostRef.collection('products'));
|
|
296
297
|
return roomRefusal(context.org, Number(used) || 0);
|
|
297
298
|
},
|
|
298
299
|
check: (content)=>checkProductDraftContent(content),
|
|
@@ -336,7 +337,7 @@ export function createProductDraftWriter(deps = {}) {
|
|
|
336
337
|
if (early) return _extends({
|
|
337
338
|
ok: false
|
|
338
339
|
}, early);
|
|
339
|
-
const used =
|
|
340
|
+
const used = await countLiveDocuments(products, (aggregate)=>tx.get(aggregate));
|
|
340
341
|
const room = roomRefusal(request.org, Number(used) || 0);
|
|
341
342
|
if (room) return _extends({
|
|
342
343
|
ok: false
|
|
@@ -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_DEFAULT_PRICE_USD,\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. A caller may ask\n * for it to be LISTED before it has a price (`comingSoon`, AGL-3676): it\n * is then `active` and the storefront says \"Price coming soon\" where the\n * price would be — still sold by no door until every variant is priced —\n * and may hand it photos already in the site's media library.\n * - THE PRICE is the one the content states, else the default price\n * (`COMMERCE_DEFAULT_PRICE_USD`, AGL-3676) for the owner to change. Only\n * a caller that says `priceUsd: null` gets a product with none, which the\n * storefront lists as \"Price coming soon\" and no door sells.\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, and optionally photos already in\n * the site's media library and whether to list it before it has a price.\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 /**\n * One price for every variant, in dollars. Absent is the default price\n * (AGL-3676); `null` leaves them unpriced on purpose.\n */\n priceUsd?: number | null\n /** Ids of the site's product categories; one the site lacks is dropped. */\n categoryIds?: string[]\n /**\n * List the product on the storefront before it has a price (AGL-3676):\n * written `active`, so the catalog shows it and its page answers, while\n * every sale door still refuses a variant with no price\n * (`variantHasPrice`) and the storefront says \"Price coming soon\" where a\n * price would be. Absent or false, an unpriced product stays a draft.\n */\n comingSoon?: boolean\n /**\n * Photos already in the site's media library, first = primary (AGL-3676):\n * the slot a guided start fills with a stock photo, or a starter, for the\n * merchant to replace with their own.\n */\n mediaUrls?: string[]\n}\n\n/** The most photos a draft is written with. */\nexport const PRODUCT_DRAFT_MEDIA_MAX = 4\n\n/** A photo address a draft keeps: an https URL or a path on the site, never a script or a data URL. */\nconst MEDIA_URL = /^(?:https:\\/\\/[^\\s\"'<>]+|\\/[^\\s\"'<>]*)$/\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 for every variant (stated, else the default), or `null` for the merchant to set. */\n priceUsd: number | null\n categoryIds: string[]\n /** Listed before it has a price (`ProductDraftContent.comingSoon`). */\n comingSoon: boolean\n mediaUrls: 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 // The stated price, else the default (AGL-3676); `null` asks for none.\n const rawPrice = content['priceUsd']\n let priceUsd: number | null = rawPrice === null ? null : COMMERCE_DEFAULT_PRICE_USD\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 const rawComingSoon = content['comingSoon']\n if (rawComingSoon !== undefined && typeof rawComingSoon !== 'boolean') {\n problems.push('Coming soon is true or false')\n }\n const mediaUrls = strings('Photos', content['mediaUrls'], problems)\n if (mediaUrls.length > PRODUCT_DRAFT_MEDIA_MAX) {\n problems.push(`A product is written with at most ${PRODUCT_DRAFT_MEDIA_MAX} photos`)\n }\n if (mediaUrls.some((url) => url.length > 2_000 || !MEDIA_URL.test(url))) {\n problems.push('A photo is an https address or a path on this site')\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 comingSoon: rawComingSoon === true,\n mediaUrls,\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 // Listed before it has a price (AGL-3676): the storefront says \"Price\n // coming soon\", and no sale door sells a variant without one.\n ...(read.comingSoon ? { status: 'active' as const } : {}),\n ...(read.mediaUrls.length ? { mediaUrls: read.mediaUrls } : {}),\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_DEFAULT_PRICE_USD","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_DRAFT_MEDIA_MAX","MEDIA_URL","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","rawComingSoon","mediaUrls","url","test","ok","proposal","type","comingSoon","productDraftDocument","read","slug","nowMs","draft","priced","variants","variant","status","productProblem","product","factsOf","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 { countLiveDocuments } from '@aglyn/aglyn/app-utils/live-document-count'\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_DEFAULT_PRICE_USD,\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. A caller may ask\n * for it to be LISTED before it has a price (`comingSoon`, AGL-3676): it\n * is then `active` and the storefront says \"Price coming soon\" where the\n * price would be — still sold by no door until every variant is priced —\n * and may hand it photos already in the site's media library.\n * - THE PRICE is the one the content states, else the default price\n * (`COMMERCE_DEFAULT_PRICE_USD`, AGL-3676) for the owner to change. Only\n * a caller that says `priceUsd: null` gets a product with none, which the\n * storefront lists as \"Price coming soon\" and no door sells.\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, and optionally photos already in\n * the site's media library and whether to list it before it has a price.\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 /**\n * One price for every variant, in dollars. Absent is the default price\n * (AGL-3676); `null` leaves them unpriced on purpose.\n */\n priceUsd?: number | null\n /** Ids of the site's product categories; one the site lacks is dropped. */\n categoryIds?: string[]\n /**\n * List the product on the storefront before it has a price (AGL-3676):\n * written `active`, so the catalog shows it and its page answers, while\n * every sale door still refuses a variant with no price\n * (`variantHasPrice`) and the storefront says \"Price coming soon\" where a\n * price would be. Absent or false, an unpriced product stays a draft.\n */\n comingSoon?: boolean\n /**\n * Photos already in the site's media library, first = primary (AGL-3676):\n * the slot a guided start fills with a stock photo, or a starter, for the\n * merchant to replace with their own.\n */\n mediaUrls?: string[]\n}\n\n/** The most photos a draft is written with. */\nexport const PRODUCT_DRAFT_MEDIA_MAX = 4\n\n/** A photo address a draft keeps: an https URL or a path on the site, never a script or a data URL. */\nconst MEDIA_URL = /^(?:https:\\/\\/[^\\s\"'<>]+|\\/[^\\s\"'<>]*)$/\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 for every variant (stated, else the default), or `null` for the merchant to set. */\n priceUsd: number | null\n categoryIds: string[]\n /** Listed before it has a price (`ProductDraftContent.comingSoon`). */\n comingSoon: boolean\n mediaUrls: 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 // The stated price, else the default (AGL-3676); `null` asks for none.\n const rawPrice = content['priceUsd']\n let priceUsd: number | null = rawPrice === null ? null : COMMERCE_DEFAULT_PRICE_USD\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 const rawComingSoon = content['comingSoon']\n if (rawComingSoon !== undefined && typeof rawComingSoon !== 'boolean') {\n problems.push('Coming soon is true or false')\n }\n const mediaUrls = strings('Photos', content['mediaUrls'], problems)\n if (mediaUrls.length > PRODUCT_DRAFT_MEDIA_MAX) {\n problems.push(`A product is written with at most ${PRODUCT_DRAFT_MEDIA_MAX} photos`)\n }\n if (mediaUrls.some((url) => url.length > 2_000 || !MEDIA_URL.test(url))) {\n problems.push('A photo is an https address or a path on this site')\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 comingSoon: rawComingSoon === true,\n mediaUrls,\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 // Listed before it has a price (AGL-3676): the storefront says \"Price\n // coming soon\", and no sale door sells a variant without one.\n ...(read.comingSoon ? { status: 'active' as const } : {}),\n ...(read.mediaUrls.length ? { mediaUrls: read.mediaUrls } : {}),\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 countLiveDocuments(hostRef.collection('products'))\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 countLiveDocuments(products, (aggregate) => tx.get(aggregate))\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","countLiveDocuments","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","COMMERCE_DEFAULT_PRICE_USD","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_DRAFT_MEDIA_MAX","MEDIA_URL","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","rawComingSoon","mediaUrls","url","test","ok","proposal","type","comingSoon","productDraftDocument","read","slug","nowMs","draft","priced","variants","variant","status","productProblem","product","factsOf","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","aggregate","room","categories","smart","query","resolve","category","now","getTime","rules","matchAll","create","collectionIds","deletedAt","createdAt","updatedAt","createdBy","productDraftWriter","registerProductDraftWriter","pluginId"],"mappings":"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"}
|
|
@@ -15,6 +15,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
|
+
import { countLiveDocuments } from "@aglyn/aglyn/app-utils/live-document-count";
|
|
18
19
|
import { firebaseAdmin, getOrgForHost, logHostActivity } from "@aglyn/tenant-data-admin";
|
|
19
20
|
import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
|
|
20
21
|
import { commerceSlug, liftLegacyProduct, validateProduct } from "../model/commerce.js";
|
|
@@ -224,7 +225,8 @@ export async function upsertSourcedProduct(write, firestore = firebaseAdmin.app(
|
|
|
224
225
|
const ref = products.doc(productId);
|
|
225
226
|
await firestore.runTransaction(async (tx)=>{
|
|
226
227
|
var _write_actorUid;
|
|
227
|
-
|
|
228
|
+
// Live products only: a deleted one frees its slot, an archived one keeps it (AGL-3918).
|
|
229
|
+
const used = await countLiveDocuments(products, (aggregate)=>tx.get(aggregate));
|
|
228
230
|
const quota = Aglyn.checkQuota(org, 'productsPerHost', Number(used) || 0);
|
|
229
231
|
if (!quota.allowed) throw new WriteRefusal({
|
|
230
232
|
outcome: 'plan_limit',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-writer.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 * as Aglyn from '@aglyn/aglyn/server'\nimport type {\n PluginProductWriter,\n SourcedProductWrite,\n SourcedProductWriteOutcome,\n} from '@aglyn/aglyn/plugin-manager/plugin-product-writer'\nimport type { TransferResourceContext } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { firebaseAdmin, getOrgForHost, logHostActivity } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport {\n commerceSlug,\n liftLegacyProduct,\n validateProduct,\n type HostProduct,\n type ProductVariant,\n} from '../model/commerce'\nimport { derivedKeys, readSmartCollections } from '../transfer/products.server'\nimport { withoutUndefined } from '../transfer/server-common'\n\n/**\n * Products another plugin brings from a source of its own — a print-on-demand\n * service, a supplier — written through the store's own rules (AGL-3641),\n * behind core's `core.product-writer`.\n *\n * The same rules the console's product route and the file import keep: a\n * create is counted against the plan's product allowance in the transaction\n * that makes it, a new product takes an address no other product holds (a\n * deleted one's included), every write carries the search, stock and\n * smart-collection keys every writer derives, and the site's activity says\n * what happened.\n *\n * A configuration the source cannot make now is sold out (`inventory: 0`);\n * one it can is untracked (`inventory: null`), because a made-to-order item\n * has no count to run down.\n */\n\ntype ProductDoc = Partial<HostProduct> & Record<string, unknown>\n\n/** The fields an update compares to tell a change from a repeat. */\nconst COMPARED_KEYS = ['name', 'description', 'mediaUrls', 'tags', 'options', 'variants'] as const\n\n/** How many suffixed addresses a new product tries before it gives up. */\nconst SLUG_TRIES = 30\nconst SLUGS_PER_QUERY = 30\n\nconst newVariantId = () => `v${Aglyn.createResourceUid().slice(0, 10)}`\n\nfunction priceUsd(minor: number): number {\n return Math.round(Math.max(0, Number(minor) || 0)) / 100\n}\n\n/** The variant a sourced configuration becomes, on top of the one it was. */\nfunction variantFrom(\n source: SourcedProductWrite['product']['variants'][number],\n existing: ProductVariant | undefined,\n options: { prices: boolean; content: boolean },\n): ProductVariant {\n const price = existing && !options.prices ? existing.priceUsd : priceUsd(source.priceMinor)\n const next: ProductVariant = {\n ...(existing ?? {}),\n id: existing?.id ?? newVariantId(),\n options: { ...source.options },\n priceUsd: price,\n inventory: source.available ? null : 0,\n }\n delete next.inventoryByLocation\n if (source.sku) next.sku = source.sku.slice(0, 120)\n else delete next.sku\n if (Number.isFinite(source.weightGrams) && Number(source.weightGrams) > 0) {\n next.weightGrams = Math.round(Number(source.weightGrams))\n }\n if (source.imageUrl && (options.content || !existing?.imageUrl)) next.imageUrl = source.imageUrl\n // A price brought below a compare-at that no longer exceeds it would be\n // refused: the strike-through goes, the price stays.\n if (next.compareAtPriceUsd != null && Number(next.compareAtPriceUsd) <= next.priceUsd) {\n delete next.compareAtPriceUsd\n }\n return next\n}\n\nasync function freeSlug(products: FirebaseFirestore.CollectionReference, name: string): Promise<string | null> {\n const base = commerceSlug(name).slice(0, 80) || 'product'\n const candidates = [base, ...Array.from({ length: SLUG_TRIES - 1 }, (_, index) => `${base}-${index + 2}`)]\n const taken = new Set<string>()\n for (let at = 0; at < candidates.length; at += SLUGS_PER_QUERY) {\n const slice = candidates.slice(at, at + SLUGS_PER_QUERY)\n for (const doc of (await products.where('slug', 'in', slice).get()).docs) taken.add(String(doc.get('slug')))\n }\n return candidates.find((candidate) => !taken.has(candidate)) ?? null\n}\n\nclass WriteRefusal extends Error {\n constructor(readonly outcome: Exclude<SourcedProductWriteOutcome, { productId: string }>) {\n super(outcome.outcome)\n }\n}\n\nexport async function upsertSourcedProduct(\n write: SourcedProductWrite,\n firestore: FirebaseFirestore.Firestore = firebaseAdmin.app().firestore(),\n): Promise<SourcedProductWriteOutcome> {\n const { hostId, product: source } = write\n if (!isDocumentId(hostId)) return { outcome: 'no_store' }\n const owner = await getOrgForHost(hostId)\n if (!owner || !Aglyn.checkEntitlement(owner.org as never, 'commerce')) return { outcome: 'no_store' }\n const org = owner.org\n const products = firestore.collection('hosts').doc(hostId).collection('products')\n const smart = await readSmartCollections(firestore, { hostId } as TransferResourceContext)\n const { Timestamp } = firebaseAdmin.firestore\n const actor = { uid: write.actorUid ?? 'import', email: null }\n const name = source.name.trim().slice(0, 200)\n const description = (source.description ?? '').trim().slice(0, 20_000)\n\n const mapping = (product: HostProduct) =>\n source.variants.map((variant, index) => ({ key: variant.key, variantId: product.variants[index].id }))\n\n try {\n // An update, when the product is still there.\n if (write.productId && isDocumentId(write.productId)) {\n const ref = products.doc(write.productId)\n const updated = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n const data = snapshot.data() as ProductDoc | undefined\n if (!snapshot.exists || (data?.['deletedAt'] !== null && data?.['deletedAt'] !== undefined)) return null\n const current = liftLegacyProduct(data as ProductDoc)\n const flags = { prices: write.prices === true, content: write.content === true }\n const byId = new Map(current.variants.map((variant) => [variant.id, variant]))\n const next: HostProduct = {\n ...current,\n options: source.options.map((option) => ({ name: option.name, values: [...option.values] })),\n variants: source.variants.map((variant) =>\n variantFrom(variant, variant.variantId ? byId.get(variant.variantId) : undefined, flags),\n ),\n ...(flags.content\n ? {\n name,\n description,\n mediaUrls: [...source.mediaUrls],\n ...(source.tags ? { tags: [...source.tags] } : {}),\n }\n : {}),\n }\n const invalid = validateProduct(next)\n if (invalid) throw new WriteRefusal({ outcome: 'invalid', message: invalid })\n const changed = COMPARED_KEYS.some(\n (key) =>\n JSON.stringify((next as unknown as Record<string, unknown>)[key] ?? null) !==\n JSON.stringify((current as unknown as Record<string, unknown>)[key] ?? null),\n )\n if (changed) {\n tx.update(\n ref,\n withoutUndefined({\n ...Object.fromEntries(COMPARED_KEYS.map((key) => [key, (next as unknown as Record<string, unknown>)[key]])),\n ...derivedKeys(next, smart),\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n }) as FirebaseFirestore.UpdateData<FirebaseFirestore.DocumentData>,\n )\n }\n return { next, changed }\n })\n if (updated) {\n if (updated.changed) {\n await logHostActivity(hostId, actor, 'Updated product from its source', {\n type: 'content',\n id: write.productId,\n name: updated.next.name,\n })\n }\n return {\n outcome: updated.changed ? 'updated' : 'unchanged',\n productId: write.productId,\n variants: mapping(updated.next),\n }\n }\n }\n\n if (write.productId && write.recreate === false) return { outcome: 'missing' }\n\n // A new product.\n const slug = await freeSlug(products, name)\n if (!slug) return { outcome: 'invalid', message: 'Every address for a product of this name is taken. Rename one first.' }\n const productId = Aglyn.createResourceUid()\n const product: HostProduct = {\n name,\n slug,\n description,\n type: 'physical',\n status: write.status === 'active' ? 'active' : 'draft',\n mediaUrls: [...source.mediaUrls],\n ...(source.tags?.length ? { tags: [...source.tags] } : {}),\n options: source.options.map((option) => ({ name: option.name, values: [...option.values] })),\n variants: source.variants.map((variant) => variantFrom(variant, undefined, { prices: true, content: true })),\n oversellPolicy: 'deny',\n }\n const invalid = validateProduct(product)\n if (invalid) return { outcome: 'invalid', message: invalid }\n const now = Date.now()\n const ref = products.doc(productId)\n await firestore.runTransaction(async (tx) => {\n const used = (await tx.get(products.count())).data().count\n const quota = Aglyn.checkQuota(org as never, 'productsPerHost', Number(used) || 0)\n if (!quota.allowed) throw new WriteRefusal({ outcome: 'plan_limit', limit: Number(quota.limit) || 0 })\n tx.create(\n ref,\n withoutUndefined({\n ...product,\n ...derivedKeys(product, smart),\n createdAtMs: now,\n updatedAtMs: now,\n deletedAt: null,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n createdBy: write.actorUid ?? null,\n }),\n )\n })\n await logHostActivity(hostId, actor, 'Imported product', { type: 'content', id: productId, name: product.name })\n return { outcome: 'created', productId, variants: mapping(product) }\n } catch (error) {\n if (error instanceof WriteRefusal) return error.outcome\n throw error\n }\n}\n\nexport const commerceProductWriter: PluginProductWriter = {\n upsertSourced: (write) => upsertSourcedProduct(write),\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","logHostActivity","isDocumentId","commerceSlug","liftLegacyProduct","validateProduct","derivedKeys","readSmartCollections","withoutUndefined","COMPARED_KEYS","SLUG_TRIES","SLUGS_PER_QUERY","newVariantId","createResourceUid","slice","priceUsd","minor","Math","round","max","Number","variantFrom","source","existing","options","price","prices","priceMinor","next","id","inventory","available","inventoryByLocation","sku","isFinite","weightGrams","imageUrl","content","compareAtPriceUsd","freeSlug","products","name","candidates","base","Array","from","length","_","index","taken","Set","at","doc","where","get","docs","add","String","find","candidate","has","WriteRefusal","Error","outcome","upsertSourcedProduct","write","firestore","app","hostId","product","owner","checkEntitlement","org","collection","smart","Timestamp","actor","uid","actorUid","email","trim","description","mapping","variants","map","variant","key","variantId","productId","ref","updated","runTransaction","tx","snapshot","data","exists","undefined","current","flags","byId","Map","option","values","mediaUrls","tags","invalid","message","changed","some","JSON","stringify","update","Object","fromEntries","updatedAtMs","Date","now","updatedAt","type","recreate","slug","status","oversellPolicy","used","count","quota","checkQuota","allowed","limit","create","createdAtMs","deletedAt","createdAt","createdBy","error","commerceProductWriter","upsertSourced"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-writer.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 * as Aglyn from '@aglyn/aglyn/server'\nimport { countLiveDocuments } from '@aglyn/aglyn/app-utils/live-document-count'\nimport type {\n PluginProductWriter,\n SourcedProductWrite,\n SourcedProductWriteOutcome,\n} from '@aglyn/aglyn/plugin-manager/plugin-product-writer'\nimport type { TransferResourceContext } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { firebaseAdmin, getOrgForHost, logHostActivity } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport {\n commerceSlug,\n liftLegacyProduct,\n validateProduct,\n type HostProduct,\n type ProductVariant,\n} from '../model/commerce'\nimport { derivedKeys, readSmartCollections } from '../transfer/products.server'\nimport { withoutUndefined } from '../transfer/server-common'\n\n/**\n * Products another plugin brings from a source of its own — a print-on-demand\n * service, a supplier — written through the store's own rules (AGL-3641),\n * behind core's `core.product-writer`.\n *\n * The same rules the console's product route and the file import keep: a\n * create is counted against the plan's product allowance in the transaction\n * that makes it, a new product takes an address no other product holds (a\n * deleted one's included), every write carries the search, stock and\n * smart-collection keys every writer derives, and the site's activity says\n * what happened.\n *\n * A configuration the source cannot make now is sold out (`inventory: 0`);\n * one it can is untracked (`inventory: null`), because a made-to-order item\n * has no count to run down.\n */\n\ntype ProductDoc = Partial<HostProduct> & Record<string, unknown>\n\n/** The fields an update compares to tell a change from a repeat. */\nconst COMPARED_KEYS = ['name', 'description', 'mediaUrls', 'tags', 'options', 'variants'] as const\n\n/** How many suffixed addresses a new product tries before it gives up. */\nconst SLUG_TRIES = 30\nconst SLUGS_PER_QUERY = 30\n\nconst newVariantId = () => `v${Aglyn.createResourceUid().slice(0, 10)}`\n\nfunction priceUsd(minor: number): number {\n return Math.round(Math.max(0, Number(minor) || 0)) / 100\n}\n\n/** The variant a sourced configuration becomes, on top of the one it was. */\nfunction variantFrom(\n source: SourcedProductWrite['product']['variants'][number],\n existing: ProductVariant | undefined,\n options: { prices: boolean; content: boolean },\n): ProductVariant {\n const price = existing && !options.prices ? existing.priceUsd : priceUsd(source.priceMinor)\n const next: ProductVariant = {\n ...(existing ?? {}),\n id: existing?.id ?? newVariantId(),\n options: { ...source.options },\n priceUsd: price,\n inventory: source.available ? null : 0,\n }\n delete next.inventoryByLocation\n if (source.sku) next.sku = source.sku.slice(0, 120)\n else delete next.sku\n if (Number.isFinite(source.weightGrams) && Number(source.weightGrams) > 0) {\n next.weightGrams = Math.round(Number(source.weightGrams))\n }\n if (source.imageUrl && (options.content || !existing?.imageUrl)) next.imageUrl = source.imageUrl\n // A price brought below a compare-at that no longer exceeds it would be\n // refused: the strike-through goes, the price stays.\n if (next.compareAtPriceUsd != null && Number(next.compareAtPriceUsd) <= next.priceUsd) {\n delete next.compareAtPriceUsd\n }\n return next\n}\n\nasync function freeSlug(products: FirebaseFirestore.CollectionReference, name: string): Promise<string | null> {\n const base = commerceSlug(name).slice(0, 80) || 'product'\n const candidates = [base, ...Array.from({ length: SLUG_TRIES - 1 }, (_, index) => `${base}-${index + 2}`)]\n const taken = new Set<string>()\n for (let at = 0; at < candidates.length; at += SLUGS_PER_QUERY) {\n const slice = candidates.slice(at, at + SLUGS_PER_QUERY)\n for (const doc of (await products.where('slug', 'in', slice).get()).docs) taken.add(String(doc.get('slug')))\n }\n return candidates.find((candidate) => !taken.has(candidate)) ?? null\n}\n\nclass WriteRefusal extends Error {\n constructor(readonly outcome: Exclude<SourcedProductWriteOutcome, { productId: string }>) {\n super(outcome.outcome)\n }\n}\n\nexport async function upsertSourcedProduct(\n write: SourcedProductWrite,\n firestore: FirebaseFirestore.Firestore = firebaseAdmin.app().firestore(),\n): Promise<SourcedProductWriteOutcome> {\n const { hostId, product: source } = write\n if (!isDocumentId(hostId)) return { outcome: 'no_store' }\n const owner = await getOrgForHost(hostId)\n if (!owner || !Aglyn.checkEntitlement(owner.org as never, 'commerce')) return { outcome: 'no_store' }\n const org = owner.org\n const products = firestore.collection('hosts').doc(hostId).collection('products')\n const smart = await readSmartCollections(firestore, { hostId } as TransferResourceContext)\n const { Timestamp } = firebaseAdmin.firestore\n const actor = { uid: write.actorUid ?? 'import', email: null }\n const name = source.name.trim().slice(0, 200)\n const description = (source.description ?? '').trim().slice(0, 20_000)\n\n const mapping = (product: HostProduct) =>\n source.variants.map((variant, index) => ({ key: variant.key, variantId: product.variants[index].id }))\n\n try {\n // An update, when the product is still there.\n if (write.productId && isDocumentId(write.productId)) {\n const ref = products.doc(write.productId)\n const updated = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n const data = snapshot.data() as ProductDoc | undefined\n if (!snapshot.exists || (data?.['deletedAt'] !== null && data?.['deletedAt'] !== undefined)) return null\n const current = liftLegacyProduct(data as ProductDoc)\n const flags = { prices: write.prices === true, content: write.content === true }\n const byId = new Map(current.variants.map((variant) => [variant.id, variant]))\n const next: HostProduct = {\n ...current,\n options: source.options.map((option) => ({ name: option.name, values: [...option.values] })),\n variants: source.variants.map((variant) =>\n variantFrom(variant, variant.variantId ? byId.get(variant.variantId) : undefined, flags),\n ),\n ...(flags.content\n ? {\n name,\n description,\n mediaUrls: [...source.mediaUrls],\n ...(source.tags ? { tags: [...source.tags] } : {}),\n }\n : {}),\n }\n const invalid = validateProduct(next)\n if (invalid) throw new WriteRefusal({ outcome: 'invalid', message: invalid })\n const changed = COMPARED_KEYS.some(\n (key) =>\n JSON.stringify((next as unknown as Record<string, unknown>)[key] ?? null) !==\n JSON.stringify((current as unknown as Record<string, unknown>)[key] ?? null),\n )\n if (changed) {\n tx.update(\n ref,\n withoutUndefined({\n ...Object.fromEntries(COMPARED_KEYS.map((key) => [key, (next as unknown as Record<string, unknown>)[key]])),\n ...derivedKeys(next, smart),\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n }) as FirebaseFirestore.UpdateData<FirebaseFirestore.DocumentData>,\n )\n }\n return { next, changed }\n })\n if (updated) {\n if (updated.changed) {\n await logHostActivity(hostId, actor, 'Updated product from its source', {\n type: 'content',\n id: write.productId,\n name: updated.next.name,\n })\n }\n return {\n outcome: updated.changed ? 'updated' : 'unchanged',\n productId: write.productId,\n variants: mapping(updated.next),\n }\n }\n }\n\n if (write.productId && write.recreate === false) return { outcome: 'missing' }\n\n // A new product.\n const slug = await freeSlug(products, name)\n if (!slug) return { outcome: 'invalid', message: 'Every address for a product of this name is taken. Rename one first.' }\n const productId = Aglyn.createResourceUid()\n const product: HostProduct = {\n name,\n slug,\n description,\n type: 'physical',\n status: write.status === 'active' ? 'active' : 'draft',\n mediaUrls: [...source.mediaUrls],\n ...(source.tags?.length ? { tags: [...source.tags] } : {}),\n options: source.options.map((option) => ({ name: option.name, values: [...option.values] })),\n variants: source.variants.map((variant) => variantFrom(variant, undefined, { prices: true, content: true })),\n oversellPolicy: 'deny',\n }\n const invalid = validateProduct(product)\n if (invalid) return { outcome: 'invalid', message: invalid }\n const now = Date.now()\n const ref = products.doc(productId)\n await firestore.runTransaction(async (tx) => {\n // Live products only: a deleted one frees its slot, an archived one keeps it (AGL-3918).\n const used = await countLiveDocuments(products, (aggregate) => tx.get(aggregate))\n const quota = Aglyn.checkQuota(org as never, 'productsPerHost', Number(used) || 0)\n if (!quota.allowed) throw new WriteRefusal({ outcome: 'plan_limit', limit: Number(quota.limit) || 0 })\n tx.create(\n ref,\n withoutUndefined({\n ...product,\n ...derivedKeys(product, smart),\n createdAtMs: now,\n updatedAtMs: now,\n deletedAt: null,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n createdBy: write.actorUid ?? null,\n }),\n )\n })\n await logHostActivity(hostId, actor, 'Imported product', { type: 'content', id: productId, name: product.name })\n return { outcome: 'created', productId, variants: mapping(product) }\n } catch (error) {\n if (error instanceof WriteRefusal) return error.outcome\n throw error\n }\n}\n\nexport const commerceProductWriter: PluginProductWriter = {\n upsertSourced: (write) => upsertSourcedProduct(write),\n}\n"],"names":["Aglyn","countLiveDocuments","firebaseAdmin","getOrgForHost","logHostActivity","isDocumentId","commerceSlug","liftLegacyProduct","validateProduct","derivedKeys","readSmartCollections","withoutUndefined","COMPARED_KEYS","SLUG_TRIES","SLUGS_PER_QUERY","newVariantId","createResourceUid","slice","priceUsd","minor","Math","round","max","Number","variantFrom","source","existing","options","price","prices","priceMinor","next","id","inventory","available","inventoryByLocation","sku","isFinite","weightGrams","imageUrl","content","compareAtPriceUsd","freeSlug","products","name","candidates","base","Array","from","length","_","index","taken","Set","at","doc","where","get","docs","add","String","find","candidate","has","WriteRefusal","Error","outcome","upsertSourcedProduct","write","firestore","app","hostId","product","owner","checkEntitlement","org","collection","smart","Timestamp","actor","uid","actorUid","email","trim","description","mapping","variants","map","variant","key","variantId","productId","ref","updated","runTransaction","tx","snapshot","data","exists","undefined","current","flags","byId","Map","option","values","mediaUrls","tags","invalid","message","changed","some","JSON","stringify","update","Object","fromEntries","updatedAtMs","Date","now","updatedAt","type","recreate","slug","status","oversellPolicy","used","aggregate","quota","checkQuota","allowed","limit","create","createdAtMs","deletedAt","createdAt","createdBy","error","commerceProductWriter","upsertSourced"],"mappings":"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"}
|
|
@@ -18,6 +18,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
18
18
|
import { isRefusedIdToken } from "@aglyn/tenant-data-admin/server/id-token-refusal";
|
|
19
19
|
import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
|
|
20
20
|
import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
|
|
21
|
+
import { countLiveDocuments } from "@aglyn/aglyn/app-utils/live-document-count";
|
|
21
22
|
import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
|
|
22
23
|
import { firebaseAdmin, getLockdownVerdict, getOrgForHost, isImpersonationSession } from "@aglyn/tenant-data-admin";
|
|
23
24
|
import { FieldValue } from "firebase-admin/firestore";
|
|
@@ -199,7 +200,7 @@ export function resolved(deps) {
|
|
|
199
200
|
error: early.error
|
|
200
201
|
}
|
|
201
202
|
};
|
|
202
|
-
const used =
|
|
203
|
+
const used = await countLiveDocuments(products, (aggregate)=>tx.get(aggregate));
|
|
203
204
|
const room = roomRefusal(door.org, Number(used) || 0);
|
|
204
205
|
if (room) return {
|
|
205
206
|
status: room.status,
|
|
@@ -447,7 +448,7 @@ export function resolved(deps) {
|
|
|
447
448
|
error: early.error
|
|
448
449
|
}
|
|
449
450
|
};
|
|
450
|
-
const used =
|
|
451
|
+
const used = await countLiveDocuments(products, (aggregate)=>tx.get(aggregate));
|
|
451
452
|
const room = roomRefusal(door.org, Number(used) || 0);
|
|
452
453
|
if (room) return {
|
|
453
454
|
status: room.status,
|