@aglyn/plugins-commerce 1.0.0-beta.234 → 1.0.0-beta.235
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +13 -13
- package/src/lib/components/cart.js +3 -2
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/console/commerce-analytics-card.component.js +3 -6
- package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
- package/src/lib/components/console/commerce-glance-card.component.js +9 -3
- package/src/lib/components/console/commerce-glance-card.component.js.map +1 -1
- package/src/lib/components/console/customer-notifications-card.component.js +3 -1
- package/src/lib/components/console/customer-notifications-card.component.js.map +1 -1
- package/src/lib/components/console/discounts-card.component.js +2 -3
- package/src/lib/components/console/discounts-card.component.js.map +1 -1
- package/src/lib/components/console/host-coupons-card.component.js +3 -4
- package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
- package/src/lib/components/console/local-delivery-settings-card.component.js +2 -2
- package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -1
- package/src/lib/components/console/locations-card.component.js +1 -1
- package/src/lib/components/console/locations-card.component.js.map +1 -1
- package/src/lib/components/console/member-posts-card.component.js +2 -2
- package/src/lib/components/console/member-posts-card.component.js.map +1 -1
- package/src/lib/components/console/order-detail-dialog.component.js +35 -30
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-fulfillment-panel.component.d.ts +11 -1
- package/src/lib/components/console/order-fulfillment-panel.component.js +75 -4
- package/src/lib/components/console/order-fulfillment-panel.component.js.map +1 -1
- package/src/lib/components/console/order-zones.d.ts +45 -1
- package/src/lib/components/console/order-zones.js +8 -0
- package/src/lib/components/console/order-zones.js.map +1 -1
- package/src/lib/components/console/pickup-delivery-queue-card.component.js +15 -1
- package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +3 -2
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/product-editor-dialog.component.d.ts +2 -0
- package/src/lib/components/console/product-editor-dialog.component.js +30 -12
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +13 -4
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.d.ts +2 -1
- package/src/lib/components/console/products-hub-zone.component.js +1 -1
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/reviews-moderation-card.component.js +1 -2
- package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -1
- package/src/lib/components/console/shipping-settings-card.component.js +2 -2
- package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
- package/src/lib/components/console/shippingeasy-card.component.js +1 -1
- package/src/lib/components/console/shippingeasy-card.component.js.map +1 -1
- package/src/lib/components/console/shipstation-card.component.js +1 -2
- package/src/lib/components/console/shipstation-card.component.js.map +1 -1
- package/src/lib/components/console/store-settings-card.component.js +3 -1
- package/src/lib/components/console/store-settings-card.component.js.map +1 -1
- package/src/lib/components/console/tax-settings-card.component.js +4 -1
- package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
- package/src/lib/components/order-status.js +24 -1
- package/src/lib/components/order-status.js.map +1 -1
- package/src/lib/components/product-detail.js +30 -16
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/components/product-grid.d.ts +7 -0
- package/src/lib/components/product-grid.js +50 -7
- package/src/lib/components/product-grid.js.map +1 -1
- package/src/lib/components/related-products.js +1 -1
- package/src/lib/components/related-products.js.map +1 -1
- package/src/lib/components/wishlist.js +1 -1
- package/src/lib/components/wishlist.js.map +1 -1
- package/src/lib/declarations.server.js +11 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +8 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce.d.ts +21 -0
- package/src/lib/model/commerce.js +39 -1
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/order-annotations.d.ts +54 -0
- package/src/lib/model/order-annotations.js +76 -0
- package/src/lib/model/order-annotations.js.map +1 -0
- package/src/lib/model/order-events.d.ts +2 -0
- package/src/lib/model/order-events.js.map +1 -1
- package/src/lib/model/order-fulfillment.d.ts +29 -0
- package/src/lib/model/order-fulfillment.js +63 -0
- package/src/lib/model/order-fulfillment.js.map +1 -1
- package/src/lib/model/order-local-fulfillment.d.ts +36 -0
- package/src/lib/model/order-local-fulfillment.js +19 -1
- package/src/lib/model/order-local-fulfillment.js.map +1 -1
- package/src/lib/model/order-status-view.d.ts +11 -0
- package/src/lib/model/order-status-view.js +20 -3
- package/src/lib/model/order-status-view.js.map +1 -1
- package/src/lib/model/product-write.js +8 -4
- package/src/lib/model/product-write.js.map +1 -1
- package/src/lib/server/api-v1/openapi.js +1 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -1
- package/src/lib/server/api-v1/order-view.d.ts +1 -0
- package/src/lib/server/api-v1/order-view.js +10 -0
- package/src/lib/server/api-v1/order-view.js.map +1 -1
- package/src/lib/server/cart-checkout.js +9 -1
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/catalog.d.ts +6 -0
- package/src/lib/server/catalog.js +3 -1
- package/src/lib/server/catalog.js.map +1 -1
- package/src/lib/server/checkout.js +11 -1
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/fulfill-order.d.ts +12 -0
- package/src/lib/server/fulfill-order.js +13 -0
- package/src/lib/server/fulfill-order.js.map +1 -1
- package/src/lib/server/local-delivery-records.d.ts +30 -0
- package/src/lib/server/local-delivery-records.js +284 -0
- package/src/lib/server/local-delivery-records.js.map +1 -0
- package/src/lib/server/order-annotations.d.ts +28 -0
- package/src/lib/server/order-annotations.js +143 -0
- package/src/lib/server/order-annotations.js.map +1 -0
- package/src/lib/server/order-events.js +2 -1
- package/src/lib/server/order-events.js.map +1 -1
- package/src/lib/server/order-notifications.js +39 -8
- package/src/lib/server/order-notifications.js.map +1 -1
- package/src/lib/server/pos-payment.js +9 -3
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/product-catalog.js +6 -2
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product-drafts.d.ts +36 -9
- package/src/lib/server/product-drafts.js +24 -5
- package/src/lib/server/product-drafts.js.map +1 -1
- package/src/lib/server/product.d.ts +6 -1
- package/src/lib/server/product.js +1 -1
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/products-write.d.ts +29 -0
- package/src/lib/server/products-write.js +7 -7
- package/src/lib/server/products-write.js.map +1 -1
- package/src/lib/server/shipment-records.js +2 -0
- package/src/lib/server/shipment-records.js.map +1 -1
- package/src/lib/server/site-page-resolver.d.ts +13 -0
- package/src/lib/server/site-page-resolver.js +138 -1
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server.js +5 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/utils/use-storefront-purchase-event.js +8 -2
- package/src/lib/utils/use-storefront-purchase-event.js.map +1 -1
|
@@ -19,7 +19,7 @@ import { checkEntitlement, checkQuota } from "@aglyn/aglyn/app-utils/plan-entitl
|
|
|
19
19
|
import { registerPluginResourceDraftWriter } from "@aglyn/aglyn/plugin-manager/plugin-resource-drafts";
|
|
20
20
|
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
21
21
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
22
|
-
import { COMMERCE_MAX_OPTION_VALUES, COMMERCE_MAX_OPTIONS, COMMERCE_MAX_PRICE_USD, COMMERCE_MAX_VARIANTS, commerceSlug, productCollectionIds, productPriceMissing, productSearchFields, unpricedProductDraft, validateProduct } from "../model/commerce.js";
|
|
22
|
+
import { COMMERCE_DEFAULT_PRICE_USD, COMMERCE_MAX_OPTION_VALUES, COMMERCE_MAX_OPTIONS, COMMERCE_MAX_PRICE_USD, COMMERCE_MAX_VARIANTS, commerceSlug, productCollectionIds, productPriceMissing, productSearchFields, unpricedProductDraft, validateProduct } from "../model/commerce.js";
|
|
23
23
|
/** The resource name this writer is registered under. */ export const PRODUCT_DRAFT_RESOURCE = 'product';
|
|
24
24
|
/** The longest product name the proposals and the editor keep. */ export const PRODUCT_DRAFT_NAME_MAX = 120;
|
|
25
25
|
/** The longest description a draft keeps. */ export const PRODUCT_DRAFT_DESCRIPTION_MAX = 5000;
|
|
@@ -33,6 +33,8 @@ export const PRODUCT_DRAFT_SEO_DESCRIPTION_MAX = 320;
|
|
|
33
33
|
/** The resources route's refusal at the plan's allowance. */ export function productDraftLimitRefusal(limit) {
|
|
34
34
|
return `Your plan includes ${limit} ${limit === 1 ? 'product' : 'products'} — ` + 'upgrade in Billing for more';
|
|
35
35
|
}
|
|
36
|
+
/** The most photos a draft is written with. */ export const PRODUCT_DRAFT_MEDIA_MAX = 4;
|
|
37
|
+
/** A photo address a draft keeps: an https URL or a path on the site, never a script or a data URL. */ const MEDIA_URL = /^(?:https:\/\/[^\s"'<>]+|\/[^\s"'<>]*)$/;
|
|
36
38
|
const PRODUCT_TYPES = [
|
|
37
39
|
'physical',
|
|
38
40
|
'digital',
|
|
@@ -124,9 +126,9 @@ function strings(label, value, problems) {
|
|
|
124
126
|
problems.push(`Those options make ${combinations} variants; a product holds at most ${COMMERCE_MAX_VARIANTS}`);
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
|
-
//
|
|
129
|
+
// The stated price, else the default (AGL-3676); `null` asks for none.
|
|
128
130
|
const rawPrice = content['priceUsd'];
|
|
129
|
-
let priceUsd = null;
|
|
131
|
+
let priceUsd = rawPrice === null ? null : COMMERCE_DEFAULT_PRICE_USD;
|
|
130
132
|
if (rawPrice !== undefined && rawPrice !== null) {
|
|
131
133
|
if (typeof rawPrice !== 'number' || !Number.isFinite(rawPrice) || rawPrice < 0 || rawPrice > COMMERCE_MAX_PRICE_USD || Math.round(rawPrice * 100) !== rawPrice * 100) {
|
|
132
134
|
problems.push(`A stated price is dollars and cents from 0 to ${COMMERCE_MAX_PRICE_USD}`);
|
|
@@ -134,6 +136,17 @@ function strings(label, value, problems) {
|
|
|
134
136
|
priceUsd = rawPrice;
|
|
135
137
|
}
|
|
136
138
|
}
|
|
139
|
+
const rawComingSoon = content['comingSoon'];
|
|
140
|
+
if (rawComingSoon !== undefined && typeof rawComingSoon !== 'boolean') {
|
|
141
|
+
problems.push('Coming soon is true or false');
|
|
142
|
+
}
|
|
143
|
+
const mediaUrls = strings('Photos', content['mediaUrls'], problems);
|
|
144
|
+
if (mediaUrls.length > PRODUCT_DRAFT_MEDIA_MAX) {
|
|
145
|
+
problems.push(`A product is written with at most ${PRODUCT_DRAFT_MEDIA_MAX} photos`);
|
|
146
|
+
}
|
|
147
|
+
if (mediaUrls.some((url)=>url.length > 2000 || !MEDIA_URL.test(url))) {
|
|
148
|
+
problems.push('A photo is an https address or a path on this site');
|
|
149
|
+
}
|
|
137
150
|
if (problems.length) return {
|
|
138
151
|
ok: false,
|
|
139
152
|
problems: [
|
|
@@ -153,7 +166,9 @@ function strings(label, value, problems) {
|
|
|
153
166
|
seoDescription
|
|
154
167
|
},
|
|
155
168
|
priceUsd,
|
|
156
|
-
categoryIds
|
|
169
|
+
categoryIds,
|
|
170
|
+
comingSoon: rawComingSoon === true,
|
|
171
|
+
mediaUrls
|
|
157
172
|
}
|
|
158
173
|
};
|
|
159
174
|
}
|
|
@@ -166,7 +181,11 @@ function strings(label, value, problems) {
|
|
|
166
181
|
});
|
|
167
182
|
return _extends({}, priced, productSearchFields(priced), {
|
|
168
183
|
slug
|
|
169
|
-
}, read.
|
|
184
|
+
}, read.comingSoon ? {
|
|
185
|
+
status: 'active'
|
|
186
|
+
} : {}, read.mediaUrls.length ? {
|
|
187
|
+
mediaUrls: read.mediaUrls
|
|
188
|
+
} : {}, read.categoryIds.length ? {
|
|
170
189
|
categoryIds: read.categoryIds
|
|
171
190
|
} : {});
|
|
172
191
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftContext,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n COMMERCE_MAX_OPTION_VALUES,\n COMMERCE_MAX_OPTIONS,\n COMMERCE_MAX_PRICE_USD,\n COMMERCE_MAX_VARIANTS,\n commerceSlug,\n type HostProduct,\n productCollectionIds,\n productPriceMissing,\n productSearchFields,\n type ProductType,\n type ProposedProduct,\n type SmartCollectionRules,\n unpricedProductDraft,\n type UnpricedProductDraft,\n validateProduct,\n} from '../model/commerce'\n\n/**\n * A PRODUCT ANOTHER PLUGIN ASKS FOR (AGL-3616).\n *\n * The draft writer this plugin registers for the `product` resource on the\n * core's resource-drafts seam. A plugin that sets a store up from a brief\n * asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is the one the products card makes from an accepted AI\n * proposal (`unpricedProductDraft`): a `draft` — off the storefront, and\n * refused by every sale door — with one variant per combination of its\n * options, no photo, and the search, stock and smart-collection keys every\n * product write derives. Born live (`deletedAt: null`), as the product\n * resource stamps it, so the products table lists it.\n * - THE PRICE is left empty unless the content states one. An unpriced\n * product is marked for the merchant to price: the editor will not save it\n * and the card will not activate it until every variant has a price.\n * - THE ROOM is the `productsPerHost` allowance, counted the way the\n * resources route and the importer count it, inside the transaction that\n * creates. THE PLAN is the `commerce` feature, and THE ROLE a member who\n * may write the site's content — refused in the resources route's words.\n * - THE CHECK is `validateProduct`, the rule every product write is held to,\n * with a placeholder price standing in where none was stated.\n *\n * Nothing is published, priced or sold. The writer touches the one new\n * product document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const PRODUCT_DRAFT_RESOURCE = 'product'\n\n/** The longest product name the proposals and the editor keep. */\nexport const PRODUCT_DRAFT_NAME_MAX = 120\n/** The longest description a draft keeps. */\nexport const PRODUCT_DRAFT_DESCRIPTION_MAX = 5_000\n/** The most tags a draft keeps, and the longest one. */\nexport const PRODUCT_DRAFT_TAGS_MAX = 20\nexport const PRODUCT_DRAFT_TAG_MAX = 40\n/** The most categories a draft is filed under. */\nexport const PRODUCT_DRAFT_CATEGORIES_MAX = 10\n/** The longest search-listing title and description a draft keeps. */\nexport const PRODUCT_DRAFT_SEO_TITLE_MAX = 120\nexport const PRODUCT_DRAFT_SEO_DESCRIPTION_MAX = 320\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const PRODUCT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n/** The resources route's refusal for a plan without the feature. */\nexport const PRODUCT_DRAFT_PLAN_REFUSAL = 'This feature is not included in your plan — see Billing'\n\n/** The resources route's refusal at the plan's allowance. */\nexport function productDraftLimitRefusal(limit: number): string {\n return `Your plan includes ${limit} ${limit === 1 ? 'product' : 'products'} — ` +\n 'upgrade in Billing for more'\n}\n\n/**\n * What a caller sends as `content`: a proposed product, as the AI products\n * job proposes one, with an optional stated price for every variant and the\n * ids of categories the site already has. A photo is never taken: the\n * merchant adds pictures in the editor.\n */\nexport interface ProductDraftContent {\n name: string\n type?: ProductType\n description?: string\n tags?: string[]\n options?: Array<{ name: string; values: string[] }>\n seoTitle?: string\n seoDescription?: string\n /** One price for every variant, in dollars; absent leaves them unpriced. */\n priceUsd?: number\n /** Ids of the site's product categories; one the site lacks is dropped. */\n categoryIds?: string[]\n}\n\nconst PRODUCT_TYPES: readonly ProductType[] = ['physical', 'digital', 'service']\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\nfunction strings(label: string, value: unknown, problems: string[]): string[] {\n if (value === undefined) return []\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {\n problems.push(`${label} are a list of words`)\n return []\n }\n return [...new Set(value.map(line).filter(Boolean))]\n}\n\n/** A draft as the writer will store it, before its slug and keys are settled. */\nexport interface ProductDraftRead {\n proposal: ProposedProduct\n /** The price stated for every variant, or `null` for the merchant to set. */\n priceUsd: number | null\n categoryIds: string[]\n}\n\nexport type ProductDraftContentRead =\n | { ok: true; value: ProductDraftRead }\n | { ok: false; problems: string[] }\n\n/** The content as the writer reads it, or every problem that stops it. */\nexport function readProductDraftContent(\n content: Readonly<Record<string, unknown>>,\n): ProductDraftContentRead {\n const problems: string[] = []\n const name = line(content['name'])\n if (!name) problems.push('The product needs a name')\n else if (name.length > PRODUCT_DRAFT_NAME_MAX) {\n problems.push(`The name is longer than ${PRODUCT_DRAFT_NAME_MAX} characters`)\n }\n const rawType = content['type'] ?? 'physical'\n if (!(PRODUCT_TYPES as readonly unknown[]).includes(rawType)) {\n problems.push('The type is physical, digital or service')\n }\n const rawDescription = content['description']\n const description = typeof rawDescription === 'string' ? rawDescription.trim() : ''\n if (rawDescription !== undefined && typeof rawDescription !== 'string') {\n problems.push('The description is text')\n } else if (description.length > PRODUCT_DRAFT_DESCRIPTION_MAX) {\n problems.push(`The description is longer than ${PRODUCT_DRAFT_DESCRIPTION_MAX} characters`)\n }\n const tags = strings('Tags', content['tags'], problems)\n if (tags.length > PRODUCT_DRAFT_TAGS_MAX) problems.push(`A product keeps at most ${PRODUCT_DRAFT_TAGS_MAX} tags`)\n if (tags.some((tag) => tag.length > PRODUCT_DRAFT_TAG_MAX)) {\n problems.push(`A tag is longer than ${PRODUCT_DRAFT_TAG_MAX} characters`)\n }\n const categoryIds = strings('Categories', content['categoryIds'], problems)\n if (categoryIds.length > PRODUCT_DRAFT_CATEGORIES_MAX) {\n problems.push(`A product is filed under at most ${PRODUCT_DRAFT_CATEGORIES_MAX} categories`)\n }\n const seoTitle = line(content['seoTitle'])\n const seoDescription = line(content['seoDescription'])\n if (seoTitle.length > PRODUCT_DRAFT_SEO_TITLE_MAX) {\n problems.push(`The search title is longer than ${PRODUCT_DRAFT_SEO_TITLE_MAX} characters`)\n }\n if (seoDescription.length > PRODUCT_DRAFT_SEO_DESCRIPTION_MAX) {\n problems.push(`The search description is longer than ${PRODUCT_DRAFT_SEO_DESCRIPTION_MAX} characters`)\n }\n\n // Options are held here before the draft expands them: a matrix past the\n // variant ceiling is refused, never built.\n const rawOptions = content['options'] ?? []\n const options: ProposedProduct['options'] = []\n if (!Array.isArray(rawOptions)) {\n problems.push('Options are a list of a name and its values')\n } else {\n if (rawOptions.length > COMMERCE_MAX_OPTIONS) {\n problems.push(`At most ${COMMERCE_MAX_OPTIONS} options per product`)\n }\n for (const option of rawOptions) {\n const optionName = isRecord(option) ? line(option['name']) : ''\n const values = isRecord(option) ? strings(`The values of \"${optionName}\"`, option['values'], problems) : []\n if (!optionName || !values.length) {\n problems.push('Each option needs a name and at least one value')\n continue\n }\n if (values.length > COMMERCE_MAX_OPTION_VALUES) {\n problems.push(`Option \"${optionName}\" has too many values`)\n continue\n }\n options.push({ name: optionName, values })\n }\n if (new Set(options.map((option) => option.name)).size !== options.length) {\n problems.push('Each option needs its own name')\n }\n const combinations = options.reduce((count, option) => count * option.values.length, 1)\n if (combinations > COMMERCE_MAX_VARIANTS) {\n problems.push(`Those options make ${combinations} variants; a product holds at most ${COMMERCE_MAX_VARIANTS}`)\n }\n }\n\n // No price unless one is stated: the merchant sets it otherwise.\n const rawPrice = content['priceUsd']\n let priceUsd: number | null = null\n if (rawPrice !== undefined && rawPrice !== null) {\n if (\n typeof rawPrice !== 'number' ||\n !Number.isFinite(rawPrice) ||\n rawPrice < 0 ||\n rawPrice > COMMERCE_MAX_PRICE_USD ||\n Math.round(rawPrice * 100) !== rawPrice * 100\n ) {\n problems.push(`A stated price is dollars and cents from 0 to ${COMMERCE_MAX_PRICE_USD}`)\n } else {\n priceUsd = rawPrice\n }\n }\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n value: {\n proposal: {\n name,\n type: rawType as ProductType,\n description,\n tags,\n options,\n seoTitle,\n seoDescription,\n },\n priceUsd,\n categoryIds,\n },\n }\n}\n\n/** The product document a read builds, under `slug`, at `nowMs`. */\nexport function productDraftDocument(\n read: ProductDraftRead,\n slug: string,\n nowMs: number,\n): UnpricedProductDraft | HostProduct {\n const draft = unpricedProductDraft(read.proposal, new Set(), nowMs)\n const priced =\n read.priceUsd === null\n ? draft\n : ({\n ...draft,\n variants: draft.variants.map((variant) => ({ ...variant, priceUsd: read.priceUsd as number })),\n } as HostProduct)\n return {\n ...priced,\n // The search keys travel with every create; recomputed for the price.\n ...productSearchFields(priced as HostProduct),\n slug,\n ...(read.categoryIds.length ? { categoryIds: read.categoryIds } : {}),\n }\n}\n\n/** What the owner's product rule says about a draft, a placeholder price standing in for none. */\nfunction productProblem(product: UnpricedProductDraft | HostProduct): string | null {\n return validateProduct({\n ...product,\n variants: product.variants.map((variant) => ({\n ...variant,\n priceUsd: (variant as { priceUsd?: number }).priceUsd ?? 0,\n })),\n } as HostProduct)\n}\n\nfunction factsOf(product: Partial<HostProduct>) {\n return {\n status: product.status ?? 'draft',\n slug: product.slug ?? '',\n type: product.type ?? 'physical',\n variants: product.variants?.length ?? 0,\n priceMissing: productPriceMissing({ variants: product.variants ?? [] }),\n }\n}\n\n/** Whether content is a product this plugin would store, with what it says about it. Pure. */\nexport function checkProductDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readProductDraftContent(content)\n if (read.ok === false) return read\n const slug = commerceSlug(read.value.proposal.name) || 'product'\n const product = productDraftDocument(read.value, slug, 0)\n const problem = productProblem(product)\n return problem ? { ok: false, problems: [problem] } : { ok: true, facts: factsOf(product as HostProduct) }\n}\n\nfunction recordOf(product: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (product.data() ?? {}) as Partial<HostProduct>\n return { id: product.id, name: String(data.name ?? ''), versionId: null, facts: factsOf(data) }\n}\n\nexport function roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: PRODUCT_DRAFT_ROLE_REFUSAL }\n}\n\nexport function planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'commerce') ? null : { status: 403, error: PRODUCT_DRAFT_PLAN_REFUSAL }\n}\n\n/** The allowance, counted as the resources route and the importer count a product create. */\nexport function roomRefusal(org: PluginDraftContext['org'], used: number): PluginDraftRefusal | null {\n const quota = checkQuota(org as never, 'productsPerHost', used)\n return quota.allowed ? null : { status: 403, error: productDraftLimitRefusal(quota.limit) }\n}\n\n/** Candidates asked of the store at once, as the console's slug ledger probes. */\nconst SLUG_PROBE = 10\n\n/**\n * The first of `base`, `base-2`, `base-3`, … that no product holds, live or\n * deleted — a soft-deleted product still answers its slug on the storefront\n * (`productSlugLedger`, AGL-3321). Asked inside the transaction.\n */\nasync function freeSlug(\n products: FirebaseFirestore.CollectionReference,\n base: string,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<string> {\n const candidate = (suffix: number) => (suffix < 2 ? base : `${base}-${suffix}`)\n for (let first = 1; ; first += SLUG_PROBE) {\n const batch = Array.from({ length: SLUG_PROBE }, (_, offset) => candidate(first + offset))\n const held = new Set(\n (await read(products.where('slug', 'in', batch))).docs.map((doc) => String(doc.get('slug') ?? '')),\n )\n const free = batch.find((slug) => !held.has(slug))\n if (free) return free\n }\n}\n\nexport interface ProductDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createProductDraftWriter(deps: ProductDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n const early = roleRefusal(host, context.uid) ?? planRefusal(context.org)\n if (early) return early\n const used = (await hostRef.collection('products').count().get()).data().count\n return roomRefusal(context.org, Number(used) || 0)\n },\n\n check: (content) => checkProductDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const product = await firestore().collection('hosts').doc(hostId).collection('products').doc(id).get()\n return product.exists ? recordOf(product) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = line(request.name)\n const read = readProductDraftContent(asked ? { ...request.content, name: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const products = hostRef.collection('products')\n const productRef = products.doc(request.id)\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(productRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n // Asked again under the same id: the draft it already wrote.\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n const used = (await tx.get(products.count())).data().count\n const room = roomRefusal(request.org, Number(used) || 0)\n if (room) return { ok: false, ...room }\n const base = commerceSlug(read.value.proposal.name) || 'product'\n const [slug, categories, smart] = await Promise.all([\n freeSlug(products, base, (query) => tx.get(query)),\n // A category the site does not have is dropped, not stored.\n read.value.categoryIds.length\n ? Promise.all(read.value.categoryIds.map((id) => tx.get(hostRef.collection('productCategories').doc(id))))\n : Promise.resolve([]),\n tx.get(hostRef.collection('collections').where('kind', '==', 'catalog').where('mode', '==', 'smart')),\n ])\n const categoryIds = categories.filter((category) => category.exists).map((category) => category.id)\n const nowMs = request.now.getTime()\n const product = productDraftDocument({ ...read.value, categoryIds }, slug, nowMs)\n const problem = productProblem(product)\n if (problem) return { ok: false, status: 400, error: problem }\n const rules: SmartCollectionRules[] = smart.docs.map((doc) => ({\n id: doc.id,\n rules: doc.get('rules') ?? [],\n matchAll: doc.get('matchAll'),\n }))\n const priced = read.value.priceUsd !== null\n tx.create(productRef, {\n ...product,\n collectionIds: productCollectionIds(product as HostProduct, rules),\n // The legacy flat price every create path still writes, only where\n // there is one: an unpriced draft is not a free one.\n ...(priced ? { priceUsd: read.value.priceUsd } : {}),\n deletedAt: null,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: product.name,\n versionId: null,\n facts: factsOf(product as HostProduct),\n }\n })\n },\n }\n}\n\nexport const productDraftWriter = createProductDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerProductDraftWriter(): void {\n registerPluginResourceDraftWriter(PRODUCT_DRAFT_RESOURCE, productDraftWriter, { pluginId: BUNDLE_ID })\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","checkQuota","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","COMMERCE_MAX_OPTION_VALUES","COMMERCE_MAX_OPTIONS","COMMERCE_MAX_PRICE_USD","COMMERCE_MAX_VARIANTS","commerceSlug","productCollectionIds","productPriceMissing","productSearchFields","unpricedProductDraft","validateProduct","PRODUCT_DRAFT_RESOURCE","PRODUCT_DRAFT_NAME_MAX","PRODUCT_DRAFT_DESCRIPTION_MAX","PRODUCT_DRAFT_TAGS_MAX","PRODUCT_DRAFT_TAG_MAX","PRODUCT_DRAFT_CATEGORIES_MAX","PRODUCT_DRAFT_SEO_TITLE_MAX","PRODUCT_DRAFT_SEO_DESCRIPTION_MAX","PRODUCT_DRAFT_ROLE_REFUSAL","PRODUCT_DRAFT_PLAN_REFUSAL","productDraftLimitRefusal","limit","PRODUCT_TYPES","isRecord","value","Array","isArray","line","replace","trim","strings","label","problems","undefined","some","entry","push","Set","map","filter","Boolean","readProductDraftContent","content","name","length","rawType","includes","rawDescription","description","tags","tag","categoryIds","seoTitle","seoDescription","rawOptions","options","option","optionName","values","size","combinations","reduce","count","rawPrice","priceUsd","Number","isFinite","Math","round","ok","proposal","type","productDraftDocument","read","slug","nowMs","draft","priced","variants","variant","productProblem","product","factsOf","status","priceMissing","checkProductDraftContent","problem","facts","recordOf","data","id","String","versionId","roleRefusal","host","uid","role","get","error","planRefusal","org","roomRefusal","used","quota","allowed","SLUG_PROBE","freeSlug","products","base","candidate","suffix","first","batch","from","_","offset","held","where","docs","doc","free","find","has","createProductDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","hostId","exists","early","check","write","request","asked","db","productRef","runTransaction","tx","existing","Promise","all","replayed","room","categories","smart","query","resolve","category","now","getTime","rules","matchAll","create","collectionIds","deletedAt","createdAt","updatedAt","createdBy","productDraftWriter","registerProductDraftWriter","pluginId"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,gBAAgB,QAAQ,uCAAsC;AACvE,SAASC,gBAAgB,EAAEC,UAAU,QAAQ,2CAA0C;AACvF,SACEC,iCAAiC,QAO5B,qDAAoD;AAC3D,SAASC,aAAa,QAAQ,2BAA0B;AACxD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SACEC,0BAA0B,EAC1BC,oBAAoB,EACpBC,sBAAsB,EACtBC,qBAAqB,EACrBC,YAAY,EAEZC,oBAAoB,EACpBC,mBAAmB,EACnBC,mBAAmB,EAInBC,oBAAoB,EAEpBC,eAAe,QACV,uBAAmB;AA+B1B,uDAAuD,GACvD,OAAO,MAAMC,yBAAyB,UAAS;AAE/C,gEAAgE,GAChE,OAAO,MAAMC,yBAAyB,IAAG;AACzC,2CAA2C,GAC3C,OAAO,MAAMC,gCAAgC,KAAK;AAClD,sDAAsD,GACtD,OAAO,MAAMC,yBAAyB,GAAE;AACxC,OAAO,MAAMC,wBAAwB,GAAE;AACvC,gDAAgD,GAChD,OAAO,MAAMC,+BAA+B,GAAE;AAC9C,oEAAoE,GACpE,OAAO,MAAMC,8BAA8B,IAAG;AAC9C,OAAO,MAAMC,oCAAoC,IAAG;AAEpD,2EAA2E,GAC3E,OAAO,MAAMC,6BAA6B,mCAAkC;AAC5E,kEAAkE,GAClE,OAAO,MAAMC,6BAA6B,0DAAyD;AAEnG,2DAA2D,GAC3D,OAAO,SAASC,yBAAyBC,KAAa;IACpD,OAAO,CAAC,mBAAmB,EAAEA,MAAM,CAAC,EAAEA,UAAU,IAAI,YAAY,WAAW,GAAG,CAAC,GAC7E;AACJ;AAsBA,MAAMC,gBAAwC;IAAC;IAAY;IAAW;CAAU;AAEhF,SAASC,SAASC,KAAc;IAC9B,OAAO,OAAOA,UAAU,YAAYA,UAAU,QAAQ,CAACC,MAAMC,OAAO,CAACF;AACvE;AAEA,sEAAsE,GACtE,SAASG,KAAKH,KAAc;IAC1B,OAAO,OAAOA,UAAU,WAAWA,MAAMI,OAAO,CAAC,sDAAiB,KAAKC,IAAI,KAAK;AAClF;AAEA,SAASC,QAAQC,KAAa,EAAEP,KAAc,EAAEQ,QAAkB;IAChE,IAAIR,UAAUS,WAAW,OAAO,EAAE;IAClC,IAAI,CAACR,MAAMC,OAAO,CAACF,UAAUA,MAAMU,IAAI,CAAC,CAACC,QAAU,OAAOA,UAAU,WAAW;QAC7EH,SAASI,IAAI,CAAC,GAAGL,MAAM,oBAAoB,CAAC;QAC5C,OAAO,EAAE;IACX;IACA,OAAO;WAAI,IAAIM,IAAIb,MAAMc,GAAG,CAACX,MAAMY,MAAM,CAACC;KAAU;AACtD;AAcA,wEAAwE,GACxE,OAAO,SAASC,wBACdC,OAA0C;QAQ1BA,eA+BGA;IArCnB,MAAMV,WAAqB,EAAE;IAC7B,MAAMW,OAAOhB,KAAKe,OAAO,CAAC,OAAO;IACjC,IAAI,CAACC,MAAMX,SAASI,IAAI,CAAC;SACpB,IAAIO,KAAKC,MAAM,GAAGjC,wBAAwB;QAC7CqB,SAASI,IAAI,CAAC,CAAC,wBAAwB,EAAEzB,uBAAuB,WAAW,CAAC;IAC9E;IACA,MAAMkC,WAAUH,gBAAAA,OAAO,CAAC,OAAO,YAAfA,gBAAmB;IACnC,IAAI,CAAC,AAACpB,cAAqCwB,QAAQ,CAACD,UAAU;QAC5Db,SAASI,IAAI,CAAC;IAChB;IACA,MAAMW,iBAAiBL,OAAO,CAAC,cAAc;IAC7C,MAAMM,cAAc,OAAOD,mBAAmB,WAAWA,eAAelB,IAAI,KAAK;IACjF,IAAIkB,mBAAmBd,aAAa,OAAOc,mBAAmB,UAAU;QACtEf,SAASI,IAAI,CAAC;IAChB,OAAO,IAAIY,YAAYJ,MAAM,GAAGhC,+BAA+B;QAC7DoB,SAASI,IAAI,CAAC,CAAC,+BAA+B,EAAExB,8BAA8B,WAAW,CAAC;IAC5F;IACA,MAAMqC,OAAOnB,QAAQ,QAAQY,OAAO,CAAC,OAAO,EAAEV;IAC9C,IAAIiB,KAAKL,MAAM,GAAG/B,wBAAwBmB,SAASI,IAAI,CAAC,CAAC,wBAAwB,EAAEvB,uBAAuB,KAAK,CAAC;IAChH,IAAIoC,KAAKf,IAAI,CAAC,CAACgB,MAAQA,IAAIN,MAAM,GAAG9B,wBAAwB;QAC1DkB,SAASI,IAAI,CAAC,CAAC,qBAAqB,EAAEtB,sBAAsB,WAAW,CAAC;IAC1E;IACA,MAAMqC,cAAcrB,QAAQ,cAAcY,OAAO,CAAC,cAAc,EAAEV;IAClE,IAAImB,YAAYP,MAAM,GAAG7B,8BAA8B;QACrDiB,SAASI,IAAI,CAAC,CAAC,iCAAiC,EAAErB,6BAA6B,WAAW,CAAC;IAC7F;IACA,MAAMqC,WAAWzB,KAAKe,OAAO,CAAC,WAAW;IACzC,MAAMW,iBAAiB1B,KAAKe,OAAO,CAAC,iBAAiB;IACrD,IAAIU,SAASR,MAAM,GAAG5B,6BAA6B;QACjDgB,SAASI,IAAI,CAAC,CAAC,gCAAgC,EAAEpB,4BAA4B,WAAW,CAAC;IAC3F;IACA,IAAIqC,eAAeT,MAAM,GAAG3B,mCAAmC;QAC7De,SAASI,IAAI,CAAC,CAAC,sCAAsC,EAAEnB,kCAAkC,WAAW,CAAC;IACvG;IAEA,yEAAyE;IACzE,2CAA2C;IAC3C,MAAMqC,cAAaZ,mBAAAA,OAAO,CAAC,UAAU,YAAlBA,mBAAsB,EAAE;IAC3C,MAAMa,UAAsC,EAAE;IAC9C,IAAI,CAAC9B,MAAMC,OAAO,CAAC4B,aAAa;QAC9BtB,SAASI,IAAI,CAAC;IAChB,OAAO;QACL,IAAIkB,WAAWV,MAAM,GAAG3C,sBAAsB;YAC5C+B,SAASI,IAAI,CAAC,CAAC,QAAQ,EAAEnC,qBAAqB,oBAAoB,CAAC;QACrE;QACA,KAAK,MAAMuD,UAAUF,WAAY;YAC/B,MAAMG,aAAalC,SAASiC,UAAU7B,KAAK6B,MAAM,CAAC,OAAO,IAAI;YAC7D,MAAME,SAASnC,SAASiC,UAAU1B,QAAQ,CAAC,eAAe,EAAE2B,WAAW,CAAC,CAAC,EAAED,MAAM,CAAC,SAAS,EAAExB,YAAY,EAAE;YAC3G,IAAI,CAACyB,cAAc,CAACC,OAAOd,MAAM,EAAE;gBACjCZ,SAASI,IAAI,CAAC;gBACd;YACF;YACA,IAAIsB,OAAOd,MAAM,GAAG5C,4BAA4B;gBAC9CgC,SAASI,IAAI,CAAC,CAAC,QAAQ,EAAEqB,WAAW,qBAAqB,CAAC;gBAC1D;YACF;YACAF,QAAQnB,IAAI,CAAC;gBAAEO,MAAMc;gBAAYC;YAAO;QAC1C;QACA,IAAI,IAAIrB,IAAIkB,QAAQjB,GAAG,CAAC,CAACkB,SAAWA,OAAOb,IAAI,GAAGgB,IAAI,KAAKJ,QAAQX,MAAM,EAAE;YACzEZ,SAASI,IAAI,CAAC;QAChB;QACA,MAAMwB,eAAeL,QAAQM,MAAM,CAAC,CAACC,OAAON,SAAWM,QAAQN,OAAOE,MAAM,CAACd,MAAM,EAAE;QACrF,IAAIgB,eAAezD,uBAAuB;YACxC6B,SAASI,IAAI,CAAC,CAAC,mBAAmB,EAAEwB,aAAa,mCAAmC,EAAEzD,uBAAuB;QAC/G;IACF;IAEA,iEAAiE;IACjE,MAAM4D,WAAWrB,OAAO,CAAC,WAAW;IACpC,IAAIsB,WAA0B;IAC9B,IAAID,aAAa9B,aAAa8B,aAAa,MAAM;QAC/C,IACE,OAAOA,aAAa,YACpB,CAACE,OAAOC,QAAQ,CAACH,aACjBA,WAAW,KACXA,WAAW7D,0BACXiE,KAAKC,KAAK,CAACL,WAAW,SAASA,WAAW,KAC1C;YACA/B,SAASI,IAAI,CAAC,CAAC,8CAA8C,EAAElC,wBAAwB;QACzF,OAAO;YACL8D,WAAWD;QACb;IACF;IACA,IAAI/B,SAASY,MAAM,EAAE,OAAO;QAAEyB,IAAI;QAAOrC,UAAU;eAAI,IAAIK,IAAIL;SAAU;IAAC;IAC1E,OAAO;QACLqC,IAAI;QACJ7C,OAAO;YACL8C,UAAU;gBACR3B;gBACA4B,MAAM1B;gBACNG;gBACAC;gBACAM;gBACAH;gBACAC;YACF;YACAW;YACAb;QACF;IACF;AACF;AAEA,kEAAkE,GAClE,OAAO,SAASqB,qBACdC,IAAsB,EACtBC,IAAY,EACZC,KAAa;IAEb,MAAMC,QAAQpE,qBAAqBiE,KAAKH,QAAQ,EAAE,IAAIjC,OAAOsC;IAC7D,MAAME,SACJJ,KAAKT,QAAQ,KAAK,OACdY,QACC,aACIA;QACHE,UAAUF,MAAME,QAAQ,CAACxC,GAAG,CAAC,CAACyC,UAAa,aAAKA;gBAASf,UAAUS,KAAKT,QAAQ;;;IAExF,OAAO,aACFa,QAEAtE,oBAAoBsE;QACvBH;OACID,KAAKtB,WAAW,CAACP,MAAM,GAAG;QAAEO,aAAasB,KAAKtB,WAAW;IAAC,IAAI,CAAC;AAEvE;AAEA,gGAAgG,GAChG,SAAS6B,eAAeC,OAA2C;IACjE,OAAOxE,gBAAgB,aAClBwE;QACHH,UAAUG,QAAQH,QAAQ,CAACxC,GAAG,CAAC,CAACyC;gBAEpB;mBAFiC,aACxCA;gBACHf,QAAQ,GAAE,oBAAA,AAACe,QAAkCf,QAAQ,YAA3C,oBAA+C;;;;AAG/D;AAEA,SAASkB,QAAQD,OAA6B;QAElCA,iBACFA,eACAA,qBAEwCA;QADpCA;IAJZ,OAAO;QACLE,MAAM,GAAEF,kBAAAA,QAAQE,MAAM,YAAdF,kBAAkB;QAC1BP,IAAI,GAAEO,gBAAAA,QAAQP,IAAI,YAAZO,gBAAgB;QACtBV,IAAI,GAAEU,gBAAAA,QAAQV,IAAI,YAAZU,gBAAgB;QACtBH,QAAQ,WAAEG,qBAAAA,QAAQH,QAAQ,qBAAhBG,mBAAkBrC,MAAM,mBAAI;QACtCwC,cAAc9E,oBAAoB;YAAEwE,QAAQ,GAAEG,oBAAAA,QAAQH,QAAQ,YAAhBG,oBAAoB,EAAE;QAAC;IACvE;AACF;AAEA,4FAA4F,GAC5F,OAAO,SAASI,yBAAyB3C,OAA0C;IACjF,MAAM+B,OAAOhC,wBAAwBC;IACrC,IAAI+B,KAAKJ,EAAE,KAAK,OAAO,OAAOI;IAC9B,MAAMC,OAAOtE,aAAaqE,KAAKjD,KAAK,CAAC8C,QAAQ,CAAC3B,IAAI,KAAK;IACvD,MAAMsC,UAAUT,qBAAqBC,KAAKjD,KAAK,EAAEkD,MAAM;IACvD,MAAMY,UAAUN,eAAeC;IAC/B,OAAOK,UAAU;QAAEjB,IAAI;QAAOrC,UAAU;YAACsD;SAAQ;IAAC,IAAI;QAAEjB,IAAI;QAAMkB,OAAOL,QAAQD;IAAwB;AAC3G;AAEA,SAASO,SAASP,OAA2C;QAC7CA,eACwBQ;IADtC,MAAMA,QAAQR,gBAAAA,QAAQQ,IAAI,cAAZR,gBAAkB,CAAC;IACjC,OAAO;QAAES,IAAIT,QAAQS,EAAE;QAAE/C,MAAMgD,QAAOF,aAAAA,KAAK9C,IAAI,YAAT8C,aAAa;QAAKG,WAAW;QAAML,OAAOL,QAAQO;IAAM;AAChG;AAEA,OAAO,SAASI,YAAYC,IAAwC,EAAEC,GAAW;QACjED;IAAd,MAAME,OAAO,EAACF,YAAAA,KAAKG,GAAG,CAAC,0BAATH,YAA2B,CAAC,EAAE,CAACC,IAAI;IACjD,OAAOrG,iBAAiBsG,QAAQ,OAAO;QAAEb,QAAQ;QAAKe,OAAOhF;IAA2B;AAC1F;AAEA,OAAO,SAASiF,YAAYC,GAA8B;IACxD,OAAOzG,iBAAiByG,KAAc,cAAc,OAAO;QAAEjB,QAAQ;QAAKe,OAAO/E;IAA2B;AAC9G;AAEA,2FAA2F,GAC3F,OAAO,SAASkF,YAAYD,GAA8B,EAAEE,IAAY;IACtE,MAAMC,QAAQ3G,WAAWwG,KAAc,mBAAmBE;IAC1D,OAAOC,MAAMC,OAAO,GAAG,OAAO;QAAErB,QAAQ;QAAKe,OAAO9E,yBAAyBmF,MAAMlF,KAAK;IAAE;AAC5F;AAEA,gFAAgF,GAChF,MAAMoF,aAAa;AAEnB;;;;CAIC,GACD,eAAeC,SACbC,QAA+C,EAC/CC,IAAY,EACZnC,IAAkF;IAElF,MAAMoC,YAAY,CAACC,SAAoBA,SAAS,IAAIF,OAAO,GAAGA,KAAK,CAAC,EAAEE,QAAQ;IAC9E,IAAK,IAAIC,QAAQ,IAAKA,SAASN,WAAY;QACzC,MAAMO,QAAQvF,MAAMwF,IAAI,CAAC;YAAErE,QAAQ6D;QAAW,GAAG,CAACS,GAAGC,SAAWN,UAAUE,QAAQI;QAClF,MAAMC,OAAO,IAAI/E,IACf,AAAC,CAAA,MAAMoC,KAAKkC,SAASU,KAAK,CAAC,QAAQ,MAAML,OAAM,EAAGM,IAAI,CAAChF,GAAG,CAAC,CAACiF;gBAAeA;mBAAP5B,QAAO4B,WAAAA,IAAItB,GAAG,CAAC,mBAARsB,WAAmB;;QAEhG,MAAMC,OAAOR,MAAMS,IAAI,CAAC,CAAC/C,OAAS,CAAC0C,KAAKM,GAAG,CAAChD;QAC5C,IAAI8C,MAAM,OAAOA;IACnB;AACF;AAOA,OAAO,SAASG,yBAAyBC,OAA+B,CAAC,CAAC;QACtDA;IAAlB,MAAMC,aAAYD,kBAAAA,KAAKC,SAAS,YAAdD,kBAAmB,IAAM9H,cAAcgI,GAAG,GAAGD,SAAS;IACxE,OAAO;QACLE,SAAS,OAAOC;gBAIAnC;YAHd,MAAMoC,UAAUJ,YAAYK,UAAU,CAAC,SAASX,GAAG,CAACS,QAAQG,MAAM;YAClE,MAAMrC,OAAO,MAAMmC,QAAQhC,GAAG;YAC9B,IAAI,CAACH,KAAKsC,MAAM,EAAE,OAAO;gBAAEjD,QAAQ;gBAAKe,OAAO;YAAe;YAC9D,MAAMmC,SAAQxC,eAAAA,YAAYC,MAAMkC,QAAQjC,GAAG,aAA7BF,eAAkCM,YAAY6B,QAAQ5B,GAAG;YACvE,IAAIiC,OAAO,OAAOA;YAClB,MAAM/B,OAAO,AAAC,CAAA,MAAM2B,QAAQC,UAAU,CAAC,YAAYpE,KAAK,GAAGmC,GAAG,EAAC,EAAGR,IAAI,GAAG3B,KAAK;YAC9E,OAAOuC,YAAY2B,QAAQ5B,GAAG,EAAEnC,OAAOqC,SAAS;QAClD;QAEAgC,OAAO,CAAC5F,UAAY2C,yBAAyB3C;QAE7C+B,MAAM,OAAO,EAAE0D,MAAM,EAAEzC,EAAE,EAAE;YACzB,MAAMT,UAAU,MAAM4C,YAAYK,UAAU,CAAC,SAASX,GAAG,CAACY,QAAQD,UAAU,CAAC,YAAYX,GAAG,CAAC7B,IAAIO,GAAG;YACpG,OAAOhB,QAAQmD,MAAM,GAAG5C,SAASP,WAAW;QAC9C;QAEAsD,OAAO,OAAOC;YACZ,oEAAoE;YACpE,MAAMC,QAAQ9G,KAAK6G,QAAQ7F,IAAI;YAC/B,MAAM8B,OAAOhC,wBAAwBgG,QAAQ,aAAKD,QAAQ9F,OAAO;gBAAEC,MAAM8F;iBAAUD,QAAQ9F,OAAO;YAClG,IAAI+B,KAAKJ,EAAE,KAAK,OAAO,OAAO;gBAAEA,IAAI;gBAAOc,QAAQ;gBAAKe,OAAOzB,KAAKzC,QAAQ,CAAC,EAAE;YAAC;YAChF,MAAM0G,KAAKb;YACX,MAAMI,UAAUS,GAAGR,UAAU,CAAC,SAASX,GAAG,CAACiB,QAAQL,MAAM;YACzD,MAAMxB,WAAWsB,QAAQC,UAAU,CAAC;YACpC,MAAMS,aAAahC,SAASY,GAAG,CAACiB,QAAQ9C,EAAE;YAC1C,OAAOgD,GAAGE,cAAc,CAAC,OAAOC;oBAMhBhD;gBALd,yDAAyD;gBACzD,MAAM,CAACC,MAAMgD,SAAS,GAAG,MAAMC,QAAQC,GAAG,CAAC;oBAACH,GAAG5C,GAAG,CAACgC;oBAAUY,GAAG5C,GAAG,CAAC0C;iBAAY;gBAChF,IAAI,CAAC7C,KAAKsC,MAAM,EAAE,OAAO;oBAAE/D,IAAI;oBAAOc,QAAQ;oBAAKe,OAAO;gBAAe;gBACzE,6DAA6D;gBAC7D,IAAI4C,SAASV,MAAM,EAAE,OAAO;oBAAE/D,IAAI;oBAAM4E,UAAU;mBAASzD,SAASsD;gBACpE,MAAMT,SAAQxC,eAAAA,YAAYC,MAAM0C,QAAQzC,GAAG,aAA7BF,eAAkCM,YAAYqC,QAAQpC,GAAG;gBACvE,IAAIiC,OAAO,OAAO;oBAAEhE,IAAI;mBAAUgE;gBAClC,MAAM/B,OAAO,AAAC,CAAA,MAAMuC,GAAG5C,GAAG,CAACU,SAAS7C,KAAK,GAAE,EAAG2B,IAAI,GAAG3B,KAAK;gBAC1D,MAAMoF,OAAO7C,YAAYmC,QAAQpC,GAAG,EAAEnC,OAAOqC,SAAS;gBACtD,IAAI4C,MAAM,OAAO;oBAAE7E,IAAI;mBAAU6E;gBACjC,MAAMtC,OAAOxG,aAAaqE,KAAKjD,KAAK,CAAC8C,QAAQ,CAAC3B,IAAI,KAAK;gBACvD,MAAM,CAAC+B,MAAMyE,YAAYC,MAAM,GAAG,MAAML,QAAQC,GAAG,CAAC;oBAClDtC,SAASC,UAAUC,MAAM,CAACyC,QAAUR,GAAG5C,GAAG,CAACoD;oBAC3C,4DAA4D;oBAC5D5E,KAAKjD,KAAK,CAAC2B,WAAW,CAACP,MAAM,GACzBmG,QAAQC,GAAG,CAACvE,KAAKjD,KAAK,CAAC2B,WAAW,CAACb,GAAG,CAAC,CAACoD,KAAOmD,GAAG5C,GAAG,CAACgC,QAAQC,UAAU,CAAC,qBAAqBX,GAAG,CAAC7B,SAClGqD,QAAQO,OAAO,CAAC,EAAE;oBACtBT,GAAG5C,GAAG,CAACgC,QAAQC,UAAU,CAAC,eAAeb,KAAK,CAAC,QAAQ,MAAM,WAAWA,KAAK,CAAC,QAAQ,MAAM;iBAC7F;gBACD,MAAMlE,cAAcgG,WAAW5G,MAAM,CAAC,CAACgH,WAAaA,SAASnB,MAAM,EAAE9F,GAAG,CAAC,CAACiH,WAAaA,SAAS7D,EAAE;gBAClG,MAAMf,QAAQ6D,QAAQgB,GAAG,CAACC,OAAO;gBACjC,MAAMxE,UAAUT,qBAAqB,aAAKC,KAAKjD,KAAK;oBAAE2B;oBAAeuB,MAAMC;gBAC3E,MAAMW,UAAUN,eAAeC;gBAC/B,IAAIK,SAAS,OAAO;oBAAEjB,IAAI;oBAAOc,QAAQ;oBAAKe,OAAOZ;gBAAQ;gBAC7D,MAAMoE,QAAgCN,MAAM9B,IAAI,CAAChF,GAAG,CAAC,CAACiF;wBAE7CA;2BAFsD;wBAC7D7B,IAAI6B,IAAI7B,EAAE;wBACVgE,KAAK,GAAEnC,WAAAA,IAAItB,GAAG,CAAC,oBAARsB,WAAoB,EAAE;wBAC7BoC,UAAUpC,IAAItB,GAAG,CAAC;oBACpB;;gBACA,MAAMpB,SAASJ,KAAKjD,KAAK,CAACwC,QAAQ,KAAK;gBACvC6E,GAAGe,MAAM,CAACjB,YAAY,aACjB1D;oBACH4E,eAAexJ,qBAAqB4E,SAAwByE;mBAGxD7E,SAAS;oBAAEb,UAAUS,KAAKjD,KAAK,CAACwC,QAAQ;gBAAC,IAAI,CAAC;oBAClD8F,WAAW;oBACXC,WAAWvB,QAAQgB,GAAG;oBACtBQ,WAAWxB,QAAQgB,GAAG;oBACtBS,WAAWzB,QAAQzC,GAAG;;gBAExB,OAAO;oBACL1B,IAAI;oBACJ4E,UAAU;oBACVvD,IAAI8C,QAAQ9C,EAAE;oBACd/C,MAAMsC,QAAQtC,IAAI;oBAClBiD,WAAW;oBACXL,OAAOL,QAAQD;gBACjB;YACF;QACF;IACF;AACF;AAEA,OAAO,MAAMiF,qBAAqBvC,2BAA0B;AAE5D;;;CAGC,GACD,OAAO,SAASwC;IACdtK,kCAAkCa,wBAAwBwJ,oBAAoB;QAAEE,UAAUrK;IAAU;AACtG"}
|
|
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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"}
|
|
@@ -27,7 +27,12 @@ export interface PublicProductDetail {
|
|
|
27
27
|
variants: Array<{
|
|
28
28
|
id: string;
|
|
29
29
|
options?: Record<string, string>;
|
|
30
|
-
|
|
30
|
+
/**
|
|
31
|
+
* `null` for a variant listed before it has a price (AGL-3676): the page
|
|
32
|
+
* says "Price coming soon" and offers nothing to buy, as no sale door
|
|
33
|
+
* would sell it.
|
|
34
|
+
*/
|
|
35
|
+
priceUsd: number | null;
|
|
31
36
|
compareAtPriceUsd?: number;
|
|
32
37
|
soldOut: boolean;
|
|
33
38
|
imageUrl?: string;
|
|
@@ -47,7 +47,7 @@ import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
|
47
47
|
}, variant.options ? {
|
|
48
48
|
options: variant.options
|
|
49
49
|
} : {}, {
|
|
50
|
-
priceUsd: variant.priceUsd
|
|
50
|
+
priceUsd: CommerceModel.variantHasPrice(variant) ? variant.priceUsd : null
|
|
51
51
|
}, variant.compareAtPriceUsd ? {
|
|
52
52
|
compareAtPriceUsd: variant.compareAtPriceUsd
|
|
53
53
|
} : {}, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\n\nexport interface PublicProductDetail {\n id: string\n name: string\n slug: string\n description?: string\n type: CommerceModel.ProductType\n mediaUrls: string[]\n options: CommerceModel.ProductOption[]\n variants: Array<{\n id: string\n options?: Record<string, string>\n priceUsd: number\n compareAtPriceUsd?: number\n soldOut: boolean\n imageUrl?: string\n /**\n * Merchant's stock-keeping unit (AGL-686). Carried into the public DTO\n * so the server can put `sku` on the Product structured data — Google\n * Merchant treats it as required for a product offer, and without it a\n * listing is simply ineligible for rich results rather than warned about.\n * It is already public information: it prints on receipts and invoices.\n */\n sku?: string\n }>\n tags?: string[]\n /** Recurring billing (AGL-303) so the PDP frames the price. */\n subscription?: { interval: 'month' | 'year'; trialDays?: number }\n /** Buyer may choose one-time OR subscribe (AGL-545). */\n subscriptionOptional?: boolean\n}\n\n/**\n * Map a lifted product onto the public PDP DTO.\n *\n * Extracted (AGL-659) so the site-page resolver can put the SAME payload\n * into page props as the API returns — the PDP block used to fetch this in\n * an effect, which meant the server rendered a skeleton and crawlers saw a\n * product page with no product in it. One definition keeps the server-\n * rendered and client-fetched shapes from drifting.\n *\n * Display-only by design: stock is a `soldOut` boolean rather than a raw\n * count, and the charge still prices server-side.\n */\nexport function toPublicProductDetail(\n id: string,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n): PublicProductDetail {\n return {\n id,\n name: product.name,\n slug: product.slug,\n ...(product.description ? { description: product.description } : {}),\n type: product.type,\n mediaUrls:\n product.mediaUrls ?? (product.imageUrl ? [product.imageUrl] : []),\n options: product.options ?? [],\n variants: product.variants.map((variant) => ({\n id: variant.id,\n ...(variant.options ? { options: variant.options } : {}),\n priceUsd: variant.priceUsd,\n ...(variant.compareAtPriceUsd\n ? { compareAtPriceUsd: variant.compareAtPriceUsd }\n : {}),\n soldOut:\n variant.inventory != null &&\n Number(variant.inventory) <= 0 &&\n product.oversellPolicy !== 'backorder',\n ...(variant.imageUrl ? { imageUrl: variant.imageUrl } : {}),\n ...(variant.sku ? { sku: variant.sku } : {}),\n })),\n ...(product.tags?.length ? { tags: product.tags } : {}),\n ...(product.subscription ? { subscription: product.subscription } : {}),\n ...(product.subscription && product.subscriptionOptional\n ? { subscriptionOptional: true }\n : {}),\n }\n}\n\n/**\n * Public product detail by slug (AGL-292): everything the PDP block\n * needs — variants with per-option prices and stock booleans, never raw\n * counts. Display-only; the charge still prices server-side.\n */\nexport const productHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n const slug = String(req.query.slug ?? '')\n if (!hostId || !slug) {\n return res.status(400).json({ error: 'Missing hostId or slug' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('products')\n .where('slug', '==', slug)\n .limit(1)\n .get()\n const docSnapshot = snapshot.docs[0]\n const raw = docSnapshot?.data() as any\n if (!raw || raw.deletedAt || raw.status !== 'active') {\n return res.status(404).json({ error: 'Unknown product' })\n }\n const product = CommerceModel.liftLegacyProduct(raw)\n const detail = toPublicProductDetail(docSnapshot.id, product)\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=60, stale-while-revalidate=300',\n )\n return res.status(200).json({ product: detail })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Product unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","toPublicProductDetail","id","product","name","slug","description","type","mediaUrls","imageUrl","options","variants","map","variant","priceUsd","compareAtPriceUsd","soldOut","inventory","Number","oversellPolicy","sku","tags","length","subscription","subscriptionOptional","productHandler","req","res","hostId","String","query","status","json","error","firestore","app","snapshot","collection","doc","where","limit","get","docSnapshot","docs","raw","data","deletedAt","liftLegacyProduct","detail","setHeader","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAID,YAAYA,mBAAmB,oBAAU;AACzC,SAASC,aAAa,QAAQ,2BAA0B;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\n\nexport interface PublicProductDetail {\n id: string\n name: string\n slug: string\n description?: string\n type: CommerceModel.ProductType\n mediaUrls: string[]\n options: CommerceModel.ProductOption[]\n variants: Array<{\n id: string\n options?: Record<string, string>\n /**\n * `null` for a variant listed before it has a price (AGL-3676): the page\n * says \"Price coming soon\" and offers nothing to buy, as no sale door\n * would sell it.\n */\n priceUsd: number | null\n compareAtPriceUsd?: number\n soldOut: boolean\n imageUrl?: string\n /**\n * Merchant's stock-keeping unit (AGL-686). Carried into the public DTO\n * so the server can put `sku` on the Product structured data — Google\n * Merchant treats it as required for a product offer, and without it a\n * listing is simply ineligible for rich results rather than warned about.\n * It is already public information: it prints on receipts and invoices.\n */\n sku?: string\n }>\n tags?: string[]\n /** Recurring billing (AGL-303) so the PDP frames the price. */\n subscription?: { interval: 'month' | 'year'; trialDays?: number }\n /** Buyer may choose one-time OR subscribe (AGL-545). */\n subscriptionOptional?: boolean\n}\n\n/**\n * Map a lifted product onto the public PDP DTO.\n *\n * Extracted (AGL-659) so the site-page resolver can put the SAME payload\n * into page props as the API returns — the PDP block used to fetch this in\n * an effect, which meant the server rendered a skeleton and crawlers saw a\n * product page with no product in it. One definition keeps the server-\n * rendered and client-fetched shapes from drifting.\n *\n * Display-only by design: stock is a `soldOut` boolean rather than a raw\n * count, and the charge still prices server-side.\n */\nexport function toPublicProductDetail(\n id: string,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n): PublicProductDetail {\n return {\n id,\n name: product.name,\n slug: product.slug,\n ...(product.description ? { description: product.description } : {}),\n type: product.type,\n mediaUrls:\n product.mediaUrls ?? (product.imageUrl ? [product.imageUrl] : []),\n options: product.options ?? [],\n variants: product.variants.map((variant) => ({\n id: variant.id,\n ...(variant.options ? { options: variant.options } : {}),\n priceUsd: CommerceModel.variantHasPrice(variant) ? variant.priceUsd : null,\n ...(variant.compareAtPriceUsd\n ? { compareAtPriceUsd: variant.compareAtPriceUsd }\n : {}),\n soldOut:\n variant.inventory != null &&\n Number(variant.inventory) <= 0 &&\n product.oversellPolicy !== 'backorder',\n ...(variant.imageUrl ? { imageUrl: variant.imageUrl } : {}),\n ...(variant.sku ? { sku: variant.sku } : {}),\n })),\n ...(product.tags?.length ? { tags: product.tags } : {}),\n ...(product.subscription ? { subscription: product.subscription } : {}),\n ...(product.subscription && product.subscriptionOptional\n ? { subscriptionOptional: true }\n : {}),\n }\n}\n\n/**\n * Public product detail by slug (AGL-292): everything the PDP block\n * needs — variants with per-option prices and stock booleans, never raw\n * counts. Display-only; the charge still prices server-side.\n */\nexport const productHandler: PluginApiHandler = async (req, res) => {\n const hostId = String(req.query.hostId ?? '')\n const slug = String(req.query.slug ?? '')\n if (!hostId || !slug) {\n return res.status(400).json({ error: 'Missing hostId or slug' })\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const snapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('products')\n .where('slug', '==', slug)\n .limit(1)\n .get()\n const docSnapshot = snapshot.docs[0]\n const raw = docSnapshot?.data() as any\n if (!raw || raw.deletedAt || raw.status !== 'active') {\n return res.status(404).json({ error: 'Unknown product' })\n }\n const product = CommerceModel.liftLegacyProduct(raw)\n const detail = toPublicProductDetail(docSnapshot.id, product)\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=60, stale-while-revalidate=300',\n )\n return res.status(200).json({ product: detail })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Product unavailable' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","toPublicProductDetail","id","product","name","slug","description","type","mediaUrls","imageUrl","options","variants","map","variant","priceUsd","variantHasPrice","compareAtPriceUsd","soldOut","inventory","Number","oversellPolicy","sku","tags","length","subscription","subscriptionOptional","productHandler","req","res","hostId","String","query","status","json","error","firestore","app","snapshot","collection","doc","where","limit","get","docSnapshot","docs","raw","data","deletedAt","liftLegacyProduct","detail","setHeader","console"],"mappings":"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"}
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
import { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache';
|
|
18
18
|
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
19
19
|
import { getLockdownVerdict } from '@aglyn/tenant-data-admin';
|
|
20
|
+
import { FieldValue } from 'firebase-admin/firestore';
|
|
20
21
|
import { type InventoryAdjustmentReason } from '../model';
|
|
21
22
|
type Firestore = FirebaseFirestore.Firestore;
|
|
23
|
+
export declare const ROLE_REFUSAL = "Editing requires the editor role";
|
|
22
24
|
/** The reasons the Adjust stock dialog offers. */
|
|
23
25
|
export declare const STOCK_ADJUST_REASONS: readonly InventoryAdjustmentReason[];
|
|
24
26
|
export interface ProductsWriteDeps {
|
|
@@ -33,6 +35,33 @@ export interface ProductsWriteDeps {
|
|
|
33
35
|
dropCache?: typeof dropPluginSiteCache;
|
|
34
36
|
now?: () => number;
|
|
35
37
|
}
|
|
38
|
+
type Reply = Parameters<PluginApiHandler>[1];
|
|
39
|
+
export declare function bodyOf(req: Parameters<PluginApiHandler>[0]): Record<string, unknown>;
|
|
40
|
+
export declare const validId: (value: unknown) => value is string;
|
|
41
|
+
export interface Gate {
|
|
42
|
+
uid: string;
|
|
43
|
+
staff: boolean;
|
|
44
|
+
host: FirebaseFirestore.DocumentSnapshot;
|
|
45
|
+
org: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Who may write this site's products, as the rules decide it. Answers the
|
|
49
|
+
* caller's reply on a refusal and returns null.
|
|
50
|
+
*/
|
|
51
|
+
export declare function gate(req: Parameters<PluginApiHandler>[0], res: Reply, hostId: string, deps: Required<ProductsWriteDeps>): Promise<Gate | null>;
|
|
52
|
+
/** The site-wide cache drop a product change is owed (AGL-3386): an outbox entry in the write, then the drop. */
|
|
53
|
+
export declare function outboxEntry(firestore: Firestore, hostId: string): {
|
|
54
|
+
ref: FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
|
|
55
|
+
data: {
|
|
56
|
+
hostId: string;
|
|
57
|
+
paths: string[];
|
|
58
|
+
createdAt: FieldValue;
|
|
59
|
+
attempts: number;
|
|
60
|
+
entireHost: boolean;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
export declare function settleCache(deps: Required<ProductsWriteDeps>, hostId: string, entry: FirebaseFirestore.DocumentReference, reason: string): Promise<void>;
|
|
64
|
+
export declare function resolved(deps: ProductsWriteDeps): Required<ProductsWriteDeps>;
|
|
36
65
|
/**
|
|
37
66
|
* Saves a product as the console editor does. `create: true` makes a new
|
|
38
67
|
* product under the client-minted `productId` (a retry finds its own product
|
|
@@ -23,7 +23,7 @@ import { firebaseAdmin, getLockdownVerdict, getOrgForHost, isImpersonationSessio
|
|
|
23
23
|
import { FieldValue } from "firebase-admin/firestore";
|
|
24
24
|
import { productCollectionIds, productEditWrite, productMembershipInput, productSaveFields, smartCollectionRulesOf, stockAdjustmentWrite, validateProduct } from "../model/index.js";
|
|
25
25
|
import { planRefusal, roomRefusal } from "./product-drafts.js";
|
|
26
|
-
const ROLE_REFUSAL = 'Editing requires the editor role';
|
|
26
|
+
export const ROLE_REFUSAL = 'Editing requires the editor role';
|
|
27
27
|
const MAX_BODY_BYTES = 256000;
|
|
28
28
|
/** The reasons the Adjust stock dialog offers. */ export const STOCK_ADJUST_REASONS = [
|
|
29
29
|
'restock',
|
|
@@ -31,7 +31,7 @@ const MAX_BODY_BYTES = 256000;
|
|
|
31
31
|
'damage',
|
|
32
32
|
'refund'
|
|
33
33
|
];
|
|
34
|
-
function bodyOf(req) {
|
|
34
|
+
export function bodyOf(req) {
|
|
35
35
|
const raw = req.body;
|
|
36
36
|
if (typeof raw === 'string') {
|
|
37
37
|
if (raw.length > MAX_BODY_BYTES) return {};
|
|
@@ -43,11 +43,11 @@ function bodyOf(req) {
|
|
|
43
43
|
}
|
|
44
44
|
return raw != null ? raw : {};
|
|
45
45
|
}
|
|
46
|
-
const validId = (value)=>typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
46
|
+
export const validId = (value)=>typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value);
|
|
47
47
|
/**
|
|
48
48
|
* Who may write this site's products, as the rules decide it. Answers the
|
|
49
49
|
* caller's reply on a refusal and returns null.
|
|
50
|
-
*/ async function gate(req, res, hostId, deps) {
|
|
50
|
+
*/ export async function gate(req, res, hostId, deps) {
|
|
51
51
|
var _req_headers_authorization, _host_get, _ref;
|
|
52
52
|
var _this;
|
|
53
53
|
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
@@ -100,7 +100,7 @@ const validId = (value)=>typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.t
|
|
|
100
100
|
org
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
/** The site-wide cache drop a product change is owed (AGL-3386): an outbox entry in the write, then the drop. */ function outboxEntry(firestore, hostId) {
|
|
103
|
+
/** The site-wide cache drop a product change is owed (AGL-3386): an outbox entry in the write, then the drop. */ export function outboxEntry(firestore, hostId) {
|
|
104
104
|
return {
|
|
105
105
|
ref: firestore.collection('publishOutbox').doc(createResourceUid()),
|
|
106
106
|
data: {
|
|
@@ -114,7 +114,7 @@ const validId = (value)=>typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.t
|
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
116
|
}
|
|
117
|
-
async function settleCache(deps, hostId, entry, reason) {
|
|
117
|
+
export async function settleCache(deps, hostId, entry, reason) {
|
|
118
118
|
const dropped = await deps.dropCache({
|
|
119
119
|
hostIds: [
|
|
120
120
|
hostId
|
|
@@ -125,7 +125,7 @@ async function settleCache(deps, hostId, entry, reason) {
|
|
|
125
125
|
}));
|
|
126
126
|
if (dropped.complete) await entry.delete().catch(()=>undefined);
|
|
127
127
|
}
|
|
128
|
-
function resolved(deps) {
|
|
128
|
+
export function resolved(deps) {
|
|
129
129
|
var _deps_firestore, _deps_verifyIdToken, _deps_orgForHost, _deps_lockdown, _deps_dropCache, _deps_now;
|
|
130
130
|
return {
|
|
131
131
|
firestore: (_deps_firestore = deps.firestore) != null ? _deps_firestore : ()=>firebaseAdmin.app().firestore(),
|
|
@@ -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\nconst 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\nfunction 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\nconst validId = (value: unknown): value is string =>\n typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value)\n\ninterface 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 */\nasync 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. */\nfunction 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\nasync 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\nfunction 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":"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"}
|
|
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":"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"}
|
|
@@ -250,6 +250,8 @@ export const commerceShipmentRecords = {
|
|
|
250
250
|
} : {}, write.labelRef ? {
|
|
251
251
|
labelRef: write.labelRef,
|
|
252
252
|
idempotencyKey: `label:${write.labelRef}`
|
|
253
|
+
} : {}, write.labelCostCents ? {
|
|
254
|
+
labelCostCents: write.labelCostCents
|
|
253
255
|
} : {}));
|
|
254
256
|
switch(outcome.outcome){
|
|
255
257
|
case 'recorded':
|