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

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/components/console/shipping-settings-card.component.tsx"],"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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n AlertTitle,\n Button,\n Divider,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { doc, setDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n useFirestoreDoc,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\n\nexport interface ShippingSettingsCardProps {\n hostId: string\n}\n\n/**\n * Shipping settings (AGL-288): zones (countries, '*' = rest of world)\n * and rates (flat / free-over / subtotal or weight tiers), stored on\n * `hosts/{hostId}/settings/store` under `shipping`. Cart checkout (AGL-1707)\n * and buy-now (AGL-1720) both read this document and declare the rates as\n * Stripe `shipping_options` (`planCheckoutShipping`) — until those landed\n * nothing read it, and everything saved here was charged to nobody. Buy-now\n * resolves only for physical one-time sales; draft orders and POS still\n * resolve no shipping at all.\n *\n * WHAT A ZONE LAYOUT COSTS THE SHOPPER (AGL-1721). A session's rates are\n * charged as picked and never re-checked against the address, so the checkout\n * can only offer rates that hold for every country it will ship to. Saving\n * zones whose rates DIFFER therefore makes the storefront ask the shopper\n * where the parcel is going before it can price one, and a destination no\n * zone covers is refused outright rather than shipped for free. One zone, or\n * one '*' zone, needs neither: the shopper is asked nothing.\n *\n * AND THE MERCHANT HAS TO BE ABLE TO SEE IT (AGL-1791). Both consequences\n * were invisible here: a merchant saved zones and the checkouts they refused\n * were refused with nothing on this page saying so, which is a support ticket\n * from someone who did nothing wrong. The coverage block reads\n * `summarizeShippingCoverage` off the same `current` object the save writes,\n * so it moves as the form is typed rather than after a round trip.\n *\n * It escalates to a warning ONLY for a merchant who priced some destinations\n * and not others. A merchant whose rates reach nowhere is not misconfigured —\n * that store charges no shipping and refuses nobody — and warning them would\n * nag every store selling downloads.\n */\nexport function ShippingSettingsCard(props: ShippingSettingsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const {\n data: store,\n status: storeStatus,\n /**\n * The settings doc this form is seeded from is unconfirmed by the server\n * (AGL-1358). `current` is `draft ?? store?.shipping ?? {}` and the save\n * writes `{shipping: current}`, so `merge: true` protects the doc's other\n * maps and nothing inside this one — `zones` and `rates` are arrays, and\n * a merge replaces an array wholesale. Against a cached read, adding one\n * rate restores that snapshot's whole price table, and a zone that\n * disappears with it means `resolveShippingRates` matches nothing, which\n * is a checkout offering no shipping option at all.\n */\n fromCache: storeFromCache,\n } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'settings', 'store'),\n [firestore, hostId],\n )\n const [draft, setDraft] = useState<CommerceModel.ShippingSettings | null>(null)\n const current: CommerceModel.ShippingSettings = draft ?? store?.shipping ?? {}\n /*\n * Live carrier rates (AGL-3612) are offered only where a plugin quotes\n * carriers for this site — the server asks core's quoter seam. A rate\n * already saved as `carrier` stays editable either way, so a merchant can\n * still see and change what checkout falls back to.\n */\n const { data: user } = useUser()\n const [carrier, setCarrier] = useState<{\n available: boolean\n services: Array<{ serviceKey: string; label: string }>\n }>({ available: false, services: [] })\n useEffect(() => {\n let live = true\n authorizedFetch(user, `/api/commerce/shipping/carrier-rates?hostId=${encodeURIComponent(hostId)}`)\n .then((response) => (response.ok ? response.json() : null))\n .then((answer) => {\n if (live && answer) setCarrier({ available: Boolean(answer.available), services: answer.services ?? [] })\n })\n .catch(() => undefined)\n return () => {\n live = false\n }\n }, [hostId, user])\n const update = (patch: Partial<CommerceModel.ShippingSettings>) =>\n setDraft({ ...current, ...patch })\n\n const updateZone = (\n index: number,\n patch: Partial<CommerceModel.ShippingZone> | null,\n ) => {\n const zones = [...(current.zones ?? [])]\n if (patch === null) {\n const removed = zones.splice(index, 1)[0]\n update({\n zones,\n rates: (current.rates ?? []).filter(\n (rate) => rate.zoneId !== removed?.id,\n ),\n })\n return\n }\n zones[index] = {\n id: Aglyn.createResourceUid(),\n name: '',\n countries: [],\n ...zones[index],\n ...patch,\n }\n update({ zones })\n }\n\n const updateRate = (\n index: number,\n patch: Partial<CommerceModel.ShippingRate> | null,\n ) => {\n const rates = [...(current.rates ?? [])]\n if (patch === null) rates.splice(index, 1)\n else\n rates[index] = {\n id: Aglyn.createResourceUid(),\n zoneId: current.zones?.[0]?.id ?? '',\n name: '',\n kind: 'flat',\n ...rates[index],\n ...patch,\n }\n update({ rates })\n }\n\n const handleSave = useCallback(async () => {\n /**\n * Refuse a save whose seed the server never confirmed (AGL-1358).\n *\n * No create path to exempt: this is a FIXED document path, so the empty\n * `{}` that `store?.shipping ?? {}` yields when the cache has never seen\n * the map is not the harmless blank a fresh uid would be — it replaces\n * the real zones and rates with nothing.\n *\n * The guard WRAPS the write — an early return is a shape you can keep\n * while losing the protection.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'shipping settings',\n unreadable: storeStatus === 'error',\n fromCache: storeFromCache,\n },\n async () => {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'settings', 'store'),\n { shipping: current },\n { merge: true },\n )\n },\n )\n // Before `setDraft(null)`, so a refusal keeps every zone and rate that\n // was typed on screen.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setDraft(null)\n enqueueSnackbar('Shipping settings saved', {\n variant: 'success',\n persist: false,\n })\n }, [current, firestore, hostId, enqueueSnackbar, storeStatus, storeFromCache])\n\n /**\n * Recomputed from `current` on every render, so the block answers for what\n * is typed rather than for what was last saved — a merchant deleting the\n * zone that covered France should see France go uncovered before they\n * commit it, not after.\n */\n const coverage = CommerceModel.summarizeShippingCoverage(current)\n const countryNames = (codes: readonly string[]) =>\n codes\n .map((code) => CommerceModel.CHECKOUT_SHIPPING_COUNTRY_NAMES[code] ?? code)\n .join(', ')\n /**\n * The one-click half of the fix: a zone claiming everywhere the merchant\n * has not named. It adds the ZONE only — a rate on it prices the parcel,\n * and choosing that price is the merchant's call, not ours. A rate row\n * defaulted here would post at whatever we picked, and a blank flat rate\n * resolves to free.\n */\n const addRestOfWorldZone = () =>\n updateZone(current.zones?.length ?? 0, {\n name: 'Rest of world',\n countries: ['*'],\n })\n\n const usd = (cents: number | undefined) =>\n cents == null ? '' : String(cents / 100)\n const cents = (raw: string) =>\n raw.trim() === '' ? undefined : Math.round(Number(raw) * 100)\n\n return (\n <CardDisplay\n header={'Shipping'}\n help={pluginDocsHelp('shipping', { anchor: '#zones-and-rates' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.5}>\n <Typography variant=\"subtitle2\">{'Zones'}</Typography>\n {(current.zones ?? []).map((zone, index) => (\n <Stack key={zone.id} direction=\"row\" spacing={1}>\n <TextField\n label=\"Zone name\"\n value={zone.name}\n onChange={(event) =>\n updateZone(index, { name: event.target.value })\n }\n size=\"small\"\n sx={{ width: 180 }}\n />\n <TextField\n label=\"Countries\"\n value={zone.countries.join(', ')}\n onChange={(event) =>\n updateZone(index, {\n countries: event.target.value\n .split(',')\n .map((code) => code.trim().toUpperCase())\n .filter(Boolean),\n })\n }\n size=\"small\"\n sx={{ flex: 1 }}\n placeholder=\"US, CA — or * for everywhere else\"\n />\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() => updateZone(index, null)}\n >\n {'✕'}\n </Button>\n </Stack>\n ))}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() => updateZone(current.zones?.length ?? 0, {})}\n >\n {'Add zone'}\n </Button>\n\n <Divider />\n <Typography variant=\"subtitle2\">{'Rates'}</Typography>\n {(current.rates ?? []).map((rate, index) => (\n <Stack key={rate.id} spacing={1}>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Zone\"\n value={rate.zoneId}\n onChange={(event) =>\n updateRate(index, { zoneId: event.target.value })\n }\n size=\"small\"\n select\n sx={{ width: 150 }}\n >\n {(current.zones ?? []).map((zone) => (\n <MenuItem key={zone.id} value={zone.id}>\n {zone.name || zone.countries.join(',')}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n label=\"Name\"\n value={rate.name}\n onChange={(event) =>\n updateRate(index, { name: event.target.value })\n }\n size=\"small\"\n sx={{ flex: 1 }}\n placeholder=\"Standard\"\n /**\n * A rate with no name is DROPPED at checkout, silently\n * (AGL-1791): Stripe requires a `display_name`, so\n * `resolveCheckoutShippingOptions` skips the row rather than\n * emit a session Stripe would reject. Saved blank it looks\n * like a priced rate on this page and is not one anywhere\n * else, which is the same invisibility the coverage block\n * below exists to end — said on the row that causes it,\n * because the block can only report the destination it costs.\n */\n error={!rate.name.trim()}\n helperText={\n rate.name.trim() ? undefined : 'Needed — checkout drops it'\n }\n />\n <TextField\n label=\"Type\"\n value={rate.kind}\n onChange={(event) =>\n updateRate(index, {\n kind: event.target.value as CommerceModel.ShippingRate['kind'],\n })\n }\n size=\"small\"\n select\n sx={{ width: 170 }}\n >\n <MenuItem value=\"flat\">{'Flat'}</MenuItem>\n <MenuItem value=\"free_over\">{'Free over subtotal'}</MenuItem>\n <MenuItem value=\"price_tiers\">{'Subtotal tiers'}</MenuItem>\n <MenuItem value=\"weight_tiers\">{'Weight tiers'}</MenuItem>\n {carrier.available || rate.kind === 'carrier' ? (\n <MenuItem value=\"carrier\">{'Carrier rates'}</MenuItem>\n ) : null}\n </TextField>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() => updateRate(index, null)}\n >\n {'✕'}\n </Button>\n </Stack>\n {rate.kind === 'carrier' ? (\n <CarrierRateFields\n rate={rate}\n services={carrier.services}\n available={carrier.available}\n fallbacks={(current.rates ?? []).filter(\n (other) =>\n other.id !== rate.id &&\n other.zoneId === rate.zoneId &&\n other.kind !== 'carrier',\n )}\n onChange={(patch) => updateRate(index, patch)}\n />\n ) : rate.kind === 'flat' || rate.kind === 'free_over' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Price ($)\"\n value={usd(rate.amountCents)}\n onChange={(event) =>\n updateRate(index, { amountCents: cents(event.target.value) })\n }\n size=\"small\"\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n {rate.kind === 'free_over' ? (\n <TextField\n label=\"Free at subtotal ($)\"\n value={usd(rate.freeOverCents)}\n onChange={(event) =>\n updateRate(index, {\n freeOverCents: cents(event.target.value),\n })\n }\n size=\"small\"\n sx={{ width: 160 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n ) : null}\n </Stack>\n ) : (\n <Stack spacing={1} sx={{ pl: 2 }}>\n {(rate.tiers ?? []).map((tier, tierIndex) => (\n <Stack key={tierIndex} direction=\"row\" spacing={1}>\n <TextField\n label={\n rate.kind === 'weight_tiers'\n ? 'Up to (g)'\n : 'Up to subtotal ($)'\n }\n value={\n rate.kind === 'weight_tiers'\n ? tier.upTo\n : tier.upTo / 100\n }\n onChange={(event) => {\n const tiers = [...(rate.tiers ?? [])]\n const value = Number(event.target.value) || 0\n tiers[tierIndex] = {\n ...tier,\n upTo:\n rate.kind === 'weight_tiers'\n ? Math.round(value)\n : Math.round(value * 100),\n }\n updateRate(index, { tiers })\n }}\n size=\"small\"\n sx={{ width: 160 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n label=\"Price ($)\"\n value={tier.amountCents / 100}\n onChange={(event) => {\n const tiers = [...(rate.tiers ?? [])]\n tiers[tierIndex] = {\n ...tier,\n amountCents: Math.round(\n (Number(event.target.value) || 0) * 100,\n ),\n }\n updateRate(index, { tiers })\n }}\n size=\"small\"\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n updateRate(index, {\n tiers: (rate.tiers ?? []).filter(\n (_item, itemIndex) => itemIndex !== tierIndex,\n ),\n })\n }\n >\n {'✕'}\n </Button>\n </Stack>\n ))}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n updateRate(index, {\n tiers: [\n ...(rate.tiers ?? []),\n { upTo: 0, amountCents: 0 },\n ],\n })\n }\n >\n {'Add tier'}\n </Button>\n </Stack>\n )}\n </Stack>\n ))}\n <Button\n size=\"small\"\n disabled={(current.zones?.length ?? 0) === 0}\n sx={{ alignSelf: 'flex-start' }}\n onClick={() => updateRate(current.rates?.length ?? 0, {})}\n >\n {'Add rate'}\n </Button>\n\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n size=\"small\"\n checked={Boolean(current.localPickup)}\n onChange={(event) =>\n update({ localPickup: event.target.checked })\n }\n />\n }\n label=\"Offer free local pickup\"\n />\n {/*\n What the toggle does NOT do, said on the toggle (AGL-2325). It\n used to silently switch off every \"we don't ship there\" refusal,\n because pickup resolved for every destination and checkout read a\n non-empty list as coverage. It no longer does — but a merchant\n reading one line of label still has to be told that turning\n collection on is not the same as adding a destination.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {\n 'A free collection choice at checkout. It does not widen where ' +\n 'you ship — a destination none of your rates reaches is still ' +\n 'turned away. To let buyers choose WHERE to collect, with hours ' +\n 'and a ready-for-pickup email, turn on pickup for a location ' +\n 'under Inventory locations; this choice then gives way to it.'\n }\n </Typography>\n </Stack>\n\n <Divider />\n <Typography variant=\"subtitle2\">{'Coverage'}</Typography>\n {coverage.unserved.length > 0 ? (\n <Alert\n severity=\"warning\"\n action={\n coverage.hasRestOfWorldZone ? undefined : (\n <Button\n size=\"small\"\n color=\"inherit\"\n onClick={addRestOfWorldZone}\n >\n {'Add rest-of-world zone'}\n </Button>\n )\n }\n >\n <AlertTitle>\n {`Checkout turns away ${coverage.unserved.length} of the ${CommerceModel.CHECKOUT_SHIPPING_COUNTRIES.length} destinations it collects addresses for`}\n </AlertTitle>\n {`No rate reaches ${countryNames(coverage.unserved)}, so an order shipping there is refused rather than posted for free. `}\n {coverage.offersLocalPickup\n ? 'Local pickup does not cover them: it is a collection choice, not a rate, so it is offered where checkout completes and closes no gap here. '\n : ''}\n {coverage.hasRestOfWorldZone\n ? 'A rest-of-world zone is already saved, so what those destinations are missing is a rate on the zone that covers them.'\n : 'Give them a zone with a rate, or add a rest-of-world zone and price one on it.'}\n </Alert>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {coverage.shipsNowhere\n ? coverage.offersLocalPickup\n ? 'No rate reaches any destination, so local pickup is the only delivery choice checkout offers. Nobody is turned away and no shipping is charged.'\n : 'No rate reaches any destination, so checkout charges no shipping and turns nobody away.'\n : `Checkout can price shipping to every destination it collects an address for: ${countryNames(coverage.served)}.`}\n </Typography>\n )}\n {coverage.asksDestination ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {\n 'Rates differ by destination, so checkout asks the shopper where the parcel is going before it can price one.'\n }\n </Typography>\n ) : null}\n\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!draft}\n onClick={handleSave}\n sx={{ alignSelf: 'flex-start' }}\n >\n {'Save shipping settings'}\n </Button>\n </Stack>\n </CardDisplay>\n )\n}\nShippingSettingsCard.displayName = 'ShippingSettingsCard'\n\nexport default ShippingSettingsCard\n\ninterface CarrierRateFieldsProps {\n rate: CommerceModel.ShippingRate\n services: Array<{ serviceKey: string; label: string }>\n available: boolean\n /** The zone's other rates, any of which checkout may fall back to. */\n fallbacks: CommerceModel.ShippingRate[]\n onChange: (patch: Partial<CommerceModel.ShippingRate>) => void\n}\n\n/**\n * A carrier rate's options (AGL-3612): which services checkout offers, what\n * is added to each quote, and the zone's rate offered when no live quote is.\n */\nfunction CarrierRateFields(props: CarrierRateFieldsProps) {\n const { rate, services, available, fallbacks, onChange } = props\n const options = rate.carrier ?? {}\n const set = (patch: Partial<CommerceModel.CarrierRateSettings>) =>\n onChange({ carrier: { ...options, ...patch } })\n const picked = new Set(options.services ?? [])\n return (\n <Stack spacing={1} sx={{ pl: 2 }}>\n {!available ? (\n <Alert severity=\"info\">\n {'Live carrier rates are not available on this site right now, so checkout offers the fallback rate.'}\n </Alert>\n ) : null}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Shoppers see each carrier’s price for their address. Leave every service unticked to offer all of them.'}\n </Typography>\n <Stack direction=\"row\" sx={{ flexWrap: 'wrap', columnGap: 2 }}>\n {services.map((service) => (\n <FormControlLabel\n key={service.serviceKey}\n control={\n <Switch\n size=\"small\"\n checked={picked.has(service.serviceKey)}\n onChange={(event) =>\n set({\n services: event.target.checked\n ? [...picked, service.serviceKey]\n : [...picked].filter((key) => key !== service.serviceKey),\n })\n }\n />\n }\n label={service.label}\n />\n ))}\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Markup (%)\"\n value={options.markupPct ?? ''}\n onChange={(event) => {\n const pct = Number(event.target.value)\n set({ markupPct: Number.isFinite(pct) && pct > 0 ? Math.min(pct, 500) : 0 })\n }}\n size=\"small\"\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n label=\"Handling fee ($)\"\n value={options.handlingCents ? options.handlingCents / 100 : ''}\n onChange={(event) => {\n const dollars = Number(event.target.value)\n set({ handlingCents: Number.isFinite(dollars) && dollars > 0 ? Math.round(dollars * 100) : 0 })\n }}\n size=\"small\"\n sx={{ width: 150 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n select\n label=\"If no live rate\"\n value={options.fallbackRateId ?? ''}\n onChange={(event) => set({ fallbackRateId: event.target.value })}\n size=\"small\"\n sx={{ minWidth: 200 }}\n error={!options.fallbackRateId}\n helperText={\n options.fallbackRateId ? undefined : 'Pick a rate of this zone, or checkout can’t price without a quote'\n }\n >\n <MenuItem value=\"\">{'None'}</MenuItem>\n {fallbacks.map((fallback) => (\n <MenuItem key={fallback.id} value={fallback.id}>\n {fallback.name || 'Unnamed rate'}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n </Stack>\n )\n}\n"],"names":["Aglyn","CommerceModel","CardDisplay","useSnackbar","Alert","AlertTitle","Button","Divider","FormControlLabel","MenuItem","Stack","Switch","TextField","Typography","authorizedFetch","doc","setDoc","useCallback","useEffect","useState","useFirestore","useUser","useFirestoreDoc","writeGuardedBySeed","pluginDocsHelp","ShippingSettingsCard","props","draft","current","hostId","firestore","enqueueSnackbar","data","store","status","storeStatus","fromCache","storeFromCache","setDraft","shipping","user","carrier","setCarrier","available","services","live","encodeURIComponent","then","response","ok","json","answer","Boolean","catch","undefined","update","patch","updateZone","index","zones","removed","splice","rates","filter","rate","zoneId","id","createResourceUid","name","countries","updateRate","kind","handleSave","verdict","subject","unreadable","merge","message","variant","persist","coverage","summarizeShippingCoverage","countryNames","codes","map","code","CHECKOUT_SHIPPING_COUNTRY_NAMES","join","addRestOfWorldZone","length","usd","cents","String","raw","trim","Math","round","Number","header","help","anchor","contentGutterX","contentGutterY","spacing","zone","direction","label","value","onChange","event","target","size","sx","width","split","toUpperCase","flex","placeholder","color","onClick","alignSelf","select","error","helperText","CarrierRateFields","fallbacks","other","amountCents","slotProps","htmlInput","inputMode","freeOverCents","pl","tiers","tier","tierIndex","upTo","_item","itemIndex","disabled","control","checked","localPickup","unserved","severity","action","hasRestOfWorldZone","CHECKOUT_SHIPPING_COUNTRIES","offersLocalPickup","shipsNowhere","served","asksDestination","displayName","options","set","picked","Set","flexWrap","columnGap","service","has","serviceKey","key","markupPct","pct","isFinite","min","handlingCents","dollars","fallbackRateId","minWidth","fallback"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GACD;;;AAEA,YAAYA,WAAW,eAAc;AACrC,YAAYC,mBAAmB,uBAAa;AAC5C,SAASC,WAAW,QAAQ,uBAAsB;AAClD,SAASC,WAAW,QAAQ,8BAA6B;AACzD,SACEC,KAAK,EACLC,UAAU,EACVC,MAAM,EACNC,OAAO,EACPC,gBAAgB,EAChBC,QAAQ,EACRC,KAAK,EACLC,MAAM,EACNC,SAAS,EACTC,UAAU,QACL,gBAAe;AACtB,SAASC,eAAe,QAAQ,2CAA0C;AAC1E,SAASC,GAAG,EAAEC,MAAM,QAAQ,qBAAoB;AAChD,SAASC,WAAW,EAAEC,SAAS,EAAEC,QAAQ,QAAQ,QAAO;AACxD,SAASC,YAAY,EAAEC,OAAO,QAAQ,iCAAgC;AACtE,SACEC,eAAe,EACfC,kBAAkB,QACb,iCAAgC;AACvC,SAASC,cAAc,QAAQ,eAAc;AAM7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GACD,OAAO,SAASC,qBAAqBC,KAAgC;QAuBnBC,MAqJxCC,gBA6CAA;QAkMWA;IA1ZnB,MAAM,EAAEC,MAAM,EAAE,GAAGH;IACnB,MAAMI,YAAYV;IAClB,MAAM,EAAEW,eAAe,EAAE,GAAG5B;IAC5B,MAAM,EACJ6B,MAAMC,KAAK,EACXC,QAAQC,WAAW,EACnB;;;;;;;;;KASC,GACDC,WAAWC,cAAc,EAC1B,GAAGf,gBACF,IAAMP,IAAIe,WAAW,SAASD,QAAQ,YAAY,UAClD;QAACC;QAAWD;KAAO;IAErB,MAAM,CAACF,OAAOW,SAAS,GAAGnB,SAAgD;IAC1E,MAAMS,WAA0CD,OAAAA,gBAAAA,QAASM,yBAAAA,MAAOM,QAAQ,YAAxBZ,OAA4B,CAAC;IAC7E;;;;;GAKC,GACD,MAAM,EAAEK,MAAMQ,IAAI,EAAE,GAAGnB;IACvB,MAAM,CAACoB,SAASC,WAAW,GAAGvB,SAG3B;QAAEwB,WAAW;QAAOC,UAAU,EAAE;IAAC;IACpC1B,UAAU;QACR,IAAI2B,OAAO;QACX/B,gBAAgB0B,MAAM,CAAC,4CAA4C,EAAEM,mBAAmBjB,SAAS,EAC9FkB,IAAI,CAAC,CAACC,WAAcA,SAASC,EAAE,GAAGD,SAASE,IAAI,KAAK,MACpDH,IAAI,CAAC,CAACI;gBAC4EA;YAAjF,IAAIN,QAAQM,QAAQT,WAAW;gBAAEC,WAAWS,QAAQD,OAAOR,SAAS;gBAAGC,QAAQ,GAAEO,mBAAAA,OAAOP,QAAQ,YAAfO,mBAAmB,EAAE;YAAC;QACzG,GACCE,KAAK,CAAC,IAAMC;QACf,OAAO;YACLT,OAAO;QACT;IACF,GAAG;QAAChB;QAAQW;KAAK;IACjB,MAAMe,SAAS,CAACC,QACdlB,SAAS,aAAKV,SAAY4B;IAE5B,MAAMC,aAAa,CACjBC,OACAF;YAEmB5B;QAAnB,MAAM+B,QAAQ;gBAAK/B,iBAAAA,QAAQ+B,KAAK,YAAb/B,iBAAiB,EAAE;SAAE;QACxC,IAAI4B,UAAU,MAAM;gBAIR5B;YAHV,MAAMgC,UAAUD,MAAME,MAAM,CAACH,OAAO,EAAE,CAAC,EAAE;YACzCH,OAAO;gBACLI;gBACAG,OAAO,EAAClC,iBAAAA,QAAQkC,KAAK,YAAblC,iBAAiB,EAAE,EAAEmC,MAAM,CACjC,CAACC,OAASA,KAAKC,MAAM,MAAKL,2BAAAA,QAASM,EAAE;YAEzC;YACA;QACF;QACAP,KAAK,CAACD,MAAM,GAAG;YACbQ,IAAIlE,MAAMmE,iBAAiB;YAC3BC,MAAM;YACNC,WAAW,EAAE;WACVV,KAAK,CAACD,MAAM,EACZF;QAELD,OAAO;YAAEI;QAAM;IACjB;IAEA,MAAMW,aAAa,CACjBZ,OACAF;YAEmB5B;YAKPA,iBAAAA;QALZ,MAAMkC,QAAQ;gBAAKlC,iBAAAA,QAAQkC,KAAK,YAAblC,iBAAiB,EAAE;SAAE;QACxC,IAAI4B,UAAU,MAAMM,MAAMD,MAAM,CAACH,OAAO;aAEtCI,KAAK,CAACJ,MAAM,GAAG;YACbQ,IAAIlE,MAAMmE,iBAAiB;YAC3BF,MAAM,WAAErC,iBAAAA,QAAQ+B,KAAK,sBAAb/B,kBAAAA,cAAe,CAAC,EAAE,qBAAlBA,gBAAoBsC,EAAE,mBAAI;YAClCE,MAAM;YACNG,MAAM;WACHT,KAAK,CAACJ,MAAM,EACZF;QAEPD,OAAO;YAAEO;QAAM;IACjB;IAEA,MAAMU,aAAavD,YAAY;QAC7B;;;;;;;;;;KAUC,GACD,MAAMwD,UAAU,MAAMlD,mBACpB;YACEmD,SAAS;YACTC,YAAYxC,gBAAgB;YAC5BC,WAAWC;QACb,GACA;YACE,MAAMrB,OACJD,IAAIe,WAAW,SAASD,QAAQ,YAAY,UAC5C;gBAAEU,UAAUX;YAAQ,GACpB;gBAAEgD,OAAO;YAAK;QAElB;QAEF,uEAAuE;QACvE,uBAAuB;QACvB,IAAI,CAACH,QAAQxB,EAAE,EAAE;YACf,OAAO,KAAKlB,gBAAgB0C,QAAQI,OAAO,EAAE;gBAC3CC,SAAS;gBACTC,SAAS;YACX;QACF;QACAzC,SAAS;QACTP,gBAAgB,2BAA2B;YACzC+C,SAAS;YACTC,SAAS;QACX;IACF,GAAG;QAACnD;QAASE;QAAWD;QAAQE;QAAiBI;QAAaE;KAAe;IAE7E;;;;;GAKC,GACD,MAAM2C,WAAW/E,cAAcgF,yBAAyB,CAACrD;IACzD,MAAMsD,eAAe,CAACC,QACpBA,MACGC,GAAG,CAAC,CAACC;gBAASpF;oBAAAA,sDAAAA,cAAcqF,+BAA+B,CAACD,KAAK,YAAnDpF,sDAAuDoF;WACrEE,IAAI,CAAC;IACV;;;;;;GAMC,GACD,MAAMC,qBAAqB;;YACd5D;eAAX6B,oBAAW7B,iBAAAA,QAAQ+B,KAAK,qBAAb/B,eAAe6D,MAAM,mBAAI,GAAG;YACrCrB,MAAM;YACNC,WAAW;gBAAC;aAAI;QAClB;;IAEF,MAAMqB,MAAM,CAACC,QACXA,SAAS,OAAO,KAAKC,OAAOD,QAAQ;IACtC,MAAMA,QAAQ,CAACE,MACbA,IAAIC,IAAI,OAAO,KAAKxC,YAAYyC,KAAKC,KAAK,CAACC,OAAOJ,OAAO;IAE3D,qBACE,KAAC3F;QACCgG,QAAQ;QACRC,MAAM3E,eAAe,YAAY;YAAE4E,QAAQ;QAAmB;QAC9DC,cAAc;QACdC,cAAc;kBAEd,cAAA,MAAC5F;YAAM6F,SAAS;;8BACd,KAAC1F;oBAAWiE,SAAQ;8BAAa;;gBAChC,EAAClD,iBAAAA,QAAQ+B,KAAK,YAAb/B,iBAAiB,EAAE,EAAEwD,GAAG,CAAC,CAACoB,MAAM9C,sBAChC,MAAChD;wBAAoB+F,WAAU;wBAAMF,SAAS;;0CAC5C,KAAC3F;gCACC8F,OAAM;gCACNC,OAAOH,KAAKpC,IAAI;gCAChBwC,UAAU,CAACC,QACTpD,WAAWC,OAAO;wCAAEU,MAAMyC,MAAMC,MAAM,CAACH,KAAK;oCAAC;gCAE/CI,MAAK;gCACLC,IAAI;oCAAEC,OAAO;gCAAI;;0CAEnB,KAACrG;gCACC8F,OAAM;gCACNC,OAAOH,KAAKnC,SAAS,CAACkB,IAAI,CAAC;gCAC3BqB,UAAU,CAACC,QACTpD,WAAWC,OAAO;wCAChBW,WAAWwC,MAAMC,MAAM,CAACH,KAAK,CAC1BO,KAAK,CAAC,KACN9B,GAAG,CAAC,CAACC,OAASA,KAAKS,IAAI,GAAGqB,WAAW,IACrCpD,MAAM,CAACX;oCACZ;gCAEF2D,MAAK;gCACLC,IAAI;oCAAEI,MAAM;gCAAE;gCACdC,aAAY;;0CAEd,KAAC/G;gCACCyG,MAAK;gCACLO,OAAM;gCACNC,SAAS,IAAM9D,WAAWC,OAAO;0CAEhC;;;uBA9BO8C,KAAKtC,EAAE;8BAkCrB,KAAC5D;oBACCyG,MAAK;oBACLC,IAAI;wBAAEQ,WAAW;oBAAa;oBAC9BD,SAAS;;4BAAiB3F;+BAAX6B,oBAAW7B,iBAAAA,QAAQ+B,KAAK,qBAAb/B,eAAe6D,MAAM,mBAAI,GAAG,CAAC;;8BAEtD;;8BAGH,KAAClF;8BACD,KAACM;oBAAWiE,SAAQ;8BAAa;;gBAChC,EAAClD,iBAAAA,QAAQkC,KAAK,YAAblC,iBAAiB,EAAE,EAAEwD,GAAG,CAAC,CAACpB,MAAMN;wBAaxB9B,gBA+DUA,gBAqCVoC;yCAhHR,MAACtD;wBAAoB6F,SAAS;;0CAC5B,MAAC7F;gCAAM+F,WAAU;gCAAMF,SAAS;;kDAC9B,KAAC3F;wCACC8F,OAAM;wCACNC,OAAO3C,KAAKC,MAAM;wCAClB2C,UAAU,CAACC,QACTvC,WAAWZ,OAAO;gDAAEO,QAAQ4C,MAAMC,MAAM,CAACH,KAAK;4CAAC;wCAEjDI,MAAK;wCACLU,MAAM;wCACNT,IAAI;4CAAEC,OAAO;wCAAI;kDAEhB,EAACrF,iBAAAA,QAAQ+B,KAAK,YAAb/B,iBAAiB,EAAE,EAAEwD,GAAG,CAAC,CAACoB,qBAC1B,KAAC/F;gDAAuBkG,OAAOH,KAAKtC,EAAE;0DACnCsC,KAAKpC,IAAI,IAAIoC,KAAKnC,SAAS,CAACkB,IAAI,CAAC;+CADrBiB,KAAKtC,EAAE;;kDAK1B,KAACtD;wCACC8F,OAAM;wCACNC,OAAO3C,KAAKI,IAAI;wCAChBwC,UAAU,CAACC,QACTvC,WAAWZ,OAAO;gDAAEU,MAAMyC,MAAMC,MAAM,CAACH,KAAK;4CAAC;wCAE/CI,MAAK;wCACLC,IAAI;4CAAEI,MAAM;wCAAE;wCACdC,aAAY;wCACZ;;;;;;;;;iBASC,GACDK,OAAO,CAAC1D,KAAKI,IAAI,CAAC0B,IAAI;wCACtB6B,YACE3D,KAAKI,IAAI,CAAC0B,IAAI,KAAKxC,YAAY;;kDAGnC,MAAC1C;wCACC8F,OAAM;wCACNC,OAAO3C,KAAKO,IAAI;wCAChBqC,UAAU,CAACC,QACTvC,WAAWZ,OAAO;gDAChBa,MAAMsC,MAAMC,MAAM,CAACH,KAAK;4CAC1B;wCAEFI,MAAK;wCACLU,MAAM;wCACNT,IAAI;4CAAEC,OAAO;wCAAI;;0DAEjB,KAACxG;gDAASkG,OAAM;0DAAQ;;0DACxB,KAAClG;gDAASkG,OAAM;0DAAa;;0DAC7B,KAAClG;gDAASkG,OAAM;0DAAe;;0DAC/B,KAAClG;gDAASkG,OAAM;0DAAgB;;4CAC/BlE,QAAQE,SAAS,IAAIqB,KAAKO,IAAI,KAAK,0BAClC,KAAC9D;gDAASkG,OAAM;0DAAW;iDACzB;;;kDAEN,KAACrG;wCACCyG,MAAK;wCACLO,OAAM;wCACNC,SAAS,IAAMjD,WAAWZ,OAAO;kDAEhC;;;;4BAGJM,KAAKO,IAAI,KAAK,0BACb,KAACqD;gCACC5D,MAAMA;gCACNpB,UAAUH,QAAQG,QAAQ;gCAC1BD,WAAWF,QAAQE,SAAS;gCAC5BkF,WAAW,EAACjG,iBAAAA,QAAQkC,KAAK,YAAblC,iBAAiB,EAAE,EAAEmC,MAAM,CACrC,CAAC+D,QACCA,MAAM5D,EAAE,KAAKF,KAAKE,EAAE,IACpB4D,MAAM7D,MAAM,KAAKD,KAAKC,MAAM,IAC5B6D,MAAMvD,IAAI,KAAK;gCAEnBqC,UAAU,CAACpD,QAAUc,WAAWZ,OAAOF;iCAEvCQ,KAAKO,IAAI,KAAK,UAAUP,KAAKO,IAAI,KAAK,4BACxC,MAAC7D;gCAAM+F,WAAU;gCAAMF,SAAS;;kDAC9B,KAAC3F;wCACC8F,OAAM;wCACNC,OAAOjB,IAAI1B,KAAK+D,WAAW;wCAC3BnB,UAAU,CAACC,QACTvC,WAAWZ,OAAO;gDAAEqE,aAAapC,MAAMkB,MAAMC,MAAM,CAACH,KAAK;4CAAE;wCAE7DI,MAAK;wCACLC,IAAI;4CAAEC,OAAO;wCAAI;wCACjBe,WAAW;4CAAEC,WAAW;gDAAEC,WAAW;4CAAU;wCAAE;;oCAElDlE,KAAKO,IAAI,KAAK,4BACb,KAAC3D;wCACC8F,OAAM;wCACNC,OAAOjB,IAAI1B,KAAKmE,aAAa;wCAC7BvB,UAAU,CAACC,QACTvC,WAAWZ,OAAO;gDAChByE,eAAexC,MAAMkB,MAAMC,MAAM,CAACH,KAAK;4CACzC;wCAEFI,MAAK;wCACLC,IAAI;4CAAEC,OAAO;wCAAI;wCACjBe,WAAW;4CAAEC,WAAW;gDAAEC,WAAW;4CAAU;wCAAE;yCAEjD;;+CAGN,MAACxH;gCAAM6F,SAAS;gCAAGS,IAAI;oCAAEoB,IAAI;gCAAE;;oCAC5B,EAACpE,cAAAA,KAAKqE,KAAK,YAAVrE,cAAc,EAAE,EAAEoB,GAAG,CAAC,CAACkD,MAAMC,0BAC7B,MAAC7H;4CAAsB+F,WAAU;4CAAMF,SAAS;;8DAC9C,KAAC3F;oDACC8F,OACE1C,KAAKO,IAAI,KAAK,iBACV,cACA;oDAENoC,OACE3C,KAAKO,IAAI,KAAK,iBACV+D,KAAKE,IAAI,GACTF,KAAKE,IAAI,GAAG;oDAElB5B,UAAU,CAACC;4DACU7C;wDAAnB,MAAMqE,QAAQ;gEAAKrE,cAAAA,KAAKqE,KAAK,YAAVrE,cAAc,EAAE;yDAAE;wDACrC,MAAM2C,QAAQV,OAAOY,MAAMC,MAAM,CAACH,KAAK,KAAK;wDAC5C0B,KAAK,CAACE,UAAU,GAAG,aACdD;4DACHE,MACExE,KAAKO,IAAI,KAAK,iBACVwB,KAAKC,KAAK,CAACW,SACXZ,KAAKC,KAAK,CAACW,QAAQ;;wDAE3BrC,WAAWZ,OAAO;4DAAE2E;wDAAM;oDAC5B;oDACAtB,MAAK;oDACLC,IAAI;wDAAEC,OAAO;oDAAI;oDACjBe,WAAW;wDAAEC,WAAW;4DAAEC,WAAW;wDAAU;oDAAE;;8DAEnD,KAACtH;oDACC8F,OAAM;oDACNC,OAAO2B,KAAKP,WAAW,GAAG;oDAC1BnB,UAAU,CAACC;4DACU7C;wDAAnB,MAAMqE,QAAQ;gEAAKrE,cAAAA,KAAKqE,KAAK,YAAVrE,cAAc,EAAE;yDAAE;wDACrCqE,KAAK,CAACE,UAAU,GAAG,aACdD;4DACHP,aAAahC,KAAKC,KAAK,CACrB,AAACC,CAAAA,OAAOY,MAAMC,MAAM,CAACH,KAAK,KAAK,CAAA,IAAK;;wDAGxCrC,WAAWZ,OAAO;4DAAE2E;wDAAM;oDAC5B;oDACAtB,MAAK;oDACLC,IAAI;wDAAEC,OAAO;oDAAI;oDACjBe,WAAW;wDAAEC,WAAW;4DAAEC,WAAW;wDAAU;oDAAE;;8DAEnD,KAAC5H;oDACCyG,MAAK;oDACLO,OAAM;oDACNC,SAAS;4DAEGvD;+DADVM,WAAWZ,OAAO;4DAChB2E,OAAO,EAACrE,cAAAA,KAAKqE,KAAK,YAAVrE,cAAc,EAAE,EAAED,MAAM,CAC9B,CAAC0E,OAAOC,YAAcA,cAAcH;wDAExC;;8DAGD;;;2CAxDOA;kDA4Dd,KAACjI;wCACCyG,MAAK;wCACLC,IAAI;4CAAEQ,WAAW;wCAAa;wCAC9BD,SAAS;gDAGCvD;mDAFRM,WAAWZ,OAAO;gDAChB2E,OAAO;wDACDrE,cAAAA,KAAKqE,KAAK,YAAVrE,cAAc,EAAE;oDACpB;wDAAEwE,MAAM;wDAAGT,aAAa;oDAAE;iDAC3B;4CACH;;kDAGD;;;;;uBAzLG/D,KAAKE,EAAE;;8BA+LrB,KAAC5D;oBACCyG,MAAK;oBACL4B,UAAU,WAAC/G,kBAAAA,QAAQ+B,KAAK,qBAAb/B,gBAAe6D,MAAM,oBAAI,OAAO;oBAC3CuB,IAAI;wBAAEQ,WAAW;oBAAa;oBAC9BD,SAAS;;4BAAiB3F;+BAAX0C,oBAAW1C,iBAAAA,QAAQkC,KAAK,qBAAblC,eAAe6D,MAAM,mBAAI,GAAG,CAAC;;8BAEtD;;8BAGH,MAAC/E;oBAAM6F,SAAS;;sCACd,KAAC/F;4BACCoI,uBACE,KAACjI;gCACCoG,MAAK;gCACL8B,SAASzF,QAAQxB,QAAQkH,WAAW;gCACpClC,UAAU,CAACC,QACTtD,OAAO;wCAAEuF,aAAajC,MAAMC,MAAM,CAAC+B,OAAO;oCAAC;;4BAIjDnC,OAAM;;sCAUR,KAAC7F;4BAAWiE,SAAQ;4BAAUwC,OAAM;sCAEhC,mEACA,kEACA,oEACA,iEACA;;;;8BAKN,KAAC/G;8BACD,KAACM;oBAAWiE,SAAQ;8BAAa;;gBAChCE,SAAS+D,QAAQ,CAACtD,MAAM,GAAG,kBAC1B,MAACrF;oBACC4I,UAAS;oBACTC,QACEjE,SAASkE,kBAAkB,GAAG5F,0BAC5B,KAAChD;wBACCyG,MAAK;wBACLO,OAAM;wBACNC,SAAS/B;kCAER;;;sCAKP,KAACnF;sCACE,CAAC,oBAAoB,EAAE2E,SAAS+D,QAAQ,CAACtD,MAAM,CAAC,QAAQ,EAAExF,cAAckJ,2BAA2B,CAAC1D,MAAM,CAAC,uCAAuC,CAAC;;wBAErJ,CAAC,gBAAgB,EAAEP,aAAaF,SAAS+D,QAAQ,EAAE,qEAAqE,CAAC;wBACzH/D,SAASoE,iBAAiB,GACvB,gJACA;wBACHpE,SAASkE,kBAAkB,GACxB,0HACA;;mCAGN,KAACrI;oBAAWiE,SAAQ;oBAAUwC,OAAM;8BACjCtC,SAASqE,YAAY,GAClBrE,SAASoE,iBAAiB,GACxB,oJACA,4FACF,CAAC,6EAA6E,EAAElE,aAAaF,SAASsE,MAAM,EAAE,CAAC,CAAC;;gBAGvHtE,SAASuE,eAAe,iBACvB,KAAC1I;oBAAWiE,SAAQ;oBAAUwC,OAAM;8BAEhC;qBAGF;8BAEJ,KAAChH;oBACCwE,SAAQ;oBACRwC,OAAM;oBACNP,MAAK;oBACL4B,UAAU,CAAChH;oBACX4F,SAAS/C;oBACTwC,IAAI;wBAAEQ,WAAW;oBAAa;8BAE7B;;;;;AAKX;AACA/F,qBAAqB+H,WAAW,GAAG;AAEnC,eAAe/H,qBAAoB;AAWnC;;;CAGC,GACD,SAASmG,kBAAkBlG,KAA6B;QAEtCsC,eAGOyF,mBAmCRA,oBAuBAA;IA9Df,MAAM,EAAEzF,IAAI,EAAEpB,QAAQ,EAAED,SAAS,EAAEkF,SAAS,EAAEjB,QAAQ,EAAE,GAAGlF;IAC3D,MAAM+H,WAAUzF,gBAAAA,KAAKvB,OAAO,YAAZuB,gBAAgB,CAAC;IACjC,MAAM0F,MAAM,CAAClG,QACXoD,SAAS;YAAEnE,SAAS,aAAKgH,SAAYjG;QAAQ;IAC/C,MAAMmG,SAAS,IAAIC,KAAIH,oBAAAA,QAAQ7G,QAAQ,YAAhB6G,oBAAoB,EAAE;IAC7C,qBACE,MAAC/I;QAAM6F,SAAS;QAAGS,IAAI;YAAEoB,IAAI;QAAE;;YAC5B,CAACzF,0BACA,KAACvC;gBAAM4I,UAAS;0BACb;iBAED;0BACJ,KAACnI;gBAAWiE,SAAQ;gBAAUwC,OAAM;0BACjC;;0BAEH,KAAC5G;gBAAM+F,WAAU;gBAAMO,IAAI;oBAAE6C,UAAU;oBAAQC,WAAW;gBAAE;0BACzDlH,SAASwC,GAAG,CAAC,CAAC2E,wBACb,KAACvJ;wBAECoI,uBACE,KAACjI;4BACCoG,MAAK;4BACL8B,SAASc,OAAOK,GAAG,CAACD,QAAQE,UAAU;4BACtCrD,UAAU,CAACC,QACT6C,IAAI;oCACF9G,UAAUiE,MAAMC,MAAM,CAAC+B,OAAO,GAC1B;2CAAIc;wCAAQI,QAAQE,UAAU;qCAAC,GAC/B;2CAAIN;qCAAO,CAAC5F,MAAM,CAAC,CAACmG,MAAQA,QAAQH,QAAQE,UAAU;gCAC5D;;wBAINvD,OAAOqD,QAAQrD,KAAK;uBAdfqD,QAAQE,UAAU;;0BAkB7B,MAACvJ;gBAAM+F,WAAU;gBAAMF,SAAS;;kCAC9B,KAAC3F;wBACC8F,OAAM;wBACNC,KAAK,GAAE8C,qBAAAA,QAAQU,SAAS,YAAjBV,qBAAqB;wBAC5B7C,UAAU,CAACC;4BACT,MAAMuD,MAAMnE,OAAOY,MAAMC,MAAM,CAACH,KAAK;4BACrC+C,IAAI;gCAAES,WAAWlE,OAAOoE,QAAQ,CAACD,QAAQA,MAAM,IAAIrE,KAAKuE,GAAG,CAACF,KAAK,OAAO;4BAAE;wBAC5E;wBACArD,MAAK;wBACLC,IAAI;4BAAEC,OAAO;wBAAI;wBACjBe,WAAW;4BAAEC,WAAW;gCAAEC,WAAW;4BAAU;wBAAE;;kCAEnD,KAACtH;wBACC8F,OAAM;wBACNC,OAAO8C,QAAQc,aAAa,GAAGd,QAAQc,aAAa,GAAG,MAAM;wBAC7D3D,UAAU,CAACC;4BACT,MAAM2D,UAAUvE,OAAOY,MAAMC,MAAM,CAACH,KAAK;4BACzC+C,IAAI;gCAAEa,eAAetE,OAAOoE,QAAQ,CAACG,YAAYA,UAAU,IAAIzE,KAAKC,KAAK,CAACwE,UAAU,OAAO;4BAAE;wBAC/F;wBACAzD,MAAK;wBACLC,IAAI;4BAAEC,OAAO;wBAAI;wBACjBe,WAAW;4BAAEC,WAAW;gCAAEC,WAAW;4BAAU;wBAAE;;kCAEnD,MAACtH;wBACC6G,MAAM;wBACNf,OAAM;wBACNC,KAAK,GAAE8C,0BAAAA,QAAQgB,cAAc,YAAtBhB,0BAA0B;wBACjC7C,UAAU,CAACC,QAAU6C,IAAI;gCAAEe,gBAAgB5D,MAAMC,MAAM,CAACH,KAAK;4BAAC;wBAC9DI,MAAK;wBACLC,IAAI;4BAAE0D,UAAU;wBAAI;wBACpBhD,OAAO,CAAC+B,QAAQgB,cAAc;wBAC9B9C,YACE8B,QAAQgB,cAAc,GAAGnH,YAAY;;0CAGvC,KAAC7C;gCAASkG,OAAM;0CAAI;;4BACnBkB,UAAUzC,GAAG,CAAC,CAACuF,yBACd,KAAClK;oCAA2BkG,OAAOgE,SAASzG,EAAE;8CAC3CyG,SAASvG,IAAI,IAAI;mCADLuG,SAASzG,EAAE;;;;;;;AAQtC"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/shipping-settings-card.component.tsx"],"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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport { CardDisplay } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Alert,\n AlertTitle,\n Button,\n Divider,\n FormControlLabel,\n MenuItem,\n Stack,\n Switch,\n TextField,\n Typography,\n} from '@mui/material'\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport { doc, setDoc } from 'firebase/firestore'\nimport { useCallback, useEffect, useState } from 'react'\nimport { useFirestore, useUser } from '@aglyn/tenant-feature-instance'\nimport {\n useFirestoreDoc,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\n\nexport interface ShippingSettingsCardProps {\n hostId: string\n}\n\n/**\n * Shipping settings (AGL-288): zones (countries, '*' = rest of world)\n * and rates (flat / free-over / subtotal or weight tiers), stored on\n * `hosts/{hostId}/settings/store` under `shipping`. Cart checkout (AGL-1707)\n * and buy-now (AGL-1720) both read this document and declare the rates as\n * Stripe `shipping_options` (`planCheckoutShipping`) — until those landed\n * nothing read it, and everything saved here was charged to nobody. Buy-now\n * resolves only for physical one-time sales; draft orders and POS still\n * resolve no shipping at all.\n *\n * WHAT A ZONE LAYOUT COSTS THE SHOPPER (AGL-1721). A session's rates are\n * charged as picked and never re-checked against the address, so the checkout\n * can only offer rates that hold for every country it will ship to. Saving\n * zones whose rates DIFFER therefore makes the storefront ask the shopper\n * where the parcel is going before it can price one, and a destination no\n * zone covers is refused outright rather than shipped for free. One zone, or\n * one '*' zone, needs neither: the shopper is asked nothing.\n *\n * AND THE MERCHANT HAS TO BE ABLE TO SEE IT (AGL-1791). Both consequences\n * were invisible here: a merchant saved zones and the checkouts they refused\n * were refused with nothing on this page saying so, which is a support ticket\n * from someone who did nothing wrong. The coverage block reads\n * `summarizeShippingCoverage` off the same `current` object the save writes,\n * so it moves as the form is typed rather than after a round trip.\n *\n * It escalates to a warning ONLY for a merchant who priced some destinations\n * and not others. A merchant whose rates reach nowhere is not misconfigured —\n * that store charges no shipping and refuses nobody — and warning them would\n * nag every store selling downloads.\n */\nexport function ShippingSettingsCard(props: ShippingSettingsCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const {\n data: store,\n status: storeStatus,\n /**\n * The settings doc this form is seeded from is unconfirmed by the server\n * (AGL-1358). `current` is `draft ?? store?.shipping ?? {}` and the save\n * writes `{shipping: current}`, so `merge: true` protects the doc's other\n * maps and nothing inside this one — `zones` and `rates` are arrays, and\n * a merge replaces an array wholesale. Against a cached read, adding one\n * rate restores that snapshot's whole price table, and a zone that\n * disappears with it means `resolveShippingRates` matches nothing, which\n * is a checkout offering no shipping option at all.\n */\n fromCache: storeFromCache,\n } = useFirestoreDoc<any>(\n () => doc(firestore, 'hosts', hostId, 'settings', 'store'),\n [firestore, hostId],\n )\n const [draft, setDraft] = useState<CommerceModel.ShippingSettings | null>(null)\n const current: CommerceModel.ShippingSettings = draft ?? store?.shipping ?? {}\n /*\n * Live carrier rates (AGL-3612) are offered only where a plugin quotes\n * carriers for this site — the server asks core's quoter seam. A rate\n * already saved as `carrier` stays editable either way, so a merchant can\n * still see and change what checkout falls back to.\n */\n const { data: user } = useUser()\n const [carrier, setCarrier] = useState<{\n available: boolean\n services: Array<{ serviceKey: string; label: string }>\n }>({ available: false, services: [] })\n useEffect(() => {\n let live = true\n authorizedFetch(user, `/api/commerce/shipping/carrier-rates?hostId=${encodeURIComponent(hostId)}`)\n .then((response) => (response.ok ? response.json() : null))\n .then((answer) => {\n if (live && answer) setCarrier({ available: Boolean(answer.available), services: answer.services ?? [] })\n })\n .catch(() => undefined)\n return () => {\n live = false\n }\n }, [hostId, user])\n const update = (patch: Partial<CommerceModel.ShippingSettings>) =>\n setDraft({ ...current, ...patch })\n\n const updateZone = (\n index: number,\n patch: Partial<CommerceModel.ShippingZone> | null,\n ) => {\n const zones = [...(current.zones ?? [])]\n if (patch === null) {\n const removed = zones.splice(index, 1)[0]\n update({\n zones,\n rates: (current.rates ?? []).filter(\n (rate) => rate.zoneId !== removed?.id,\n ),\n })\n return\n }\n zones[index] = {\n id: Aglyn.createResourceUid(),\n name: '',\n countries: [],\n ...zones[index],\n ...patch,\n }\n update({ zones })\n }\n\n const updateRate = (\n index: number,\n patch: Partial<CommerceModel.ShippingRate> | null,\n ) => {\n const rates = [...(current.rates ?? [])]\n if (patch === null) rates.splice(index, 1)\n else\n rates[index] = {\n id: Aglyn.createResourceUid(),\n zoneId: current.zones?.[0]?.id ?? '',\n name: '',\n kind: 'flat',\n ...rates[index],\n ...patch,\n }\n update({ rates })\n }\n\n const handleSave = useCallback(async () => {\n /**\n * Refuse a save whose seed the server never confirmed (AGL-1358).\n *\n * No create path to exempt: this is a FIXED document path, so the empty\n * `{}` that `store?.shipping ?? {}` yields when the cache has never seen\n * the map is not the harmless blank a fresh uid would be — it replaces\n * the real zones and rates with nothing.\n *\n * The guard WRAPS the write — an early return is a shape you can keep\n * while losing the protection.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'shipping settings',\n unreadable: storeStatus === 'error',\n fromCache: storeFromCache,\n },\n async () => {\n await setDoc(\n doc(firestore, 'hosts', hostId, 'settings', 'store'),\n { shipping: current },\n { merge: true },\n )\n },\n )\n // Before `setDraft(null)`, so a refusal keeps every zone and rate that\n // was typed on screen.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setDraft(null)\n enqueueSnackbar('Shipping settings saved', {\n variant: 'success',\n persist: false,\n })\n }, [current, firestore, hostId, enqueueSnackbar, storeStatus, storeFromCache])\n\n /**\n * Recomputed from `current` on every render, so the block answers for what\n * is typed rather than for what was last saved — a merchant deleting the\n * zone that covered France should see France go uncovered before they\n * commit it, not after.\n */\n const coverage = CommerceModel.summarizeShippingCoverage(current)\n const words = CommerceModel.describeShippingCoverage(coverage)\n /**\n * The one-click half of the fix: a zone claiming everywhere the merchant\n * has not named. It adds the ZONE only — a rate on it prices the parcel,\n * and choosing that price is the merchant's call, not ours. A rate row\n * defaulted here would post at whatever we picked, and a blank flat rate\n * resolves to free.\n */\n const addRestOfWorldZone = () =>\n updateZone(current.zones?.length ?? 0, {\n name: 'Rest of world',\n countries: ['*'],\n })\n\n const usd = (cents: number | undefined) =>\n cents == null ? '' : String(cents / 100)\n const cents = (raw: string) =>\n raw.trim() === '' ? undefined : Math.round(Number(raw) * 100)\n\n return (\n <CardDisplay\n header={'Shipping'}\n help={pluginDocsHelp('shipping', { anchor: '#zones-and-rates' })}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1.5}>\n <Typography variant=\"subtitle2\">{'Zones'}</Typography>\n {(current.zones ?? []).map((zone, index) => (\n <Stack key={zone.id} direction=\"row\" spacing={1}>\n <TextField\n label=\"Zone name\"\n value={zone.name}\n onChange={(event) =>\n updateZone(index, { name: event.target.value })\n }\n size=\"small\"\n sx={{ width: 180 }}\n />\n <TextField\n label=\"Countries\"\n value={zone.countries.join(', ')}\n onChange={(event) =>\n updateZone(index, {\n countries: event.target.value\n .split(',')\n .map((code) => code.trim().toUpperCase())\n .filter(Boolean),\n })\n }\n size=\"small\"\n sx={{ flex: 1 }}\n placeholder=\"US, CA — or * for everywhere else\"\n />\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() => updateZone(index, null)}\n >\n {'✕'}\n </Button>\n </Stack>\n ))}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() => updateZone(current.zones?.length ?? 0, {})}\n >\n {'Add zone'}\n </Button>\n\n <Divider />\n <Typography variant=\"subtitle2\">{'Rates'}</Typography>\n {(current.rates ?? []).map((rate, index) => (\n <Stack key={rate.id} spacing={1}>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Zone\"\n value={rate.zoneId}\n onChange={(event) =>\n updateRate(index, { zoneId: event.target.value })\n }\n size=\"small\"\n select\n sx={{ width: 150 }}\n >\n {(current.zones ?? []).map((zone) => (\n <MenuItem key={zone.id} value={zone.id}>\n {zone.name || zone.countries.join(',')}\n </MenuItem>\n ))}\n </TextField>\n <TextField\n label=\"Name\"\n value={rate.name}\n onChange={(event) =>\n updateRate(index, { name: event.target.value })\n }\n size=\"small\"\n sx={{ flex: 1 }}\n placeholder=\"Standard\"\n /**\n * A rate with no name is DROPPED at checkout, silently\n * (AGL-1791): Stripe requires a `display_name`, so\n * `resolveCheckoutShippingOptions` skips the row rather than\n * emit a session Stripe would reject. Saved blank it looks\n * like a priced rate on this page and is not one anywhere\n * else, which is the same invisibility the coverage block\n * below exists to end — said on the row that causes it,\n * because the block can only report the destination it costs.\n */\n error={!rate.name.trim()}\n helperText={\n rate.name.trim() ? undefined : 'Needed — checkout drops it'\n }\n />\n <TextField\n label=\"Type\"\n value={rate.kind}\n onChange={(event) =>\n updateRate(index, {\n kind: event.target.value as CommerceModel.ShippingRate['kind'],\n })\n }\n size=\"small\"\n select\n sx={{ width: 170 }}\n >\n <MenuItem value=\"flat\">{'Flat'}</MenuItem>\n <MenuItem value=\"free_over\">{'Free over subtotal'}</MenuItem>\n <MenuItem value=\"price_tiers\">{'Subtotal tiers'}</MenuItem>\n <MenuItem value=\"weight_tiers\">{'Weight tiers'}</MenuItem>\n {carrier.available || rate.kind === 'carrier' ? (\n <MenuItem value=\"carrier\">{'Carrier rates'}</MenuItem>\n ) : null}\n </TextField>\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() => updateRate(index, null)}\n >\n {'✕'}\n </Button>\n </Stack>\n {rate.kind === 'carrier' ? (\n <CarrierRateFields\n rate={rate}\n services={carrier.services}\n available={carrier.available}\n fallbacks={(current.rates ?? []).filter(\n (other) =>\n other.id !== rate.id &&\n other.zoneId === rate.zoneId &&\n other.kind !== 'carrier',\n )}\n onChange={(patch) => updateRate(index, patch)}\n />\n ) : rate.kind === 'flat' || rate.kind === 'free_over' ? (\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Price ($)\"\n value={usd(rate.amountCents)}\n onChange={(event) =>\n updateRate(index, { amountCents: cents(event.target.value) })\n }\n size=\"small\"\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n {rate.kind === 'free_over' ? (\n <TextField\n label=\"Free at subtotal ($)\"\n value={usd(rate.freeOverCents)}\n onChange={(event) =>\n updateRate(index, {\n freeOverCents: cents(event.target.value),\n })\n }\n size=\"small\"\n sx={{ width: 160 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n ) : null}\n </Stack>\n ) : (\n <Stack spacing={1} sx={{ pl: 2 }}>\n {(rate.tiers ?? []).map((tier, tierIndex) => (\n <Stack key={tierIndex} direction=\"row\" spacing={1}>\n <TextField\n label={\n rate.kind === 'weight_tiers'\n ? 'Up to (g)'\n : 'Up to subtotal ($)'\n }\n value={\n rate.kind === 'weight_tiers'\n ? tier.upTo\n : tier.upTo / 100\n }\n onChange={(event) => {\n const tiers = [...(rate.tiers ?? [])]\n const value = Number(event.target.value) || 0\n tiers[tierIndex] = {\n ...tier,\n upTo:\n rate.kind === 'weight_tiers'\n ? Math.round(value)\n : Math.round(value * 100),\n }\n updateRate(index, { tiers })\n }}\n size=\"small\"\n sx={{ width: 160 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n label=\"Price ($)\"\n value={tier.amountCents / 100}\n onChange={(event) => {\n const tiers = [...(rate.tiers ?? [])]\n tiers[tierIndex] = {\n ...tier,\n amountCents: Math.round(\n (Number(event.target.value) || 0) * 100,\n ),\n }\n updateRate(index, { tiers })\n }}\n size=\"small\"\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <Button\n size=\"small\"\n color=\"error\"\n onClick={() =>\n updateRate(index, {\n tiers: (rate.tiers ?? []).filter(\n (_item, itemIndex) => itemIndex !== tierIndex,\n ),\n })\n }\n >\n {'✕'}\n </Button>\n </Stack>\n ))}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n updateRate(index, {\n tiers: [\n ...(rate.tiers ?? []),\n { upTo: 0, amountCents: 0 },\n ],\n })\n }\n >\n {'Add tier'}\n </Button>\n </Stack>\n )}\n </Stack>\n ))}\n <Button\n size=\"small\"\n disabled={(current.zones?.length ?? 0) === 0}\n sx={{ alignSelf: 'flex-start' }}\n onClick={() => updateRate(current.rates?.length ?? 0, {})}\n >\n {'Add rate'}\n </Button>\n\n <Stack spacing={0}>\n <FormControlLabel\n control={\n <Switch\n size=\"small\"\n checked={Boolean(current.localPickup)}\n onChange={(event) =>\n update({ localPickup: event.target.checked })\n }\n />\n }\n label=\"Offer free local pickup\"\n />\n {/*\n What the toggle does NOT do, said on the toggle (AGL-2325). It\n used to silently switch off every \"we don't ship there\" refusal,\n because pickup resolved for every destination and checkout read a\n non-empty list as coverage. It no longer does — but a merchant\n reading one line of label still has to be told that turning\n collection on is not the same as adding a destination.\n */}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {\n 'A free collection choice at checkout. It does not widen where ' +\n 'you ship — a destination none of your rates reaches is still ' +\n 'turned away. To let buyers choose WHERE to collect, with hours ' +\n 'and a ready-for-pickup email, turn on pickup for a location ' +\n 'under Inventory locations; this choice then gives way to it.'\n }\n </Typography>\n </Stack>\n\n <Divider />\n <Typography variant=\"subtitle2\">{'Coverage'}</Typography>\n {coverage.unserved.length > 0 ? (\n <Alert\n severity=\"warning\"\n action={\n coverage.hasRestOfWorldZone ? undefined : (\n <Button\n size=\"small\"\n color=\"inherit\"\n onClick={addRestOfWorldZone}\n >\n {'Add rest-of-world zone'}\n </Button>\n )\n }\n >\n <AlertTitle>{words.headline}</AlertTitle>\n {words.detail}\n </Alert>\n ) : (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {words.note}\n </Typography>\n )}\n {words.asks ? (\n <Typography variant=\"caption\" color=\"text.secondary\">\n {words.asks}\n </Typography>\n ) : null}\n\n <Button\n variant=\"contained\"\n color=\"primary\"\n size=\"small\"\n disabled={!draft}\n onClick={handleSave}\n sx={{ alignSelf: 'flex-start' }}\n >\n {'Save shipping settings'}\n </Button>\n </Stack>\n </CardDisplay>\n )\n}\nShippingSettingsCard.displayName = 'ShippingSettingsCard'\n\nexport default ShippingSettingsCard\n\ninterface CarrierRateFieldsProps {\n rate: CommerceModel.ShippingRate\n services: Array<{ serviceKey: string; label: string }>\n available: boolean\n /** The zone's other rates, any of which checkout may fall back to. */\n fallbacks: CommerceModel.ShippingRate[]\n onChange: (patch: Partial<CommerceModel.ShippingRate>) => void\n}\n\n/**\n * A carrier rate's options (AGL-3612): which services checkout offers, what\n * is added to each quote, and the zone's rate offered when no live quote is.\n */\nfunction CarrierRateFields(props: CarrierRateFieldsProps) {\n const { rate, services, available, fallbacks, onChange } = props\n const options = rate.carrier ?? {}\n const set = (patch: Partial<CommerceModel.CarrierRateSettings>) =>\n onChange({ carrier: { ...options, ...patch } })\n const picked = new Set(options.services ?? [])\n return (\n <Stack spacing={1} sx={{ pl: 2 }}>\n {!available ? (\n <Alert severity=\"info\">\n {'Live carrier rates are not available on this site right now, so checkout offers the fallback rate.'}\n </Alert>\n ) : null}\n <Typography variant=\"caption\" color=\"text.secondary\">\n {'Shoppers see each carrier’s price for their address. Leave every service unticked to offer all of them.'}\n </Typography>\n <Stack direction=\"row\" sx={{ flexWrap: 'wrap', columnGap: 2 }}>\n {services.map((service) => (\n <FormControlLabel\n key={service.serviceKey}\n control={\n <Switch\n size=\"small\"\n checked={picked.has(service.serviceKey)}\n onChange={(event) =>\n set({\n services: event.target.checked\n ? [...picked, service.serviceKey]\n : [...picked].filter((key) => key !== service.serviceKey),\n })\n }\n />\n }\n label={service.label}\n />\n ))}\n </Stack>\n <Stack direction=\"row\" spacing={1}>\n <TextField\n label=\"Markup (%)\"\n value={options.markupPct ?? ''}\n onChange={(event) => {\n const pct = Number(event.target.value)\n set({ markupPct: Number.isFinite(pct) && pct > 0 ? Math.min(pct, 500) : 0 })\n }}\n size=\"small\"\n sx={{ width: 120 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n label=\"Handling fee ($)\"\n value={options.handlingCents ? options.handlingCents / 100 : ''}\n onChange={(event) => {\n const dollars = Number(event.target.value)\n set({ handlingCents: Number.isFinite(dollars) && dollars > 0 ? Math.round(dollars * 100) : 0 })\n }}\n size=\"small\"\n sx={{ width: 150 }}\n slotProps={{ htmlInput: { inputMode: 'decimal' } }}\n />\n <TextField\n select\n label=\"If no live rate\"\n value={options.fallbackRateId ?? ''}\n onChange={(event) => set({ fallbackRateId: event.target.value })}\n size=\"small\"\n sx={{ minWidth: 200 }}\n error={!options.fallbackRateId}\n helperText={\n options.fallbackRateId ? undefined : 'Pick a rate of this zone, or checkout can’t price without a quote'\n }\n >\n <MenuItem value=\"\">{'None'}</MenuItem>\n {fallbacks.map((fallback) => (\n <MenuItem key={fallback.id} value={fallback.id}>\n {fallback.name || 'Unnamed rate'}\n </MenuItem>\n ))}\n </TextField>\n </Stack>\n </Stack>\n )\n}\n"],"names":["Aglyn","CommerceModel","CardDisplay","useSnackbar","Alert","AlertTitle","Button","Divider","FormControlLabel","MenuItem","Stack","Switch","TextField","Typography","authorizedFetch","doc","setDoc","useCallback","useEffect","useState","useFirestore","useUser","useFirestoreDoc","writeGuardedBySeed","pluginDocsHelp","ShippingSettingsCard","props","draft","current","hostId","firestore","enqueueSnackbar","data","store","status","storeStatus","fromCache","storeFromCache","setDraft","shipping","user","carrier","setCarrier","available","services","live","encodeURIComponent","then","response","ok","json","answer","Boolean","catch","undefined","update","patch","updateZone","index","zones","removed","splice","rates","filter","rate","zoneId","id","createResourceUid","name","countries","updateRate","kind","handleSave","verdict","subject","unreadable","merge","message","variant","persist","coverage","summarizeShippingCoverage","words","describeShippingCoverage","addRestOfWorldZone","length","usd","cents","String","raw","trim","Math","round","Number","header","help","anchor","contentGutterX","contentGutterY","spacing","map","zone","direction","label","value","onChange","event","target","size","sx","width","join","split","code","toUpperCase","flex","placeholder","color","onClick","alignSelf","select","error","helperText","CarrierRateFields","fallbacks","other","amountCents","slotProps","htmlInput","inputMode","freeOverCents","pl","tiers","tier","tierIndex","upTo","_item","itemIndex","disabled","control","checked","localPickup","unserved","severity","action","hasRestOfWorldZone","headline","detail","note","asks","displayName","options","set","picked","Set","flexWrap","columnGap","service","has","serviceKey","key","markupPct","pct","isFinite","min","handlingCents","dollars","fallbackRateId","minWidth","fallback"],"mappings":"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"}
@@ -37,7 +37,22 @@ export interface ConsoleCommerceSettingsZoneProps {
37
37
  orgId: string | undefined;
38
38
  }
39
39
  export declare const ORDERS_BULK_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<ConsoleOrdersBulkZoneProps>;
40
+ /**
41
+ * The Integrations page's zone (AGL-3713): every store-settings widget named
42
+ * it before the settings were split into pages, and a widget that names no
43
+ * page still does — so it lands on Integrations rather than nowhere.
44
+ */
40
45
  export declare const COMMERCE_SETTINGS_ZONE: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<ConsoleCommerceSettingsZoneProps>;
46
+ /**
47
+ * One zone per settings page (AGL-3713), drawn at the foot of that page: a
48
+ * plugin chooses its page by the zone it names. Same props as
49
+ * `commerceSettings`. Ids from `COMMERCE_SETTINGS_PAGE_ZONES`, the list the
50
+ * rail and the pages read, so a page and its zone cannot disagree.
51
+ */
52
+ export declare const COMMERCE_SETTINGS_PAGE_ZONE_DEFS: {
53
+ page: "payments" | "checkout" | "taxes" | "shipping" | "notifications" | "pos" | "channels";
54
+ zone: import("@aglyn/aglyn/plugin-manager/plugin-zones").PluginZone<ConsoleCommerceSettingsZoneProps>;
55
+ }[];
41
56
  /** What `commercePromotions` hands a widget: the site (AGL-3640). */
42
57
  export type ConsoleCommercePromotionsZoneProps = ConsoleCommerceSettingsZoneProps;
43
58
  /**
@@ -15,8 +15,31 @@
15
15
  * limitations under the License.
16
16
  */ import { definePluginZone, registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
17
17
  import { BUNDLE_ID } from "../../constants/bundle-common.js";
18
+ import { COMMERCE_SETTINGS_PAGE_ZONES } from "../commerce-console-sections.js";
18
19
  export const ORDERS_BULK_ZONE = definePluginZone('ordersBulk');
19
- export const COMMERCE_SETTINGS_ZONE = definePluginZone('commerceSettings');
20
+ /**
21
+ * The Integrations page's zone (AGL-3713): every store-settings widget named
22
+ * it before the settings were split into pages, and a widget that names no
23
+ * page still does — so it lands on Integrations rather than nowhere.
24
+ */ export const COMMERCE_SETTINGS_ZONE = definePluginZone('commerceSettings');
25
+ /**
26
+ * One zone per settings page (AGL-3713), drawn at the foot of that page: a
27
+ * plugin chooses its page by the zone it names. Same props as
28
+ * `commerceSettings`. Ids from `COMMERCE_SETTINGS_PAGE_ZONES`, the list the
29
+ * rail and the pages read, so a page and its zone cannot disagree.
30
+ */ export const COMMERCE_SETTINGS_PAGE_ZONE_DEFS = Object.keys(COMMERCE_SETTINGS_PAGE_ZONES).filter((page)=>page !== 'integrations').map((page)=>({
31
+ page,
32
+ zone: definePluginZone(COMMERCE_SETTINGS_PAGE_ZONES[page])
33
+ }));
34
+ /** The settings page each page zone sits on, by name (AGL-3713). */ const PAGE_NAMES = {
35
+ payments: 'Payments',
36
+ checkout: 'Checkout',
37
+ taxes: 'Taxes',
38
+ shipping: 'Shipping & delivery',
39
+ notifications: 'Notifications',
40
+ pos: 'Point of sale',
41
+ channels: 'Sales channels'
42
+ };
20
43
  /**
21
44
  * At the foot of the store's Promotions, beside its discounts and gift
22
45
  * cards (AGL-3640): a rewards program is the first widget.
@@ -36,14 +59,23 @@ export const COMMERCE_SETTINGS_ZONE = definePluginZone('commerceSettings');
36
59
  zone: COMMERCE_PROMOTIONS_ZONE,
37
60
  label: 'Store promotions',
38
61
  surface: 'console',
39
- description: 'At the foot of the store’s Promotions, beside its discounts and gift cards. A widget here keeps its own programs and balances through its own routes; it writes none of the store’s.'
62
+ description: 'At the foot of the store’s Promotions, beside its discounts, gift cards and reviews. A widget here keeps its own programs, balances and connections through its own routes; it writes none of the store’s.'
40
63
  }, owner);
41
64
  registerPluginZone({
42
65
  zone: COMMERCE_SETTINGS_ZONE,
43
- label: 'Store settings',
66
+ label: 'Store integrations',
44
67
  surface: 'console',
45
- description: 'At the foot of the store’s Settings. A widget here keeps settings of its own beside the store’s; it writes none of the store’s.'
68
+ description: 'The store’s Integrations settings page, and where a store-settings widget that names no other page lands. A widget here keeps settings of its own beside the store’s; it writes none of the store’s.'
46
69
  }, owner);
70
+ for (const { page, zone } of COMMERCE_SETTINGS_PAGE_ZONE_DEFS){
71
+ const name = PAGE_NAMES[page];
72
+ registerPluginZone({
73
+ zone,
74
+ label: `Store settings: ${name}`,
75
+ surface: 'console',
76
+ description: `At the foot of the store’s ${name} settings page. A widget here keeps settings of its own beside the store’s; it writes none of the store’s.`
77
+ }, owner);
78
+ }
47
79
  }
48
80
 
49
81
  //# sourceMappingURL=store-zones.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/store-zones.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 definePluginZone,\n registerPluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { BUNDLE_ID } from '../../constants/bundle-common'\n\n/**\n * Two more zones the store's console hosts (AGL-3612): `ordersBulk`, beside\n * the orders list's bulk actions, and `commerceSettings`, at the foot of the\n * store's Settings. A shipping plugin's batch labels and its label settings\n * are the first widgets; another plugin restates these props rather than\n * importing this package.\n */\n\n/** What `ordersBulk` hands a widget: the orders ticked in the list. */\nexport interface ConsoleOrdersBulkZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n /** The ticked orders' ids, in the list's order. */\n selectedOrderIds: readonly string[]\n /** The store's name, for anything printed. */\n storeName: string\n}\n\n/** What `commerceSettings` hands a widget: the site. */\nexport interface ConsoleCommerceSettingsZoneProps {\n hostId: string\n orgId: string | undefined\n}\n\nexport const ORDERS_BULK_ZONE = definePluginZone<ConsoleOrdersBulkZoneProps>('ordersBulk')\n\nexport const COMMERCE_SETTINGS_ZONE =\n definePluginZone<ConsoleCommerceSettingsZoneProps>('commerceSettings')\n\n/** What `commercePromotions` hands a widget: the site (AGL-3640). */\nexport type ConsoleCommercePromotionsZoneProps = ConsoleCommerceSettingsZoneProps\n\n/**\n * At the foot of the store's Promotions, beside its discounts and gift\n * cards (AGL-3640): a rewards program is the first widget.\n */\nexport const COMMERCE_PROMOTIONS_ZONE =\n definePluginZone<ConsoleCommercePromotionsZoneProps>('commercePromotions')\n\n/** Declares them, from the console registrar. */\nexport function registerCommerceStoreZones(): void {\n const owner = { pluginId: BUNDLE_ID }\n registerPluginZone(\n {\n zone: ORDERS_BULK_ZONE,\n label: 'Orders bulk actions',\n surface: 'console',\n layout: 'bare',\n description:\n 'Beside the orders list’s bulk actions, while orders are ticked. A widget here acts on the ticked orders through its own routes and writes no order itself.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: COMMERCE_PROMOTIONS_ZONE,\n label: 'Store promotions',\n surface: 'console',\n description:\n 'At the foot of the store’s Promotions, beside its discounts and gift cards. A widget here keeps its own programs and balances through its own routes; it writes none of the store’s.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: COMMERCE_SETTINGS_ZONE,\n label: 'Store settings',\n surface: 'console',\n description:\n 'At the foot of the store’s Settings. A widget here keeps settings of its own beside the store’s; it writes none of the store’s.',\n },\n owner,\n )\n}\n"],"names":["definePluginZone","registerPluginZone","BUNDLE_ID","ORDERS_BULK_ZONE","COMMERCE_SETTINGS_ZONE","COMMERCE_PROMOTIONS_ZONE","registerCommerceStoreZones","owner","pluginId","zone","label","surface","layout","description"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAChBC,kBAAkB,QACb,2CAA0C;AACjD,SAASC,SAAS,QAAQ,mCAA+B;AA2BzD,OAAO,MAAMC,mBAAmBH,iBAA6C,cAAa;AAE1F,OAAO,MAAMI,yBACXJ,iBAAmD,oBAAmB;AAKxE;;;CAGC,GACD,OAAO,MAAMK,2BACXL,iBAAqD,sBAAqB;AAE5E,+CAA+C,GAC/C,OAAO,SAASM;IACd,MAAMC,QAAQ;QAAEC,UAAUN;IAAU;IACpCD,mBACE;QACEQ,MAAMN;QACNO,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAN;IAEFN,mBACE;QACEQ,MAAMJ;QACNK,OAAO;QACPC,SAAS;QACTE,aACE;IACJ,GACAN;IAEFN,mBACE;QACEQ,MAAML;QACNM,OAAO;QACPC,SAAS;QACTE,aACE;IACJ,GACAN;AAEJ"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/store-zones.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 definePluginZone,\n registerPluginZone,\n} from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport { BUNDLE_ID } from '../../constants/bundle-common'\nimport {\n COMMERCE_SETTINGS_PAGE_ZONES,\n type CommerceSettingsSectionId,\n} from '../commerce-console-sections'\n\n/**\n * Two more zones the store's console hosts (AGL-3612): `ordersBulk`, beside\n * the orders list's bulk actions, and `commerceSettings`, at the foot of the\n * store's Settings. A shipping plugin's batch labels and its label settings\n * are the first widgets; another plugin restates these props rather than\n * importing this package.\n */\n\n/** What `ordersBulk` hands a widget: the orders ticked in the list. */\nexport interface ConsoleOrdersBulkZoneProps {\n hostId: string\n /** The org the page names; `undefined` where the host does not know it. */\n orgId: string | undefined\n /** The ticked orders' ids, in the list's order. */\n selectedOrderIds: readonly string[]\n /** The store's name, for anything printed. */\n storeName: string\n}\n\n/** What `commerceSettings` hands a widget: the site. */\nexport interface ConsoleCommerceSettingsZoneProps {\n hostId: string\n orgId: string | undefined\n}\n\nexport const ORDERS_BULK_ZONE = definePluginZone<ConsoleOrdersBulkZoneProps>('ordersBulk')\n\n/**\n * The Integrations page's zone (AGL-3713): every store-settings widget named\n * it before the settings were split into pages, and a widget that names no\n * page still does — so it lands on Integrations rather than nowhere.\n */\nexport const COMMERCE_SETTINGS_ZONE =\n definePluginZone<ConsoleCommerceSettingsZoneProps>('commerceSettings')\n\n/**\n * One zone per settings page (AGL-3713), drawn at the foot of that page: a\n * plugin chooses its page by the zone it names. Same props as\n * `commerceSettings`. Ids from `COMMERCE_SETTINGS_PAGE_ZONES`, the list the\n * rail and the pages read, so a page and its zone cannot disagree.\n */\nexport const COMMERCE_SETTINGS_PAGE_ZONE_DEFS = (\n Object.keys(COMMERCE_SETTINGS_PAGE_ZONES) as CommerceSettingsSectionId[]\n)\n .filter((page) => page !== 'integrations')\n .map((page) => ({\n page,\n zone: definePluginZone<ConsoleCommerceSettingsZoneProps>(\n COMMERCE_SETTINGS_PAGE_ZONES[page],\n ),\n }))\n\n/** The settings page each page zone sits on, by name (AGL-3713). */\nconst PAGE_NAMES: Readonly<\n Record<Exclude<CommerceSettingsSectionId, 'integrations'>, string>\n> = {\n payments: 'Payments',\n checkout: 'Checkout',\n taxes: 'Taxes',\n shipping: 'Shipping & delivery',\n notifications: 'Notifications',\n pos: 'Point of sale',\n channels: 'Sales channels',\n}\n\n/** What `commercePromotions` hands a widget: the site (AGL-3640). */\nexport type ConsoleCommercePromotionsZoneProps = ConsoleCommerceSettingsZoneProps\n\n/**\n * At the foot of the store's Promotions, beside its discounts and gift\n * cards (AGL-3640): a rewards program is the first widget.\n */\nexport const COMMERCE_PROMOTIONS_ZONE =\n definePluginZone<ConsoleCommercePromotionsZoneProps>('commercePromotions')\n\n/** Declares them, from the console registrar. */\nexport function registerCommerceStoreZones(): void {\n const owner = { pluginId: BUNDLE_ID }\n registerPluginZone(\n {\n zone: ORDERS_BULK_ZONE,\n label: 'Orders bulk actions',\n surface: 'console',\n layout: 'bare',\n description:\n 'Beside the orders list’s bulk actions, while orders are ticked. A widget here acts on the ticked orders through its own routes and writes no order itself.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: COMMERCE_PROMOTIONS_ZONE,\n label: 'Store promotions',\n surface: 'console',\n description:\n 'At the foot of the store’s Promotions, beside its discounts, gift cards and reviews. A widget here keeps its own programs, balances and connections through its own routes; it writes none of the store’s.',\n },\n owner,\n )\n registerPluginZone(\n {\n zone: COMMERCE_SETTINGS_ZONE,\n label: 'Store integrations',\n surface: 'console',\n description:\n 'The store’s Integrations settings page, and where a store-settings widget that names no other page lands. A widget here keeps settings of its own beside the store’s; it writes none of the store’s.',\n },\n owner,\n )\n for (const { page, zone } of COMMERCE_SETTINGS_PAGE_ZONE_DEFS) {\n const name = PAGE_NAMES[page as Exclude<CommerceSettingsSectionId, 'integrations'>]\n registerPluginZone(\n {\n zone,\n label: `Store settings: ${name}`,\n surface: 'console',\n description: `At the foot of the store’s ${name} settings page. A widget here keeps settings of its own beside the store’s; it writes none of the store’s.`,\n },\n owner,\n )\n }\n}\n"],"names":["definePluginZone","registerPluginZone","BUNDLE_ID","COMMERCE_SETTINGS_PAGE_ZONES","ORDERS_BULK_ZONE","COMMERCE_SETTINGS_ZONE","COMMERCE_SETTINGS_PAGE_ZONE_DEFS","Object","keys","filter","page","map","zone","PAGE_NAMES","payments","checkout","taxes","shipping","notifications","pos","channels","COMMERCE_PROMOTIONS_ZONE","registerCommerceStoreZones","owner","pluginId","label","surface","layout","description","name"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,gBAAgB,EAChBC,kBAAkB,QACb,2CAA0C;AACjD,SAASC,SAAS,QAAQ,mCAA+B;AACzD,SACEC,4BAA4B,QAEvB,kCAA8B;AA2BrC,OAAO,MAAMC,mBAAmBJ,iBAA6C,cAAa;AAE1F;;;;CAIC,GACD,OAAO,MAAMK,yBACXL,iBAAmD,oBAAmB;AAExE;;;;;CAKC,GACD,OAAO,MAAMM,mCAAmC,AAC9CC,OAAOC,IAAI,CAACL,8BAEXM,MAAM,CAAC,CAACC,OAASA,SAAS,gBAC1BC,GAAG,CAAC,CAACD,OAAU,CAAA;QACdA;QACAE,MAAMZ,iBACJG,4BAA4B,CAACO,KAAK;IAEtC,CAAA,GAAG;AAEL,kEAAkE,GAClE,MAAMG,aAEF;IACFC,UAAU;IACVC,UAAU;IACVC,OAAO;IACPC,UAAU;IACVC,eAAe;IACfC,KAAK;IACLC,UAAU;AACZ;AAKA;;;CAGC,GACD,OAAO,MAAMC,2BACXrB,iBAAqD,sBAAqB;AAE5E,+CAA+C,GAC/C,OAAO,SAASsB;IACd,MAAMC,QAAQ;QAAEC,UAAUtB;IAAU;IACpCD,mBACE;QACEW,MAAMR;QACNqB,OAAO;QACPC,SAAS;QACTC,QAAQ;QACRC,aACE;IACJ,GACAL;IAEFtB,mBACE;QACEW,MAAMS;QACNI,OAAO;QACPC,SAAS;QACTE,aACE;IACJ,GACAL;IAEFtB,mBACE;QACEW,MAAMP;QACNoB,OAAO;QACPC,SAAS;QACTE,aACE;IACJ,GACAL;IAEF,KAAK,MAAM,EAAEb,IAAI,EAAEE,IAAI,EAAE,IAAIN,iCAAkC;QAC7D,MAAMuB,OAAOhB,UAAU,CAACH,KAA2D;QACnFT,mBACE;YACEW;YACAa,OAAO,CAAC,gBAAgB,EAAEI,MAAM;YAChCH,SAAS;YACTE,aAAa,CAAC,2BAA2B,EAAEC,KAAK,0GAA0G,CAAC;QAC7J,GACAN;IAEJ;AACF"}
@@ -38,8 +38,8 @@ import { pluginDocsHelp } from "@aglyn/aglyn";
38
38
  * enforces it, so an unentitled org could create suppliers, assign them to
39
39
  * products, and watch every paid order silently fail to route — a supplier
40
40
  * that looks configured and never hears about an order.
41
- */ const suppliersHelp = pluginDocsHelp('commerce', {
42
- anchor: '#dropshipping'
41
+ */ const suppliersHelp = pluginDocsHelp('dropshipSuppliers', {
42
+ anchor: '#add-a-supplier'
43
43
  });
44
44
  export function SuppliersCard(props) {
45
45
  var _ref, _ref1, _ref2, _ref3;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/suppliers-card.component.tsx"],"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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, deleteDoc, doc, setDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collectionPage,\n usePagedCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { EntitlementGatedCard } from './entitlement-gate.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\n\nexport interface SuppliersCardProps {\n hostId: string\n}\n\n/**\n * Dropship suppliers (AGL-289): paid orders whose product points at a\n * supplier notify it by email and/or HMAC-signed webhook, and the\n * supplier posts tracking back through a token link — no Aglyn account\n * needed. Assign suppliers per product in the product editor.\n *\n * Gated on `dropshipRouting` since AGL-2080. `server/billing-webhook.ts:2474`\n * enforces it, so an unentitled org could create suppliers, assign them to\n * products, and watch every paid order silently fail to route — a supplier\n * that looks configured and never hears about an order.\n */\n\nconst suppliersHelp = pluginDocsHelp('commerce', { anchor: '#dropshipping' })\n\nexport function SuppliersCard(props: SuppliersCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const {\n status: suppliersStatus,\n /**\n * The list this dialog is seeded from is unconfirmed by the server\n * (AGL-1358). Editing a supplier copies the whole stored row into\n * `draft` and writes all of it back, so a cached seed can restore a\n * ROTATED `webhookSecret` — the supplier keeps signing with a key we\n * meant to retire, and nothing anywhere reports it.\n */\n fromCache: suppliersFromCache,\n rows: suppliers,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n /*\n * SERVER-PAGED, and ordered (AGL-2501).\n *\n * `limit(50)` alone is answered in DOCUMENT-ID order, and suppliers are\n * created with a generated id — so the fifty were a pseudo-random sample\n * and the fifty-first was not merely unrendered, it was UNREACHABLE. A\n * merchant with sixty fulfillment partners saw fifty of them and nothing\n * said which.\n *\n * This one really can be paged by the query, unlike the cards that carry a\n * duplicate-name check or a count over the whole set: nothing here is\n * derived from rows off the page. Suppliers are assigned to products by\n * document id in the product editor, and the empty state and the row list\n * are the only consumers.\n *\n * `collectionPage` orders on the document NAME, which cannot be absent —\n * `orderBy('name')` would HIDE every supplier saved without one rather\n * than mis-sorting the list, and `/api/hosts/resources` validates no field\n * for presence.\n */\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'suppliers'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const [draft, setDraft] = useState<\n (CommerceModel.HostSupplier & { id: string | null }) | null\n >(null)\n\n const handleSave = useCallback(async () => {\n if (!draft?.name.trim()) return\n const { id, ...data } = draft\n /**\n * Refuse an EDIT whose seed the server never confirmed (AGL-1358).\n *\n * This write has no options argument at all, so it is a full document\n * replace of a payload copied wholesale off the listener — `merge`\n * would not have helped anyway, since every field is present. The one\n * that matters is `webhookSecret`: rotate it somewhere else, come back\n * to a console still serving the old row out of `persistentLocalCache`,\n * rename the supplier, and the retired secret is stored again.\n *\n * Only the edit path is guarded. A NEW supplier is built from blanks at\n * a fresh uid and can overwrite nothing, so refusing it would be a false\n * positive on a path that is never unsafe — and this guard stands in\n * front of the ordinary save.\n *\n * The guard WRAPS the write: an early return is a shape you can keep\n * while losing the protection.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'supplier',\n unreadable: Boolean(id) && suppliersStatus === 'error',\n fromCache: Boolean(id) && suppliersFromCache,\n },\n async () => {\n await setDoc(\n doc(\n firestore,\n 'hosts',\n hostId,\n 'suppliers',\n id ?? Aglyn.createResourceUid(),\n ),\n {\n ...data,\n name: draft.name.trim().slice(0, 80),\n },\n )\n },\n )\n // A refusal keeps the dialog open with what was typed. A save that\n // silently does nothing sends the user back to retype a form that will\n // be refused again just as quietly.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setDraft(null)\n enqueueSnackbar('Supplier saved', { variant: 'success', persist: false })\n }, [\n draft,\n firestore,\n hostId,\n enqueueSnackbar,\n suppliersFromCache,\n suppliersStatus,\n ])\n\n const handleDelete = useCallback(\n (supplier: any) => async () => {\n const confirmed = await confirm({\n title: 'Remove this supplier?',\n description:\n `Products assigned to \"${supplier.name}\" stop routing; orders ` +\n 'already sent are unaffected.',\n confirmationText: 'Remove',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await deleteDoc(doc(firestore, 'hosts', hostId, 'suppliers', supplier.$id))\n },\n [confirm, firestore, hostId],\n )\n\n return (\n <EntitlementGatedCard\n help={suppliersHelp}\n hostId={hostId}\n feature=\"dropshipRouting\"\n header={'Dropship suppliers'}\n upsell={\n 'Dropship routing forwards each paid order to the right supplier ' +\n 'by email or signed webhook, and takes tracking numbers back ' +\n `without the supplier needing an ${Aglyn.PLATFORM_BRAND_NAME} account.`\n }\n >\n <CardDisplay\n header={'Dropship suppliers'}\n help={suppliersHelp}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {suppliers.length === 0 && page === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Route paid orders straight to a fulfillment partner: add a ' +\n 'supplier, then assign it on products.'}\n </Typography>\n ) : (\n suppliers.map((supplier: any) => (\n <Stack\n key={supplier.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {supplier.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {[supplier.email, supplier.webhookUrl]\n .filter(Boolean)\n .join(' · ') || 'No delivery method set'}\n </Typography>\n </Stack>\n <Button\n size=\"small\"\n onClick={() =>\n setDraft({\n id: supplier.$id,\n name: supplier.name ?? '',\n email: supplier.email ?? '',\n webhookUrl: supplier.webhookUrl ?? '',\n webhookSecret: supplier.webhookSecret ?? '',\n })\n }\n >\n {'Edit'}\n </Button>\n <Button size=\"small\" color=\"error\" onClick={handleDelete(supplier)}>\n {'Remove'}\n </Button>\n </Stack>\n ))\n )}\n {suppliers.length === 0 && page === 0 ? null : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={suppliers.length}\n // No `count`: this is a server window, and nobody knows the total\n // without paying to read it. `hasMore` is the probe row's answer.\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setDraft({ id: null, name: '', email: '', webhookUrl: '', webhookSecret: '' })\n }\n >\n {'Add supplier'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit supplier' : 'New supplier'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n />\n <TextField\n label=\"Notification email\"\n value={draft?.email ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, email: event.target.value } : prev,\n )\n }\n size=\"small\"\n helperText=\"Gets order details + a tracking link per sale\"\n />\n <TextField\n label=\"Webhook URL\"\n value={draft?.webhookUrl ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, webhookUrl: event.target.value } : prev,\n )\n }\n size=\"small\"\n placeholder=\"https://…\"\n />\n <TextField\n label=\"Webhook secret\"\n value={draft?.webhookSecret ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, webhookSecret: event.target.value } : prev,\n )\n }\n size=\"small\"\n helperText=\"Payloads carry an x-aglyn-signature HMAC header\"\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!draft?.name.trim()}\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n </EntitlementGatedCard>\n )\n}\nSuppliersCard.displayName = 'SuppliersCard'\n\nexport default SuppliersCard\n"],"names":["Aglyn","CardDisplay","useConfirmationContext","useSnackbar","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","collection","deleteDoc","doc","setDoc","useCallback","useState","useFirestore","collectionPage","usePagedCollection","writeGuardedBySeed","ListPagination","EntitlementGatedCard","pluginDocsHelp","suppliersHelp","anchor","SuppliersCard","props","hostId","firestore","enqueueSnackbar","confirm","status","suppliersStatus","fromCache","suppliersFromCache","rows","suppliers","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","draft","setDraft","handleSave","name","trim","id","data","verdict","subject","unreadable","Boolean","createResourceUid","slice","ok","message","variant","persist","handleDelete","supplier","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","$id","help","feature","header","upsell","PLATFORM_BRAND_NAME","contentGutterX","contentGutterY","spacing","length","map","direction","sx","alignItems","flex","minWidth","noWrap","email","webhookUrl","filter","join","size","onClick","webhookSecret","rowCount","onPageChange","onPageSizeChange","alignSelf","open","onClose","maxWidth","fullWidth","display","flexDirection","gap","label","value","onChange","event","prev","target","autoFocus","mt","helperText","placeholder","disabled","displayName"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/components/console/suppliers-card.component.tsx"],"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'use client'\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport * as CommerceModel from '../../model'\nimport { CardDisplay, useConfirmationContext } from '@aglyn/shared-ui-jsx'\nimport { useSnackbar } from '@aglyn/shared-ui-snackstack'\nimport {\n Button,\n Dialog,\n DialogActions,\n DialogContent,\n DialogTitle,\n Stack,\n TextField,\n Typography,\n} from '@mui/material'\nimport { collection, deleteDoc, doc, setDoc } from 'firebase/firestore'\nimport { useCallback, useState } from 'react'\nimport { useFirestore } from '@aglyn/tenant-feature-instance'\nimport {\n collectionPage,\n usePagedCollection,\n writeGuardedBySeed,\n} from '@aglyn/tenant-feature-instance'\nimport { ListPagination } from '@aglyn/shared-ui-jsx/components/list-pagination.component'\nimport { EntitlementGatedCard } from './entitlement-gate.component'\nimport { pluginDocsHelp } from '@aglyn/aglyn'\n\nexport interface SuppliersCardProps {\n hostId: string\n}\n\n/**\n * Dropship suppliers (AGL-289): paid orders whose product points at a\n * supplier notify it by email and/or HMAC-signed webhook, and the\n * supplier posts tracking back through a token link — no Aglyn account\n * needed. Assign suppliers per product in the product editor.\n *\n * Gated on `dropshipRouting` since AGL-2080. `server/billing-webhook.ts:2474`\n * enforces it, so an unentitled org could create suppliers, assign them to\n * products, and watch every paid order silently fail to route — a supplier\n * that looks configured and never hears about an order.\n */\n\nconst suppliersHelp = pluginDocsHelp('dropshipSuppliers', { anchor: '#add-a-supplier' })\n\nexport function SuppliersCard(props: SuppliersCardProps) {\n const { hostId } = props\n const firestore = useFirestore()\n const { enqueueSnackbar } = useSnackbar()\n const { confirm } = useConfirmationContext()\n const {\n status: suppliersStatus,\n /**\n * The list this dialog is seeded from is unconfirmed by the server\n * (AGL-1358). Editing a supplier copies the whole stored row into\n * `draft` and writes all of it back, so a cached seed can restore a\n * ROTATED `webhookSecret` — the supplier keeps signing with a key we\n * meant to retire, and nothing anywhere reports it.\n */\n fromCache: suppliersFromCache,\n rows: suppliers,\n hasMore,\n page,\n setPage,\n pageSize,\n setPageSize,\n } = usePagedCollection<any>(\n /*\n * SERVER-PAGED, and ordered (AGL-2501).\n *\n * `limit(50)` alone is answered in DOCUMENT-ID order, and suppliers are\n * created with a generated id — so the fifty were a pseudo-random sample\n * and the fifty-first was not merely unrendered, it was UNREACHABLE. A\n * merchant with sixty fulfillment partners saw fifty of them and nothing\n * said which.\n *\n * This one really can be paged by the query, unlike the cards that carry a\n * duplicate-name check or a count over the whole set: nothing here is\n * derived from rows off the page. Suppliers are assigned to products by\n * document id in the product editor, and the empty state and the row list\n * are the only consumers.\n *\n * `collectionPage` orders on the document NAME, which cannot be absent —\n * `orderBy('name')` would HIDE every supplier saved without one rather\n * than mis-sorting the list, and `/api/hosts/resources` validates no field\n * for presence.\n */\n (pageLimit) =>\n collectionPage(\n collection(firestore, 'hosts', hostId, 'suppliers'),\n pageLimit,\n ),\n [firestore, hostId],\n { idField: '$id' },\n )\n const [draft, setDraft] = useState<\n (CommerceModel.HostSupplier & { id: string | null }) | null\n >(null)\n\n const handleSave = useCallback(async () => {\n if (!draft?.name.trim()) return\n const { id, ...data } = draft\n /**\n * Refuse an EDIT whose seed the server never confirmed (AGL-1358).\n *\n * This write has no options argument at all, so it is a full document\n * replace of a payload copied wholesale off the listener — `merge`\n * would not have helped anyway, since every field is present. The one\n * that matters is `webhookSecret`: rotate it somewhere else, come back\n * to a console still serving the old row out of `persistentLocalCache`,\n * rename the supplier, and the retired secret is stored again.\n *\n * Only the edit path is guarded. A NEW supplier is built from blanks at\n * a fresh uid and can overwrite nothing, so refusing it would be a false\n * positive on a path that is never unsafe — and this guard stands in\n * front of the ordinary save.\n *\n * The guard WRAPS the write: an early return is a shape you can keep\n * while losing the protection.\n */\n const verdict = await writeGuardedBySeed(\n {\n subject: 'supplier',\n unreadable: Boolean(id) && suppliersStatus === 'error',\n fromCache: Boolean(id) && suppliersFromCache,\n },\n async () => {\n await setDoc(\n doc(\n firestore,\n 'hosts',\n hostId,\n 'suppliers',\n id ?? Aglyn.createResourceUid(),\n ),\n {\n ...data,\n name: draft.name.trim().slice(0, 80),\n },\n )\n },\n )\n // A refusal keeps the dialog open with what was typed. A save that\n // silently does nothing sends the user back to retype a form that will\n // be refused again just as quietly.\n if (!verdict.ok) {\n return void enqueueSnackbar(verdict.message, {\n variant: 'warning',\n persist: false,\n })\n }\n setDraft(null)\n enqueueSnackbar('Supplier saved', { variant: 'success', persist: false })\n }, [\n draft,\n firestore,\n hostId,\n enqueueSnackbar,\n suppliersFromCache,\n suppliersStatus,\n ])\n\n const handleDelete = useCallback(\n (supplier: any) => async () => {\n const confirmed = await confirm({\n title: 'Remove this supplier?',\n description:\n `Products assigned to \"${supplier.name}\" stop routing; orders ` +\n 'already sent are unaffected.',\n confirmationText: 'Remove',\n confirmationButtonProps: { color: 'error' },\n })\n .then(() => true)\n .catch(() => false)\n if (!confirmed) return\n await deleteDoc(doc(firestore, 'hosts', hostId, 'suppliers', supplier.$id))\n },\n [confirm, firestore, hostId],\n )\n\n return (\n <EntitlementGatedCard\n help={suppliersHelp}\n hostId={hostId}\n feature=\"dropshipRouting\"\n header={'Dropship suppliers'}\n upsell={\n 'Dropship routing forwards each paid order to the right supplier ' +\n 'by email or signed webhook, and takes tracking numbers back ' +\n `without the supplier needing an ${Aglyn.PLATFORM_BRAND_NAME} account.`\n }\n >\n <CardDisplay\n header={'Dropship suppliers'}\n help={suppliersHelp}\n contentGutterX\n contentGutterY\n >\n <Stack spacing={1}>\n {suppliers.length === 0 && page === 0 ? (\n <Typography variant=\"body2\" color=\"text.secondary\">\n {'Route paid orders straight to a fulfillment partner: add a ' +\n 'supplier, then assign it on products.'}\n </Typography>\n ) : (\n suppliers.map((supplier: any) => (\n <Stack\n key={supplier.$id}\n direction=\"row\"\n spacing={1}\n sx={{ alignItems: 'center' }}\n >\n <Stack sx={{ flex: 1, minWidth: 0 }}>\n <Typography variant=\"body2\" noWrap>\n {supplier.name}\n </Typography>\n <Typography variant=\"caption\" color=\"text.secondary\" noWrap>\n {[supplier.email, supplier.webhookUrl]\n .filter(Boolean)\n .join(' · ') || 'No delivery method set'}\n </Typography>\n </Stack>\n <Button\n size=\"small\"\n onClick={() =>\n setDraft({\n id: supplier.$id,\n name: supplier.name ?? '',\n email: supplier.email ?? '',\n webhookUrl: supplier.webhookUrl ?? '',\n webhookSecret: supplier.webhookSecret ?? '',\n })\n }\n >\n {'Edit'}\n </Button>\n <Button size=\"small\" color=\"error\" onClick={handleDelete(supplier)}>\n {'Remove'}\n </Button>\n </Stack>\n ))\n )}\n {suppliers.length === 0 && page === 0 ? null : (\n <ListPagination\n page={page}\n pageSize={pageSize}\n rowCount={suppliers.length}\n // No `count`: this is a server window, and nobody knows the total\n // without paying to read it. `hasMore` is the probe row's answer.\n hasMore={hasMore}\n onPageChange={setPage}\n onPageSizeChange={setPageSize}\n />\n )}\n <Button\n size=\"small\"\n sx={{ alignSelf: 'flex-start' }}\n onClick={() =>\n setDraft({ id: null, name: '', email: '', webhookUrl: '', webhookSecret: '' })\n }\n >\n {'Add supplier'}\n </Button>\n </Stack>\n <Dialog\n open={Boolean(draft)}\n onClose={() => setDraft(null)}\n maxWidth=\"xs\"\n fullWidth\n >\n <DialogTitle>{draft?.id ? 'Edit supplier' : 'New supplier'}</DialogTitle>\n <DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <TextField\n label=\"Name\"\n value={draft?.name ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, name: event.target.value } : prev,\n )\n }\n size=\"small\"\n autoFocus\n sx={{ mt: 1 }}\n />\n <TextField\n label=\"Notification email\"\n value={draft?.email ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, email: event.target.value } : prev,\n )\n }\n size=\"small\"\n helperText=\"Gets order details + a tracking link per sale\"\n />\n <TextField\n label=\"Webhook URL\"\n value={draft?.webhookUrl ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, webhookUrl: event.target.value } : prev,\n )\n }\n size=\"small\"\n placeholder=\"https://…\"\n />\n <TextField\n label=\"Webhook secret\"\n value={draft?.webhookSecret ?? ''}\n onChange={(event) =>\n setDraft((prev) =>\n prev ? { ...prev, webhookSecret: event.target.value } : prev,\n )\n }\n size=\"small\"\n helperText=\"Payloads carry an x-aglyn-signature HMAC header\"\n />\n </DialogContent>\n <DialogActions>\n <Button onClick={() => setDraft(null)}>{'Cancel'}</Button>\n <Button\n variant=\"contained\"\n color=\"primary\"\n disabled={!draft?.name.trim()}\n onClick={handleSave}\n >\n {'Save'}\n </Button>\n </DialogActions>\n </Dialog>\n </CardDisplay>\n </EntitlementGatedCard>\n )\n}\nSuppliersCard.displayName = 'SuppliersCard'\n\nexport default SuppliersCard\n"],"names":["Aglyn","CardDisplay","useConfirmationContext","useSnackbar","Button","Dialog","DialogActions","DialogContent","DialogTitle","Stack","TextField","Typography","collection","deleteDoc","doc","setDoc","useCallback","useState","useFirestore","collectionPage","usePagedCollection","writeGuardedBySeed","ListPagination","EntitlementGatedCard","pluginDocsHelp","suppliersHelp","anchor","SuppliersCard","props","hostId","firestore","enqueueSnackbar","confirm","status","suppliersStatus","fromCache","suppliersFromCache","rows","suppliers","hasMore","page","setPage","pageSize","setPageSize","pageLimit","idField","draft","setDraft","handleSave","name","trim","id","data","verdict","subject","unreadable","Boolean","createResourceUid","slice","ok","message","variant","persist","handleDelete","supplier","confirmed","title","description","confirmationText","confirmationButtonProps","color","then","catch","$id","help","feature","header","upsell","PLATFORM_BRAND_NAME","contentGutterX","contentGutterY","spacing","length","map","direction","sx","alignItems","flex","minWidth","noWrap","email","webhookUrl","filter","join","size","onClick","webhookSecret","rowCount","onPageChange","onPageSizeChange","alignSelf","open","onClose","maxWidth","fullWidth","display","flexDirection","gap","label","value","onChange","event","prev","target","autoFocus","mt","helperText","placeholder","disabled","displayName"],"mappings":"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"}
@@ -13,6 +13,7 @@
13
13
  */ import { buildRoute, Route } from "@aglyn/aglyn/app-utils/console-routes";
14
14
  import { registerPluginRecordRoute } from "@aglyn/aglyn/plugin-manager/plugin-record-routes";
15
15
  import { BUNDLE_ID } from "../constants/bundle-common.js";
16
+ import { COMMERCE_SETTINGS_PAGE_ZONES } from "../components/commerce-console-sections.js";
16
17
  /**
17
18
  * Where a product and an order are read, published for every other surface
18
19
  * (AGL-3080).
@@ -53,9 +54,15 @@ function orders(context, key, value) {
53
54
  * The store's settings — payments (Stripe), shipping, tax and returns — under
54
55
  * `store-settings` (AGL-3676): where an AI-built store's "Finish your store"
55
56
  * list sends its owner, without spelling this plugin's console path.
56
- */ function settings(context) {
57
+ *
58
+ * A page each since AGL-3713: the list is Payments, the first of them, and a
59
+ * record is the settings page of that id (`shipping`, `taxes` …). An id that
60
+ * is not a settings page opens the list rather than a 404.
61
+ */ function settings(context, page) {
57
62
  const list = catalog(context);
58
- return list ? `${list}/settings` : null;
63
+ if (!list) return null;
64
+ const id = page && Object.hasOwn(COMMERCE_SETTINGS_PAGE_ZONES, page) ? page : 'payments';
65
+ return `${list}/${id}`;
59
66
  }
60
67
  /**
61
68
  * Called from the console registrar. The owner is named rather than read off
@@ -69,7 +76,7 @@ function orders(context, key, value) {
69
76
  pluginId: BUNDLE_ID
70
77
  });
71
78
  registerPluginRecordRoute('store-settings', {
72
- list: settings,
79
+ list: (context)=>settings(context),
73
80
  record: settings
74
81
  }, {
75
82
  pluginId: BUNDLE_ID
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-record-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\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 * http://www.apache.org/licenses/LICENSE-2.0\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\n\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * Where a product and an order are read, published for every other surface\n * (AGL-3080).\n *\n * A surface elsewhere — the AI plugin's job drawer opening a product it\n * drafted, a contact's timeline opening the order it names, a person's page\n * listing their orders — asks the record-route registry for `product` or\n * `order` rather than spelling this plugin's console path, and gets `null`\n * (text instead of a link) where this plugin is not loaded.\n *\n * Both are a site's, so only a site has an address. The catalog has no page\n * per product — a product opens in the hub's editor — so the record answers\n * the catalog a caller would find it in. An order opens as a dialog over the\n * Orders list, which reads {@link ORDERS_ORDER_PARAM} on arrival; the list\n * narrowed to one buyer reads {@link ORDERS_CUSTOMER_PARAM}.\n */\n\n/** The nav slug the shell resolves the catalog by. */\nexport const PRODUCTS_SLUG = 'products'\n\n/** The query key the Orders list opens one order's dialog by. */\nexport const ORDERS_ORDER_PARAM = 'order'\n\n/**\n * The query key the Orders list narrows to one buyer by — the same word the\n * CRM's contacts list opens a person by, so a reader who edits one URL into\n * the other is not surprised.\n */\nexport const ORDERS_CUSTOMER_PARAM = 'email'\n\nfunction catalog(context: PluginRecordRouteContext): string | null {\n return context.host\n ? buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: PRODUCTS_SLUG,\n })\n : null\n}\n\nfunction orders(context: PluginRecordRouteContext, key?: string, value?: string): string | null {\n const list = catalog(context)\n if (!list) return null\n return key && value\n ? `${list}/orders?${new URLSearchParams({ [key]: value }).toString()}`\n : `${list}/orders`\n}\n\n/**\n * The store's settings — payments (Stripe), shipping, tax and returns — under\n * `store-settings` (AGL-3676): where an AI-built store's \"Finish your store\"\n * list sends its owner, without spelling this plugin's console path.\n */\nfunction settings(context: PluginRecordRouteContext): string | null {\n const list = catalog(context)\n return list ? `${list}/settings` : null\n}\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the route registers the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerCommerceRecordRoutes(): void {\n registerPluginRecordRoute(\n 'product',\n { list: catalog, record: catalog },\n { pluginId: BUNDLE_ID },\n )\n registerPluginRecordRoute('store-settings', { list: settings, record: settings }, { pluginId: BUNDLE_ID })\n registerPluginRecordRoute(\n 'order',\n {\n list: (context) => orders(context),\n record: (context, id) => orders(context, ORDERS_ORDER_PARAM, id),\n byEmail: (context, email) => orders(context, ORDERS_CUSTOMER_PARAM, email),\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","PRODUCTS_SLUG","ORDERS_ORDER_PARAM","ORDERS_CUSTOMER_PARAM","catalog","context","host","HOST_PLUGIN","orgSlug","pluginSlug","orders","key","value","list","URLSearchParams","toString","settings","registerCommerceRecordRoutes","record","pluginId","id","byEmail","email"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GAGD,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;;;;;;CAeC,GAED,oDAAoD,GACpD,OAAO,MAAMC,gBAAgB,WAAU;AAEvC,+DAA+D,GAC/D,OAAO,MAAMC,qBAAqB,QAAO;AAEzC;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB,QAAO;AAE5C,SAASC,QAAQC,OAAiC;IAChD,OAAOA,QAAQC,IAAI,GACfT,WAAWC,MAAMS,WAAW,EAAE;QAC5BC,SAASH,QAAQG,OAAO;QACxBF,MAAMD,QAAQC,IAAI;QAClBG,YAAYR;IACd,KACA;AACN;AAEA,SAASS,OAAOL,OAAiC,EAAEM,GAAY,EAAEC,KAAc;IAC7E,MAAMC,OAAOT,QAAQC;IACrB,IAAI,CAACQ,MAAM,OAAO;IAClB,OAAOF,OAAOC,QACV,GAAGC,KAAK,QAAQ,EAAE,IAAIC,gBAAgB;QAAE,CAACH,IAAI,EAAEC;IAAM,GAAGG,QAAQ,IAAI,GACpE,GAAGF,KAAK,OAAO,CAAC;AACtB;AAEA;;;;CAIC,GACD,SAASG,SAASX,OAAiC;IACjD,MAAMQ,OAAOT,QAAQC;IACrB,OAAOQ,OAAO,GAAGA,KAAK,SAAS,CAAC,GAAG;AACrC;AAEA;;;;CAIC,GACD,OAAO,SAASI;IACdlB,0BACE,WACA;QAAEc,MAAMT;QAASc,QAAQd;IAAQ,GACjC;QAAEe,UAAUnB;IAAU;IAExBD,0BAA0B,kBAAkB;QAAEc,MAAMG;QAAUE,QAAQF;IAAS,GAAG;QAAEG,UAAUnB;IAAU;IACxGD,0BACE,SACA;QACEc,MAAM,CAACR,UAAYK,OAAOL;QAC1Ba,QAAQ,CAACb,SAASe,KAAOV,OAAOL,SAASH,oBAAoBkB;QAC7DC,SAAS,CAAChB,SAASiB,QAAUZ,OAAOL,SAASF,uBAAuBmB;IACtE,GACA;QAAEH,UAAUnB;IAAU;AAE1B"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/commerce-record-routes.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\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 * http://www.apache.org/licenses/LICENSE-2.0\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\n\nimport { buildRoute, Route } from '@aglyn/aglyn/app-utils/console-routes'\nimport {\n registerPluginRecordRoute,\n type PluginRecordRouteContext,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-routes'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { COMMERCE_SETTINGS_PAGE_ZONES } from '../components/commerce-console-sections'\n\n/**\n * Where a product and an order are read, published for every other surface\n * (AGL-3080).\n *\n * A surface elsewhere — the AI plugin's job drawer opening a product it\n * drafted, a contact's timeline opening the order it names, a person's page\n * listing their orders — asks the record-route registry for `product` or\n * `order` rather than spelling this plugin's console path, and gets `null`\n * (text instead of a link) where this plugin is not loaded.\n *\n * Both are a site's, so only a site has an address. The catalog has no page\n * per product — a product opens in the hub's editor — so the record answers\n * the catalog a caller would find it in. An order opens as a dialog over the\n * Orders list, which reads {@link ORDERS_ORDER_PARAM} on arrival; the list\n * narrowed to one buyer reads {@link ORDERS_CUSTOMER_PARAM}.\n */\n\n/** The nav slug the shell resolves the catalog by. */\nexport const PRODUCTS_SLUG = 'products'\n\n/** The query key the Orders list opens one order's dialog by. */\nexport const ORDERS_ORDER_PARAM = 'order'\n\n/**\n * The query key the Orders list narrows to one buyer by — the same word the\n * CRM's contacts list opens a person by, so a reader who edits one URL into\n * the other is not surprised.\n */\nexport const ORDERS_CUSTOMER_PARAM = 'email'\n\nfunction catalog(context: PluginRecordRouteContext): string | null {\n return context.host\n ? buildRoute(Route.HOST_PLUGIN, {\n orgSlug: context.orgSlug,\n host: context.host,\n pluginSlug: PRODUCTS_SLUG,\n })\n : null\n}\n\nfunction orders(context: PluginRecordRouteContext, key?: string, value?: string): string | null {\n const list = catalog(context)\n if (!list) return null\n return key && value\n ? `${list}/orders?${new URLSearchParams({ [key]: value }).toString()}`\n : `${list}/orders`\n}\n\n/**\n * The store's settings — payments (Stripe), shipping, tax and returns — under\n * `store-settings` (AGL-3676): where an AI-built store's \"Finish your store\"\n * list sends its owner, without spelling this plugin's console path.\n *\n * A page each since AGL-3713: the list is Payments, the first of them, and a\n * record is the settings page of that id (`shipping`, `taxes` …). An id that\n * is not a settings page opens the list rather than a 404.\n */\nfunction settings(context: PluginRecordRouteContext, page?: string): string | null {\n const list = catalog(context)\n if (!list) return null\n const id = page && Object.hasOwn(COMMERCE_SETTINGS_PAGE_ZONES, page) ? page : 'payments'\n return `${list}/${id}`\n}\n\n/**\n * Called from the console registrar. The owner is named rather than read off\n * the loader's marker, so the route registers the same way under a spec that\n * calls the registrar directly.\n */\nexport function registerCommerceRecordRoutes(): void {\n registerPluginRecordRoute(\n 'product',\n { list: catalog, record: catalog },\n { pluginId: BUNDLE_ID },\n )\n registerPluginRecordRoute(\n 'store-settings',\n { list: (context) => settings(context), record: settings },\n { pluginId: BUNDLE_ID },\n )\n registerPluginRecordRoute(\n 'order',\n {\n list: (context) => orders(context),\n record: (context, id) => orders(context, ORDERS_ORDER_PARAM, id),\n byEmail: (context, email) => orders(context, ORDERS_CUSTOMER_PARAM, email),\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["buildRoute","Route","registerPluginRecordRoute","BUNDLE_ID","COMMERCE_SETTINGS_PAGE_ZONES","PRODUCTS_SLUG","ORDERS_ORDER_PARAM","ORDERS_CUSTOMER_PARAM","catalog","context","host","HOST_PLUGIN","orgSlug","pluginSlug","orders","key","value","list","URLSearchParams","toString","settings","page","id","Object","hasOwn","registerCommerceRecordRoutes","record","pluginId","byEmail","email"],"mappings":"AAAA;;;;;;;;;;;;CAYC,GAGD,SAASA,UAAU,EAAEC,KAAK,QAAQ,wCAAuC;AACzE,SACEC,yBAAyB,QAEpB,mDAAkD;AACzD,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAASC,4BAA4B,QAAQ,6CAAyC;AAEtF;;;;;;;;;;;;;;;CAeC,GAED,oDAAoD,GACpD,OAAO,MAAMC,gBAAgB,WAAU;AAEvC,+DAA+D,GAC/D,OAAO,MAAMC,qBAAqB,QAAO;AAEzC;;;;CAIC,GACD,OAAO,MAAMC,wBAAwB,QAAO;AAE5C,SAASC,QAAQC,OAAiC;IAChD,OAAOA,QAAQC,IAAI,GACfV,WAAWC,MAAMU,WAAW,EAAE;QAC5BC,SAASH,QAAQG,OAAO;QACxBF,MAAMD,QAAQC,IAAI;QAClBG,YAAYR;IACd,KACA;AACN;AAEA,SAASS,OAAOL,OAAiC,EAAEM,GAAY,EAAEC,KAAc;IAC7E,MAAMC,OAAOT,QAAQC;IACrB,IAAI,CAACQ,MAAM,OAAO;IAClB,OAAOF,OAAOC,QACV,GAAGC,KAAK,QAAQ,EAAE,IAAIC,gBAAgB;QAAE,CAACH,IAAI,EAAEC;IAAM,GAAGG,QAAQ,IAAI,GACpE,GAAGF,KAAK,OAAO,CAAC;AACtB;AAEA;;;;;;;;CAQC,GACD,SAASG,SAASX,OAAiC,EAAEY,IAAa;IAChE,MAAMJ,OAAOT,QAAQC;IACrB,IAAI,CAACQ,MAAM,OAAO;IAClB,MAAMK,KAAKD,QAAQE,OAAOC,MAAM,CAACpB,8BAA8BiB,QAAQA,OAAO;IAC9E,OAAO,GAAGJ,KAAK,CAAC,EAAEK,IAAI;AACxB;AAEA;;;;CAIC,GACD,OAAO,SAASG;IACdvB,0BACE,WACA;QAAEe,MAAMT;QAASkB,QAAQlB;IAAQ,GACjC;QAAEmB,UAAUxB;IAAU;IAExBD,0BACE,kBACA;QAAEe,MAAM,CAACR,UAAYW,SAASX;QAAUiB,QAAQN;IAAS,GACzD;QAAEO,UAAUxB;IAAU;IAExBD,0BACE,SACA;QACEe,MAAM,CAACR,UAAYK,OAAOL;QAC1BiB,QAAQ,CAACjB,SAASa,KAAOR,OAAOL,SAASH,oBAAoBgB;QAC7DM,SAAS,CAACnB,SAASoB,QAAUf,OAAOL,SAASF,uBAAuBsB;IACtE,GACA;QAAEF,UAAUxB;IAAU;AAE1B"}
@@ -335,6 +335,22 @@ export interface ShippingCoverage {
335
335
  * checkout.
336
336
  */
337
337
  export declare function summarizeShippingCoverage(settings: ShippingSettings | undefined): ShippingCoverage;
338
+ /**
339
+ * What the Shipping card says about a store's coverage, in words (AGL-1791): the warning's headline and body when checkout
340
+ * turns destinations away, otherwise the one line that says what it covers, and the line that says checkout asks where the
341
+ * parcel is going. The console card and the native apps both read these, so the same store reads the same everywhere.
342
+ */
343
+ export interface ShippingCoverageWords {
344
+ /** Set when destinations are turned away: the warning's title. */
345
+ headline: string;
346
+ /** The warning's body; empty when nothing is turned away. */
347
+ detail: string;
348
+ /** The caption shown when nothing is turned away; empty when something is. */
349
+ note: string;
350
+ /** Shown when rates differ by destination; empty otherwise. */
351
+ asks: string;
352
+ }
353
+ export declare function describeShippingCoverage(coverage: ShippingCoverage): ShippingCoverageWords;
338
354
  /**
339
355
  * Emit `shipping_address_collection[allowed_countries][n]` onto a session's
340
356
  * form body. Stripe will not apply a `shipping_options` rate without an
@@ -492,6 +492,27 @@ function tierAmount(tiers, value) {
492
492
  hasRestOfWorldZone: ((_ref = settings == null ? void 0 : settings.zones) != null ? _ref : []).some((zone)=>zone.countries.some((code)=>code === '*'))
493
493
  };
494
494
  }
495
+ const coverageCountryNames = (codes)=>codes.map((code)=>{
496
+ var _CHECKOUT_SHIPPING_COUNTRY_NAMES_code;
497
+ return (_CHECKOUT_SHIPPING_COUNTRY_NAMES_code = CHECKOUT_SHIPPING_COUNTRY_NAMES[code]) != null ? _CHECKOUT_SHIPPING_COUNTRY_NAMES_code : code;
498
+ }).join(', ');
499
+ export function describeShippingCoverage(coverage) {
500
+ const asks = coverage.asksDestination ? 'Rates differ by destination, so checkout asks the shopper where the parcel is going before it can price one.' : '';
501
+ if (coverage.unserved.length > 0) {
502
+ return {
503
+ headline: `Checkout turns away ${coverage.unserved.length} of the ${CHECKOUT_SHIPPING_COUNTRIES.length} destinations it collects addresses for`,
504
+ detail: `No rate reaches ${coverageCountryNames(coverage.unserved)}, so an order shipping there is refused rather than posted for free. ` + (coverage.offersLocalPickup ? 'Local pickup does not cover them: it is a collection choice, not a rate, so it is offered where checkout completes and closes no gap here. ' : '') + (coverage.hasRestOfWorldZone ? 'A rest-of-world zone is already saved, so what those destinations are missing is a rate on the zone that covers them.' : 'Give them a zone with a rate, or add a rest-of-world zone and price one on it.'),
505
+ note: '',
506
+ asks
507
+ };
508
+ }
509
+ return {
510
+ headline: '',
511
+ detail: '',
512
+ note: coverage.shipsNowhere ? coverage.offersLocalPickup ? 'No rate reaches any destination, so local pickup is the only delivery choice checkout offers. Nobody is turned away and no shipping is charged.' : 'No rate reaches any destination, so checkout charges no shipping and turns nobody away.' : `Checkout can price shipping to every destination it collects an address for: ${coverageCountryNames(coverage.served)}.`,
513
+ asks
514
+ };
515
+ }
495
516
  /**
496
517
  * Emit `shipping_address_collection[allowed_countries][n]` onto a session's
497
518
  * form body. Stripe will not apply a `shipping_options` rate without an