@aglyn/plugins-commerce 1.0.0-beta.244 → 1.0.0-beta.247
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/README.md +4 -1
- package/package.json +13 -13
- package/src/lib/components/commerce-console-page.js +10 -0
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/local-delivery-settings-card.component.js +10 -13
- package/src/lib/components/console/local-delivery-settings-card.component.js.map +1 -1
- package/src/lib/components/console/locations-card.component.js +5 -9
- package/src/lib/components/console/locations-card.component.js.map +1 -1
- package/src/lib/components/console/product-zones.d.ts +7 -1
- package/src/lib/components/console/product-zones.js.map +1 -1
- package/src/lib/components/console/products-hub-zone.component.d.ts +3 -2
- package/src/lib/components/console/products-hub-zone.component.js +9 -1
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
- package/src/lib/components/console/review-settings-card.component.d.ts +14 -0
- package/src/lib/components/console/review-settings-card.component.js +168 -0
- package/src/lib/components/console/review-settings-card.component.js.map +1 -0
- package/src/lib/components/console/reviews-moderation-card.component.d.ts +14 -0
- package/src/lib/components/console/reviews-moderation-card.component.js +85 -11
- package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -1
- package/src/lib/components/product-reviews.d.ts +29 -1
- package/src/lib/components/product-reviews.js +97 -12
- package/src/lib/components/product-reviews.js.map +1 -1
- package/src/lib/model/commerce.d.ts +11 -2
- package/src/lib/model/commerce.js +14 -2
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/index.js +1 -0
- package/src/lib/model/index.js.map +1 -1
- package/src/lib/model/product-reviews.d.ts +124 -0
- package/src/lib/model/product-reviews.js +133 -0
- package/src/lib/model/product-reviews.js.map +1 -0
- package/src/lib/model/store-setup-content.d.ts +145 -0
- package/src/lib/model/store-setup-content.js +813 -0
- package/src/lib/model/store-setup-content.js.map +1 -0
- package/src/lib/server/membership-reset.js +8 -2
- package/src/lib/server/membership-reset.js.map +1 -1
- package/src/lib/server/order-event-triggers.d.ts +2 -0
- package/src/lib/server/order-event-triggers.js +18 -1
- package/src/lib/server/order-event-triggers.js.map +1 -1
- package/src/lib/server/review-requests.d.ts +102 -0
- package/src/lib/server/review-requests.js +346 -0
- package/src/lib/server/review-requests.js.map +1 -0
- package/src/lib/server/review-tokens.d.ts +82 -0
- package/src/lib/server/review-tokens.js +119 -0
- package/src/lib/server/review-tokens.js.map +1 -0
- package/src/lib/server/reviews.d.ts +36 -3
- package/src/lib/server/reviews.js +310 -52
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/store-setup-ai-capability.d.ts +61 -0
- package/src/lib/server/store-setup-ai-capability.js +343 -0
- package/src/lib/server/store-setup-ai-capability.js.map +1 -0
- package/src/lib/server/store-setup-ai-writer.console.d.ts +27 -0
- package/src/lib/server/store-setup-ai-writer.console.js +44 -0
- package/src/lib/server/store-setup-ai-writer.console.js.map +1 -0
- package/src/lib/server/store-setup-ai-writer.d.ts +37 -0
- package/src/lib/server/store-setup-ai-writer.js +174 -0
- package/src/lib/server/store-setup-ai-writer.js.map +1 -0
- package/src/lib/server.js +27 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/tenant-emails.js +58 -0
- package/src/lib/tenant-emails.js.map +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/store-setup-content.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 { invalidOpeningHoursLines } from '@aglyn/aglyn/app-utils/local-business'\nimport { COMMERCE_MAX_PRICE_USD } from './commerce'\nimport {\n summarizeShippingCoverage,\n type ShippingRate,\n type ShippingSettings,\n type ShippingTier,\n type ShippingZone,\n} from './commerce-shipping'\nimport type { TaxRate, TaxSettings } from './commerce-tax'\nimport { storefrontTaxMisconfiguration } from './commerce-tax-decision'\nimport {\n LOCAL_DELIVERY_POSTCODES_MAX,\n LOCAL_DELIVERY_ZONES_MAX,\n PICKUP_HOURS_MAX,\n PICKUP_INSTRUCTIONS_MAX,\n localDeliveryProblems,\n normalizeLocalDeliverySettings,\n normalizePickupSettings,\n type LocalDeliverySettings,\n type LocalDeliveryZone,\n} from './local-fulfillment-settings'\nimport type { PickupLocationSettings } from './order-local-fulfillment'\n\n/*==========================================\n * STORE SETUP AN AI BUILD CHANGES (AGL-3749)\n *\n * One change to the store's shipping, pickup, local delivery or tax settings,\n * as the `store-settings` writer receives it, and the pure step that turns\n * the settings a site has into the settings it has after the change.\n *\n * Every field is one the console's cards already save, in the shape they\n * save it:\n *\n * - `shipping`: a zone (`settings/store.shipping.zones`) and one rate in it\n * (`.rates`), priced flat, free over a subtotal, or by subtotal or weight\n * tiers — the Shipping card's four kinds. Carrier rates are not offered:\n * they quote through a carrier account a person connects.\n * - `pickup`: the store-wide \"Offer free local pickup\" switch\n * (`shipping.localPickup`), or pickup at one of the store's inventory\n * locations (`locations/{id}.pickup`), as the Inventory locations card\n * saves it. A location is found, never created.\n * - `local-delivery`: the Local delivery card's settings\n * (`settings/store.localDelivery`) with postal-code zones. Distance zones\n * need the store placed on a map first, which is the card's to do.\n * - `tax`: how the store handles sales tax (`settings/store.tax.mode`),\n * whether prices include it, the store origin and manual rates. The\n * lodging and service rates are kept as they are.\n *\n * Nothing here names a price or a rate the change did not carry: a blank\n * flat rate charges nothing at checkout, so a flat or free-over rate needs\n * its amount, and a tax rate needs its percentage. Pure.\n *=========================================*/\n\n/** The resource the writer is registered under; the commerce record route `store-settings` links it. */\nexport const STORE_SETUP_RESOURCE = 'store-settings'\n\nexport const STORE_SETUP_SECTIONS = ['shipping', 'pickup', 'local-delivery', 'tax'] as const\nexport type StoreSetupSection = (typeof STORE_SETUP_SECTIONS)[number]\n\n/** The rate kinds an AI change may set: every kind of the Shipping card except carrier rates. */\nexport const STORE_SHIPPING_AI_KINDS = ['flat', 'free_over', 'price_tiers', 'weight_tiers'] as const\nexport type StoreShippingAiKind = (typeof STORE_SHIPPING_AI_KINDS)[number]\n\nexport const STORE_SHIPPING_ACTIONS = ['set', 'remove'] as const\nexport const STORE_TAX_MODES = ['manual', 'stripe', 'none'] as const\n\nexport const STORE_SETUP_NAME_MAX = 80\nexport const STORE_SETUP_COUNTRIES_MAX = 60\nexport const STORE_SHIPPING_TIERS_MAX = 10\nexport const STORE_SETUP_LOCATION_MAX = 80\nexport const STORE_TAX_LABEL_MAX = 60\n/** Grams; the heaviest tier a weight table may name (1,000 kg). */\nexport const STORE_SHIPPING_MAX_GRAMS = 1_000_000\n/** Two weeks, the longest preparation or lead time the cards keep. */\nexport const STORE_SETUP_MAX_MINUTES = 14 * 24 * 60\n\nexport interface StoreShippingChange {\n section: 'shipping'\n action: 'set' | 'remove'\n zoneName: string\n countries?: string[]\n rateName?: string\n kind?: StoreShippingAiKind\n amountCents?: number\n freeOverCents?: number\n tiers?: ShippingTier[]\n}\n\nexport interface StorePickupChange {\n section: 'pickup'\n /** An inventory location's name, city or address; absent = the store-wide switch. */\n location?: string\n enabled: boolean\n hours?: string\n instructions?: string\n readyWithinMinutes?: number\n}\n\nexport interface StoreLocalDeliveryChange {\n section: 'local-delivery'\n enabled: boolean\n country?: string\n location?: string\n zoneName?: string\n postcodes?: string[]\n feeCents?: number\n minimumCents?: number\n freeOverCents?: number\n removeZone?: boolean\n windows?: string\n leadTimeMinutes?: number\n daysAhead?: number\n instructions?: string\n}\n\nexport interface StoreTaxChange {\n section: 'tax'\n mode?: 'manual' | 'stripe' | 'none'\n pricesIncludeTax?: boolean\n originCountry?: string\n originState?: string\n rateCountry?: string\n rateState?: string\n ratePct?: number\n rateLabel?: string\n removeRate?: boolean\n}\n\nexport type StoreSetupChange = StoreShippingChange | StorePickupChange | StoreLocalDeliveryChange | StoreTaxChange\n\nexport type StoreSetupRead = { ok: true; value: StoreSetupChange } | { ok: false; problems: string[] }\n\nconst COUNTRY = /^[A-Z]{2}$/\nconst STATE = /^[A-Z0-9]{1,3}$/\n\ntype Raw = Readonly<Record<string, unknown>>\n\nconst has = (raw: Raw, key: string) => raw[key] !== undefined && raw[key] !== null && raw[key] !== ''\n\nfunction text(raw: Raw, key: string, max: number, problems: string[]): string | undefined {\n if (!has(raw, key)) return undefined\n const value = raw[key]\n if (typeof value !== 'string') {\n problems.push(`\"${key}\" must be text`)\n return undefined\n }\n const trimmed = value.trim()\n if (trimmed.length > max) problems.push(`\"${key}\" must be at most ${max} characters`)\n return trimmed || undefined\n}\n\nfunction bool(raw: Raw, key: string, problems: string[]): boolean | undefined {\n if (!has(raw, key)) return undefined\n if (typeof raw[key] !== 'boolean') {\n problems.push(`\"${key}\" must be true or false`)\n return undefined\n }\n return raw[key] as boolean\n}\n\nfunction number(raw: Raw, key: string, min: number, max: number, problems: string[]): number | undefined {\n if (!has(raw, key)) return undefined\n const value = raw[key]\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n problems.push(`\"${key}\" must be a number`)\n return undefined\n }\n if (value < min || value > max) {\n problems.push(`\"${key}\" must be between ${min} and ${max}`)\n return undefined\n }\n return value\n}\n\nfunction minutes(raw: Raw, key: string, problems: string[]): number | undefined {\n const value = number(raw, key, 0, STORE_SETUP_MAX_MINUTES, problems)\n return value === undefined ? undefined : Math.round(value)\n}\n\n/** Dollars to the cents the settings store. */\nfunction cents(raw: Raw, key: string, problems: string[]): number | undefined {\n const value = number(raw, key, 0, COMMERCE_MAX_PRICE_USD, problems)\n return value === undefined ? undefined : Math.round(value * 100)\n}\n\nfunction list(raw: Raw, key: string, max: number, problems: string[]): string[] | undefined {\n if (!has(raw, key)) return undefined\n const value = raw[key]\n if (!Array.isArray(value) || value.some((one) => typeof one !== 'string')) {\n problems.push(`\"${key}\" must be a list of text`)\n return undefined\n }\n const items = (value as string[]).map((one) => one.trim()).filter(Boolean)\n if (items.length > max) problems.push(`\"${key}\" must hold at most ${max}`)\n return items\n}\n\nfunction countryCode(raw: Raw, key: string, problems: string[]): string | undefined {\n const value = text(raw, key, 2, problems)?.toUpperCase()\n if (value !== undefined && !COUNTRY.test(value)) problems.push(`\"${key}\" must be a two-letter country code like US`)\n return value\n}\n\nfunction stateCode(raw: Raw, key: string, problems: string[]): string | undefined {\n const value = text(raw, key, 3, problems)?.toUpperCase()\n if (value !== undefined && !STATE.test(value)) problems.push(`\"${key}\" must be a state or region code like TX`)\n return value\n}\n\n/** Opening-hours lines; a `;` separates lines as a newline does. */\nfunction hoursText(raw: Raw, key: string, problems: string[]): string | undefined {\n const value = text(raw, key, PICKUP_HOURS_MAX, problems)\n if (value === undefined) return undefined\n const lines = value\n .split(/\\s*(?:;|\\r?\\n)\\s*/)\n .filter(Boolean)\n .join('\\n')\n const bad = invalidOpeningHoursLines(lines)\n if (bad.length) {\n problems.push(`\"${key}\" must be lines like \"Mo-Fr 09:00-17:00\" or \"Sa 10:00-14:00\"`)\n }\n return lines\n}\n\n/** \"50:5\" → up to 50 (dollars, or grams for weight) costs $5. */\nfunction tiersOf(raw: Raw, kind: StoreShippingAiKind | undefined, problems: string[]): ShippingTier[] | undefined {\n const entries = list(raw, 'tiers', STORE_SHIPPING_TIERS_MAX, problems)\n if (entries === undefined) return undefined\n const weight = kind === 'weight_tiers'\n const tiers: ShippingTier[] = []\n for (const entry of entries) {\n const match = /^\\s*\\$?([0-9]+(?:\\.[0-9]+)?)\\s*(?:g)?\\s*:\\s*\\$?([0-9]+(?:\\.[0-9]+)?)\\s*$/.exec(entry)\n const upTo = match ? Number(match[1]) : NaN\n const amount = match ? Number(match[2]) : NaN\n if (!match || !(upTo > 0) || amount > COMMERCE_MAX_PRICE_USD || (weight && upTo > STORE_SHIPPING_MAX_GRAMS)) {\n problems.push(`\"tiers\" holds \"${entry}\", which is not \"up to:price\" like \"50:5\"`)\n return undefined\n }\n tiers.push({ upTo: weight ? Math.round(upTo) : Math.round(upTo * 100), amountCents: Math.round(amount * 100) })\n }\n tiers.sort((a, b) => a.upTo - b.upTo)\n if (tiers.some((tier, index) => index > 0 && tier.upTo === tiers[index - 1].upTo)) {\n problems.push('\"tiers\" names the same limit twice')\n }\n return tiers\n}\n\nconst KNOWN: Record<StoreSetupSection, readonly string[]> = {\n shipping: ['action', 'zoneName', 'countries', 'rateName', 'kind', 'amountUsd', 'freeOverUsd', 'tiers'],\n pickup: ['location', 'enabled', 'hours', 'instructions', 'readyWithinMinutes'],\n 'local-delivery': [\n 'enabled',\n 'country',\n 'location',\n 'zoneName',\n 'postcodes',\n 'feeUsd',\n 'minimumUsd',\n 'freeOverUsd',\n 'removeZone',\n 'windows',\n 'leadTimeMinutes',\n 'daysAhead',\n 'instructions',\n ],\n tax: [\n 'mode',\n 'pricesIncludeTax',\n 'originCountry',\n 'originState',\n 'rateCountry',\n 'rateState',\n 'ratePercent',\n 'rateLabel',\n 'removeRate',\n ],\n}\n\nfunction readShipping(raw: Raw, problems: string[]): StoreShippingChange {\n const action = has(raw, 'action') ? raw['action'] : 'set'\n if (!STORE_SHIPPING_ACTIONS.includes(action as never)) problems.push('\"action\" must be set or remove')\n const zoneName = text(raw, 'zoneName', STORE_SETUP_NAME_MAX, problems)\n if (!zoneName) problems.push('\"zoneName\" is required: the shipping zone, like \"United States\"')\n const countries = list(raw, 'countries', STORE_SETUP_COUNTRIES_MAX, problems)?.map((code) => code.toUpperCase())\n for (const code of countries ?? []) {\n if (code !== '*' && !COUNTRY.test(code)) {\n problems.push(`\"countries\" holds \"${code}\", which is not a two-letter country code or *`)\n break\n }\n }\n const rateName = text(raw, 'rateName', STORE_SETUP_NAME_MAX, problems)\n const kind = has(raw, 'kind') ? (raw['kind'] as StoreShippingAiKind) : undefined\n if (kind !== undefined && !STORE_SHIPPING_AI_KINDS.includes(kind)) {\n problems.push(`\"kind\" must be one of ${STORE_SHIPPING_AI_KINDS.join(', ')}`)\n }\n const amountCents = cents(raw, 'amountUsd', problems)\n const freeOverCents = cents(raw, 'freeOverUsd', problems)\n const tiers = tiersOf(raw, kind, problems)\n if (action === 'set') {\n // A rate with no name is dropped at checkout (AGL-1791), and a blank\n // amount charges nothing: both are the person's to state.\n if (!rateName) problems.push('\"rateName\" is required: what the shopper sees, like \"Standard\"')\n if (!kind) problems.push('\"kind\" is required')\n if ((kind === 'flat' || kind === 'free_over') && amountCents === undefined) {\n problems.push(`\"amountUsd\" is required for a ${kind === 'flat' ? 'flat' : 'free-over'} rate`)\n }\n if (kind === 'free_over' && !freeOverCents) problems.push('\"freeOverUsd\" is required for a free-over rate')\n if ((kind === 'price_tiers' || kind === 'weight_tiers') && !tiers?.length) {\n problems.push('\"tiers\" is required for a tiered rate')\n }\n }\n return {\n section: 'shipping',\n action: action as StoreShippingChange['action'],\n zoneName: zoneName ?? '',\n ...(countries ? { countries } : {}),\n ...(rateName ? { rateName } : {}),\n ...(kind ? { kind } : {}),\n ...(amountCents !== undefined ? { amountCents } : {}),\n ...(freeOverCents !== undefined ? { freeOverCents } : {}),\n ...(tiers ? { tiers } : {}),\n }\n}\n\nfunction readPickup(raw: Raw, problems: string[]): StorePickupChange {\n const location = text(raw, 'location', STORE_SETUP_LOCATION_MAX, problems)\n const hours = hoursText(raw, 'hours', problems)\n const instructions = text(raw, 'instructions', PICKUP_INSTRUCTIONS_MAX, problems)\n const readyWithinMinutes = minutes(raw, 'readyWithinMinutes', problems)\n if (!location && (hours !== undefined || instructions !== undefined || readyWithinMinutes !== undefined)) {\n problems.push('Pickup hours, instructions and preparation time belong to a location: name the inventory location')\n }\n return {\n section: 'pickup',\n enabled: bool(raw, 'enabled', problems) ?? true,\n ...(location ? { location } : {}),\n ...(hours !== undefined ? { hours } : {}),\n ...(instructions !== undefined ? { instructions } : {}),\n ...(readyWithinMinutes !== undefined ? { readyWithinMinutes } : {}),\n }\n}\n\nfunction readLocalDelivery(raw: Raw, problems: string[]): StoreLocalDeliveryChange {\n const zoneName = text(raw, 'zoneName', STORE_SETUP_NAME_MAX, problems)\n const postcodes = list(raw, 'postcodes', LOCAL_DELIVERY_POSTCODES_MAX, problems)\n const feeCents = cents(raw, 'feeUsd', problems)\n const minimumCents = cents(raw, 'minimumUsd', problems)\n const freeOverCents = cents(raw, 'freeOverUsd', problems)\n const removeZone = bool(raw, 'removeZone', problems)\n if (!zoneName && (postcodes || feeCents !== undefined || minimumCents !== undefined || freeOverCents !== undefined || removeZone)) {\n problems.push('\"zoneName\" is required to add, change or remove a delivery zone')\n }\n const daysAhead = number(raw, 'daysAhead', 1, 30, problems)\n return {\n section: 'local-delivery',\n enabled: bool(raw, 'enabled', problems) ?? true,\n ...(has(raw, 'country') ? { country: countryCode(raw, 'country', problems) } : {}),\n ...(has(raw, 'location') ? { location: text(raw, 'location', STORE_SETUP_LOCATION_MAX, problems) } : {}),\n ...(zoneName ? { zoneName } : {}),\n ...(postcodes ? { postcodes } : {}),\n ...(feeCents !== undefined ? { feeCents } : {}),\n ...(minimumCents !== undefined ? { minimumCents } : {}),\n ...(freeOverCents !== undefined ? { freeOverCents } : {}),\n ...(removeZone ? { removeZone } : {}),\n ...(has(raw, 'windows') ? { windows: hoursText(raw, 'windows', problems) } : {}),\n ...(has(raw, 'leadTimeMinutes') ? { leadTimeMinutes: minutes(raw, 'leadTimeMinutes', problems) } : {}),\n ...(daysAhead !== undefined ? { daysAhead: Math.round(daysAhead) } : {}),\n ...(has(raw, 'instructions') ? { instructions: text(raw, 'instructions', PICKUP_INSTRUCTIONS_MAX, problems) } : {}),\n }\n}\n\nfunction readTax(raw: Raw, problems: string[]): StoreTaxChange {\n const mode = has(raw, 'mode') ? raw['mode'] : undefined\n if (mode !== undefined && !STORE_TAX_MODES.includes(mode as never)) {\n problems.push(`\"mode\" must be one of ${STORE_TAX_MODES.join(', ')}`)\n }\n const change: StoreTaxChange = {\n section: 'tax',\n ...(mode !== undefined ? { mode: mode as StoreTaxChange['mode'] } : {}),\n }\n const pricesIncludeTax = bool(raw, 'pricesIncludeTax', problems)\n if (pricesIncludeTax !== undefined) change.pricesIncludeTax = pricesIncludeTax\n const originCountry = countryCode(raw, 'originCountry', problems)\n if (originCountry) change.originCountry = originCountry\n const originState = stateCode(raw, 'originState', problems)\n if (originState) change.originState = originState\n const rateCountry = countryCode(raw, 'rateCountry', problems)\n if (rateCountry) change.rateCountry = rateCountry\n const rateState = stateCode(raw, 'rateState', problems)\n if (rateState) change.rateState = rateState\n if (has(raw, 'ratePercent')) {\n const value = raw['ratePercent']\n if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 100) {\n problems.push('\"ratePercent\" must be a percentage above 0 and at most 100')\n } else change.ratePct = value\n }\n const rateLabel = text(raw, 'rateLabel', STORE_TAX_LABEL_MAX, problems)\n if (rateLabel) change.rateLabel = rateLabel\n const removeRate = bool(raw, 'removeRate', problems)\n if (removeRate) change.removeRate = true\n\n const touchesRate = change.ratePct !== undefined || change.removeRate || change.rateState || change.rateLabel\n if (touchesRate && !change.rateCountry) problems.push('\"rateCountry\" is required with a tax rate')\n if (change.rateCountry && change.ratePct === undefined && !change.removeRate) {\n // Never a rate the person did not state.\n problems.push('\"ratePercent\" is required with a tax rate: the percentage the request states')\n }\n if (change.ratePct !== undefined && change.removeRate) problems.push('Give \"ratePercent\" or \"removeRate\", not both')\n if (originState && !originCountry) problems.push('\"originCountry\" is required with \"originState\"')\n const said = Object.keys(change).filter((key) => key !== 'section')\n if (!said.length) {\n problems.push(\n 'Say how the store handles sales tax: manual rates (with the percentage), automatic calculation, or not collecting it',\n )\n }\n return change\n}\n\n/** Reads a change from content; the problems name what is wrong, in words a person reads. */\nexport function readStoreSetupChange(content: Raw): StoreSetupRead {\n const section = content['section'] as StoreSetupSection\n if (!STORE_SETUP_SECTIONS.includes(section)) {\n return { ok: false, problems: [`\"section\" must be one of ${STORE_SETUP_SECTIONS.join(', ')}`] }\n }\n const problems: string[] = []\n for (const key of Object.keys(content)) {\n if (key !== 'section' && !KNOWN[section].includes(key)) problems.push(`\"${key}\" is not a ${section} setting`)\n }\n const value =\n section === 'shipping'\n ? readShipping(content, problems)\n : section === 'pickup'\n ? readPickup(content, problems)\n : section === 'local-delivery'\n ? readLocalDelivery(content, problems)\n : readTax(content, problems)\n return problems.length ? { ok: false, problems } : { ok: true, value }\n}\n\n// ---------------------------------------------------------------------------\n// Applying a change\n// ---------------------------------------------------------------------------\n\n/** An inventory location as the writer reads it. */\nexport interface StoreSetupLocation {\n id: string\n name?: string\n address?: string\n city?: string\n pickup?: unknown\n}\n\n/** What the change writes: one map of the settings document, or one location's pickup. */\nexport type StoreSetupWrite =\n | { target: 'settings'; field: 'shipping' | 'localDelivery' | 'tax'; value: Record<string, unknown> }\n | { target: 'location'; locationId: string; value: PickupLocationSettings }\n\nexport type StoreSetupApplied =\n | {\n ok: true\n /** `null` when the settings already say what the change asks: nothing to write. */\n write: StoreSetupWrite | null\n /** One sentence on what the store now does. */\n summary: string\n /** What the person should know, in the console's words. */\n warnings: string[]\n }\n | { ok: false; status: 400 | 404 | 409; error: string }\n\nexport interface StoreSetupState {\n /** `hosts/{hostId}/settings/store`, or `{}`. */\n settings: Readonly<Record<string, unknown>>\n locations: readonly StoreSetupLocation[]\n}\n\n/** Ids for what a change creates, derived from the request so a retry makes the same ones. */\nexport interface StoreSetupIds {\n zoneId: string\n rateId: string\n}\n\nconst fail = (status: 400 | 404 | 409, error: string): StoreSetupApplied => ({ ok: false, status, error })\n\n/** Key-order-blind equality over plain JSON values. */\nexport function sameSetting(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (Array.isArray(a) || Array.isArray(b)) {\n return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((one, index) => sameSetting(one, b[index]))\n }\n if (a && b && typeof a === 'object' && typeof b === 'object') {\n const left = Object.entries(a as Record<string, unknown>).filter(([, value]) => value !== undefined)\n const right = Object.entries(b as Record<string, unknown>).filter(([, value]) => value !== undefined)\n return left.length === right.length && left.every(([key, value]) => sameSetting(value, (b as Record<string, unknown>)[key]))\n }\n return false\n}\n\nconst lower = (value: unknown) => String(value ?? '').trim().toLowerCase()\nconst usd = (centsValue: number) => `$${(centsValue / 100).toFixed(centsValue % 100 ? 2 : 0)}`\nconst sameSet = (a: readonly string[], b: readonly string[]) =>\n a.length === b.length && [...a].sort().join(',') === [...b].sort().join(',')\n\n/** The store's locations a phrase names: by exact name first, then by city or address. */\nexport function matchStoreLocations(\n locations: readonly StoreSetupLocation[],\n phrase: string,\n): StoreSetupLocation[] {\n const wanted = lower(phrase)\n const byName = locations.filter((one) => lower(one.name) === wanted)\n if (byName.length) return byName\n return locations.filter(\n (one) => lower(one.city) === wanted || (wanted.length >= 3 && lower(one.address).includes(wanted)) || lower(one.name).includes(wanted),\n )\n}\n\nfunction findLocation(\n locations: readonly StoreSetupLocation[],\n phrase: string,\n): StoreSetupLocation | StoreSetupApplied {\n const found = matchStoreLocations(locations, phrase)\n if (!found.length) {\n return fail(\n 404,\n `The store has no inventory location matching \"${phrase}\". Add it under Products → Inventory locations, then ask again.`,\n )\n }\n if (found.length > 1) {\n return fail(409, `More than one location matches \"${phrase}\" (${found.map((one) => one.name || one.id).join(', ')}); say which`)\n }\n return found[0]\n}\n\nfunction rateSummary(rate: ShippingRate): string {\n switch (rate.kind) {\n case 'flat':\n return rate.amountCents ? `${usd(rate.amountCents)} flat` : 'free'\n case 'free_over':\n return `free over ${usd(rate.freeOverCents ?? 0)}, otherwise ${usd(rate.amountCents ?? 0)}`\n case 'price_tiers':\n return `${(rate.tiers ?? []).length} subtotal tiers`\n case 'weight_tiers':\n return `${(rate.tiers ?? []).length} weight tiers`\n default:\n return rate.kind\n }\n}\n\nfunction applyShipping(state: StoreSetupState, change: StoreShippingChange, ids: StoreSetupIds): StoreSetupApplied {\n const current = (state.settings['shipping'] ?? {}) as ShippingSettings\n const zones: ShippingZone[] = [...(current.zones ?? [])]\n const rates: ShippingRate[] = [...(current.rates ?? [])]\n const zoneIndex = (() => {\n const byName = zones.findIndex((zone) => lower(zone.name) === lower(change.zoneName))\n if (byName >= 0 || !change.countries?.length) return byName\n return zones.findIndex((zone) => sameSet((zone.countries ?? []).map((code) => String(code).trim().toUpperCase()), change.countries ?? []))\n })()\n const zone = zoneIndex >= 0 ? zones[zoneIndex] : undefined\n\n if (change.action === 'remove') {\n if (!zone) return { ok: true, write: null, summary: `There is no \"${change.zoneName}\" shipping zone.`, warnings: [] }\n if (change.rateName) {\n const kept = rates.filter((rate) => !(rate.zoneId === zone.id && lower(rate.name) === lower(change.rateName)))\n if (kept.length === rates.length) {\n return { ok: true, write: null, summary: `\"${zone.name}\" has no \"${change.rateName}\" rate.`, warnings: [] }\n }\n return shippingResult(current, { ...current, zones, rates: kept }, `Removed the \"${change.rateName}\" rate from ${zone.name}.`)\n }\n // The card's ✕ on a zone: the zone and its rates go together.\n return shippingResult(\n current,\n { ...current, zones: zones.filter((one) => one.id !== zone.id), rates: rates.filter((rate) => rate.zoneId !== zone.id) },\n `Removed the ${zone.name} shipping zone and its rates.`,\n )\n }\n\n let zoneId: string\n if (zone) {\n zoneId = zone.id\n if (change.countries?.length && !sameSet(zone.countries ?? [], change.countries)) {\n zones[zoneIndex] = { ...zone, countries: [...change.countries] }\n }\n } else {\n if (!change.countries?.length) {\n return fail(400, `Say which countries the new \"${change.zoneName}\" zone ships to (two-letter codes, or * for everywhere else)`)\n }\n zoneId = ids.zoneId\n zones.push({ id: zoneId, name: change.zoneName, countries: [...change.countries] })\n }\n const rateIndex = rates.findIndex((rate) => rate.zoneId === zoneId && lower(rate.name) === lower(change.rateName))\n const existing = rateIndex >= 0 ? rates[rateIndex] : undefined\n if (existing?.kind === 'carrier') {\n return fail(409, `\"${existing.name}\" is a carrier rate; change it in Shipping settings`)\n }\n const kind = change.kind as StoreShippingAiKind\n const rate: ShippingRate = {\n id: existing?.id ?? ids.rateId,\n zoneId,\n name: change.rateName as string,\n kind,\n ...(kind === 'flat' || kind === 'free_over' ? { amountCents: change.amountCents } : {}),\n ...(kind === 'free_over' ? { freeOverCents: change.freeOverCents } : {}),\n ...(kind === 'price_tiers' || kind === 'weight_tiers' ? { tiers: change.tiers } : {}),\n }\n if (rateIndex >= 0) rates[rateIndex] = rate\n else rates.push(rate)\n const zoneName = zone?.name ?? change.zoneName\n return shippingResult(current, { ...current, zones, rates }, `${zoneName}: \"${rate.name}\" is ${rateSummary(rate)}.`, rate)\n}\n\nfunction shippingResult(\n before: ShippingSettings,\n after: ShippingSettings,\n summary: string,\n changed?: ShippingRate,\n): StoreSetupApplied {\n const coverage = summarizeShippingCoverage(after)\n const warnings: string[] = []\n if (coverage.unserved.length && !coverage.shipsNowhere) {\n warnings.push(`Checkout turns away buyers in ${coverage.unserved.join(', ')}: no rate reaches them.`)\n }\n if (changed?.kind === 'price_tiers' || changed?.kind === 'weight_tiers') {\n warnings.push(`\"${changed.name}\" applies only up to its last tier; a larger or heavier order gets no rate from it.`)\n }\n return {\n ok: true,\n write: sameSetting(before, after) ? null : { target: 'settings', field: 'shipping', value: after as Record<string, unknown> },\n summary,\n warnings,\n }\n}\n\nfunction applyPickup(state: StoreSetupState, change: StorePickupChange): StoreSetupApplied {\n if (!change.location) {\n const current = (state.settings['shipping'] ?? {}) as ShippingSettings\n const after: ShippingSettings = { ...current, localPickup: change.enabled }\n return {\n ok: true,\n write: Boolean(current.localPickup) === change.enabled ? null : { target: 'settings', field: 'shipping', value: after as Record<string, unknown> },\n summary: change.enabled ? 'Checkout offers free local pickup.' : 'Checkout no longer offers free local pickup.',\n warnings: change.enabled ? ['Pickup is a collection choice; it does not widen where the store ships.'] : [],\n }\n }\n const found = findLocation(state.locations, change.location)\n if ('ok' in found) return found\n const before = normalizePickupSettings(found.pickup)\n const after = normalizePickupSettings({\n ...before,\n enabled: change.enabled,\n ...(change.hours !== undefined ? { hours: change.hours } : {}),\n ...(change.instructions !== undefined ? { instructions: change.instructions } : {}),\n ...(change.readyWithinMinutes !== undefined ? { readyWithinMinutes: change.readyWithinMinutes } : {}),\n })\n const name = found.name || 'the location'\n return {\n ok: true,\n write: sameSetting(before, after) ? null : { target: 'location', locationId: found.id, value: after },\n summary: after.enabled ? `Buyers can pick up orders at ${name}.` : `Pickup at ${name} is off.`,\n warnings: after.enabled && !after.hours ? [`${name} has no pickup hours yet; buyers are not told when to come.`] : [],\n }\n}\n\nfunction applyLocalDelivery(state: StoreSetupState, change: StoreLocalDeliveryChange, ids: StoreSetupIds): StoreSetupApplied {\n const before = normalizeLocalDeliverySettings(state.settings['localDelivery'])\n const after: LocalDeliverySettings = { ...before, zones: [...(before.zones ?? [])] }\n if (!after.zones?.length) delete after.zones\n if (change.enabled) after.enabled = true\n else delete after.enabled\n if (change.country) after.country = change.country\n if (change.location) {\n const found = findLocation(state.locations, change.location)\n if ('ok' in found) return found\n after.locationId = found.id\n }\n if (change.windows !== undefined) after.windows = change.windows\n if (change.leadTimeMinutes !== undefined) after.leadTimeMinutes = change.leadTimeMinutes\n if (change.daysAhead !== undefined) after.daysAhead = change.daysAhead\n if (change.instructions !== undefined) after.instructions = change.instructions\n if (change.zoneName) {\n const zones: LocalDeliveryZone[] = [...(after.zones ?? [])]\n const index = zones.findIndex((zone) => lower(zone.name) === lower(change.zoneName))\n if (change.removeZone) {\n if (index >= 0) zones.splice(index, 1)\n } else if (index >= 0) {\n const zone = zones[index]\n if (change.postcodes && zone.kind === 'radius') {\n return fail(409, `\"${zone.name}\" is a distance zone; change it under Local delivery`)\n }\n zones[index] = {\n ...zone,\n ...(change.postcodes ? { postcodes: change.postcodes } : {}),\n ...(change.feeCents !== undefined ? { feeCents: change.feeCents } : {}),\n ...(change.minimumCents !== undefined ? { minimumCents: change.minimumCents } : {}),\n ...(change.freeOverCents !== undefined ? { freeOverCents: change.freeOverCents } : {}),\n }\n } else {\n if (!change.postcodes?.length) return fail(400, `Say which postal codes the \"${change.zoneName}\" zone delivers to`)\n if (change.feeCents === undefined) return fail(400, `Say what delivery to \"${change.zoneName}\" costs (0 for free)`)\n if (zones.length >= LOCAL_DELIVERY_ZONES_MAX) return fail(409, `Local delivery holds at most ${LOCAL_DELIVERY_ZONES_MAX} zones`)\n zones.push({\n id: ids.zoneId,\n name: change.zoneName,\n kind: 'postcode',\n postcodes: change.postcodes,\n feeCents: change.feeCents,\n ...(change.minimumCents !== undefined ? { minimumCents: change.minimumCents } : {}),\n ...(change.freeOverCents !== undefined ? { freeOverCents: change.freeOverCents } : {}),\n })\n }\n after.zones = zones\n }\n const normalized = normalizeLocalDeliverySettings(after)\n const problems = localDeliveryProblems(normalized)\n if (problems.length) {\n return fail(400, `Local delivery can't be switched on yet: ${problems.join(' ')}`)\n }\n return {\n ok: true,\n write: sameSetting(before, normalized)\n ? null\n : { target: 'settings', field: 'localDelivery', value: normalized as Record<string, unknown> },\n summary: normalized.enabled\n ? `The store delivers locally to ${(normalized.zones ?? []).length} zone${(normalized.zones ?? []).length === 1 ? '' : 's'}.`\n : 'Local delivery is off.',\n warnings: normalized.enabled && !normalized.locationId ? ['No location is set for delivery orders to leave from.'] : [],\n }\n}\n\nconst TAX_MODE_WORDS: Record<string, string> = {\n manual: 'manual rates',\n stripe: 'automatic calculation',\n none: 'not collecting sales tax',\n}\n\nfunction applyTax(state: StoreSetupState, change: StoreTaxChange): StoreSetupApplied {\n const before = (state.settings['tax'] ?? {}) as TaxSettings\n const after: TaxSettings = { ...before }\n const settingRate = change.ratePct !== undefined\n if (change.mode) after.mode = change.mode\n else if (settingRate && after.mode === undefined) after.mode = 'manual'\n if (settingRate && after.mode !== 'manual') {\n return fail(\n 409,\n `This store uses ${TAX_MODE_WORDS[String(after.mode)] ?? 'another tax mode'}, so a manual rate would not apply. Ask to switch to manual rates as well.`,\n )\n }\n if (change.pricesIncludeTax !== undefined) after.pricesIncludeTax = change.pricesIncludeTax\n if (change.originCountry) {\n after.origin = {\n country: change.originCountry,\n ...(change.originState ? { state: change.originState } : {}),\n }\n }\n if (change.rateCountry) {\n const rates: TaxRate[] = [...(before.rates ?? [])]\n const index = rates.findIndex(\n (rate) =>\n String(rate.country ?? '').toUpperCase() === change.rateCountry &&\n String(rate.state ?? '').toUpperCase() === (change.rateState ?? ''),\n )\n if (change.removeRate) {\n if (index >= 0) rates.splice(index, 1)\n } else {\n const rate: TaxRate = {\n country: change.rateCountry,\n ...(change.rateState ? { state: change.rateState } : {}),\n pct: change.ratePct as number,\n ...(change.rateLabel ? { label: change.rateLabel } : index >= 0 && rates[index].label ? { label: rates[index].label } : {}),\n }\n if (index >= 0) rates[index] = rate\n else rates.push(rate)\n }\n after.rates = rates\n }\n // Manual mode with no origin refuses every sale (AGL-2145): never leave a store there.\n if (storefrontTaxMisconfiguration(after)) {\n return fail(400, 'Manual tax rates need the store’s address: say which country (and state) the store sells from.')\n }\n const warnings: string[] = []\n if (after.mode === 'manual' && !(after.rates ?? []).length) {\n warnings.push('No tax rate is set yet, so orders are charged no sales tax.')\n }\n if (after.mode === 'stripe') {\n warnings.push('Automatic tax is calculated per buyer location at checkout; the register cannot calculate it for in-person sales.')\n }\n const mode = TAX_MODE_WORDS[String(after.mode)] ?? 'undecided'\n const rateLine = settingRate\n ? ` ${change.ratePct}% for ${change.rateCountry}${change.rateState ? `-${change.rateState}` : ''}.`\n : ''\n return {\n ok: true,\n write: sameSetting(before, after) ? null : { target: 'settings', field: 'tax', value: after as Record<string, unknown> },\n summary: `Sales tax: ${mode}.${rateLine}`,\n warnings,\n }\n}\n\n/** The settings after a change, what to write, and what the person is told. */\nexport function applyStoreSetupChange(\n state: StoreSetupState,\n change: StoreSetupChange,\n ids: StoreSetupIds,\n): StoreSetupApplied {\n switch (change.section) {\n case 'shipping':\n return applyShipping(state, change, ids)\n case 'pickup':\n return applyPickup(state, change)\n case 'local-delivery':\n return applyLocalDelivery(state, change, ids)\n case 'tax':\n return applyTax(state, change)\n }\n}\n\n/** What a reader is told about a store's setup, for the writer's `read`. */\nexport function storeSetupFacts(settings: Readonly<Record<string, unknown>>): Record<string, unknown> {\n const shipping = (settings['shipping'] ?? {}) as ShippingSettings\n const tax = (settings['tax'] ?? {}) as TaxSettings\n const delivery = normalizeLocalDeliverySettings(settings['localDelivery'])\n return {\n shippingZones: (shipping.zones ?? []).length,\n shippingRates: (shipping.rates ?? []).length,\n localPickup: Boolean(shipping.localPickup),\n localDelivery: Boolean(delivery.enabled),\n taxMode: tax.mode ?? null,\n taxRates: (tax.rates ?? []).length,\n live: true,\n }\n}\n"],"names":["invalidOpeningHoursLines","COMMERCE_MAX_PRICE_USD","summarizeShippingCoverage","storefrontTaxMisconfiguration","LOCAL_DELIVERY_POSTCODES_MAX","LOCAL_DELIVERY_ZONES_MAX","PICKUP_HOURS_MAX","PICKUP_INSTRUCTIONS_MAX","localDeliveryProblems","normalizeLocalDeliverySettings","normalizePickupSettings","STORE_SETUP_RESOURCE","STORE_SETUP_SECTIONS","STORE_SHIPPING_AI_KINDS","STORE_SHIPPING_ACTIONS","STORE_TAX_MODES","STORE_SETUP_NAME_MAX","STORE_SETUP_COUNTRIES_MAX","STORE_SHIPPING_TIERS_MAX","STORE_SETUP_LOCATION_MAX","STORE_TAX_LABEL_MAX","STORE_SHIPPING_MAX_GRAMS","STORE_SETUP_MAX_MINUTES","COUNTRY","STATE","has","raw","key","undefined","text","max","problems","value","push","trimmed","trim","length","bool","number","min","Number","isFinite","minutes","Math","round","cents","list","Array","isArray","some","one","items","map","filter","Boolean","countryCode","toUpperCase","test","stateCode","hoursText","lines","split","join","bad","tiersOf","kind","entries","weight","tiers","entry","match","exec","upTo","NaN","amount","amountCents","sort","a","b","tier","index","KNOWN","shipping","pickup","tax","readShipping","action","includes","zoneName","countries","code","rateName","freeOverCents","section","readPickup","location","hours","instructions","readyWithinMinutes","enabled","readLocalDelivery","postcodes","feeCents","minimumCents","removeZone","daysAhead","country","windows","leadTimeMinutes","readTax","mode","change","pricesIncludeTax","originCountry","originState","rateCountry","rateState","ratePct","rateLabel","removeRate","touchesRate","said","Object","keys","readStoreSetupChange","content","ok","fail","status","error","sameSetting","every","left","right","lower","String","toLowerCase","usd","centsValue","toFixed","sameSet","matchStoreLocations","locations","phrase","wanted","byName","name","city","address","findLocation","found","id","rateSummary","rate","applyShipping","state","ids","current","settings","zones","rates","zoneIndex","findIndex","zone","write","summary","warnings","kept","zoneId","shippingResult","rateIndex","existing","rateId","before","after","changed","coverage","unserved","shipsNowhere","target","field","applyPickup","localPickup","locationId","applyLocalDelivery","normalized","splice","TAX_MODE_WORDS","manual","stripe","none","applyTax","settingRate","origin","pct","label","rateLine","applyStoreSetupChange","storeSetupFacts","delivery","shippingZones","shippingRates","localDelivery","taxMode","taxRates","live"],"mappings":"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"}
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/ import {
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
17
18
|
import { MEMBER_SUSPENDED_ERROR, hashMemberPassword, passwordResetTokenMemberId, verifyPasswordResetToken } from "./membership.js";
|
|
18
19
|
import { isMemberDocumentId, memberCredentialRef, retiredCredentialFields, storedPasswordHash } from "./member-credentials.js";
|
|
19
20
|
/**
|
|
@@ -84,7 +85,12 @@ import { isMemberDocumentId, memberCredentialRef, retiredCredentialFields, store
|
|
|
84
85
|
}, {
|
|
85
86
|
merge: true
|
|
86
87
|
});
|
|
87
|
-
tx.update(memberRef, retiredCredentialFields()
|
|
88
|
+
tx.update(memberRef, _extends({}, retiredCredentialFields(), {
|
|
89
|
+
// The link was mailed to the member's address, so following it
|
|
90
|
+
// proves they own it (AGL-3736): what lets a signed-in member's
|
|
91
|
+
// purchases verify their product reviews.
|
|
92
|
+
emailVerifiedAtMs: Date.now()
|
|
93
|
+
}));
|
|
88
94
|
return 'reset';
|
|
89
95
|
});
|
|
90
96
|
if (outcome === 'invalid') return invalid();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/membership-reset.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n MEMBER_SUSPENDED_ERROR,\n hashMemberPassword,\n passwordResetTokenMemberId,\n verifyPasswordResetToken,\n} from './membership'\nimport {\n isMemberDocumentId,\n memberCredentialRef,\n retiredCredentialFields,\n storedPasswordHash,\n} from './member-credentials'\n\n/**\n * Password reset completion (AGL-552): verifies the recover token\n * (signature + expiry + binding to the member's CURRENT password hash —\n * so a completed reset invalidates every outstanding token) and writes the\n * new scrypt hash to the member's credential document (AGL-3308). No session\n * is minted here; the client signs in through the normal login route with the\n * new password. Token guessing is a non-issue (256-bit HMAC), so no extra\n * damper beyond login's.\n *\n * ONE TRANSACTION, from the token check to the write. The hash the token is\n * checked against is the hash the write replaces, so two submissions of one\n * link cannot both land — the second re-reads the new hash and fails the\n * binding — and a member removed mid-request is not recreated as a stub.\n */\nexport const membershipResetHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const token = String(req.body?.token ?? '')\n const password = String(req.body?.password ?? '')\n if (!hostId || !token) {\n return res.status(400).json({ error: 'Invalid request' })\n }\n if (password.length < 8) {\n return res\n .status(400)\n .json({ error: 'Password must be at least 8 characters' })\n }\n const invalid = () =>\n res.status(400).json({\n error: 'That reset link is invalid or has expired — request a new one.',\n })\n // Structural pre-check only (host + expiry); the signature binds to the\n // member's current hash, which we have to load first.\n const memberId = passwordResetTokenMemberId(hostId, token)\n if (!memberId || !isMemberDocumentId(memberId)) return invalid()\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const memberRef = hostRef.collection('siteMembers').doc(memberId)\n const credentialRef = memberCredentialRef(hostRef, memberId)\n // Hashed once, however often the body retries, and only for a link that\n // verified: ~100 ms of CPU is not spent on a stale or forged token.\n let newHash: string | undefined\n const outcome = await firestore.runTransaction(async (tx) => {\n const [memberDoc, credentialDoc] = await tx.getAll(memberRef, credentialRef)\n if (!memberDoc.exists) return 'invalid'\n if (\n !verifyPasswordResetToken(\n hostId,\n token,\n storedPasswordHash(credentialDoc),\n )\n ) {\n return 'invalid'\n }\n // Suspension gate (AGL-546/550): a token minted before the\n // suspension stays cryptographically valid for its hour, so reject\n // here — recovery must not rehabilitate a suspended account. Checked\n // AFTER the signature so only the mailed-link holder (who already\n // knows the account exists) can observe the suspension.\n if (memberDoc.get('suspended') === true) return 'suspended'\n newHash ??= hashMemberPassword(password)\n tx.set(\n credentialRef,\n {\n passwordScrypt: newHash,\n passwordResetAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n tx.update(memberRef, retiredCredentialFields())\n return 'reset'\n })\n if (outcome === 'invalid') return invalid()\n if (outcome === 'suspended') {\n return res.status(403).json({ error: MEMBER_SUSPENDED_ERROR })\n }\n return res.status(200).json({ ok: true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reset failed' })\n }\n}\n"],"names":["firebaseAdmin","MEMBER_SUSPENDED_ERROR","hashMemberPassword","passwordResetTokenMemberId","verifyPasswordResetToken","isMemberDocumentId","memberCredentialRef","retiredCredentialFields","storedPasswordHash","membershipResetHandler","req","res","method","status","json","error","hostId","String","body","token","password","length","invalid","memberId","firestore","app","hostRef","collection","doc","memberRef","credentialRef","newHash","outcome","runTransaction","tx","memberDoc","credentialDoc","getAll","exists","get","set","passwordScrypt","passwordResetAt","FieldValue","serverTimestamp","merge","update","ok","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/membership-reset.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { PluginApiHandler } from '@aglyn/aglyn/server'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport {\n MEMBER_SUSPENDED_ERROR,\n hashMemberPassword,\n passwordResetTokenMemberId,\n verifyPasswordResetToken,\n} from './membership'\nimport {\n isMemberDocumentId,\n memberCredentialRef,\n retiredCredentialFields,\n storedPasswordHash,\n} from './member-credentials'\n\n/**\n * Password reset completion (AGL-552): verifies the recover token\n * (signature + expiry + binding to the member's CURRENT password hash —\n * so a completed reset invalidates every outstanding token) and writes the\n * new scrypt hash to the member's credential document (AGL-3308). No session\n * is minted here; the client signs in through the normal login route with the\n * new password. Token guessing is a non-issue (256-bit HMAC), so no extra\n * damper beyond login's.\n *\n * ONE TRANSACTION, from the token check to the write. The hash the token is\n * checked against is the hash the write replaces, so two submissions of one\n * link cannot both land — the second re-reads the new hash and fails the\n * binding — and a member removed mid-request is not recreated as a stub.\n */\nexport const membershipResetHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const hostId = String(req.body?.hostId ?? '')\n const token = String(req.body?.token ?? '')\n const password = String(req.body?.password ?? '')\n if (!hostId || !token) {\n return res.status(400).json({ error: 'Invalid request' })\n }\n if (password.length < 8) {\n return res\n .status(400)\n .json({ error: 'Password must be at least 8 characters' })\n }\n const invalid = () =>\n res.status(400).json({\n error: 'That reset link is invalid or has expired — request a new one.',\n })\n // Structural pre-check only (host + expiry); the signature binds to the\n // member's current hash, which we have to load first.\n const memberId = passwordResetTokenMemberId(hostId, token)\n if (!memberId || !isMemberDocumentId(memberId)) return invalid()\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const memberRef = hostRef.collection('siteMembers').doc(memberId)\n const credentialRef = memberCredentialRef(hostRef, memberId)\n // Hashed once, however often the body retries, and only for a link that\n // verified: ~100 ms of CPU is not spent on a stale or forged token.\n let newHash: string | undefined\n const outcome = await firestore.runTransaction(async (tx) => {\n const [memberDoc, credentialDoc] = await tx.getAll(memberRef, credentialRef)\n if (!memberDoc.exists) return 'invalid'\n if (\n !verifyPasswordResetToken(\n hostId,\n token,\n storedPasswordHash(credentialDoc),\n )\n ) {\n return 'invalid'\n }\n // Suspension gate (AGL-546/550): a token minted before the\n // suspension stays cryptographically valid for its hour, so reject\n // here — recovery must not rehabilitate a suspended account. Checked\n // AFTER the signature so only the mailed-link holder (who already\n // knows the account exists) can observe the suspension.\n if (memberDoc.get('suspended') === true) return 'suspended'\n newHash ??= hashMemberPassword(password)\n tx.set(\n credentialRef,\n {\n passwordScrypt: newHash,\n passwordResetAt: firebaseAdmin.firestore.FieldValue.serverTimestamp(),\n },\n { merge: true },\n )\n tx.update(memberRef, {\n ...retiredCredentialFields(),\n // The link was mailed to the member's address, so following it\n // proves they own it (AGL-3736): what lets a signed-in member's\n // purchases verify their product reviews.\n emailVerifiedAtMs: Date.now(),\n })\n return 'reset'\n })\n if (outcome === 'invalid') return invalid()\n if (outcome === 'suspended') {\n return res.status(403).json({ error: MEMBER_SUSPENDED_ERROR })\n }\n return res.status(200).json({ ok: true })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reset failed' })\n }\n}\n"],"names":["firebaseAdmin","MEMBER_SUSPENDED_ERROR","hashMemberPassword","passwordResetTokenMemberId","verifyPasswordResetToken","isMemberDocumentId","memberCredentialRef","retiredCredentialFields","storedPasswordHash","membershipResetHandler","req","res","method","status","json","error","hostId","String","body","token","password","length","invalid","memberId","firestore","app","hostRef","collection","doc","memberRef","credentialRef","newHash","outcome","runTransaction","tx","memberDoc","credentialDoc","getAll","exists","get","set","passwordScrypt","passwordResetAt","FieldValue","serverTimestamp","merge","update","emailVerifiedAtMs","Date","now","ok","console"],"mappings":"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"}
|
|
@@ -33,5 +33,7 @@ export declare function hostEventPayloadFor(envelope: Pick<PluginDomainEventEnve
|
|
|
33
33
|
* with the boot.
|
|
34
34
|
*/
|
|
35
35
|
export declare function registerCommerceEventTriggers(): void;
|
|
36
|
+
/** The order events that schedule a review request (AGL-3736). */
|
|
37
|
+
export declare const REVIEW_REQUEST_EVENTS: readonly [import("@aglyn/aglyn/plugin-manager/plugin-domain-events").PluginDomainEvent<import("../model").OrderFulfilledEventPayload>, import("@aglyn/aglyn/plugin-manager/plugin-domain-events").PluginDomainEvent<import("../model").OrderEventPayload>, import("@aglyn/aglyn/plugin-manager/plugin-domain-events").PluginDomainEvent<import("../model").OrderEventPayload>];
|
|
36
38
|
/** The order events that change what ShippingEasy should hold. */
|
|
37
39
|
export declare const SHIPPINGEASY_EVENTS: readonly [import("@aglyn/aglyn/plugin-manager/plugin-domain-events").PluginDomainEvent<import("../model").OrderEventPayload>, import("@aglyn/aglyn/plugin-manager/plugin-domain-events").PluginDomainEvent<import("../model").OrderEventPayload>, import("@aglyn/aglyn/plugin-manager/plugin-domain-events").PluginDomainEvent<import("../model").OrderRefundedEventPayload>];
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { declarePluginDomainEvents, subscribePluginDomainEvent } from "@aglyn/aglyn/plugin-manager/plugin-domain-events";
|
|
17
17
|
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
18
|
-
import { CHECKOUT_STARTED_EVENT_DECLARATION, COMMERCE_EVENT_DECLARATIONS, ORDER_CANCELLED_EVENT, ORDER_PAID_EVENT, ORDER_REFUNDED_EVENT } from "../model/order-events.js";
|
|
18
|
+
import { CHECKOUT_STARTED_EVENT_DECLARATION, COMMERCE_EVENT_DECLARATIONS, ORDER_CANCELLED_EVENT, ORDER_DELIVERED_EVENT, ORDER_FULFILLED_EVENT, ORDER_PAID_EVENT, ORDER_REFUNDED_EVENT } from "../model/order-events.js";
|
|
19
19
|
/**
|
|
20
20
|
* Commerce's events as workflow triggers (AGL-3611).
|
|
21
21
|
*
|
|
@@ -95,6 +95,18 @@ import { CHECKOUT_STARTED_EVENT_DECLARATION, COMMERCE_EVENT_DECLARATIONS, ORDER_
|
|
|
95
95
|
name: 'webhooks'
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
+
// The review-request email (AGL-3736): a fulfilled or delivered order —
|
|
99
|
+
// or a paid one with nothing to ship — is scheduled for its request. Its
|
|
100
|
+
// own subscriber name; scheduling is idempotent on the order.
|
|
101
|
+
for (const event of REVIEW_REQUEST_EVENTS){
|
|
102
|
+
subscribePluginDomainEvent(event, async (envelope)=>{
|
|
103
|
+
const { scheduleReviewRequestFromEvent } = await import("./review-requests.js");
|
|
104
|
+
await scheduleReviewRequestFromEvent(envelope);
|
|
105
|
+
}, {
|
|
106
|
+
pluginId: BUNDLE_ID,
|
|
107
|
+
name: 'review-requests'
|
|
108
|
+
});
|
|
109
|
+
}
|
|
98
110
|
// ShippingEasy (AGL-3633): a paid order is sent to the merchant's
|
|
99
111
|
// ShippingEasy account, and a canceled or refunded one is canceled there.
|
|
100
112
|
// Its own subscriber name, so a ShippingEasy outage is retried without
|
|
@@ -110,6 +122,11 @@ import { CHECKOUT_STARTED_EVENT_DECLARATION, COMMERCE_EVENT_DECLARATIONS, ORDER_
|
|
|
110
122
|
});
|
|
111
123
|
}
|
|
112
124
|
}
|
|
125
|
+
/** The order events that schedule a review request (AGL-3736). */ export const REVIEW_REQUEST_EVENTS = [
|
|
126
|
+
ORDER_FULFILLED_EVENT,
|
|
127
|
+
ORDER_DELIVERED_EVENT,
|
|
128
|
+
ORDER_PAID_EVENT
|
|
129
|
+
];
|
|
113
130
|
/** The order events that change what ShippingEasy should hold. */ export const SHIPPINGEASY_EVENTS = [
|
|
114
131
|
ORDER_PAID_EVENT,
|
|
115
132
|
ORDER_CANCELLED_EVENT,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-event-triggers.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 declarePluginDomainEvents,\n subscribePluginDomainEvent,\n type PluginDomainEventEnvelope,\n} from '@aglyn/aglyn/plugin-manager/plugin-domain-events'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n CHECKOUT_STARTED_EVENT_DECLARATION,\n COMMERCE_EVENT_DECLARATIONS,\n ORDER_CANCELLED_EVENT,\n ORDER_PAID_EVENT,\n ORDER_REFUNDED_EVENT,\n} from '../model/order-events'\n\n/**\n * Commerce's events as workflow triggers (AGL-3611).\n *\n * The workflows plugin listens to host events, flat key/value payloads it\n * puts in an automation's scope. Commerce subscribes to its own order and\n * return events and raises each as the host event its declaration names\n * (`orderPaid`, `orderFulfilled`, …), declared in `plugins.config.json` so\n * every trigger picker lists them. The automation engine is never imported:\n * the host event bus is the seam.\n */\n\n/** The flat scope an automation reads: the order's facts, then the event's own. */\nexport function hostEventPayloadFor(\n envelope: Pick<PluginDomainEventEnvelope, 'event' | 'payload'>,\n): Record<string, string | number | boolean> {\n const payload = (envelope.payload ?? {}) as Record<string, any>\n const order = (payload.order ?? {}) as Record<string, any>\n const flat: Record<string, string | number | boolean> = {}\n const put = (key: string, value: unknown) => {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n flat[key] = value\n }\n }\n put('orderId', order.id)\n put('orderNumber', typeof order.number === 'number' ? `#${order.number}` : order.id)\n put('status', order.status)\n put('channel', order.channel)\n put('email', order.customerEmail)\n put('name', order.customerName)\n put('totalCents', order.totals?.totalCents)\n put('currency', order.currency)\n const fulfillment = payload.fulfillment as Record<string, any> | undefined\n if (fulfillment) {\n put('fulfillmentId', fulfillment.id)\n put('carrier', fulfillment.carrier)\n put('trackingNumber', fulfillment.trackingNumber)\n put('trackingUrl', fulfillment.trackingUrl)\n }\n const refund = payload.refund as Record<string, any> | undefined\n if (refund) {\n put('refundCents', refund.amountCents)\n put('fullRefund', refund.full)\n }\n const returned = payload.return as Record<string, any> | undefined\n if (returned) {\n put('returnId', returned.id)\n put('returnStatus', returned.status)\n put('returnRefundCents', returned.refundCents)\n }\n return flat\n}\n\n/**\n * Declares commerce's events and subscribes the trigger bridge, from the\n * server declarations. `emitHostEvent` arrives with the first event, not\n * with the boot.\n */\nexport function registerCommerceEventTriggers(): void {\n declarePluginDomainEvents([...COMMERCE_EVENT_DECLARATIONS, CHECKOUT_STARTED_EVENT_DECLARATION], { pluginId: BUNDLE_ID })\n for (const declaration of COMMERCE_EVENT_DECLARATIONS) {\n subscribePluginDomainEvent(\n declaration.event,\n async (envelope) => {\n const { emitCommerceHostEvent } = await import('./order-event-host-emit')\n await emitCommerceHostEvent(envelope.hostId, declaration.hostEvent, hostEventPayloadFor(envelope))\n },\n // Named, so the outbox records it apart from the webhook subscriber\n // this plugin also registers.\n { pluginId: BUNDLE_ID, name: 'workflow-triggers' },\n )\n // The merchant's own webhooks (AGL-3611). Its own subscriber name, so a\n // failing endpoint is retried without raising the workflow trigger again.\n subscribePluginDomainEvent(\n declaration.event,\n async (envelope) => {\n const { deliverOrderEventToWebhooks } = await import('./order-webhooks')\n await deliverOrderEventToWebhooks(envelope)\n },\n { pluginId: BUNDLE_ID, name: 'webhooks' },\n )\n }\n // ShippingEasy (AGL-3633): a paid order is sent to the merchant's\n // ShippingEasy account, and a canceled or refunded one is canceled there.\n // Its own subscriber name, so a ShippingEasy outage is retried without\n // re-posting the merchant's webhooks. A site with no connection costs one\n // document read.\n for (const event of SHIPPINGEASY_EVENTS) {\n subscribePluginDomainEvent(\n event,\n async (envelope) => {\n const { deliverOrderEventToShippingEasy } = await import('./shippingeasy')\n await deliverOrderEventToShippingEasy(envelope)\n },\n { pluginId: BUNDLE_ID, name: 'shippingeasy' },\n )\n }\n}\n\n/** The order events that change what ShippingEasy should hold. */\nexport const SHIPPINGEASY_EVENTS = [ORDER_PAID_EVENT, ORDER_CANCELLED_EVENT, ORDER_REFUNDED_EVENT] as const\n"],"names":["declarePluginDomainEvents","subscribePluginDomainEvent","BUNDLE_ID","CHECKOUT_STARTED_EVENT_DECLARATION","COMMERCE_EVENT_DECLARATIONS","ORDER_CANCELLED_EVENT","ORDER_PAID_EVENT","ORDER_REFUNDED_EVENT","hostEventPayloadFor","envelope","payload","order","flat","put","key","value","id","number","status","channel","customerEmail","customerName","totals","totalCents","currency","fulfillment","carrier","trackingNumber","trackingUrl","refund","amountCents","full","returned","return","refundCents","registerCommerceEventTriggers","pluginId","declaration","event","emitCommerceHostEvent","hostId","hostEvent","name","deliverOrderEventToWebhooks","SHIPPINGEASY_EVENTS","deliverOrderEventToShippingEasy"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,yBAAyB,EACzBC,0BAA0B,QAErB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SACEC,kCAAkC,EAClCC,2BAA2B,EAC3BC,qBAAqB,EACrBC,gBAAgB,EAChBC,oBAAoB,QACf,2BAAuB;AAE9B;;;;;;;;;CASC,GAED,iFAAiF,GACjF,OAAO,SAASC,oBACdC,QAA8D;QAE7CA,mBACFC;QAaGC;IAdlB,MAAMD,WAAWD,oBAAAA,SAASC,OAAO,YAAhBD,oBAAoB,CAAC;IACtC,MAAME,SAASD,iBAAAA,QAAQC,KAAK,YAAbD,iBAAiB,CAAC;IACjC,MAAME,OAAkD,CAAC;IACzD,MAAMC,MAAM,CAACC,KAAaC;QACxB,IAAI,OAAOA,UAAU,YAAY,OAAOA,UAAU,YAAY,OAAOA,UAAU,WAAW;YACxFH,IAAI,CAACE,IAAI,GAAGC;QACd;IACF;IACAF,IAAI,WAAWF,MAAMK,EAAE;IACvBH,IAAI,eAAe,OAAOF,MAAMM,MAAM,KAAK,WAAW,CAAC,CAAC,EAAEN,MAAMM,MAAM,EAAE,GAAGN,MAAMK,EAAE;IACnFH,IAAI,UAAUF,MAAMO,MAAM;IAC1BL,IAAI,WAAWF,MAAMQ,OAAO;IAC5BN,IAAI,SAASF,MAAMS,aAAa;IAChCP,IAAI,QAAQF,MAAMU,YAAY;IAC9BR,IAAI,eAAcF,gBAAAA,MAAMW,MAAM,qBAAZX,cAAcY,UAAU;IAC1CV,IAAI,YAAYF,MAAMa,QAAQ;IAC9B,MAAMC,cAAcf,QAAQe,WAAW;IACvC,IAAIA,aAAa;QACfZ,IAAI,iBAAiBY,YAAYT,EAAE;QACnCH,IAAI,WAAWY,YAAYC,OAAO;QAClCb,IAAI,kBAAkBY,YAAYE,cAAc;QAChDd,IAAI,eAAeY,YAAYG,WAAW;IAC5C;IACA,MAAMC,SAASnB,QAAQmB,MAAM;IAC7B,IAAIA,QAAQ;QACVhB,IAAI,eAAegB,OAAOC,WAAW;QACrCjB,IAAI,cAAcgB,OAAOE,IAAI;IAC/B;IACA,MAAMC,WAAWtB,QAAQuB,MAAM;IAC/B,IAAID,UAAU;QACZnB,IAAI,YAAYmB,SAAShB,EAAE;QAC3BH,IAAI,gBAAgBmB,SAASd,MAAM;QACnCL,IAAI,qBAAqBmB,SAASE,WAAW;IAC/C;IACA,OAAOtB;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASuB;
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/order-event-triggers.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 declarePluginDomainEvents,\n subscribePluginDomainEvent,\n type PluginDomainEventEnvelope,\n} from '@aglyn/aglyn/plugin-manager/plugin-domain-events'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport {\n CHECKOUT_STARTED_EVENT_DECLARATION,\n COMMERCE_EVENT_DECLARATIONS,\n ORDER_CANCELLED_EVENT,\n ORDER_DELIVERED_EVENT,\n ORDER_FULFILLED_EVENT,\n ORDER_PAID_EVENT,\n ORDER_REFUNDED_EVENT,\n} from '../model/order-events'\n\n/**\n * Commerce's events as workflow triggers (AGL-3611).\n *\n * The workflows plugin listens to host events, flat key/value payloads it\n * puts in an automation's scope. Commerce subscribes to its own order and\n * return events and raises each as the host event its declaration names\n * (`orderPaid`, `orderFulfilled`, …), declared in `plugins.config.json` so\n * every trigger picker lists them. The automation engine is never imported:\n * the host event bus is the seam.\n */\n\n/** The flat scope an automation reads: the order's facts, then the event's own. */\nexport function hostEventPayloadFor(\n envelope: Pick<PluginDomainEventEnvelope, 'event' | 'payload'>,\n): Record<string, string | number | boolean> {\n const payload = (envelope.payload ?? {}) as Record<string, any>\n const order = (payload.order ?? {}) as Record<string, any>\n const flat: Record<string, string | number | boolean> = {}\n const put = (key: string, value: unknown) => {\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n flat[key] = value\n }\n }\n put('orderId', order.id)\n put('orderNumber', typeof order.number === 'number' ? `#${order.number}` : order.id)\n put('status', order.status)\n put('channel', order.channel)\n put('email', order.customerEmail)\n put('name', order.customerName)\n put('totalCents', order.totals?.totalCents)\n put('currency', order.currency)\n const fulfillment = payload.fulfillment as Record<string, any> | undefined\n if (fulfillment) {\n put('fulfillmentId', fulfillment.id)\n put('carrier', fulfillment.carrier)\n put('trackingNumber', fulfillment.trackingNumber)\n put('trackingUrl', fulfillment.trackingUrl)\n }\n const refund = payload.refund as Record<string, any> | undefined\n if (refund) {\n put('refundCents', refund.amountCents)\n put('fullRefund', refund.full)\n }\n const returned = payload.return as Record<string, any> | undefined\n if (returned) {\n put('returnId', returned.id)\n put('returnStatus', returned.status)\n put('returnRefundCents', returned.refundCents)\n }\n return flat\n}\n\n/**\n * Declares commerce's events and subscribes the trigger bridge, from the\n * server declarations. `emitHostEvent` arrives with the first event, not\n * with the boot.\n */\nexport function registerCommerceEventTriggers(): void {\n declarePluginDomainEvents([...COMMERCE_EVENT_DECLARATIONS, CHECKOUT_STARTED_EVENT_DECLARATION], { pluginId: BUNDLE_ID })\n for (const declaration of COMMERCE_EVENT_DECLARATIONS) {\n subscribePluginDomainEvent(\n declaration.event,\n async (envelope) => {\n const { emitCommerceHostEvent } = await import('./order-event-host-emit')\n await emitCommerceHostEvent(envelope.hostId, declaration.hostEvent, hostEventPayloadFor(envelope))\n },\n // Named, so the outbox records it apart from the webhook subscriber\n // this plugin also registers.\n { pluginId: BUNDLE_ID, name: 'workflow-triggers' },\n )\n // The merchant's own webhooks (AGL-3611). Its own subscriber name, so a\n // failing endpoint is retried without raising the workflow trigger again.\n subscribePluginDomainEvent(\n declaration.event,\n async (envelope) => {\n const { deliverOrderEventToWebhooks } = await import('./order-webhooks')\n await deliverOrderEventToWebhooks(envelope)\n },\n { pluginId: BUNDLE_ID, name: 'webhooks' },\n )\n }\n // The review-request email (AGL-3736): a fulfilled or delivered order —\n // or a paid one with nothing to ship — is scheduled for its request. Its\n // own subscriber name; scheduling is idempotent on the order.\n for (const event of REVIEW_REQUEST_EVENTS) {\n subscribePluginDomainEvent(\n event,\n async (envelope) => {\n const { scheduleReviewRequestFromEvent } = await import('./review-requests')\n await scheduleReviewRequestFromEvent(envelope)\n },\n { pluginId: BUNDLE_ID, name: 'review-requests' },\n )\n }\n // ShippingEasy (AGL-3633): a paid order is sent to the merchant's\n // ShippingEasy account, and a canceled or refunded one is canceled there.\n // Its own subscriber name, so a ShippingEasy outage is retried without\n // re-posting the merchant's webhooks. A site with no connection costs one\n // document read.\n for (const event of SHIPPINGEASY_EVENTS) {\n subscribePluginDomainEvent(\n event,\n async (envelope) => {\n const { deliverOrderEventToShippingEasy } = await import('./shippingeasy')\n await deliverOrderEventToShippingEasy(envelope)\n },\n { pluginId: BUNDLE_ID, name: 'shippingeasy' },\n )\n }\n}\n\n/** The order events that schedule a review request (AGL-3736). */\nexport const REVIEW_REQUEST_EVENTS = [ORDER_FULFILLED_EVENT, ORDER_DELIVERED_EVENT, ORDER_PAID_EVENT] as const\n\n/** The order events that change what ShippingEasy should hold. */\nexport const SHIPPINGEASY_EVENTS = [ORDER_PAID_EVENT, ORDER_CANCELLED_EVENT, ORDER_REFUNDED_EVENT] as const\n"],"names":["declarePluginDomainEvents","subscribePluginDomainEvent","BUNDLE_ID","CHECKOUT_STARTED_EVENT_DECLARATION","COMMERCE_EVENT_DECLARATIONS","ORDER_CANCELLED_EVENT","ORDER_DELIVERED_EVENT","ORDER_FULFILLED_EVENT","ORDER_PAID_EVENT","ORDER_REFUNDED_EVENT","hostEventPayloadFor","envelope","payload","order","flat","put","key","value","id","number","status","channel","customerEmail","customerName","totals","totalCents","currency","fulfillment","carrier","trackingNumber","trackingUrl","refund","amountCents","full","returned","return","refundCents","registerCommerceEventTriggers","pluginId","declaration","event","emitCommerceHostEvent","hostId","hostEvent","name","deliverOrderEventToWebhooks","REVIEW_REQUEST_EVENTS","scheduleReviewRequestFromEvent","SHIPPINGEASY_EVENTS","deliverOrderEventToShippingEasy"],"mappings":"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"}
|
|
@@ -0,0 +1,102 @@
|
|
|
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 { PluginJobHostGate } from '@aglyn/aglyn/server';
|
|
18
|
+
import type { PluginDomainEventEnvelope } from '@aglyn/aglyn/plugin-manager/plugin-domain-events';
|
|
19
|
+
/**
|
|
20
|
+
* The review-request email (AGL-3736).
|
|
21
|
+
*
|
|
22
|
+
* ## When
|
|
23
|
+
*
|
|
24
|
+
* N days (3–30, default 7, per store) after an order is fulfilled. Every door
|
|
25
|
+
* that ships or delivers an order already raises `order.fulfilled` or
|
|
26
|
+
* `order.delivered` (AGL-3611), and an order with nothing to ship raises
|
|
27
|
+
* `order.paid`, so commerce subscribes to its own events and SCHEDULES the
|
|
28
|
+
* request by stamping the order: `reviewRequestDueAtMs` at the earliest
|
|
29
|
+
* possible moment (fulfilment + 3 days) and `reviewRequest.state:
|
|
30
|
+
* 'scheduled'`. The hourly sweep reads the store's CURRENT delay when the
|
|
31
|
+
* stamp comes due and pushes it out if the store asked for longer, so a
|
|
32
|
+
* setting changed after fulfilment still applies.
|
|
33
|
+
*
|
|
34
|
+
* ## Once
|
|
35
|
+
*
|
|
36
|
+
* `reviewRequestDueAtMs` exists only while a request is owed — it is the
|
|
37
|
+
* field the collection-group sweep queries, so a sent or skipped order costs
|
|
38
|
+
* the sweep nothing. Scheduling refuses an order that already has a
|
|
39
|
+
* `reviewRequest`, and the send CLAIMS the order in a transaction that deletes
|
|
40
|
+
* the due field before anything is mailed: two overlapping sweeps cannot both
|
|
41
|
+
* send, and a redelivered event cannot schedule twice.
|
|
42
|
+
*
|
|
43
|
+
* ## What it respects
|
|
44
|
+
*
|
|
45
|
+
* Sent through the marketing seam (no stream), so the suppression lists, the
|
|
46
|
+
* site's unsubscribe link and header, and the per-person frequency ceiling
|
|
47
|
+
* all apply; a frequency or hourly refusal is retried an hour later. A line
|
|
48
|
+
* refunded by name, and a refunded or cancelled order, are never asked about.
|
|
49
|
+
* Register sales and marketplace orders are never asked. The store turns it
|
|
50
|
+
* off on its review settings, and words it under Emails (`review-request`).
|
|
51
|
+
*/
|
|
52
|
+
export declare const REVIEW_REQUEST_EMAIL_KEY = "review-request";
|
|
53
|
+
/** What a stamped order carries. */
|
|
54
|
+
export interface OrderReviewRequest {
|
|
55
|
+
state: 'scheduled' | 'sending' | 'sent' | 'skipped';
|
|
56
|
+
fulfilledAtMs: number;
|
|
57
|
+
atMs?: number;
|
|
58
|
+
reason?: string;
|
|
59
|
+
productIds?: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Schedules one order's request, once. Returns whether this call scheduled it.
|
|
63
|
+
*/
|
|
64
|
+
export declare function scheduleReviewRequest(input: {
|
|
65
|
+
hostId: string;
|
|
66
|
+
orderId: string;
|
|
67
|
+
fulfilledAtMs: number;
|
|
68
|
+
onlyIfNothingShips?: boolean;
|
|
69
|
+
}, firestore?: FirebaseFirestore.Firestore): Promise<boolean>;
|
|
70
|
+
/** The `order.fulfilled` / `order.delivered` / `order.paid` subscriber. */
|
|
71
|
+
export declare function scheduleReviewRequestFromEvent(envelope: Pick<PluginDomainEventEnvelope, 'event' | 'hostId' | 'occurredAtMs' | 'payload'>, firestore?: FirebaseFirestore.Firestore): Promise<void>;
|
|
72
|
+
/** One reviewable product as the email lists it. */
|
|
73
|
+
export interface ReviewRequestLine {
|
|
74
|
+
productId: string;
|
|
75
|
+
name: string;
|
|
76
|
+
url: string;
|
|
77
|
+
}
|
|
78
|
+
/** The email's words. Pure. */
|
|
79
|
+
export declare function composeReviewRequest(input: {
|
|
80
|
+
businessName: string;
|
|
81
|
+
orderNumber: string;
|
|
82
|
+
lines: ReviewRequestLine[];
|
|
83
|
+
}): {
|
|
84
|
+
subject: string;
|
|
85
|
+
text: string;
|
|
86
|
+
tokens: Record<string, string>;
|
|
87
|
+
};
|
|
88
|
+
export interface ReviewRequestScanResult {
|
|
89
|
+
scanned: number;
|
|
90
|
+
sent: number;
|
|
91
|
+
skipped: number;
|
|
92
|
+
deferred: number;
|
|
93
|
+
skippedLocked: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* One pass over the requests that have come due. Bounded (100) and
|
|
97
|
+
* idempotent; registered on the hourly plugin-job beat in `server.ts`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function scanReviewRequests(gate: PluginJobHostGate, options?: {
|
|
100
|
+
firestore?: FirebaseFirestore.Firestore;
|
|
101
|
+
nowMs?: number;
|
|
102
|
+
}): Promise<ReviewRequestScanResult>;
|