@aglyn/plugins-commerce 1.0.0-beta.231 → 1.0.0-beta.233
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-extras.d.ts +6 -0
- package/src/lib/components/cart-extras.js +4 -1
- package/src/lib/components/cart-extras.js.map +1 -1
- package/src/lib/components/cart-fulfillment.d.ts +117 -0
- package/src/lib/components/cart-fulfillment.js +367 -0
- package/src/lib/components/cart-fulfillment.js.map +1 -0
- package/src/lib/components/cart.js +52 -10
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/commerce-console-page.js +42 -1
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/catalog-organization-card.component.js +1 -0
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -1
- package/src/lib/components/console/host-orders-card.component.js +42 -11
- package/src/lib/components/console/host-orders-card.component.js.map +1 -1
- package/src/lib/components/console/local-delivery-settings-card.component.d.ts +37 -0
- package/src/lib/components/console/local-delivery-settings-card.component.js +542 -0
- package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -0
- package/src/lib/components/console/locations-card.component.js +177 -3
- package/src/lib/components/console/locations-card.component.js.map +1 -1
- package/src/lib/components/console/order-detail-dialog.component.js +10 -0
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-local-fulfillment-panel.component.d.ts +44 -0
- package/src/lib/components/console/order-local-fulfillment-panel.component.js +265 -0
- package/src/lib/components/console/order-local-fulfillment-panel.component.js.map +1 -0
- package/src/lib/components/console/order-offline-notice.component.d.ts +12 -0
- package/src/lib/components/console/order-offline-notice.component.js +53 -0
- package/src/lib/components/console/order-offline-notice.component.js.map +1 -0
- package/src/lib/components/console/pickup-delivery-queue-card.component.d.ts +35 -0
- package/src/lib/components/console/pickup-delivery-queue-card.component.js +222 -0
- package/src/lib/components/console/pickup-delivery-queue-card.component.js.map +1 -0
- package/src/lib/components/console/pos/pos-api.d.ts +24 -1
- package/src/lib/components/console/pos/pos-api.js +13 -0
- package/src/lib/components/console/pos/pos-api.js.map +1 -1
- package/src/lib/components/console/pos/pos-item-dialog.component.d.ts +12 -0
- package/src/lib/components/console/pos/pos-item-dialog.component.js +10 -8
- package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-kiosk-queue.component.d.ts +22 -0
- package/src/lib/components/console/pos/pos-kiosk-queue.component.js +168 -0
- package/src/lib/components/console/pos/pos-kiosk-queue.component.js.map +1 -0
- package/src/lib/components/console/pos/pos-tender-dialogs.component.d.ts +18 -0
- package/src/lib/components/console/pos/pos-tender-dialogs.component.js +116 -1
- package/src/lib/components/console/pos/pos-tender-dialogs.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-tender-panel.component.d.ts +8 -0
- package/src/lib/components/console/pos/pos-tender-panel.component.js +77 -18
- package/src/lib/components/console/pos/pos-tender-panel.component.js.map +1 -1
- package/src/lib/components/console/pos/use-pos-display.d.ts +5 -2
- package/src/lib/components/console/pos/use-pos-display.js +3 -2
- package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
- package/src/lib/components/console/pos-devices-card.component.js +2 -2
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-display/pairing-screen.d.ts +5 -2
- package/src/lib/components/console/pos-display/pairing-screen.js +7 -5
- package/src/lib/components/console/pos-display/pairing-screen.js.map +1 -1
- package/src/lib/components/console/pos-display/pos-display-api.d.ts +7 -1
- package/src/lib/components/console/pos-display/pos-display-api.js +7 -2
- package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
- package/src/lib/components/console/pos-kiosk/kiosk-checkout.d.ts +41 -0
- package/src/lib/components/console/pos-kiosk/kiosk-checkout.js +275 -0
- package/src/lib/components/console/pos-kiosk/kiosk-checkout.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/kiosk-menu.d.ts +32 -0
- package/src/lib/components/console/pos-kiosk/kiosk-menu.js +344 -0
- package/src/lib/components/console/pos-kiosk/kiosk-menu.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/kiosk-staff.d.ts +12 -0
- package/src/lib/components/console/pos-kiosk/kiosk-staff.js +257 -0
- package/src/lib/components/console/pos-kiosk/kiosk-staff.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/pos-kiosk-api.d.ts +114 -0
- package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js +189 -0
- package/src/lib/components/console/pos-kiosk/pos-kiosk-api.js.map +1 -0
- package/src/lib/components/console/pos-kiosk/use-kiosk-idle.d.ts +16 -0
- package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js +80 -0
- package/src/lib/components/console/pos-kiosk/use-kiosk-idle.js.map +1 -0
- package/src/lib/components/console/pos-kiosk-page.component.d.ts +28 -0
- package/src/lib/components/console/pos-kiosk-page.component.js +791 -0
- package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-api.d.ts +44 -0
- package/src/lib/components/console/pos-offline/pos-offline-api.js +69 -0
- package/src/lib/components/console/pos-offline/pos-offline-api.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-banner.component.d.ts +7 -0
- package/src/lib/components/console/pos-offline/pos-offline-banner.component.js +135 -0
- package/src/lib/components/console/pos-offline/pos-offline-banner.component.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-checkout.component.d.ts +32 -0
- package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js +287 -0
- package/src/lib/components/console/pos-offline/pos-offline-checkout.component.js.map +1 -0
- package/src/lib/components/console/pos-offline/pos-offline-store.d.ts +63 -0
- package/src/lib/components/console/pos-offline/pos-offline-store.js +145 -0
- package/src/lib/components/console/pos-offline/pos-offline-store.js.map +1 -0
- package/src/lib/components/console/pos-offline/register-offline.d.ts +24 -0
- package/src/lib/components/console/pos-offline/register-offline.js +24 -0
- package/src/lib/components/console/pos-offline/register-offline.js.map +1 -0
- package/src/lib/components/console/pos-offline/use-pos-offline.d.ts +85 -0
- package/src/lib/components/console/pos-offline/use-pos-offline.js +542 -0
- package/src/lib/components/console/pos-offline/use-pos-offline.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js +72 -17
- package/src/lib/components/console/pos-ops/pos-shift-history-card.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.js +228 -51
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-zones.d.ts +23 -0
- package/src/lib/components/console/pos-zones.js +31 -0
- package/src/lib/components/console/pos-zones.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.js +10 -64
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +46 -30
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/shipping-settings-card.component.js +1 -1
- package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
- package/src/lib/components/console/stock-movements-card.component.js +48 -16
- package/src/lib/components/console/stock-movements-card.component.js.map +1 -1
- package/src/lib/components/console/store-zones.d.ts +8 -1
- package/src/lib/components/console/store-zones.js +11 -1
- package/src/lib/components/console/store-zones.js.map +1 -1
- package/src/lib/components/console/storefront-tax-summary-card.component.js +1 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -1
- package/src/lib/components/order-status.js +66 -0
- package/src/lib/components/order-status.js.map +1 -1
- package/src/lib/components/product-detail.js +42 -15
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/constants/local-fulfillment-list-query.d.ts +48 -0
- package/src/lib/constants/local-fulfillment-list-query.js +84 -0
- package/src/lib/constants/local-fulfillment-list-query.js.map +1 -0
- package/src/lib/constants/orders-list-query.d.ts +7 -2
- package/src/lib/constants/orders-list-query.js +89 -8
- package/src/lib/constants/orders-list-query.js.map +1 -1
- package/src/lib/constants/pos-shift-list-query.d.ts +22 -0
- package/src/lib/constants/pos-shift-list-query.js +115 -0
- package/src/lib/constants/pos-shift-list-query.js.map +1 -0
- package/src/lib/constants/product-list-query.d.ts +6 -1
- package/src/lib/constants/product-list-query.js +77 -8
- package/src/lib/constants/product-list-query.js.map +1 -1
- package/src/lib/constants/stock-movement-query.d.ts +3 -2
- package/src/lib/constants/stock-movement-query.js +52 -8
- package/src/lib/constants/stock-movement-query.js.map +1 -1
- package/src/lib/declarations.server.js +21 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/buyer-notifications.d.ts +5 -3
- package/src/lib/model/buyer-notifications.js +25 -4
- package/src/lib/model/buyer-notifications.js.map +1 -1
- package/src/lib/model/commerce-orders.d.ts +103 -1
- package/src/lib/model/commerce-orders.js +2 -1
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-pos-kiosk.d.ts +175 -0
- package/src/lib/model/commerce-pos-kiosk.js +147 -0
- package/src/lib/model/commerce-pos-kiosk.js.map +1 -0
- package/src/lib/model/commerce-pos-offline.d.ts +234 -0
- package/src/lib/model/commerce-pos-offline.js +301 -0
- package/src/lib/model/commerce-pos-offline.js.map +1 -0
- package/src/lib/model/commerce-pos-ops.d.ts +16 -6
- package/src/lib/model/commerce-pos-ops.js +12 -4
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +13 -1
- package/src/lib/model/commerce-pos.js +5 -4
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-stock-holds.d.ts +24 -3
- package/src/lib/model/commerce-stock-holds.js +26 -8
- package/src/lib/model/commerce-stock-holds.js.map +1 -1
- package/src/lib/model/commerce.d.ts +7 -1
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +5 -0
- package/src/lib/model/index.js +5 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/local-fulfillment-settings.d.ts +178 -0
- package/src/lib/model/local-fulfillment-settings.js +361 -0
- package/src/lib/model/local-fulfillment-settings.js.map +1 -0
- package/src/lib/model/order-list-fields.d.ts +10 -1
- package/src/lib/model/order-list-fields.js +1 -0
- package/src/lib/model/order-list-fields.js.map +1 -1
- package/src/lib/model/order-local-fulfillment.d.ts +131 -0
- package/src/lib/model/order-local-fulfillment.js +125 -0
- package/src/lib/model/order-local-fulfillment.js.map +1 -0
- package/src/lib/model/order-shipping-export.js +4 -0
- package/src/lib/model/order-shipping-export.js.map +1 -1
- package/src/lib/model/order-status-view.d.ts +20 -1
- package/src/lib/model/order-status-view.js +60 -6
- package/src/lib/model/order-status-view.js.map +1 -1
- package/src/lib/model/product-write.d.ts +186 -0
- package/src/lib/model/product-write.js +116 -0
- package/src/lib/model/product-write.js.map +1 -0
- package/src/lib/plugin-config.js +6 -2
- package/src/lib/plugin-config.js.map +1 -1
- package/src/lib/plugin.js +11 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/pos-kiosk-config.d.ts +26 -0
- package/src/lib/pos-kiosk-config.js +59 -0
- package/src/lib/pos-kiosk-config.js.map +1 -0
- package/src/lib/server/api-v1/openapi.js +8 -0
- package/src/lib/server/api-v1/openapi.js.map +1 -1
- package/src/lib/server/api-v1/order-view.d.ts +19 -0
- package/src/lib/server/api-v1/order-view.js +35 -0
- package/src/lib/server/api-v1/order-view.js.map +1 -1
- package/src/lib/server/billing-webhook.js +238 -87
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cart-checkout.d.ts +2 -0
- package/src/lib/server/cart-checkout.js +173 -13
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/channel-order-updates.d.ts +50 -0
- package/src/lib/server/channel-order-updates.js +290 -0
- package/src/lib/server/channel-order-updates.js.map +1 -0
- package/src/lib/server/channel-orders.d.ts +85 -0
- package/src/lib/server/channel-orders.js +543 -0
- package/src/lib/server/channel-orders.js.map +1 -0
- package/src/lib/server/checkout-extras.js +18 -4
- package/src/lib/server/checkout-extras.js.map +1 -1
- package/src/lib/server/checkout.js +71 -14
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/local-fulfillment-status.d.ts +86 -0
- package/src/lib/server/local-fulfillment-status.js +379 -0
- package/src/lib/server/local-fulfillment-status.js.map +1 -0
- package/src/lib/server/local-fulfillment.d.ts +190 -0
- package/src/lib/server/local-fulfillment.js +580 -0
- package/src/lib/server/local-fulfillment.js.map +1 -0
- package/src/lib/server/membership-register.js +3 -1
- package/src/lib/server/membership-register.js.map +1 -1
- package/src/lib/server/order-notifications.js +61 -0
- package/src/lib/server/order-notifications.js.map +1 -1
- package/src/lib/server/pos-display.d.ts +27 -1
- package/src/lib/server/pos-display.js +50 -9
- package/src/lib/server/pos-display.js.map +1 -1
- package/src/lib/server/pos-kiosk-principal.d.ts +29 -0
- package/src/lib/server/pos-kiosk-principal.js +42 -0
- package/src/lib/server/pos-kiosk-principal.js.map +1 -0
- package/src/lib/server/pos-kiosk.d.ts +54 -0
- package/src/lib/server/pos-kiosk.js +972 -0
- package/src/lib/server/pos-kiosk.js.map +1 -0
- package/src/lib/server/pos-offline-sync.d.ts +85 -0
- package/src/lib/server/pos-offline-sync.js +765 -0
- package/src/lib/server/pos-offline-sync.js.map +1 -0
- package/src/lib/server/pos-order.js +22 -4
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.d.ts +6 -0
- package/src/lib/server/pos-payment.js +225 -6
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-print.js +5 -0
- package/src/lib/server/pos-print.js.map +1 -1
- package/src/lib/server/pos-return.js +27 -0
- package/src/lib/server/pos-return.js.map +1 -1
- package/src/lib/server/pos-sale.d.ts +6 -0
- package/src/lib/server/pos-sale.js +2 -0
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-shift.js +8 -1
- package/src/lib/server/pos-shift.js.map +1 -1
- package/src/lib/server/product-catalog.js +5 -1
- package/src/lib/server/product-catalog.js.map +1 -1
- package/src/lib/server/product-drafts.d.ts +5 -1
- package/src/lib/server/product-drafts.js +3 -3
- package/src/lib/server/product-drafts.js.map +1 -1
- package/src/lib/server/product-writer.d.ts +19 -0
- package/src/lib/server/product-writer.js +261 -0
- package/src/lib/server/product-writer.js.map +1 -0
- package/src/lib/server/products-write.d.ts +61 -0
- package/src/lib/server/products-write.js +375 -0
- package/src/lib/server/products-write.js.map +1 -0
- package/src/lib/server/refund.js +3 -0
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/stock-hold.d.ts +9 -1
- package/src/lib/server/stock-hold.js +15 -7
- package/src/lib/server/stock-hold.js.map +1 -1
- package/src/lib/server.js +21 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.js +161 -0
- package/src/lib/tenant-emails.js.map +1 -1
- package/src/lib/testing/memory-firestore.d.ts +16 -3
- package/src/lib/testing/memory-firestore.js +11 -3
- package/src/lib/testing/memory-firestore.js.map +1 -1
- package/src/lib/transfer/products.server.d.ts +5 -0
- package/src/lib/transfer/products.server.js +2 -2
- package/src/lib/transfer/products.server.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftContext,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n COMMERCE_MAX_OPTION_VALUES,\n COMMERCE_MAX_OPTIONS,\n COMMERCE_MAX_PRICE_USD,\n COMMERCE_MAX_VARIANTS,\n commerceSlug,\n type HostProduct,\n productCollectionIds,\n productPriceMissing,\n productSearchFields,\n type ProductType,\n type ProposedProduct,\n type SmartCollectionRules,\n unpricedProductDraft,\n type UnpricedProductDraft,\n validateProduct,\n} from '../model/commerce'\n\n/**\n * A PRODUCT ANOTHER PLUGIN ASKS FOR (AGL-3616).\n *\n * The draft writer this plugin registers for the `product` resource on the\n * core's resource-drafts seam. A plugin that sets a store up from a brief\n * asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is the one the products card makes from an accepted AI\n * proposal (`unpricedProductDraft`): a `draft` — off the storefront, and\n * refused by every sale door — with one variant per combination of its\n * options, no photo, and the search, stock and smart-collection keys every\n * product write derives. Born live (`deletedAt: null`), as the product\n * resource stamps it, so the products table lists it.\n * - THE PRICE is left empty unless the content states one. An unpriced\n * product is marked for the merchant to price: the editor will not save it\n * and the card will not activate it until every variant has a price.\n * - THE ROOM is the `productsPerHost` allowance, counted the way the\n * resources route and the importer count it, inside the transaction that\n * creates. THE PLAN is the `commerce` feature, and THE ROLE a member who\n * may write the site's content — refused in the resources route's words.\n * - THE CHECK is `validateProduct`, the rule every product write is held to,\n * with a placeholder price standing in where none was stated.\n *\n * Nothing is published, priced or sold. The writer touches the one new\n * product document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const PRODUCT_DRAFT_RESOURCE = 'product'\n\n/** The longest product name the proposals and the editor keep. */\nexport const PRODUCT_DRAFT_NAME_MAX = 120\n/** The longest description a draft keeps. */\nexport const PRODUCT_DRAFT_DESCRIPTION_MAX = 5_000\n/** The most tags a draft keeps, and the longest one. */\nexport const PRODUCT_DRAFT_TAGS_MAX = 20\nexport const PRODUCT_DRAFT_TAG_MAX = 40\n/** The most categories a draft is filed under. */\nexport const PRODUCT_DRAFT_CATEGORIES_MAX = 10\n/** The longest search-listing title and description a draft keeps. */\nexport const PRODUCT_DRAFT_SEO_TITLE_MAX = 120\nexport const PRODUCT_DRAFT_SEO_DESCRIPTION_MAX = 320\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const PRODUCT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n/** The resources route's refusal for a plan without the feature. */\nexport const PRODUCT_DRAFT_PLAN_REFUSAL = 'This feature is not included in your plan — see Billing'\n\n/** The resources route's refusal at the plan's allowance. */\nexport function productDraftLimitRefusal(limit: number): string {\n return `Your plan includes ${limit} ${limit === 1 ? 'product' : 'products'} — ` +\n 'upgrade in Billing for more'\n}\n\n/**\n * What a caller sends as `content`: a proposed product, as the AI products\n * job proposes one, with an optional stated price for every variant and the\n * ids of categories the site already has. A photo is never taken: the\n * merchant adds pictures in the editor.\n */\nexport interface ProductDraftContent {\n name: string\n type?: ProductType\n description?: string\n tags?: string[]\n options?: Array<{ name: string; values: string[] }>\n seoTitle?: string\n seoDescription?: string\n /** One price for every variant, in dollars; absent leaves them unpriced. */\n priceUsd?: number\n /** Ids of the site's product categories; one the site lacks is dropped. */\n categoryIds?: string[]\n}\n\nconst PRODUCT_TYPES: readonly ProductType[] = ['physical', 'digital', 'service']\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\nfunction strings(label: string, value: unknown, problems: string[]): string[] {\n if (value === undefined) return []\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {\n problems.push(`${label} are a list of words`)\n return []\n }\n return [...new Set(value.map(line).filter(Boolean))]\n}\n\n/** A draft as the writer will store it, before its slug and keys are settled. */\nexport interface ProductDraftRead {\n proposal: ProposedProduct\n /** The price stated for every variant, or `null` for the merchant to set. */\n priceUsd: number | null\n categoryIds: string[]\n}\n\nexport type ProductDraftContentRead =\n | { ok: true; value: ProductDraftRead }\n | { ok: false; problems: string[] }\n\n/** The content as the writer reads it, or every problem that stops it. */\nexport function readProductDraftContent(\n content: Readonly<Record<string, unknown>>,\n): ProductDraftContentRead {\n const problems: string[] = []\n const name = line(content['name'])\n if (!name) problems.push('The product needs a name')\n else if (name.length > PRODUCT_DRAFT_NAME_MAX) {\n problems.push(`The name is longer than ${PRODUCT_DRAFT_NAME_MAX} characters`)\n }\n const rawType = content['type'] ?? 'physical'\n if (!(PRODUCT_TYPES as readonly unknown[]).includes(rawType)) {\n problems.push('The type is physical, digital or service')\n }\n const rawDescription = content['description']\n const description = typeof rawDescription === 'string' ? rawDescription.trim() : ''\n if (rawDescription !== undefined && typeof rawDescription !== 'string') {\n problems.push('The description is text')\n } else if (description.length > PRODUCT_DRAFT_DESCRIPTION_MAX) {\n problems.push(`The description is longer than ${PRODUCT_DRAFT_DESCRIPTION_MAX} characters`)\n }\n const tags = strings('Tags', content['tags'], problems)\n if (tags.length > PRODUCT_DRAFT_TAGS_MAX) problems.push(`A product keeps at most ${PRODUCT_DRAFT_TAGS_MAX} tags`)\n if (tags.some((tag) => tag.length > PRODUCT_DRAFT_TAG_MAX)) {\n problems.push(`A tag is longer than ${PRODUCT_DRAFT_TAG_MAX} characters`)\n }\n const categoryIds = strings('Categories', content['categoryIds'], problems)\n if (categoryIds.length > PRODUCT_DRAFT_CATEGORIES_MAX) {\n problems.push(`A product is filed under at most ${PRODUCT_DRAFT_CATEGORIES_MAX} categories`)\n }\n const seoTitle = line(content['seoTitle'])\n const seoDescription = line(content['seoDescription'])\n if (seoTitle.length > PRODUCT_DRAFT_SEO_TITLE_MAX) {\n problems.push(`The search title is longer than ${PRODUCT_DRAFT_SEO_TITLE_MAX} characters`)\n }\n if (seoDescription.length > PRODUCT_DRAFT_SEO_DESCRIPTION_MAX) {\n problems.push(`The search description is longer than ${PRODUCT_DRAFT_SEO_DESCRIPTION_MAX} characters`)\n }\n\n // Options are held here before the draft expands them: a matrix past the\n // variant ceiling is refused, never built.\n const rawOptions = content['options'] ?? []\n const options: ProposedProduct['options'] = []\n if (!Array.isArray(rawOptions)) {\n problems.push('Options are a list of a name and its values')\n } else {\n if (rawOptions.length > COMMERCE_MAX_OPTIONS) {\n problems.push(`At most ${COMMERCE_MAX_OPTIONS} options per product`)\n }\n for (const option of rawOptions) {\n const optionName = isRecord(option) ? line(option['name']) : ''\n const values = isRecord(option) ? strings(`The values of \"${optionName}\"`, option['values'], problems) : []\n if (!optionName || !values.length) {\n problems.push('Each option needs a name and at least one value')\n continue\n }\n if (values.length > COMMERCE_MAX_OPTION_VALUES) {\n problems.push(`Option \"${optionName}\" has too many values`)\n continue\n }\n options.push({ name: optionName, values })\n }\n if (new Set(options.map((option) => option.name)).size !== options.length) {\n problems.push('Each option needs its own name')\n }\n const combinations = options.reduce((count, option) => count * option.values.length, 1)\n if (combinations > COMMERCE_MAX_VARIANTS) {\n problems.push(`Those options make ${combinations} variants; a product holds at most ${COMMERCE_MAX_VARIANTS}`)\n }\n }\n\n // No price unless one is stated: the merchant sets it otherwise.\n const rawPrice = content['priceUsd']\n let priceUsd: number | null = null\n if (rawPrice !== undefined && rawPrice !== null) {\n if (\n typeof rawPrice !== 'number' ||\n !Number.isFinite(rawPrice) ||\n rawPrice < 0 ||\n rawPrice > COMMERCE_MAX_PRICE_USD ||\n Math.round(rawPrice * 100) !== rawPrice * 100\n ) {\n problems.push(`A stated price is dollars and cents from 0 to ${COMMERCE_MAX_PRICE_USD}`)\n } else {\n priceUsd = rawPrice\n }\n }\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n value: {\n proposal: {\n name,\n type: rawType as ProductType,\n description,\n tags,\n options,\n seoTitle,\n seoDescription,\n },\n priceUsd,\n categoryIds,\n },\n }\n}\n\n/** The product document a read builds, under `slug`, at `nowMs`. */\nexport function productDraftDocument(\n read: ProductDraftRead,\n slug: string,\n nowMs: number,\n): UnpricedProductDraft | HostProduct {\n const draft = unpricedProductDraft(read.proposal, new Set(), nowMs)\n const priced =\n read.priceUsd === null\n ? draft\n : ({\n ...draft,\n variants: draft.variants.map((variant) => ({ ...variant, priceUsd: read.priceUsd as number })),\n } as HostProduct)\n return {\n ...priced,\n // The search keys travel with every create; recomputed for the price.\n ...productSearchFields(priced as HostProduct),\n slug,\n ...(read.categoryIds.length ? { categoryIds: read.categoryIds } : {}),\n }\n}\n\n/** What the owner's product rule says about a draft, a placeholder price standing in for none. */\nfunction productProblem(product: UnpricedProductDraft | HostProduct): string | null {\n return validateProduct({\n ...product,\n variants: product.variants.map((variant) => ({\n ...variant,\n priceUsd: (variant as { priceUsd?: number }).priceUsd ?? 0,\n })),\n } as HostProduct)\n}\n\nfunction factsOf(product: Partial<HostProduct>) {\n return {\n status: product.status ?? 'draft',\n slug: product.slug ?? '',\n type: product.type ?? 'physical',\n variants: product.variants?.length ?? 0,\n priceMissing: productPriceMissing({ variants: product.variants ?? [] }),\n }\n}\n\n/** Whether content is a product this plugin would store, with what it says about it. Pure. */\nexport function checkProductDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readProductDraftContent(content)\n if (read.ok === false) return read\n const slug = commerceSlug(read.value.proposal.name) || 'product'\n const product = productDraftDocument(read.value, slug, 0)\n const problem = productProblem(product)\n return problem ? { ok: false, problems: [problem] } : { ok: true, facts: factsOf(product as HostProduct) }\n}\n\nfunction recordOf(product: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (product.data() ?? {}) as Partial<HostProduct>\n return { id: product.id, name: String(data.name ?? ''), versionId: null, facts: factsOf(data) }\n}\n\nfunction roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: PRODUCT_DRAFT_ROLE_REFUSAL }\n}\n\nfunction planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'commerce') ? null : { status: 403, error: PRODUCT_DRAFT_PLAN_REFUSAL }\n}\n\n/** The allowance, counted as the resources route and the importer count a product create. */\nfunction roomRefusal(org: PluginDraftContext['org'], used: number): PluginDraftRefusal | null {\n const quota = checkQuota(org as never, 'productsPerHost', used)\n return quota.allowed ? null : { status: 403, error: productDraftLimitRefusal(quota.limit) }\n}\n\n/** Candidates asked of the store at once, as the console's slug ledger probes. */\nconst SLUG_PROBE = 10\n\n/**\n * The first of `base`, `base-2`, `base-3`, … that no product holds, live or\n * deleted — a soft-deleted product still answers its slug on the storefront\n * (`productSlugLedger`, AGL-3321). Asked inside the transaction.\n */\nasync function freeSlug(\n products: FirebaseFirestore.CollectionReference,\n base: string,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<string> {\n const candidate = (suffix: number) => (suffix < 2 ? base : `${base}-${suffix}`)\n for (let first = 1; ; first += SLUG_PROBE) {\n const batch = Array.from({ length: SLUG_PROBE }, (_, offset) => candidate(first + offset))\n const held = new Set(\n (await read(products.where('slug', 'in', batch))).docs.map((doc) => String(doc.get('slug') ?? '')),\n )\n const free = batch.find((slug) => !held.has(slug))\n if (free) return free\n }\n}\n\nexport interface ProductDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createProductDraftWriter(deps: ProductDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n const early = roleRefusal(host, context.uid) ?? planRefusal(context.org)\n if (early) return early\n const used = (await hostRef.collection('products').count().get()).data().count\n return roomRefusal(context.org, Number(used) || 0)\n },\n\n check: (content) => checkProductDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const product = await firestore().collection('hosts').doc(hostId).collection('products').doc(id).get()\n return product.exists ? recordOf(product) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = line(request.name)\n const read = readProductDraftContent(asked ? { ...request.content, name: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const products = hostRef.collection('products')\n const productRef = products.doc(request.id)\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(productRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n // Asked again under the same id: the draft it already wrote.\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n const used = (await tx.get(products.count())).data().count\n const room = roomRefusal(request.org, Number(used) || 0)\n if (room) return { ok: false, ...room }\n const base = commerceSlug(read.value.proposal.name) || 'product'\n const [slug, categories, smart] = await Promise.all([\n freeSlug(products, base, (query) => tx.get(query)),\n // A category the site does not have is dropped, not stored.\n read.value.categoryIds.length\n ? Promise.all(read.value.categoryIds.map((id) => tx.get(hostRef.collection('productCategories').doc(id))))\n : Promise.resolve([]),\n tx.get(hostRef.collection('collections').where('kind', '==', 'catalog').where('mode', '==', 'smart')),\n ])\n const categoryIds = categories.filter((category) => category.exists).map((category) => category.id)\n const nowMs = request.now.getTime()\n const product = productDraftDocument({ ...read.value, categoryIds }, slug, nowMs)\n const problem = productProblem(product)\n if (problem) return { ok: false, status: 400, error: problem }\n const rules: SmartCollectionRules[] = smart.docs.map((doc) => ({\n id: doc.id,\n rules: doc.get('rules') ?? [],\n matchAll: doc.get('matchAll'),\n }))\n const priced = read.value.priceUsd !== null\n tx.create(productRef, {\n ...product,\n collectionIds: productCollectionIds(product as HostProduct, rules),\n // The legacy flat price every create path still writes, only where\n // there is one: an unpriced draft is not a free one.\n ...(priced ? { priceUsd: read.value.priceUsd } : {}),\n deletedAt: null,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: product.name,\n versionId: null,\n facts: factsOf(product as HostProduct),\n }\n })\n },\n }\n}\n\nexport const productDraftWriter = createProductDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerProductDraftWriter(): void {\n registerPluginResourceDraftWriter(PRODUCT_DRAFT_RESOURCE, productDraftWriter, { pluginId: BUNDLE_ID })\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","checkQuota","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","COMMERCE_MAX_OPTION_VALUES","COMMERCE_MAX_OPTIONS","COMMERCE_MAX_PRICE_USD","COMMERCE_MAX_VARIANTS","commerceSlug","productCollectionIds","productPriceMissing","productSearchFields","unpricedProductDraft","validateProduct","PRODUCT_DRAFT_RESOURCE","PRODUCT_DRAFT_NAME_MAX","PRODUCT_DRAFT_DESCRIPTION_MAX","PRODUCT_DRAFT_TAGS_MAX","PRODUCT_DRAFT_TAG_MAX","PRODUCT_DRAFT_CATEGORIES_MAX","PRODUCT_DRAFT_SEO_TITLE_MAX","PRODUCT_DRAFT_SEO_DESCRIPTION_MAX","PRODUCT_DRAFT_ROLE_REFUSAL","PRODUCT_DRAFT_PLAN_REFUSAL","productDraftLimitRefusal","limit","PRODUCT_TYPES","isRecord","value","Array","isArray","line","replace","trim","strings","label","problems","undefined","some","entry","push","Set","map","filter","Boolean","readProductDraftContent","content","name","length","rawType","includes","rawDescription","description","tags","tag","categoryIds","seoTitle","seoDescription","rawOptions","options","option","optionName","values","size","combinations","reduce","count","rawPrice","priceUsd","Number","isFinite","Math","round","ok","proposal","type","productDraftDocument","read","slug","nowMs","draft","priced","variants","variant","productProblem","product","factsOf","status","priceMissing","checkProductDraftContent","problem","facts","recordOf","data","id","String","versionId","roleRefusal","host","uid","role","get","error","planRefusal","org","roomRefusal","used","quota","allowed","SLUG_PROBE","freeSlug","products","base","candidate","suffix","first","batch","from","_","offset","held","where","docs","doc","free","find","has","createProductDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","hostId","exists","early","check","write","request","asked","db","productRef","runTransaction","tx","existing","Promise","all","replayed","room","categories","smart","query","resolve","category","now","getTime","rules","matchAll","create","collectionIds","deletedAt","createdAt","updatedAt","createdBy","productDraftWriter","registerProductDraftWriter","pluginId"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-drafts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { hostRoleCanWrite } from '@aglyn/aglyn/app-utils/organizations'\nimport { checkEntitlement, checkQuota } from '@aglyn/aglyn/app-utils/plan-entitlements'\nimport {\n registerPluginResourceDraftWriter,\n type PluginDraftCheck,\n type PluginDraftContext,\n type PluginDraftRecord,\n type PluginDraftRefusal,\n type PluginDraftWrite,\n type PluginResourceDraftWriter,\n} from '@aglyn/aglyn/plugin-manager/plugin-resource-drafts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n COMMERCE_MAX_OPTION_VALUES,\n COMMERCE_MAX_OPTIONS,\n COMMERCE_MAX_PRICE_USD,\n COMMERCE_MAX_VARIANTS,\n commerceSlug,\n type HostProduct,\n productCollectionIds,\n productPriceMissing,\n productSearchFields,\n type ProductType,\n type ProposedProduct,\n type SmartCollectionRules,\n unpricedProductDraft,\n type UnpricedProductDraft,\n validateProduct,\n} from '../model/commerce'\n\n/**\n * A PRODUCT ANOTHER PLUGIN ASKS FOR (AGL-3616).\n *\n * The draft writer this plugin registers for the `product` resource on the\n * core's resource-drafts seam. A plugin that sets a store up from a brief\n * asks for the writer by name and gets this plugin's rules:\n *\n * - THE DOCUMENT is the one the products card makes from an accepted AI\n * proposal (`unpricedProductDraft`): a `draft` — off the storefront, and\n * refused by every sale door — with one variant per combination of its\n * options, no photo, and the search, stock and smart-collection keys every\n * product write derives. Born live (`deletedAt: null`), as the product\n * resource stamps it, so the products table lists it.\n * - THE PRICE is left empty unless the content states one. An unpriced\n * product is marked for the merchant to price: the editor will not save it\n * and the card will not activate it until every variant has a price.\n * - THE ROOM is the `productsPerHost` allowance, counted the way the\n * resources route and the importer count it, inside the transaction that\n * creates. THE PLAN is the `commerce` feature, and THE ROLE a member who\n * may write the site's content — refused in the resources route's words.\n * - THE CHECK is `validateProduct`, the rule every product write is held to,\n * with a placeholder price standing in where none was stated.\n *\n * Nothing is published, priced or sold. The writer touches the one new\n * product document.\n */\n\ntype Firestore = FirebaseFirestore.Firestore\n\n/** The resource name this writer is registered under. */\nexport const PRODUCT_DRAFT_RESOURCE = 'product'\n\n/** The longest product name the proposals and the editor keep. */\nexport const PRODUCT_DRAFT_NAME_MAX = 120\n/** The longest description a draft keeps. */\nexport const PRODUCT_DRAFT_DESCRIPTION_MAX = 5_000\n/** The most tags a draft keeps, and the longest one. */\nexport const PRODUCT_DRAFT_TAGS_MAX = 20\nexport const PRODUCT_DRAFT_TAG_MAX = 40\n/** The most categories a draft is filed under. */\nexport const PRODUCT_DRAFT_CATEGORIES_MAX = 10\n/** The longest search-listing title and description a draft keeps. */\nexport const PRODUCT_DRAFT_SEO_TITLE_MAX = 120\nexport const PRODUCT_DRAFT_SEO_DESCRIPTION_MAX = 320\n\n/** The resources route's refusal for a member who may not write the site. */\nexport const PRODUCT_DRAFT_ROLE_REFUSAL = 'Editing requires the editor role'\n/** The resources route's refusal for a plan without the feature. */\nexport const PRODUCT_DRAFT_PLAN_REFUSAL = 'This feature is not included in your plan — see Billing'\n\n/** The resources route's refusal at the plan's allowance. */\nexport function productDraftLimitRefusal(limit: number): string {\n return `Your plan includes ${limit} ${limit === 1 ? 'product' : 'products'} — ` +\n 'upgrade in Billing for more'\n}\n\n/**\n * What a caller sends as `content`: a proposed product, as the AI products\n * job proposes one, with an optional stated price for every variant and the\n * ids of categories the site already has. A photo is never taken: the\n * merchant adds pictures in the editor.\n */\nexport interface ProductDraftContent {\n name: string\n type?: ProductType\n description?: string\n tags?: string[]\n options?: Array<{ name: string; values: string[] }>\n seoTitle?: string\n seoDescription?: string\n /** One price for every variant, in dollars; absent leaves them unpriced. */\n priceUsd?: number\n /** Ids of the site's product categories; one the site lacks is dropped. */\n categoryIds?: string[]\n}\n\nconst PRODUCT_TYPES: readonly ProductType[] = ['physical', 'digital', 'service']\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** Text on one line: controls and runs of space folded to one space. */\nfunction line(value: unknown): string {\n return typeof value === 'string' ? value.replace(/[\\p{Cc}\\s]+/gu, ' ').trim() : ''\n}\n\nfunction strings(label: string, value: unknown, problems: string[]): string[] {\n if (value === undefined) return []\n if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string')) {\n problems.push(`${label} are a list of words`)\n return []\n }\n return [...new Set(value.map(line).filter(Boolean))]\n}\n\n/** A draft as the writer will store it, before its slug and keys are settled. */\nexport interface ProductDraftRead {\n proposal: ProposedProduct\n /** The price stated for every variant, or `null` for the merchant to set. */\n priceUsd: number | null\n categoryIds: string[]\n}\n\nexport type ProductDraftContentRead =\n | { ok: true; value: ProductDraftRead }\n | { ok: false; problems: string[] }\n\n/** The content as the writer reads it, or every problem that stops it. */\nexport function readProductDraftContent(\n content: Readonly<Record<string, unknown>>,\n): ProductDraftContentRead {\n const problems: string[] = []\n const name = line(content['name'])\n if (!name) problems.push('The product needs a name')\n else if (name.length > PRODUCT_DRAFT_NAME_MAX) {\n problems.push(`The name is longer than ${PRODUCT_DRAFT_NAME_MAX} characters`)\n }\n const rawType = content['type'] ?? 'physical'\n if (!(PRODUCT_TYPES as readonly unknown[]).includes(rawType)) {\n problems.push('The type is physical, digital or service')\n }\n const rawDescription = content['description']\n const description = typeof rawDescription === 'string' ? rawDescription.trim() : ''\n if (rawDescription !== undefined && typeof rawDescription !== 'string') {\n problems.push('The description is text')\n } else if (description.length > PRODUCT_DRAFT_DESCRIPTION_MAX) {\n problems.push(`The description is longer than ${PRODUCT_DRAFT_DESCRIPTION_MAX} characters`)\n }\n const tags = strings('Tags', content['tags'], problems)\n if (tags.length > PRODUCT_DRAFT_TAGS_MAX) problems.push(`A product keeps at most ${PRODUCT_DRAFT_TAGS_MAX} tags`)\n if (tags.some((tag) => tag.length > PRODUCT_DRAFT_TAG_MAX)) {\n problems.push(`A tag is longer than ${PRODUCT_DRAFT_TAG_MAX} characters`)\n }\n const categoryIds = strings('Categories', content['categoryIds'], problems)\n if (categoryIds.length > PRODUCT_DRAFT_CATEGORIES_MAX) {\n problems.push(`A product is filed under at most ${PRODUCT_DRAFT_CATEGORIES_MAX} categories`)\n }\n const seoTitle = line(content['seoTitle'])\n const seoDescription = line(content['seoDescription'])\n if (seoTitle.length > PRODUCT_DRAFT_SEO_TITLE_MAX) {\n problems.push(`The search title is longer than ${PRODUCT_DRAFT_SEO_TITLE_MAX} characters`)\n }\n if (seoDescription.length > PRODUCT_DRAFT_SEO_DESCRIPTION_MAX) {\n problems.push(`The search description is longer than ${PRODUCT_DRAFT_SEO_DESCRIPTION_MAX} characters`)\n }\n\n // Options are held here before the draft expands them: a matrix past the\n // variant ceiling is refused, never built.\n const rawOptions = content['options'] ?? []\n const options: ProposedProduct['options'] = []\n if (!Array.isArray(rawOptions)) {\n problems.push('Options are a list of a name and its values')\n } else {\n if (rawOptions.length > COMMERCE_MAX_OPTIONS) {\n problems.push(`At most ${COMMERCE_MAX_OPTIONS} options per product`)\n }\n for (const option of rawOptions) {\n const optionName = isRecord(option) ? line(option['name']) : ''\n const values = isRecord(option) ? strings(`The values of \"${optionName}\"`, option['values'], problems) : []\n if (!optionName || !values.length) {\n problems.push('Each option needs a name and at least one value')\n continue\n }\n if (values.length > COMMERCE_MAX_OPTION_VALUES) {\n problems.push(`Option \"${optionName}\" has too many values`)\n continue\n }\n options.push({ name: optionName, values })\n }\n if (new Set(options.map((option) => option.name)).size !== options.length) {\n problems.push('Each option needs its own name')\n }\n const combinations = options.reduce((count, option) => count * option.values.length, 1)\n if (combinations > COMMERCE_MAX_VARIANTS) {\n problems.push(`Those options make ${combinations} variants; a product holds at most ${COMMERCE_MAX_VARIANTS}`)\n }\n }\n\n // No price unless one is stated: the merchant sets it otherwise.\n const rawPrice = content['priceUsd']\n let priceUsd: number | null = null\n if (rawPrice !== undefined && rawPrice !== null) {\n if (\n typeof rawPrice !== 'number' ||\n !Number.isFinite(rawPrice) ||\n rawPrice < 0 ||\n rawPrice > COMMERCE_MAX_PRICE_USD ||\n Math.round(rawPrice * 100) !== rawPrice * 100\n ) {\n problems.push(`A stated price is dollars and cents from 0 to ${COMMERCE_MAX_PRICE_USD}`)\n } else {\n priceUsd = rawPrice\n }\n }\n if (problems.length) return { ok: false, problems: [...new Set(problems)] }\n return {\n ok: true,\n value: {\n proposal: {\n name,\n type: rawType as ProductType,\n description,\n tags,\n options,\n seoTitle,\n seoDescription,\n },\n priceUsd,\n categoryIds,\n },\n }\n}\n\n/** The product document a read builds, under `slug`, at `nowMs`. */\nexport function productDraftDocument(\n read: ProductDraftRead,\n slug: string,\n nowMs: number,\n): UnpricedProductDraft | HostProduct {\n const draft = unpricedProductDraft(read.proposal, new Set(), nowMs)\n const priced =\n read.priceUsd === null\n ? draft\n : ({\n ...draft,\n variants: draft.variants.map((variant) => ({ ...variant, priceUsd: read.priceUsd as number })),\n } as HostProduct)\n return {\n ...priced,\n // The search keys travel with every create; recomputed for the price.\n ...productSearchFields(priced as HostProduct),\n slug,\n ...(read.categoryIds.length ? { categoryIds: read.categoryIds } : {}),\n }\n}\n\n/** What the owner's product rule says about a draft, a placeholder price standing in for none. */\nfunction productProblem(product: UnpricedProductDraft | HostProduct): string | null {\n return validateProduct({\n ...product,\n variants: product.variants.map((variant) => ({\n ...variant,\n priceUsd: (variant as { priceUsd?: number }).priceUsd ?? 0,\n })),\n } as HostProduct)\n}\n\nfunction factsOf(product: Partial<HostProduct>) {\n return {\n status: product.status ?? 'draft',\n slug: product.slug ?? '',\n type: product.type ?? 'physical',\n variants: product.variants?.length ?? 0,\n priceMissing: productPriceMissing({ variants: product.variants ?? [] }),\n }\n}\n\n/** Whether content is a product this plugin would store, with what it says about it. Pure. */\nexport function checkProductDraftContent(content: Readonly<Record<string, unknown>>): PluginDraftCheck {\n const read = readProductDraftContent(content)\n if (read.ok === false) return read\n const slug = commerceSlug(read.value.proposal.name) || 'product'\n const product = productDraftDocument(read.value, slug, 0)\n const problem = productProblem(product)\n return problem ? { ok: false, problems: [problem] } : { ok: true, facts: factsOf(product as HostProduct) }\n}\n\nfunction recordOf(product: FirebaseFirestore.DocumentSnapshot): PluginDraftRecord {\n const data = (product.data() ?? {}) as Partial<HostProduct>\n return { id: product.id, name: String(data.name ?? ''), versionId: null, facts: factsOf(data) }\n}\n\nexport function roleRefusal(host: FirebaseFirestore.DocumentSnapshot, uid: string): PluginDraftRefusal | null {\n const role = (host.get('memberRoles') ?? {})[uid]\n return hostRoleCanWrite(role) ? null : { status: 403, error: PRODUCT_DRAFT_ROLE_REFUSAL }\n}\n\nexport function planRefusal(org: PluginDraftContext['org']): PluginDraftRefusal | null {\n return checkEntitlement(org as never, 'commerce') ? null : { status: 403, error: PRODUCT_DRAFT_PLAN_REFUSAL }\n}\n\n/** The allowance, counted as the resources route and the importer count a product create. */\nexport function roomRefusal(org: PluginDraftContext['org'], used: number): PluginDraftRefusal | null {\n const quota = checkQuota(org as never, 'productsPerHost', used)\n return quota.allowed ? null : { status: 403, error: productDraftLimitRefusal(quota.limit) }\n}\n\n/** Candidates asked of the store at once, as the console's slug ledger probes. */\nconst SLUG_PROBE = 10\n\n/**\n * The first of `base`, `base-2`, `base-3`, … that no product holds, live or\n * deleted — a soft-deleted product still answers its slug on the storefront\n * (`productSlugLedger`, AGL-3321). Asked inside the transaction.\n */\nasync function freeSlug(\n products: FirebaseFirestore.CollectionReference,\n base: string,\n read: (query: FirebaseFirestore.Query) => Promise<FirebaseFirestore.QuerySnapshot>,\n): Promise<string> {\n const candidate = (suffix: number) => (suffix < 2 ? base : `${base}-${suffix}`)\n for (let first = 1; ; first += SLUG_PROBE) {\n const batch = Array.from({ length: SLUG_PROBE }, (_, offset) => candidate(first + offset))\n const held = new Set(\n (await read(products.where('slug', 'in', batch))).docs.map((doc) => String(doc.get('slug') ?? '')),\n )\n const free = batch.find((slug) => !held.has(slug))\n if (free) return free\n }\n}\n\nexport interface ProductDraftWriterDeps {\n /** The Admin SDK handle; specs hand in a double. */\n firestore?: () => Firestore\n}\n\nexport function createProductDraftWriter(deps: ProductDraftWriterDeps = {}): PluginResourceDraftWriter {\n const firestore = deps.firestore ?? (() => firebaseAdmin.app().firestore() as unknown as Firestore)\n return {\n refusal: async (context) => {\n const hostRef = firestore().collection('hosts').doc(context.hostId)\n const host = await hostRef.get()\n if (!host.exists) return { status: 404, error: 'Unknown site' }\n const early = roleRefusal(host, context.uid) ?? planRefusal(context.org)\n if (early) return early\n const used = (await hostRef.collection('products').count().get()).data().count\n return roomRefusal(context.org, Number(used) || 0)\n },\n\n check: (content) => checkProductDraftContent(content),\n\n read: async ({ hostId, id }) => {\n const product = await firestore().collection('hosts').doc(hostId).collection('products').doc(id).get()\n return product.exists ? recordOf(product) : null\n },\n\n write: async (request): Promise<PluginDraftWrite> => {\n // The request's name is the one asked for; the content's stands in.\n const asked = line(request.name)\n const read = readProductDraftContent(asked ? { ...request.content, name: asked } : request.content)\n if (read.ok === false) return { ok: false, status: 400, error: read.problems[0] }\n const db = firestore()\n const hostRef = db.collection('hosts').doc(request.hostId)\n const products = hostRef.collection('products')\n const productRef = products.doc(request.id)\n return db.runTransaction(async (tx): Promise<PluginDraftWrite> => {\n // Every read before any write, which Firestore requires.\n const [host, existing] = await Promise.all([tx.get(hostRef), tx.get(productRef)])\n if (!host.exists) return { ok: false, status: 404, error: 'Unknown site' }\n // Asked again under the same id: the draft it already wrote.\n if (existing.exists) return { ok: true, replayed: true, ...recordOf(existing) }\n const early = roleRefusal(host, request.uid) ?? planRefusal(request.org)\n if (early) return { ok: false, ...early }\n const used = (await tx.get(products.count())).data().count\n const room = roomRefusal(request.org, Number(used) || 0)\n if (room) return { ok: false, ...room }\n const base = commerceSlug(read.value.proposal.name) || 'product'\n const [slug, categories, smart] = await Promise.all([\n freeSlug(products, base, (query) => tx.get(query)),\n // A category the site does not have is dropped, not stored.\n read.value.categoryIds.length\n ? Promise.all(read.value.categoryIds.map((id) => tx.get(hostRef.collection('productCategories').doc(id))))\n : Promise.resolve([]),\n tx.get(hostRef.collection('collections').where('kind', '==', 'catalog').where('mode', '==', 'smart')),\n ])\n const categoryIds = categories.filter((category) => category.exists).map((category) => category.id)\n const nowMs = request.now.getTime()\n const product = productDraftDocument({ ...read.value, categoryIds }, slug, nowMs)\n const problem = productProblem(product)\n if (problem) return { ok: false, status: 400, error: problem }\n const rules: SmartCollectionRules[] = smart.docs.map((doc) => ({\n id: doc.id,\n rules: doc.get('rules') ?? [],\n matchAll: doc.get('matchAll'),\n }))\n const priced = read.value.priceUsd !== null\n tx.create(productRef, {\n ...product,\n collectionIds: productCollectionIds(product as HostProduct, rules),\n // The legacy flat price every create path still writes, only where\n // there is one: an unpriced draft is not a free one.\n ...(priced ? { priceUsd: read.value.priceUsd } : {}),\n deletedAt: null,\n createdAt: request.now,\n updatedAt: request.now,\n createdBy: request.uid,\n })\n return {\n ok: true,\n replayed: false,\n id: request.id,\n name: product.name,\n versionId: null,\n facts: factsOf(product as HostProduct),\n }\n })\n },\n }\n}\n\nexport const productDraftWriter = createProductDraftWriter()\n\n/**\n * Registers the writer; the console surface calls it, since only the console\n * runs AI jobs (AGL-3026). Idempotent: a second call replaces the first.\n */\nexport function registerProductDraftWriter(): void {\n registerPluginResourceDraftWriter(PRODUCT_DRAFT_RESOURCE, productDraftWriter, { pluginId: BUNDLE_ID })\n}\n"],"names":["hostRoleCanWrite","checkEntitlement","checkQuota","registerPluginResourceDraftWriter","firebaseAdmin","BUNDLE_ID","COMMERCE_MAX_OPTION_VALUES","COMMERCE_MAX_OPTIONS","COMMERCE_MAX_PRICE_USD","COMMERCE_MAX_VARIANTS","commerceSlug","productCollectionIds","productPriceMissing","productSearchFields","unpricedProductDraft","validateProduct","PRODUCT_DRAFT_RESOURCE","PRODUCT_DRAFT_NAME_MAX","PRODUCT_DRAFT_DESCRIPTION_MAX","PRODUCT_DRAFT_TAGS_MAX","PRODUCT_DRAFT_TAG_MAX","PRODUCT_DRAFT_CATEGORIES_MAX","PRODUCT_DRAFT_SEO_TITLE_MAX","PRODUCT_DRAFT_SEO_DESCRIPTION_MAX","PRODUCT_DRAFT_ROLE_REFUSAL","PRODUCT_DRAFT_PLAN_REFUSAL","productDraftLimitRefusal","limit","PRODUCT_TYPES","isRecord","value","Array","isArray","line","replace","trim","strings","label","problems","undefined","some","entry","push","Set","map","filter","Boolean","readProductDraftContent","content","name","length","rawType","includes","rawDescription","description","tags","tag","categoryIds","seoTitle","seoDescription","rawOptions","options","option","optionName","values","size","combinations","reduce","count","rawPrice","priceUsd","Number","isFinite","Math","round","ok","proposal","type","productDraftDocument","read","slug","nowMs","draft","priced","variants","variant","productProblem","product","factsOf","status","priceMissing","checkProductDraftContent","problem","facts","recordOf","data","id","String","versionId","roleRefusal","host","uid","role","get","error","planRefusal","org","roomRefusal","used","quota","allowed","SLUG_PROBE","freeSlug","products","base","candidate","suffix","first","batch","from","_","offset","held","where","docs","doc","free","find","has","createProductDraftWriter","deps","firestore","app","refusal","context","hostRef","collection","hostId","exists","early","check","write","request","asked","db","productRef","runTransaction","tx","existing","Promise","all","replayed","room","categories","smart","query","resolve","category","now","getTime","rules","matchAll","create","collectionIds","deletedAt","createdAt","updatedAt","createdBy","productDraftWriter","registerProductDraftWriter","pluginId"],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { PluginProductWriter, SourcedProductWrite, SourcedProductWriteOutcome } from '@aglyn/aglyn/plugin-manager/plugin-product-writer';
|
|
18
|
+
export declare function upsertSourcedProduct(write: SourcedProductWrite, firestore?: FirebaseFirestore.Firestore): Promise<SourcedProductWriteOutcome>;
|
|
19
|
+
export declare const commerceProductWriter: PluginProductWriter;
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
|
+
import { firebaseAdmin, getOrgForHost, logHostActivity } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { isDocumentId } from "@aglyn/tenant-data-admin/server/document-id";
|
|
20
|
+
import { commerceSlug, liftLegacyProduct, validateProduct } from "../model/commerce.js";
|
|
21
|
+
import { derivedKeys, readSmartCollections } from "../transfer/products.server.js";
|
|
22
|
+
import { withoutUndefined } from "../transfer/server-common.js";
|
|
23
|
+
/** The fields an update compares to tell a change from a repeat. */ const COMPARED_KEYS = [
|
|
24
|
+
'name',
|
|
25
|
+
'description',
|
|
26
|
+
'mediaUrls',
|
|
27
|
+
'tags',
|
|
28
|
+
'options',
|
|
29
|
+
'variants'
|
|
30
|
+
];
|
|
31
|
+
/** How many suffixed addresses a new product tries before it gives up. */ const SLUG_TRIES = 30;
|
|
32
|
+
const SLUGS_PER_QUERY = 30;
|
|
33
|
+
const newVariantId = ()=>`v${Aglyn.createResourceUid().slice(0, 10)}`;
|
|
34
|
+
function priceUsd(minor) {
|
|
35
|
+
return Math.round(Math.max(0, Number(minor) || 0)) / 100;
|
|
36
|
+
}
|
|
37
|
+
/** The variant a sourced configuration becomes, on top of the one it was. */ function variantFrom(source, existing, options) {
|
|
38
|
+
var _ref;
|
|
39
|
+
const price = existing && !options.prices ? existing.priceUsd : priceUsd(source.priceMinor);
|
|
40
|
+
const next = _extends({}, existing != null ? existing : {}, {
|
|
41
|
+
id: (_ref = existing == null ? void 0 : existing.id) != null ? _ref : newVariantId(),
|
|
42
|
+
options: _extends({}, source.options),
|
|
43
|
+
priceUsd: price,
|
|
44
|
+
inventory: source.available ? null : 0
|
|
45
|
+
});
|
|
46
|
+
delete next.inventoryByLocation;
|
|
47
|
+
if (source.sku) next.sku = source.sku.slice(0, 120);
|
|
48
|
+
else delete next.sku;
|
|
49
|
+
if (Number.isFinite(source.weightGrams) && Number(source.weightGrams) > 0) {
|
|
50
|
+
next.weightGrams = Math.round(Number(source.weightGrams));
|
|
51
|
+
}
|
|
52
|
+
if (source.imageUrl && (options.content || !(existing == null ? void 0 : existing.imageUrl))) next.imageUrl = source.imageUrl;
|
|
53
|
+
// A price brought below a compare-at that no longer exceeds it would be
|
|
54
|
+
// refused: the strike-through goes, the price stays.
|
|
55
|
+
if (next.compareAtPriceUsd != null && Number(next.compareAtPriceUsd) <= next.priceUsd) {
|
|
56
|
+
delete next.compareAtPriceUsd;
|
|
57
|
+
}
|
|
58
|
+
return next;
|
|
59
|
+
}
|
|
60
|
+
async function freeSlug(products, name) {
|
|
61
|
+
var _candidates_find;
|
|
62
|
+
const base = commerceSlug(name).slice(0, 80) || 'product';
|
|
63
|
+
const candidates = [
|
|
64
|
+
base,
|
|
65
|
+
...Array.from({
|
|
66
|
+
length: SLUG_TRIES - 1
|
|
67
|
+
}, (_, index)=>`${base}-${index + 2}`)
|
|
68
|
+
];
|
|
69
|
+
const taken = new Set();
|
|
70
|
+
for(let at = 0; at < candidates.length; at += SLUGS_PER_QUERY){
|
|
71
|
+
const slice = candidates.slice(at, at + SLUGS_PER_QUERY);
|
|
72
|
+
for (const doc of (await products.where('slug', 'in', slice).get()).docs)taken.add(String(doc.get('slug')));
|
|
73
|
+
}
|
|
74
|
+
return (_candidates_find = candidates.find((candidate)=>!taken.has(candidate))) != null ? _candidates_find : null;
|
|
75
|
+
}
|
|
76
|
+
let WriteRefusal = class WriteRefusal extends Error {
|
|
77
|
+
constructor(outcome){
|
|
78
|
+
super(outcome.outcome), this.outcome = outcome;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
export async function upsertSourcedProduct(write, firestore = firebaseAdmin.app().firestore()) {
|
|
82
|
+
var _write_actorUid, _source_description;
|
|
83
|
+
const { hostId, product: source } = write;
|
|
84
|
+
if (!isDocumentId(hostId)) return {
|
|
85
|
+
outcome: 'no_store'
|
|
86
|
+
};
|
|
87
|
+
const owner = await getOrgForHost(hostId);
|
|
88
|
+
if (!owner || !Aglyn.checkEntitlement(owner.org, 'commerce')) return {
|
|
89
|
+
outcome: 'no_store'
|
|
90
|
+
};
|
|
91
|
+
const org = owner.org;
|
|
92
|
+
const products = firestore.collection('hosts').doc(hostId).collection('products');
|
|
93
|
+
const smart = await readSmartCollections(firestore, {
|
|
94
|
+
hostId
|
|
95
|
+
});
|
|
96
|
+
const { Timestamp } = firebaseAdmin.firestore;
|
|
97
|
+
const actor = {
|
|
98
|
+
uid: (_write_actorUid = write.actorUid) != null ? _write_actorUid : 'import',
|
|
99
|
+
email: null
|
|
100
|
+
};
|
|
101
|
+
const name = source.name.trim().slice(0, 200);
|
|
102
|
+
const description = ((_source_description = source.description) != null ? _source_description : '').trim().slice(0, 20000);
|
|
103
|
+
const mapping = (product)=>source.variants.map((variant, index)=>({
|
|
104
|
+
key: variant.key,
|
|
105
|
+
variantId: product.variants[index].id
|
|
106
|
+
}));
|
|
107
|
+
try {
|
|
108
|
+
var _source_tags;
|
|
109
|
+
// An update, when the product is still there.
|
|
110
|
+
if (write.productId && isDocumentId(write.productId)) {
|
|
111
|
+
const ref = products.doc(write.productId);
|
|
112
|
+
const updated = await firestore.runTransaction(async (tx)=>{
|
|
113
|
+
const snapshot = await tx.get(ref);
|
|
114
|
+
const data = snapshot.data();
|
|
115
|
+
if (!snapshot.exists || (data == null ? void 0 : data['deletedAt']) !== null && (data == null ? void 0 : data['deletedAt']) !== undefined) return null;
|
|
116
|
+
const current = liftLegacyProduct(data);
|
|
117
|
+
const flags = {
|
|
118
|
+
prices: write.prices === true,
|
|
119
|
+
content: write.content === true
|
|
120
|
+
};
|
|
121
|
+
const byId = new Map(current.variants.map((variant)=>[
|
|
122
|
+
variant.id,
|
|
123
|
+
variant
|
|
124
|
+
]));
|
|
125
|
+
const next = _extends({}, current, {
|
|
126
|
+
options: source.options.map((option)=>({
|
|
127
|
+
name: option.name,
|
|
128
|
+
values: [
|
|
129
|
+
...option.values
|
|
130
|
+
]
|
|
131
|
+
})),
|
|
132
|
+
variants: source.variants.map((variant)=>variantFrom(variant, variant.variantId ? byId.get(variant.variantId) : undefined, flags))
|
|
133
|
+
}, flags.content ? _extends({
|
|
134
|
+
name,
|
|
135
|
+
description,
|
|
136
|
+
mediaUrls: [
|
|
137
|
+
...source.mediaUrls
|
|
138
|
+
]
|
|
139
|
+
}, source.tags ? {
|
|
140
|
+
tags: [
|
|
141
|
+
...source.tags
|
|
142
|
+
]
|
|
143
|
+
} : {}) : {});
|
|
144
|
+
const invalid = validateProduct(next);
|
|
145
|
+
if (invalid) throw new WriteRefusal({
|
|
146
|
+
outcome: 'invalid',
|
|
147
|
+
message: invalid
|
|
148
|
+
});
|
|
149
|
+
const changed = COMPARED_KEYS.some((key)=>{
|
|
150
|
+
var _next_key, _current_key;
|
|
151
|
+
return JSON.stringify((_next_key = next[key]) != null ? _next_key : null) !== JSON.stringify((_current_key = current[key]) != null ? _current_key : null);
|
|
152
|
+
});
|
|
153
|
+
if (changed) {
|
|
154
|
+
tx.update(ref, withoutUndefined(_extends({}, Object.fromEntries(COMPARED_KEYS.map((key)=>[
|
|
155
|
+
key,
|
|
156
|
+
next[key]
|
|
157
|
+
])), derivedKeys(next, smart), {
|
|
158
|
+
updatedAtMs: Date.now(),
|
|
159
|
+
updatedAt: Timestamp.now()
|
|
160
|
+
})));
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
next,
|
|
164
|
+
changed
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
if (updated) {
|
|
168
|
+
if (updated.changed) {
|
|
169
|
+
await logHostActivity(hostId, actor, 'Updated product from its source', {
|
|
170
|
+
type: 'content',
|
|
171
|
+
id: write.productId,
|
|
172
|
+
name: updated.next.name
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
outcome: updated.changed ? 'updated' : 'unchanged',
|
|
177
|
+
productId: write.productId,
|
|
178
|
+
variants: mapping(updated.next)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (write.productId && write.recreate === false) return {
|
|
183
|
+
outcome: 'missing'
|
|
184
|
+
};
|
|
185
|
+
// A new product.
|
|
186
|
+
const slug = await freeSlug(products, name);
|
|
187
|
+
if (!slug) return {
|
|
188
|
+
outcome: 'invalid',
|
|
189
|
+
message: 'Every address for a product of this name is taken. Rename one first.'
|
|
190
|
+
};
|
|
191
|
+
const productId = Aglyn.createResourceUid();
|
|
192
|
+
const product = _extends({
|
|
193
|
+
name,
|
|
194
|
+
slug,
|
|
195
|
+
description,
|
|
196
|
+
type: 'physical',
|
|
197
|
+
status: write.status === 'active' ? 'active' : 'draft',
|
|
198
|
+
mediaUrls: [
|
|
199
|
+
...source.mediaUrls
|
|
200
|
+
]
|
|
201
|
+
}, ((_source_tags = source.tags) == null ? void 0 : _source_tags.length) ? {
|
|
202
|
+
tags: [
|
|
203
|
+
...source.tags
|
|
204
|
+
]
|
|
205
|
+
} : {}, {
|
|
206
|
+
options: source.options.map((option)=>({
|
|
207
|
+
name: option.name,
|
|
208
|
+
values: [
|
|
209
|
+
...option.values
|
|
210
|
+
]
|
|
211
|
+
})),
|
|
212
|
+
variants: source.variants.map((variant)=>variantFrom(variant, undefined, {
|
|
213
|
+
prices: true,
|
|
214
|
+
content: true
|
|
215
|
+
})),
|
|
216
|
+
oversellPolicy: 'deny'
|
|
217
|
+
});
|
|
218
|
+
const invalid = validateProduct(product);
|
|
219
|
+
if (invalid) return {
|
|
220
|
+
outcome: 'invalid',
|
|
221
|
+
message: invalid
|
|
222
|
+
};
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
const ref = products.doc(productId);
|
|
225
|
+
await firestore.runTransaction(async (tx)=>{
|
|
226
|
+
var _write_actorUid;
|
|
227
|
+
const used = (await tx.get(products.count())).data().count;
|
|
228
|
+
const quota = Aglyn.checkQuota(org, 'productsPerHost', Number(used) || 0);
|
|
229
|
+
if (!quota.allowed) throw new WriteRefusal({
|
|
230
|
+
outcome: 'plan_limit',
|
|
231
|
+
limit: Number(quota.limit) || 0
|
|
232
|
+
});
|
|
233
|
+
tx.create(ref, withoutUndefined(_extends({}, product, derivedKeys(product, smart), {
|
|
234
|
+
createdAtMs: now,
|
|
235
|
+
updatedAtMs: now,
|
|
236
|
+
deletedAt: null,
|
|
237
|
+
createdAt: Timestamp.now(),
|
|
238
|
+
updatedAt: Timestamp.now(),
|
|
239
|
+
createdBy: (_write_actorUid = write.actorUid) != null ? _write_actorUid : null
|
|
240
|
+
})));
|
|
241
|
+
});
|
|
242
|
+
await logHostActivity(hostId, actor, 'Imported product', {
|
|
243
|
+
type: 'content',
|
|
244
|
+
id: productId,
|
|
245
|
+
name: product.name
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
outcome: 'created',
|
|
249
|
+
productId,
|
|
250
|
+
variants: mapping(product)
|
|
251
|
+
};
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error instanceof WriteRefusal) return error.outcome;
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export const commerceProductWriter = {
|
|
258
|
+
upsertSourced: (write)=>upsertSourcedProduct(write)
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
//# sourceMappingURL=product-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/product-writer.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport type {\n PluginProductWriter,\n SourcedProductWrite,\n SourcedProductWriteOutcome,\n} from '@aglyn/aglyn/plugin-manager/plugin-product-writer'\nimport type { TransferResourceContext } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { firebaseAdmin, getOrgForHost, logHostActivity } from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport {\n commerceSlug,\n liftLegacyProduct,\n validateProduct,\n type HostProduct,\n type ProductVariant,\n} from '../model/commerce'\nimport { derivedKeys, readSmartCollections } from '../transfer/products.server'\nimport { withoutUndefined } from '../transfer/server-common'\n\n/**\n * Products another plugin brings from a source of its own — a print-on-demand\n * service, a supplier — written through the store's own rules (AGL-3641),\n * behind core's `core.product-writer`.\n *\n * The same rules the console's product route and the file import keep: a\n * create is counted against the plan's product allowance in the transaction\n * that makes it, a new product takes an address no other product holds (a\n * deleted one's included), every write carries the search, stock and\n * smart-collection keys every writer derives, and the site's activity says\n * what happened.\n *\n * A configuration the source cannot make now is sold out (`inventory: 0`);\n * one it can is untracked (`inventory: null`), because a made-to-order item\n * has no count to run down.\n */\n\ntype ProductDoc = Partial<HostProduct> & Record<string, unknown>\n\n/** The fields an update compares to tell a change from a repeat. */\nconst COMPARED_KEYS = ['name', 'description', 'mediaUrls', 'tags', 'options', 'variants'] as const\n\n/** How many suffixed addresses a new product tries before it gives up. */\nconst SLUG_TRIES = 30\nconst SLUGS_PER_QUERY = 30\n\nconst newVariantId = () => `v${Aglyn.createResourceUid().slice(0, 10)}`\n\nfunction priceUsd(minor: number): number {\n return Math.round(Math.max(0, Number(minor) || 0)) / 100\n}\n\n/** The variant a sourced configuration becomes, on top of the one it was. */\nfunction variantFrom(\n source: SourcedProductWrite['product']['variants'][number],\n existing: ProductVariant | undefined,\n options: { prices: boolean; content: boolean },\n): ProductVariant {\n const price = existing && !options.prices ? existing.priceUsd : priceUsd(source.priceMinor)\n const next: ProductVariant = {\n ...(existing ?? {}),\n id: existing?.id ?? newVariantId(),\n options: { ...source.options },\n priceUsd: price,\n inventory: source.available ? null : 0,\n }\n delete next.inventoryByLocation\n if (source.sku) next.sku = source.sku.slice(0, 120)\n else delete next.sku\n if (Number.isFinite(source.weightGrams) && Number(source.weightGrams) > 0) {\n next.weightGrams = Math.round(Number(source.weightGrams))\n }\n if (source.imageUrl && (options.content || !existing?.imageUrl)) next.imageUrl = source.imageUrl\n // A price brought below a compare-at that no longer exceeds it would be\n // refused: the strike-through goes, the price stays.\n if (next.compareAtPriceUsd != null && Number(next.compareAtPriceUsd) <= next.priceUsd) {\n delete next.compareAtPriceUsd\n }\n return next\n}\n\nasync function freeSlug(products: FirebaseFirestore.CollectionReference, name: string): Promise<string | null> {\n const base = commerceSlug(name).slice(0, 80) || 'product'\n const candidates = [base, ...Array.from({ length: SLUG_TRIES - 1 }, (_, index) => `${base}-${index + 2}`)]\n const taken = new Set<string>()\n for (let at = 0; at < candidates.length; at += SLUGS_PER_QUERY) {\n const slice = candidates.slice(at, at + SLUGS_PER_QUERY)\n for (const doc of (await products.where('slug', 'in', slice).get()).docs) taken.add(String(doc.get('slug')))\n }\n return candidates.find((candidate) => !taken.has(candidate)) ?? null\n}\n\nclass WriteRefusal extends Error {\n constructor(readonly outcome: Exclude<SourcedProductWriteOutcome, { productId: string }>) {\n super(outcome.outcome)\n }\n}\n\nexport async function upsertSourcedProduct(\n write: SourcedProductWrite,\n firestore: FirebaseFirestore.Firestore = firebaseAdmin.app().firestore(),\n): Promise<SourcedProductWriteOutcome> {\n const { hostId, product: source } = write\n if (!isDocumentId(hostId)) return { outcome: 'no_store' }\n const owner = await getOrgForHost(hostId)\n if (!owner || !Aglyn.checkEntitlement(owner.org as never, 'commerce')) return { outcome: 'no_store' }\n const org = owner.org\n const products = firestore.collection('hosts').doc(hostId).collection('products')\n const smart = await readSmartCollections(firestore, { hostId } as TransferResourceContext)\n const { Timestamp } = firebaseAdmin.firestore\n const actor = { uid: write.actorUid ?? 'import', email: null }\n const name = source.name.trim().slice(0, 200)\n const description = (source.description ?? '').trim().slice(0, 20_000)\n\n const mapping = (product: HostProduct) =>\n source.variants.map((variant, index) => ({ key: variant.key, variantId: product.variants[index].id }))\n\n try {\n // An update, when the product is still there.\n if (write.productId && isDocumentId(write.productId)) {\n const ref = products.doc(write.productId)\n const updated = await firestore.runTransaction(async (tx) => {\n const snapshot = await tx.get(ref)\n const data = snapshot.data() as ProductDoc | undefined\n if (!snapshot.exists || (data?.['deletedAt'] !== null && data?.['deletedAt'] !== undefined)) return null\n const current = liftLegacyProduct(data as ProductDoc)\n const flags = { prices: write.prices === true, content: write.content === true }\n const byId = new Map(current.variants.map((variant) => [variant.id, variant]))\n const next: HostProduct = {\n ...current,\n options: source.options.map((option) => ({ name: option.name, values: [...option.values] })),\n variants: source.variants.map((variant) =>\n variantFrom(variant, variant.variantId ? byId.get(variant.variantId) : undefined, flags),\n ),\n ...(flags.content\n ? {\n name,\n description,\n mediaUrls: [...source.mediaUrls],\n ...(source.tags ? { tags: [...source.tags] } : {}),\n }\n : {}),\n }\n const invalid = validateProduct(next)\n if (invalid) throw new WriteRefusal({ outcome: 'invalid', message: invalid })\n const changed = COMPARED_KEYS.some(\n (key) =>\n JSON.stringify((next as unknown as Record<string, unknown>)[key] ?? null) !==\n JSON.stringify((current as unknown as Record<string, unknown>)[key] ?? null),\n )\n if (changed) {\n tx.update(\n ref,\n withoutUndefined({\n ...Object.fromEntries(COMPARED_KEYS.map((key) => [key, (next as unknown as Record<string, unknown>)[key]])),\n ...derivedKeys(next, smart),\n updatedAtMs: Date.now(),\n updatedAt: Timestamp.now(),\n }) as FirebaseFirestore.UpdateData<FirebaseFirestore.DocumentData>,\n )\n }\n return { next, changed }\n })\n if (updated) {\n if (updated.changed) {\n await logHostActivity(hostId, actor, 'Updated product from its source', {\n type: 'content',\n id: write.productId,\n name: updated.next.name,\n })\n }\n return {\n outcome: updated.changed ? 'updated' : 'unchanged',\n productId: write.productId,\n variants: mapping(updated.next),\n }\n }\n }\n\n if (write.productId && write.recreate === false) return { outcome: 'missing' }\n\n // A new product.\n const slug = await freeSlug(products, name)\n if (!slug) return { outcome: 'invalid', message: 'Every address for a product of this name is taken. Rename one first.' }\n const productId = Aglyn.createResourceUid()\n const product: HostProduct = {\n name,\n slug,\n description,\n type: 'physical',\n status: write.status === 'active' ? 'active' : 'draft',\n mediaUrls: [...source.mediaUrls],\n ...(source.tags?.length ? { tags: [...source.tags] } : {}),\n options: source.options.map((option) => ({ name: option.name, values: [...option.values] })),\n variants: source.variants.map((variant) => variantFrom(variant, undefined, { prices: true, content: true })),\n oversellPolicy: 'deny',\n }\n const invalid = validateProduct(product)\n if (invalid) return { outcome: 'invalid', message: invalid }\n const now = Date.now()\n const ref = products.doc(productId)\n await firestore.runTransaction(async (tx) => {\n const used = (await tx.get(products.count())).data().count\n const quota = Aglyn.checkQuota(org as never, 'productsPerHost', Number(used) || 0)\n if (!quota.allowed) throw new WriteRefusal({ outcome: 'plan_limit', limit: Number(quota.limit) || 0 })\n tx.create(\n ref,\n withoutUndefined({\n ...product,\n ...derivedKeys(product, smart),\n createdAtMs: now,\n updatedAtMs: now,\n deletedAt: null,\n createdAt: Timestamp.now(),\n updatedAt: Timestamp.now(),\n createdBy: write.actorUid ?? null,\n }),\n )\n })\n await logHostActivity(hostId, actor, 'Imported product', { type: 'content', id: productId, name: product.name })\n return { outcome: 'created', productId, variants: mapping(product) }\n } catch (error) {\n if (error instanceof WriteRefusal) return error.outcome\n throw error\n }\n}\n\nexport const commerceProductWriter: PluginProductWriter = {\n upsertSourced: (write) => upsertSourcedProduct(write),\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","logHostActivity","isDocumentId","commerceSlug","liftLegacyProduct","validateProduct","derivedKeys","readSmartCollections","withoutUndefined","COMPARED_KEYS","SLUG_TRIES","SLUGS_PER_QUERY","newVariantId","createResourceUid","slice","priceUsd","minor","Math","round","max","Number","variantFrom","source","existing","options","price","prices","priceMinor","next","id","inventory","available","inventoryByLocation","sku","isFinite","weightGrams","imageUrl","content","compareAtPriceUsd","freeSlug","products","name","candidates","base","Array","from","length","_","index","taken","Set","at","doc","where","get","docs","add","String","find","candidate","has","WriteRefusal","Error","outcome","upsertSourcedProduct","write","firestore","app","hostId","product","owner","checkEntitlement","org","collection","smart","Timestamp","actor","uid","actorUid","email","trim","description","mapping","variants","map","variant","key","variantId","productId","ref","updated","runTransaction","tx","snapshot","data","exists","undefined","current","flags","byId","Map","option","values","mediaUrls","tags","invalid","message","changed","some","JSON","stringify","update","Object","fromEntries","updatedAtMs","Date","now","updatedAt","type","recreate","slug","status","oversellPolicy","used","count","quota","checkQuota","allowed","limit","create","createdAtMs","deletedAt","createdAt","createdBy","error","commerceProductWriter","upsertSourced"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAO5C,SAASC,aAAa,EAAEC,aAAa,EAAEC,eAAe,QAAQ,2BAA0B;AACxF,SAASC,YAAY,QAAQ,8CAA6C;AAC1E,SACEC,YAAY,EACZC,iBAAiB,EACjBC,eAAe,QAGV,uBAAmB;AAC1B,SAASC,WAAW,EAAEC,oBAAoB,QAAQ,iCAA6B;AAC/E,SAASC,gBAAgB,QAAQ,+BAA2B;AAqB5D,kEAAkE,GAClE,MAAMC,gBAAgB;IAAC;IAAQ;IAAe;IAAa;IAAQ;IAAW;CAAW;AAEzF,wEAAwE,GACxE,MAAMC,aAAa;AACnB,MAAMC,kBAAkB;AAExB,MAAMC,eAAe,IAAM,CAAC,CAAC,EAAEd,MAAMe,iBAAiB,GAAGC,KAAK,CAAC,GAAG,KAAK;AAEvE,SAASC,SAASC,KAAa;IAC7B,OAAOC,KAAKC,KAAK,CAACD,KAAKE,GAAG,CAAC,GAAGC,OAAOJ,UAAU,MAAM;AACvD;AAEA,2EAA2E,GAC3E,SAASK,YACPC,MAA0D,EAC1DC,QAAoC,EACpCC,OAA8C;;IAE9C,MAAMC,QAAQF,YAAY,CAACC,QAAQE,MAAM,GAAGH,SAASR,QAAQ,GAAGA,SAASO,OAAOK,UAAU;IAC1F,MAAMC,OAAuB,aACvBL,mBAAAA,WAAY,CAAC;QACjBM,EAAE,UAAEN,4BAAAA,SAAUM,EAAE,mBAAIjB;QACpBY,SAAS,aAAKF,OAAOE,OAAO;QAC5BT,UAAUU;QACVK,WAAWR,OAAOS,SAAS,GAAG,OAAO;;IAEvC,OAAOH,KAAKI,mBAAmB;IAC/B,IAAIV,OAAOW,GAAG,EAAEL,KAAKK,GAAG,GAAGX,OAAOW,GAAG,CAACnB,KAAK,CAAC,GAAG;SAC1C,OAAOc,KAAKK,GAAG;IACpB,IAAIb,OAAOc,QAAQ,CAACZ,OAAOa,WAAW,KAAKf,OAAOE,OAAOa,WAAW,IAAI,GAAG;QACzEP,KAAKO,WAAW,GAAGlB,KAAKC,KAAK,CAACE,OAAOE,OAAOa,WAAW;IACzD;IACA,IAAIb,OAAOc,QAAQ,IAAKZ,CAAAA,QAAQa,OAAO,IAAI,EAACd,4BAAAA,SAAUa,QAAQ,CAAD,GAAIR,KAAKQ,QAAQ,GAAGd,OAAOc,QAAQ;IAChG,wEAAwE;IACxE,qDAAqD;IACrD,IAAIR,KAAKU,iBAAiB,IAAI,QAAQlB,OAAOQ,KAAKU,iBAAiB,KAAKV,KAAKb,QAAQ,EAAE;QACrF,OAAOa,KAAKU,iBAAiB;IAC/B;IACA,OAAOV;AACT;AAEA,eAAeW,SAASC,QAA+C,EAAEC,IAAY;QAQ5EC;IAPP,MAAMC,OAAOxC,aAAasC,MAAM3B,KAAK,CAAC,GAAG,OAAO;IAChD,MAAM4B,aAAa;QAACC;WAASC,MAAMC,IAAI,CAAC;YAAEC,QAAQpC,aAAa;QAAE,GAAG,CAACqC,GAAGC,QAAU,GAAGL,KAAK,CAAC,EAAEK,QAAQ,GAAG;KAAE;IAC1G,MAAMC,QAAQ,IAAIC;IAClB,IAAK,IAAIC,KAAK,GAAGA,KAAKT,WAAWI,MAAM,EAAEK,MAAMxC,gBAAiB;QAC9D,MAAMG,QAAQ4B,WAAW5B,KAAK,CAACqC,IAAIA,KAAKxC;QACxC,KAAK,MAAMyC,OAAO,AAAC,CAAA,MAAMZ,SAASa,KAAK,CAAC,QAAQ,MAAMvC,OAAOwC,GAAG,EAAC,EAAGC,IAAI,CAAEN,MAAMO,GAAG,CAACC,OAAOL,IAAIE,GAAG,CAAC;IACrG;IACA,QAAOZ,mBAAAA,WAAWgB,IAAI,CAAC,CAACC,YAAc,CAACV,MAAMW,GAAG,CAACD,uBAA1CjB,mBAAyD;AAClE;AAEA,IAAA,AAAMmB,eAAN,MAAMA,qBAAqBC;IACzB,YAAY,AAASC,OAAmE,CAAE;QACxF,KAAK,CAACA,QAAQA,OAAO,QADFA,UAAAA;IAErB;AACF;AAEA,OAAO,eAAeC,qBACpBC,KAA0B,EAC1BC,YAAyCnE,cAAcoE,GAAG,GAAGD,SAAS,EAAE;QAUnDD,iBAEA3C;IAVrB,MAAM,EAAE8C,MAAM,EAAEC,SAAS/C,MAAM,EAAE,GAAG2C;IACpC,IAAI,CAAC/D,aAAakE,SAAS,OAAO;QAAEL,SAAS;IAAW;IACxD,MAAMO,QAAQ,MAAMtE,cAAcoE;IAClC,IAAI,CAACE,SAAS,CAACxE,MAAMyE,gBAAgB,CAACD,MAAME,GAAG,EAAW,aAAa,OAAO;QAAET,SAAS;IAAW;IACpG,MAAMS,MAAMF,MAAME,GAAG;IACrB,MAAMhC,WAAW0B,UAAUO,UAAU,CAAC,SAASrB,GAAG,CAACgB,QAAQK,UAAU,CAAC;IACtE,MAAMC,QAAQ,MAAMnE,qBAAqB2D,WAAW;QAAEE;IAAO;IAC7D,MAAM,EAAEO,SAAS,EAAE,GAAG5E,cAAcmE,SAAS;IAC7C,MAAMU,QAAQ;QAAEC,GAAG,GAAEZ,kBAAAA,MAAMa,QAAQ,YAAdb,kBAAkB;QAAUc,OAAO;IAAK;IAC7D,MAAMtC,OAAOnB,OAAOmB,IAAI,CAACuC,IAAI,GAAGlE,KAAK,CAAC,GAAG;IACzC,MAAMmE,cAAc,EAAC3D,sBAAAA,OAAO2D,WAAW,YAAlB3D,sBAAsB,IAAI0D,IAAI,GAAGlE,KAAK,CAAC,GAAG;IAE/D,MAAMoE,UAAU,CAACb,UACf/C,OAAO6D,QAAQ,CAACC,GAAG,CAAC,CAACC,SAASrC,QAAW,CAAA;gBAAEsC,KAAKD,QAAQC,GAAG;gBAAEC,WAAWlB,QAAQc,QAAQ,CAACnC,MAAM,CAACnB,EAAE;YAAC,CAAA;IAErG,IAAI;YA2EIP;QA1EN,8CAA8C;QAC9C,IAAI2C,MAAMuB,SAAS,IAAItF,aAAa+D,MAAMuB,SAAS,GAAG;YACpD,MAAMC,MAAMjD,SAASY,GAAG,CAACa,MAAMuB,SAAS;YACxC,MAAME,UAAU,MAAMxB,UAAUyB,cAAc,CAAC,OAAOC;gBACpD,MAAMC,WAAW,MAAMD,GAAGtC,GAAG,CAACmC;gBAC9B,MAAMK,OAAOD,SAASC,IAAI;gBAC1B,IAAI,CAACD,SAASE,MAAM,IAAKD,CAAAA,wBAAAA,IAAM,CAAC,YAAY,MAAK,QAAQA,CAAAA,wBAAAA,IAAM,CAAC,YAAY,MAAKE,WAAY,OAAO;gBACpG,MAAMC,UAAU7F,kBAAkB0F;gBAClC,MAAMI,QAAQ;oBAAExE,QAAQuC,MAAMvC,MAAM,KAAK;oBAAMW,SAAS4B,MAAM5B,OAAO,KAAK;gBAAK;gBAC/E,MAAM8D,OAAO,IAAIC,IAAIH,QAAQd,QAAQ,CAACC,GAAG,CAAC,CAACC,UAAY;wBAACA,QAAQxD,EAAE;wBAAEwD;qBAAQ;gBAC5E,MAAMzD,OAAoB,aACrBqE;oBACHzE,SAASF,OAAOE,OAAO,CAAC4D,GAAG,CAAC,CAACiB,SAAY,CAAA;4BAAE5D,MAAM4D,OAAO5D,IAAI;4BAAE6D,QAAQ;mCAAID,OAAOC,MAAM;6BAAC;wBAAC,CAAA;oBACzFnB,UAAU7D,OAAO6D,QAAQ,CAACC,GAAG,CAAC,CAACC,UAC7BhE,YAAYgE,SAASA,QAAQE,SAAS,GAAGY,KAAK7C,GAAG,CAAC+B,QAAQE,SAAS,IAAIS,WAAWE;mBAEhFA,MAAM7D,OAAO,GACb;oBACEI;oBACAwC;oBACAsB,WAAW;2BAAIjF,OAAOiF,SAAS;qBAAC;mBAC5BjF,OAAOkF,IAAI,GAAG;oBAAEA,MAAM;2BAAIlF,OAAOkF,IAAI;qBAAC;gBAAC,IAAI,CAAC,KAElD,CAAC;gBAEP,MAAMC,UAAUpG,gBAAgBuB;gBAChC,IAAI6E,SAAS,MAAM,IAAI5C,aAAa;oBAAEE,SAAS;oBAAW2C,SAASD;gBAAQ;gBAC3E,MAAME,UAAUlG,cAAcmG,IAAI,CAChC,CAACtB;wBACgB,WACA;2BADfuB,KAAKC,SAAS,EAAC,YAAA,AAAClF,IAA2C,CAAC0D,IAAI,YAAjD,YAAqD,UACpEuB,KAAKC,SAAS,EAAC,eAAA,AAACb,OAA8C,CAACX,IAAI,YAApD,eAAwD;;gBAE3E,IAAIqB,SAAS;oBACXf,GAAGmB,MAAM,CACPtB,KACAjF,iBAAiB,aACZwG,OAAOC,WAAW,CAACxG,cAAc2E,GAAG,CAAC,CAACE,MAAQ;4BAACA;4BAAM1D,IAA2C,CAAC0D,IAAI;yBAAC,IACtGhF,YAAYsB,MAAM8C;wBACrBwC,aAAaC,KAAKC,GAAG;wBACrBC,WAAW1C,UAAUyC,GAAG;;gBAG9B;gBACA,OAAO;oBAAExF;oBAAM+E;gBAAQ;YACzB;YACA,IAAIjB,SAAS;gBACX,IAAIA,QAAQiB,OAAO,EAAE;oBACnB,MAAM1G,gBAAgBmE,QAAQQ,OAAO,mCAAmC;wBACtE0C,MAAM;wBACNzF,IAAIoC,MAAMuB,SAAS;wBACnB/C,MAAMiD,QAAQ9D,IAAI,CAACa,IAAI;oBACzB;gBACF;gBACA,OAAO;oBACLsB,SAAS2B,QAAQiB,OAAO,GAAG,YAAY;oBACvCnB,WAAWvB,MAAMuB,SAAS;oBAC1BL,UAAUD,QAAQQ,QAAQ9D,IAAI;gBAChC;YACF;QACF;QAEA,IAAIqC,MAAMuB,SAAS,IAAIvB,MAAMsD,QAAQ,KAAK,OAAO,OAAO;YAAExD,SAAS;QAAU;QAE7E,iBAAiB;QACjB,MAAMyD,OAAO,MAAMjF,SAASC,UAAUC;QACtC,IAAI,CAAC+E,MAAM,OAAO;YAAEzD,SAAS;YAAW2C,SAAS;QAAuE;QACxH,MAAMlB,YAAY1F,MAAMe,iBAAiB;QACzC,MAAMwD,UAAuB;YAC3B5B;YACA+E;YACAvC;YACAqC,MAAM;YACNG,QAAQxD,MAAMwD,MAAM,KAAK,WAAW,WAAW;YAC/ClB,WAAW;mBAAIjF,OAAOiF,SAAS;aAAC;WAC5BjF,EAAAA,eAAAA,OAAOkF,IAAI,qBAAXlF,aAAawB,MAAM,IAAG;YAAE0D,MAAM;mBAAIlF,OAAOkF,IAAI;aAAC;QAAC,IAAI,CAAC;YACxDhF,SAASF,OAAOE,OAAO,CAAC4D,GAAG,CAAC,CAACiB,SAAY,CAAA;oBAAE5D,MAAM4D,OAAO5D,IAAI;oBAAE6D,QAAQ;2BAAID,OAAOC,MAAM;qBAAC;gBAAC,CAAA;YACzFnB,UAAU7D,OAAO6D,QAAQ,CAACC,GAAG,CAAC,CAACC,UAAYhE,YAAYgE,SAASW,WAAW;oBAAEtE,QAAQ;oBAAMW,SAAS;gBAAK;YACzGqF,gBAAgB;;QAElB,MAAMjB,UAAUpG,gBAAgBgE;QAChC,IAAIoC,SAAS,OAAO;YAAE1C,SAAS;YAAW2C,SAASD;QAAQ;QAC3D,MAAMW,MAAMD,KAAKC,GAAG;QACpB,MAAM3B,MAAMjD,SAASY,GAAG,CAACoC;QACzB,MAAMtB,UAAUyB,cAAc,CAAC,OAAOC;gBAcrB3B;YAbf,MAAM0D,OAAO,AAAC,CAAA,MAAM/B,GAAGtC,GAAG,CAACd,SAASoF,KAAK,GAAE,EAAG9B,IAAI,GAAG8B,KAAK;YAC1D,MAAMC,QAAQ/H,MAAMgI,UAAU,CAACtD,KAAc,mBAAmBpD,OAAOuG,SAAS;YAChF,IAAI,CAACE,MAAME,OAAO,EAAE,MAAM,IAAIlE,aAAa;gBAAEE,SAAS;gBAAciE,OAAO5G,OAAOyG,MAAMG,KAAK,KAAK;YAAE;YACpGpC,GAAGqC,MAAM,CACPxC,KACAjF,iBAAiB,aACZ6D,SACA/D,YAAY+D,SAASK;gBACxBwD,aAAad;gBACbF,aAAaE;gBACbe,WAAW;gBACXC,WAAWzD,UAAUyC,GAAG;gBACxBC,WAAW1C,UAAUyC,GAAG;gBACxBiB,SAAS,GAAEpE,kBAAAA,MAAMa,QAAQ,YAAdb,kBAAkB;;QAGnC;QACA,MAAMhE,gBAAgBmE,QAAQQ,OAAO,oBAAoB;YAAE0C,MAAM;YAAWzF,IAAI2D;YAAW/C,MAAM4B,QAAQ5B,IAAI;QAAC;QAC9G,OAAO;YAAEsB,SAAS;YAAWyB;YAAWL,UAAUD,QAAQb;QAAS;IACrE,EAAE,OAAOiE,OAAO;QACd,IAAIA,iBAAiBzE,cAAc,OAAOyE,MAAMvE,OAAO;QACvD,MAAMuE;IACR;AACF;AAEA,OAAO,MAAMC,wBAA6C;IACxDC,eAAe,CAACvE,QAAUD,qBAAqBC;AACjD,EAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache';
|
|
18
|
+
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
19
|
+
import { getLockdownVerdict } from '@aglyn/tenant-data-admin';
|
|
20
|
+
import { type InventoryAdjustmentReason } from '../model';
|
|
21
|
+
type Firestore = FirebaseFirestore.Firestore;
|
|
22
|
+
/** The reasons the Adjust stock dialog offers. */
|
|
23
|
+
export declare const STOCK_ADJUST_REASONS: readonly InventoryAdjustmentReason[];
|
|
24
|
+
export interface ProductsWriteDeps {
|
|
25
|
+
firestore?: () => Firestore;
|
|
26
|
+
verifyIdToken?: (token: string) => Promise<Record<string, unknown> & {
|
|
27
|
+
uid: string;
|
|
28
|
+
}>;
|
|
29
|
+
orgForHost?: (hostId: string) => Promise<{
|
|
30
|
+
org: Record<string, unknown>;
|
|
31
|
+
} | null>;
|
|
32
|
+
lockdown?: (options: Parameters<typeof getLockdownVerdict>[0]) => Promise<unknown>;
|
|
33
|
+
dropCache?: typeof dropPluginSiteCache;
|
|
34
|
+
now?: () => number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Saves a product as the console editor does. `create: true` makes a new
|
|
38
|
+
* product under the client-minted `productId` (a retry finds its own product
|
|
39
|
+
* and answers `replayed`); otherwise it replaces the live product, keeping
|
|
40
|
+
* its live stock holds.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createProductSaveHandler(options?: ProductsWriteDeps): PluginApiHandler;
|
|
43
|
+
/**
|
|
44
|
+
* The stored timestamps an edit keeps. The console's replace writes back the
|
|
45
|
+
* Timestamps its seed carried (`createdAt`, a publish time); a client's JSON
|
|
46
|
+
* cannot carry one, so each stored Timestamp field the request does not name
|
|
47
|
+
* is kept as stored.
|
|
48
|
+
*/
|
|
49
|
+
export declare function storedTimestamps(stored: Record<string, unknown>, asked: Record<string, unknown>): Record<string, unknown>;
|
|
50
|
+
/** The ledger row id one attempt owns: the same key from the same member is the same row. */
|
|
51
|
+
export declare function stockAttemptId(uid: string, key: string): string;
|
|
52
|
+
/**
|
|
53
|
+
* Adjusts one variant's stock as the Adjust stock dialog does: the variants
|
|
54
|
+
* and stock fields on the product, and one `inventoryAdjustments` row, in one
|
|
55
|
+
* transaction. The Idempotency-Key names that row, so a retried attempt finds
|
|
56
|
+
* it and moves nothing twice.
|
|
57
|
+
*/
|
|
58
|
+
export declare function createProductStockHandler(options?: ProductsWriteDeps): PluginApiHandler;
|
|
59
|
+
export declare const productSaveHandler: PluginApiHandler;
|
|
60
|
+
export declare const productStockHandler: PluginApiHandler;
|
|
61
|
+
export {};
|