@aglyn/plugins-commerce 1.0.0-beta.261 → 1.0.0-beta.262

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/console/order-detail-dialog.component.js +17 -45
  3. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  4. package/src/lib/components/console/order-fulfillment-panel.component.js +12 -25
  5. package/src/lib/components/console/order-fulfillment-panel.component.js.map +1 -1
  6. package/src/lib/components/console/order-invoice.component.js +2 -10
  7. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  8. package/src/lib/components/console/order-shipping-actions.component.d.ts +1 -15
  9. package/src/lib/components/console/order-shipping-actions.component.js +3 -23
  10. package/src/lib/components/console/order-shipping-actions.component.js.map +1 -1
  11. package/src/lib/components/console/product-labels-dialog.component.d.ts +2 -5
  12. package/src/lib/components/console/product-labels-dialog.component.js +6 -24
  13. package/src/lib/components/console/product-labels-dialog.component.js.map +1 -1
  14. package/src/lib/components/console/products-hub-card.component.js +7 -20
  15. package/src/lib/components/console/products-hub-card.component.js.map +1 -1
  16. package/src/lib/model/commerce-shipping.d.ts +2 -0
  17. package/src/lib/model/commerce-shipping.js +1 -0
  18. package/src/lib/model/commerce-shipping.js.map +1 -1
  19. package/src/lib/model/order-dialog.d.ts +52 -0
  20. package/src/lib/model/order-dialog.js +85 -0
  21. package/src/lib/model/order-dialog.js.map +1 -0
  22. package/src/lib/model/product-write.d.ts +71 -0
  23. package/src/lib/model/product-write.js +28 -0
  24. package/src/lib/model/product-write.js.map +1 -1
  25. package/src/lib/printing/product-labels.d.ts +22 -0
  26. package/src/lib/printing/product-labels.js +39 -2
  27. package/src/lib/printing/product-labels.js.map +1 -1
  28. package/src/lib/server/order-figures.js +31 -0
  29. package/src/lib/server/order-figures.js.map +1 -1
  30. package/src/lib/server/order-print.d.ts +40 -0
  31. package/src/lib/server/order-print.js +113 -0
  32. package/src/lib/server/order-print.js.map +1 -0
  33. package/src/lib/server/products-write.d.ts +26 -0
  34. package/src/lib/server/products-write.js +184 -1
  35. package/src/lib/server/products-write.js.map +1 -1
  36. package/src/lib/server.js +8 -0
  37. package/src/lib/server.js.map +1 -1
  38. package/src/lib/transfer/shipping-presets.d.ts +16 -0
  39. package/src/lib/transfer/shipping-presets.js +34 -1
  40. package/src/lib/transfer/shipping-presets.js.map +1 -1
  41. package/src/lib/utils/order-invoice.d.ts +5 -0
  42. package/src/lib/utils/order-invoice.js +14 -0
  43. package/src/lib/utils/order-invoice.js.map +1 -1
  44. package/src/lib/utils/order-packing-slip.d.ts +31 -0
  45. package/src/lib/utils/order-packing-slip.js +46 -0
  46. package/src/lib/utils/order-packing-slip.js.map +1 -0
@@ -21,7 +21,7 @@ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
21
21
  import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
22
22
  import { firebaseAdmin, getLockdownVerdict, getOrgForHost, isImpersonationSession } from "@aglyn/tenant-data-admin";
23
23
  import { FieldValue } from "firebase-admin/firestore";
24
- import { productCollectionIds, productEditWrite, productMembershipInput, productSaveFields, smartCollectionRulesOf, stockAdjustmentWrite, validateProduct } from "../model/index.js";
24
+ import { liftLegacyProduct, productCollectionIds, productDuplicateFields, productDuplicateName, productDuplicateSlugBase, productEditWrite, productMembershipInput, productSaveFields, smartCollectionRulesOf, stockAdjustmentWrite, validateProduct } from "../model/index.js";
25
25
  import { planRefusal, roomRefusal } from "./product-drafts.js";
26
26
  export const ROLE_REFUSAL = 'Editing requires the editor role';
27
27
  const MAX_BODY_BYTES = 256000;
@@ -369,7 +369,190 @@ export function resolved(deps) {
369
369
  }
370
370
  };
371
371
  }
372
+ /**
373
+ * The first slug in `base`, `base-2`, `base-3`… no product of the site holds,
374
+ * as the console's slug ledger claims one (`product-slugs.ts`): a second
375
+ * Duplicate of the same product does not mint a second `-copy` (AGL-3321).
376
+ */ export async function claimProductSlug(products, base) {
377
+ for(let suffix = 1; suffix < 1000; suffix += 1){
378
+ const candidate = suffix < 2 ? base : `${base}-${suffix}`;
379
+ const held = await products.where('slug', '==', candidate).limit(1).get();
380
+ if (held.empty) return candidate;
381
+ }
382
+ return `${base}-${Date.now()}`;
383
+ }
384
+ /**
385
+ * `POST /api/commerce/products/duplicate` (AGL-3841): the products list's
386
+ * Duplicate for the native apps. The console builds the copy in the browser
387
+ * (`productDuplicateFields`, its slug ledger, the copy's smart collections)
388
+ * and creates it through the quota-enforcing resources route; this runs the
389
+ * same model over the stored product and creates it under the save route's
390
+ * create door (the plan's commerce entitlement and its product quota, counted
391
+ * in the same transaction). `newProductId` is the client's, so a retry finds
392
+ * its own copy and answers `replayed`.
393
+ */ export function createProductDuplicateHandler(options = {}) {
394
+ return async (req, res)=>{
395
+ if (req.method !== 'POST') return res.status(405).json({
396
+ error: 'Method not allowed'
397
+ });
398
+ const deps = resolved(options);
399
+ const body = bodyOf(req);
400
+ const { hostId, productId, newProductId } = body;
401
+ if (!validId(hostId) || !validId(productId) || !validId(newProductId) || productId === newProductId) {
402
+ return res.status(400).json({
403
+ error: 'Missing site or product'
404
+ });
405
+ }
406
+ try {
407
+ var _source_data;
408
+ const door = await gate(req, res, hostId, deps);
409
+ if (!door) return;
410
+ const db = deps.firestore();
411
+ const hostRef = db.collection('hosts').doc(hostId);
412
+ const products = hostRef.collection('products');
413
+ const copyRef = products.doc(newProductId);
414
+ const already = await copyRef.get();
415
+ if (already.exists) return res.status(200).json({
416
+ ok: true,
417
+ id: newProductId,
418
+ replayed: true
419
+ });
420
+ const source = await products.doc(productId).get();
421
+ if (!source.exists || source.get('deletedAt')) return res.status(404).json({
422
+ error: 'This product was deleted'
423
+ });
424
+ const product = liftLegacyProduct((_source_data = source.data()) != null ? _source_data : {});
425
+ const slug = await claimProductSlug(products, productDuplicateSlugBase(product.slug, product.name));
426
+ const nowMs = deps.now();
427
+ const fields = productDuplicateFields(product, slug, nowMs);
428
+ const smartQuery = hostRef.collection('collections').where('kind', '==', 'catalog').where('mode', '==', 'smart');
429
+ const outbox = outboxEntry(db, hostId);
430
+ const answer = await db.runTransaction(async (tx)=>{
431
+ const [existing, smart] = await Promise.all([
432
+ tx.get(copyRef),
433
+ tx.get(smartQuery)
434
+ ]);
435
+ if (existing.exists) return {
436
+ status: 200,
437
+ body: {
438
+ ok: true,
439
+ id: newProductId,
440
+ replayed: true
441
+ }
442
+ };
443
+ const early = planRefusal(door.org);
444
+ if (early) return {
445
+ status: early.status,
446
+ body: {
447
+ error: early.error
448
+ }
449
+ };
450
+ const used = (await tx.get(products.count())).data().count;
451
+ const room = roomRefusal(door.org, Number(used) || 0);
452
+ if (room) return {
453
+ status: room.status,
454
+ body: {
455
+ error: room.error
456
+ }
457
+ };
458
+ // The COPY's smart collections: its name differs from the source's.
459
+ const collectionIds = productCollectionIds(productMembershipInput({
460
+ name: productDuplicateName(product.name)
461
+ }, product), smart.docs.map((doc)=>smartCollectionRulesOf(doc.id, doc.data())));
462
+ tx.create(copyRef, _extends({}, fields, {
463
+ collectionIds,
464
+ deletedAt: null,
465
+ createdAt: FieldValue.serverTimestamp(),
466
+ updatedAt: FieldValue.serverTimestamp(),
467
+ createdBy: door.uid
468
+ }));
469
+ tx.set(outbox.ref, outbox.data);
470
+ return {
471
+ status: 200,
472
+ body: {
473
+ ok: true,
474
+ id: newProductId,
475
+ replayed: false,
476
+ slug
477
+ }
478
+ };
479
+ });
480
+ if (answer.status === 200 && !answer.body.replayed) {
481
+ await settleCache(deps, hostId, outbox.ref, 'product duplicated from the app');
482
+ }
483
+ return res.status(answer.status).json(answer.body);
484
+ } catch (error) {
485
+ console.error('[commerce/products/duplicate]', error);
486
+ return res.status(500).json({
487
+ error: 'Could not duplicate product'
488
+ });
489
+ }
490
+ };
491
+ }
492
+ /**
493
+ * `POST /api/commerce/products/delete` (AGL-3841): the products list's Delete
494
+ * for the native apps, the console's write (`deletedAt` on the product, which
495
+ * stops it being purchasable) with the site-wide cache drop, under the save
496
+ * route's gate. Deleting a deleted product answers `already`.
497
+ */ export function createProductDeleteHandler(options = {}) {
498
+ return async (req, res)=>{
499
+ if (req.method !== 'POST') return res.status(405).json({
500
+ error: 'Method not allowed'
501
+ });
502
+ const deps = resolved(options);
503
+ const body = bodyOf(req);
504
+ const { hostId, productId } = body;
505
+ if (!validId(hostId) || !validId(productId)) return res.status(400).json({
506
+ error: 'Missing site or product'
507
+ });
508
+ try {
509
+ const door = await gate(req, res, hostId, deps);
510
+ if (!door) return;
511
+ const db = deps.firestore();
512
+ const productRef = db.collection('hosts').doc(hostId).collection('products').doc(productId);
513
+ const outbox = outboxEntry(db, hostId);
514
+ const answer = await db.runTransaction(async (tx)=>{
515
+ const stored = await tx.get(productRef);
516
+ if (!stored.exists) return {
517
+ status: 404,
518
+ body: {
519
+ error: 'This product no longer exists'
520
+ }
521
+ };
522
+ if (stored.get('deletedAt')) return {
523
+ status: 200,
524
+ body: {
525
+ ok: true,
526
+ already: true
527
+ }
528
+ };
529
+ tx.update(productRef, {
530
+ deletedAt: FieldValue.serverTimestamp()
531
+ });
532
+ tx.set(outbox.ref, outbox.data);
533
+ return {
534
+ status: 200,
535
+ body: {
536
+ ok: true,
537
+ already: false
538
+ }
539
+ };
540
+ });
541
+ if (answer.status === 200 && !answer.body.already) {
542
+ await settleCache(deps, hostId, outbox.ref, 'product deleted from the app');
543
+ }
544
+ return res.status(answer.status).json(answer.body);
545
+ } catch (error) {
546
+ console.error('[commerce/products/delete]', error);
547
+ return res.status(500).json({
548
+ error: 'Could not delete product'
549
+ });
550
+ }
551
+ };
552
+ }
372
553
  export const productSaveHandler = createProductSaveHandler();
373
554
  export const productStockHandler = createProductStockHandler();
555
+ export const productDuplicateHandler = createProductDuplicateHandler();
556
+ export const productDeleteHandler = createProductDeleteHandler();
374
557
 
