@aglyn/plugins-commerce 1.0.0-beta.288 → 1.0.0-beta.289

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 (87) hide show
  1. package/README.md +2 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/commerce-console-page.js +29 -2
  4. package/src/lib/components/commerce-console-page.js.map +1 -1
  5. package/src/lib/components/console/commerce-analytics-card.component.js +4 -2
  6. package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -1
  7. package/src/lib/components/console/host-orders-card.component.js +5 -1
  8. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  9. package/src/lib/components/console/host-quotes-card.component.d.ts +34 -0
  10. package/src/lib/components/console/host-quotes-card.component.js +247 -0
  11. package/src/lib/components/console/host-quotes-card.component.js.map +1 -0
  12. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +33 -28
  13. package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
  14. package/src/lib/components/console/quote-editor-dialog.component.d.ts +45 -0
  15. package/src/lib/components/console/quote-editor-dialog.component.js +1266 -0
  16. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -0
  17. package/src/lib/components/console/quote-pay-link.component.d.ts +38 -0
  18. package/src/lib/components/console/quote-pay-link.component.js +213 -0
  19. package/src/lib/components/console/quote-pay-link.component.js.map +1 -0
  20. package/src/lib/components/console/quote-settings-card.component.d.ts +29 -0
  21. package/src/lib/components/console/quote-settings-card.component.js +121 -0
  22. package/src/lib/components/console/quote-settings-card.component.js.map +1 -0
  23. package/src/lib/components/console/tax-settings-card.component.js +4 -1
  24. package/src/lib/components/console/tax-settings-card.component.js.map +1 -1
  25. package/src/lib/components/quote-page-body.d.ts +33 -0
  26. package/src/lib/components/quote-page-body.js +696 -0
  27. package/src/lib/components/quote-page-body.js.map +1 -0
  28. package/src/lib/components/quote-page.d.ts +29 -0
  29. package/src/lib/components/quote-page.js +96 -0
  30. package/src/lib/components/quote-page.js.map +1 -0
  31. package/src/lib/components/storefront-payment-element.d.ts +7 -1
  32. package/src/lib/components/storefront-payment-element.js +8 -3
  33. package/src/lib/components/storefront-payment-element.js.map +1 -1
  34. package/src/lib/constants/orders-list-query.js +6 -1
  35. package/src/lib/constants/orders-list-query.js.map +1 -1
  36. package/src/lib/constants/quote-page.d.ts +30 -0
  37. package/src/lib/constants/quote-page.js +35 -0
  38. package/src/lib/constants/quote-page.js.map +1 -0
  39. package/src/lib/model/commerce-orders.d.ts +25 -2
  40. package/src/lib/model/commerce-orders.js +10 -1
  41. package/src/lib/model/commerce-orders.js.map +1 -1
  42. package/src/lib/model/commerce-quote-pay.d.ts +229 -0
  43. package/src/lib/model/commerce-quote-pay.js +393 -0
  44. package/src/lib/model/commerce-quote-pay.js.map +1 -0
  45. package/src/lib/model/commerce-quotes.d.ts +279 -0
  46. package/src/lib/model/commerce-quotes.js +522 -0
  47. package/src/lib/model/commerce-quotes.js.map +1 -0
  48. package/src/lib/model/index.d.ts +2 -0
  49. package/src/lib/model/index.js +2 -0
  50. package/src/lib/model/index.js.map +1 -1
  51. package/src/lib/model/plugin-permissions.d.ts +1 -1
  52. package/src/lib/model/plugin-permissions.js +20 -0
  53. package/src/lib/model/plugin-permissions.js.map +1 -1
  54. package/src/lib/server/api-v1/orders-and-products.js +6 -2
  55. package/src/lib/server/api-v1/orders-and-products.js.map +1 -1
  56. package/src/lib/server/billing-webhook.js +127 -52
  57. package/src/lib/server/billing-webhook.js.map +1 -1
  58. package/src/lib/server/checkout-session-expiry.d.ts +36 -0
  59. package/src/lib/server/checkout-session-expiry.js +63 -0
  60. package/src/lib/server/checkout-session-expiry.js.map +1 -0
  61. package/src/lib/server/draft-order.js +16 -8
  62. package/src/lib/server/draft-order.js.map +1 -1
  63. package/src/lib/server/person-eraser.d.ts +7 -0
  64. package/src/lib/server/person-eraser.js +24 -2
  65. package/src/lib/server/person-eraser.js.map +1 -1
  66. package/src/lib/server/pos-return.js +3 -1
  67. package/src/lib/server/pos-return.js.map +1 -1
  68. package/src/lib/server/quote-link-token.d.ts +39 -0
  69. package/src/lib/server/quote-link-token.js +97 -0
  70. package/src/lib/server/quote-link-token.js.map +1 -0
  71. package/src/lib/server/quote-pay.d.ts +83 -0
  72. package/src/lib/server/quote-pay.js +606 -0
  73. package/src/lib/server/quote-pay.js.map +1 -0
  74. package/src/lib/server/quote.d.ts +49 -0
  75. package/src/lib/server/quote.js +513 -0
  76. package/src/lib/server/quote.js.map +1 -0
  77. package/src/lib/server/related.js +2 -0
  78. package/src/lib/server/related.js.map +1 -1
  79. package/src/lib/server/site-page-resolver.d.ts +2 -0
  80. package/src/lib/server/site-page-resolver.js +75 -0
  81. package/src/lib/server/site-page-resolver.js.map +1 -1
  82. package/src/lib/server.js +14 -0
  83. package/src/lib/server.js.map +1 -1
  84. package/src/lib/site.js +6 -0
  85. package/src/lib/site.js.map +1 -1
  86. package/src/lib/transfer/tracking.server.js +6 -0
  87. package/src/lib/transfer/tracking.server.js.map +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/index.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\n/**\n * Commerce domain model (AGL-411): pure types + logic relocated from the\n * core `@aglyn/aglyn` app-utils — the commerce plugin owns its domain.\n * Context-free and framework-free, so both the plugin's client components\n * and its `/server` handlers (and other plugins/apps via\n * `@aglyn/plugins-commerce/model`) can import it.\n */\nexport * from './commerce'\nexport * from './commerce-cart'\nexport * from './commerce-check-in'\nexport * from './commerce-connect-transfer'\nexport * from './commerce-discounts'\nexport * from './commerce-dispute'\nexport * from './commerce-topics'\nexport * from './commerce-gift-cards'\nexport * from './commerce-orders'\nexport * from './commerce-printers'\nexport * from './commerce-pos'\nexport * from './commerce-pos-offline'\nexport * from './commerce-pos-kiosk'\nexport * from './product-modifiers'\nexport * from './commerce-promotions'\nexport * from './commerce-receipt'\nexport * from './commerce-reservations'\nexport * from './commerce-shipping'\nexport * from './commerce-stock-holds'\nexport * from './commerce-subscription-status'\nexport * from './commerce-tax'\nexport * from './commerce-tax-decision'\nexport * from './order-figures'\nexport * from './order-list-fields'\nexport * from './purchase-analytics'\nexport * from './tracking-url'\nexport * from './buyer-notifications'\nexport * from './order-fulfillment'\nexport * from './order-local-fulfillment'\nexport * from './local-fulfillment-settings'\nexport * from './order-events'\nexport * from './commerce-returns'\nexport * from './order-shipping-export'\nexport * from './order-status-view'\nexport * from './order-webhooks'\nexport * from './shipstation'\nexport * from './commerce-payment-methods'\nexport * from './product-channel'\nexport * from './order-annotations'\nexport * from './product-write'\nexport * from './product-reviews'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;CAMC,GACD,cAAc,gBAAY;AAC1B,cAAc,qBAAiB;AAC/B,cAAc,yBAAqB;AACnC,cAAc,iCAA6B;AAC3C,cAAc,0BAAsB;AACpC,cAAc,wBAAoB;AAClC,cAAc,uBAAmB;AACjC,cAAc,2BAAuB;AACrC,cAAc,uBAAmB;AACjC,cAAc,yBAAqB;AACnC,cAAc,oBAAgB;AAC9B,cAAc,4BAAwB;AACtC,cAAc,0BAAsB;AACpC,cAAc,yBAAqB;AACnC,cAAc,2BAAuB;AACrC,cAAc,wBAAoB;AAClC,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,4BAAwB;AACtC,cAAc,oCAAgC;AAC9C,cAAc,oBAAgB;AAC9B,cAAc,6BAAyB;AACvC,cAAc,qBAAiB;AAC/B,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,oBAAgB;AAC9B,cAAc,2BAAuB;AACrC,cAAc,yBAAqB;AACnC,cAAc,+BAA2B;AACzC,cAAc,kCAA8B;AAC5C,cAAc,oBAAgB;AAC9B,cAAc,wBAAoB;AAClC,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,sBAAkB;AAChC,cAAc,mBAAe;AAC7B,cAAc,gCAA4B;AAC1C,cAAc,uBAAmB;AACjC,cAAc,yBAAqB;AACnC,cAAc,qBAAiB;AAC/B,cAAc,uBAAmB"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/index.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\n/**\n * Commerce domain model (AGL-411): pure types + logic relocated from the\n * core `@aglyn/aglyn` app-utils — the commerce plugin owns its domain.\n * Context-free and framework-free, so both the plugin's client components\n * and its `/server` handlers (and other plugins/apps via\n * `@aglyn/plugins-commerce/model`) can import it.\n */\nexport * from './commerce'\nexport * from './commerce-cart'\nexport * from './commerce-check-in'\nexport * from './commerce-connect-transfer'\nexport * from './commerce-discounts'\nexport * from './commerce-dispute'\nexport * from './commerce-topics'\nexport * from './commerce-gift-cards'\nexport * from './commerce-orders'\nexport * from './commerce-quotes'\nexport * from './commerce-quote-pay'\nexport * from './commerce-printers'\nexport * from './commerce-pos'\nexport * from './commerce-pos-offline'\nexport * from './commerce-pos-kiosk'\nexport * from './product-modifiers'\nexport * from './commerce-promotions'\nexport * from './commerce-receipt'\nexport * from './commerce-reservations'\nexport * from './commerce-shipping'\nexport * from './commerce-stock-holds'\nexport * from './commerce-subscription-status'\nexport * from './commerce-tax'\nexport * from './commerce-tax-decision'\nexport * from './order-figures'\nexport * from './order-list-fields'\nexport * from './purchase-analytics'\nexport * from './tracking-url'\nexport * from './buyer-notifications'\nexport * from './order-fulfillment'\nexport * from './order-local-fulfillment'\nexport * from './local-fulfillment-settings'\nexport * from './order-events'\nexport * from './commerce-returns'\nexport * from './order-shipping-export'\nexport * from './order-status-view'\nexport * from './order-webhooks'\nexport * from './shipstation'\nexport * from './commerce-payment-methods'\nexport * from './product-channel'\nexport * from './order-annotations'\nexport * from './product-write'\nexport * from './product-reviews'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;CAMC,GACD,cAAc,gBAAY;AAC1B,cAAc,qBAAiB;AAC/B,cAAc,yBAAqB;AACnC,cAAc,iCAA6B;AAC3C,cAAc,0BAAsB;AACpC,cAAc,wBAAoB;AAClC,cAAc,uBAAmB;AACjC,cAAc,2BAAuB;AACrC,cAAc,uBAAmB;AACjC,cAAc,uBAAmB;AACjC,cAAc,0BAAsB;AACpC,cAAc,yBAAqB;AACnC,cAAc,oBAAgB;AAC9B,cAAc,4BAAwB;AACtC,cAAc,0BAAsB;AACpC,cAAc,yBAAqB;AACnC,cAAc,2BAAuB;AACrC,cAAc,wBAAoB;AAClC,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,4BAAwB;AACtC,cAAc,oCAAgC;AAC9C,cAAc,oBAAgB;AAC9B,cAAc,6BAAyB;AACvC,cAAc,qBAAiB;AAC/B,cAAc,yBAAqB;AACnC,cAAc,0BAAsB;AACpC,cAAc,oBAAgB;AAC9B,cAAc,2BAAuB;AACrC,cAAc,yBAAqB;AACnC,cAAc,+BAA2B;AACzC,cAAc,kCAA8B;AAC5C,cAAc,oBAAgB;AAC9B,cAAc,wBAAoB;AAClC,cAAc,6BAAyB;AACvC,cAAc,yBAAqB;AACnC,cAAc,sBAAkB;AAChC,cAAc,mBAAe;AAC7B,cAAc,gCAA4B;AAC1C,cAAc,uBAAmB;AACjC,cAAc,yBAAqB;AACnC,cAAc,qBAAiB;AAC/B,cAAc,uBAAmB"}
