@aglyn/plugins-commerce 1.0.0-beta.290 → 1.0.0-beta.292

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 (183) hide show
  1. package/README.md +4 -2
  2. package/package.json +13 -13
  3. package/src/lib/components/cart-fulfillment.d.ts +3 -0
  4. package/src/lib/components/cart-fulfillment.js.map +1 -1
  5. package/src/lib/components/cart.js +14 -4
  6. package/src/lib/components/cart.js.map +1 -1
  7. package/src/lib/components/commerce-console-page.js +2 -1
  8. package/src/lib/components/commerce-console-page.js.map +1 -1
  9. package/src/lib/components/console/host-orders-card.component.d.ts +6 -0
  10. package/src/lib/components/console/host-orders-card.component.js +3 -2
  11. package/src/lib/components/console/host-orders-card.component.js.map +1 -1
  12. package/src/lib/components/console/invoice-send.component.d.ts +44 -0
  13. package/src/lib/components/console/invoice-send.component.js +237 -0
  14. package/src/lib/components/console/invoice-send.component.js.map +1 -0
  15. package/src/lib/components/console/order-detail-dialog.component.d.ts +5 -0
  16. package/src/lib/components/console/order-detail-dialog.component.js +9 -2
  17. package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
  18. package/src/lib/components/console/order-invoice.component.d.ts +9 -0
  19. package/src/lib/components/console/order-invoice.component.js +27 -2
  20. package/src/lib/components/console/order-invoice.component.js.map +1 -1
  21. package/src/lib/components/console/pos/pos-item-dialog.component.js +3 -2
  22. package/src/lib/components/console/pos/pos-item-dialog.component.js.map +1 -1
  23. package/src/lib/components/console/pos-kiosk-page.component.js +2 -1
  24. package/src/lib/components/console/pos-kiosk-page.component.js.map +1 -1
  25. package/src/lib/components/console/pos-page.component.js +1 -1
  26. package/src/lib/components/console/pos-page.component.js.map +1 -1
  27. package/src/lib/components/console/product-add-ons-fields.component.d.ts +41 -0
  28. package/src/lib/components/console/product-add-ons-fields.component.js +356 -0
  29. package/src/lib/components/console/product-add-ons-fields.component.js.map +1 -0
  30. package/src/lib/components/console/product-editor-dialog.component.js +31 -5
  31. package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
  32. package/src/lib/components/console/product-register-fields.component.d.ts +4 -5
  33. package/src/lib/components/console/product-register-fields.component.js +8 -197
  34. package/src/lib/components/console/product-register-fields.component.js.map +1 -1
  35. package/src/lib/components/console/products-hub-zone.component.js +6 -0
  36. package/src/lib/components/console/products-hub-zone.component.js.map +1 -1
  37. package/src/lib/components/console/quote-editor-dialog.component.d.ts +29 -0
  38. package/src/lib/components/console/quote-editor-dialog.component.js +102 -4
  39. package/src/lib/components/console/quote-editor-dialog.component.js.map +1 -1
  40. package/src/lib/components/console/quote-pay-link.component.d.ts +8 -1
  41. package/src/lib/components/console/quote-pay-link.component.js +29 -3
  42. package/src/lib/components/console/quote-pay-link.component.js.map +1 -1
  43. package/src/lib/components/console/quote-proofs.component.d.ts +11 -0
  44. package/src/lib/components/console/quote-proofs.component.js +68 -0
  45. package/src/lib/components/console/quote-proofs.component.js.map +1 -1
  46. package/src/lib/components/console/quote-send.component.d.ts +42 -0
  47. package/src/lib/components/console/quote-send.component.js +304 -0
  48. package/src/lib/components/console/quote-send.component.js.map +1 -0
  49. package/src/lib/components/console/quote-start.component.d.ts +45 -0
  50. package/src/lib/components/console/quote-start.component.js +182 -0
  51. package/src/lib/components/console/quote-start.component.js.map +1 -0
  52. package/src/lib/components/product-add-ons.d.ts +28 -0
  53. package/src/lib/components/product-add-ons.js +136 -0
  54. package/src/lib/components/product-add-ons.js.map +1 -0
  55. package/src/lib/components/product-detail.d.ts +16 -0
  56. package/src/lib/components/product-detail.js +112 -19
  57. package/src/lib/components/product-detail.js.map +1 -1
  58. package/src/lib/components/product-grid.d.ts +8 -2
  59. package/src/lib/components/product-grid.js +6 -1
  60. package/src/lib/components/product-grid.js.map +1 -1
  61. package/src/lib/components/product-options-field-body.d.ts +44 -0
  62. package/src/lib/components/product-options-field-body.js +260 -0
  63. package/src/lib/components/product-options-field-body.js.map +1 -0
  64. package/src/lib/components/product-options-field.d.ts +32 -0
  65. package/src/lib/components/product-options-field.js +128 -0
  66. package/src/lib/components/product-options-field.js.map +1 -0
  67. package/src/lib/components/quote-page-body.js +39 -20
  68. package/src/lib/components/quote-page-body.js.map +1 -1
  69. package/src/lib/model/commerce-cart.d.ts +15 -3
  70. package/src/lib/model/commerce-cart.js +10 -3
  71. package/src/lib/model/commerce-cart.js.map +1 -1
  72. package/src/lib/model/commerce-invoices.d.ts +139 -0
  73. package/src/lib/model/commerce-invoices.js +343 -0
  74. package/src/lib/model/commerce-invoices.js.map +1 -0
  75. package/src/lib/model/commerce-orders.d.ts +35 -5
  76. package/src/lib/model/commerce-orders.js +3 -0
  77. package/src/lib/model/commerce-orders.js.map +1 -1
  78. package/src/lib/model/commerce-pos-kiosk.d.ts +1 -1
  79. package/src/lib/model/commerce-pos-kiosk.js +3 -2
  80. package/src/lib/model/commerce-pos-kiosk.js.map +1 -1
  81. package/src/lib/model/commerce-product-field.d.ts +23 -0
  82. package/src/lib/model/commerce-product-field.js +26 -0
  83. package/src/lib/model/commerce-product-field.js.map +1 -0
  84. package/src/lib/model/commerce-quote-email.d.ts +81 -0
  85. package/src/lib/model/commerce-quote-email.js +170 -0
  86. package/src/lib/model/commerce-quote-email.js.map +1 -0
  87. package/src/lib/model/commerce-quote-pay.d.ts +49 -6
  88. package/src/lib/model/commerce-quote-pay.js +69 -36
  89. package/src/lib/model/commerce-quote-pay.js.map +1 -1
  90. package/src/lib/model/commerce-quote-request.d.ts +93 -0
  91. package/src/lib/model/commerce-quote-request.js +207 -0
  92. package/src/lib/model/commerce-quote-request.js.map +1 -0
  93. package/src/lib/model/commerce-quotes.d.ts +81 -0
  94. package/src/lib/model/commerce-quotes.js +1 -1
  95. package/src/lib/model/commerce-quotes.js.map +1 -1
  96. package/src/lib/model/commerce.d.ts +18 -0
  97. package/src/lib/model/commerce.js +26 -0
  98. package/src/lib/model/commerce.js.map +1 -1
  99. package/src/lib/model/index.d.ts +3 -0
  100. package/src/lib/model/index.js +3 -0
  101. package/src/lib/model/index.js.map +1 -1
  102. package/src/lib/model/product-modifiers.d.ts +74 -4
  103. package/src/lib/model/product-modifiers.js +131 -10
  104. package/src/lib/model/product-modifiers.js.map +1 -1
  105. package/src/lib/plugin.js +19 -0
  106. package/src/lib/plugin.js.map +1 -1
  107. package/src/lib/server/billing-webhook.js +164 -98
  108. package/src/lib/server/billing-webhook.js.map +1 -1
  109. package/src/lib/server/cart-checkout.js +14 -2
  110. package/src/lib/server/cart-checkout.js.map +1 -1
  111. package/src/lib/server/cart.d.ts +5 -0
  112. package/src/lib/server/cart.js +41 -4
  113. package/src/lib/server/cart.js.map +1 -1
  114. package/src/lib/server/catalog.d.ts +6 -0
  115. package/src/lib/server/catalog.js +2 -0
  116. package/src/lib/server/catalog.js.map +1 -1
  117. package/src/lib/server/checkout-extras.js +3 -1
  118. package/src/lib/server/checkout-extras.js.map +1 -1
  119. package/src/lib/server/checkout.js +28 -3
  120. package/src/lib/server/checkout.js.map +1 -1
  121. package/src/lib/server/commerce-pdf.d.ts +5 -1
  122. package/src/lib/server/commerce-pdf.js +37 -10
  123. package/src/lib/server/commerce-pdf.js.map +1 -1
  124. package/src/lib/server/draft-order.js +23 -3
  125. package/src/lib/server/draft-order.js.map +1 -1
  126. package/src/lib/server/invoice-email.d.ts +76 -0
  127. package/src/lib/server/invoice-email.js +183 -0
  128. package/src/lib/server/invoice-email.js.map +1 -0
  129. package/src/lib/server/local-fulfillment.js +5 -1
  130. package/src/lib/server/local-fulfillment.js.map +1 -1
  131. package/src/lib/server/order-invoice-send.d.ts +43 -0
  132. package/src/lib/server/order-invoice-send.js +218 -0
  133. package/src/lib/server/order-invoice-send.js.map +1 -0
  134. package/src/lib/server/order-status.d.ts +11 -0
  135. package/src/lib/server/order-status.js +44 -8
  136. package/src/lib/server/order-status.js.map +1 -1
  137. package/src/lib/server/person-eraser.d.ts +3 -2
  138. package/src/lib/server/person-eraser.js +25 -6
  139. package/src/lib/server/person-eraser.js.map +1 -1
  140. package/src/lib/server/pos-offline-sync.js +1 -1
  141. package/src/lib/server/pos-offline-sync.js.map +1 -1
  142. package/src/lib/server/pos-order.js +1 -1
  143. package/src/lib/server/pos-order.js.map +1 -1
  144. package/src/lib/server/product.d.ts +26 -0
  145. package/src/lib/server/product.js +18 -0
  146. package/src/lib/server/product.js.map +1 -1
  147. package/src/lib/server/products-write.js +5 -2
  148. package/src/lib/server/products-write.js.map +1 -1
  149. package/src/lib/server/quote-email.d.ts +85 -0
  150. package/src/lib/server/quote-email.js +211 -0
  151. package/src/lib/server/quote-email.js.map +1 -0
  152. package/src/lib/server/quote-link-token.d.ts +11 -0
  153. package/src/lib/server/quote-link-token.js +21 -0
  154. package/src/lib/server/quote-link-token.js.map +1 -1
  155. package/src/lib/server/quote-pay.d.ts +8 -3
  156. package/src/lib/server/quote-pay.js +105 -43
  157. package/src/lib/server/quote-pay.js.map +1 -1
  158. package/src/lib/server/quote.d.ts +11 -0
  159. package/src/lib/server/quote.js +309 -12
  160. package/src/lib/server/quote.js.map +1 -1
  161. package/src/lib/server/site-page-resolver.js +2 -1
  162. package/src/lib/server/site-page-resolver.js.map +1 -1
  163. package/src/lib/server.js +5 -0
  164. package/src/lib/server.js.map +1 -1
  165. package/src/lib/site.js +8 -0
  166. package/src/lib/site.js.map +1 -1
  167. package/src/lib/tenant-emails.js +243 -0
  168. package/src/lib/tenant-emails.js.map +1 -1
  169. package/src/lib/utils/add-to-cart.d.ts +8 -0
  170. package/src/lib/utils/add-to-cart.js +10 -2
  171. package/src/lib/utils/add-to-cart.js.map +1 -1
  172. package/src/lib/utils/commerce-document-pdf.d.ts +5 -0
  173. package/src/lib/utils/commerce-document-pdf.js +6 -1
  174. package/src/lib/utils/commerce-document-pdf.js.map +1 -1
  175. package/src/lib/utils/invoice-email-content.d.ts +45 -0
  176. package/src/lib/utils/invoice-email-content.js +126 -0
  177. package/src/lib/utils/invoice-email-content.js.map +1 -0
  178. package/src/lib/utils/order-invoice.d.ts +1 -1
  179. package/src/lib/utils/order-invoice.js +14 -6
  180. package/src/lib/utils/order-invoice.js.map +1 -1
  181. package/src/lib/utils/quote-email-content.d.ts +63 -0
  182. package/src/lib/utils/quote-email-content.js +157 -0
  183. package/src/lib/utils/quote-email-content.js.map +1 -0