375
558
  //# sourceMappingURL=products-write.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/products-write.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 { createHash } from 'node:crypto'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getLockdownVerdict, getOrgForHost, isImpersonationSession } from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n type EditedProduct,\n productCollectionIds,\n productEditWrite,\n productMembershipInput,\n productSaveFields,\n smartCollectionRulesOf,\n stockAdjustmentWrite,\n validateProduct,\n type InventoryAdjustmentReason,\n} from '../model'\nimport { planRefusal, roomRefusal } from './product-drafts'\n\n/*\n * `POST /api/commerce/products/save` and `POST /api/commerce/products/stock`\n * (AGL-3652): the console product editor's save and its Adjust stock dialog,\n * for the native apps.\n *\n * The console makes both writes from the browser, computing derived fields\n * first (search keys, stock verdict, price, image, smart-collection\n * membership). These routes run THAT computation (`model/product-write.ts`,\n * which the console imports too) and make the same writes, under the gate\n * the Firestore rules put on them: a verified member with a write role on the\n * site (`canWriteHostContent`), or staff, while the site, its org and the\n * platform are not frozen (`hostWritesFrozen`). A new product also meets the\n * create path's own door: the plan's `commerce` entitlement and its\n * `productsPerHost` quota, counted in the same transaction as the create.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const ROLE_REFUSAL = 'Editing requires the editor role'\nconst MAX_BODY_BYTES = 256_000\n\n/** The reasons the Adjust stock dialog offers. */\nexport const STOCK_ADJUST_REASONS: readonly InventoryAdjustmentReason[] = ['restock', 'correction', 'damage', 'refund']\n\nexport interface ProductsWriteDeps {\n firestore?: () => Firestore\n verifyIdToken?: (token: string) => Promise<Record<string, unknown> & { uid: string }>\n orgForHost?: (hostId: string) => Promise<{ org: Record<string, unknown> } | null>\n lockdown?: (options: Parameters<typeof getLockdownVerdict>[0]) => Promise<unknown>\n dropCache?: typeof dropPluginSiteCache\n now?: () => number\n}\n\ntype Reply = Parameters<PluginApiHandler>[1]\n\nexport function bodyOf(req: Parameters<PluginApiHandler>[0]): Record<string, unknown> {\n const raw = req.body\n if (typeof raw === 'string') {\n if (raw.length > MAX_BODY_BYTES) return {}\n try {\n return JSON.parse(raw || '{}')\n } catch {\n return {}\n }\n }\n return (raw ?? {}) as Record<string, unknown>\n}\n\nexport const validId = (value: unknown): value is string =>\n typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value)\n\nexport interface Gate {\n uid: string\n staff: boolean\n host: FirebaseFirestore.DocumentSnapshot\n org: Record<string, unknown>\n}\n\n/**\n * Who may write this site's products, as the rules decide it. Answers the\n * caller's reply on a refusal and returns null.\n */\nexport async function gate(req: Parameters<PluginApiHandler>[0], res: Reply, hostId: string, deps: Required<ProductsWriteDeps>): Promise<Gate | null> {\n const authorization = String(req.headers.authorization ?? '')\n const token = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!token) return void res.status(401).json({ error: 'Unauthenticated' }), null\n let decoded: Record<string, unknown> & { uid: string }\n try {\n decoded = await deps.verifyIdToken(token)\n } catch (error) {\n // Only a refused token is the caller's; an Auth outage is a 500 (AGL-2852).\n if (!isRefusedIdToken(error)) throw error\n return void res.status(401).json({ error: 'Sign in again to continue' }), null\n }\n const staff = decoded['staff'] === true\n if (!staff && decoded['email_verified'] !== true && !isImpersonationSession(decoded as never)) {\n return void res.status(403).json({ error: 'Verify your email address to make changes' }), null\n }\n const host = await deps.firestore().collection('hosts').doc(hostId).get()\n if (!host.exists) return void res.status(404).json({ error: 'Unknown site' }), null\n if (!staff && !hostRoleCanWrite((host.get('memberRoles') ?? {})[decoded.uid])) {\n return void res.status(403).json({ error: ROLE_REFUSAL }), null\n }\n const org = ((await deps.orgForHost(hostId))?.org ?? {}) as Record<string, unknown>\n const locked = await deps.lockdown({ staff, uid: decoded.uid, org: org as never, host: host.data() as never, request: { method: 'POST' } as never })\n if (locked) return void res.status(423).json({ error: 'This site is not accepting changes right now' }), null\n return { uid: decoded.uid, staff, host, org }\n}\n\n/** The site-wide cache drop a product change is owed (AGL-3386): an outbox entry in the write, then the drop. */\nexport function outboxEntry(firestore: Firestore, hostId: string) {\n return {\n ref: firestore.collection('publishOutbox').doc(createResourceUid()),\n data: { hostId, paths: ['/'], createdAt: FieldValue.serverTimestamp(), attempts: 0, entireHost: true },\n }\n}\n\nexport async function settleCache(deps: Required<ProductsWriteDeps>, hostId: string, entry: FirebaseFirestore.DocumentReference, reason: string) {\n const dropped = await deps.dropCache({ hostIds: [hostId], reason }).catch(() => ({ complete: false }))\n if (dropped.complete) await entry.delete().catch(() => undefined)\n}\n\nexport function resolved(deps: ProductsWriteDeps): Required<ProductsWriteDeps> {\n return {\n firestore: deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore),\n verifyIdToken: deps.verifyIdToken ?? ((token) => firebaseAdmin.app().auth().verifyIdToken(token) as never),\n orgForHost: deps.orgForHost ?? ((hostId) => getOrgForHost(hostId) as never),\n lockdown: deps.lockdown ?? ((options) => getLockdownVerdict(options)),\n dropCache: deps.dropCache ?? dropPluginSiteCache,\n now: deps.now ?? (() => Date.now()),\n }\n}\n\n/**\n * Saves a product as the console editor does. `create: true` makes a new\n * product under the client-minted `productId` (a retry finds its own product\n * and answers `replayed`); otherwise it replaces the live product, keeping\n * its live stock holds.\n */\nexport function createProductSaveHandler(options: ProductsWriteDeps = {}): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const deps = resolved(options)\n const body = bodyOf(req)\n const hostId = body['hostId']\n const productId = body['productId']\n const product = body['product'] as EditedProduct | undefined\n if (!validId(hostId) || !validId(productId) || !product || typeof product !== 'object' || !Array.isArray(product.variants)) {\n return res.status(400).json({ error: 'Missing site, product or its variants' })\n }\n if (typeof product.name !== 'string' || !product.name.trim()) return res.status(400).json({ error: 'Product name is required' })\n try {\n const door = await gate(req, res, hostId, deps)\n if (!door) return\n const nowMs = deps.now()\n const fields = productSaveFields(product, nowMs)\n const problem = validateProduct(fields as never)\n if (problem) return res.status(400).json({ error: problem })\n const db = deps.firestore()\n const hostRef = db.collection('hosts').doc(hostId)\n const products = hostRef.collection('products')\n const productRef = products.doc(productId)\n const smartQuery = hostRef.collection('collections').where('kind', '==', 'catalog').where('mode', '==', 'smart')\n const outbox = outboxEntry(db, hostId)\n const create = body['create'] === true\n const answer = await db.runTransaction(async (tx) => {\n const [existing, smart] = await Promise.all([tx.get(productRef), tx.get(smartQuery)])\n const membership = {\n collectionIds: productCollectionIds(\n productMembershipInput(fields, product) as never,\n smart.docs.map((doc) => smartCollectionRulesOf(doc.id, doc.data())),\n ),\n }\n if (create) {\n if (existing.exists) return { status: 200, body: { ok: true, id: productId, replayed: true } }\n const early = planRefusal(door.org as never)\n if (early) return { status: early.status, body: { error: early.error } }\n const used = (await tx.get(products.count())).data().count\n const room = roomRefusal(door.org as never, Number(used) || 0)\n if (room) return { status: room.status, body: { error: room.error } }\n tx.create(productRef, {\n ...fields,\n ...membership,\n createdAtMs: nowMs,\n deletedAt: null,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n createdBy: door.uid,\n })\n } else {\n if (!existing.exists || existing.get('deletedAt')) return { status: 404, body: { error: 'This product was deleted' } }\n tx.set(productRef, {\n ...storedTimestamps(existing.data() ?? {}, product as unknown as Record<string, unknown>),\n ...productEditWrite(fields, existing.get('stockHolds')),\n ...membership,\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n tx.set(outbox.ref, outbox.data)\n return { status: 200, body: { ok: true, id: productId, replayed: false } }\n })\n if (answer.status === 200 && !(answer.body as { replayed?: boolean }).replayed) {\n await settleCache(deps, hostId, outbox.ref, 'product saved from the app')\n }\n return res.status(answer.status).json(answer.body)\n } catch (error) {\n console.error('[commerce/products/save]', error)\n return res.status(500).json({ error: 'The product could not be saved' })\n }\n }\n}\n\n/**\n * The stored timestamps an edit keeps. The console's replace writes back the\n * Timestamps its seed carried (`createdAt`, a publish time); a client's JSON\n * cannot carry one, so each stored Timestamp field the request does not name\n * is kept as stored.\n */\nexport function storedTimestamps(stored: Record<string, unknown>, asked: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(stored).filter(\n ([key, value]) => !(key in asked) && typeof (value as { toDate?: unknown } | null)?.toDate === 'function',\n ),\n )\n}\n\n/** The ledger row id one attempt owns: the same key from the same member is the same row. */\nexport function stockAttemptId(uid: string, key: string): string {\n return 'app-' + createHash('sha256').update(`${uid}:${key}`).digest('hex').slice(0, 40)\n}\n\n/**\n * Adjusts one variant's stock as the Adjust stock dialog does: the variants\n * and stock fields on the product, and one `inventoryAdjustments` row, in one\n * transaction. The Idempotency-Key names that row, so a retried attempt finds\n * it and moves nothing twice.\n */\nexport function createProductStockHandler(options: ProductsWriteDeps = {}): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const deps = resolved(options)\n const body = bodyOf(req)\n const hostId = body['hostId']\n const productId = body['productId']\n const variantId = body['variantId']\n const key = String(req.headers['idempotency-key'] ?? '').trim().slice(0, 200)\n const reason = body['reason'] as InventoryAdjustmentReason\n const delta = Math.round(Number(body['delta']))\n const locationId = typeof body['locationId'] === 'string' && body['locationId'] ? body['locationId'] : undefined\n if (!validId(hostId) || !validId(productId) || typeof variantId !== 'string' || !variantId) {\n return res.status(400).json({ error: 'Missing site, product or variant' })\n }\n if (!key) return res.status(400).json({ error: 'Missing Idempotency-Key' })\n if (!STOCK_ADJUST_REASONS.includes(reason)) return res.status(400).json({ error: 'Unknown reason' })\n if (!Number.isFinite(delta) || delta === 0) return res.status(400).json({ error: 'Enter a number of units' })\n try {\n const door = await gate(req, res, hostId, deps)\n if (!door) return\n const db = deps.firestore()\n const hostRef = db.collection('hosts').doc(hostId)\n const productRef = hostRef.collection('products').doc(productId)\n const ledgerRef = hostRef.collection('inventoryAdjustments').doc(stockAttemptId(door.uid, key))\n const outbox = outboxEntry(db, hostId)\n const answer = await db.runTransaction(async (tx) => {\n const [ledger, stored] = await Promise.all([tx.get(ledgerRef), tx.get(productRef)])\n if (ledger.exists) {\n return { status: 200, body: { ok: true, replayed: true, inventory: stored.get('inventory') ?? null } }\n }\n if (!stored.exists || stored.get('deletedAt')) return { status: 404, body: { error: 'This product was deleted' } }\n const product = stored.data() as never as Parameters<typeof stockAdjustmentWrite>[0]\n if (!(product.variants ?? []).some((variant) => variant.id === variantId)) {\n return { status: 404, body: { error: 'That variant is no longer on this product' } }\n }\n const write = stockAdjustmentWrite(product, productId, { variantId, delta, reason, locationId }, deps.now())\n if (!write) return { status: 400, body: { error: 'Enter a number of units' } }\n tx.update(productRef, write.update)\n tx.create(ledgerRef, write.ledger)\n tx.set(outbox.ref, outbox.data)\n return { status: 200, body: { ok: true, replayed: false, inventory: (write.update as { inventory?: unknown }).inventory ?? null } }\n })\n if (answer.status === 200 && !(answer.body as { replayed?: boolean }).replayed) {\n await settleCache(deps, hostId, outbox.ref, 'stock adjusted from the app')\n }\n return res.status(answer.status).json(answer.body)\n } catch (error) {\n console.error('[commerce/products/stock]', error)\n return res.status(500).json({ error: 'Stock could not be adjusted' })\n }\n }\n}\n\nexport const productSaveHandler = createProductSaveHandler()\nexport const productStockHandler = createProductStockHandler()\n"],"names":["createHash","isRefusedIdToken","createResourceUid","hostRoleCanWrite","dropPluginSiteCache","firebaseAdmin","getLockdownVerdict","getOrgForHost","isImpersonationSession","FieldValue","productCollectionIds","productEditWrite","productMembershipInput","productSaveFields","smartCollectionRulesOf","stockAdjustmentWrite","validateProduct","planRefusal","roomRefusal","ROLE_REFUSAL","MAX_BODY_BYTES","STOCK_ADJUST_REASONS","bodyOf","req","raw","body","length","JSON","parse","validId","value","test","gate","res","hostId","deps","host","authorization","String","headers","token","startsWith","slice","status","json","error","decoded","verifyIdToken","staff","firestore","collection","doc","get","exists","uid","org","orgForHost","locked","lockdown","data","request","method","outboxEntry","ref","paths","createdAt","serverTimestamp","attempts","entireHost","settleCache","entry","reason","dropped","dropCache","hostIds","catch","complete","delete","undefined","resolved","app","auth","options","now","Date","createProductSaveHandler","productId","product","Array","isArray","variants","name","trim","door","nowMs","fields","problem","db","hostRef","products","productRef","smartQuery","where","outbox","create","answer","runTransaction","tx","existing","smart","Promise","all","membership","collectionIds","docs","map","id","ok","replayed","early","used","count","room","Number","createdAtMs","deletedAt","updatedAt","createdBy","set","storedTimestamps","console","stored","asked","Object","fromEntries","entries","filter","key","toDate","stockAttemptId","update","digest","createProductStockHandler","variantId","delta","Math","round","locationId","includes","isFinite","ledgerRef","ledger","inventory","some","variant","write","productSaveHandler","productStockHandler"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,gBAAgB,QAAQ,mDAAkD;AACnF,SAASC,iBAAiB,QAAQ,6CAA4C;AAC9E,SAASC,gBAAgB,QAAQ,uCAAsC;AACvE,SAASC,mBAAmB,QAAQ,gDAA+C;AAEnF,SAASC,aAAa,EAAEC,kBAAkB,EAAEC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AACnH,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAEEC,oBAAoB,EACpBC,gBAAgB,EAChBC,sBAAsB,EACtBC,iBAAiB,EACjBC,sBAAsB,EACtBC,oBAAoB,EACpBC,eAAe,QAEV,oBAAU;AACjB,SAASC,WAAW,EAAEC,WAAW,QAAQ,sBAAkB;AAoB3D,OAAO,MAAMC,eAAe,mCAAkC;AAC9D,MAAMC,iBAAiB;AAEvB,gDAAgD,GAChD,OAAO,MAAMC,uBAA6D;IAAC;IAAW;IAAc;IAAU;CAAS,CAAA;AAavH,OAAO,SAASC,OAAOC,GAAoC;IACzD,MAAMC,MAAMD,IAAIE,IAAI;IACpB,IAAI,OAAOD,QAAQ,UAAU;QAC3B,IAAIA,IAAIE,MAAM,GAAGN,gBAAgB,OAAO,CAAC;QACzC,IAAI;YACF,OAAOO,KAAKC,KAAK,CAACJ,OAAO;QAC3B,EAAE,eAAM;YACN,OAAO,CAAC;QACV;IACF;IACA,OAAQA,cAAAA,MAAO,CAAC;AAClB;AAEA,OAAO,MAAMK,UAAU,CAACC,QACtB,OAAOA,UAAU,YAAY,yBAAyBC,IAAI,CAACD,OAAM;AASnE;;;CAGC,GACD,OAAO,eAAeE,KAAKT,GAAoC,EAAEU,GAAU,EAAEC,MAAc,EAAEC,IAAiC;QAC/FZ,4BAiBIa;QAGnB;IApBd,MAAMC,gBAAgBC,QAAOf,6BAAAA,IAAIgB,OAAO,CAACF,aAAa,YAAzBd,6BAA6B;IAC1D,MAAMiB,QAAQH,cAAcI,UAAU,CAAC,aAAaJ,cAAcK,KAAK,CAAC,UAAUhB,MAAM,IAAI;IAC5F,IAAI,CAACc,OAAO,OAAO,AAAKP,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB,IAAI;IAC5E,IAAIC;IACJ,IAAI;QACFA,UAAU,MAAMX,KAAKY,aAAa,CAACP;IACrC,EAAE,OAAOK,OAAO;QACd,4EAA4E;QAC5E,IAAI,CAAC5C,iBAAiB4C,QAAQ,MAAMA;QACpC,OAAO,AAAKZ,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA4B,IAAI;IAC5E;IACA,MAAMG,QAAQF,OAAO,CAAC,QAAQ,KAAK;IACnC,IAAI,CAACE,SAASF,OAAO,CAAC,iBAAiB,KAAK,QAAQ,CAACtC,uBAAuBsC,UAAmB;QAC7F,OAAO,AAAKb,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA4C,IAAI;IAC5F;IACA,MAAMT,OAAO,MAAMD,KAAKc,SAAS,GAAGC,UAAU,CAAC,SAASC,GAAG,CAACjB,QAAQkB,GAAG;IACvE,IAAI,CAAChB,KAAKiB,MAAM,EAAE,OAAO,AAAKpB,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAe,IAAI;IAC/E,IAAI,CAACG,SAAS,CAAC7C,iBAAiB,EAACiC,YAAAA,KAAKgB,GAAG,CAAC,0BAAThB,YAA2B,CAAC,EAAE,CAACU,QAAQQ,GAAG,CAAC,GAAG;QAC7E,OAAO,AAAKrB,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO1B;QAAa,IAAI;IAC7D;IACA,MAAMoC,eAAQ,QAAA,MAAMpB,KAAKqB,UAAU,CAACtB,4BAAvB,AAAC,MAAgCqB,GAAG,mBAAI,CAAC;IACtD,MAAME,SAAS,MAAMtB,KAAKuB,QAAQ,CAAC;QAAEV;QAAOM,KAAKR,QAAQQ,GAAG;QAAEC,KAAKA;QAAcnB,MAAMA,KAAKuB,IAAI;QAAaC,SAAS;YAAEC,QAAQ;QAAO;IAAW;IAClJ,IAAIJ,QAAQ,OAAO,AAAKxB,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAA+C,IAAI;IACzG,OAAO;QAAES,KAAKR,QAAQQ,GAAG;QAAEN;QAAOZ;QAAMmB;IAAI;AAC9C;AAEA,+GAA+G,GAC/G,OAAO,SAASO,YAAYb,SAAoB,EAAEf,MAAc;IAC9D,OAAO;QACL6B,KAAKd,UAAUC,UAAU,CAAC,iBAAiBC,GAAG,CAACjD;QAC/CyD,MAAM;YAAEzB;YAAQ8B,OAAO;gBAAC;aAAI;YAAEC,WAAWxD,WAAWyD,eAAe;YAAIC,UAAU;YAAGC,YAAY;QAAK;IACvG;AACF;AAEA,OAAO,eAAeC,YAAYlC,IAAiC,EAAED,MAAc,EAAEoC,KAA0C,EAAEC,MAAc;IAC7I,MAAMC,UAAU,MAAMrC,KAAKsC,SAAS,CAAC;QAAEC,SAAS;YAACxC;SAAO;QAAEqC;IAAO,GAAGI,KAAK,CAAC,IAAO,CAAA;YAAEC,UAAU;QAAM,CAAA;IACnG,IAAIJ,QAAQI,QAAQ,EAAE,MAAMN,MAAMO,MAAM,GAAGF,KAAK,CAAC,IAAMG;AACzD;AAEA,OAAO,SAASC,SAAS5C,IAAuB;QAEjCA,iBACIA,qBACHA,kBACFA,gBACCA,iBACNA;IANP,OAAO;QACLc,SAAS,GAAEd,kBAAAA,KAAKc,SAAS,YAAdd,kBAAmB,IAAM9B,cAAc2E,GAAG,GAAG/B,SAAS;QACjEF,aAAa,GAAEZ,sBAAAA,KAAKY,aAAa,YAAlBZ,sBAAuB,CAACK,QAAUnC,cAAc2E,GAAG,GAAGC,IAAI,GAAGlC,aAAa,CAACP;QAC1FgB,UAAU,GAAErB,mBAAAA,KAAKqB,UAAU,YAAfrB,mBAAoB,CAACD,SAAW3B,cAAc2B;QAC1DwB,QAAQ,GAAEvB,iBAAAA,KAAKuB,QAAQ,YAAbvB,iBAAkB,CAAC+C,UAAY5E,mBAAmB4E;QAC5DT,SAAS,GAAEtC,kBAAAA,KAAKsC,SAAS,YAAdtC,kBAAkB/B;QAC7B+E,GAAG,GAAEhD,YAAAA,KAAKgD,GAAG,YAARhD,YAAa,IAAMiD,KAAKD,GAAG;IAClC;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASE,yBAAyBH,UAA6B,CAAC,CAAC;IACtE,OAAO,OAAO3D,KAAKU;QACjB,IAAIV,IAAIsC,MAAM,KAAK,QAAQ,OAAO5B,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACrF,MAAMV,OAAO4C,SAASG;QACtB,MAAMzD,OAAOH,OAAOC;QACpB,MAAMW,SAAST,IAAI,CAAC,SAAS;QAC7B,MAAM6D,YAAY7D,IAAI,CAAC,YAAY;QACnC,MAAM8D,UAAU9D,IAAI,CAAC,UAAU;QAC/B,IAAI,CAACI,QAAQK,WAAW,CAACL,QAAQyD,cAAc,CAACC,WAAW,OAAOA,YAAY,YAAY,CAACC,MAAMC,OAAO,CAACF,QAAQG,QAAQ,GAAG;YAC1H,OAAOzD,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAwC;QAC/E;QACA,IAAI,OAAO0C,QAAQI,IAAI,KAAK,YAAY,CAACJ,QAAQI,IAAI,CAACC,IAAI,IAAI,OAAO3D,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA2B;QAC9H,IAAI;YACF,MAAMgD,OAAO,MAAM7D,KAAKT,KAAKU,KAAKC,QAAQC;YAC1C,IAAI,CAAC0D,MAAM;YACX,MAAMC,QAAQ3D,KAAKgD,GAAG;YACtB,MAAMY,SAASlF,kBAAkB0E,SAASO;YAC1C,MAAME,UAAUhF,gBAAgB+E;YAChC,IAAIC,SAAS,OAAO/D,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAOmD;YAAQ;YAC1D,MAAMC,KAAK9D,KAAKc,SAAS;YACzB,MAAMiD,UAAUD,GAAG/C,UAAU,CAAC,SAASC,GAAG,CAACjB;YAC3C,MAAMiE,WAAWD,QAAQhD,UAAU,CAAC;YACpC,MAAMkD,aAAaD,SAAShD,GAAG,CAACmC;YAChC,MAAMe,aAAaH,QAAQhD,UAAU,CAAC,eAAeoD,KAAK,CAAC,QAAQ,MAAM,WAAWA,KAAK,CAAC,QAAQ,MAAM;YACxG,MAAMC,SAASzC,YAAYmC,IAAI/D;YAC/B,MAAMsE,SAAS/E,IAAI,CAAC,SAAS,KAAK;YAClC,MAAMgF,SAAS,MAAMR,GAAGS,cAAc,CAAC,OAAOC;gBAC5C,MAAM,CAACC,UAAUC,MAAM,GAAG,MAAMC,QAAQC,GAAG,CAAC;oBAACJ,GAAGvD,GAAG,CAACgD;oBAAaO,GAAGvD,GAAG,CAACiD;iBAAY;gBACpF,MAAMW,aAAa;oBACjBC,eAAevG,qBACbE,uBAAuBmF,QAAQR,UAC/BsB,MAAMK,IAAI,CAACC,GAAG,CAAC,CAAChE,MAAQrC,uBAAuBqC,IAAIiE,EAAE,EAAEjE,IAAIQ,IAAI;gBAEnE;gBACA,IAAI6C,QAAQ;oBACV,IAAII,SAASvD,MAAM,EAAE,OAAO;wBAAEV,QAAQ;wBAAKlB,MAAM;4BAAE4F,IAAI;4BAAMD,IAAI9B;4BAAWgC,UAAU;wBAAK;oBAAE;oBAC7F,MAAMC,QAAQtG,YAAY4E,KAAKtC,GAAG;oBAClC,IAAIgE,OAAO,OAAO;wBAAE5E,QAAQ4E,MAAM5E,MAAM;wBAAElB,MAAM;4BAAEoB,OAAO0E,MAAM1E,KAAK;wBAAC;oBAAE;oBACvE,MAAM2E,OAAO,AAAC,CAAA,MAAMb,GAAGvD,GAAG,CAAC+C,SAASsB,KAAK,GAAE,EAAG9D,IAAI,GAAG8D,KAAK;oBAC1D,MAAMC,OAAOxG,YAAY2E,KAAKtC,GAAG,EAAWoE,OAAOH,SAAS;oBAC5D,IAAIE,MAAM,OAAO;wBAAE/E,QAAQ+E,KAAK/E,MAAM;wBAAElB,MAAM;4BAAEoB,OAAO6E,KAAK7E,KAAK;wBAAC;oBAAE;oBACpE8D,GAAGH,MAAM,CAACJ,YAAY,aACjBL,QACAiB;wBACHY,aAAa9B;wBACb+B,WAAW;wBACX5D,WAAWxD,WAAWyD,eAAe;wBACrC4D,WAAWrH,WAAWyD,eAAe;wBACrC6D,WAAWlC,KAAKvC,GAAG;;gBAEvB,OAAO;wBAGiBsD;oBAFtB,IAAI,CAACA,SAASvD,MAAM,IAAIuD,SAASxD,GAAG,CAAC,cAAc,OAAO;wBAAET,QAAQ;wBAAKlB,MAAM;4BAAEoB,OAAO;wBAA2B;oBAAE;oBACrH8D,GAAGqB,GAAG,CAAC5B,YAAY,aACd6B,kBAAiBrB,iBAAAA,SAASjD,IAAI,cAAbiD,iBAAmB,CAAC,GAAGrB,UACxC5E,iBAAiBoF,QAAQa,SAASxD,GAAG,CAAC,gBACtC4D;wBACHc,WAAWrH,WAAWyD,eAAe;;gBAEzC;gBACAyC,GAAGqB,GAAG,CAACzB,OAAOxC,GAAG,EAAEwC,OAAO5C,IAAI;gBAC9B,OAAO;oBAAEhB,QAAQ;oBAAKlB,MAAM;wBAAE4F,IAAI;wBAAMD,IAAI9B;wBAAWgC,UAAU;oBAAM;gBAAE;YAC3E;YACA,IAAIb,OAAO9D,MAAM,KAAK,OAAO,CAAC,AAAC8D,OAAOhF,IAAI,CAA4B6F,QAAQ,EAAE;gBAC9E,MAAMjD,YAAYlC,MAAMD,QAAQqE,OAAOxC,GAAG,EAAE;YAC9C;YACA,OAAO9B,IAAIU,MAAM,CAAC8D,OAAO9D,MAAM,EAAEC,IAAI,CAAC6D,OAAOhF,IAAI;QACnD,EAAE,OAAOoB,OAAO;YACdqF,QAAQrF,KAAK,CAAC,4BAA4BA;YAC1C,OAAOZ,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAiC;QACxE;IACF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASoF,iBAAiBE,MAA+B,EAAEC,KAA8B;IAC9F,OAAOC,OAAOC,WAAW,CACvBD,OAAOE,OAAO,CAACJ,QAAQK,MAAM,CAC3B,CAAC,CAACC,KAAK3G,MAAM,GAAK,CAAE2G,CAAAA,OAAOL,KAAI,KAAM,QAAQtG,yBAAD,AAACA,MAAuC4G,MAAM,MAAK;AAGrG;AAEA,2FAA2F,GAC3F,OAAO,SAASC,eAAerF,GAAW,EAAEmF,GAAW;IACrD,OAAO,SAASzI,WAAW,UAAU4I,MAAM,CAAC,GAAGtF,IAAI,CAAC,EAAEmF,KAAK,EAAEI,MAAM,CAAC,OAAOnG,KAAK,CAAC,GAAG;AACtF;AAEA;;;;;CAKC,GACD,OAAO,SAASoG,0BAA0B5D,UAA6B,CAAC,CAAC;IACvE,OAAO,OAAO3D,KAAKU;YAOEV;QANnB,IAAIA,IAAIsC,MAAM,KAAK,QAAQ,OAAO5B,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACrF,MAAMV,OAAO4C,SAASG;QACtB,MAAMzD,OAAOH,OAAOC;QACpB,MAAMW,SAAST,IAAI,CAAC,SAAS;QAC7B,MAAM6D,YAAY7D,IAAI,CAAC,YAAY;QACnC,MAAMsH,YAAYtH,IAAI,CAAC,YAAY;QACnC,MAAMgH,MAAMnG,QAAOf,8BAAAA,IAAIgB,OAAO,CAAC,kBAAkB,YAA9BhB,8BAAkC,IAAIqE,IAAI,GAAGlD,KAAK,CAAC,GAAG;QACzE,MAAM6B,SAAS9C,IAAI,CAAC,SAAS;QAC7B,MAAMuH,QAAQC,KAAKC,KAAK,CAACvB,OAAOlG,IAAI,CAAC,QAAQ;QAC7C,MAAM0H,aAAa,OAAO1H,IAAI,CAAC,aAAa,KAAK,YAAYA,IAAI,CAAC,aAAa,GAAGA,IAAI,CAAC,aAAa,GAAGqD;QACvG,IAAI,CAACjD,QAAQK,WAAW,CAACL,QAAQyD,cAAc,OAAOyD,cAAc,YAAY,CAACA,WAAW;YAC1F,OAAO9G,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAmC;QAC1E;QACA,IAAI,CAAC4F,KAAK,OAAOxG,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA0B;QACzE,IAAI,CAACxB,qBAAqB+H,QAAQ,CAAC7E,SAAS,OAAOtC,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAiB;QAClG,IAAI,CAAC8E,OAAO0B,QAAQ,CAACL,UAAUA,UAAU,GAAG,OAAO/G,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA0B;QAC3G,IAAI;YACF,MAAMgD,OAAO,MAAM7D,KAAKT,KAAKU,KAAKC,QAAQC;YAC1C,IAAI,CAAC0D,MAAM;YACX,MAAMI,KAAK9D,KAAKc,SAAS;YACzB,MAAMiD,UAAUD,GAAG/C,UAAU,CAAC,SAASC,GAAG,CAACjB;YAC3C,MAAMkE,aAAaF,QAAQhD,UAAU,CAAC,YAAYC,GAAG,CAACmC;YACtD,MAAMgE,YAAYpD,QAAQhD,UAAU,CAAC,wBAAwBC,GAAG,CAACwF,eAAe9C,KAAKvC,GAAG,EAAEmF;YAC1F,MAAMlC,SAASzC,YAAYmC,IAAI/D;YAC/B,MAAMuE,SAAS,MAAMR,GAAGS,cAAc,CAAC,OAAOC;oBAOtCpB,mBAQ8D;gBAdpE,MAAM,CAACgE,QAAQpB,OAAO,GAAG,MAAMrB,QAAQC,GAAG,CAAC;oBAACJ,GAAGvD,GAAG,CAACkG;oBAAY3C,GAAGvD,GAAG,CAACgD;iBAAY;gBAClF,IAAImD,OAAOlG,MAAM,EAAE;wBACkD8E;oBAAnE,OAAO;wBAAExF,QAAQ;wBAAKlB,MAAM;4BAAE4F,IAAI;4BAAMC,UAAU;4BAAMkC,SAAS,GAAErB,cAAAA,OAAO/E,GAAG,CAAC,wBAAX+E,cAA2B;wBAAK;oBAAE;gBACvG;gBACA,IAAI,CAACA,OAAO9E,MAAM,IAAI8E,OAAO/E,GAAG,CAAC,cAAc,OAAO;oBAAET,QAAQ;oBAAKlB,MAAM;wBAAEoB,OAAO;oBAA2B;gBAAE;gBACjH,MAAM0C,UAAU4C,OAAOxE,IAAI;gBAC3B,IAAI,CAAC,EAAC4B,oBAAAA,QAAQG,QAAQ,YAAhBH,oBAAoB,EAAE,EAAEkE,IAAI,CAAC,CAACC,UAAYA,QAAQtC,EAAE,KAAK2B,YAAY;oBACzE,OAAO;wBAAEpG,QAAQ;wBAAKlB,MAAM;4BAAEoB,OAAO;wBAA4C;oBAAE;gBACrF;gBACA,MAAM8G,QAAQ5I,qBAAqBwE,SAASD,WAAW;oBAAEyD;oBAAWC;oBAAOzE;oBAAQ4E;gBAAW,GAAGhH,KAAKgD,GAAG;gBACzG,IAAI,CAACwE,OAAO,OAAO;oBAAEhH,QAAQ;oBAAKlB,MAAM;wBAAEoB,OAAO;oBAA0B;gBAAE;gBAC7E8D,GAAGiC,MAAM,CAACxC,YAAYuD,MAAMf,MAAM;gBAClCjC,GAAGH,MAAM,CAAC8C,WAAWK,MAAMJ,MAAM;gBACjC5C,GAAGqB,GAAG,CAACzB,OAAOxC,GAAG,EAAEwC,OAAO5C,IAAI;gBAC9B,OAAO;oBAAEhB,QAAQ;oBAAKlB,MAAM;wBAAE4F,IAAI;wBAAMC,UAAU;wBAAOkC,SAAS,GAAE,0BAAA,AAACG,MAAMf,MAAM,CAA6BY,SAAS,YAAnD,0BAAuD;oBAAK;gBAAE;YACpI;YACA,IAAI/C,OAAO9D,MAAM,KAAK,OAAO,CAAC,AAAC8D,OAAOhF,IAAI,CAA4B6F,QAAQ,EAAE;gBAC9E,MAAMjD,YAAYlC,MAAMD,QAAQqE,OAAOxC,GAAG,EAAE;YAC9C;YACA,OAAO9B,IAAIU,MAAM,CAAC8D,OAAO9D,MAAM,EAAEC,IAAI,CAAC6D,OAAOhF,IAAI;QACnD,EAAE,OAAOoB,OAAO;YACdqF,QAAQrF,KAAK,CAAC,6BAA6BA;YAC3C,OAAOZ,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA8B;QACrE;IACF;AACF;AAEA,OAAO,MAAM+G,qBAAqBvE,2BAA0B;AAC5D,OAAO,MAAMwE,sBAAsBf,4BAA2B"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/products-write.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 { createHash } from 'node:crypto'\nimport { isRefusedIdToken } from '@aglyn/tenant-data-admin/server/id-token-refusal'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getLockdownVerdict, getOrgForHost, isImpersonationSession } from '@aglyn/tenant-data-admin'\nimport { FieldValue } from 'firebase-admin/firestore'\nimport {\n type EditedProduct,\n liftLegacyProduct,\n productCollectionIds,\n productDuplicateFields,\n productDuplicateName,\n productDuplicateSlugBase,\n productEditWrite,\n productMembershipInput,\n productSaveFields,\n smartCollectionRulesOf,\n stockAdjustmentWrite,\n validateProduct,\n type InventoryAdjustmentReason,\n} from '../model'\nimport { planRefusal, roomRefusal } from './product-drafts'\n\n/*\n * `POST /api/commerce/products/save` and `POST /api/commerce/products/stock`\n * (AGL-3652): the console product editor's save and its Adjust stock dialog,\n * for the native apps.\n *\n * The console makes both writes from the browser, computing derived fields\n * first (search keys, stock verdict, price, image, smart-collection\n * membership). These routes run THAT computation (`model/product-write.ts`,\n * which the console imports too) and make the same writes, under the gate\n * the Firestore rules put on them: a verified member with a write role on the\n * site (`canWriteHostContent`), or staff, while the site, its org and the\n * platform are not frozen (`hostWritesFrozen`). A new product also meets the\n * create path's own door: the plan's `commerce` entitlement and its\n * `productsPerHost` quota, counted in the same transaction as the create.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\nexport const ROLE_REFUSAL = 'Editing requires the editor role'\nconst MAX_BODY_BYTES = 256_000\n\n/** The reasons the Adjust stock dialog offers. */\nexport const STOCK_ADJUST_REASONS: readonly InventoryAdjustmentReason[] = ['restock', 'correction', 'damage', 'refund']\n\nexport interface ProductsWriteDeps {\n firestore?: () => Firestore\n verifyIdToken?: (token: string) => Promise<Record<string, unknown> & { uid: string }>\n orgForHost?: (hostId: string) => Promise<{ org: Record<string, unknown> } | null>\n lockdown?: (options: Parameters<typeof getLockdownVerdict>[0]) => Promise<unknown>\n dropCache?: typeof dropPluginSiteCache\n now?: () => number\n}\n\ntype Reply = Parameters<PluginApiHandler>[1]\n\nexport function bodyOf(req: Parameters<PluginApiHandler>[0]): Record<string, unknown> {\n const raw = req.body\n if (typeof raw === 'string') {\n if (raw.length > MAX_BODY_BYTES) return {}\n try {\n return JSON.parse(raw || '{}')\n } catch {\n return {}\n }\n }\n return (raw ?? {}) as Record<string, unknown>\n}\n\nexport const validId = (value: unknown): value is string =>\n typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value)\n\nexport interface Gate {\n uid: string\n staff: boolean\n host: FirebaseFirestore.DocumentSnapshot\n org: Record<string, unknown>\n}\n\n/**\n * Who may write this site's products, as the rules decide it. Answers the\n * caller's reply on a refusal and returns null.\n */\nexport async function gate(req: Parameters<PluginApiHandler>[0], res: Reply, hostId: string, deps: Required<ProductsWriteDeps>): Promise<Gate | null> {\n const authorization = String(req.headers.authorization ?? '')\n const token = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : ''\n if (!token) return void res.status(401).json({ error: 'Unauthenticated' }), null\n let decoded: Record<string, unknown> & { uid: string }\n try {\n decoded = await deps.verifyIdToken(token)\n } catch (error) {\n // Only a refused token is the caller's; an Auth outage is a 500 (AGL-2852).\n if (!isRefusedIdToken(error)) throw error\n return void res.status(401).json({ error: 'Sign in again to continue' }), null\n }\n const staff = decoded['staff'] === true\n if (!staff && decoded['email_verified'] !== true && !isImpersonationSession(decoded as never)) {\n return void res.status(403).json({ error: 'Verify your email address to make changes' }), null\n }\n const host = await deps.firestore().collection('hosts').doc(hostId).get()\n if (!host.exists) return void res.status(404).json({ error: 'Unknown site' }), null\n if (!staff && !hostRoleCanWrite((host.get('memberRoles') ?? {})[decoded.uid])) {\n return void res.status(403).json({ error: ROLE_REFUSAL }), null\n }\n const org = ((await deps.orgForHost(hostId))?.org ?? {}) as Record<string, unknown>\n const locked = await deps.lockdown({ staff, uid: decoded.uid, org: org as never, host: host.data() as never, request: { method: 'POST' } as never })\n if (locked) return void res.status(423).json({ error: 'This site is not accepting changes right now' }), null\n return { uid: decoded.uid, staff, host, org }\n}\n\n/** The site-wide cache drop a product change is owed (AGL-3386): an outbox entry in the write, then the drop. */\nexport function outboxEntry(firestore: Firestore, hostId: string) {\n return {\n ref: firestore.collection('publishOutbox').doc(createResourceUid()),\n data: { hostId, paths: ['/'], createdAt: FieldValue.serverTimestamp(), attempts: 0, entireHost: true },\n }\n}\n\nexport async function settleCache(deps: Required<ProductsWriteDeps>, hostId: string, entry: FirebaseFirestore.DocumentReference, reason: string) {\n const dropped = await deps.dropCache({ hostIds: [hostId], reason }).catch(() => ({ complete: false }))\n if (dropped.complete) await entry.delete().catch(() => undefined)\n}\n\nexport function resolved(deps: ProductsWriteDeps): Required<ProductsWriteDeps> {\n return {\n firestore: deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore),\n verifyIdToken: deps.verifyIdToken ?? ((token) => firebaseAdmin.app().auth().verifyIdToken(token) as never),\n orgForHost: deps.orgForHost ?? ((hostId) => getOrgForHost(hostId) as never),\n lockdown: deps.lockdown ?? ((options) => getLockdownVerdict(options)),\n dropCache: deps.dropCache ?? dropPluginSiteCache,\n now: deps.now ?? (() => Date.now()),\n }\n}\n\n/**\n * Saves a product as the console editor does. `create: true` makes a new\n * product under the client-minted `productId` (a retry finds its own product\n * and answers `replayed`); otherwise it replaces the live product, keeping\n * its live stock holds.\n */\nexport function createProductSaveHandler(options: ProductsWriteDeps = {}): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const deps = resolved(options)\n const body = bodyOf(req)\n const hostId = body['hostId']\n const productId = body['productId']\n const product = body['product'] as EditedProduct | undefined\n if (!validId(hostId) || !validId(productId) || !product || typeof product !== 'object' || !Array.isArray(product.variants)) {\n return res.status(400).json({ error: 'Missing site, product or its variants' })\n }\n if (typeof product.name !== 'string' || !product.name.trim()) return res.status(400).json({ error: 'Product name is required' })\n try {\n const door = await gate(req, res, hostId, deps)\n if (!door) return\n const nowMs = deps.now()\n const fields = productSaveFields(product, nowMs)\n const problem = validateProduct(fields as never)\n if (problem) return res.status(400).json({ error: problem })\n const db = deps.firestore()\n const hostRef = db.collection('hosts').doc(hostId)\n const products = hostRef.collection('products')\n const productRef = products.doc(productId)\n const smartQuery = hostRef.collection('collections').where('kind', '==', 'catalog').where('mode', '==', 'smart')\n const outbox = outboxEntry(db, hostId)\n const create = body['create'] === true\n const answer = await db.runTransaction(async (tx) => {\n const [existing, smart] = await Promise.all([tx.get(productRef), tx.get(smartQuery)])\n const membership = {\n collectionIds: productCollectionIds(\n productMembershipInput(fields, product) as never,\n smart.docs.map((doc) => smartCollectionRulesOf(doc.id, doc.data())),\n ),\n }\n if (create) {\n if (existing.exists) return { status: 200, body: { ok: true, id: productId, replayed: true } }\n const early = planRefusal(door.org as never)\n if (early) return { status: early.status, body: { error: early.error } }\n const used = (await tx.get(products.count())).data().count\n const room = roomRefusal(door.org as never, Number(used) || 0)\n if (room) return { status: room.status, body: { error: room.error } }\n tx.create(productRef, {\n ...fields,\n ...membership,\n createdAtMs: nowMs,\n deletedAt: null,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n createdBy: door.uid,\n })\n } else {\n if (!existing.exists || existing.get('deletedAt')) return { status: 404, body: { error: 'This product was deleted' } }\n tx.set(productRef, {\n ...storedTimestamps(existing.data() ?? {}, product as unknown as Record<string, unknown>),\n ...productEditWrite(fields, existing.get('stockHolds')),\n ...membership,\n updatedAt: FieldValue.serverTimestamp(),\n })\n }\n tx.set(outbox.ref, outbox.data)\n return { status: 200, body: { ok: true, id: productId, replayed: false } }\n })\n if (answer.status === 200 && !(answer.body as { replayed?: boolean }).replayed) {\n await settleCache(deps, hostId, outbox.ref, 'product saved from the app')\n }\n return res.status(answer.status).json(answer.body)\n } catch (error) {\n console.error('[commerce/products/save]', error)\n return res.status(500).json({ error: 'The product could not be saved' })\n }\n }\n}\n\n/**\n * The stored timestamps an edit keeps. The console's replace writes back the\n * Timestamps its seed carried (`createdAt`, a publish time); a client's JSON\n * cannot carry one, so each stored Timestamp field the request does not name\n * is kept as stored.\n */\nexport function storedTimestamps(stored: Record<string, unknown>, asked: Record<string, unknown>): Record<string, unknown> {\n return Object.fromEntries(\n Object.entries(stored).filter(\n ([key, value]) => !(key in asked) && typeof (value as { toDate?: unknown } | null)?.toDate === 'function',\n ),\n )\n}\n\n/** The ledger row id one attempt owns: the same key from the same member is the same row. */\nexport function stockAttemptId(uid: string, key: string): string {\n return 'app-' + createHash('sha256').update(`${uid}:${key}`).digest('hex').slice(0, 40)\n}\n\n/**\n * Adjusts one variant's stock as the Adjust stock dialog does: the variants\n * and stock fields on the product, and one `inventoryAdjustments` row, in one\n * transaction. The Idempotency-Key names that row, so a retried attempt finds\n * it and moves nothing twice.\n */\nexport function createProductStockHandler(options: ProductsWriteDeps = {}): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const deps = resolved(options)\n const body = bodyOf(req)\n const hostId = body['hostId']\n const productId = body['productId']\n const variantId = body['variantId']\n const key = String(req.headers['idempotency-key'] ?? '').trim().slice(0, 200)\n const reason = body['reason'] as InventoryAdjustmentReason\n const delta = Math.round(Number(body['delta']))\n const locationId = typeof body['locationId'] === 'string' && body['locationId'] ? body['locationId'] : undefined\n if (!validId(hostId) || !validId(productId) || typeof variantId !== 'string' || !variantId) {\n return res.status(400).json({ error: 'Missing site, product or variant' })\n }\n if (!key) return res.status(400).json({ error: 'Missing Idempotency-Key' })\n if (!STOCK_ADJUST_REASONS.includes(reason)) return res.status(400).json({ error: 'Unknown reason' })\n if (!Number.isFinite(delta) || delta === 0) return res.status(400).json({ error: 'Enter a number of units' })\n try {\n const door = await gate(req, res, hostId, deps)\n if (!door) return\n const db = deps.firestore()\n const hostRef = db.collection('hosts').doc(hostId)\n const productRef = hostRef.collection('products').doc(productId)\n const ledgerRef = hostRef.collection('inventoryAdjustments').doc(stockAttemptId(door.uid, key))\n const outbox = outboxEntry(db, hostId)\n const answer = await db.runTransaction(async (tx) => {\n const [ledger, stored] = await Promise.all([tx.get(ledgerRef), tx.get(productRef)])\n if (ledger.exists) {\n return { status: 200, body: { ok: true, replayed: true, inventory: stored.get('inventory') ?? null } }\n }\n if (!stored.exists || stored.get('deletedAt')) return { status: 404, body: { error: 'This product was deleted' } }\n const product = stored.data() as never as Parameters<typeof stockAdjustmentWrite>[0]\n if (!(product.variants ?? []).some((variant) => variant.id === variantId)) {\n return { status: 404, body: { error: 'That variant is no longer on this product' } }\n }\n const write = stockAdjustmentWrite(product, productId, { variantId, delta, reason, locationId }, deps.now())\n if (!write) return { status: 400, body: { error: 'Enter a number of units' } }\n tx.update(productRef, write.update)\n tx.create(ledgerRef, write.ledger)\n tx.set(outbox.ref, outbox.data)\n return { status: 200, body: { ok: true, replayed: false, inventory: (write.update as { inventory?: unknown }).inventory ?? null } }\n })\n if (answer.status === 200 && !(answer.body as { replayed?: boolean }).replayed) {\n await settleCache(deps, hostId, outbox.ref, 'stock adjusted from the app')\n }\n return res.status(answer.status).json(answer.body)\n } catch (error) {\n console.error('[commerce/products/stock]', error)\n return res.status(500).json({ error: 'Stock could not be adjusted' })\n }\n }\n}\n\n/**\n * The first slug in `base`, `base-2`, `base-3`… no product of the site holds,\n * as the console's slug ledger claims one (`product-slugs.ts`): a second\n * Duplicate of the same product does not mint a second `-copy` (AGL-3321).\n */\nexport async function claimProductSlug(products: FirebaseFirestore.CollectionReference, base: string): Promise<string> {\n for (let suffix = 1; suffix < 1000; suffix += 1) {\n const candidate = suffix < 2 ? base : `${base}-${suffix}`\n const held = await products.where('slug', '==', candidate).limit(1).get()\n if (held.empty) return candidate\n }\n return `${base}-${Date.now()}`\n}\n\n/**\n * `POST /api/commerce/products/duplicate` (AGL-3841): the products list's\n * Duplicate for the native apps. The console builds the copy in the browser\n * (`productDuplicateFields`, its slug ledger, the copy's smart collections)\n * and creates it through the quota-enforcing resources route; this runs the\n * same model over the stored product and creates it under the save route's\n * create door (the plan's commerce entitlement and its product quota, counted\n * in the same transaction). `newProductId` is the client's, so a retry finds\n * its own copy and answers `replayed`.\n */\nexport function createProductDuplicateHandler(options: ProductsWriteDeps = {}): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const deps = resolved(options)\n const body = bodyOf(req)\n const { hostId, productId, newProductId } = body\n if (!validId(hostId) || !validId(productId) || !validId(newProductId) || productId === newProductId) {\n return res.status(400).json({ error: 'Missing site or product' })\n }\n try {\n const door = await gate(req, res, hostId, deps)\n if (!door) return\n const db = deps.firestore()\n const hostRef = db.collection('hosts').doc(hostId)\n const products = hostRef.collection('products')\n const copyRef = products.doc(newProductId)\n const already = await copyRef.get()\n if (already.exists) return res.status(200).json({ ok: true, id: newProductId, replayed: true })\n const source = await products.doc(productId).get()\n if (!source.exists || source.get('deletedAt')) return res.status(404).json({ error: 'This product was deleted' })\n const product = liftLegacyProduct((source.data() ?? {}) as never)\n const slug = await claimProductSlug(products, productDuplicateSlugBase(product.slug, product.name))\n const nowMs = deps.now()\n const fields = productDuplicateFields(product, slug, nowMs)\n const smartQuery = hostRef.collection('collections').where('kind', '==', 'catalog').where('mode', '==', 'smart')\n const outbox = outboxEntry(db, hostId)\n const answer = await db.runTransaction(async (tx) => {\n const [existing, smart] = await Promise.all([tx.get(copyRef), tx.get(smartQuery)])\n if (existing.exists) return { status: 200, body: { ok: true, id: newProductId, replayed: true } }\n const early = planRefusal(door.org as never)\n if (early) return { status: early.status, body: { error: early.error } }\n const used = (await tx.get(products.count())).data().count\n const room = roomRefusal(door.org as never, Number(used) || 0)\n if (room) return { status: room.status, body: { error: room.error } }\n // The COPY's smart collections: its name differs from the source's.\n const collectionIds = productCollectionIds(\n productMembershipInput({ name: productDuplicateName(product.name) }, product) as never,\n smart.docs.map((doc) => smartCollectionRulesOf(doc.id, doc.data())),\n )\n tx.create(copyRef, {\n ...fields,\n collectionIds,\n deletedAt: null,\n createdAt: FieldValue.serverTimestamp(),\n updatedAt: FieldValue.serverTimestamp(),\n createdBy: door.uid,\n })\n tx.set(outbox.ref, outbox.data)\n return { status: 200, body: { ok: true, id: newProductId, replayed: false, slug } }\n })\n if (answer.status === 200 && !(answer.body as { replayed?: boolean }).replayed) {\n await settleCache(deps, hostId, outbox.ref, 'product duplicated from the app')\n }\n return res.status(answer.status).json(answer.body)\n } catch (error) {\n console.error('[commerce/products/duplicate]', error)\n return res.status(500).json({ error: 'Could not duplicate product' })\n }\n }\n}\n\n/**\n * `POST /api/commerce/products/delete` (AGL-3841): the products list's Delete\n * for the native apps, the console's write (`deletedAt` on the product, which\n * stops it being purchasable) with the site-wide cache drop, under the save\n * route's gate. Deleting a deleted product answers `already`.\n */\nexport function createProductDeleteHandler(options: ProductsWriteDeps = {}): PluginApiHandler {\n return async (req, res) => {\n if (req.method !== 'POST') return res.status(405).json({ error: 'Method not allowed' })\n const deps = resolved(options)\n const body = bodyOf(req)\n const { hostId, productId } = body\n if (!validId(hostId) || !validId(productId)) return res.status(400).json({ error: 'Missing site or product' })\n try {\n const door = await gate(req, res, hostId, deps)\n if (!door) return\n const db = deps.firestore()\n const productRef = db.collection('hosts').doc(hostId).collection('products').doc(productId)\n const outbox = outboxEntry(db, hostId)\n const answer = await db.runTransaction(async (tx) => {\n const stored = await tx.get(productRef)\n if (!stored.exists) return { status: 404, body: { error: 'This product no longer exists' } }\n if (stored.get('deletedAt')) return { status: 200, body: { ok: true, already: true } }\n tx.update(productRef, { deletedAt: FieldValue.serverTimestamp() })\n tx.set(outbox.ref, outbox.data)\n return { status: 200, body: { ok: true, already: false } }\n })\n if (answer.status === 200 && !(answer.body as { already?: boolean }).already) {\n await settleCache(deps, hostId, outbox.ref, 'product deleted from the app')\n }\n return res.status(answer.status).json(answer.body)\n } catch (error) {\n console.error('[commerce/products/delete]', error)\n return res.status(500).json({ error: 'Could not delete product' })\n }\n }\n}\n\nexport const productSaveHandler = createProductSaveHandler()\nexport const productStockHandler = createProductStockHandler()\nexport const productDuplicateHandler = createProductDuplicateHandler()\nexport const productDeleteHandler = createProductDeleteHandler()\n"],"names":["createHash","isRefusedIdToken","createResourceUid","hostRoleCanWrite","dropPluginSiteCache","firebaseAdmin","getLockdownVerdict","getOrgForHost","isImpersonationSession","FieldValue","liftLegacyProduct","productCollectionIds","productDuplicateFields","productDuplicateName","productDuplicateSlugBase","productEditWrite","productMembershipInput","productSaveFields","smartCollectionRulesOf","stockAdjustmentWrite","validateProduct","planRefusal","roomRefusal","ROLE_REFUSAL","MAX_BODY_BYTES","STOCK_ADJUST_REASONS","bodyOf","req","raw","body","length","JSON","parse","validId","value","test","gate","res","hostId","deps","host","authorization","String","headers","token","startsWith","slice","status","json","error","decoded","verifyIdToken","staff","firestore","collection","doc","get","exists","uid","org","orgForHost","locked","lockdown","data","request","method","outboxEntry","ref","paths","createdAt","serverTimestamp","attempts","entireHost","settleCache","entry","reason","dropped","dropCache","hostIds","catch","complete","delete","undefined","resolved","app","auth","options","now","Date","createProductSaveHandler","productId","product","Array","isArray","variants","name","trim","door","nowMs","fields","problem","db","hostRef","products","productRef","smartQuery","where","outbox","create","answer","runTransaction","tx","existing","smart","Promise","all","membership","collectionIds","docs","map","id","ok","replayed","early","used","count","room","Number","createdAtMs","deletedAt","updatedAt","createdBy","set","storedTimestamps","console","stored","asked","Object","fromEntries","entries","filter","key","toDate","stockAttemptId","update","digest","createProductStockHandler","variantId","delta","Math","round","locationId","includes","isFinite","ledgerRef","ledger","inventory","some","variant","write","claimProductSlug","base","suffix","candidate","held","limit","empty","createProductDuplicateHandler","newProductId","source","copyRef","already","slug","createProductDeleteHandler","productSaveHandler","productStockHandler","productDuplicateHandler","productDeleteHandler"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,gBAAgB,QAAQ,mDAAkD;AACnF,SAASC,iBAAiB,QAAQ,6CAA4C;AAC9E,SAASC,gBAAgB,QAAQ,uCAAsC;AACvE,SAASC,mBAAmB,QAAQ,gDAA+C;AAEnF,SAASC,aAAa,EAAEC,kBAAkB,EAAEC,aAAa,EAAEC,sBAAsB,QAAQ,2BAA0B;AACnH,SAASC,UAAU,QAAQ,2BAA0B;AACrD,SAEEC,iBAAiB,EACjBC,oBAAoB,EACpBC,sBAAsB,EACtBC,oBAAoB,EACpBC,wBAAwB,EACxBC,gBAAgB,EAChBC,sBAAsB,EACtBC,iBAAiB,EACjBC,sBAAsB,EACtBC,oBAAoB,EACpBC,eAAe,QAEV,oBAAU;AACjB,SAASC,WAAW,EAAEC,WAAW,QAAQ,sBAAkB;AAoB3D,OAAO,MAAMC,eAAe,mCAAkC;AAC9D,MAAMC,iBAAiB;AAEvB,gDAAgD,GAChD,OAAO,MAAMC,uBAA6D;IAAC;IAAW;IAAc;IAAU;CAAS,CAAA;AAavH,OAAO,SAASC,OAAOC,GAAoC;IACzD,MAAMC,MAAMD,IAAIE,IAAI;IACpB,IAAI,OAAOD,QAAQ,UAAU;QAC3B,IAAIA,IAAIE,MAAM,GAAGN,gBAAgB,OAAO,CAAC;QACzC,IAAI;YACF,OAAOO,KAAKC,KAAK,CAACJ,OAAO;QAC3B,EAAE,eAAM;YACN,OAAO,CAAC;QACV;IACF;IACA,OAAQA,cAAAA,MAAO,CAAC;AAClB;AAEA,OAAO,MAAMK,UAAU,CAACC,QACtB,OAAOA,UAAU,YAAY,yBAAyBC,IAAI,CAACD,OAAM;AASnE;;;CAGC,GACD,OAAO,eAAeE,KAAKT,GAAoC,EAAEU,GAAU,EAAEC,MAAc,EAAEC,IAAiC;QAC/FZ,4BAiBIa;QAGnB;IApBd,MAAMC,gBAAgBC,QAAOf,6BAAAA,IAAIgB,OAAO,CAACF,aAAa,YAAzBd,6BAA6B;IAC1D,MAAMiB,QAAQH,cAAcI,UAAU,CAAC,aAAaJ,cAAcK,KAAK,CAAC,UAAUhB,MAAM,IAAI;IAC5F,IAAI,CAACc,OAAO,OAAO,AAAKP,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAkB,IAAI;IAC5E,IAAIC;IACJ,IAAI;QACFA,UAAU,MAAMX,KAAKY,aAAa,CAACP;IACrC,EAAE,OAAOK,OAAO;QACd,4EAA4E;QAC5E,IAAI,CAAChD,iBAAiBgD,QAAQ,MAAMA;QACpC,OAAO,AAAKZ,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA4B,IAAI;IAC5E;IACA,MAAMG,QAAQF,OAAO,CAAC,QAAQ,KAAK;IACnC,IAAI,CAACE,SAASF,OAAO,CAAC,iBAAiB,KAAK,QAAQ,CAAC1C,uBAAuB0C,UAAmB;QAC7F,OAAO,AAAKb,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA4C,IAAI;IAC5F;IACA,MAAMT,OAAO,MAAMD,KAAKc,SAAS,GAAGC,UAAU,CAAC,SAASC,GAAG,CAACjB,QAAQkB,GAAG;IACvE,IAAI,CAAChB,KAAKiB,MAAM,EAAE,OAAO,AAAKpB,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAAe,IAAI;IAC/E,IAAI,CAACG,SAAS,CAACjD,iBAAiB,EAACqC,YAAAA,KAAKgB,GAAG,CAAC,0BAAThB,YAA2B,CAAC,EAAE,CAACU,QAAQQ,GAAG,CAAC,GAAG;QAC7E,OAAO,AAAKrB,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO1B;QAAa,IAAI;IAC7D;IACA,MAAMoC,eAAQ,QAAA,MAAMpB,KAAKqB,UAAU,CAACtB,4BAAvB,AAAC,MAAgCqB,GAAG,mBAAI,CAAC;IACtD,MAAME,SAAS,MAAMtB,KAAKuB,QAAQ,CAAC;QAAEV;QAAOM,KAAKR,QAAQQ,GAAG;QAAEC,KAAKA;QAAcnB,MAAMA,KAAKuB,IAAI;QAAaC,SAAS;YAAEC,QAAQ;QAAO;IAAW;IAClJ,IAAIJ,QAAQ,OAAO,AAAKxB,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;QAAEC,OAAO;IAA+C,IAAI;IACzG,OAAO;QAAES,KAAKR,QAAQQ,GAAG;QAAEN;QAAOZ;QAAMmB;IAAI;AAC9C;AAEA,+GAA+G,GAC/G,OAAO,SAASO,YAAYb,SAAoB,EAAEf,MAAc;IAC9D,OAAO;QACL6B,KAAKd,UAAUC,UAAU,CAAC,iBAAiBC,GAAG,CAACrD;QAC/C6D,MAAM;YAAEzB;YAAQ8B,OAAO;gBAAC;aAAI;YAAEC,WAAW5D,WAAW6D,eAAe;YAAIC,UAAU;YAAGC,YAAY;QAAK;IACvG;AACF;AAEA,OAAO,eAAeC,YAAYlC,IAAiC,EAAED,MAAc,EAAEoC,KAA0C,EAAEC,MAAc;IAC7I,MAAMC,UAAU,MAAMrC,KAAKsC,SAAS,CAAC;QAAEC,SAAS;YAACxC;SAAO;QAAEqC;IAAO,GAAGI,KAAK,CAAC,IAAO,CAAA;YAAEC,UAAU;QAAM,CAAA;IACnG,IAAIJ,QAAQI,QAAQ,EAAE,MAAMN,MAAMO,MAAM,GAAGF,KAAK,CAAC,IAAMG;AACzD;AAEA,OAAO,SAASC,SAAS5C,IAAuB;QAEjCA,iBACIA,qBACHA,kBACFA,gBACCA,iBACNA;IANP,OAAO;QACLc,SAAS,GAAEd,kBAAAA,KAAKc,SAAS,YAAdd,kBAAmB,IAAMlC,cAAc+E,GAAG,GAAG/B,SAAS;QACjEF,aAAa,GAAEZ,sBAAAA,KAAKY,aAAa,YAAlBZ,sBAAuB,CAACK,QAAUvC,cAAc+E,GAAG,GAAGC,IAAI,GAAGlC,aAAa,CAACP;QAC1FgB,UAAU,GAAErB,mBAAAA,KAAKqB,UAAU,YAAfrB,mBAAoB,CAACD,SAAW/B,cAAc+B;QAC1DwB,QAAQ,GAAEvB,iBAAAA,KAAKuB,QAAQ,YAAbvB,iBAAkB,CAAC+C,UAAYhF,mBAAmBgF;QAC5DT,SAAS,GAAEtC,kBAAAA,KAAKsC,SAAS,YAAdtC,kBAAkBnC;QAC7BmF,GAAG,GAAEhD,YAAAA,KAAKgD,GAAG,YAARhD,YAAa,IAAMiD,KAAKD,GAAG;IAClC;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASE,yBAAyBH,UAA6B,CAAC,CAAC;IACtE,OAAO,OAAO3D,KAAKU;QACjB,IAAIV,IAAIsC,MAAM,KAAK,QAAQ,OAAO5B,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACrF,MAAMV,OAAO4C,SAASG;QACtB,MAAMzD,OAAOH,OAAOC;QACpB,MAAMW,SAAST,IAAI,CAAC,SAAS;QAC7B,MAAM6D,YAAY7D,IAAI,CAAC,YAAY;QACnC,MAAM8D,UAAU9D,IAAI,CAAC,UAAU;QAC/B,IAAI,CAACI,QAAQK,WAAW,CAACL,QAAQyD,cAAc,CAACC,WAAW,OAAOA,YAAY,YAAY,CAACC,MAAMC,OAAO,CAACF,QAAQG,QAAQ,GAAG;YAC1H,OAAOzD,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAwC;QAC/E;QACA,IAAI,OAAO0C,QAAQI,IAAI,KAAK,YAAY,CAACJ,QAAQI,IAAI,CAACC,IAAI,IAAI,OAAO3D,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA2B;QAC9H,IAAI;YACF,MAAMgD,OAAO,MAAM7D,KAAKT,KAAKU,KAAKC,QAAQC;YAC1C,IAAI,CAAC0D,MAAM;YACX,MAAMC,QAAQ3D,KAAKgD,GAAG;YACtB,MAAMY,SAASlF,kBAAkB0E,SAASO;YAC1C,MAAME,UAAUhF,gBAAgB+E;YAChC,IAAIC,SAAS,OAAO/D,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAOmD;YAAQ;YAC1D,MAAMC,KAAK9D,KAAKc,SAAS;YACzB,MAAMiD,UAAUD,GAAG/C,UAAU,CAAC,SAASC,GAAG,CAACjB;YAC3C,MAAMiE,WAAWD,QAAQhD,UAAU,CAAC;YACpC,MAAMkD,aAAaD,SAAShD,GAAG,CAACmC;YAChC,MAAMe,aAAaH,QAAQhD,UAAU,CAAC,eAAeoD,KAAK,CAAC,QAAQ,MAAM,WAAWA,KAAK,CAAC,QAAQ,MAAM;YACxG,MAAMC,SAASzC,YAAYmC,IAAI/D;YAC/B,MAAMsE,SAAS/E,IAAI,CAAC,SAAS,KAAK;YAClC,MAAMgF,SAAS,MAAMR,GAAGS,cAAc,CAAC,OAAOC;gBAC5C,MAAM,CAACC,UAAUC,MAAM,GAAG,MAAMC,QAAQC,GAAG,CAAC;oBAACJ,GAAGvD,GAAG,CAACgD;oBAAaO,GAAGvD,GAAG,CAACiD;iBAAY;gBACpF,MAAMW,aAAa;oBACjBC,eAAe1G,qBACbK,uBAAuBmF,QAAQR,UAC/BsB,MAAMK,IAAI,CAACC,GAAG,CAAC,CAAChE,MAAQrC,uBAAuBqC,IAAIiE,EAAE,EAAEjE,IAAIQ,IAAI;gBAEnE;gBACA,IAAI6C,QAAQ;oBACV,IAAII,SAASvD,MAAM,EAAE,OAAO;wBAAEV,QAAQ;wBAAKlB,MAAM;4BAAE4F,IAAI;4BAAMD,IAAI9B;4BAAWgC,UAAU;wBAAK;oBAAE;oBAC7F,MAAMC,QAAQtG,YAAY4E,KAAKtC,GAAG;oBAClC,IAAIgE,OAAO,OAAO;wBAAE5E,QAAQ4E,MAAM5E,MAAM;wBAAElB,MAAM;4BAAEoB,OAAO0E,MAAM1E,KAAK;wBAAC;oBAAE;oBACvE,MAAM2E,OAAO,AAAC,CAAA,MAAMb,GAAGvD,GAAG,CAAC+C,SAASsB,KAAK,GAAE,EAAG9D,IAAI,GAAG8D,KAAK;oBAC1D,MAAMC,OAAOxG,YAAY2E,KAAKtC,GAAG,EAAWoE,OAAOH,SAAS;oBAC5D,IAAIE,MAAM,OAAO;wBAAE/E,QAAQ+E,KAAK/E,MAAM;wBAAElB,MAAM;4BAAEoB,OAAO6E,KAAK7E,KAAK;wBAAC;oBAAE;oBACpE8D,GAAGH,MAAM,CAACJ,YAAY,aACjBL,QACAiB;wBACHY,aAAa9B;wBACb+B,WAAW;wBACX5D,WAAW5D,WAAW6D,eAAe;wBACrC4D,WAAWzH,WAAW6D,eAAe;wBACrC6D,WAAWlC,KAAKvC,GAAG;;gBAEvB,OAAO;wBAGiBsD;oBAFtB,IAAI,CAACA,SAASvD,MAAM,IAAIuD,SAASxD,GAAG,CAAC,cAAc,OAAO;wBAAET,QAAQ;wBAAKlB,MAAM;4BAAEoB,OAAO;wBAA2B;oBAAE;oBACrH8D,GAAGqB,GAAG,CAAC5B,YAAY,aACd6B,kBAAiBrB,iBAAAA,SAASjD,IAAI,cAAbiD,iBAAmB,CAAC,GAAGrB,UACxC5E,iBAAiBoF,QAAQa,SAASxD,GAAG,CAAC,gBACtC4D;wBACHc,WAAWzH,WAAW6D,eAAe;;gBAEzC;gBACAyC,GAAGqB,GAAG,CAACzB,OAAOxC,GAAG,EAAEwC,OAAO5C,IAAI;gBAC9B,OAAO;oBAAEhB,QAAQ;oBAAKlB,MAAM;wBAAE4F,IAAI;wBAAMD,IAAI9B;wBAAWgC,UAAU;oBAAM;gBAAE;YAC3E;YACA,IAAIb,OAAO9D,MAAM,KAAK,OAAO,CAAC,AAAC8D,OAAOhF,IAAI,CAA4B6F,QAAQ,EAAE;gBAC9E,MAAMjD,YAAYlC,MAAMD,QAAQqE,OAAOxC,GAAG,EAAE;YAC9C;YACA,OAAO9B,IAAIU,MAAM,CAAC8D,OAAO9D,MAAM,EAAEC,IAAI,CAAC6D,OAAOhF,IAAI;QACnD,EAAE,OAAOoB,OAAO;YACdqF,QAAQrF,KAAK,CAAC,4BAA4BA;YAC1C,OAAOZ,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAiC;QACxE;IACF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAASoF,iBAAiBE,MAA+B,EAAEC,KAA8B;IAC9F,OAAOC,OAAOC,WAAW,CACvBD,OAAOE,OAAO,CAACJ,QAAQK,MAAM,CAC3B,CAAC,CAACC,KAAK3G,MAAM,GAAK,CAAE2G,CAAAA,OAAOL,KAAI,KAAM,QAAQtG,yBAAD,AAACA,MAAuC4G,MAAM,MAAK;AAGrG;AAEA,2FAA2F,GAC3F,OAAO,SAASC,eAAerF,GAAW,EAAEmF,GAAW;IACrD,OAAO,SAAS7I,WAAW,UAAUgJ,MAAM,CAAC,GAAGtF,IAAI,CAAC,EAAEmF,KAAK,EAAEI,MAAM,CAAC,OAAOnG,KAAK,CAAC,GAAG;AACtF;AAEA;;;;;CAKC,GACD,OAAO,SAASoG,0BAA0B5D,UAA6B,CAAC,CAAC;IACvE,OAAO,OAAO3D,KAAKU;YAOEV;QANnB,IAAIA,IAAIsC,MAAM,KAAK,QAAQ,OAAO5B,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACrF,MAAMV,OAAO4C,SAASG;QACtB,MAAMzD,OAAOH,OAAOC;QACpB,MAAMW,SAAST,IAAI,CAAC,SAAS;QAC7B,MAAM6D,YAAY7D,IAAI,CAAC,YAAY;QACnC,MAAMsH,YAAYtH,IAAI,CAAC,YAAY;QACnC,MAAMgH,MAAMnG,QAAOf,8BAAAA,IAAIgB,OAAO,CAAC,kBAAkB,YAA9BhB,8BAAkC,IAAIqE,IAAI,GAAGlD,KAAK,CAAC,GAAG;QACzE,MAAM6B,SAAS9C,IAAI,CAAC,SAAS;QAC7B,MAAMuH,QAAQC,KAAKC,KAAK,CAACvB,OAAOlG,IAAI,CAAC,QAAQ;QAC7C,MAAM0H,aAAa,OAAO1H,IAAI,CAAC,aAAa,KAAK,YAAYA,IAAI,CAAC,aAAa,GAAGA,IAAI,CAAC,aAAa,GAAGqD;QACvG,IAAI,CAACjD,QAAQK,WAAW,CAACL,QAAQyD,cAAc,OAAOyD,cAAc,YAAY,CAACA,WAAW;YAC1F,OAAO9G,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAAmC;QAC1E;QACA,IAAI,CAAC4F,KAAK,OAAOxG,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA0B;QACzE,IAAI,CAACxB,qBAAqB+H,QAAQ,CAAC7E,SAAS,OAAOtC,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAiB;QAClG,IAAI,CAAC8E,OAAO0B,QAAQ,CAACL,UAAUA,UAAU,GAAG,OAAO/G,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA0B;QAC3G,IAAI;YACF,MAAMgD,OAAO,MAAM7D,KAAKT,KAAKU,KAAKC,QAAQC;YAC1C,IAAI,CAAC0D,MAAM;YACX,MAAMI,KAAK9D,KAAKc,SAAS;YACzB,MAAMiD,UAAUD,GAAG/C,UAAU,CAAC,SAASC,GAAG,CAACjB;YAC3C,MAAMkE,aAAaF,QAAQhD,UAAU,CAAC,YAAYC,GAAG,CAACmC;YACtD,MAAMgE,YAAYpD,QAAQhD,UAAU,CAAC,wBAAwBC,GAAG,CAACwF,eAAe9C,KAAKvC,GAAG,EAAEmF;YAC1F,MAAMlC,SAASzC,YAAYmC,IAAI/D;YAC/B,MAAMuE,SAAS,MAAMR,GAAGS,cAAc,CAAC,OAAOC;oBAOtCpB,mBAQ8D;gBAdpE,MAAM,CAACgE,QAAQpB,OAAO,GAAG,MAAMrB,QAAQC,GAAG,CAAC;oBAACJ,GAAGvD,GAAG,CAACkG;oBAAY3C,GAAGvD,GAAG,CAACgD;iBAAY;gBAClF,IAAImD,OAAOlG,MAAM,EAAE;wBACkD8E;oBAAnE,OAAO;wBAAExF,QAAQ;wBAAKlB,MAAM;4BAAE4F,IAAI;4BAAMC,UAAU;4BAAMkC,SAAS,GAAErB,cAAAA,OAAO/E,GAAG,CAAC,wBAAX+E,cAA2B;wBAAK;oBAAE;gBACvG;gBACA,IAAI,CAACA,OAAO9E,MAAM,IAAI8E,OAAO/E,GAAG,CAAC,cAAc,OAAO;oBAAET,QAAQ;oBAAKlB,MAAM;wBAAEoB,OAAO;oBAA2B;gBAAE;gBACjH,MAAM0C,UAAU4C,OAAOxE,IAAI;gBAC3B,IAAI,CAAC,EAAC4B,oBAAAA,QAAQG,QAAQ,YAAhBH,oBAAoB,EAAE,EAAEkE,IAAI,CAAC,CAACC,UAAYA,QAAQtC,EAAE,KAAK2B,YAAY;oBACzE,OAAO;wBAAEpG,QAAQ;wBAAKlB,MAAM;4BAAEoB,OAAO;wBAA4C;oBAAE;gBACrF;gBACA,MAAM8G,QAAQ5I,qBAAqBwE,SAASD,WAAW;oBAAEyD;oBAAWC;oBAAOzE;oBAAQ4E;gBAAW,GAAGhH,KAAKgD,GAAG;gBACzG,IAAI,CAACwE,OAAO,OAAO;oBAAEhH,QAAQ;oBAAKlB,MAAM;wBAAEoB,OAAO;oBAA0B;gBAAE;gBAC7E8D,GAAGiC,MAAM,CAACxC,YAAYuD,MAAMf,MAAM;gBAClCjC,GAAGH,MAAM,CAAC8C,WAAWK,MAAMJ,MAAM;gBACjC5C,GAAGqB,GAAG,CAACzB,OAAOxC,GAAG,EAAEwC,OAAO5C,IAAI;gBAC9B,OAAO;oBAAEhB,QAAQ;oBAAKlB,MAAM;wBAAE4F,IAAI;wBAAMC,UAAU;wBAAOkC,SAAS,GAAE,0BAAA,AAACG,MAAMf,MAAM,CAA6BY,SAAS,YAAnD,0BAAuD;oBAAK;gBAAE;YACpI;YACA,IAAI/C,OAAO9D,MAAM,KAAK,OAAO,CAAC,AAAC8D,OAAOhF,IAAI,CAA4B6F,QAAQ,EAAE;gBAC9E,MAAMjD,YAAYlC,MAAMD,QAAQqE,OAAOxC,GAAG,EAAE;YAC9C;YACA,OAAO9B,IAAIU,MAAM,CAAC8D,OAAO9D,MAAM,EAAEC,IAAI,CAAC6D,OAAOhF,IAAI;QACnD,EAAE,OAAOoB,OAAO;YACdqF,QAAQrF,KAAK,CAAC,6BAA6BA;YAC3C,OAAOZ,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA8B;QACrE;IACF;AACF;AAEA;;;;CAIC,GACD,OAAO,eAAe+G,iBAAiBzD,QAA+C,EAAE0D,IAAY;IAClG,IAAK,IAAIC,SAAS,GAAGA,SAAS,MAAMA,UAAU,EAAG;QAC/C,MAAMC,YAAYD,SAAS,IAAID,OAAO,GAAGA,KAAK,CAAC,EAAEC,QAAQ;QACzD,MAAME,OAAO,MAAM7D,SAASG,KAAK,CAAC,QAAQ,MAAMyD,WAAWE,KAAK,CAAC,GAAG7G,GAAG;QACvE,IAAI4G,KAAKE,KAAK,EAAE,OAAOH;IACzB;IACA,OAAO,GAAGF,KAAK,CAAC,EAAEzE,KAAKD,GAAG,IAAI;AAChC;AAEA;;;;;;;;;CASC,GACD,OAAO,SAASgF,8BAA8BjF,UAA6B,CAAC,CAAC;IAC3E,OAAO,OAAO3D,KAAKU;QACjB,IAAIV,IAAIsC,MAAM,KAAK,QAAQ,OAAO5B,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACrF,MAAMV,OAAO4C,SAASG;QACtB,MAAMzD,OAAOH,OAAOC;QACpB,MAAM,EAAEW,MAAM,EAAEoD,SAAS,EAAE8E,YAAY,EAAE,GAAG3I;QAC5C,IAAI,CAACI,QAAQK,WAAW,CAACL,QAAQyD,cAAc,CAACzD,QAAQuI,iBAAiB9E,cAAc8E,cAAc;YACnG,OAAOnI,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA0B;QACjE;QACA,IAAI;gBAWiCwH;YAVnC,MAAMxE,OAAO,MAAM7D,KAAKT,KAAKU,KAAKC,QAAQC;YAC1C,IAAI,CAAC0D,MAAM;YACX,MAAMI,KAAK9D,KAAKc,SAAS;YACzB,MAAMiD,UAAUD,GAAG/C,UAAU,CAAC,SAASC,GAAG,CAACjB;YAC3C,MAAMiE,WAAWD,QAAQhD,UAAU,CAAC;YACpC,MAAMoH,UAAUnE,SAAShD,GAAG,CAACiH;YAC7B,MAAMG,UAAU,MAAMD,QAAQlH,GAAG;YACjC,IAAImH,QAAQlH,MAAM,EAAE,OAAOpB,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEyE,IAAI;gBAAMD,IAAIgD;gBAAc9C,UAAU;YAAK;YAC7F,MAAM+C,SAAS,MAAMlE,SAAShD,GAAG,CAACmC,WAAWlC,GAAG;YAChD,IAAI,CAACiH,OAAOhH,MAAM,IAAIgH,OAAOjH,GAAG,CAAC,cAAc,OAAOnB,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA2B;YAC/G,MAAM0C,UAAUjF,mBAAmB+J,eAAAA,OAAO1G,IAAI,cAAX0G,eAAiB,CAAC;YACrD,MAAMG,OAAO,MAAMZ,iBAAiBzD,UAAUzF,yBAAyB6E,QAAQiF,IAAI,EAAEjF,QAAQI,IAAI;YACjG,MAAMG,QAAQ3D,KAAKgD,GAAG;YACtB,MAAMY,SAASvF,uBAAuB+E,SAASiF,MAAM1E;YACrD,MAAMO,aAAaH,QAAQhD,UAAU,CAAC,eAAeoD,KAAK,CAAC,QAAQ,MAAM,WAAWA,KAAK,CAAC,QAAQ,MAAM;YACxG,MAAMC,SAASzC,YAAYmC,IAAI/D;YAC/B,MAAMuE,SAAS,MAAMR,GAAGS,cAAc,CAAC,OAAOC;gBAC5C,MAAM,CAACC,UAAUC,MAAM,GAAG,MAAMC,QAAQC,GAAG,CAAC;oBAACJ,GAAGvD,GAAG,CAACkH;oBAAU3D,GAAGvD,GAAG,CAACiD;iBAAY;gBACjF,IAAIO,SAASvD,MAAM,EAAE,OAAO;oBAAEV,QAAQ;oBAAKlB,MAAM;wBAAE4F,IAAI;wBAAMD,IAAIgD;wBAAc9C,UAAU;oBAAK;gBAAE;gBAChG,MAAMC,QAAQtG,YAAY4E,KAAKtC,GAAG;gBAClC,IAAIgE,OAAO,OAAO;oBAAE5E,QAAQ4E,MAAM5E,MAAM;oBAAElB,MAAM;wBAAEoB,OAAO0E,MAAM1E,KAAK;oBAAC;gBAAE;gBACvE,MAAM2E,OAAO,AAAC,CAAA,MAAMb,GAAGvD,GAAG,CAAC+C,SAASsB,KAAK,GAAE,EAAG9D,IAAI,GAAG8D,KAAK;gBAC1D,MAAMC,OAAOxG,YAAY2E,KAAKtC,GAAG,EAAWoE,OAAOH,SAAS;gBAC5D,IAAIE,MAAM,OAAO;oBAAE/E,QAAQ+E,KAAK/E,MAAM;oBAAElB,MAAM;wBAAEoB,OAAO6E,KAAK7E,KAAK;oBAAC;gBAAE;gBACpE,oEAAoE;gBACpE,MAAMoE,gBAAgB1G,qBACpBK,uBAAuB;oBAAE+E,MAAMlF,qBAAqB8E,QAAQI,IAAI;gBAAE,GAAGJ,UACrEsB,MAAMK,IAAI,CAACC,GAAG,CAAC,CAAChE,MAAQrC,uBAAuBqC,IAAIiE,EAAE,EAAEjE,IAAIQ,IAAI;gBAEjEgD,GAAGH,MAAM,CAAC8D,SAAS,aACdvE;oBACHkB;oBACAY,WAAW;oBACX5D,WAAW5D,WAAW6D,eAAe;oBACrC4D,WAAWzH,WAAW6D,eAAe;oBACrC6D,WAAWlC,KAAKvC,GAAG;;gBAErBqD,GAAGqB,GAAG,CAACzB,OAAOxC,GAAG,EAAEwC,OAAO5C,IAAI;gBAC9B,OAAO;oBAAEhB,QAAQ;oBAAKlB,MAAM;wBAAE4F,IAAI;wBAAMD,IAAIgD;wBAAc9C,UAAU;wBAAOkD;oBAAK;gBAAE;YACpF;YACA,IAAI/D,OAAO9D,MAAM,KAAK,OAAO,CAAC,AAAC8D,OAAOhF,IAAI,CAA4B6F,QAAQ,EAAE;gBAC9E,MAAMjD,YAAYlC,MAAMD,QAAQqE,OAAOxC,GAAG,EAAE;YAC9C;YACA,OAAO9B,IAAIU,MAAM,CAAC8D,OAAO9D,MAAM,EAAEC,IAAI,CAAC6D,OAAOhF,IAAI;QACnD,EAAE,OAAOoB,OAAO;YACdqF,QAAQrF,KAAK,CAAC,iCAAiCA;YAC/C,OAAOZ,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA8B;QACrE;IACF;AACF;AAEA;;;;;CAKC,GACD,OAAO,SAAS4H,2BAA2BvF,UAA6B,CAAC,CAAC;IACxE,OAAO,OAAO3D,KAAKU;QACjB,IAAIV,IAAIsC,MAAM,KAAK,QAAQ,OAAO5B,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAqB;QACrF,MAAMV,OAAO4C,SAASG;QACtB,MAAMzD,OAAOH,OAAOC;QACpB,MAAM,EAAEW,MAAM,EAAEoD,SAAS,EAAE,GAAG7D;QAC9B,IAAI,CAACI,QAAQK,WAAW,CAACL,QAAQyD,YAAY,OAAOrD,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAA0B;QAC5G,IAAI;YACF,MAAMgD,OAAO,MAAM7D,KAAKT,KAAKU,KAAKC,QAAQC;YAC1C,IAAI,CAAC0D,MAAM;YACX,MAAMI,KAAK9D,KAAKc,SAAS;YACzB,MAAMmD,aAAaH,GAAG/C,UAAU,CAAC,SAASC,GAAG,CAACjB,QAAQgB,UAAU,CAAC,YAAYC,GAAG,CAACmC;YACjF,MAAMiB,SAASzC,YAAYmC,IAAI/D;YAC/B,MAAMuE,SAAS,MAAMR,GAAGS,cAAc,CAAC,OAAOC;gBAC5C,MAAMwB,SAAS,MAAMxB,GAAGvD,GAAG,CAACgD;gBAC5B,IAAI,CAAC+B,OAAO9E,MAAM,EAAE,OAAO;oBAAEV,QAAQ;oBAAKlB,MAAM;wBAAEoB,OAAO;oBAAgC;gBAAE;gBAC3F,IAAIsF,OAAO/E,GAAG,CAAC,cAAc,OAAO;oBAAET,QAAQ;oBAAKlB,MAAM;wBAAE4F,IAAI;wBAAMkD,SAAS;oBAAK;gBAAE;gBACrF5D,GAAGiC,MAAM,CAACxC,YAAY;oBAAEyB,WAAWxH,WAAW6D,eAAe;gBAAG;gBAChEyC,GAAGqB,GAAG,CAACzB,OAAOxC,GAAG,EAAEwC,OAAO5C,IAAI;gBAC9B,OAAO;oBAAEhB,QAAQ;oBAAKlB,MAAM;wBAAE4F,IAAI;wBAAMkD,SAAS;oBAAM;gBAAE;YAC3D;YACA,IAAI9D,OAAO9D,MAAM,KAAK,OAAO,CAAC,AAAC8D,OAAOhF,IAAI,CAA2B8I,OAAO,EAAE;gBAC5E,MAAMlG,YAAYlC,MAAMD,QAAQqE,OAAOxC,GAAG,EAAE;YAC9C;YACA,OAAO9B,IAAIU,MAAM,CAAC8D,OAAO9D,MAAM,EAAEC,IAAI,CAAC6D,OAAOhF,IAAI;QACnD,EAAE,OAAOoB,OAAO;YACdqF,QAAQrF,KAAK,CAAC,8BAA8BA;YAC5C,OAAOZ,IAAIU,MAAM,CAAC,KAAKC,IAAI,CAAC;gBAAEC,OAAO;YAA2B;QAClE;IACF;AACF;AAEA,OAAO,MAAM6H,qBAAqBrF,2BAA0B;AAC5D,OAAO,MAAMsF,sBAAsB7B,4BAA2B;AAC9D,OAAO,MAAM8B,0BAA0BT,gCAA+B;AACtE,OAAO,MAAMU,uBAAuBJ,6BAA4B"}