@@ -63,7 +63,7 @@ export declare function posPermissionRefusal(permission: PosPermission): string;
63
63
  /** The reservation keys. */
64
64
  export type ReservationPermission = 'commerce.reservations' | 'commerce.walkIns';
65
65
  /** Commerce's keys whose preset is the site role (AGL-4047). */
66
- export type CommerceSitePermission = ReservationPermission | 'commerce.orders' | 'commerce.giftCards' | 'commerce.paymentMethods' | 'commerce.shippingConnections';
66
+ export type CommerceSitePermission = ReservationPermission | 'commerce.orders' | 'commerce.quotes' | 'commerce.quotes.send' | 'commerce.giftCards' | 'commerce.paymentMethods' | 'commerce.shippingConnections';
67
67
  /** Who held reservations on a site before they were keys: Admin, Editor and Author. */
68
68
  export declare const RESERVATION_SITE_ROLE_PRESET: Readonly<Record<HostAccessRole, boolean>>;
69
69
  /**
@@ -203,6 +203,26 @@
203
203
  groupLabel: 'Commerce',
204
204
  presetFromSiteRole: true
205
205
  },
206
+ {
207
+ key: 'commerce.quotes',
208
+ label: 'Create and edit quotes',
209
+ description: 'Create, edit and delete quotes for customers. Follows the site role unless a role or member sets it.',
210
+ roleDefaults: _extends({}, RESERVATION_ORG_ROLE_PRESET),
211
+ hostRoleDefaults: _extends({}, ORDERS_SITE_ROLE_PRESET),
212
+ group: 'commerce',
213
+ groupLabel: 'Commerce',
214
+ presetFromSiteRole: true
215
+ },
216
+ {
217
+ key: 'commerce.quotes.send',
218
+ label: 'Send quotes and invoices to customers',
219
+ description: 'Copy a quote’s pay link to send to the customer, and replace it so the old link stops working. Follows the site role unless a role or member sets it.',
220
+ roleDefaults: _extends({}, RESERVATION_ORG_ROLE_PRESET),
221
+ hostRoleDefaults: _extends({}, ORDERS_SITE_ROLE_PRESET),
222
+ group: 'commerce',
223
+ groupLabel: 'Commerce',
224
+ presetFromSiteRole: true
225
+ },
206
226
  {
207
227
  key: 'commerce.giftCards',
208
228
  label: 'Issue gift cards',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/plugin-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostAccessRole } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport type { PluginOrgPermissionDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-entitlements'\nimport type { PluginPermission } from '@aglyn/aglyn/plugin-manager/plugin-permissions'\n\n/**\n * The register's finer permissions (AGL-4047), each a part of `managePos`\n * (Use the point of sale). Each REFINES it: never held without it, and,\n * where no custom role or override sets one, it follows `managePos` — so a\n * member keeps exactly the register they had until somebody narrows it.\n */\nexport type PosRegisterPermission =\n | 'posSell'\n | 'posRefund'\n | 'posDiscount'\n | 'posCashDrawer'\n | 'posDevices'\n\n/**\n * The register's keys and `posBuyHardware`, which stands on its own\n * (AGL-4047): it FOLLOWS `managePos` where no custom role or override sets\n * it, so every member keeps the hardware shop they had, but it may be\n * granted without the register — a bookkeeper who orders the store's readers\n * and never rings a sale.\n */\nexport type PosPermission = PosRegisterPermission | 'posBuyHardware'\n\n/** The tier defaults `managePos` has, which every finer key mirrors. */\nconst POS_TIER_DEFAULTS = { admin: true, editor: true, viewer: false } as const\n\n/*\n * The register is a site's own (AGL-4047): `managePos` and its five finer\n * keys are `narrowsPerSite`, so an editor of the whole workspace given Viewer\n * or Author access to one site does not work that site's register unless the\n * site grants it to them. `posBuyHardware` is not: the hardware shop spends\n * the workspace's money, and its verdict is the workspace's on every site.\n */\n\n/**\n * Commerce's plugin-declared permissions (AGL-435) — the reference\n * adopter. Pure data: the client barrel and /server entry both register\n * it, so console gating and API-side resolution agree.\n */\nexport const COMMERCE_PERMISSIONS: readonly PluginPermission[] = [\n {\n key: 'managePos',\n pluginId: 'commerce',\n label: 'Use the point of sale',\n description:\n 'Open the point of sale. Each point of sale permission below follows this one unless a role or member sets it.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n },\n {\n key: 'posSell',\n pluginId: 'commerce',\n label: 'Sell at the register',\n description: 'Ring up sales and take payment at the register, the kiosk and the point of sale apps.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posRefund',\n pluginId: 'commerce',\n label: 'Refunds and returns',\n description: 'Refund point of sale orders at the register, up to its refund limit, and take returns and exchanges.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posDiscount',\n pluginId: 'commerce',\n label: 'Give discounts at the register',\n description:\n 'Take a discount off a sale at the register. A shopper’s discount code and the store’s own promotions apply without it.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posCashDrawer',\n pluginId: 'commerce',\n label: 'Cash drawer and shifts',\n description: 'Open and close shifts, record cash in and out, print shift reports and open the drawer without a sale.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posDevices',\n pluginId: 'commerce',\n label: 'Registers and devices',\n description: 'Add, change and remove card readers, receipt printers and customer displays, and set the reader screen.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posBuyHardware',\n pluginId: 'commerce',\n label: 'Buy point of sale hardware',\n description:\n 'Order card readers and other hardware from the hardware shop. Follows \"Use the point of sale\" unless a role or member sets it, and may be allowed without it.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n follows: 'managePos',\n },\n]\n\n/** One finer register key's label, for a refusal or a disabled control's reason. */\nexport function posPermissionLabel(permission: PosPermission): string {\n return COMMERCE_PERMISSIONS.find((entry) => entry.key === permission)?.label ?? permission\n}\n\n/**\n * Whether a resolved permission map allows one of the register's finer\n * actions (AGL-4047): `managePos`, and the finer key not refused. A map\n * that does not carry the finer key at all follows `managePos` — the\n * resolvers always carry it once commerce has registered, so an absent key\n * is a map built before that, which keeps the register it had.\n *\n * `posBuyHardware` stands on its own: its own verdict when the map carries\n * one, granted with or without `managePos`, and `managePos`'s when it does\n * not.\n */\nexport function holdsPosPermission(\n permissions: Readonly<Record<string, boolean | undefined>> | null | undefined,\n permission: PosPermission,\n): boolean {\n if (permission === 'posBuyHardware') {\n const own = permissions?.[permission]\n return typeof own === 'boolean' ? own : permissions?.['managePos'] === true\n }\n return permissions?.['managePos'] === true && permissions?.[permission] !== false\n}\n\n/**\n * Whether a member may open the hardware shop at all (AGL-4047): see what\n * it offers and what the store ordered. Whoever works the register, and\n * whoever may buy hardware without working it.\n */\nexport function mayVisitPosHardwareShop(\n permissions: Readonly<Record<string, boolean | undefined>> | null | undefined,\n): boolean {\n return permissions?.['managePos'] === true || holdsPosPermission(permissions, 'posBuyHardware')\n}\n\n/** The refusal a register route answers when a finer key is not held. */\nexport function posPermissionRefusal(permission: PosPermission): string {\n return `Your role does not include \"${posPermissionLabel(permission)}\" — ask an organization admin`\n}\n\n/*\n * RESERVATIONS (AGL-4047).\n *\n * A site's stays and what they book were decided by the member's role ON THE\n * SITE: Admin, Editor and Author could book a walk-in, change a stay's guests\n * and edit or cancel stays, and a Viewer could not (`canWriteHostContent` in\n * the rules, the same three roles at the routes). Those are now keys whose\n * preset is that site role (`presetFromSiteRole`), so every member keeps\n * exactly the access they had, and a custom role, a member's own permissions\n * or a site's toggle can grant or refuse either one: the window's staff can\n * be allowed walk-ins without editing anyone's booking.\n */\n\n/** The reservation keys. */\nexport type ReservationPermission = 'commerce.reservations' | 'commerce.walkIns'\n\n/** Commerce's keys whose preset is the site role (AGL-4047). */\nexport type CommerceSitePermission =\n | ReservationPermission\n | 'commerce.orders'\n | 'commerce.giftCards'\n | 'commerce.paymentMethods'\n | 'commerce.shippingConnections'\n\n/** Who held reservations on a site before they were keys: Admin, Editor and Author. */\nexport const RESERVATION_SITE_ROLE_PRESET: Readonly<Record<HostAccessRole, boolean>> = {\n admin: true,\n editor: true,\n author: true,\n viewer: false,\n}\n\n/** The same, per workspace role: the answer with no site named. */\nconst RESERVATION_ORG_ROLE_PRESET = { owner: true, admin: true, editor: true, viewer: false } as const\n\n/**\n * Who worked a site's orders and gift cards before they were keys: the site's\n * Admin and Editor (an Author edits the catalog and never touched an order).\n */\nexport const ORDERS_SITE_ROLE_PRESET: Readonly<Record<HostAccessRole, boolean>> = {\n admin: true,\n editor: true,\n author: false,\n viewer: false,\n}\n\n/**\n * Who changed a site's payment methods and its shipping app connections\n * before they were keys: the site's Admin alone (an Editor could see them, as\n * whoever works the orders still can).\n */\nexport const STORE_SETTINGS_SITE_ROLE_PRESET: Readonly<Record<HostAccessRole, boolean>> = {\n admin: true,\n editor: false,\n author: false,\n viewer: false,\n}\n\n/** The same, per workspace role: the answer with no site named. */\nexport const STORE_SETTINGS_ORG_ROLE_PRESET = { owner: true, admin: true, editor: false, viewer: false } as const\n\n/**\n * The reservation keys as commerce declares them into the org permission\n * catalog (AGL-4047), registered beside `COMMERCE_PERMISSIONS`.\n */\nexport const COMMERCE_SITE_PERMISSIONS: readonly PluginOrgPermissionDeclaration[] = [\n {\n key: 'commerce.reservations',\n label: 'Manage reservations',\n description:\n 'Edit, cancel and check out stays, and add, change and remove what can be booked. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...RESERVATION_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.walkIns',\n label: 'Book walk-ins and change guests',\n description:\n 'Book a walk-in stay and change how many guests a stay is for. Follows \"Manage reservations\" unless a role or member sets it, and may be allowed without it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...RESERVATION_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n follows: 'commerce.reservations',\n },\n {\n key: 'commerce.orders',\n label: 'Manage orders',\n description:\n 'Fulfill, cancel and print orders, resend receipts, build draft orders, take returns and record shipments. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...ORDERS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.giftCards',\n label: 'Issue gift cards',\n description: 'Issue, adjust and void the store’s gift cards. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...ORDERS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.paymentMethods',\n label: 'Manage payment methods',\n description:\n 'Turn the store’s payment methods on and off. Anyone with \"Manage orders\" sees them. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...STORE_SETTINGS_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...STORE_SETTINGS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.shippingConnections',\n label: 'Connect shipping apps',\n description:\n 'Connect, sync and disconnect ShipStation and ShippingEasy, and show or replace their credentials. Anyone with \"Manage orders\" sees whether they are connected. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...STORE_SETTINGS_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...STORE_SETTINGS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n]\n\n/** The refusal a door answers when a commerce site key is not held: names the permission. */\nexport function commerceSitePermissionRefusal(key: CommerceSitePermission): string {\n const label = COMMERCE_SITE_PERMISSIONS.find((entry) => entry.key === key)?.label ?? key\n return `Your role does not include \"${label}\" — ask an organization admin`\n}\n\n/** One reservation key's label, for a refusal. */\nexport function reservationPermissionLabel(permission: ReservationPermission): string {\n return COMMERCE_SITE_PERMISSIONS.find((entry) => entry.key === permission)?.label ?? permission\n}\n\n/** The refusal a reservation route answers when its key is not held; names the permission, never a role. */\nexport function reservationPermissionRefusal(permission: ReservationPermission): string {\n return commerceSitePermissionRefusal(permission)\n}\n"],"names":["POS_TIER_DEFAULTS","admin","editor","viewer","COMMERCE_PERMISSIONS","key","pluginId","label","description","defaults","groupLabel","narrowsPerSite","refines","follows","posPermissionLabel","permission","find","entry","holdsPosPermission","permissions","own","mayVisitPosHardwareShop","posPermissionRefusal","RESERVATION_SITE_ROLE_PRESET","author","RESERVATION_ORG_ROLE_PRESET","owner","ORDERS_SITE_ROLE_PRESET","STORE_SETTINGS_SITE_ROLE_PRESET","STORE_SETTINGS_ORG_ROLE_PRESET","COMMERCE_SITE_PERMISSIONS","roleDefaults","hostRoleDefaults","group","presetFromSiteRole","commerceSitePermissionRefusal","reservationPermissionLabel","reservationPermissionRefusal"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AA4BD,sEAAsE,GACtE,MAAMA,oBAAoB;IAAEC,OAAO;IAAMC,QAAQ;IAAMC,QAAQ;AAAM;AAErE;;;;;;CAMC,GAED;;;;CAIC,GACD,OAAO,MAAMC,uBAAoD;IAC/D;QACEC,KAAK;QACLC,UAAU;QACVC,OAAO;QACPC,aACE;QACFC,UAAU,aAAKT;QACfU,YAAY;QACZC,gBAAgB;IAClB;IACA;QACEN,KAAK;QACLC,UAAU;QACVC,OAAO;QACPC,aAAa;QACbC,UAAU,aAAKT;QACfU,YAAY;QACZC,gBAAgB;QAChBC,SAAS;IACX;IACA;QACEP,KAAK;QACLC,UAAU;QACVC,OAAO;QACPC,aAAa;QACbC,UAAU,aAAKT;QACfU,YAAY;QACZC,gBAAgB;QAChBC,SAAS;IACX;IACA;QACEP,KAAK;QACLC,UAAU;QACVC,OAAO;QACPC,aACE;QACFC,UAAU,aAAKT;QACfU,YAAY;QACZC,gBAAgB;QAChBC,SAAS;IACX;IACA;QACEP,KAAK;QACLC,UAAU;QACVC,OAAO;QACPC,aAAa;QACbC,UAAU,aAAKT;QACfU,YAAY;QACZC,gBAAgB;QAChBC,SAAS;IACX;IACA;QACEP,KAAK;QACLC,UAAU;QACVC,OAAO;QACPC,aAAa;QACbC,UAAU,aAAKT;QACfU,YAAY;QACZC,gBAAgB;QAChBC,SAAS;IACX;IACA;QACEP,KAAK;QACLC,UAAU;QACVC,OAAO;QACPC,aACE;QACFC,UAAU,aAAKT;QACfU,YAAY;QACZG,SAAS;IACX;CACD,CAAA;AAED,kFAAkF,GAClF,OAAO,SAASC,mBAAmBC,UAAyB;;QACnDX;IAAP,gBAAOA,6BAAAA,qBAAqBY,IAAI,CAAC,CAACC,QAAUA,MAAMZ,GAAG,KAAKU,gCAAnDX,2BAAgEG,KAAK,mBAAIQ;AAClF;AAEA;;;;;;;;;;CAUC,GACD,OAAO,SAASG,mBACdC,WAA6E,EAC7EJ,UAAyB;IAEzB,IAAIA,eAAe,kBAAkB;QACnC,MAAMK,MAAMD,+BAAAA,WAAa,CAACJ,WAAW;QACrC,OAAO,OAAOK,QAAQ,YAAYA,MAAMD,CAAAA,+BAAAA,WAAa,CAAC,YAAY,MAAK;IACzE;IACA,OAAOA,CAAAA,+BAAAA,WAAa,CAAC,YAAY,MAAK,QAAQA,CAAAA,+BAAAA,WAAa,CAACJ,WAAW,MAAK;AAC9E;AAEA;;;;CAIC,GACD,OAAO,SAASM,wBACdF,WAA6E;IAE7E,OAAOA,CAAAA,+BAAAA,WAAa,CAAC,YAAY,MAAK,QAAQD,mBAAmBC,aAAa;AAChF;AAEA,uEAAuE,GACvE,OAAO,SAASG,qBAAqBP,UAAyB;IAC5D,OAAO,CAAC,4BAA4B,EAAED,mBAAmBC,YAAY,6BAA6B,CAAC;AACrG;AA0BA,qFAAqF,GACrF,OAAO,MAAMQ,+BAA0E;IACrFtB,OAAO;IACPC,QAAQ;IACRsB,QAAQ;IACRrB,QAAQ;AACV,EAAC;AAED,iEAAiE,GACjE,MAAMsB,8BAA8B;IAAEC,OAAO;IAAMzB,OAAO;IAAMC,QAAQ;IAAMC,QAAQ;AAAM;AAE5F;;;CAGC,GACD,OAAO,MAAMwB,0BAAqE;IAChF1B,OAAO;IACPC,QAAQ;IACRsB,QAAQ;IACRrB,QAAQ;AACV,EAAC;AAED;;;;CAIC,GACD,OAAO,MAAMyB,kCAA6E;IACxF3B,OAAO;IACPC,QAAQ;IACRsB,QAAQ;IACRrB,QAAQ;AACV,EAAC;AAED,iEAAiE,GACjE,OAAO,MAAM0B,iCAAiC;IAAEH,OAAO;IAAMzB,OAAO;IAAMC,QAAQ;IAAOC,QAAQ;AAAM,EAAU;AAEjH;;;CAGC,GACD,OAAO,MAAM2B,4BAAuE;IAClF;QACEzB,KAAK;QACLE,OAAO;QACPC,aACE;QACFuB,cAAc,aAAKN;QACnBO,kBAAkB,aAAKT;QACvBU,OAAO;QACPvB,YAAY;QACZwB,oBAAoB;IACtB;IACA;QACE7B,KAAK;QACLE,OAAO;QACPC,aACE;QACFuB,cAAc,aAAKN;QACnBO,kBAAkB,aAAKT;QACvBU,OAAO;QACPvB,YAAY;QACZwB,oBAAoB;QACpBrB,SAAS;IACX;IACA;QACER,KAAK;QACLE,OAAO;QACPC,aACE;QACFuB,cAAc,aAAKN;QACnBO,kBAAkB,aAAKL;QACvBM,OAAO;QACPvB,YAAY;QACZwB,oBAAoB;IACtB;IACA;QACE7B,KAAK;QACLE,OAAO;QACPC,aAAa;QACbuB,cAAc,aAAKN;QACnBO,kBAAkB,aAAKL;QACvBM,OAAO;QACPvB,YAAY;QACZwB,oBAAoB;IACtB;IACA;QACE7B,KAAK;QACLE,OAAO;QACPC,aACE;QACFuB,cAAc,aAAKF;QACnBG,kBAAkB,aAAKJ;QACvBK,OAAO;QACPvB,YAAY;QACZwB,oBAAoB;IACtB;IACA;QACE7B,KAAK;QACLE,OAAO;QACPC,aACE;QACFuB,cAAc,aAAKF;QACnBG,kBAAkB,aAAKJ;QACvBK,OAAO;QACPvB,YAAY;QACZwB,oBAAoB;IACtB;CACD,CAAA;AAED,2FAA2F,GAC3F,OAAO,SAASC,8BAA8B9B,GAA2B;;QACzDyB;IAAd,MAAMvB,iBAAQuB,kCAAAA,0BAA0Bd,IAAI,CAAC,CAACC,QAAUA,MAAMZ,GAAG,KAAKA,yBAAxDyB,gCAA8DvB,KAAK,mBAAIF;IACrF,OAAO,CAAC,4BAA4B,EAAEE,MAAM,6BAA6B,CAAC;AAC5E;AAEA,gDAAgD,GAChD,OAAO,SAAS6B,2BAA2BrB,UAAiC;;QACnEe;IAAP,gBAAOA,kCAAAA,0BAA0Bd,IAAI,CAAC,CAACC,QAAUA,MAAMZ,GAAG,KAAKU,gCAAxDe,gCAAqEvB,KAAK,mBAAIQ;AACvF;AAEA,0GAA0G,GAC1G,OAAO,SAASsB,6BAA6BtB,UAAiC;IAC5E,OAAOoB,8BAA8BpB;AACvC"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/plugin-permissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { HostAccessRole } from '@aglyn/aglyn/foundation/definitions/organization.types'\nimport type { PluginOrgPermissionDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-entitlements'\nimport type { PluginPermission } from '@aglyn/aglyn/plugin-manager/plugin-permissions'\n\n/**\n * The register's finer permissions (AGL-4047), each a part of `managePos`\n * (Use the point of sale). Each REFINES it: never held without it, and,\n * where no custom role or override sets one, it follows `managePos` — so a\n * member keeps exactly the register they had until somebody narrows it.\n */\nexport type PosRegisterPermission =\n | 'posSell'\n | 'posRefund'\n | 'posDiscount'\n | 'posCashDrawer'\n | 'posDevices'\n\n/**\n * The register's keys and `posBuyHardware`, which stands on its own\n * (AGL-4047): it FOLLOWS `managePos` where no custom role or override sets\n * it, so every member keeps the hardware shop they had, but it may be\n * granted without the register — a bookkeeper who orders the store's readers\n * and never rings a sale.\n */\nexport type PosPermission = PosRegisterPermission | 'posBuyHardware'\n\n/** The tier defaults `managePos` has, which every finer key mirrors. */\nconst POS_TIER_DEFAULTS = { admin: true, editor: true, viewer: false } as const\n\n/*\n * The register is a site's own (AGL-4047): `managePos` and its five finer\n * keys are `narrowsPerSite`, so an editor of the whole workspace given Viewer\n * or Author access to one site does not work that site's register unless the\n * site grants it to them. `posBuyHardware` is not: the hardware shop spends\n * the workspace's money, and its verdict is the workspace's on every site.\n */\n\n/**\n * Commerce's plugin-declared permissions (AGL-435) — the reference\n * adopter. Pure data: the client barrel and /server entry both register\n * it, so console gating and API-side resolution agree.\n */\nexport const COMMERCE_PERMISSIONS: readonly PluginPermission[] = [\n {\n key: 'managePos',\n pluginId: 'commerce',\n label: 'Use the point of sale',\n description:\n 'Open the point of sale. Each point of sale permission below follows this one unless a role or member sets it.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n },\n {\n key: 'posSell',\n pluginId: 'commerce',\n label: 'Sell at the register',\n description: 'Ring up sales and take payment at the register, the kiosk and the point of sale apps.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posRefund',\n pluginId: 'commerce',\n label: 'Refunds and returns',\n description: 'Refund point of sale orders at the register, up to its refund limit, and take returns and exchanges.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posDiscount',\n pluginId: 'commerce',\n label: 'Give discounts at the register',\n description:\n 'Take a discount off a sale at the register. A shopper’s discount code and the store’s own promotions apply without it.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posCashDrawer',\n pluginId: 'commerce',\n label: 'Cash drawer and shifts',\n description: 'Open and close shifts, record cash in and out, print shift reports and open the drawer without a sale.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posDevices',\n pluginId: 'commerce',\n label: 'Registers and devices',\n description: 'Add, change and remove card readers, receipt printers and customer displays, and set the reader screen.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n narrowsPerSite: true,\n refines: 'managePos',\n },\n {\n key: 'posBuyHardware',\n pluginId: 'commerce',\n label: 'Buy point of sale hardware',\n description:\n 'Order card readers and other hardware from the hardware shop. Follows \"Use the point of sale\" unless a role or member sets it, and may be allowed without it.',\n defaults: { ...POS_TIER_DEFAULTS },\n groupLabel: 'Commerce',\n follows: 'managePos',\n },\n]\n\n/** One finer register key's label, for a refusal or a disabled control's reason. */\nexport function posPermissionLabel(permission: PosPermission): string {\n return COMMERCE_PERMISSIONS.find((entry) => entry.key === permission)?.label ?? permission\n}\n\n/**\n * Whether a resolved permission map allows one of the register's finer\n * actions (AGL-4047): `managePos`, and the finer key not refused. A map\n * that does not carry the finer key at all follows `managePos` — the\n * resolvers always carry it once commerce has registered, so an absent key\n * is a map built before that, which keeps the register it had.\n *\n * `posBuyHardware` stands on its own: its own verdict when the map carries\n * one, granted with or without `managePos`, and `managePos`'s when it does\n * not.\n */\nexport function holdsPosPermission(\n permissions: Readonly<Record<string, boolean | undefined>> | null | undefined,\n permission: PosPermission,\n): boolean {\n if (permission === 'posBuyHardware') {\n const own = permissions?.[permission]\n return typeof own === 'boolean' ? own : permissions?.['managePos'] === true\n }\n return permissions?.['managePos'] === true && permissions?.[permission] !== false\n}\n\n/**\n * Whether a member may open the hardware shop at all (AGL-4047): see what\n * it offers and what the store ordered. Whoever works the register, and\n * whoever may buy hardware without working it.\n */\nexport function mayVisitPosHardwareShop(\n permissions: Readonly<Record<string, boolean | undefined>> | null | undefined,\n): boolean {\n return permissions?.['managePos'] === true || holdsPosPermission(permissions, 'posBuyHardware')\n}\n\n/** The refusal a register route answers when a finer key is not held. */\nexport function posPermissionRefusal(permission: PosPermission): string {\n return `Your role does not include \"${posPermissionLabel(permission)}\" — ask an organization admin`\n}\n\n/*\n * RESERVATIONS (AGL-4047).\n *\n * A site's stays and what they book were decided by the member's role ON THE\n * SITE: Admin, Editor and Author could book a walk-in, change a stay's guests\n * and edit or cancel stays, and a Viewer could not (`canWriteHostContent` in\n * the rules, the same three roles at the routes). Those are now keys whose\n * preset is that site role (`presetFromSiteRole`), so every member keeps\n * exactly the access they had, and a custom role, a member's own permissions\n * or a site's toggle can grant or refuse either one: the window's staff can\n * be allowed walk-ins without editing anyone's booking.\n */\n\n/** The reservation keys. */\nexport type ReservationPermission = 'commerce.reservations' | 'commerce.walkIns'\n\n/** Commerce's keys whose preset is the site role (AGL-4047). */\nexport type CommerceSitePermission =\n | ReservationPermission\n | 'commerce.orders'\n | 'commerce.quotes'\n | 'commerce.quotes.send'\n | 'commerce.giftCards'\n | 'commerce.paymentMethods'\n | 'commerce.shippingConnections'\n\n/** Who held reservations on a site before they were keys: Admin, Editor and Author. */\nexport const RESERVATION_SITE_ROLE_PRESET: Readonly<Record<HostAccessRole, boolean>> = {\n admin: true,\n editor: true,\n author: true,\n viewer: false,\n}\n\n/** The same, per workspace role: the answer with no site named. */\nconst RESERVATION_ORG_ROLE_PRESET = { owner: true, admin: true, editor: true, viewer: false } as const\n\n/**\n * Who worked a site's orders and gift cards before they were keys: the site's\n * Admin and Editor (an Author edits the catalog and never touched an order).\n */\nexport const ORDERS_SITE_ROLE_PRESET: Readonly<Record<HostAccessRole, boolean>> = {\n admin: true,\n editor: true,\n author: false,\n viewer: false,\n}\n\n/**\n * Who changed a site's payment methods and its shipping app connections\n * before they were keys: the site's Admin alone (an Editor could see them, as\n * whoever works the orders still can).\n */\nexport const STORE_SETTINGS_SITE_ROLE_PRESET: Readonly<Record<HostAccessRole, boolean>> = {\n admin: true,\n editor: false,\n author: false,\n viewer: false,\n}\n\n/** The same, per workspace role: the answer with no site named. */\nexport const STORE_SETTINGS_ORG_ROLE_PRESET = { owner: true, admin: true, editor: false, viewer: false } as const\n\n/**\n * The reservation keys as commerce declares them into the org permission\n * catalog (AGL-4047), registered beside `COMMERCE_PERMISSIONS`.\n */\nexport const COMMERCE_SITE_PERMISSIONS: readonly PluginOrgPermissionDeclaration[] = [\n {\n key: 'commerce.reservations',\n label: 'Manage reservations',\n description:\n 'Edit, cancel and check out stays, and add, change and remove what can be booked. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...RESERVATION_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.walkIns',\n label: 'Book walk-ins and change guests',\n description:\n 'Book a walk-in stay and change how many guests a stay is for. Follows \"Manage reservations\" unless a role or member sets it, and may be allowed without it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...RESERVATION_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n follows: 'commerce.reservations',\n },\n {\n key: 'commerce.orders',\n label: 'Manage orders',\n description:\n 'Fulfill, cancel and print orders, resend receipts, build draft orders, take returns and record shipments. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...ORDERS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.quotes',\n label: 'Create and edit quotes',\n description:\n 'Create, edit and delete quotes for customers. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...ORDERS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.quotes.send',\n label: 'Send quotes and invoices to customers',\n description:\n 'Copy a quote’s pay link to send to the customer, and replace it so the old link stops working. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...ORDERS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.giftCards',\n label: 'Issue gift cards',\n description: 'Issue, adjust and void the store’s gift cards. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...RESERVATION_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...ORDERS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.paymentMethods',\n label: 'Manage payment methods',\n description:\n 'Turn the store’s payment methods on and off. Anyone with \"Manage orders\" sees them. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...STORE_SETTINGS_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...STORE_SETTINGS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n {\n key: 'commerce.shippingConnections',\n label: 'Connect shipping apps',\n description:\n 'Connect, sync and disconnect ShipStation and ShippingEasy, and show or replace their credentials. Anyone with \"Manage orders\" sees whether they are connected. Follows the site role unless a role or member sets it.',\n roleDefaults: { ...STORE_SETTINGS_ORG_ROLE_PRESET },\n hostRoleDefaults: { ...STORE_SETTINGS_SITE_ROLE_PRESET },\n group: 'commerce',\n groupLabel: 'Commerce',\n presetFromSiteRole: true,\n },\n]\n\n/** The refusal a door answers when a commerce site key is not held: names the permission. */\nexport function commerceSitePermissionRefusal(key: CommerceSitePermission): string {\n const label = COMMERCE_SITE_PERMISSIONS.find((entry) => entry.key === key)?.label ?? key\n return `Your role does not include \"${label}\" — ask an organization admin`\n}\n\n/** One reservation key's label, for a refusal. */\nexport function reservationPermissionLabel(permission: ReservationPermission): string {\n return COMMERCE_SITE_PERMISSIONS.find((entry) => entry.key === permission)?.label ?? permission\n}\n\n/** The refusal a reservation route answers when its key is not held; names the permission, never a role. */\nexport function reservationPermissionRefusal(permission: ReservationPermission): string {\n return commerceSitePermissionRefusal(permission)\n}\n"],"names":["POS_TIER_DEFAULTS","admin","editor","viewer","COMMERCE_PERMISSIONS","key","pluginId","label","description","defaults","groupLabel","narrowsPerSite","refines","follows","posPermissionLabel","permission","find","entry","holdsPosPermission","permissions","own","mayVisitPosHardwareShop","posPermissionRefusal","RESERVATION_SITE_ROLE_PRESET","author","RESERVATION_ORG_ROLE_PRESET","owner","ORDERS_SITE_ROLE_PRESET","STORE_SETTINGS_SITE_ROLE_PRESET","STORE_SETTINGS_ORG_ROLE_PRESET","COMMERCE_SITE_PERMISSIONS","roleDefaults","hostRoleDefaults","group","presetFromSiteRole","commerceSitePermissionRefusal","reservationPermissionLabel","reservationPermissionRefusal"],"mappings":"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"}