@@ -19,6 +19,12 @@ export interface ProductModifierOption {
19
19
  id: string;
20
20
  name: string;
21
21
  priceCents: number;
22
+ /**
23
+ * What the choice adds on one variant, keyed by variant id (AGL-4100):
24
+ * walls cost more on a larger canopy. A variant not listed pays
25
+ * `priceCents`. Read through {@link modifierOptionPriceCents}.
26
+ */
27
+ priceCentsByVariant?: Record<string, number>;
22
28
  }
23
29
  /**
24
30
  * A set of choices offered together ("Milk", "Add-ons"). `min` of 1 or more
@@ -30,6 +36,13 @@ export interface ProductModifierGroup {
30
36
  min: number;
31
37
  max: number;
32
38
  options: ProductModifierOption[];
39
+ /**
40
+ * Offered on the product's page on the website as well as at the register
41
+ * (AGL-4100), "Show on the website". Absent reads as register only, so a
42
+ * group made before the switch changes nothing on a live store; the
43
+ * console makes new groups with it on.
44
+ */
45
+ online?: boolean;
33
46
  }
34
47
  /** A modifier as the order line keeps it: names and price as sold. */
35
48
  export interface OrderLineModifier {
@@ -57,10 +70,23 @@ export declare function productModifierGroups(product: {
57
70
  /** Whether a group must be answered before the item can be added. */
58
71
  export declare function modifierGroupRequired(group: ProductModifierGroup): boolean;
59
72
  /**
60
- * Prices a line's chosen modifiers from the PRODUCT, in group order, and
61
- * checks every group's minimum and maximum. Unknown ids, a repeated choice
62
- * and a group answered past its maximum are refused with what to fix.
73
+ * The groups the product's page on the website offers (AGL-4100): those
74
+ * switched to "Show on the website". A register-only group is never sent to
75
+ * a shopper, never asked of one and never priced on an online order.
63
76
  */
77
+ export declare function websiteModifierGroups(product: {
78
+ modifierGroups?: unknown;
79
+ }): ProductModifierGroup[];
80
+ /** Whether the website must ask a shopper a choice before the item goes in a cart. */
81
+ export declare function websiteModifiersRequired(product: {
82
+ modifierGroups?: unknown;
83
+ }): boolean;
84
+ /**
85
+ * What one choice adds to one unit of the picked variant (AGL-4100): its
86
+ * price on that variant where it has one, else its own price.
87
+ */
88
+ export declare function modifierOptionPriceCents(option: Pick<ProductModifierOption, 'priceCents' | 'priceCentsByVariant'>, variantId?: string | null): number;
89
+ /** A line's chosen modifiers, priced. */
64
90
  export interface ResolvedLineModifiers {
65
91
  ok: boolean;
66
92
  /** The chosen modifiers, in group order; empty when not `ok`. */
@@ -70,10 +96,54 @@ export interface ResolvedLineModifiers {
70
96
  /** What to fix, when not `ok`. */
71
97
  error?: string;
72
98
  }
99
+ /** The picks a request names, as ids only, bounded; nothing for a value that is not a list. */
100
+ export declare function modifierSelectionFrom(raw: unknown): ModifierSelection[];
101
+ /**
102
+ * Prices a line's chosen modifiers from the PRODUCT, in group order, and
103
+ * checks every group's minimum and maximum. Unknown ids, a repeated choice
104
+ * and a group answered past its maximum are refused with what to fix.
105
+ *
106
+ * `variantId` is the variant the line sells (AGL-4100): a choice priced by
107
+ * variant adds its price on that one. Without it every choice adds its own
108
+ * `priceCents`, as it always did.
109
+ */
73
110
  export declare function resolveLineModifiers(product: {
74
111
  name?: string;
75
112
  modifierGroups?: unknown;
76
- }, raw: unknown): ResolvedLineModifiers;
113
+ }, raw: unknown, variantId?: string | null): ResolvedLineModifiers;
114
+ /**
115
+ * {@link resolveLineModifiers} for a sale made online (AGL-4100): only the
116
+ * groups shown on the website are offered, asked or priced. A register-only
117
+ * group, required or not, is not the shopper's to answer.
118
+ */
119
+ export declare function resolveWebsiteLineModifiers(product: {
120
+ name?: string;
121
+ modifierGroups?: unknown;
122
+ }, raw: unknown, variantId?: string | null): ResolvedLineModifiers;
123
+ /**
124
+ * The modifiers a PAID line names, for its order record (AGL-4100): every
125
+ * pick the product still offers, in group order, priced as it is now, with no
126
+ * minimum or maximum asked. The money was taken when the sale was priced;
127
+ * a choice the merchant removed since is left off the record, never a reason
128
+ * to lose the order.
129
+ */
130
+ export declare function soldLineModifiers(product: {
131
+ modifierGroups?: unknown;
132
+ }, raw: unknown, variantId?: string | null): OrderLineModifier[];
133
+ /**
134
+ * What a shopper is told about a cart line whose add-ons no longer answer
135
+ * the product (AGL-4100): the resolver's reason, and how to put it right.
136
+ */
137
+ export declare function cartLineModifierProblem(error: string | undefined): string;
138
+ /** The picks a priced line keeps: ids only, in the product's group order. */
139
+ export declare function modifierSelectionOf(modifiers: readonly Pick<OrderLineModifier, 'groupId' | 'optionId'>[]): ModifierSelection[];
140
+ /**
141
+ * The picks as one metadata value (AGL-4100), for a payment processor that
142
+ * holds strings only: `[["group","option"],…]`. Empty for no picks.
143
+ */
144
+ export declare function encodeModifierSelection(selection: readonly ModifierSelection[] | undefined): string;
145
+ /** {@link encodeModifierSelection} read back; nothing for a value it did not write. */
146
+ export declare function decodeModifierSelection(value: unknown): ModifierSelection[];
77
147
  /** The label a line prints: the variant, then each modifier (`Large / Oat milk, Extra shot`). */
78
148
  export declare function lineLabelWithModifiers(variantLabel: string | undefined, modifiers: readonly Pick<OrderLineModifier, 'name'>[]): string;
79
149
  /** Two lines merge only when they are the same item with the same choices. */
@@ -26,9 +26,19 @@
26
26
  * structured in `modifiers`: every receipt, email, order view and export
27
27
  * already prints the label, so the choice reaches all of them without each
28
28
  * learning a new field.
29
+ *
30
+ * ADD-ONS ON THE WEBSITE (AGL-4100). A group switched to "Show on the
31
+ * website" (`online`) is offered on the product's page too, and a cart line,
32
+ * Buy now and the order carry its picks the same way: ids from the shopper,
33
+ * prices from the product. A choice may cost more on one variant than
34
+ * another (`priceCentsByVariant`), which is how one page sells a canopy whose
35
+ * walls cost more at a larger size without a variant for every combination.
29
36
  *=========================================*/ /** One choice in a modifier group. `priceCents` is added to the item, 0 for free. */ export const POS_MAX_MODIFIER_GROUPS = 10;
30
37
  export const POS_MAX_MODIFIER_OPTIONS = 20;
31
38
  /** $1,000 on one modifier: anything larger is a product, not an add-on. */ export const POS_MAX_MODIFIER_PRICE_CENTS = 100000;
39
+ function modifierPriceIsValid(price) {
40
+ return typeof price === 'number' && Number.isInteger(price) && price >= 0 && price <= POS_MAX_MODIFIER_PRICE_CENTS;
41
+ }
32
42
  /** Why a product's modifier groups cannot be saved, or null when they can. */ export function modifierGroupsProblem(groups) {
33
43
  if (groups == null) return null;
34
44
  if (!Array.isArray(groups)) return 'Modifiers must be a list';
@@ -53,9 +63,21 @@ export const POS_MAX_MODIFIER_OPTIONS = 20;
53
63
  optionIds.add(option.id);
54
64
  if (!String((_option_name = option.name) != null ? _option_name : '').trim()) return `Name every choice in “${group.name}”`;
55
65
  const price = option.priceCents;
56
- if (!Number.isInteger(price) || price < 0 || price > POS_MAX_MODIFIER_PRICE_CENTS) {
66
+ if (!modifierPriceIsValid(price)) {
57
67
  return `“${option.name}” needs a price of $0 to $1,000`;
58
68
  }
69
+ const byVariant = option.priceCentsByVariant;
70
+ if (byVariant != null) {
71
+ if (typeof byVariant !== 'object' || Array.isArray(byVariant)) {
72
+ return `“${option.name}” has prices that are not a list by option`;
73
+ }
74
+ if (Object.values(byVariant).some((cents)=>!modifierPriceIsValid(cents))) {
75
+ return `“${option.name}” needs a price of $0 to $1,000 for each option it is priced by`;
76
+ }
77
+ }
78
+ }
79
+ if (group.online != null && typeof group.online !== 'boolean') {
80
+ return `“${group.name}” is shown on the website or not`;
59
81
  }
60
82
  const { min, max } = group;
61
83
  if (!Number.isInteger(min) || min < 0) return `“${group.name}” needs a minimum of 0 or more`;
@@ -74,7 +96,42 @@ export const POS_MAX_MODIFIER_OPTIONS = 20;
74
96
  /** Whether a group must be answered before the item can be added. */ export function modifierGroupRequired(group) {
75
97
  return group.min > 0;
76
98
  }
77
- export function resolveLineModifiers(product, raw) {
99
+ /**
100
+ * The groups the product's page on the website offers (AGL-4100): those
101
+ * switched to "Show on the website". A register-only group is never sent to
102
+ * a shopper, never asked of one and never priced on an online order.
103
+ */ export function websiteModifierGroups(product) {
104
+ return productModifierGroups(product).filter((group)=>group.online === true);
105
+ }
106
+ /** Whether the website must ask a shopper a choice before the item goes in a cart. */ export function websiteModifiersRequired(product) {
107
+ return websiteModifierGroups(product).some(modifierGroupRequired);
108
+ }
109
+ /**
110
+ * What one choice adds to one unit of the picked variant (AGL-4100): its
111
+ * price on that variant where it has one, else its own price.
112
+ */ export function modifierOptionPriceCents(option, variantId) {
113
+ var _option_priceCentsByVariant;
114
+ const byVariant = variantId ? (_option_priceCentsByVariant = option.priceCentsByVariant) == null ? void 0 : _option_priceCentsByVariant[variantId] : undefined;
115
+ return typeof byVariant === 'number' && Number.isFinite(byVariant) ? byVariant : option.priceCents;
116
+ }
117
+ /** The picks a request names, as ids only, bounded; nothing for a value that is not a list. */ export function modifierSelectionFrom(raw) {
118
+ return (Array.isArray(raw) ? raw : []).slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS).map((entry)=>{
119
+ var _ref, _ref1;
120
+ return {
121
+ groupId: String((_ref = entry == null ? void 0 : entry.groupId) != null ? _ref : ''),
122
+ optionId: String((_ref1 = entry == null ? void 0 : entry.optionId) != null ? _ref1 : '')
123
+ };
124
+ });
125
+ }
126
+ /**
127
+ * Prices a line's chosen modifiers from the PRODUCT, in group order, and
128
+ * checks every group's minimum and maximum. Unknown ids, a repeated choice
129
+ * and a group answered past its maximum are refused with what to fix.
130
+ *
131
+ * `variantId` is the variant the line sells (AGL-4100): a choice priced by
132
+ * variant adds its price on that one. Without it every choice adds its own
133
+ * `priceCents`, as it always did.
134
+ */ export function resolveLineModifiers(product, raw, variantId) {
78
135
  const refuse = (error)=>({
79
136
  ok: false,
80
137
  modifiers: [],
@@ -82,13 +139,7 @@ export function resolveLineModifiers(product, raw) {
82
139
  error
83
140
  });
84
141
  const groups = productModifierGroups(product);
85
- const picks = (Array.isArray(raw) ? raw : []).slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS).map((entry)=>{
86
- var _ref, _ref1;
87
- return {
88
- groupId: String((_ref = entry == null ? void 0 : entry.groupId) != null ? _ref : ''),
89
- optionId: String((_ref1 = entry == null ? void 0 : entry.optionId) != null ? _ref1 : '')
90
- };
91
- });
142
+ const picks = modifierSelectionFrom(raw);
92
143
  const seen = new Set();
93
144
  for (const pick of picks){
94
145
  const key = `${pick.groupId}:${pick.optionId}`;
@@ -114,7 +165,7 @@ export function resolveLineModifiers(product, raw) {
114
165
  optionId: option.id,
115
166
  group: group.name,
116
167
  name: option.name,
117
- priceCents: option.priceCents
168
+ priceCents: modifierOptionPriceCents(option, variantId)
118
169
  });
119
170
  }
120
171
  }
@@ -124,6 +175,76 @@ export function resolveLineModifiers(product, raw) {
124
175
  extraCents: modifiers.reduce((sum, modifier)=>sum + modifier.priceCents, 0)
125
176
  };
126
177
  }
178
+ /**
179
+ * {@link resolveLineModifiers} for a sale made online (AGL-4100): only the
180
+ * groups shown on the website are offered, asked or priced. A register-only
181
+ * group, required or not, is not the shopper's to answer.
182
+ */ export function resolveWebsiteLineModifiers(product, raw, variantId) {
183
+ return resolveLineModifiers({
184
+ name: product.name,
185
+ modifierGroups: websiteModifierGroups(product)
186
+ }, raw, variantId);
187
+ }
188
+ /**
189
+ * The modifiers a PAID line names, for its order record (AGL-4100): every
190
+ * pick the product still offers, in group order, priced as it is now, with no
191
+ * minimum or maximum asked. The money was taken when the sale was priced;
192
+ * a choice the merchant removed since is left off the record, never a reason
193
+ * to lose the order.
194
+ */ export function soldLineModifiers(product, raw, variantId) {
195
+ const picks = modifierSelectionFrom(raw);
196
+ const modifiers = [];
197
+ for (const group of productModifierGroups(product)){
198
+ for (const option of group.options){
199
+ if (picks.some((pick)=>pick.groupId === group.id && pick.optionId === option.id)) {
200
+ modifiers.push({
201
+ groupId: group.id,
202
+ optionId: option.id,
203
+ group: group.name,
204
+ name: option.name,
205
+ priceCents: modifierOptionPriceCents(option, variantId)
206
+ });
207
+ }
208
+ }
209
+ }
210
+ return modifiers;
211
+ }
212
+ /**
213
+ * What a shopper is told about a cart line whose add-ons no longer answer
214
+ * the product (AGL-4100): the resolver's reason, and how to put it right.
215
+ */ export function cartLineModifierProblem(error) {
216
+ const reason = String(error != null ? error : '').trim() || 'The choices on this item have changed.';
217
+ return /Remove it and add it again/.test(reason) ? reason : `${reason} Remove it and add it again from its page.`;
218
+ }
219
+ /** The picks a priced line keeps: ids only, in the product's group order. */ export function modifierSelectionOf(modifiers) {
220
+ return modifiers.map((modifier)=>({
221
+ groupId: modifier.groupId,
222
+ optionId: modifier.optionId
223
+ }));
224
+ }
225
+ /**
226
+ * The picks as one metadata value (AGL-4100), for a payment processor that
227
+ * holds strings only: `[["group","option"],…]`. Empty for no picks.
228
+ */ export function encodeModifierSelection(selection) {
229
+ if (!(selection == null ? void 0 : selection.length)) return '';
230
+ return JSON.stringify(selection.map((pick)=>[
231
+ pick.groupId,
232
+ pick.optionId
233
+ ]));
234
+ }
235
+ /** {@link encodeModifierSelection} read back; nothing for a value it did not write. */ export function decodeModifierSelection(value) {
236
+ if (typeof value !== 'string' || !value) return [];
237
+ try {
238
+ const parsed = JSON.parse(value);
239
+ if (!Array.isArray(parsed)) return [];
240
+ return parsed.filter((pair)=>Array.isArray(pair) && typeof pair[0] === 'string' && typeof pair[1] === 'string').slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS).map(([groupId, optionId])=>({
241
+ groupId,
242
+ optionId
243
+ }));
244
+ } catch (unused) {
245
+ return [];
246
+ }
247
+ }
127
248
  /** The label a line prints: the variant, then each modifier (`Large / Oat milk, Extra shot`). */ export function lineLabelWithModifiers(variantLabel, modifiers) {
128
249
  const added = modifiers.map((modifier)=>modifier.name).join(', ');
129
250
  return [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/product-modifiers.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 * REGISTER MODIFIERS AND QUICK KEYS (AGL-3607).\n *\n * A modifier is a choice added to an item as it is rung up — \"Oat milk\",\n * \"Extra shot +$0.75\", \"No onions\" — that is not a stocked variant. Groups\n * live on the product (`modifierGroups`); the register sends the chosen\n * option ids with each line and the SERVER prices them from the product\n * document, so a register never names what a modifier costs.\n *\n * A chosen modifier is folded into the line's `variantLabel` as well as kept\n * structured in `modifiers`: every receipt, email, order view and export\n * already prints the label, so the choice reaches all of them without each\n * learning a new field.\n *=========================================*/\n\n/** One choice in a modifier group. `priceCents` is added to the item, 0 for free. */\nexport interface ProductModifierOption {\n id: string\n name: string\n priceCents: number\n}\n\n/**\n * A set of choices offered together (\"Milk\", \"Add-ons\"). `min` of 1 or more\n * makes it required; `max` caps how many can be picked (1 = pick one).\n */\nexport interface ProductModifierGroup {\n id: string\n name: string\n min: number\n max: number\n options: ProductModifierOption[]\n}\n\n/** A modifier as the order line keeps it: names and price as sold. */\nexport interface OrderLineModifier {\n groupId: string\n optionId: string\n group: string\n name: string\n priceCents: number\n}\n\n/** What the register sends for a line: the option ids it picked, nothing priced. */\nexport interface ModifierSelection {\n groupId: string\n optionId: string\n}\n\nexport const POS_MAX_MODIFIER_GROUPS = 10\nexport const POS_MAX_MODIFIER_OPTIONS = 20\n/** $1,000 on one modifier: anything larger is a product, not an add-on. */\nexport const POS_MAX_MODIFIER_PRICE_CENTS = 100_000\n\n/** Why a product's modifier groups cannot be saved, or null when they can. */\nexport function modifierGroupsProblem(groups: unknown): string | null {\n if (groups == null) return null\n if (!Array.isArray(groups)) return 'Modifiers must be a list'\n if (groups.length > POS_MAX_MODIFIER_GROUPS) {\n return `At most ${POS_MAX_MODIFIER_GROUPS} modifier groups per product`\n }\n const groupIds = new Set<string>()\n for (const group of groups as ProductModifierGroup[]) {\n if (!group?.id || groupIds.has(group.id)) return 'Modifier groups need unique ids'\n groupIds.add(group.id)\n if (!String(group.name ?? '').trim()) return 'Name every modifier group'\n const options = Array.isArray(group.options) ? group.options : []\n if (options.length === 0) return `Add a choice to “${group.name}”`\n if (options.length > POS_MAX_MODIFIER_OPTIONS) {\n return `“${group.name}” has more than ${POS_MAX_MODIFIER_OPTIONS} choices`\n }\n const optionIds = new Set<string>()\n for (const option of options) {\n if (!option?.id || optionIds.has(option.id)) return 'Modifier choices need unique ids'\n optionIds.add(option.id)\n if (!String(option.name ?? '').trim()) return `Name every choice in “${group.name}”`\n const price = option.priceCents\n if (!Number.isInteger(price) || price < 0 || price > POS_MAX_MODIFIER_PRICE_CENTS) {\n return `“${option.name}” needs a price of $0 to $1,000`\n }\n }\n const { min, max } = group\n if (!Number.isInteger(min) || min < 0) return `“${group.name}” needs a minimum of 0 or more`\n if (!Number.isInteger(max) || max < 1 || max > options.length) {\n return `“${group.name}” allows 1 to ${options.length} choices`\n }\n if (min > max) return `“${group.name}” requires more choices than it allows`\n }\n return null\n}\n\n/** The product's groups, or none when they are missing or malformed. */\nexport function productModifierGroups(product: {\n modifierGroups?: unknown\n}): ProductModifierGroup[] {\n const groups = product?.modifierGroups\n if (!Array.isArray(groups) || modifierGroupsProblem(groups)) return []\n return groups as ProductModifierGroup[]\n}\n\n/** Whether a group must be answered before the item can be added. */\nexport function modifierGroupRequired(group: ProductModifierGroup): boolean {\n return group.min > 0\n}\n\n/**\n * Prices a line's chosen modifiers from the PRODUCT, in group order, and\n * checks every group's minimum and maximum. Unknown ids, a repeated choice\n * and a group answered past its maximum are refused with what to fix.\n */\nexport interface ResolvedLineModifiers {\n ok: boolean\n /** The chosen modifiers, in group order; empty when not `ok`. */\n modifiers: OrderLineModifier[]\n /** What they add to one unit, in cents. */\n extraCents: number\n /** What to fix, when not `ok`. */\n error?: string\n}\n\nexport function resolveLineModifiers(\n product: { name?: string; modifierGroups?: unknown },\n raw: unknown,\n): ResolvedLineModifiers {\n const refuse = (error: string): ResolvedLineModifiers => ({\n ok: false,\n modifiers: [],\n extraCents: 0,\n error,\n })\n const groups = productModifierGroups(product)\n const picks: ModifierSelection[] = (Array.isArray(raw) ? raw : [])\n .slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS)\n .map((entry: any) => ({\n groupId: String(entry?.groupId ?? ''),\n optionId: String(entry?.optionId ?? ''),\n }))\n const seen = new Set<string>()\n for (const pick of picks) {\n const key = `${pick.groupId}:${pick.optionId}`\n if (seen.has(key)) return refuse(`A choice on ${product.name} was picked twice.`)\n seen.add(key)\n const group = groups.find((entry) => entry.id === pick.groupId)\n if (!group || !group.options.some((option) => option.id === pick.optionId)) {\n return refuse(`A choice on ${product.name} is no longer offered. Remove it and add it again.`)\n }\n }\n const modifiers: OrderLineModifier[] = []\n for (const group of groups) {\n const chosen = group.options.filter((option) =>\n picks.some((pick) => pick.groupId === group.id && pick.optionId === option.id),\n )\n if (chosen.length < group.min) {\n return refuse(`Choose ${group.name.toLowerCase()} for ${product.name}.`)\n }\n if (chosen.length > group.max) {\n return refuse(`Choose at most ${group.max} for ${group.name.toLowerCase()} on ${product.name}.`)\n }\n for (const option of chosen) {\n modifiers.push({\n groupId: group.id,\n optionId: option.id,\n group: group.name,\n name: option.name,\n priceCents: option.priceCents,\n })\n }\n }\n return {\n ok: true,\n modifiers,\n extraCents: modifiers.reduce((sum, modifier) => sum + modifier.priceCents, 0),\n }\n}\n\n/** The label a line prints: the variant, then each modifier (`Large / Oat milk, Extra shot`). */\nexport function lineLabelWithModifiers(\n variantLabel: string | undefined,\n modifiers: readonly Pick<OrderLineModifier, 'name'>[],\n): string {\n const added = modifiers.map((modifier) => modifier.name).join(', ')\n return [variantLabel, added].filter(Boolean).join(' / ')\n}\n\n/** Two lines merge only when they are the same item with the same choices. */\nexport function modifierSelectionKey(selection: readonly ModifierSelection[] | undefined): string {\n return [...(selection ?? [])]\n .map((pick) => `${pick.groupId}:${pick.optionId}`)\n .sort()\n .join('|')\n}\n"],"names":["POS_MAX_MODIFIER_GROUPS","POS_MAX_MODIFIER_OPTIONS","POS_MAX_MODIFIER_PRICE_CENTS","modifierGroupsProblem","groups","Array","isArray","length","groupIds","Set","group","id","has","add","String","name","trim","options","optionIds","option","price","priceCents","Number","isInteger","min","max","productModifierGroups","product","modifierGroups","modifierGroupRequired","resolveLineModifiers","raw","refuse","error","ok","modifiers","extraCents","picks","slice","map","entry","groupId","optionId","seen","pick","key","find","some","chosen","filter","toLowerCase","push","reduce","sum","modifier","lineLabelWithModifiers","variantLabel","added","join","Boolean","modifierSelectionKey","selection","sort"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/model/product-modifiers.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 * REGISTER MODIFIERS AND QUICK KEYS (AGL-3607).\n *\n * A modifier is a choice added to an item as it is rung up — \"Oat milk\",\n * \"Extra shot +$0.75\", \"No onions\" — that is not a stocked variant. Groups\n * live on the product (`modifierGroups`); the register sends the chosen\n * option ids with each line and the SERVER prices them from the product\n * document, so a register never names what a modifier costs.\n *\n * A chosen modifier is folded into the line's `variantLabel` as well as kept\n * structured in `modifiers`: every receipt, email, order view and export\n * already prints the label, so the choice reaches all of them without each\n * learning a new field.\n *\n * ADD-ONS ON THE WEBSITE (AGL-4100). A group switched to \"Show on the\n * website\" (`online`) is offered on the product's page too, and a cart line,\n * Buy now and the order carry its picks the same way: ids from the shopper,\n * prices from the product. A choice may cost more on one variant than\n * another (`priceCentsByVariant`), which is how one page sells a canopy whose\n * walls cost more at a larger size without a variant for every combination.\n *=========================================*/\n\n/** One choice in a modifier group. `priceCents` is added to the item, 0 for free. */\nexport interface ProductModifierOption {\n id: string\n name: string\n priceCents: number\n /**\n * What the choice adds on one variant, keyed by variant id (AGL-4100):\n * walls cost more on a larger canopy. A variant not listed pays\n * `priceCents`. Read through {@link modifierOptionPriceCents}.\n */\n priceCentsByVariant?: Record<string, number>\n}\n\n/**\n * A set of choices offered together (\"Milk\", \"Add-ons\"). `min` of 1 or more\n * makes it required; `max` caps how many can be picked (1 = pick one).\n */\nexport interface ProductModifierGroup {\n id: string\n name: string\n min: number\n max: number\n options: ProductModifierOption[]\n /**\n * Offered on the product's page on the website as well as at the register\n * (AGL-4100), \"Show on the website\". Absent reads as register only, so a\n * group made before the switch changes nothing on a live store; the\n * console makes new groups with it on.\n */\n online?: boolean\n}\n\n/** A modifier as the order line keeps it: names and price as sold. */\nexport interface OrderLineModifier {\n groupId: string\n optionId: string\n group: string\n name: string\n priceCents: number\n}\n\n/** What the register sends for a line: the option ids it picked, nothing priced. */\nexport interface ModifierSelection {\n groupId: string\n optionId: string\n}\n\nexport const POS_MAX_MODIFIER_GROUPS = 10\nexport const POS_MAX_MODIFIER_OPTIONS = 20\n/** $1,000 on one modifier: anything larger is a product, not an add-on. */\nexport const POS_MAX_MODIFIER_PRICE_CENTS = 100_000\n\nfunction modifierPriceIsValid(price: unknown): boolean {\n return (\n typeof price === 'number' &&\n Number.isInteger(price) &&\n price >= 0 &&\n price <= POS_MAX_MODIFIER_PRICE_CENTS\n )\n}\n\n/** Why a product's modifier groups cannot be saved, or null when they can. */\nexport function modifierGroupsProblem(groups: unknown): string | null {\n if (groups == null) return null\n if (!Array.isArray(groups)) return 'Modifiers must be a list'\n if (groups.length > POS_MAX_MODIFIER_GROUPS) {\n return `At most ${POS_MAX_MODIFIER_GROUPS} modifier groups per product`\n }\n const groupIds = new Set<string>()\n for (const group of groups as ProductModifierGroup[]) {\n if (!group?.id || groupIds.has(group.id)) return 'Modifier groups need unique ids'\n groupIds.add(group.id)\n if (!String(group.name ?? '').trim()) return 'Name every modifier group'\n const options = Array.isArray(group.options) ? group.options : []\n if (options.length === 0) return `Add a choice to “${group.name}”`\n if (options.length > POS_MAX_MODIFIER_OPTIONS) {\n return `“${group.name}” has more than ${POS_MAX_MODIFIER_OPTIONS} choices`\n }\n const optionIds = new Set<string>()\n for (const option of options) {\n if (!option?.id || optionIds.has(option.id)) return 'Modifier choices need unique ids'\n optionIds.add(option.id)\n if (!String(option.name ?? '').trim()) return `Name every choice in “${group.name}”`\n const price = option.priceCents\n if (!modifierPriceIsValid(price)) {\n return `“${option.name}” needs a price of $0 to $1,000`\n }\n const byVariant = option.priceCentsByVariant\n if (byVariant != null) {\n if (typeof byVariant !== 'object' || Array.isArray(byVariant)) {\n return `“${option.name}” has prices that are not a list by option`\n }\n if (Object.values(byVariant).some((cents) => !modifierPriceIsValid(cents))) {\n return `“${option.name}” needs a price of $0 to $1,000 for each option it is priced by`\n }\n }\n }\n if (group.online != null && typeof group.online !== 'boolean') {\n return `“${group.name}” is shown on the website or not`\n }\n const { min, max } = group\n if (!Number.isInteger(min) || min < 0) return `“${group.name}” needs a minimum of 0 or more`\n if (!Number.isInteger(max) || max < 1 || max > options.length) {\n return `“${group.name}” allows 1 to ${options.length} choices`\n }\n if (min > max) return `“${group.name}” requires more choices than it allows`\n }\n return null\n}\n\n/** The product's groups, or none when they are missing or malformed. */\nexport function productModifierGroups(product: {\n modifierGroups?: unknown\n}): ProductModifierGroup[] {\n const groups = product?.modifierGroups\n if (!Array.isArray(groups) || modifierGroupsProblem(groups)) return []\n return groups as ProductModifierGroup[]\n}\n\n/** Whether a group must be answered before the item can be added. */\nexport function modifierGroupRequired(group: ProductModifierGroup): boolean {\n return group.min > 0\n}\n\n/**\n * The groups the product's page on the website offers (AGL-4100): those\n * switched to \"Show on the website\". A register-only group is never sent to\n * a shopper, never asked of one and never priced on an online order.\n */\nexport function websiteModifierGroups(product: { modifierGroups?: unknown }): ProductModifierGroup[] {\n return productModifierGroups(product).filter((group) => group.online === true)\n}\n\n/** Whether the website must ask a shopper a choice before the item goes in a cart. */\nexport function websiteModifiersRequired(product: { modifierGroups?: unknown }): boolean {\n return websiteModifierGroups(product).some(modifierGroupRequired)\n}\n\n/**\n * What one choice adds to one unit of the picked variant (AGL-4100): its\n * price on that variant where it has one, else its own price.\n */\nexport function modifierOptionPriceCents(\n option: Pick<ProductModifierOption, 'priceCents' | 'priceCentsByVariant'>,\n variantId?: string | null,\n): number {\n const byVariant = variantId ? option.priceCentsByVariant?.[variantId] : undefined\n return typeof byVariant === 'number' && Number.isFinite(byVariant) ? byVariant : option.priceCents\n}\n\n/** A line's chosen modifiers, priced. */\nexport interface ResolvedLineModifiers {\n ok: boolean\n /** The chosen modifiers, in group order; empty when not `ok`. */\n modifiers: OrderLineModifier[]\n /** What they add to one unit, in cents. */\n extraCents: number\n /** What to fix, when not `ok`. */\n error?: string\n}\n\n/** The picks a request names, as ids only, bounded; nothing for a value that is not a list. */\nexport function modifierSelectionFrom(raw: unknown): ModifierSelection[] {\n return (Array.isArray(raw) ? raw : [])\n .slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS)\n .map((entry: any) => ({\n groupId: String(entry?.groupId ?? ''),\n optionId: String(entry?.optionId ?? ''),\n }))\n}\n\n/**\n * Prices a line's chosen modifiers from the PRODUCT, in group order, and\n * checks every group's minimum and maximum. Unknown ids, a repeated choice\n * and a group answered past its maximum are refused with what to fix.\n *\n * `variantId` is the variant the line sells (AGL-4100): a choice priced by\n * variant adds its price on that one. Without it every choice adds its own\n * `priceCents`, as it always did.\n */\nexport function resolveLineModifiers(\n product: { name?: string; modifierGroups?: unknown },\n raw: unknown,\n variantId?: string | null,\n): ResolvedLineModifiers {\n const refuse = (error: string): ResolvedLineModifiers => ({\n ok: false,\n modifiers: [],\n extraCents: 0,\n error,\n })\n const groups = productModifierGroups(product)\n const picks = modifierSelectionFrom(raw)\n const seen = new Set<string>()\n for (const pick of picks) {\n const key = `${pick.groupId}:${pick.optionId}`\n if (seen.has(key)) return refuse(`A choice on ${product.name} was picked twice.`)\n seen.add(key)\n const group = groups.find((entry) => entry.id === pick.groupId)\n if (!group || !group.options.some((option) => option.id === pick.optionId)) {\n return refuse(`A choice on ${product.name} is no longer offered. Remove it and add it again.`)\n }\n }\n const modifiers: OrderLineModifier[] = []\n for (const group of groups) {\n const chosen = group.options.filter((option) =>\n picks.some((pick) => pick.groupId === group.id && pick.optionId === option.id),\n )\n if (chosen.length < group.min) {\n return refuse(`Choose ${group.name.toLowerCase()} for ${product.name}.`)\n }\n if (chosen.length > group.max) {\n return refuse(`Choose at most ${group.max} for ${group.name.toLowerCase()} on ${product.name}.`)\n }\n for (const option of chosen) {\n modifiers.push({\n groupId: group.id,\n optionId: option.id,\n group: group.name,\n name: option.name,\n priceCents: modifierOptionPriceCents(option, variantId),\n })\n }\n }\n return {\n ok: true,\n modifiers,\n extraCents: modifiers.reduce((sum, modifier) => sum + modifier.priceCents, 0),\n }\n}\n\n/**\n * {@link resolveLineModifiers} for a sale made online (AGL-4100): only the\n * groups shown on the website are offered, asked or priced. A register-only\n * group, required or not, is not the shopper's to answer.\n */\nexport function resolveWebsiteLineModifiers(\n product: { name?: string; modifierGroups?: unknown },\n raw: unknown,\n variantId?: string | null,\n): ResolvedLineModifiers {\n return resolveLineModifiers(\n { name: product.name, modifierGroups: websiteModifierGroups(product) },\n raw,\n variantId,\n )\n}\n\n/**\n * The modifiers a PAID line names, for its order record (AGL-4100): every\n * pick the product still offers, in group order, priced as it is now, with no\n * minimum or maximum asked. The money was taken when the sale was priced;\n * a choice the merchant removed since is left off the record, never a reason\n * to lose the order.\n */\nexport function soldLineModifiers(\n product: { modifierGroups?: unknown },\n raw: unknown,\n variantId?: string | null,\n): OrderLineModifier[] {\n const picks = modifierSelectionFrom(raw)\n const modifiers: OrderLineModifier[] = []\n for (const group of productModifierGroups(product)) {\n for (const option of group.options) {\n if (picks.some((pick) => pick.groupId === group.id && pick.optionId === option.id)) {\n modifiers.push({\n groupId: group.id,\n optionId: option.id,\n group: group.name,\n name: option.name,\n priceCents: modifierOptionPriceCents(option, variantId),\n })\n }\n }\n }\n return modifiers\n}\n\n/**\n * What a shopper is told about a cart line whose add-ons no longer answer\n * the product (AGL-4100): the resolver's reason, and how to put it right.\n */\nexport function cartLineModifierProblem(error: string | undefined): string {\n const reason = String(error ?? '').trim() || 'The choices on this item have changed.'\n return /Remove it and add it again/.test(reason)\n ? reason\n : `${reason} Remove it and add it again from its page.`\n}\n\n/** The picks a priced line keeps: ids only, in the product's group order. */\nexport function modifierSelectionOf(\n modifiers: readonly Pick<OrderLineModifier, 'groupId' | 'optionId'>[],\n): ModifierSelection[] {\n return modifiers.map((modifier) => ({ groupId: modifier.groupId, optionId: modifier.optionId }))\n}\n\n/**\n * The picks as one metadata value (AGL-4100), for a payment processor that\n * holds strings only: `[[\"group\",\"option\"],…]`. Empty for no picks.\n */\nexport function encodeModifierSelection(selection: readonly ModifierSelection[] | undefined): string {\n if (!selection?.length) return ''\n return JSON.stringify(selection.map((pick) => [pick.groupId, pick.optionId]))\n}\n\n/** {@link encodeModifierSelection} read back; nothing for a value it did not write. */\nexport function decodeModifierSelection(value: unknown): ModifierSelection[] {\n if (typeof value !== 'string' || !value) return []\n try {\n const parsed: unknown = JSON.parse(value)\n if (!Array.isArray(parsed)) return []\n return parsed\n .filter(\n (pair): pair is [string, string] =>\n Array.isArray(pair) && typeof pair[0] === 'string' && typeof pair[1] === 'string',\n )\n .slice(0, POS_MAX_MODIFIER_GROUPS * POS_MAX_MODIFIER_OPTIONS)\n .map(([groupId, optionId]) => ({ groupId, optionId }))\n } catch {\n return []\n }\n}\n\n/** The label a line prints: the variant, then each modifier (`Large / Oat milk, Extra shot`). */\nexport function lineLabelWithModifiers(\n variantLabel: string | undefined,\n modifiers: readonly Pick<OrderLineModifier, 'name'>[],\n): string {\n const added = modifiers.map((modifier) => modifier.name).join(', ')\n return [variantLabel, added].filter(Boolean).join(' / ')\n}\n\n/** Two lines merge only when they are the same item with the same choices. */\nexport function modifierSelectionKey(selection: readonly ModifierSelection[] | undefined): string {\n return [...(selection ?? [])]\n .map((pick) => `${pick.groupId}:${pick.optionId}`)\n .sort()\n .join('|')\n}\n"],"names":["POS_MAX_MODIFIER_GROUPS","POS_MAX_MODIFIER_OPTIONS","POS_MAX_MODIFIER_PRICE_CENTS","modifierPriceIsValid","price","Number","isInteger","modifierGroupsProblem","groups","Array","isArray","length","groupIds","Set","group","id","has","add","String","name","trim","options","optionIds","option","priceCents","byVariant","priceCentsByVariant","Object","values","some","cents","online","min","max","productModifierGroups","product","modifierGroups","modifierGroupRequired","websiteModifierGroups","filter","websiteModifiersRequired","modifierOptionPriceCents","variantId","undefined","isFinite","modifierSelectionFrom","raw","slice","map","entry","groupId","optionId","resolveLineModifiers","refuse","error","ok","modifiers","extraCents","picks","seen","pick","key","find","chosen","toLowerCase","push","reduce","sum","modifier","resolveWebsiteLineModifiers","soldLineModifiers","cartLineModifierProblem","reason","test","modifierSelectionOf","encodeModifierSelection","selection","JSON","stringify","decodeModifierSelection","value","parsed","parse","pair","lineLabelWithModifiers","variantLabel","added","join","Boolean","modifierSelectionKey","sort"],"mappings":"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"}
