@aglyn/plugins-commerce 1.0.0-beta.250 → 1.0.0-beta.252

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/package.json +13 -13
  2. package/src/lib/components/commerce-console-page.d.ts +1 -1
  3. package/src/lib/components/commerce-console-page.js +139 -175
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/commerce-console-sections.d.ts +30 -2
  6. package/src/lib/components/commerce-console-sections.js +82 -7
  7. package/src/lib/components/commerce-console-sections.js.map +1 -1
  8. package/src/lib/components/console/discounts-card.component.js +36 -3
  9. package/src/lib/components/console/discounts-card.component.js.map +1 -1
  10. package/src/lib/components/console/host-coupons-card.component.js +1 -1
  11. package/src/lib/components/console/host-coupons-card.component.js.map +1 -1
  12. package/src/lib/components/console/pos/pos-pairing.component.d.ts +59 -0
  13. package/src/lib/components/console/pos/pos-pairing.component.js +174 -0
  14. package/src/lib/components/console/pos/pos-pairing.component.js.map +1 -0
  15. package/src/lib/components/console/pos/use-pos-display.d.ts +0 -8
  16. package/src/lib/components/console/pos/use-pos-display.js +2 -16
  17. package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
  18. package/src/lib/components/console/pos-devices-card.component.d.ts +12 -1
  19. package/src/lib/components/console/pos-devices-card.component.js +132 -59
  20. package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
  21. package/src/lib/components/console/pos-page.component.d.ts +1 -1
  22. package/src/lib/components/console/pos-page.component.js +37 -86
  23. package/src/lib/components/console/pos-page.component.js.map +1 -1
  24. package/src/lib/components/console/registers-card.component.js +13 -0
  25. package/src/lib/components/console/registers-card.component.js.map +1 -1
  26. package/src/lib/components/console/shipping-settings-card.component.js +6 -11
  27. package/src/lib/components/console/shipping-settings-card.component.js.map +1 -1
  28. package/src/lib/components/console/store-zones.d.ts +15 -0
  29. package/src/lib/components/console/store-zones.js +36 -4
  30. package/src/lib/components/console/store-zones.js.map +1 -1
  31. package/src/lib/components/console/suppliers-card.component.js +2 -2
  32. package/src/lib/components/console/suppliers-card.component.js.map +1 -1
  33. package/src/lib/model/commerce-record-routes.js +10 -3
  34. package/src/lib/model/commerce-record-routes.js.map +1 -1
  35. package/src/lib/model/commerce-shipping.d.ts +16 -0
  36. package/src/lib/model/commerce-shipping.js +21 -0
  37. package/src/lib/model/commerce-shipping.js.map +1 -1
  38. package/src/lib/model/commerce-tax-decision.js +2 -2
  39. package/src/lib/model/commerce-tax-decision.js.map +1 -1
  40. package/src/lib/plugin.js +4 -1
  41. package/src/lib/plugin.js.map +1 -1
  42. package/src/lib/server/billing-webhook.js +13 -0
  43. package/src/lib/server/billing-webhook.js.map +1 -1
  44. package/src/lib/server/order-webhooks.js +1 -1
  45. package/src/lib/server/order-webhooks.js.map +1 -1
  46. package/src/lib/server/shippingeasy.js +1 -1
  47. package/src/lib/server/shippingeasy.js.map +1 -1
  48. package/src/lib/server/store-setup-ai-capability.js +10 -2
  49. package/src/lib/server/store-setup-ai-capability.js.map +1 -1
  50. package/src/lib/server/supplier-routing.d.ts +86 -0
  51. package/src/lib/server/supplier-routing.js +175 -0
  52. package/src/lib/server/supplier-routing.js.map +1 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/store-setup-ai-capability.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 {\n registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { COMMERCE_MAX_PRICE_USD } from '../model/commerce'\nimport {\n LOCAL_DELIVERY_POSTCODES_MAX,\n PICKUP_HOURS_MAX,\n PICKUP_INSTRUCTIONS_MAX,\n} from '../model/local-fulfillment-settings'\nimport {\n STORE_SETUP_COUNTRIES_MAX,\n STORE_SETUP_LOCATION_MAX,\n STORE_SETUP_MAX_MINUTES,\n STORE_SETUP_NAME_MAX,\n STORE_SETUP_RESOURCE,\n STORE_SHIPPING_ACTIONS,\n STORE_SHIPPING_AI_KINDS,\n STORE_SHIPPING_TIERS_MAX,\n STORE_TAX_LABEL_MAX,\n STORE_TAX_MODES,\n type StoreSetupSection,\n} from '../model/store-setup-content'\n\n/**\n * WHAT AN AI BUILD CAN SET UP IN A STORE (AGL-3749): shipping zones and\n * rates, local pickup, local delivery and sales tax — \"free shipping over $50,\n * otherwise $5 flat in the US\", \"offer local pickup at our Austin shop\",\n * \"charge 8.25% Texas sales tax\".\n *\n * Four operations the AI plugin's build planner offers, all written by this\n * plugin's `store-settings` writer (`store-setup-ai-writer.ts`), so every\n * change is the console cards' own: the role that may save them, the plan's\n * `commerce` feature, lockdown, and the cards' normalizers and checks. No\n * model runs: the planner fills the arguments and they ARE the change, so an\n * item costs no credits.\n *\n * NEVER A NUMBER THE PERSON DID NOT GIVE. A blank flat rate charges nothing\n * and a tax rate is the merchant's legal call, so every amount and\n * percentage is \"only when the request states it\", and the writer refuses a\n * rate without one. A request to \"set up tax\" that says neither how nor what\n * rate is left out of the plan rather than guessed at.\n *\n * NEVER AN ACCOUNT. Carrier rates, label services and outside tax services\n * need a person to connect an account; none of them is offered here.\n *\n * Settings are live once written: the cards save them live, and a store\n * setting has no draft. `live` (the contract's flag, AGL-3745) tells the\n * build step so, and the job reports the change as live.\n * Free has no commerce, so a Free workspace's build is never offered one.\n */\n\nexport const STORE_SHIPPING_AI_OP = 'store-shipping'\nexport const STORE_PICKUP_AI_OP = 'store-pickup'\nexport const STORE_DELIVERY_AI_OP = 'store-delivery'\nexport const STORE_TAX_AI_OP = 'store-tax'\n\n/** Where a person finds what the build set. */\nconst WHERE = 'Products → Settings'\n\n/** The writer's content for an item: its arguments, tagged with the section its op changes. */\nexport function storeSetupContentFromArgs(\n section: StoreSetupSection,\n args: PluginAiCapabilityArgs,\n): Readonly<Record<string, unknown>> {\n const content: Record<string, unknown> = { section }\n for (const [key, value] of Object.entries(args)) {\n if (value === undefined || value === null || value === '') continue\n content[key] = value\n }\n return content\n}\n\nconst STATED = 'ONLY when the request states it; never a typical or suggested amount.'\n\n/** What every store-setup operation shares. */\nfunction storeSetupCapability(\n op: string,\n section: StoreSetupSection,\n rest: Pick<PluginAiCapability, 'noun' | 'intents' | 'argsSchema' | 'maxPerPlan'>,\n): PluginAiCapability {\n return {\n op,\n where: WHERE,\n ...rest,\n // Free includes no commerce.\n freeAllowed: false,\n feature: 'commerce',\n draftResource: STORE_SETUP_RESOURCE,\n live: true,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n draftContent: (item) => storeSetupContentFromArgs(section, item.args),\n }\n}\n\nexport const storeShippingAiCapability = storeSetupCapability(STORE_SHIPPING_AI_OP, 'shipping', {\n noun: 'shipping rate',\n intents: [\n 'store shipping: a shipping zone (countries) and its rate — flat, free over an order total, or tiered by order total or weight; change or remove a rate or zone. Live once the build runs. Amounts only as the request states them',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n description: 'set (the default) adds or replaces the rate named rateName in the zone; remove deletes that rate, or the whole zone and its rates when rateName is left out.',\n enum: STORE_SHIPPING_ACTIONS,\n },\n zoneName: {\n type: 'string',\n description: 'The shipping zone, e.g. \"United States\" or \"Rest of world\". An existing zone is matched by this name or by the same countries.',\n maxLength: STORE_SETUP_NAME_MAX,\n },\n countries: {\n type: 'array',\n description: 'Two-letter country codes the zone ships to ([\"US\"], [\"US\",\"CA\"]), or [\"*\"] for everywhere no other zone names. Required for a new zone.',\n items: { type: 'string', maxLength: 2 },\n maxItems: STORE_SETUP_COUNTRIES_MAX,\n },\n rateName: {\n type: 'string',\n description: 'What the shopper sees at checkout, e.g. \"Standard\". Required for set.',\n maxLength: STORE_SETUP_NAME_MAX,\n },\n kind: {\n type: 'string',\n description:\n 'flat: one price. free_over: free at or above an order subtotal, otherwise amountUsd. price_tiers: priced by order subtotal. weight_tiers: priced by parcel weight in grams. Required for set.',\n enum: STORE_SHIPPING_AI_KINDS,\n },\n amountUsd: {\n type: 'number',\n description: `flat: the price; free_over: the price below the threshold. In dollars, 0 for free. ${STATED}`,\n minimum: 0,\n maximum: COMMERCE_MAX_PRICE_USD,\n },\n freeOverUsd: {\n type: 'number',\n description: `free_over: the order subtotal in dollars at which shipping becomes free. ${STATED}`,\n minimum: 0,\n maximum: COMMERCE_MAX_PRICE_USD,\n },\n tiers: {\n type: 'array',\n description: `price_tiers and weight_tiers: \"upTo:price\" entries — upTo is the subtotal in dollars (price_tiers) or grams (weight_tiers), price in dollars, e.g. [\"50:5\", \"100:8\"]. A larger order than the last tier gets no rate from it. ${STATED}`,\n items: { type: 'string', maxLength: 24 },\n maxItems: STORE_SHIPPING_TIERS_MAX,\n },\n },\n required: ['zoneName'],\n additionalProperties: false,\n },\n maxPerPlan: 10,\n})\n\nexport const storePickupAiCapability = storeSetupCapability(STORE_PICKUP_AI_OP, 'pickup', {\n noun: 'pickup setting',\n intents: [\n 'store local pickup: let buyers collect orders free, at the store or at a named store location with its pickup hours and arrival instructions; switch pickup off. Live once the build runs',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n location: {\n type: 'string',\n description:\n 'The store location buyers collect from — its name, city or street, e.g. \"Austin\" — which must already be one of the store’s inventory locations. Leave out for a plain \"Local pickup\" choice at checkout with no location.',\n maxLength: STORE_SETUP_LOCATION_MAX,\n },\n enabled: { type: 'boolean', description: 'false to stop offering pickup; leave out to offer it.' },\n hours: {\n type: 'string',\n description: 'With location: pickup hours as the request gives them, one set of days per line or separated by \";\", e.g. \"Mo-Fr 09:00-17:00; Sa 10:00-14:00\".',\n maxLength: PICKUP_HOURS_MAX,\n },\n instructions: {\n type: 'string',\n description: 'With location: what the buyer does on arrival, as the request says it.',\n maxLength: PICKUP_INSTRUCTIONS_MAX,\n },\n readyWithinMinutes: {\n type: 'integer',\n description: 'With location: how long an order usually takes to prepare, in minutes, when the request says.',\n minimum: 0,\n maximum: STORE_SETUP_MAX_MINUTES,\n },\n },\n additionalProperties: false,\n },\n maxPerPlan: 5,\n})\n\nexport const storeDeliveryAiCapability = storeSetupCapability(STORE_DELIVERY_AI_OP, 'local-delivery', {\n noun: 'local delivery setting',\n intents: [\n 'store local delivery the merchant does themselves: delivery zones by postal code with a fee, a minimum and a free-over amount, delivery windows and instructions; switch it off. Live once the build runs. Fees only as the request states them',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n enabled: { type: 'boolean', description: 'false to stop offering local delivery; leave out to offer it.' },\n country: { type: 'string', description: 'The two-letter country the store delivers within, e.g. US.', maxLength: 2 },\n location: {\n type: 'string',\n description: 'The inventory location orders leave from, by name or city, when the request names one.',\n maxLength: STORE_SETUP_LOCATION_MAX,\n },\n zoneName: {\n type: 'string',\n description: 'The delivery zone to add, change or remove, e.g. \"Downtown\".',\n maxLength: STORE_SETUP_NAME_MAX,\n },\n postcodes: {\n type: 'array',\n description: 'The zone’s postal codes: exact (\"78701\"), a prefix (\"787*\") or a range (\"78701-78705\"). Required for a new zone.',\n items: { type: 'string', maxLength: 24 },\n maxItems: LOCAL_DELIVERY_POSTCODES_MAX,\n },\n feeUsd: {\n type: 'number',\n description: `The zone’s delivery fee in dollars, 0 for free. Required for a new zone. ${STATED}`,\n minimum: 0,\n maximum: COMMERCE_MAX_PRICE_USD,\n },\n minimumUsd: { type: 'number', description: `The smallest order the zone delivers, in dollars. ${STATED}`, minimum: 0, maximum: COMMERCE_MAX_PRICE_USD },\n freeOverUsd: { type: 'number', description: `Order total in dollars at which delivery is free. ${STATED}`, minimum: 0, maximum: COMMERCE_MAX_PRICE_USD },\n removeZone: { type: 'boolean', description: 'true to remove the zone named zoneName.' },\n windows: {\n type: 'string',\n description: 'Delivery windows as the request gives them, e.g. \"Mo-Fr 09:00-12:00; Sa 10:00-14:00\". Delivery is offered only with at least one window.',\n maxLength: PICKUP_HOURS_MAX,\n },\n leadTimeMinutes: {\n type: 'integer',\n description: 'How long before a window an order must be placed, in minutes, when the request says.',\n minimum: 0,\n maximum: STORE_SETUP_MAX_MINUTES,\n },\n daysAhead: { type: 'integer', description: 'How many days ahead a window can be booked (1 to 30), when the request says.', minimum: 1, maximum: 30 },\n instructions: { type: 'string', description: 'What the buyer is told about delivery, as the request says it.', maxLength: PICKUP_INSTRUCTIONS_MAX },\n },\n additionalProperties: false,\n },\n maxPerPlan: 5,\n})\n\nexport const storeTaxAiCapability = storeSetupCapability(STORE_TAX_AI_OP, 'tax', {\n noun: 'tax setting',\n intents: [\n 'store sales tax: collect it at the manual rate the request states for a country or state, calculate it automatically, or not collect it; whether prices include tax; where the store sells from. Live once the build runs. Never a rate the request does not state — \"set up tax\" alone is a question back, not an item',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n mode: {\n type: 'string',\n description:\n 'manual: the rates the merchant states. stripe: automatic calculation per buyer location, ONLY when the request asks for automatic tax. none: the store does not collect sales tax, ONLY when the request says so. Leave out to keep the store’s choice.',\n enum: STORE_TAX_MODES,\n },\n pricesIncludeTax: {\n type: 'boolean',\n description: 'true when the request says prices already include tax (VAT-style); false when it says tax is added on top.',\n },\n originCountry: {\n type: 'string',\n description: 'The two-letter country the store sells from, e.g. US. Manual rates need it.',\n maxLength: 2,\n },\n originState: { type: 'string', description: 'The state or region the store sells from, e.g. TX.', maxLength: 3 },\n rateCountry: { type: 'string', description: 'The two-letter country a tax rate applies to.', maxLength: 2 },\n rateState: {\n type: 'string',\n description: 'The state or region a tax rate applies to, e.g. TX; leave out for the whole country.',\n maxLength: 3,\n },\n ratePercent: {\n type: 'number',\n description: `The tax rate in percent, e.g. 8.25. ${STATED}`,\n minimum: 0,\n maximum: 100,\n },\n rateLabel: { type: 'string', description: 'The receipt label, e.g. \"TX sales tax\".', maxLength: STORE_TAX_LABEL_MAX },\n removeRate: { type: 'boolean', description: 'true to remove the rate for rateCountry (and rateState).' },\n },\n additionalProperties: false,\n },\n maxPerPlan: 6,\n})\n\nexport const STORE_SETUP_AI_CAPABILITIES: readonly PluginAiCapability[] = [\n storeShippingAiCapability,\n storePickupAiCapability,\n storeDeliveryAiCapability,\n storeTaxAiCapability,\n]\n\n/**\n * Registers the four operations; the console surface calls it beside the\n * writer, since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerStoreSetupAiCapabilities(): void {\n for (const capability of STORE_SETUP_AI_CAPABILITIES) {\n registerPluginAiCapability(capability, { pluginId: BUNDLE_ID })\n }\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","COMMERCE_MAX_PRICE_USD","LOCAL_DELIVERY_POSTCODES_MAX","PICKUP_HOURS_MAX","PICKUP_INSTRUCTIONS_MAX","STORE_SETUP_COUNTRIES_MAX","STORE_SETUP_LOCATION_MAX","STORE_SETUP_MAX_MINUTES","STORE_SETUP_NAME_MAX","STORE_SETUP_RESOURCE","STORE_SHIPPING_ACTIONS","STORE_SHIPPING_AI_KINDS","STORE_SHIPPING_TIERS_MAX","STORE_TAX_LABEL_MAX","STORE_TAX_MODES","STORE_SHIPPING_AI_OP","STORE_PICKUP_AI_OP","STORE_DELIVERY_AI_OP","STORE_TAX_AI_OP","WHERE","storeSetupContentFromArgs","section","args","content","key","value","Object","entries","undefined","STATED","storeSetupCapability","op","rest","where","freeAllowed","feature","draftResource","live","estimateCredits","degrade","draftContent","item","storeShippingAiCapability","noun","intents","argsSchema","type","properties","action","description","enum","zoneName","maxLength","countries","items","maxItems","rateName","kind","amountUsd","minimum","maximum","freeOverUsd","tiers","required","additionalProperties","maxPerPlan","storePickupAiCapability","location","enabled","hours","instructions","readyWithinMinutes","storeDeliveryAiCapability","country","postcodes","feeUsd","minimumUsd","removeZone","windows","leadTimeMinutes","daysAhead","storeTaxAiCapability","mode","pricesIncludeTax","originCountry","originState","rateCountry","rateState","ratePercent","rateLabel","removeRate","STORE_SETUP_AI_CAPABILITIES","registerStoreSetupAiCapabilities","capability","pluginId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/store-setup-ai-capability.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 {\n registerPluginAiCapability,\n type PluginAiCapability,\n type PluginAiCapabilityArgs,\n} from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { COMMERCE_MAX_PRICE_USD } from '../model/commerce'\nimport {\n LOCAL_DELIVERY_POSTCODES_MAX,\n PICKUP_HOURS_MAX,\n PICKUP_INSTRUCTIONS_MAX,\n} from '../model/local-fulfillment-settings'\nimport {\n STORE_SETUP_COUNTRIES_MAX,\n STORE_SETUP_LOCATION_MAX,\n STORE_SETUP_MAX_MINUTES,\n STORE_SETUP_NAME_MAX,\n STORE_SETUP_RESOURCE,\n STORE_SHIPPING_ACTIONS,\n STORE_SHIPPING_AI_KINDS,\n STORE_SHIPPING_TIERS_MAX,\n STORE_TAX_LABEL_MAX,\n STORE_TAX_MODES,\n type StoreSetupSection,\n} from '../model/store-setup-content'\n\n/**\n * WHAT AN AI BUILD CAN SET UP IN A STORE (AGL-3749): shipping zones and\n * rates, local pickup, local delivery and sales tax — \"free shipping over $50,\n * otherwise $5 flat in the US\", \"offer local pickup at our Austin shop\",\n * \"charge 8.25% Texas sales tax\".\n *\n * Four operations the AI plugin's build planner offers, all written by this\n * plugin's `store-settings` writer (`store-setup-ai-writer.ts`), so every\n * change is the console cards' own: the role that may save them, the plan's\n * `commerce` feature, lockdown, and the cards' normalizers and checks. No\n * model runs: the planner fills the arguments and they ARE the change, so an\n * item costs no credits.\n *\n * NEVER A NUMBER THE PERSON DID NOT GIVE. A blank flat rate charges nothing\n * and a tax rate is the merchant's legal call, so every amount and\n * percentage is \"only when the request states it\", and the writer refuses a\n * rate without one. A request to \"set up tax\" that says neither how nor what\n * rate is left out of the plan rather than guessed at.\n *\n * NEVER AN ACCOUNT. Carrier rates, label services and outside tax services\n * need a person to connect an account; none of them is offered here.\n *\n * Settings are live once written: the cards save them live, and a store\n * setting has no draft. `live` (the contract's flag, AGL-3745) tells the\n * build step so, and the job reports the change as live.\n * Free has no commerce, so a Free workspace's build is never offered one.\n */\n\nexport const STORE_SHIPPING_AI_OP = 'store-shipping'\nexport const STORE_PICKUP_AI_OP = 'store-pickup'\nexport const STORE_DELIVERY_AI_OP = 'store-delivery'\nexport const STORE_TAX_AI_OP = 'store-tax'\n\n/**\n * Where a person finds what the build set: the settings page each section's\n * card is on (AGL-3713). Pickup is per location, on Shipping & delivery.\n */\nconst WHERE: Readonly<Record<StoreSetupSection, string>> = {\n shipping: 'Products → Shipping & delivery',\n pickup: 'Products → Shipping & delivery',\n 'local-delivery': 'Products → Shipping & delivery',\n tax: 'Products → Taxes',\n}\n\n/** The writer's content for an item: its arguments, tagged with the section its op changes. */\nexport function storeSetupContentFromArgs(\n section: StoreSetupSection,\n args: PluginAiCapabilityArgs,\n): Readonly<Record<string, unknown>> {\n const content: Record<string, unknown> = { section }\n for (const [key, value] of Object.entries(args)) {\n if (value === undefined || value === null || value === '') continue\n content[key] = value\n }\n return content\n}\n\nconst STATED = 'ONLY when the request states it; never a typical or suggested amount.'\n\n/** What every store-setup operation shares. */\nfunction storeSetupCapability(\n op: string,\n section: StoreSetupSection,\n rest: Pick<PluginAiCapability, 'noun' | 'intents' | 'argsSchema' | 'maxPerPlan'>,\n): PluginAiCapability {\n return {\n op,\n where: WHERE[section],\n ...rest,\n // Free includes no commerce.\n freeAllowed: false,\n feature: 'commerce',\n draftResource: STORE_SETUP_RESOURCE,\n live: true,\n // Written without a model: the planner already filled the arguments.\n estimateCredits: () => 0,\n degrade: 'omit',\n draftContent: (item) => storeSetupContentFromArgs(section, item.args),\n }\n}\n\nexport const storeShippingAiCapability = storeSetupCapability(STORE_SHIPPING_AI_OP, 'shipping', {\n noun: 'shipping rate',\n intents: [\n 'store shipping: a shipping zone (countries) and its rate — flat, free over an order total, or tiered by order total or weight; change or remove a rate or zone. Live once the build runs. Amounts only as the request states them',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n action: {\n type: 'string',\n description: 'set (the default) adds or replaces the rate named rateName in the zone; remove deletes that rate, or the whole zone and its rates when rateName is left out.',\n enum: STORE_SHIPPING_ACTIONS,\n },\n zoneName: {\n type: 'string',\n description: 'The shipping zone, e.g. \"United States\" or \"Rest of world\". An existing zone is matched by this name or by the same countries.',\n maxLength: STORE_SETUP_NAME_MAX,\n },\n countries: {\n type: 'array',\n description: 'Two-letter country codes the zone ships to ([\"US\"], [\"US\",\"CA\"]), or [\"*\"] for everywhere no other zone names. Required for a new zone.',\n items: { type: 'string', maxLength: 2 },\n maxItems: STORE_SETUP_COUNTRIES_MAX,\n },\n rateName: {\n type: 'string',\n description: 'What the shopper sees at checkout, e.g. \"Standard\". Required for set.',\n maxLength: STORE_SETUP_NAME_MAX,\n },\n kind: {\n type: 'string',\n description:\n 'flat: one price. free_over: free at or above an order subtotal, otherwise amountUsd. price_tiers: priced by order subtotal. weight_tiers: priced by parcel weight in grams. Required for set.',\n enum: STORE_SHIPPING_AI_KINDS,\n },\n amountUsd: {\n type: 'number',\n description: `flat: the price; free_over: the price below the threshold. In dollars, 0 for free. ${STATED}`,\n minimum: 0,\n maximum: COMMERCE_MAX_PRICE_USD,\n },\n freeOverUsd: {\n type: 'number',\n description: `free_over: the order subtotal in dollars at which shipping becomes free. ${STATED}`,\n minimum: 0,\n maximum: COMMERCE_MAX_PRICE_USD,\n },\n tiers: {\n type: 'array',\n description: `price_tiers and weight_tiers: \"upTo:price\" entries — upTo is the subtotal in dollars (price_tiers) or grams (weight_tiers), price in dollars, e.g. [\"50:5\", \"100:8\"]. A larger order than the last tier gets no rate from it. ${STATED}`,\n items: { type: 'string', maxLength: 24 },\n maxItems: STORE_SHIPPING_TIERS_MAX,\n },\n },\n required: ['zoneName'],\n additionalProperties: false,\n },\n maxPerPlan: 10,\n})\n\nexport const storePickupAiCapability = storeSetupCapability(STORE_PICKUP_AI_OP, 'pickup', {\n noun: 'pickup setting',\n intents: [\n 'store local pickup: let buyers collect orders free, at the store or at a named store location with its pickup hours and arrival instructions; switch pickup off. Live once the build runs',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n location: {\n type: 'string',\n description:\n 'The store location buyers collect from — its name, city or street, e.g. \"Austin\" — which must already be one of the store’s inventory locations. Leave out for a plain \"Local pickup\" choice at checkout with no location.',\n maxLength: STORE_SETUP_LOCATION_MAX,\n },\n enabled: { type: 'boolean', description: 'false to stop offering pickup; leave out to offer it.' },\n hours: {\n type: 'string',\n description: 'With location: pickup hours as the request gives them, one set of days per line or separated by \";\", e.g. \"Mo-Fr 09:00-17:00; Sa 10:00-14:00\".',\n maxLength: PICKUP_HOURS_MAX,\n },\n instructions: {\n type: 'string',\n description: 'With location: what the buyer does on arrival, as the request says it.',\n maxLength: PICKUP_INSTRUCTIONS_MAX,\n },\n readyWithinMinutes: {\n type: 'integer',\n description: 'With location: how long an order usually takes to prepare, in minutes, when the request says.',\n minimum: 0,\n maximum: STORE_SETUP_MAX_MINUTES,\n },\n },\n additionalProperties: false,\n },\n maxPerPlan: 5,\n})\n\nexport const storeDeliveryAiCapability = storeSetupCapability(STORE_DELIVERY_AI_OP, 'local-delivery', {\n noun: 'local delivery setting',\n intents: [\n 'store local delivery the merchant does themselves: delivery zones by postal code with a fee, a minimum and a free-over amount, delivery windows and instructions; switch it off. Live once the build runs. Fees only as the request states them',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n enabled: { type: 'boolean', description: 'false to stop offering local delivery; leave out to offer it.' },\n country: { type: 'string', description: 'The two-letter country the store delivers within, e.g. US.', maxLength: 2 },\n location: {\n type: 'string',\n description: 'The inventory location orders leave from, by name or city, when the request names one.',\n maxLength: STORE_SETUP_LOCATION_MAX,\n },\n zoneName: {\n type: 'string',\n description: 'The delivery zone to add, change or remove, e.g. \"Downtown\".',\n maxLength: STORE_SETUP_NAME_MAX,\n },\n postcodes: {\n type: 'array',\n description: 'The zone’s postal codes: exact (\"78701\"), a prefix (\"787*\") or a range (\"78701-78705\"). Required for a new zone.',\n items: { type: 'string', maxLength: 24 },\n maxItems: LOCAL_DELIVERY_POSTCODES_MAX,\n },\n feeUsd: {\n type: 'number',\n description: `The zone’s delivery fee in dollars, 0 for free. Required for a new zone. ${STATED}`,\n minimum: 0,\n maximum: COMMERCE_MAX_PRICE_USD,\n },\n minimumUsd: { type: 'number', description: `The smallest order the zone delivers, in dollars. ${STATED}`, minimum: 0, maximum: COMMERCE_MAX_PRICE_USD },\n freeOverUsd: { type: 'number', description: `Order total in dollars at which delivery is free. ${STATED}`, minimum: 0, maximum: COMMERCE_MAX_PRICE_USD },\n removeZone: { type: 'boolean', description: 'true to remove the zone named zoneName.' },\n windows: {\n type: 'string',\n description: 'Delivery windows as the request gives them, e.g. \"Mo-Fr 09:00-12:00; Sa 10:00-14:00\". Delivery is offered only with at least one window.',\n maxLength: PICKUP_HOURS_MAX,\n },\n leadTimeMinutes: {\n type: 'integer',\n description: 'How long before a window an order must be placed, in minutes, when the request says.',\n minimum: 0,\n maximum: STORE_SETUP_MAX_MINUTES,\n },\n daysAhead: { type: 'integer', description: 'How many days ahead a window can be booked (1 to 30), when the request says.', minimum: 1, maximum: 30 },\n instructions: { type: 'string', description: 'What the buyer is told about delivery, as the request says it.', maxLength: PICKUP_INSTRUCTIONS_MAX },\n },\n additionalProperties: false,\n },\n maxPerPlan: 5,\n})\n\nexport const storeTaxAiCapability = storeSetupCapability(STORE_TAX_AI_OP, 'tax', {\n noun: 'tax setting',\n intents: [\n 'store sales tax: collect it at the manual rate the request states for a country or state, calculate it automatically, or not collect it; whether prices include tax; where the store sells from. Live once the build runs. Never a rate the request does not state — \"set up tax\" alone is a question back, not an item',\n ],\n argsSchema: {\n type: 'object',\n properties: {\n mode: {\n type: 'string',\n description:\n 'manual: the rates the merchant states. stripe: automatic calculation per buyer location, ONLY when the request asks for automatic tax. none: the store does not collect sales tax, ONLY when the request says so. Leave out to keep the store’s choice.',\n enum: STORE_TAX_MODES,\n },\n pricesIncludeTax: {\n type: 'boolean',\n description: 'true when the request says prices already include tax (VAT-style); false when it says tax is added on top.',\n },\n originCountry: {\n type: 'string',\n description: 'The two-letter country the store sells from, e.g. US. Manual rates need it.',\n maxLength: 2,\n },\n originState: { type: 'string', description: 'The state or region the store sells from, e.g. TX.', maxLength: 3 },\n rateCountry: { type: 'string', description: 'The two-letter country a tax rate applies to.', maxLength: 2 },\n rateState: {\n type: 'string',\n description: 'The state or region a tax rate applies to, e.g. TX; leave out for the whole country.',\n maxLength: 3,\n },\n ratePercent: {\n type: 'number',\n description: `The tax rate in percent, e.g. 8.25. ${STATED}`,\n minimum: 0,\n maximum: 100,\n },\n rateLabel: { type: 'string', description: 'The receipt label, e.g. \"TX sales tax\".', maxLength: STORE_TAX_LABEL_MAX },\n removeRate: { type: 'boolean', description: 'true to remove the rate for rateCountry (and rateState).' },\n },\n additionalProperties: false,\n },\n maxPerPlan: 6,\n})\n\nexport const STORE_SETUP_AI_CAPABILITIES: readonly PluginAiCapability[] = [\n storeShippingAiCapability,\n storePickupAiCapability,\n storeDeliveryAiCapability,\n storeTaxAiCapability,\n]\n\n/**\n * Registers the four operations; the console surface calls it beside the\n * writer, since only the console runs AI jobs (AGL-3026). Idempotent.\n */\nexport function registerStoreSetupAiCapabilities(): void {\n for (const capability of STORE_SETUP_AI_CAPABILITIES) {\n registerPluginAiCapability(capability, { pluginId: BUNDLE_ID })\n }\n}\n"],"names":["registerPluginAiCapability","BUNDLE_ID","COMMERCE_MAX_PRICE_USD","LOCAL_DELIVERY_POSTCODES_MAX","PICKUP_HOURS_MAX","PICKUP_INSTRUCTIONS_MAX","STORE_SETUP_COUNTRIES_MAX","STORE_SETUP_LOCATION_MAX","STORE_SETUP_MAX_MINUTES","STORE_SETUP_NAME_MAX","STORE_SETUP_RESOURCE","STORE_SHIPPING_ACTIONS","STORE_SHIPPING_AI_KINDS","STORE_SHIPPING_TIERS_MAX","STORE_TAX_LABEL_MAX","STORE_TAX_MODES","STORE_SHIPPING_AI_OP","STORE_PICKUP_AI_OP","STORE_DELIVERY_AI_OP","STORE_TAX_AI_OP","WHERE","shipping","pickup","tax","storeSetupContentFromArgs","section","args","content","key","value","Object","entries","undefined","STATED","storeSetupCapability","op","rest","where","freeAllowed","feature","draftResource","live","estimateCredits","degrade","draftContent","item","storeShippingAiCapability","noun","intents","argsSchema","type","properties","action","description","enum","zoneName","maxLength","countries","items","maxItems","rateName","kind","amountUsd","minimum","maximum","freeOverUsd","tiers","required","additionalProperties","maxPerPlan","storePickupAiCapability","location","enabled","hours","instructions","readyWithinMinutes","storeDeliveryAiCapability","country","postcodes","feeUsd","minimumUsd","removeZone","windows","leadTimeMinutes","daysAhead","storeTaxAiCapability","mode","pricesIncludeTax","originCountry","originState","rateCountry","rateState","ratePercent","rateLabel","removeRate","STORE_SETUP_AI_CAPABILITIES","registerStoreSetupAiCapabilities","capability","pluginId"],"mappings":"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"}
@@ -0,0 +1,86 @@
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 * as CommerceModel from '../model';
18
+ /**
19
+ * Dropship routing for a multi-line CART order (AGL-3773).
20
+ *
21
+ * The Buy-button path has routed a paid order to its product's supplier since
22
+ * AGL-289; the cart path recorded each line's `supplierId` and told nobody, so
23
+ * a supplier assigned to a product never heard about an order placed through
24
+ * the cart. This sends one notice per supplier on the order, naming only that
25
+ * supplier's lines.
26
+ *
27
+ * - **Token.** Each supplier gets `HMAC(hostId:orderId:supplierId)`, the token
28
+ * `supplier-update.ts` re-derives for every supplier on the order, so the
29
+ * link it posts tracking through marks only its own lines shipped (AGL-2455).
30
+ * Nothing is stored for it.
31
+ * - **Webhook.** Queued, never posted from the webhook, through the same
32
+ * outbox as the Buy-button path (AGL-2473): a deterministic id and
33
+ * `create()`, so a second pass cannot queue it twice or reset its retries.
34
+ * - **Email.** Sent inline through Aglyn's own provider, like the receipt.
35
+ *
36
+ * The caller runs this once, behind the AGL-498 `created` guard, and it never
37
+ * throws: a supplier that cannot be told is logged and stamped on the order's
38
+ * timeline as routed only when the notice went out, and the paid order
39
+ * stands.
40
+ */
41
+ /** What the supplier is told about one line. */
42
+ export interface SupplierRoutedLine {
43
+ productId: string;
44
+ name: string;
45
+ variantLabel?: string;
46
+ sku?: string;
47
+ quantity: number;
48
+ }
49
+ /** The session fields the notice is built from. */
50
+ export interface SupplierRoutingSession {
51
+ customer_details?: {
52
+ email?: string | null;
53
+ name?: string | null;
54
+ } | null;
55
+ shipping_details?: {
56
+ name?: string | null;
57
+ address?: {
58
+ line1?: string | null;
59
+ line2?: string | null;
60
+ city?: string | null;
61
+ state?: string | null;
62
+ postal_code?: string | null;
63
+ country?: string | null;
64
+ } | null;
65
+ } | null;
66
+ }
67
+ export interface RouteCartOrderOptions {
68
+ firestore: FirebaseFirestore.Firestore;
69
+ hostId: string;
70
+ orderId: string;
71
+ /** The number the merchant and supplier know the order by, e.g. `#12`. */
72
+ orderLabel: string;
73
+ lineItems: readonly CommerceModel.OrderLineItem[];
74
+ session: SupplierRoutingSession;
75
+ /** The host this request came in on; the tracking link is built on it. */
76
+ requestHost: string;
77
+ }
78
+ /** The callback token for one supplier on one order. */
79
+ export declare function supplierRoutingToken(hostId: string, orderId: string, supplierId: string): string;
80
+ /** The order's lines, grouped by the supplier that fulfills them. */
81
+ export declare function linesBySupplier(lineItems: readonly CommerceModel.OrderLineItem[]): Map<string, SupplierRoutedLine[]>;
82
+ /**
83
+ * Notifies every supplier with lines on a paid cart order. Resolves once each
84
+ * notice is queued or sent; never rejects.
85
+ */
86
+ export declare function routeCartOrderToSuppliers(options: RouteCartOrderOptions): Promise<void>;
@@ -0,0 +1,175 @@
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, hostSendingIdentity, meterHostEmail } from "@aglyn/tenant-data-admin";
19
+ import { resolveHostToken } from "@aglyn/aglyn/app-utils/host-tokens";
20
+ import { sendEmail } from "@aglyn/shared-util-email";
21
+ import { createHmac } from "crypto";
22
+ import { tokenSigningSecret } from "./download.js";
23
+ import { enqueueSupplierDelivery } from "./supplier-outbox.js";
24
+ /** The callback token for one supplier on one order. */ export function supplierRoutingToken(hostId, orderId, supplierId) {
25
+ return createHmac('sha256', tokenSigningSecret()).update(`${hostId}:${orderId}:${supplierId}`).digest('hex').slice(0, 32);
26
+ }
27
+ /** The order's lines, grouped by the supplier that fulfills them. */ export function linesBySupplier(lineItems) {
28
+ const grouped = new Map();
29
+ for (const line of lineItems){
30
+ var _line_supplierId, _grouped_get, _line_productId, _line_name, _line_quantity;
31
+ const supplierId = String((_line_supplierId = line.supplierId) != null ? _line_supplierId : '');
32
+ if (!supplierId) continue;
33
+ const lines = (_grouped_get = grouped.get(supplierId)) != null ? _grouped_get : [];
34
+ lines.push(_extends({
35
+ productId: String((_line_productId = line.productId) != null ? _line_productId : ''),
36
+ name: String((_line_name = line.name) != null ? _line_name : 'Product')
37
+ }, line.variantLabel ? {
38
+ variantLabel: line.variantLabel
39
+ } : {}, line.sku ? {
40
+ sku: line.sku
41
+ } : {}, {
42
+ quantity: Number((_line_quantity = line.quantity) != null ? _line_quantity : 1)
43
+ }));
44
+ grouped.set(supplierId, lines);
45
+ }
46
+ return grouped;
47
+ }
48
+ function describeLine(line) {
49
+ return `${line.quantity}× ${line.name}` + (line.variantLabel ? ` (${line.variantLabel})` : '') + (line.sku ? `, SKU ${line.sku}` : '');
50
+ }
51
+ /**
52
+ * Notifies every supplier with lines on a paid cart order. Resolves once each
53
+ * notice is queued or sent; never rejects.
54
+ */ export async function routeCartOrderToSuppliers(options) {
55
+ const { firestore, hostId, orderId, orderLabel, session, requestHost } = options;
56
+ const grouped = linesBySupplier(options.lineItems);
57
+ if (grouped.size === 0) return;
58
+ try {
59
+ var _ref, _ref1, _ref2, _shipTo_address_line1, _shipTo_address_line2, _shipTo_address_city, _shipTo_address_state, _shipTo_address_postal_code, _shipTo_address_country;
60
+ var _session_shipping_details, _session_customer_details, _session_customer_details1;
61
+ const routedOrg = await getOrgForHost(hostId);
62
+ if (!Aglyn.checkEntitlement(routedOrg == null ? void 0 : routedOrg.org, 'dropshipRouting')) {
63
+ return;
64
+ }
65
+ const hostRef = firestore.collection('hosts').doc(hostId);
66
+ const orderRef = hostRef.collection('orders').doc(orderId);
67
+ const shipTo = (session == null ? void 0 : (_session_shipping_details = session.shipping_details) == null ? void 0 : _session_shipping_details.address) ? session.shipping_details : null;
68
+ const shippingName = (_ref = (_ref1 = shipTo == null ? void 0 : shipTo.name) != null ? _ref1 : session == null ? void 0 : (_session_customer_details = session.customer_details) == null ? void 0 : _session_customer_details.name) != null ? _ref : null;
69
+ const customerEmail = (_ref2 = session == null ? void 0 : (_session_customer_details1 = session.customer_details) == null ? void 0 : _session_customer_details1.email) != null ? _ref2 : null;
70
+ const shippingAddress = (shipTo == null ? void 0 : shipTo.address) ? {
71
+ line1: (_shipTo_address_line1 = shipTo.address.line1) != null ? _shipTo_address_line1 : null,
72
+ line2: (_shipTo_address_line2 = shipTo.address.line2) != null ? _shipTo_address_line2 : null,
73
+ city: (_shipTo_address_city = shipTo.address.city) != null ? _shipTo_address_city : null,
74
+ state: (_shipTo_address_state = shipTo.address.state) != null ? _shipTo_address_state : null,
75
+ postalCode: (_shipTo_address_postal_code = shipTo.address.postal_code) != null ? _shipTo_address_postal_code : null,
76
+ country: (_shipTo_address_country = shipTo.address.country) != null ? _shipTo_address_country : null
77
+ } : null;
78
+ let storeName = null;
79
+ const storeNameOnce = async ()=>{
80
+ var _resolveHostToken;
81
+ if (storeName !== null) return storeName;
82
+ storeName = (_resolveHostToken = resolveHostToken('businessName', await hostRef.get().then((snapshot)=>snapshot.data()).catch(()=>undefined))) != null ? _resolveHostToken : 'A store';
83
+ return storeName;
84
+ };
85
+ for (const [supplierId, lines] of grouped){
86
+ try {
87
+ const supplier = (await hostRef.collection('suppliers').doc(supplierId).get()).data();
88
+ if (!supplier) continue;
89
+ const token = supplierRoutingToken(hostId, orderId, supplierId);
90
+ const updateUrl = `https://${requestHost}/api/commerce/supplier-update` + `?hostId=${hostId}&orderId=${orderId}&token=${token}`;
91
+ const payload = {
92
+ hostId,
93
+ orderId,
94
+ lines: lines.map((line)=>_extends({
95
+ productId: line.productId,
96
+ productName: line.name
97
+ }, line.variantLabel ? {
98
+ variantLabel: line.variantLabel
99
+ } : {}, line.sku ? {
100
+ sku: line.sku
101
+ } : {}, {
102
+ quantity: line.quantity
103
+ })),
104
+ customerEmail,
105
+ shippingName,
106
+ shippingAddress,
107
+ updateUrl
108
+ };
109
+ let told = false;
110
+ if (supplier.webhookUrl) {
111
+ var _supplier_name;
112
+ const queued = await enqueueSupplierDelivery({
113
+ firestore,
114
+ hostId,
115
+ orderId,
116
+ supplierId,
117
+ supplierName: String((_supplier_name = supplier.name) != null ? _supplier_name : ''),
118
+ url: String(supplier.webhookUrl),
119
+ body: JSON.stringify(payload)
120
+ });
121
+ told = told || queued !== 'skipped';
122
+ }
123
+ if (supplier.email) {
124
+ var _ref3;
125
+ const store = await storeNameOnce();
126
+ const recipient = (_ref3 = shippingName != null ? shippingName : customerEmail) != null ? _ref3 : 'the buyer';
127
+ const address = shippingAddress ? [
128
+ shippingAddress.line1,
129
+ shippingAddress.line2,
130
+ [
131
+ shippingAddress.city,
132
+ [
133
+ shippingAddress.state,
134
+ shippingAddress.postalCode
135
+ ].filter(Boolean).join(' ')
136
+ ].filter(Boolean).join(', '),
137
+ shippingAddress.country
138
+ ].filter(Boolean).join(', ') : '';
139
+ const subject = lines.length === 1 ? `New order to fulfill for ${store}: ${lines[0].name}` : `New order to fulfill for ${store}: ${lines.length} items`;
140
+ await sendEmail({
141
+ to: supplier.email,
142
+ subject,
143
+ text: `${store} has a new order for you to ship (order ${orderLabel}):\n` + lines.map((line)=>`- ${describeLine(line)}`).join('\n') + '\n\n' + (address ? `Ship to: ${recipient}, ${address}\n\n` : `Ship to: ${recipient}. No shipping address was ` + `collected at checkout; ask ${store} for it.\n\n`) + 'When it ships, open this link to enter the carrier and ' + `tracking number:\n${updateUrl}`,
144
+ fromName: Aglyn.resolveBrandingProfile(routedOrg == null ? void 0 : routedOrg.org).fromName,
145
+ sendingIdentity: await hostSendingIdentity(hostId),
146
+ audience: 'tenant',
147
+ context: 'dropship supplier notice',
148
+ // Owed to the recipient by their own order (AGL-3356).
149
+ owedFor: 'order'
150
+ });
151
+ // Cost meter (AGL-1438): transactional, the order needs it.
152
+ await meterHostEmail(hostId);
153
+ told = true;
154
+ }
155
+ if (!told) continue;
156
+ await orderRef.set({
157
+ timeline: firebaseAdmin.firestore.FieldValue.arrayUnion({
158
+ atMs: Date.now(),
159
+ event: 'routed',
160
+ detail: `Sent ${lines.length} line${lines.length === 1 ? '' : 's'} ` + `to supplier ${supplier.name}`
161
+ })
162
+ }, {
163
+ merge: true
164
+ });
165
+ } catch (supplierError) {
166
+ // One supplier failing never keeps the others from being told.
167
+ console.error('Dropship routing failed', supplierId, supplierError);
168
+ }
169
+ }
170
+ } catch (routingError) {
171
+ console.error('Dropship routing failed', routingError);
172
+ }
173
+ }
174
+
175
+ //# sourceMappingURL=supplier-routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/supplier-routing.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 {\n firebaseAdmin,\n getOrgForHost,\n hostSendingIdentity,\n meterHostEmail,\n} from '@aglyn/tenant-data-admin'\nimport {\n resolveHostToken,\n type HostTokenSource,\n} from '@aglyn/aglyn/app-utils/host-tokens'\nimport { sendEmail } from '@aglyn/shared-util-email'\nimport { createHmac } from 'crypto'\nimport * as CommerceModel from '../model'\nimport { tokenSigningSecret } from './download'\nimport { enqueueSupplierDelivery } from './supplier-outbox'\n\n/**\n * Dropship routing for a multi-line CART order (AGL-3773).\n *\n * The Buy-button path has routed a paid order to its product's supplier since\n * AGL-289; the cart path recorded each line's `supplierId` and told nobody, so\n * a supplier assigned to a product never heard about an order placed through\n * the cart. This sends one notice per supplier on the order, naming only that\n * supplier's lines.\n *\n * - **Token.** Each supplier gets `HMAC(hostId:orderId:supplierId)`, the token\n * `supplier-update.ts` re-derives for every supplier on the order, so the\n * link it posts tracking through marks only its own lines shipped (AGL-2455).\n * Nothing is stored for it.\n * - **Webhook.** Queued, never posted from the webhook, through the same\n * outbox as the Buy-button path (AGL-2473): a deterministic id and\n * `create()`, so a second pass cannot queue it twice or reset its retries.\n * - **Email.** Sent inline through Aglyn's own provider, like the receipt.\n *\n * The caller runs this once, behind the AGL-498 `created` guard, and it never\n * throws: a supplier that cannot be told is logged and stamped on the order's\n * timeline as routed only when the notice went out, and the paid order\n * stands.\n */\n\n/** What the supplier is told about one line. */\nexport interface SupplierRoutedLine {\n productId: string\n name: string\n variantLabel?: string\n sku?: string\n quantity: number\n}\n\n/** The session fields the notice is built from. */\nexport interface SupplierRoutingSession {\n customer_details?: { email?: string | null; name?: string | null } | null\n shipping_details?: {\n name?: string | null\n address?: {\n line1?: string | null\n line2?: string | null\n city?: string | null\n state?: string | null\n postal_code?: string | null\n country?: string | null\n } | null\n } | null\n}\n\nexport interface RouteCartOrderOptions {\n firestore: FirebaseFirestore.Firestore\n hostId: string\n orderId: string\n /** The number the merchant and supplier know the order by, e.g. `#12`. */\n orderLabel: string\n lineItems: readonly CommerceModel.OrderLineItem[]\n session: SupplierRoutingSession\n /** The host this request came in on; the tracking link is built on it. */\n requestHost: string\n}\n\n/** The callback token for one supplier on one order. */\nexport function supplierRoutingToken(\n hostId: string,\n orderId: string,\n supplierId: string,\n): string {\n return createHmac('sha256', tokenSigningSecret())\n .update(`${hostId}:${orderId}:${supplierId}`)\n .digest('hex')\n .slice(0, 32)\n}\n\n/** The order's lines, grouped by the supplier that fulfills them. */\nexport function linesBySupplier(\n lineItems: readonly CommerceModel.OrderLineItem[],\n): Map<string, SupplierRoutedLine[]> {\n const grouped = new Map<string, SupplierRoutedLine[]>()\n for (const line of lineItems) {\n const supplierId = String(line.supplierId ?? '')\n if (!supplierId) continue\n const lines = grouped.get(supplierId) ?? []\n lines.push({\n productId: String(line.productId ?? ''),\n name: String(line.name ?? 'Product'),\n ...(line.variantLabel ? { variantLabel: line.variantLabel } : {}),\n ...(line.sku ? { sku: line.sku } : {}),\n quantity: Number(line.quantity ?? 1),\n })\n grouped.set(supplierId, lines)\n }\n return grouped\n}\n\nfunction describeLine(line: SupplierRoutedLine): string {\n return (\n `${line.quantity}× ${line.name}` +\n (line.variantLabel ? ` (${line.variantLabel})` : '') +\n (line.sku ? `, SKU ${line.sku}` : '')\n )\n}\n\n/**\n * Notifies every supplier with lines on a paid cart order. Resolves once each\n * notice is queued or sent; never rejects.\n */\nexport async function routeCartOrderToSuppliers(\n options: RouteCartOrderOptions,\n): Promise<void> {\n const { firestore, hostId, orderId, orderLabel, session, requestHost } =\n options\n const grouped = linesBySupplier(options.lineItems)\n if (grouped.size === 0) return\n try {\n const routedOrg = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(routedOrg?.org as any, 'dropshipRouting')) {\n return\n }\n const hostRef = firestore.collection('hosts').doc(hostId)\n const orderRef = hostRef.collection('orders').doc(orderId)\n const shipTo = session?.shipping_details?.address\n ? session.shipping_details\n : null\n const shippingName =\n shipTo?.name ?? session?.customer_details?.name ?? null\n const customerEmail = session?.customer_details?.email ?? null\n const shippingAddress = shipTo?.address\n ? {\n line1: shipTo.address.line1 ?? null,\n line2: shipTo.address.line2 ?? null,\n city: shipTo.address.city ?? null,\n state: shipTo.address.state ?? null,\n postalCode: shipTo.address.postal_code ?? null,\n country: shipTo.address.country ?? null,\n }\n : null\n let storeName: string | null = null\n const storeNameOnce = async () => {\n if (storeName !== null) return storeName\n storeName =\n resolveHostToken(\n 'businessName',\n (await hostRef\n .get()\n .then((snapshot) => snapshot.data())\n .catch(() => undefined)) as HostTokenSource | undefined,\n ) ?? 'A store'\n return storeName\n }\n\n for (const [supplierId, lines] of grouped) {\n try {\n const supplier = (\n await hostRef.collection('suppliers').doc(supplierId).get()\n ).data() as CommerceModel.HostSupplier | undefined\n if (!supplier) continue\n const token = supplierRoutingToken(hostId, orderId, supplierId)\n const updateUrl =\n `https://${requestHost}/api/commerce/supplier-update` +\n `?hostId=${hostId}&orderId=${orderId}&token=${token}`\n const payload = {\n hostId,\n orderId,\n lines: lines.map((line) => ({\n productId: line.productId,\n productName: line.name,\n ...(line.variantLabel ? { variantLabel: line.variantLabel } : {}),\n ...(line.sku ? { sku: line.sku } : {}),\n quantity: line.quantity,\n })),\n customerEmail,\n shippingName,\n shippingAddress,\n updateUrl,\n }\n let told = false\n if (supplier.webhookUrl) {\n const queued = await enqueueSupplierDelivery({\n firestore,\n hostId,\n orderId,\n supplierId,\n supplierName: String(supplier.name ?? ''),\n url: String(supplier.webhookUrl),\n body: JSON.stringify(payload),\n })\n told = told || queued !== 'skipped'\n }\n if (supplier.email) {\n const store = await storeNameOnce()\n const recipient = shippingName ?? customerEmail ?? 'the buyer'\n const address = shippingAddress\n ? [\n shippingAddress.line1,\n shippingAddress.line2,\n [\n shippingAddress.city,\n [shippingAddress.state, shippingAddress.postalCode]\n .filter(Boolean)\n .join(' '),\n ]\n .filter(Boolean)\n .join(', '),\n shippingAddress.country,\n ]\n .filter(Boolean)\n .join(', ')\n : ''\n const subject =\n lines.length === 1\n ? `New order to fulfill for ${store}: ${lines[0].name}`\n : `New order to fulfill for ${store}: ${lines.length} items`\n await sendEmail({\n to: supplier.email,\n subject,\n text:\n `${store} has a new order for you to ship (order ${orderLabel}):\\n` +\n lines.map((line) => `- ${describeLine(line)}`).join('\\n') +\n '\\n\\n' +\n (address\n ? `Ship to: ${recipient}, ${address}\\n\\n`\n : `Ship to: ${recipient}. No shipping address was ` +\n `collected at checkout; ask ${store} for it.\\n\\n`) +\n 'When it ships, open this link to enter the carrier and ' +\n `tracking number:\\n${updateUrl}`,\n fromName: Aglyn.resolveBrandingProfile(routedOrg?.org as never)\n .fromName,\n sendingIdentity: await hostSendingIdentity(hostId),\n audience: 'tenant',\n context: 'dropship supplier notice',\n // Owed to the recipient by their own order (AGL-3356).\n owedFor: 'order',\n })\n // Cost meter (AGL-1438): transactional, the order needs it.\n await meterHostEmail(hostId)\n told = true\n }\n if (!told) continue\n await orderRef.set(\n {\n timeline: firebaseAdmin.firestore.FieldValue.arrayUnion({\n atMs: Date.now(),\n event: 'routed',\n detail:\n `Sent ${lines.length} line${lines.length === 1 ? '' : 's'} ` +\n `to supplier ${supplier.name}`,\n }),\n },\n { merge: true },\n )\n } catch (supplierError) {\n // One supplier failing never keeps the others from being told.\n console.error('Dropship routing failed', supplierId, supplierError)\n }\n }\n } catch (routingError) {\n console.error('Dropship routing failed', routingError)\n }\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","hostSendingIdentity","meterHostEmail","resolveHostToken","sendEmail","createHmac","tokenSigningSecret","enqueueSupplierDelivery","supplierRoutingToken","hostId","orderId","supplierId","update","digest","slice","linesBySupplier","lineItems","grouped","Map","line","String","lines","get","push","productId","name","variantLabel","sku","quantity","Number","set","describeLine","routeCartOrderToSuppliers","options","firestore","orderLabel","session","requestHost","size","shipTo","routedOrg","checkEntitlement","org","hostRef","collection","doc","orderRef","shipping_details","address","shippingName","customer_details","customerEmail","email","shippingAddress","line1","line2","city","state","postalCode","postal_code","country","storeName","storeNameOnce","then","snapshot","data","catch","undefined","supplier","token","updateUrl","payload","map","productName","told","webhookUrl","queued","supplierName","url","body","JSON","stringify","store","recipient","filter","Boolean","join","subject","length","to","text","fromName","resolveBrandingProfile","sendingIdentity","audience","context","owedFor","timeline","FieldValue","arrayUnion","atMs","Date","now","event","detail","merge","supplierError","console","error","routingError"],"mappings":";AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,sBAAqB;AAC5C,SACEC,aAAa,EACbC,aAAa,EACbC,mBAAmB,EACnBC,cAAc,QACT,2BAA0B;AACjC,SACEC,gBAAgB,QAEX,qCAAoC;AAC3C,SAASC,SAAS,QAAQ,2BAA0B;AACpD,SAASC,UAAU,QAAQ,SAAQ;AAEnC,SAASC,kBAAkB,QAAQ,gBAAY;AAC/C,SAASC,uBAAuB,QAAQ,uBAAmB;AA+D3D,sDAAsD,GACtD,OAAO,SAASC,qBACdC,MAAc,EACdC,OAAe,EACfC,UAAkB;IAElB,OAAON,WAAW,UAAUC,sBACzBM,MAAM,CAAC,GAAGH,OAAO,CAAC,EAAEC,QAAQ,CAAC,EAAEC,YAAY,EAC3CE,MAAM,CAAC,OACPC,KAAK,CAAC,GAAG;AACd;AAEA,mEAAmE,GACnE,OAAO,SAASC,gBACdC,SAAiD;IAEjD,MAAMC,UAAU,IAAIC;IACpB,KAAK,MAAMC,QAAQH,UAAW;YACFG,kBAEZF,cAEME,iBACLA,YAGIA;QARnB,MAAMR,aAAaS,QAAOD,mBAAAA,KAAKR,UAAU,YAAfQ,mBAAmB;QAC7C,IAAI,CAACR,YAAY;QACjB,MAAMU,SAAQJ,eAAAA,QAAQK,GAAG,CAACX,uBAAZM,eAA2B,EAAE;QAC3CI,MAAME,IAAI,CAAC;YACTC,WAAWJ,QAAOD,kBAAAA,KAAKK,SAAS,YAAdL,kBAAkB;YACpCM,MAAML,QAAOD,aAAAA,KAAKM,IAAI,YAATN,aAAa;WACtBA,KAAKO,YAAY,GAAG;YAAEA,cAAcP,KAAKO,YAAY;QAAC,IAAI,CAAC,GAC3DP,KAAKQ,GAAG,GAAG;YAAEA,KAAKR,KAAKQ,GAAG;QAAC,IAAI,CAAC;YACpCC,UAAUC,QAAOV,iBAAAA,KAAKS,QAAQ,YAAbT,iBAAiB;;QAEpCF,QAAQa,GAAG,CAACnB,YAAYU;IAC1B;IACA,OAAOJ;AACT;AAEA,SAASc,aAAaZ,IAAwB;IAC5C,OACE,GAAGA,KAAKS,QAAQ,CAAC,EAAE,EAAET,KAAKM,IAAI,EAAE,GAC/BN,CAAAA,KAAKO,YAAY,GAAG,CAAC,EAAE,EAAEP,KAAKO,YAAY,CAAC,CAAC,CAAC,GAAG,EAAC,IACjDP,CAAAA,KAAKQ,GAAG,GAAG,CAAC,MAAM,EAAER,KAAKQ,GAAG,EAAE,GAAG,EAAC;AAEvC;AAEA;;;CAGC,GACD,OAAO,eAAeK,0BACpBC,OAA8B;IAE9B,MAAM,EAAEC,SAAS,EAAEzB,MAAM,EAAEC,OAAO,EAAEyB,UAAU,EAAEC,OAAO,EAAEC,WAAW,EAAE,GACpEJ;IACF,MAAMhB,UAAUF,gBAAgBkB,QAAQjB,SAAS;IACjD,IAAIC,QAAQqB,IAAI,KAAK,GAAG;IACxB,IAAI;YAWAC,oBAIWA,uBACAA,uBACDA,sBACCA,uBACKA,6BACHA;YAbAH,2BAIGA,2BACIA;QAXtB,MAAMI,YAAY,MAAMxC,cAAcS;QACtC,IAAI,CAACX,MAAM2C,gBAAgB,CAACD,6BAAAA,UAAWE,GAAG,EAAS,oBAAoB;YACrE;QACF;QACA,MAAMC,UAAUT,UAAUU,UAAU,CAAC,SAASC,GAAG,CAACpC;QAClD,MAAMqC,WAAWH,QAAQC,UAAU,CAAC,UAAUC,GAAG,CAACnC;QAClD,MAAM6B,SAASH,CAAAA,4BAAAA,4BAAAA,QAASW,gBAAgB,qBAAzBX,0BAA2BY,OAAO,IAC7CZ,QAAQW,gBAAgB,GACxB;QACJ,MAAME,gBACJV,gBAAAA,0BAAAA,OAAQd,IAAI,oBAAIW,4BAAAA,4BAAAA,QAASc,gBAAgB,qBAAzBd,0BAA2BX,IAAI,YAA/Cc,OAAmD;QACrD,MAAMY,yBAAgBf,4BAAAA,6BAAAA,QAASc,gBAAgB,qBAAzBd,2BAA2BgB,KAAK,oBAAI;QAC1D,MAAMC,kBAAkBd,CAAAA,0BAAAA,OAAQS,OAAO,IACnC;YACEM,KAAK,GAAEf,wBAAAA,OAAOS,OAAO,CAACM,KAAK,YAApBf,wBAAwB;YAC/BgB,KAAK,GAAEhB,wBAAAA,OAAOS,OAAO,CAACO,KAAK,YAApBhB,wBAAwB;YAC/BiB,IAAI,GAAEjB,uBAAAA,OAAOS,OAAO,CAACQ,IAAI,YAAnBjB,uBAAuB;YAC7BkB,KAAK,GAAElB,wBAAAA,OAAOS,OAAO,CAACS,KAAK,YAApBlB,wBAAwB;YAC/BmB,UAAU,GAAEnB,8BAAAA,OAAOS,OAAO,CAACW,WAAW,YAA1BpB,8BAA8B;YAC1CqB,OAAO,GAAErB,0BAAAA,OAAOS,OAAO,CAACY,OAAO,YAAtBrB,0BAA0B;QACrC,IACA;QACJ,IAAIsB,YAA2B;QAC/B,MAAMC,gBAAgB;gBAGlB3D;YAFF,IAAI0D,cAAc,MAAM,OAAOA;YAC/BA,aACE1D,oBAAAA,iBACE,gBACC,MAAMwC,QACJrB,GAAG,GACHyC,IAAI,CAAC,CAACC,WAAaA,SAASC,IAAI,IAChCC,KAAK,CAAC,IAAMC,uBALjBhE,oBAMK;YACP,OAAO0D;QACT;QAEA,KAAK,MAAM,CAAClD,YAAYU,MAAM,IAAIJ,QAAS;YACzC,IAAI;gBACF,MAAMmD,WAAW,AACf,CAAA,MAAMzB,QAAQC,UAAU,CAAC,aAAaC,GAAG,CAAClC,YAAYW,GAAG,EAAC,EAC1D2C,IAAI;gBACN,IAAI,CAACG,UAAU;gBACf,MAAMC,QAAQ7D,qBAAqBC,QAAQC,SAASC;gBACpD,MAAM2D,YACJ,CAAC,QAAQ,EAAEjC,YAAY,6BAA6B,CAAC,GACrD,CAAC,QAAQ,EAAE5B,OAAO,SAAS,EAAEC,QAAQ,OAAO,EAAE2D,OAAO;gBACvD,MAAME,UAAU;oBACd9D;oBACAC;oBACAW,OAAOA,MAAMmD,GAAG,CAAC,CAACrD,OAAU;4BAC1BK,WAAWL,KAAKK,SAAS;4BACzBiD,aAAatD,KAAKM,IAAI;2BAClBN,KAAKO,YAAY,GAAG;4BAAEA,cAAcP,KAAKO,YAAY;wBAAC,IAAI,CAAC,GAC3DP,KAAKQ,GAAG,GAAG;4BAAEA,KAAKR,KAAKQ,GAAG;wBAAC,IAAI,CAAC;4BACpCC,UAAUT,KAAKS,QAAQ;;oBAEzBuB;oBACAF;oBACAI;oBACAiB;gBACF;gBACA,IAAII,OAAO;gBACX,IAAIN,SAASO,UAAU,EAAE;wBAMAP;oBALvB,MAAMQ,SAAS,MAAMrE,wBAAwB;wBAC3C2B;wBACAzB;wBACAC;wBACAC;wBACAkE,cAAczD,QAAOgD,iBAAAA,SAAS3C,IAAI,YAAb2C,iBAAiB;wBACtCU,KAAK1D,OAAOgD,SAASO,UAAU;wBAC/BI,MAAMC,KAAKC,SAAS,CAACV;oBACvB;oBACAG,OAAOA,QAAQE,WAAW;gBAC5B;gBACA,IAAIR,SAAShB,KAAK,EAAE;wBAEAH;oBADlB,MAAMiC,QAAQ,MAAMpB;oBACpB,MAAMqB,aAAYlC,QAAAA,uBAAAA,eAAgBE,yBAAhBF,QAAiC;oBACnD,MAAMD,UAAUK,kBACZ;wBACEA,gBAAgBC,KAAK;wBACrBD,gBAAgBE,KAAK;wBACrB;4BACEF,gBAAgBG,IAAI;4BACpB;gCAACH,gBAAgBI,KAAK;gCAAEJ,gBAAgBK,UAAU;6BAAC,CAChD0B,MAAM,CAACC,SACPC,IAAI,CAAC;yBACT,CACEF,MAAM,CAACC,SACPC,IAAI,CAAC;wBACRjC,gBAAgBO,OAAO;qBACxB,CACEwB,MAAM,CAACC,SACPC,IAAI,CAAC,QACR;oBACJ,MAAMC,UACJlE,MAAMmE,MAAM,KAAK,IACb,CAAC,yBAAyB,EAAEN,MAAM,EAAE,EAAE7D,KAAK,CAAC,EAAE,CAACI,IAAI,EAAE,GACrD,CAAC,yBAAyB,EAAEyD,MAAM,EAAE,EAAE7D,MAAMmE,MAAM,CAAC,MAAM,CAAC;oBAChE,MAAMpF,UAAU;wBACdqF,IAAIrB,SAAShB,KAAK;wBAClBmC;wBACAG,MACE,GAAGR,MAAM,wCAAwC,EAAE/C,WAAW,IAAI,CAAC,GACnEd,MAAMmD,GAAG,CAAC,CAACrD,OAAS,CAAC,EAAE,EAAEY,aAAaZ,OAAO,EAAEmE,IAAI,CAAC,QACpD,SACCtC,CAAAA,UACG,CAAC,SAAS,EAAEmC,UAAU,EAAE,EAAEnC,QAAQ,IAAI,CAAC,GACvC,CAAC,SAAS,EAAEmC,UAAU,0BAA0B,CAAC,GACjD,CAAC,2BAA2B,EAAED,MAAM,YAAY,CAAC,AAAD,IACpD,4DACA,CAAC,kBAAkB,EAAEZ,WAAW;wBAClCqB,UAAU7F,MAAM8F,sBAAsB,CAACpD,6BAAAA,UAAWE,GAAG,EAClDiD,QAAQ;wBACXE,iBAAiB,MAAM5F,oBAAoBQ;wBAC3CqF,UAAU;wBACVC,SAAS;wBACT,uDAAuD;wBACvDC,SAAS;oBACX;oBACA,4DAA4D;oBAC5D,MAAM9F,eAAeO;oBACrBiE,OAAO;gBACT;gBACA,IAAI,CAACA,MAAM;gBACX,MAAM5B,SAAShB,GAAG,CAChB;oBACEmE,UAAUlG,cAAcmC,SAAS,CAACgE,UAAU,CAACC,UAAU,CAAC;wBACtDC,MAAMC,KAAKC,GAAG;wBACdC,OAAO;wBACPC,QACE,CAAC,KAAK,EAAEnF,MAAMmE,MAAM,CAAC,KAAK,EAAEnE,MAAMmE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC,CAAC,GAC5D,CAAC,YAAY,EAAEpB,SAAS3C,IAAI,EAAE;oBAClC;gBACF,GACA;oBAAEgF,OAAO;gBAAK;YAElB,EAAE,OAAOC,eAAe;gBACtB,+DAA+D;gBAC/DC,QAAQC,KAAK,CAAC,2BAA2BjG,YAAY+F;YACvD;QACF;IACF,EAAE,OAAOG,cAAc;QACrBF,QAAQC,KAAK,CAAC,2BAA2BC;IAC3C;AACF"}