package/src/lib/server.js CHANGED
@@ -423,11 +423,19 @@ registerPluginJob({
423
423
  // on first call.
424
424
  registerPluginApiRoute('commerce/products/save', async (req, res)=>(await import("./server/products-write.js")).productSaveHandler(req, res));
425
425
  registerPluginApiRoute('commerce/products/stock', async (req, res)=>(await import("./server/products-write.js")).productStockHandler(req, res));
426
+ // The products list's Duplicate and Delete for the native apps (AGL-3841):
427
+ // the console's own copy and soft delete, with the site-wide cache drop.
428
+ registerPluginApiRoute('commerce/products/duplicate', async (req, res)=>(await import("./server/products-write.js")).productDuplicateHandler(req, res));
429
+ registerPluginApiRoute('commerce/products/delete', async (req, res)=>(await import("./server/products-write.js")).productDeleteHandler(req, res));
426
430
  // The order dialog's note and restock answer for the native apps
427
431
  // (AGL-3651, AGL-3652): the console's own timeline computation, in one
428
432
  // transaction on the stored order. Loaded on first call.
429
433
  registerPluginApiRoute('commerce/order-note', async (req, res)=>(await import("./server/order-annotations.js")).orderNoteHandler(req, res));
430
434
  registerPluginApiRoute('commerce/order-restock-answer', async (req, res)=>(await import("./server/order-annotations.js")).orderRestockAnswerHandler(req, res));
435
+ // The order dialog's Packing slip and Invoice for the native apps
436
+ // (AGL-3841): the console's own two pages over the stored order, which the
437
+ // apps print on the device's print sheet. Loaded on first call.
438
+ registerPluginApiRoute('commerce/order-print', async (req, res)=>(await import("./server/order-print.js")).orderPrintHandler(req, res));
431
439
  // Issue / void store credit (AGL-2226). Server-side because the host
432
440
  // catch-all in the Firestore rules would otherwise let a client write
433
441
  // its own `balanceCents`, which checkout applies as amount-off.