@@ -20,6 +20,7 @@ import { paginate, readJsonBody, requireScope, serialize } from "@aglyn/tenant-d
20
20
  import { BUNDLE_ID } from "../../constants/bundle-common.js";
21
21
  import { recordOrderShipment } from "../fulfill-order.js";
22
22
  import { orderViewFromData } from "./order-view.js";
23
+ import { isUnpaidQuote } from "../../model/commerce-quotes.js";
23
24
  /**
24
25
  * A site's orders and products on the customer REST API,
25
26
  * `/v1/sites/{siteId}/orders/…` and `/v1/sites/{siteId}/products/…`
@@ -92,7 +93,8 @@ export async function handleOrders(request, ctx, segments, url) {
92
93
  const collection = hostRef(ctx, hostId).collection('orders');
93
94
  if (orderId) {
94
95
  const snap = await collection.doc(orderId).get();
95
- if (!snap.exists) {
96
+ // An unpaid quote is not an order yet (AGL-4093).
97
+ if (!snap.exists || isUnpaidQuote(snap.data())) {
96
98
  return ApiErrors.notFound({
97
99
  message: 'No such order',
98
100
  headers: ctx.headers
@@ -115,7 +117,9 @@ export async function handleOrders(request, ctx, segments, url) {
115
117
  // `has_more`, never a page's length).
116
118
  if (channel && channel !== 'online') query = query.where('channel', '==', channel);
117
119
  const { docs, nextCursor } = await paginate(query, url);
118
- const data = docs.map(orderView).filter((order)=>channel === 'online' ? order.channel === 'online' : true);
120
+ // Unpaid quotes share the collection and are not orders yet (AGL-4093);
121
+ // left out after the read, like `online` above, so a page can be short.
122
+ const data = docs.filter((doc)=>!isUnpaidQuote(doc.data())).map(orderView).filter((order)=>channel === 'online' ? order.channel === 'online' : true);
119
123
  return listResponse(data, nextCursor, ctx.headers);
120
124
  }
121
125
  /**
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/server/api-v1/orders-and-products.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 { checkEntitlement, isHostPluginEnabled } from '@aglyn/aglyn/server'\nimport {\n ApiErrors,\n apiJson,\n listResponse,\n} from '@aglyn/tenant-data-admin'\nimport {\n type ApiV1Context,\n paginate,\n readJsonBody,\n requireScope,\n serialize,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport { BUNDLE_ID } from '../../constants/bundle-common'\nimport { type OrderFulfilmentTarget, recordOrderShipment } from '../fulfill-order'\nimport { orderViewFromData } from './order-view'\n\n/**\n * A site's orders and products on the customer REST API,\n * `/v1/sites/{siteId}/orders/…` and `/v1/sites/{siteId}/products/…`\n * (AGL-1928, AGL-2461, AGL-3080). Registered from this plugin's console\n * server declarations; the console's `/v1` router owns the pipeline in\n * front — the key, the plan's API access, the request quota, the rate limit —\n * and refuses a site the key's organization does not own before it hands a\n * request here.\n */\n\n/**\n * Commerce resources need the `commerce` entitlement as well as the scope\n * (AGL-1928). `apiAccess` alone is the wrong gate here: it says the org may\n * call the API at all, not that it may still use the store. AGL-1873 closed\n * exactly this class on the write side — two money doors that asked the\n * plugin switch (`org.enabledPlugins`) instead of the plan, so a lapsed org\n * kept selling — and the same reasoning applies to a read. `enabledPlugins`\n * is the customer's own on/off switch and survives a downgrade; the plan does\n * not, and the published rule is that paid features stop at the door when the\n * plan no longer includes them.\n *\n * Answered as `plan_required` rather than `not_found`, deliberately. Hiding a\n * store that plainly exists behind a 404 sends an integrator hunting a wrong\n * site id; the honest answer names the plan.\n */\nfunction requireCommerce(ctx: ApiV1Context): Response | null {\n return checkEntitlement(ctx.org, 'commerce')\n ? null\n : ApiErrors.planRequired({\n message: 'Commerce is not included in this organization’s plan',\n code: 'commerce',\n headers: ctx.headers,\n })\n}\n\nfunction hostRef(ctx: ApiV1Context, hostId: string) {\n return ctx.firestore.collection('hosts').doc(hostId)\n}\n\n/**\n * Orders carry a legacy Commerce Starter shape (AGL-90) alongside the modern\n * one: `amountCents`/`feeCents` at the top level rather than a `totals` map.\n * The console lifts them through `CommerceModel.liftLegacyOrder` and reads\n * `lifted.totals?.totalCents ?? order.amountCents`. The API cannot publish two\n * shapes for one object, so the legacy fields are folded into `totals` here\n * and a client only ever sees the modern one. `channel` defaults to `online`\n * for the same reason the console's list does — an absent channel is a\n * pre-channel order, not an unknown one.\n */\nfunction orderView(doc: FirebaseFirestore.DocumentSnapshot) {\n return orderViewFromData(doc.id, doc.data() ?? {})\n}\n\nexport async function handleOrders(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const [, hostId, , orderId] = segments\n // `orders:write` is checked BEFORE `orders:read`, matching `handleSites`\n // and `handleScopedMedia` (AGL-900): a fulfilment key that only records\n // shipments must not be told it lacks a read scope it was never meant to\n // hold.\n if (request.method === 'PATCH' && orderId) {\n const deniedWrite = requireScope(ctx, 'orders:write')\n if (deniedWrite) return deniedWrite\n const unentitledWrite = requireCommerce(ctx)\n if (unentitledWrite) return unentitledWrite\n return updateOrder(request, ctx, hostId, orderId)\n }\n const denied = requireScope(ctx, 'orders:read')\n if (denied) return denied\n const unentitled = requireCommerce(ctx)\n if (unentitled) return unentitled\n if (request.method !== 'GET') {\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: orderId ? 'GET, PATCH' : 'GET' },\n })\n }\n const collection = hostRef(ctx, hostId).collection('orders')\n\n if (orderId) {\n const snap = await collection.doc(orderId).get()\n if (!snap.exists) {\n return ApiErrors.notFound({ message: 'No such order', headers: ctx.headers })\n }\n return apiJson(orderView(snap), { headers: ctx.headers })\n }\n\n let query: FirebaseFirestore.Query = collection\n const status = url.searchParams.get('status')\n if (status) query = query.where('status', '==', status)\n const channel = url.searchParams.get('channel')\n // `online` is the DEFAULT, not a stored value on older orders, so filtering\n // for it in Firestore would silently drop every pre-channel order. Those are\n // exactly the oldest orders an accounting backfill is reaching for, so this\n // one value is filtered after the read instead. The page can therefore come\n // back shorter than `limit` while `has_more` is still true — which the\n // published pagination contract already tells clients to expect (check\n // `has_more`, never a page's length).\n if (channel && channel !== 'online') query = query.where('channel', '==', channel)\n const { docs, nextCursor } = await paginate(query, url)\n const data = docs\n .map(orderView)\n .filter((order) => (channel === 'online' ? order.channel === 'online' : true))\n return listResponse(data, nextCursor, ctx.headers)\n}\n\n/**\n * Statuses this endpoint will move an order TO, and the two it names as\n * refused. Kept as data so the 400 can list them and the docs can be checked\n * against the same source the handler branches on.\n */\nconst ORDER_WRITE_TARGETS: OrderFulfilmentTarget[] = ['fulfilled', 'delivered']\n\n/** The keys a PATCH may carry (AGL-3611 added the last three). */\nconst ORDER_WRITABLE_KEYS = ['status', 'carrier', 'trackingNumber', 'trackingUrl', 'lineItems', 'notify']\n\n/**\n * Transitions that exist in the commerce model and are DELIBERATELY not\n * reachable here — refused by name with a 400 that says why, never silently\n * ignored. `cancelled` releases held stock under its own transaction and\n * `refunded` moves money under another; admitting either here would hand a\n * caller a door around exactly the specifics those two routes exist to\n * enforce, and an API key is the credential least able to answer the\n * questions they ask.\n */\nconst ORDER_WRITE_REFUSED: Record<string, string> = {\n cancelled:\n 'Canceling an order releases held stock, so it is not part of this endpoint. Cancel it in the console.',\n refunded:\n 'Refunding an order moves money, so it is not part of this endpoint. Refund it in the console.',\n}\n\n/**\n * `PATCH /v1/sites/{siteId}/orders/{orderId}` — record a shipment (AGL-2461).\n *\n * ## The write is the console's, shared\n *\n * The transition rule, the transaction that re-asks it under the write, the\n * fulfillment append and the timeline entry are `recordOrderShipment`'s,\n * the one implementation the console's `commerce/fulfill-order` route calls\n * too. A second copy of `ORDER_TRANSITIONS` here would be the bug rather\n * than the fix: two tables drift, and drift here means the API writing an\n * order status the console forbids — `paid → delivered` skipping\n * fulfilment, or a write onto a `refunded` order. That is the class\n * AGL-1818/AGL-1819 exist to close, and it is money-adjacent.\n *\n * ## What THIS function is responsible for: all of the authorization\n *\n * `recordOrderShipment` is pre-authorized by contract — it takes `hostId`\n * on trust — so every gate is here, and all four are load-bearing:\n *\n * 1. `orders:write` on the key (checked by the caller, above).\n * 2. The `commerce` PLAN entitlement (checked by the caller, above) — the\n * plan, never the plugin switch, which is the AGL-1873 distinction.\n * 3. **Org owns the site** — the console's `handleSites` refuses an unowned\n * `hostId` with a 404 before it hands any site resource to its plugin,\n * which is what keeps this from being a cross-tenant write primitive\n * addressable by anyone who can guess a host id. Deliberately NOT\n * re-checked here: a second copy would be a gate no test can redden\n * (removing either one alone leaves the other answering), and an unproven\n * guard on a cross-tenant write is worse than the one guard a test\n * actually holds. `api-v1-order-fulfilment.spec.ts`'s \"CANNOT move\n * another org's order\" case is that test, and it fails when\n * `handleSites`' `orgOwnsHost` is removed.\n * 4. **This plugin is switched on for this site.** The resource is\n * registered for every organization the console serves, so its being here\n * says nothing about one org's configuration; without this an org that\n * switched commerce off for a site would still accept writes into it,\n * which is the per-site enablement rule (AGL-1014) the plugin API\n * dispatcher applies to every other commerce door.\n *\n * ## No `Idempotency-Key`\n *\n * None is needed and none is accepted: the write returns without writing when\n * the order is already in the target status, so a retry lands the same state\n * AND returns the same `200` with the same order body — the contract\n * `updateRecord`, `updateContact` and `updateFormSubmission` are held to.\n */\nasync function updateOrder(\n request: Request,\n ctx: ApiV1Context,\n hostId: string,\n orderId: string,\n): Promise<Response> {\n const body = await readJsonBody(request)\n const unknown = Object.keys(body).filter((key) => !ORDER_WRITABLE_KEYS.includes(key))\n if (unknown.length > 0) {\n // Named, not dropped — the `updateFormSubmission` / `updateContact` rule.\n // A silently ignored `trackingUrl` here reads as \"we recorded your\n // shipment as you described it\" when half of it went nowhere, and the\n // caller is a warehouse system that will never look again.\n return ApiErrors.badRequest({\n message:\n 'Only `status`, `carrier`, `trackingNumber`, `trackingUrl`, `lineItems` and `notify` can be set on an order',\n code: 'validation_failed',\n fields: Object.fromEntries(\n unknown.map((key) => [key, 'Not writable on an order']),\n ),\n headers: ctx.headers,\n })\n }\n\n const status = String(body.status ?? '')\n const refusal = ORDER_WRITE_REFUSED[status]\n if (refusal) {\n return ApiErrors.badRequest({\n message: refusal,\n code: 'validation_failed',\n fields: { status: refusal },\n headers: ctx.headers,\n })\n }\n if (!(ORDER_WRITE_TARGETS as string[]).includes(status)) {\n return ApiErrors.badRequest({\n message: 'Order failed validation',\n code: 'validation_failed',\n fields: {\n status: `Must be one of: ${ORDER_WRITE_TARGETS.join(', ')}`,\n },\n headers: ctx.headers,\n })\n }\n // Bounded exactly as the console route bounds them, so one field cannot be\n // used to stuff an order document through a door the console keeps narrow.\n const carrier = String(body.carrier ?? '').slice(0, 40)\n const trackingNumber = String(body.trackingNumber ?? '').slice(0, 60)\n // Partial shipments (AGL-3611): `lineItems: [{ lineItemId, quantity }]`.\n // Absent keeps the old meaning, everything still to ship.\n let lineItems: Array<{ lineItemId: number; quantity: number }> | undefined\n if (body.lineItems !== undefined) {\n if (status !== 'fulfilled' || !Array.isArray(body.lineItems) || body.lineItems.length === 0) {\n return ApiErrors.badRequest({\n message: 'Order failed validation',\n code: 'validation_failed',\n fields: {\n lineItems:\n status !== 'fulfilled'\n ? 'Only a `fulfilled` update names line items'\n : 'Must be a non-empty array of { lineItemId, quantity }',\n },\n headers: ctx.headers,\n })\n }\n lineItems = (body.lineItems as unknown[]).slice(0, 500).map((entry) => ({\n lineItemId: Number((entry as Record<string, unknown>)?.lineItemId),\n quantity: Number((entry as Record<string, unknown>)?.quantity),\n }))\n }\n const idempotencyKey = request.headers.get('idempotency-key') ?? undefined\n\n // Gate 4: this plugin, on for this site.\n const hostSnap = await hostRef(ctx, hostId).get()\n if (!isHostPluginEnabled(ctx.org, hostSnap.data(), BUNDLE_ID)) {\n return ApiErrors.notFound({\n message: 'No such site',\n headers: ctx.headers,\n })\n }\n\n const outcome = await recordOrderShipment({\n hostId,\n orderId,\n to: status as OrderFulfilmentTarget,\n carrier,\n trackingNumber,\n ...(lineItems ? { lineItems } : {}),\n ...(typeof body.trackingUrl === 'string' ? { trackingUrl: body.trackingUrl } : {}),\n ...(body.notify === false ? { notify: false } : {}),\n ...(idempotencyKey ? { idempotencyKey } : {}),\n })\n if (outcome.outcome === 'invalid_lines') {\n return outcome.problem.problem === 'over_fulfilled'\n ? ApiErrors.conflict({\n message: outcome.message,\n code: 'over_fulfilled',\n headers: ctx.headers,\n })\n : ApiErrors.badRequest({\n message: 'Order failed validation',\n code: 'validation_failed',\n fields: { lineItems: outcome.message },\n headers: ctx.headers,\n })\n }\n if (outcome.outcome === 'no_such_order') {\n return ApiErrors.notFound({\n message: 'No such order',\n headers: ctx.headers,\n })\n }\n if (outcome.outcome === 'blocked') {\n // A NEW 409 code (`order_transition`), because an integrator has to be\n // able to tell \"the order moved on without me\" apart from every other\n // conflict this API can raise — it is the one a fulfilment poller will\n // actually hit, and the one it must not retry forever.\n return ApiErrors.conflict({\n message: `Orders in \"${outcome.from}\" cannot be marked ${status}`,\n code: 'order_transition',\n headers: ctx.headers,\n })\n }\n // The order object, on both `recorded` and `already` — a retry lands the\n // same state and reads the same 200. Re-read after the write so the body\n // shows the shipment that was just recorded rather than the one before it.\n return apiJson(orderView(await hostRef(ctx, hostId).collection('orders').doc(orderId).get()), {\n headers: ctx.headers,\n })\n}\n\n/**\n * Price and stock live on VARIANTS, never on the product — a product-level\n * `inventory` exists only as a denormalized sum the console rewrites on every\n * decrement, and a product-level `priceUsd` is the legacy single-variant\n * shape. Publishing either as the product's price would be wrong the moment a\n * product has two variants, so the variant array is the contract and the\n * product carries only the roll-up, clearly named.\n *\n * `inventory: null` on a variant means UNTRACKED and `0` means SOLD OUT.\n * Collapsing them (the `?? 0` an integrator writes on the first day) turns\n * every untracked product into an out-of-stock one, so the distinction is\n * carried through verbatim rather than defaulted.\n */\nfunction variantView(variant: Record<string, unknown>) {\n const inventory = variant.inventory\n return {\n id: variant.id ?? null,\n sku: variant.sku ?? null,\n barcode: variant.barcode ?? null,\n options: variant.options ?? {},\n priceUsd: typeof variant.priceUsd === 'number' ? variant.priceUsd : null,\n compareAtPriceUsd:\n typeof variant.compareAtPriceUsd === 'number'\n ? variant.compareAtPriceUsd\n : null,\n weightGrams:\n typeof variant.weightGrams === 'number' ? variant.weightGrams : null,\n inventory: typeof inventory === 'number' ? inventory : null,\n inventoryTracked: typeof inventory === 'number',\n }\n}\n\nfunction productView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = doc.data() ?? {}\n const variants = Array.isArray(data.variants)\n ? (data.variants as Array<Record<string, unknown>>)\n : []\n const tracked = variants.filter((v) => typeof v.inventory === 'number')\n return {\n id: doc.id,\n object: 'product',\n name: data.name ?? null,\n slug: data.slug ?? null,\n description: data.description ?? null,\n type: data.type ?? null,\n status: data.status ?? null,\n tags: data.tags ?? [],\n categoryIds: data.categoryIds ?? [],\n mediaUrls: data.mediaUrls ?? [],\n options: data.options ?? [],\n variants: variants.map(variantView),\n // The sum across TRACKED variants only, and `null` when none of them is\n // tracked — so an untracked catalogue reads as \"we don't count this\"\n // rather than as a store with nothing left to sell.\n inventory: tracked.length\n ? tracked.reduce((sum, v) => sum + Number(v.inventory ?? 0), 0)\n : null,\n subscription: serialize(data.subscription) ?? null,\n created: data.createdAtMs ? new Date(Number(data.createdAtMs)).toISOString() : null,\n updated: data.updatedAtMs ? new Date(Number(data.updatedAtMs)).toISOString() : null,\n }\n}\n\nexport async function handleProducts(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const [, hostId, , productId] = segments\n const denied = requireScope(ctx, 'products:read')\n if (denied) return denied\n const unentitled = requireCommerce(ctx)\n if (unentitled) return unentitled\n if (request.method !== 'GET') {\n return ApiErrors.methodNotAllowed({ headers: ctx.headers })\n }\n const collection = hostRef(ctx, hostId).collection('products')\n\n if (productId) {\n const snap = await collection.doc(productId).get()\n // A soft-deleted product is gone as far as a customer is concerned. The\n // console filters `deletedAt` client-side; the API must not hand back a\n // product the merchant deleted just because the document survives.\n if (!snap.exists || snap.get('deletedAt')) {\n return ApiErrors.notFound({ message: 'No such product', headers: ctx.headers })\n }\n return apiJson(productView(snap), { headers: ctx.headers })\n }\n\n let query: FirebaseFirestore.Query = collection\n const status = url.searchParams.get('status')\n if (status) query = query.where('status', '==', status)\n const { docs, nextCursor } = await paginate(query, url)\n const data = docs.filter((doc) => !doc.get('deletedAt')).map(productView)\n return listResponse(data, nextCursor, ctx.headers)\n}\n\n"],"names":["checkEntitlement","isHostPluginEnabled","ApiErrors","apiJson","listResponse","paginate","readJsonBody","requireScope","serialize","BUNDLE_ID","recordOrderShipment","orderViewFromData","requireCommerce","ctx","org","planRequired","message","code","headers","hostRef","hostId","firestore","collection","doc","orderView","id","data","handleOrders","request","segments","url","orderId","method","deniedWrite","unentitledWrite","updateOrder","denied","unentitled","methodNotAllowed","Allow","snap","get","exists","notFound","query","status","searchParams","where","channel","docs","nextCursor","map","filter","order","ORDER_WRITE_TARGETS","ORDER_WRITABLE_KEYS","ORDER_WRITE_REFUSED","cancelled","refunded","body","unknown","Object","keys","key","includes","length","badRequest","fields","fromEntries","String","refusal","join","carrier","slice","trackingNumber","lineItems","undefined","Array","isArray","entry","lineItemId","Number","quantity","idempotencyKey","hostSnap","outcome","to","trackingUrl","notify","problem","conflict","from","variantView","variant","inventory","sku","barcode","options","priceUsd","compareAtPriceUsd","weightGrams","inventoryTracked","productView","variants","tracked","v","object","name","slug","description","type","tags","categoryIds","mediaUrls","reduce","sum","subscription","created","createdAtMs","Date","toISOString","updated","updatedAtMs","handleProducts","productId"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/commerce/src/lib/server/api-v1/orders-and-products.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 { checkEntitlement, isHostPluginEnabled } from '@aglyn/aglyn/server'\nimport {\n ApiErrors,\n apiJson,\n listResponse,\n} from '@aglyn/tenant-data-admin'\nimport {\n type ApiV1Context,\n paginate,\n readJsonBody,\n requireScope,\n serialize,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport { BUNDLE_ID } from '../../constants/bundle-common'\nimport { type OrderFulfilmentTarget, recordOrderShipment } from '../fulfill-order'\nimport { orderViewFromData } from './order-view'\nimport { isUnpaidQuote } from '../../model/commerce-quotes'\n\n/**\n * A site's orders and products on the customer REST API,\n * `/v1/sites/{siteId}/orders/…` and `/v1/sites/{siteId}/products/…`\n * (AGL-1928, AGL-2461, AGL-3080). Registered from this plugin's console\n * server declarations; the console's `/v1` router owns the pipeline in\n * front — the key, the plan's API access, the request quota, the rate limit —\n * and refuses a site the key's organization does not own before it hands a\n * request here.\n */\n\n/**\n * Commerce resources need the `commerce` entitlement as well as the scope\n * (AGL-1928). `apiAccess` alone is the wrong gate here: it says the org may\n * call the API at all, not that it may still use the store. AGL-1873 closed\n * exactly this class on the write side — two money doors that asked the\n * plugin switch (`org.enabledPlugins`) instead of the plan, so a lapsed org\n * kept selling — and the same reasoning applies to a read. `enabledPlugins`\n * is the customer's own on/off switch and survives a downgrade; the plan does\n * not, and the published rule is that paid features stop at the door when the\n * plan no longer includes them.\n *\n * Answered as `plan_required` rather than `not_found`, deliberately. Hiding a\n * store that plainly exists behind a 404 sends an integrator hunting a wrong\n * site id; the honest answer names the plan.\n */\nfunction requireCommerce(ctx: ApiV1Context): Response | null {\n return checkEntitlement(ctx.org, 'commerce')\n ? null\n : ApiErrors.planRequired({\n message: 'Commerce is not included in this organization’s plan',\n code: 'commerce',\n headers: ctx.headers,\n })\n}\n\nfunction hostRef(ctx: ApiV1Context, hostId: string) {\n return ctx.firestore.collection('hosts').doc(hostId)\n}\n\n/**\n * Orders carry a legacy Commerce Starter shape (AGL-90) alongside the modern\n * one: `amountCents`/`feeCents` at the top level rather than a `totals` map.\n * The console lifts them through `CommerceModel.liftLegacyOrder` and reads\n * `lifted.totals?.totalCents ?? order.amountCents`. The API cannot publish two\n * shapes for one object, so the legacy fields are folded into `totals` here\n * and a client only ever sees the modern one. `channel` defaults to `online`\n * for the same reason the console's list does — an absent channel is a\n * pre-channel order, not an unknown one.\n */\nfunction orderView(doc: FirebaseFirestore.DocumentSnapshot) {\n return orderViewFromData(doc.id, doc.data() ?? {})\n}\n\nexport async function handleOrders(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const [, hostId, , orderId] = segments\n // `orders:write` is checked BEFORE `orders:read`, matching `handleSites`\n // and `handleScopedMedia` (AGL-900): a fulfilment key that only records\n // shipments must not be told it lacks a read scope it was never meant to\n // hold.\n if (request.method === 'PATCH' && orderId) {\n const deniedWrite = requireScope(ctx, 'orders:write')\n if (deniedWrite) return deniedWrite\n const unentitledWrite = requireCommerce(ctx)\n if (unentitledWrite) return unentitledWrite\n return updateOrder(request, ctx, hostId, orderId)\n }\n const denied = requireScope(ctx, 'orders:read')\n if (denied) return denied\n const unentitled = requireCommerce(ctx)\n if (unentitled) return unentitled\n if (request.method !== 'GET') {\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: orderId ? 'GET, PATCH' : 'GET' },\n })\n }\n const collection = hostRef(ctx, hostId).collection('orders')\n\n if (orderId) {\n const snap = await collection.doc(orderId).get()\n // An unpaid quote is not an order yet (AGL-4093).\n if (!snap.exists || isUnpaidQuote(snap.data())) {\n return ApiErrors.notFound({ message: 'No such order', headers: ctx.headers })\n }\n return apiJson(orderView(snap), { headers: ctx.headers })\n }\n\n let query: FirebaseFirestore.Query = collection\n const status = url.searchParams.get('status')\n if (status) query = query.where('status', '==', status)\n const channel = url.searchParams.get('channel')\n // `online` is the DEFAULT, not a stored value on older orders, so filtering\n // for it in Firestore would silently drop every pre-channel order. Those are\n // exactly the oldest orders an accounting backfill is reaching for, so this\n // one value is filtered after the read instead. The page can therefore come\n // back shorter than `limit` while `has_more` is still true — which the\n // published pagination contract already tells clients to expect (check\n // `has_more`, never a page's length).\n if (channel && channel !== 'online') query = query.where('channel', '==', channel)\n const { docs, nextCursor } = await paginate(query, url)\n // Unpaid quotes share the collection and are not orders yet (AGL-4093);\n // left out after the read, like `online` above, so a page can be short.\n const data = docs\n .filter((doc) => !isUnpaidQuote(doc.data()))\n .map(orderView)\n .filter((order) => (channel === 'online' ? order.channel === 'online' : true))\n return listResponse(data, nextCursor, ctx.headers)\n}\n\n/**\n * Statuses this endpoint will move an order TO, and the two it names as\n * refused. Kept as data so the 400 can list them and the docs can be checked\n * against the same source the handler branches on.\n */\nconst ORDER_WRITE_TARGETS: OrderFulfilmentTarget[] = ['fulfilled', 'delivered']\n\n/** The keys a PATCH may carry (AGL-3611 added the last three). */\nconst ORDER_WRITABLE_KEYS = ['status', 'carrier', 'trackingNumber', 'trackingUrl', 'lineItems', 'notify']\n\n/**\n * Transitions that exist in the commerce model and are DELIBERATELY not\n * reachable here — refused by name with a 400 that says why, never silently\n * ignored. `cancelled` releases held stock under its own transaction and\n * `refunded` moves money under another; admitting either here would hand a\n * caller a door around exactly the specifics those two routes exist to\n * enforce, and an API key is the credential least able to answer the\n * questions they ask.\n */\nconst ORDER_WRITE_REFUSED: Record<string, string> = {\n cancelled:\n 'Canceling an order releases held stock, so it is not part of this endpoint. Cancel it in the console.',\n refunded:\n 'Refunding an order moves money, so it is not part of this endpoint. Refund it in the console.',\n}\n\n/**\n * `PATCH /v1/sites/{siteId}/orders/{orderId}` — record a shipment (AGL-2461).\n *\n * ## The write is the console's, shared\n *\n * The transition rule, the transaction that re-asks it under the write, the\n * fulfillment append and the timeline entry are `recordOrderShipment`'s,\n * the one implementation the console's `commerce/fulfill-order` route calls\n * too. A second copy of `ORDER_TRANSITIONS` here would be the bug rather\n * than the fix: two tables drift, and drift here means the API writing an\n * order status the console forbids — `paid → delivered` skipping\n * fulfilment, or a write onto a `refunded` order. That is the class\n * AGL-1818/AGL-1819 exist to close, and it is money-adjacent.\n *\n * ## What THIS function is responsible for: all of the authorization\n *\n * `recordOrderShipment` is pre-authorized by contract — it takes `hostId`\n * on trust — so every gate is here, and all four are load-bearing:\n *\n * 1. `orders:write` on the key (checked by the caller, above).\n * 2. The `commerce` PLAN entitlement (checked by the caller, above) — the\n * plan, never the plugin switch, which is the AGL-1873 distinction.\n * 3. **Org owns the site** — the console's `handleSites` refuses an unowned\n * `hostId` with a 404 before it hands any site resource to its plugin,\n * which is what keeps this from being a cross-tenant write primitive\n * addressable by anyone who can guess a host id. Deliberately NOT\n * re-checked here: a second copy would be a gate no test can redden\n * (removing either one alone leaves the other answering), and an unproven\n * guard on a cross-tenant write is worse than the one guard a test\n * actually holds. `api-v1-order-fulfilment.spec.ts`'s \"CANNOT move\n * another org's order\" case is that test, and it fails when\n * `handleSites`' `orgOwnsHost` is removed.\n * 4. **This plugin is switched on for this site.** The resource is\n * registered for every organization the console serves, so its being here\n * says nothing about one org's configuration; without this an org that\n * switched commerce off for a site would still accept writes into it,\n * which is the per-site enablement rule (AGL-1014) the plugin API\n * dispatcher applies to every other commerce door.\n *\n * ## No `Idempotency-Key`\n *\n * None is needed and none is accepted: the write returns without writing when\n * the order is already in the target status, so a retry lands the same state\n * AND returns the same `200` with the same order body — the contract\n * `updateRecord`, `updateContact` and `updateFormSubmission` are held to.\n */\nasync function updateOrder(\n request: Request,\n ctx: ApiV1Context,\n hostId: string,\n orderId: string,\n): Promise<Response> {\n const body = await readJsonBody(request)\n const unknown = Object.keys(body).filter((key) => !ORDER_WRITABLE_KEYS.includes(key))\n if (unknown.length > 0) {\n // Named, not dropped — the `updateFormSubmission` / `updateContact` rule.\n // A silently ignored `trackingUrl` here reads as \"we recorded your\n // shipment as you described it\" when half of it went nowhere, and the\n // caller is a warehouse system that will never look again.\n return ApiErrors.badRequest({\n message:\n 'Only `status`, `carrier`, `trackingNumber`, `trackingUrl`, `lineItems` and `notify` can be set on an order',\n code: 'validation_failed',\n fields: Object.fromEntries(\n unknown.map((key) => [key, 'Not writable on an order']),\n ),\n headers: ctx.headers,\n })\n }\n\n const status = String(body.status ?? '')\n const refusal = ORDER_WRITE_REFUSED[status]\n if (refusal) {\n return ApiErrors.badRequest({\n message: refusal,\n code: 'validation_failed',\n fields: { status: refusal },\n headers: ctx.headers,\n })\n }\n if (!(ORDER_WRITE_TARGETS as string[]).includes(status)) {\n return ApiErrors.badRequest({\n message: 'Order failed validation',\n code: 'validation_failed',\n fields: {\n status: `Must be one of: ${ORDER_WRITE_TARGETS.join(', ')}`,\n },\n headers: ctx.headers,\n })\n }\n // Bounded exactly as the console route bounds them, so one field cannot be\n // used to stuff an order document through a door the console keeps narrow.\n const carrier = String(body.carrier ?? '').slice(0, 40)\n const trackingNumber = String(body.trackingNumber ?? '').slice(0, 60)\n // Partial shipments (AGL-3611): `lineItems: [{ lineItemId, quantity }]`.\n // Absent keeps the old meaning, everything still to ship.\n let lineItems: Array<{ lineItemId: number; quantity: number }> | undefined\n if (body.lineItems !== undefined) {\n if (status !== 'fulfilled' || !Array.isArray(body.lineItems) || body.lineItems.length === 0) {\n return ApiErrors.badRequest({\n message: 'Order failed validation',\n code: 'validation_failed',\n fields: {\n lineItems:\n status !== 'fulfilled'\n ? 'Only a `fulfilled` update names line items'\n : 'Must be a non-empty array of { lineItemId, quantity }',\n },\n headers: ctx.headers,\n })\n }\n lineItems = (body.lineItems as unknown[]).slice(0, 500).map((entry) => ({\n lineItemId: Number((entry as Record<string, unknown>)?.lineItemId),\n quantity: Number((entry as Record<string, unknown>)?.quantity),\n }))\n }\n const idempotencyKey = request.headers.get('idempotency-key') ?? undefined\n\n // Gate 4: this plugin, on for this site.\n const hostSnap = await hostRef(ctx, hostId).get()\n if (!isHostPluginEnabled(ctx.org, hostSnap.data(), BUNDLE_ID)) {\n return ApiErrors.notFound({\n message: 'No such site',\n headers: ctx.headers,\n })\n }\n\n const outcome = await recordOrderShipment({\n hostId,\n orderId,\n to: status as OrderFulfilmentTarget,\n carrier,\n trackingNumber,\n ...(lineItems ? { lineItems } : {}),\n ...(typeof body.trackingUrl === 'string' ? { trackingUrl: body.trackingUrl } : {}),\n ...(body.notify === false ? { notify: false } : {}),\n ...(idempotencyKey ? { idempotencyKey } : {}),\n })\n if (outcome.outcome === 'invalid_lines') {\n return outcome.problem.problem === 'over_fulfilled'\n ? ApiErrors.conflict({\n message: outcome.message,\n code: 'over_fulfilled',\n headers: ctx.headers,\n })\n : ApiErrors.badRequest({\n message: 'Order failed validation',\n code: 'validation_failed',\n fields: { lineItems: outcome.message },\n headers: ctx.headers,\n })\n }\n if (outcome.outcome === 'no_such_order') {\n return ApiErrors.notFound({\n message: 'No such order',\n headers: ctx.headers,\n })\n }\n if (outcome.outcome === 'blocked') {\n // A NEW 409 code (`order_transition`), because an integrator has to be\n // able to tell \"the order moved on without me\" apart from every other\n // conflict this API can raise — it is the one a fulfilment poller will\n // actually hit, and the one it must not retry forever.\n return ApiErrors.conflict({\n message: `Orders in \"${outcome.from}\" cannot be marked ${status}`,\n code: 'order_transition',\n headers: ctx.headers,\n })\n }\n // The order object, on both `recorded` and `already` — a retry lands the\n // same state and reads the same 200. Re-read after the write so the body\n // shows the shipment that was just recorded rather than the one before it.\n return apiJson(orderView(await hostRef(ctx, hostId).collection('orders').doc(orderId).get()), {\n headers: ctx.headers,\n })\n}\n\n/**\n * Price and stock live on VARIANTS, never on the product — a product-level\n * `inventory` exists only as a denormalized sum the console rewrites on every\n * decrement, and a product-level `priceUsd` is the legacy single-variant\n * shape. Publishing either as the product's price would be wrong the moment a\n * product has two variants, so the variant array is the contract and the\n * product carries only the roll-up, clearly named.\n *\n * `inventory: null` on a variant means UNTRACKED and `0` means SOLD OUT.\n * Collapsing them (the `?? 0` an integrator writes on the first day) turns\n * every untracked product into an out-of-stock one, so the distinction is\n * carried through verbatim rather than defaulted.\n */\nfunction variantView(variant: Record<string, unknown>) {\n const inventory = variant.inventory\n return {\n id: variant.id ?? null,\n sku: variant.sku ?? null,\n barcode: variant.barcode ?? null,\n options: variant.options ?? {},\n priceUsd: typeof variant.priceUsd === 'number' ? variant.priceUsd : null,\n compareAtPriceUsd:\n typeof variant.compareAtPriceUsd === 'number'\n ? variant.compareAtPriceUsd\n : null,\n weightGrams:\n typeof variant.weightGrams === 'number' ? variant.weightGrams : null,\n inventory: typeof inventory === 'number' ? inventory : null,\n inventoryTracked: typeof inventory === 'number',\n }\n}\n\nfunction productView(doc: FirebaseFirestore.DocumentSnapshot) {\n const data = doc.data() ?? {}\n const variants = Array.isArray(data.variants)\n ? (data.variants as Array<Record<string, unknown>>)\n : []\n const tracked = variants.filter((v) => typeof v.inventory === 'number')\n return {\n id: doc.id,\n object: 'product',\n name: data.name ?? null,\n slug: data.slug ?? null,\n description: data.description ?? null,\n type: data.type ?? null,\n status: data.status ?? null,\n tags: data.tags ?? [],\n categoryIds: data.categoryIds ?? [],\n mediaUrls: data.mediaUrls ?? [],\n options: data.options ?? [],\n variants: variants.map(variantView),\n // The sum across TRACKED variants only, and `null` when none of them is\n // tracked — so an untracked catalogue reads as \"we don't count this\"\n // rather than as a store with nothing left to sell.\n inventory: tracked.length\n ? tracked.reduce((sum, v) => sum + Number(v.inventory ?? 0), 0)\n : null,\n subscription: serialize(data.subscription) ?? null,\n created: data.createdAtMs ? new Date(Number(data.createdAtMs)).toISOString() : null,\n updated: data.updatedAtMs ? new Date(Number(data.updatedAtMs)).toISOString() : null,\n }\n}\n\nexport async function handleProducts(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const [, hostId, , productId] = segments\n const denied = requireScope(ctx, 'products:read')\n if (denied) return denied\n const unentitled = requireCommerce(ctx)\n if (unentitled) return unentitled\n if (request.method !== 'GET') {\n return ApiErrors.methodNotAllowed({ headers: ctx.headers })\n }\n const collection = hostRef(ctx, hostId).collection('products')\n\n if (productId) {\n const snap = await collection.doc(productId).get()\n // A soft-deleted product is gone as far as a customer is concerned. The\n // console filters `deletedAt` client-side; the API must not hand back a\n // product the merchant deleted just because the document survives.\n if (!snap.exists || snap.get('deletedAt')) {\n return ApiErrors.notFound({ message: 'No such product', headers: ctx.headers })\n }\n return apiJson(productView(snap), { headers: ctx.headers })\n }\n\n let query: FirebaseFirestore.Query = collection\n const status = url.searchParams.get('status')\n if (status) query = query.where('status', '==', status)\n const { docs, nextCursor } = await paginate(query, url)\n const data = docs.filter((doc) => !doc.get('deletedAt')).map(productView)\n return listResponse(data, nextCursor, ctx.headers)\n}\n\n"],"names":["checkEntitlement","isHostPluginEnabled","ApiErrors","apiJson","listResponse","paginate","readJsonBody","requireScope","serialize","BUNDLE_ID","recordOrderShipment","orderViewFromData","isUnpaidQuote","requireCommerce","ctx","org","planRequired","message","code","headers","hostRef","hostId","firestore","collection","doc","orderView","id","data","handleOrders","request","segments","url","orderId","method","deniedWrite","unentitledWrite","updateOrder","denied","unentitled","methodNotAllowed","Allow","snap","get","exists","notFound","query","status","searchParams","where","channel","docs","nextCursor","filter","map","order","ORDER_WRITE_TARGETS","ORDER_WRITABLE_KEYS","ORDER_WRITE_REFUSED","cancelled","refunded","body","unknown","Object","keys","key","includes","length","badRequest","fields","fromEntries","String","refusal","join","carrier","slice","trackingNumber","lineItems","undefined","Array","isArray","entry","lineItemId","Number","quantity","idempotencyKey","hostSnap","outcome","to","trackingUrl","notify","problem","conflict","from","variantView","variant","inventory","sku","barcode","options","priceUsd","compareAtPriceUsd","weightGrams","inventoryTracked","productView","variants","tracked","v","object","name","slug","description","type","tags","categoryIds","mediaUrls","reduce","sum","subscription","created","createdAtMs","Date","toISOString","updated","updatedAtMs","handleProducts","productId"],"mappings":"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"}