package/src/lib/plugin.js CHANGED
@@ -40,6 +40,10 @@ import { registerCommerceTransferUi } from "./transfer/register-transfer-ui.js";
40
40
  import { COMMERCE_SEARCH_SOURCES } from "./model/commerce-search-sources.js";
41
41
  import { COMMERCE_CONFIG_SCHEMA } from "./plugin-config.js";
42
42
  /** Code-split: the Products console page only loads when opened. */ const CommerceConsolePage = lazy(()=>import("./components/commerce-console-page.js"));
43
+ /** "Start a quote" on a quote request in the Inbox and on a contact in the CRM (AGL-4098). */ const QuoteStartFromRequest = lazy(()=>import("./components/console/quote-start.component.js"));
44
+ const QuoteStartFromContact = lazy(()=>import("./components/console/quote-start.component.js").then((module)=>({
45
+ default: module.QuoteStartFromContact
46
+ })));
43
47
  /**
44
48
  * Console half (AGL-395): registers the Products nav item in the
45
49
  * ConsoleExtension registry so the commerce console surface is owned by the
@@ -155,6 +159,21 @@ import { COMMERCE_CONFIG_SCHEMA } from "./plugin-config.js";
155
159
  widgetId: 'commerce-site-member-purchases',
156
160
  title: 'Purchases',
157
161
  Component: SiteMemberPurchases
162
+ },
163
+ // Start a quote from a quote request, among an open submission's
164
+ // actions in the Inbox, and from a contact, in its header in the CRM
165
+ // (AGL-4098). Each zone is its host's; neither host imports this plugin.
166
+ {
167
+ slot: 'inboxSubmissionActions',
168
+ widgetId: 'commerce-quote-from-request',
169
+ title: 'Start a quote',
170
+ Component: QuoteStartFromRequest
171
+ },
172
+ {
173
+ slot: 'crmRecordActions',
174
+ widgetId: 'commerce-quote-from-contact',
175
+ title: 'Start a quote',
176
+ Component: QuoteStartFromContact
158
177
  }
159
178
  ],
160
179
  navItems: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiStorefrontOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nconst PosConsolePage = lazy(() => import('./components/console/pos-page.component'))\n/** The customer display (AGL-3608): loads only on the public page that shows it. */\nconst PosDisplayPage = lazy(() => import('./components/console/pos-display-page.component'))\nconst PosKioskPage = lazy(() => import('./components/console/pos-kiosk-page.component'))\nconst PosHardwareTermsPage = lazy(() => import('./components/console/pos-hardware-terms-page.component'))\nconst PosHardwareTermsSignPage = lazy(() => import('./components/console/pos-hardware-terms-sign-page.component'))\n/** Staff → Hardware orders (AGL-4038): loads only on its staff page. */\nconst PosHardwareOrdersStaffPage = lazy(\n () => import('./components/console/pos-hardware-orders-staff-page.component'),\n)\n/** Staff → Reader start-up screen (AGL-4048): loads only on its staff page. */\nconst PosReaderSplashStaffPage = lazy(\n () => import('./components/console/pos-reader-splash-staff-page.component'),\n)\nconst CommerceGlanceCard = lazy(\n () => import('./components/console/commerce-glance-card.component'),\n)\nconst NewestSiteUsersCard = lazy(\n () => import('./components/console/newest-site-users-card.component'),\n)\nconst SiteMemberPurchases = lazy(\n () => import('./components/console/site-member-purchases.component'),\n)\nimport {\n COMMERCE_CONSOLE_SECTIONS,\n COMMERCE_RETIRED_SECTIONS,\n} from './components/commerce-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { COMMERCE_PERMISSIONS } from './model/plugin-permissions'\nimport { registerCommerceRecordLists } from './model/commerce-record-lists'\nimport { registerCommerceRecordRoutes } from './model/commerce-record-routes'\nimport { registerCommerceZones } from './components/console/product-zones'\nimport { registerCommerceOrderZones } from './components/console/order-zones'\nimport { registerCommerceReturnZones } from './components/console/return-zones'\nimport { registerCommerceStoreZones } from './components/console/store-zones'\nimport { registerCommercePosZones } from './components/console/pos-zones'\nimport { registerCommerceTransferUi } from './transfer/register-transfer-ui'\nimport { COMMERCE_SEARCH_SOURCES } from './model/commerce-search-sources'\nimport { COMMERCE_CONFIG_SCHEMA } from './plugin-config'\n\n/** Code-split: the Products console page only loads when opened. */\nconst CommerceConsolePage = lazy(() => import('./components/commerce-console-page'))\n\n/**\n * Console half (AGL-395): registers the Products nav item in the\n * ConsoleExtension registry so the commerce console surface is owned by the\n * plugin, like events and email. The page body still lives in the console\n * app (`/[hostId]/products`): its product editor depends on the app's media\n * browser, which transitively pulls in the org/session context, so a full\n * body relocation needs a plugin-consumable media-picker context first —\n * tracked as follow-up. The nav item omits a `Component`, so the shell's\n * nav renders the link and the existing named route serves the page.\n */\nexport function registerCommerceConsole(): void {\n // Where a product is read, for surfaces outside this plugin (AGL-3080).\n registerCommerceRecordRoutes()\n registerCommerceRecordLists()\n // The zones its product editor, products hub and CSV import dialog host,\n // with the props each hands a widget (AGL-2916, AGL-3080).\n registerCommerceZones()\n // …and the order dialog's two (AGL-3611), where a shipping plugin's widget\n // buys a label and records the shipment through the dialog's own route.\n registerCommerceOrderZones()\n // …and the return dialog's (AGL-3611), where a shipping plugin's widget\n // buys the buyer a return label and attaches it through this plugin's route.\n registerCommerceReturnZones()\n // The orders list's bulk actions and the store's Settings (AGL-3612).\n registerCommerceStoreZones()\n // The register's orders from other channels — a delivery app's (AGL-3644).\n registerCommercePosZones()\n // Its import and export resources' names and the products wizard's own\n // step (AGL-3531); the server halves register from the console's server\n // declarations.\n registerCommerceTransferUi()\n // Plugin-declared permissions (AGL-435): tier defaults ride every\n // resolved role set; custom roles override key-by-key.\n Aglyn.registerPluginPermissions(COMMERCE_PERMISSIONS)\n // Register discount ceiling (AGL-2161) — the generic settings card renders\n // it from this schema, so there is no bespoke UI to keep in sync.\n Aglyn.registerPluginConfigSchema(COMMERCE_CONFIG_SCHEMA)\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Commerce',\n searchSources: COMMERCE_SEARCH_SOURCES,\n // The screen a register turns toward its customer (AGL-3608), at\n // `/kiosk/commerce/pos-display`. No staff session: the device proves\n // itself with the display token it got by pairing, and\n // `server/pos-display.ts` refuses every read without one.\n // Every workspace's orders from the POS hardware shop (AGL-4038), where\n // staff record Stripe's order id and the tracking, move each order on,\n // and cancel or refund one that has not shipped. The staff area loads\n // this registrar through commerce's `staff` surface.\n staffPages: [\n {\n id: 'hardware-orders',\n label: 'Hardware orders',\n header: { title: 'Hardware Orders', docsTopic: 'posHardware' },\n Component: PosHardwareOrdersStaffPage,\n },\n // The platform's default card reader start-up screen (AGL-4048), which\n // every store that has not picked its own logo or image shows.\n {\n id: 'reader-start-up-screen',\n label: 'Reader start-up screen',\n header: { title: 'Reader Start-up Screen', docsTopic: 'posHardware' },\n Component: PosReaderSplashStaffPage,\n },\n ],\n publicPages: [\n { path: '/pos-display', title: 'Customer display', Component: PosDisplayPage },\n // The self-service kiosk (AGL-3623), paired the same way with a kiosk\n // code; `server/pos-kiosk.ts` refuses every call without its token.\n { path: '/pos-kiosk', title: 'Self-service kiosk', Component: PosKioskPage },\n // The terms of sale for a card reader sold at the platform's register\n // (AGL-4038): on its customer display, by the display's own token\n // (`server/pos-display.ts`), and the printable booth sign, which holds\n // only the published terms' address and summary.\n { path: '/hardware-terms', title: 'Terms of sale', Component: PosHardwareTermsPage },\n { path: '/hardware-terms-sign', title: 'Terms of sale sign', Component: PosHardwareTermsSignPage },\n ],\n // Dashboard/analytics glance card (AGL-419): rendered through the\n // shell's 'commerceGlance' widget slot.\n widgets: [\n {\n slot: 'commerceGlance',\n widgetId: 'commerce-glance',\n title: 'Commerce',\n Component: CommerceGlanceCard,\n },\n // What a site user bought — lifetime total, orders and subscriptions —\n // in their drawer on the site's Users page: the orders and\n // subscriptions are this plugin's.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.siteMember,\n widgetId: 'commerce-site-member-purchases',\n title: 'Purchases',\n Component: SiteMemberPurchases,\n },\n ],\n navItems: [\n {\n label: 'Products',\n href: '/products',\n icon: { path: mdiStorefrontOutline.path },\n // Sections as ROUTES (AGL-2501): `/products/orders` and friends are\n // real URLs the shell resolves and gates, so the page mounts the one\n // being read instead of subscribing all six.\n sections: COMMERCE_CONSOLE_SECTIONS,\n // `/products/settings` was split into a page per setting (AGL-3713);\n // links to it open the first of them.\n retiredSections: COMMERCE_RETIRED_SECTIONS,\n header: {\n title: 'Products',\n icon: { path: mdiStorefrontOutline.path },\n // The overview, not `catalog`: this surface is the whole commerce\n // console — Catalog, Orders, Promotions and Reservations.\n docsTopic: 'commerce',\n },\n Component: CommerceConsolePage,\n },\n {\n // POS register (AGL-312/419): relocated from the app's /pos route —\n // the generic [pluginSlug] shell now serves it at the same URL.\n label: 'POS',\n href: '/pos',\n /*\n * WHO may open the register, declared on THIS nav item rather than on\n * the extension.\n *\n * A key here narrows one surface; a key on the extension would gate\n * every surface it registers, and Products is the sibling — a catalog\n * anyone who works on the site may open. The register is the one that\n * takes money and lists checked-in guests by name, so the two\n * genuinely have different answers and only this one is narrowed.\n *\n * `managePos` rather than a dotted catalog key because it is the key\n * the sale itself is refused by: `server/pos-order.ts` resolves the\n * member's permission map and returns 403 unless `managePos` holds.\n * The shell answers this declaration out of that same resolved map, so\n * the console offers a register exactly to the people the route will\n * accept a sale from — the gate and its server twin cannot disagree,\n * because they are reading one value.\n *\n * `pos-order.ts` stays the enforcement point regardless. This decides\n * whether the surface is built at all, which is the part a check\n * inside the page cannot do: by the time the page can refuse itself it\n * has mounted and its product, register, location and reservation\n * listeners are already open.\n */\n permission: 'managePos',\n icon: { path: mdiStorefrontOutline.path },\n header: { title: 'Point of Sale', docsTopic: 'pos' },\n Component: PosConsolePage,\n },\n ],\n })\n /*\n * The USER ACCOUNTS half, registered under its own id.\n *\n * `accounts` is a switch rather than a bundle: the member blocks and every\n * `membership/*` handler ship inside this package, which is why the id\n * carries no loader manifest entry of its own. A second extension is how a\n * card about visitor accounts is gated on the switch that decides whether\n * the site serves them — `listConsoleWidgets` filters by the workspace's\n * effective plugin ids, and `accounts` is default-OFF per site.\n *\n * A separate call rather than more widgets on the commerce extension: the\n * registry is keyed by `pluginId` and that key IS the gate, so folding this\n * card in above would have kept the dashboard defect it fixes — `Newest\n * site users` on a site whose /signin returns 404.\n */\n Aglyn.registerConsoleExtension({\n pluginId: Aglyn.ACCOUNTS_PLUGIN_ID,\n displayName: 'User Accounts',\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'accounts-newest-site-users',\n title: 'Newest site users',\n Component: NewestSiteUsersCard,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiStorefrontOutline","lazy","PosConsolePage","PosDisplayPage","PosKioskPage","PosHardwareTermsPage","PosHardwareTermsSignPage","PosHardwareOrdersStaffPage","PosReaderSplashStaffPage","CommerceGlanceCard","NewestSiteUsersCard","SiteMemberPurchases","COMMERCE_CONSOLE_SECTIONS","COMMERCE_RETIRED_SECTIONS","BUNDLE_ID","COMMERCE_PERMISSIONS","registerCommerceRecordLists","registerCommerceRecordRoutes","registerCommerceZones","registerCommerceOrderZones","registerCommerceReturnZones","registerCommerceStoreZones","registerCommercePosZones","registerCommerceTransferUi","COMMERCE_SEARCH_SOURCES","COMMERCE_CONFIG_SCHEMA","CommerceConsolePage","registerCommerceConsole","registerPluginPermissions","registerPluginConfigSchema","registerConsoleExtension","pluginId","displayName","searchSources","staffPages","id","label","header","title","docsTopic","Component","publicPages","path","widgets","slot","widgetId","CONSOLE_WIDGET_SLOTS","siteMember","navItems","href","icon","sections","retiredSections","permission","ACCOUNTS_PLUGIN_ID","hostDashboard"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,oBAAoB,QAAQ,yBAAwB;AAC7D,SAASC,IAAI,QAAQ,QAAO;AAC5B,MAAMC,iBAAiBD,KAAK,IAAM,MAAM,CAAC;AACzC,kFAAkF,GAClF,MAAME,iBAAiBF,KAAK,IAAM,MAAM,CAAC;AACzC,MAAMG,eAAeH,KAAK,IAAM,MAAM,CAAC;AACvC,MAAMI,uBAAuBJ,KAAK,IAAM,MAAM,CAAC;AAC/C,MAAMK,2BAA2BL,KAAK,IAAM,MAAM,CAAC;AACnD,sEAAsE,GACtE,MAAMM,6BAA6BN,KACjC,IAAM,MAAM,CAAC;AAEf,6EAA6E,GAC7E,MAAMO,2BAA2BP,KAC/B,IAAM,MAAM,CAAC;AAEf,MAAMQ,qBAAqBR,KACzB,IAAM,MAAM,CAAC;AAEf,MAAMS,sBAAsBT,KAC1B,IAAM,MAAM,CAAC;AAEf,MAAMU,sBAAsBV,KAC1B,IAAM,MAAM,CAAC;AAEf,SACEW,yBAAyB,EACzBC,yBAAyB,QACpB,4CAAwC;AAC/C,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,oBAAoB,QAAQ,gCAA4B;AACjE,SAASC,2BAA2B,QAAQ,mCAA+B;AAC3E,SAASC,4BAA4B,QAAQ,oCAAgC;AAC7E,SAASC,qBAAqB,QAAQ,wCAAoC;AAC1E,SAASC,0BAA0B,QAAQ,sCAAkC;AAC7E,SAASC,2BAA2B,QAAQ,uCAAmC;AAC/E,SAASC,0BAA0B,QAAQ,sCAAkC;AAC7E,SAASC,wBAAwB,QAAQ,oCAAgC;AACzE,SAASC,0BAA0B,QAAQ,qCAAiC;AAC5E,SAASC,uBAAuB,QAAQ,qCAAiC;AACzE,SAASC,sBAAsB,QAAQ,qBAAiB;AAExD,kEAAkE,GAClE,MAAMC,sBAAsBzB,KAAK,IAAM,MAAM,CAAC;AAE9C;;;;;;;;;CASC,GACD,OAAO,SAAS0B;IACd,wEAAwE;IACxEV;IACAD;IACA,yEAAyE;IACzE,2DAA2D;IAC3DE;IACA,2EAA2E;IAC3E,wEAAwE;IACxEC;IACA,wEAAwE;IACxE,6EAA6E;IAC7EC;IACA,sEAAsE;IACtEC;IACA,2EAA2E;IAC3EC;IACA,uEAAuE;IACvE,wEAAwE;IACxE,gBAAgB;IAChBC;IACA,kEAAkE;IAClE,uDAAuD;IACvDxB,MAAM6B,yBAAyB,CAACb;IAChC,2EAA2E;IAC3E,kEAAkE;IAClEhB,MAAM8B,0BAA0B,CAACJ;IACjC1B,MAAM+B,wBAAwB,CAAC;QAC7BC,UAAUjB;QACVkB,aAAa;QACbC,eAAeT;QACf,iEAAiE;QACjE,qEAAqE;QACrE,uDAAuD;QACvD,0DAA0D;QAC1D,wEAAwE;QACxE,uEAAuE;QACvE,sEAAsE;QACtE,qDAAqD;QACrDU,YAAY;YACV;gBACEC,IAAI;gBACJC,OAAO;gBACPC,QAAQ;oBAAEC,OAAO;oBAAmBC,WAAW;gBAAc;gBAC7DC,WAAWjC;YACb;YACA,uEAAuE;YACvE,+DAA+D;YAC/D;gBACE4B,IAAI;gBACJC,OAAO;gBACPC,QAAQ;oBAAEC,OAAO;oBAA0BC,WAAW;gBAAc;gBACpEC,WAAWhC;YACb;SACD;QACDiC,aAAa;YACX;gBAAEC,MAAM;gBAAgBJ,OAAO;gBAAoBE,WAAWrC;YAAe;YAC7E,sEAAsE;YACtE,oEAAoE;YACpE;gBAAEuC,MAAM;gBAAcJ,OAAO;gBAAsBE,WAAWpC;YAAa;YAC3E,sEAAsE;YACtE,kEAAkE;YAClE,uEAAuE;YACvE,iDAAiD;YACjD;gBAAEsC,MAAM;gBAAmBJ,OAAO;gBAAiBE,WAAWnC;YAAqB;YACnF;gBAAEqC,MAAM;gBAAwBJ,OAAO;gBAAsBE,WAAWlC;YAAyB;SAClG;QACD,kEAAkE;QAClE,wCAAwC;QACxCqC,SAAS;YACP;gBACEC,MAAM;gBACNC,UAAU;gBACVP,OAAO;gBACPE,WAAW/B;YACb;YACA,uEAAuE;YACvE,2DAA2D;YAC3D,mCAAmC;YACnC;gBACEmC,MAAM7C,MAAM+C,oBAAoB,CAACC,UAAU;gBAC3CF,UAAU;gBACVP,OAAO;gBACPE,WAAW7B;YACb;SACD;QACDqC,UAAU;YACR;gBACEZ,OAAO;gBACPa,MAAM;gBACNC,MAAM;oBAAER,MAAM1C,qBAAqB0C,IAAI;gBAAC;gBACxC,oEAAoE;gBACpE,qEAAqE;gBACrE,6CAA6C;gBAC7CS,UAAUvC;gBACV,qEAAqE;gBACrE,sCAAsC;gBACtCwC,iBAAiBvC;gBACjBwB,QAAQ;oBACNC,OAAO;oBACPY,MAAM;wBAAER,MAAM1C,qBAAqB0C,IAAI;oBAAC;oBACxC,kEAAkE;oBAClE,0DAA0D;oBAC1DH,WAAW;gBACb;gBACAC,WAAWd;YACb;YACA;gBACE,oEAAoE;gBACpE,gEAAgE;gBAChEU,OAAO;gBACPa,MAAM;gBACN;;;;;;;;;;;;;;;;;;;;;;;SAuBC,GACDI,YAAY;gBACZH,MAAM;oBAAER,MAAM1C,qBAAqB0C,IAAI;gBAAC;gBACxCL,QAAQ;oBAAEC,OAAO;oBAAiBC,WAAW;gBAAM;gBACnDC,WAAWtC;YACb;SACD;IACH;IACA;;;;;;;;;;;;;;GAcC,GACDH,MAAM+B,wBAAwB,CAAC;QAC7BC,UAAUhC,MAAMuD,kBAAkB;QAClCtB,aAAa;QACbW,SAAS;YACP;gBACEC,MAAM7C,MAAM+C,oBAAoB,CAACS,aAAa;gBAC9CV,UAAU;gBACVP,OAAO;gBACPE,WAAW9B;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/commerce/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiStorefrontOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nconst PosConsolePage = lazy(() => import('./components/console/pos-page.component'))\n/** The customer display (AGL-3608): loads only on the public page that shows it. */\nconst PosDisplayPage = lazy(() => import('./components/console/pos-display-page.component'))\nconst PosKioskPage = lazy(() => import('./components/console/pos-kiosk-page.component'))\nconst PosHardwareTermsPage = lazy(() => import('./components/console/pos-hardware-terms-page.component'))\nconst PosHardwareTermsSignPage = lazy(() => import('./components/console/pos-hardware-terms-sign-page.component'))\n/** Staff → Hardware orders (AGL-4038): loads only on its staff page. */\nconst PosHardwareOrdersStaffPage = lazy(\n () => import('./components/console/pos-hardware-orders-staff-page.component'),\n)\n/** Staff → Reader start-up screen (AGL-4048): loads only on its staff page. */\nconst PosReaderSplashStaffPage = lazy(\n () => import('./components/console/pos-reader-splash-staff-page.component'),\n)\nconst CommerceGlanceCard = lazy(\n () => import('./components/console/commerce-glance-card.component'),\n)\nconst NewestSiteUsersCard = lazy(\n () => import('./components/console/newest-site-users-card.component'),\n)\nconst SiteMemberPurchases = lazy(\n () => import('./components/console/site-member-purchases.component'),\n)\nimport {\n COMMERCE_CONSOLE_SECTIONS,\n COMMERCE_RETIRED_SECTIONS,\n} from './components/commerce-console-sections'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { COMMERCE_PERMISSIONS } from './model/plugin-permissions'\nimport { registerCommerceRecordLists } from './model/commerce-record-lists'\nimport { registerCommerceRecordRoutes } from './model/commerce-record-routes'\nimport { registerCommerceZones } from './components/console/product-zones'\nimport { registerCommerceOrderZones } from './components/console/order-zones'\nimport { registerCommerceReturnZones } from './components/console/return-zones'\nimport { registerCommerceStoreZones } from './components/console/store-zones'\nimport { registerCommercePosZones } from './components/console/pos-zones'\nimport { registerCommerceTransferUi } from './transfer/register-transfer-ui'\nimport { COMMERCE_SEARCH_SOURCES } from './model/commerce-search-sources'\nimport { COMMERCE_CONFIG_SCHEMA } from './plugin-config'\n\n/** Code-split: the Products console page only loads when opened. */\nconst CommerceConsolePage = lazy(() => import('./components/commerce-console-page'))\n/** \"Start a quote\" on a quote request in the Inbox and on a contact in the CRM (AGL-4098). */\nconst QuoteStartFromRequest = lazy(() => import('./components/console/quote-start.component'))\nconst QuoteStartFromContact = lazy(() =>\n import('./components/console/quote-start.component').then((module) => ({ default: module.QuoteStartFromContact })),\n)\n\n/**\n * Console half (AGL-395): registers the Products nav item in the\n * ConsoleExtension registry so the commerce console surface is owned by the\n * plugin, like events and email. The page body still lives in the console\n * app (`/[hostId]/products`): its product editor depends on the app's media\n * browser, which transitively pulls in the org/session context, so a full\n * body relocation needs a plugin-consumable media-picker context first —\n * tracked as follow-up. The nav item omits a `Component`, so the shell's\n * nav renders the link and the existing named route serves the page.\n */\nexport function registerCommerceConsole(): void {\n // Where a product is read, for surfaces outside this plugin (AGL-3080).\n registerCommerceRecordRoutes()\n registerCommerceRecordLists()\n // The zones its product editor, products hub and CSV import dialog host,\n // with the props each hands a widget (AGL-2916, AGL-3080).\n registerCommerceZones()\n // …and the order dialog's two (AGL-3611), where a shipping plugin's widget\n // buys a label and records the shipment through the dialog's own route.\n registerCommerceOrderZones()\n // …and the return dialog's (AGL-3611), where a shipping plugin's widget\n // buys the buyer a return label and attaches it through this plugin's route.\n registerCommerceReturnZones()\n // The orders list's bulk actions and the store's Settings (AGL-3612).\n registerCommerceStoreZones()\n // The register's orders from other channels — a delivery app's (AGL-3644).\n registerCommercePosZones()\n // Its import and export resources' names and the products wizard's own\n // step (AGL-3531); the server halves register from the console's server\n // declarations.\n registerCommerceTransferUi()\n // Plugin-declared permissions (AGL-435): tier defaults ride every\n // resolved role set; custom roles override key-by-key.\n Aglyn.registerPluginPermissions(COMMERCE_PERMISSIONS)\n // Register discount ceiling (AGL-2161) — the generic settings card renders\n // it from this schema, so there is no bespoke UI to keep in sync.\n Aglyn.registerPluginConfigSchema(COMMERCE_CONFIG_SCHEMA)\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Commerce',\n searchSources: COMMERCE_SEARCH_SOURCES,\n // The screen a register turns toward its customer (AGL-3608), at\n // `/kiosk/commerce/pos-display`. No staff session: the device proves\n // itself with the display token it got by pairing, and\n // `server/pos-display.ts` refuses every read without one.\n // Every workspace's orders from the POS hardware shop (AGL-4038), where\n // staff record Stripe's order id and the tracking, move each order on,\n // and cancel or refund one that has not shipped. The staff area loads\n // this registrar through commerce's `staff` surface.\n staffPages: [\n {\n id: 'hardware-orders',\n label: 'Hardware orders',\n header: { title: 'Hardware Orders', docsTopic: 'posHardware' },\n Component: PosHardwareOrdersStaffPage,\n },\n // The platform's default card reader start-up screen (AGL-4048), which\n // every store that has not picked its own logo or image shows.\n {\n id: 'reader-start-up-screen',\n label: 'Reader start-up screen',\n header: { title: 'Reader Start-up Screen', docsTopic: 'posHardware' },\n Component: PosReaderSplashStaffPage,\n },\n ],\n publicPages: [\n { path: '/pos-display', title: 'Customer display', Component: PosDisplayPage },\n // The self-service kiosk (AGL-3623), paired the same way with a kiosk\n // code; `server/pos-kiosk.ts` refuses every call without its token.\n { path: '/pos-kiosk', title: 'Self-service kiosk', Component: PosKioskPage },\n // The terms of sale for a card reader sold at the platform's register\n // (AGL-4038): on its customer display, by the display's own token\n // (`server/pos-display.ts`), and the printable booth sign, which holds\n // only the published terms' address and summary.\n { path: '/hardware-terms', title: 'Terms of sale', Component: PosHardwareTermsPage },\n { path: '/hardware-terms-sign', title: 'Terms of sale sign', Component: PosHardwareTermsSignPage },\n ],\n // Dashboard/analytics glance card (AGL-419): rendered through the\n // shell's 'commerceGlance' widget slot.\n widgets: [\n {\n slot: 'commerceGlance',\n widgetId: 'commerce-glance',\n title: 'Commerce',\n Component: CommerceGlanceCard,\n },\n // What a site user bought — lifetime total, orders and subscriptions —\n // in their drawer on the site's Users page: the orders and\n // subscriptions are this plugin's.\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.siteMember,\n widgetId: 'commerce-site-member-purchases',\n title: 'Purchases',\n Component: SiteMemberPurchases,\n },\n // Start a quote from a quote request, among an open submission's\n // actions in the Inbox, and from a contact, in its header in the CRM\n // (AGL-4098). Each zone is its host's; neither host imports this plugin.\n {\n slot: 'inboxSubmissionActions',\n widgetId: 'commerce-quote-from-request',\n title: 'Start a quote',\n Component: QuoteStartFromRequest,\n },\n {\n slot: 'crmRecordActions',\n widgetId: 'commerce-quote-from-contact',\n title: 'Start a quote',\n Component: QuoteStartFromContact,\n },\n ],\n navItems: [\n {\n label: 'Products',\n href: '/products',\n icon: { path: mdiStorefrontOutline.path },\n // Sections as ROUTES (AGL-2501): `/products/orders` and friends are\n // real URLs the shell resolves and gates, so the page mounts the one\n // being read instead of subscribing all six.\n sections: COMMERCE_CONSOLE_SECTIONS,\n // `/products/settings` was split into a page per setting (AGL-3713);\n // links to it open the first of them.\n retiredSections: COMMERCE_RETIRED_SECTIONS,\n header: {\n title: 'Products',\n icon: { path: mdiStorefrontOutline.path },\n // The overview, not `catalog`: this surface is the whole commerce\n // console — Catalog, Orders, Promotions and Reservations.\n docsTopic: 'commerce',\n },\n Component: CommerceConsolePage,\n },\n {\n // POS register (AGL-312/419): relocated from the app's /pos route —\n // the generic [pluginSlug] shell now serves it at the same URL.\n label: 'POS',\n href: '/pos',\n /*\n * WHO may open the register, declared on THIS nav item rather than on\n * the extension.\n *\n * A key here narrows one surface; a key on the extension would gate\n * every surface it registers, and Products is the sibling — a catalog\n * anyone who works on the site may open. The register is the one that\n * takes money and lists checked-in guests by name, so the two\n * genuinely have different answers and only this one is narrowed.\n *\n * `managePos` rather than a dotted catalog key because it is the key\n * the sale itself is refused by: `server/pos-order.ts` resolves the\n * member's permission map and returns 403 unless `managePos` holds.\n * The shell answers this declaration out of that same resolved map, so\n * the console offers a register exactly to the people the route will\n * accept a sale from — the gate and its server twin cannot disagree,\n * because they are reading one value.\n *\n * `pos-order.ts` stays the enforcement point regardless. This decides\n * whether the surface is built at all, which is the part a check\n * inside the page cannot do: by the time the page can refuse itself it\n * has mounted and its product, register, location and reservation\n * listeners are already open.\n */\n permission: 'managePos',\n icon: { path: mdiStorefrontOutline.path },\n header: { title: 'Point of Sale', docsTopic: 'pos' },\n Component: PosConsolePage,\n },\n ],\n })\n /*\n * The USER ACCOUNTS half, registered under its own id.\n *\n * `accounts` is a switch rather than a bundle: the member blocks and every\n * `membership/*` handler ship inside this package, which is why the id\n * carries no loader manifest entry of its own. A second extension is how a\n * card about visitor accounts is gated on the switch that decides whether\n * the site serves them — `listConsoleWidgets` filters by the workspace's\n * effective plugin ids, and `accounts` is default-OFF per site.\n *\n * A separate call rather than more widgets on the commerce extension: the\n * registry is keyed by `pluginId` and that key IS the gate, so folding this\n * card in above would have kept the dashboard defect it fixes — `Newest\n * site users` on a site whose /signin returns 404.\n */\n Aglyn.registerConsoleExtension({\n pluginId: Aglyn.ACCOUNTS_PLUGIN_ID,\n displayName: 'User Accounts',\n widgets: [\n {\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.hostDashboard,\n widgetId: 'accounts-newest-site-users',\n title: 'Newest site users',\n Component: NewestSiteUsersCard,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiStorefrontOutline","lazy","PosConsolePage","PosDisplayPage","PosKioskPage","PosHardwareTermsPage","PosHardwareTermsSignPage","PosHardwareOrdersStaffPage","PosReaderSplashStaffPage","CommerceGlanceCard","NewestSiteUsersCard","SiteMemberPurchases","COMMERCE_CONSOLE_SECTIONS","COMMERCE_RETIRED_SECTIONS","BUNDLE_ID","COMMERCE_PERMISSIONS","registerCommerceRecordLists","registerCommerceRecordRoutes","registerCommerceZones","registerCommerceOrderZones","registerCommerceReturnZones","registerCommerceStoreZones","registerCommercePosZones","registerCommerceTransferUi","COMMERCE_SEARCH_SOURCES","COMMERCE_CONFIG_SCHEMA","CommerceConsolePage","QuoteStartFromRequest","QuoteStartFromContact","then","module","default","registerCommerceConsole","registerPluginPermissions","registerPluginConfigSchema","registerConsoleExtension","pluginId","displayName","searchSources","staffPages","id","label","header","title","docsTopic","Component","publicPages","path","widgets","slot","widgetId","CONSOLE_WIDGET_SLOTS","siteMember","navItems","href","icon","sections","retiredSections","permission","ACCOUNTS_PLUGIN_ID","hostDashboard"],"mappings":"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"}