@endora-commerce/mod-price-lists 0.100.0

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 (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/components/ApplicationRuleBuilder.d.ts +24 -0
  4. package/dist/admin/components/ApplicationRuleBuilder.d.ts.map +1 -0
  5. package/dist/admin/components/ApplicationRuleBuilder.js +249 -0
  6. package/dist/admin/components/ApplicationRuleBuilder.js.map +1 -0
  7. package/dist/admin/components/BracketGrid.d.ts +29 -0
  8. package/dist/admin/components/BracketGrid.d.ts.map +1 -0
  9. package/dist/admin/components/BracketGrid.js +252 -0
  10. package/dist/admin/components/BracketGrid.js.map +1 -0
  11. package/dist/admin/components/DisplayModeOverrideRow.d.ts +46 -0
  12. package/dist/admin/components/DisplayModeOverrideRow.d.ts.map +1 -0
  13. package/dist/admin/components/DisplayModeOverrideRow.js +109 -0
  14. package/dist/admin/components/DisplayModeOverrideRow.js.map +1 -0
  15. package/dist/admin/components/LinkedPriceListsPanel.d.ts +55 -0
  16. package/dist/admin/components/LinkedPriceListsPanel.d.ts.map +1 -0
  17. package/dist/admin/components/LinkedPriceListsPanel.js +131 -0
  18. package/dist/admin/components/LinkedPriceListsPanel.js.map +1 -0
  19. package/dist/admin/index.d.ts +34 -0
  20. package/dist/admin/index.d.ts.map +1 -0
  21. package/dist/admin/index.js +119 -0
  22. package/dist/admin/index.js.map +1 -0
  23. package/dist/admin/pages/DisplayModeOverridesPage.d.ts +25 -0
  24. package/dist/admin/pages/DisplayModeOverridesPage.d.ts.map +1 -0
  25. package/dist/admin/pages/DisplayModeOverridesPage.js +266 -0
  26. package/dist/admin/pages/DisplayModeOverridesPage.js.map +1 -0
  27. package/dist/admin/pages/PriceListDetailPage.d.ts +23 -0
  28. package/dist/admin/pages/PriceListDetailPage.d.ts.map +1 -0
  29. package/dist/admin/pages/PriceListDetailPage.js +421 -0
  30. package/dist/admin/pages/PriceListDetailPage.js.map +1 -0
  31. package/dist/admin/pages/PriceListsPage.d.ts +17 -0
  32. package/dist/admin/pages/PriceListsPage.d.ts.map +1 -0
  33. package/dist/admin/pages/PriceListsPage.js +198 -0
  34. package/dist/admin/pages/PriceListsPage.js.map +1 -0
  35. package/dist/admin/zones/CategoryDisplayMode.d.ts +27 -0
  36. package/dist/admin/zones/CategoryDisplayMode.d.ts.map +1 -0
  37. package/dist/admin/zones/CategoryDisplayMode.js +11 -0
  38. package/dist/admin/zones/CategoryDisplayMode.js.map +1 -0
  39. package/dist/admin/zones/OrganizationDisplayMode.d.ts +36 -0
  40. package/dist/admin/zones/OrganizationDisplayMode.d.ts.map +1 -0
  41. package/dist/admin/zones/OrganizationDisplayMode.js +12 -0
  42. package/dist/admin/zones/OrganizationDisplayMode.js.map +1 -0
  43. package/dist/admin/zones/ProductLinkedPriceLists.d.ts +16 -0
  44. package/dist/admin/zones/ProductLinkedPriceLists.d.ts.map +1 -0
  45. package/dist/admin/zones/ProductLinkedPriceLists.js +7 -0
  46. package/dist/admin/zones/ProductLinkedPriceLists.js.map +1 -0
  47. package/dist/backend/entities/price-display-mode-override.entity.d.ts +23 -0
  48. package/dist/backend/entities/price-display-mode-override.entity.d.ts.map +1 -0
  49. package/dist/backend/entities/price-display-mode-override.entity.js +58 -0
  50. package/dist/backend/entities/price-display-mode-override.entity.js.map +1 -0
  51. package/dist/backend/entities/price-list-price-bracket.entity.d.ts +26 -0
  52. package/dist/backend/entities/price-list-price-bracket.entity.d.ts.map +1 -0
  53. package/dist/backend/entities/price-list-price-bracket.entity.js +73 -0
  54. package/dist/backend/entities/price-list-price-bracket.entity.js.map +1 -0
  55. package/dist/backend/entities/price-list-product.entity.d.ts +15 -0
  56. package/dist/backend/entities/price-list-product.entity.d.ts.map +1 -0
  57. package/dist/backend/entities/price-list-product.entity.js +43 -0
  58. package/dist/backend/entities/price-list-product.entity.js.map +1 -0
  59. package/dist/backend/entities/price-list.entity.d.ts +34 -0
  60. package/dist/backend/entities/price-list.entity.d.ts.map +1 -0
  61. package/dist/backend/entities/price-list.entity.js +115 -0
  62. package/dist/backend/entities/price-list.entity.js.map +1 -0
  63. package/dist/backend/index.d.ts +125 -0
  64. package/dist/backend/index.d.ts.map +1 -0
  65. package/dist/backend/index.js +179 -0
  66. package/dist/backend/index.js.map +1 -0
  67. package/dist/backend/plugin.d.ts +68 -0
  68. package/dist/backend/plugin.d.ts.map +1 -0
  69. package/dist/backend/plugin.js +88 -0
  70. package/dist/backend/plugin.js.map +1 -0
  71. package/dist/backend/routes.d.ts +40 -0
  72. package/dist/backend/routes.d.ts.map +1 -0
  73. package/dist/backend/routes.js +447 -0
  74. package/dist/backend/routes.js.map +1 -0
  75. package/dist/backend/routes.storefront.d.ts +47 -0
  76. package/dist/backend/routes.storefront.d.ts.map +1 -0
  77. package/dist/backend/routes.storefront.js +131 -0
  78. package/dist/backend/routes.storefront.js.map +1 -0
  79. package/dist/backend/services/application-rule-evaluator.d.ts +25 -0
  80. package/dist/backend/services/application-rule-evaluator.d.ts.map +1 -0
  81. package/dist/backend/services/application-rule-evaluator.js +2 -0
  82. package/dist/backend/services/application-rule-evaluator.js.map +1 -0
  83. package/dist/backend/services/audit-references.d.ts +11 -0
  84. package/dist/backend/services/audit-references.d.ts.map +1 -0
  85. package/dist/backend/services/audit-references.js +23 -0
  86. package/dist/backend/services/audit-references.js.map +1 -0
  87. package/dist/backend/services/default-price-list-migration.d.ts +70 -0
  88. package/dist/backend/services/default-price-list-migration.d.ts.map +1 -0
  89. package/dist/backend/services/default-price-list-migration.js +177 -0
  90. package/dist/backend/services/default-price-list-migration.js.map +1 -0
  91. package/dist/backend/services/display-mode-resolver.d.ts +78 -0
  92. package/dist/backend/services/display-mode-resolver.d.ts.map +1 -0
  93. package/dist/backend/services/display-mode-resolver.js +71 -0
  94. package/dist/backend/services/display-mode-resolver.js.map +1 -0
  95. package/dist/backend/services/listing-price-chain.d.ts +26 -0
  96. package/dist/backend/services/listing-price-chain.d.ts.map +1 -0
  97. package/dist/backend/services/listing-price-chain.js +55 -0
  98. package/dist/backend/services/listing-price-chain.js.map +1 -0
  99. package/dist/backend/services/price-bracket-resolver.d.ts +24 -0
  100. package/dist/backend/services/price-bracket-resolver.d.ts.map +1 -0
  101. package/dist/backend/services/price-bracket-resolver.js +16 -0
  102. package/dist/backend/services/price-bracket-resolver.js.map +1 -0
  103. package/dist/backend/services/price-list-candidate-vector.d.ts +116 -0
  104. package/dist/backend/services/price-list-candidate-vector.d.ts.map +1 -0
  105. package/dist/backend/services/price-list-candidate-vector.js +133 -0
  106. package/dist/backend/services/price-list-candidate-vector.js.map +1 -0
  107. package/dist/backend/services/price-list-currency-reference.d.ts +12 -0
  108. package/dist/backend/services/price-list-currency-reference.d.ts.map +1 -0
  109. package/dist/backend/services/price-list-currency-reference.js +24 -0
  110. package/dist/backend/services/price-list-currency-reference.js.map +1 -0
  111. package/dist/backend/services/price-list-read-port.d.ts +29 -0
  112. package/dist/backend/services/price-list-read-port.d.ts.map +1 -0
  113. package/dist/backend/services/price-list-read-port.js +64 -0
  114. package/dist/backend/services/price-list-read-port.js.map +1 -0
  115. package/dist/backend/services/price-list-resolver.d.ts +26 -0
  116. package/dist/backend/services/price-list-resolver.d.ts.map +1 -0
  117. package/dist/backend/services/price-list-resolver.js +37 -0
  118. package/dist/backend/services/price-list-resolver.js.map +1 -0
  119. package/dist/backend/services/price-list-service.d.ts +431 -0
  120. package/dist/backend/services/price-list-service.d.ts.map +1 -0
  121. package/dist/backend/services/price-list-service.js +1238 -0
  122. package/dist/backend/services/price-list-service.js.map +1 -0
  123. package/dist/backend/services/price-list-status-worker.d.ts +28 -0
  124. package/dist/backend/services/price-list-status-worker.d.ts.map +1 -0
  125. package/dist/backend/services/price-list-status-worker.js +38 -0
  126. package/dist/backend/services/price-list-status-worker.js.map +1 -0
  127. package/dist/backend/services/pricing-cache.d.ts +53 -0
  128. package/dist/backend/services/pricing-cache.d.ts.map +1 -0
  129. package/dist/backend/services/pricing-cache.js +88 -0
  130. package/dist/backend/services/pricing-cache.js.map +1 -0
  131. package/dist/backend/services/pricing-service.d.ts +304 -0
  132. package/dist/backend/services/pricing-service.d.ts.map +1 -0
  133. package/dist/backend/services/pricing-service.interface.d.ts +151 -0
  134. package/dist/backend/services/pricing-service.interface.d.ts.map +1 -0
  135. package/dist/backend/services/pricing-service.interface.js +10 -0
  136. package/dist/backend/services/pricing-service.interface.js.map +1 -0
  137. package/dist/backend/services/pricing-service.js +803 -0
  138. package/dist/backend/services/pricing-service.js.map +1 -0
  139. package/dist/backend/services/unit-price-ordering.d.ts +121 -0
  140. package/dist/backend/services/unit-price-ordering.d.ts.map +1 -0
  141. package/dist/backend/services/unit-price-ordering.js +164 -0
  142. package/dist/backend/services/unit-price-ordering.js.map +1 -0
  143. package/dist/manifest.d.ts +218 -0
  144. package/dist/manifest.d.ts.map +1 -0
  145. package/dist/manifest.js +228 -0
  146. package/dist/manifest.js.map +1 -0
  147. package/dist/migrations/20260426T075235_price_lists_pricing_init.d.ts +24 -0
  148. package/dist/migrations/20260426T075235_price_lists_pricing_init.d.ts.map +1 -0
  149. package/dist/migrations/20260426T075235_price_lists_pricing_init.js +89 -0
  150. package/dist/migrations/20260426T075235_price_lists_pricing_init.js.map +1 -0
  151. package/dist/migrations/20260504T125655_price_lists_engine.d.ts +6 -0
  152. package/dist/migrations/20260504T125655_price_lists_engine.d.ts.map +1 -0
  153. package/dist/migrations/20260504T125655_price_lists_engine.js +319 -0
  154. package/dist/migrations/20260504T125655_price_lists_engine.js.map +1 -0
  155. package/dist/migrations/20260817T055457_price_lists_single_system_price_list.d.ts +15 -0
  156. package/dist/migrations/20260817T055457_price_lists_single_system_price_list.d.ts.map +1 -0
  157. package/dist/migrations/20260817T055457_price_lists_single_system_price_list.js +58 -0
  158. package/dist/migrations/20260817T055457_price_lists_single_system_price_list.js.map +1 -0
  159. package/dist/migrations/20260821T135907_price_lists_unit_price_amount_index.d.ts +40 -0
  160. package/dist/migrations/20260821T135907_price_lists_unit_price_amount_index.d.ts.map +1 -0
  161. package/dist/migrations/20260821T135907_price_lists_unit_price_amount_index.js +46 -0
  162. package/dist/migrations/20260821T135907_price_lists_unit_price_amount_index.js.map +1 -0
  163. package/dist/migrations/index.d.ts +30 -0
  164. package/dist/migrations/index.d.ts.map +1 -0
  165. package/dist/migrations/index.js +35 -0
  166. package/dist/migrations/index.js.map +1 -0
  167. package/docs/price_lists.md +278 -0
  168. package/i18n/en.json +14 -0
  169. package/i18n/pl.json +14 -0
  170. package/package.json +99 -0
  171. package/tailwind.css +14 -0
@@ -0,0 +1,803 @@
1
+ import { PriceList } from '../entities/price-list.entity.js';
2
+ import { PriceListPriceBracket } from '../entities/price-list-price-bracket.entity.js';
3
+ import { evaluateApplicationRule, } from './application-rule-evaluator.js';
4
+ import { pickPriorityChain, } from './price-list-resolver.js';
5
+ import { resolvePriceBracket, } from './price-bracket-resolver.js';
6
+ import { listingPriceFrom } from './listing-price-chain.js';
7
+ import { buildCandidateVector, } from './price-list-candidate-vector.js';
8
+ import { buildUnitPriceMergeQuery, compareOrderRows, uuidArrayLiteral, } from './unit-price-ordering.js';
9
+ /**
10
+ * The engine's answer, as a charged line (FR-034): Sale when present, Base
11
+ * otherwise, `null` when nothing on any matching list prices the product.
12
+ *
13
+ * Extracted from `resolveLinePrice` so the batched listing path applies exactly
14
+ * the same rule to the engine results it assembles — the sale-wins step is one
15
+ * of the places a second implementation could have quietly disagreed.
16
+ */
17
+ export function lineFromEngine(out) {
18
+ if (out.sale) {
19
+ return {
20
+ amount: out.sale.bracket.amount,
21
+ currency: out.currencyCode,
22
+ priceListId: out.sale.listId,
23
+ isSale: true,
24
+ bracketStartQuantity: out.sale.bracket.minQuantity,
25
+ displayMode: out.displayMode,
26
+ };
27
+ }
28
+ if (!out.base.bracket)
29
+ return null;
30
+ return {
31
+ amount: out.base.bracket.amount,
32
+ currency: out.currencyCode,
33
+ priceListId: out.base.listId,
34
+ isSale: false,
35
+ bracketStartQuantity: out.base.bracket.minQuantity,
36
+ displayMode: out.displayMode,
37
+ };
38
+ }
39
+ export class PricingService {
40
+ emFactory;
41
+ cache;
42
+ resolveOrgChain;
43
+ targetReads;
44
+ constructor(emFactory, cache,
45
+ /**
46
+ * Feature 056 — resolves the acting org's inheritance chain (nearest-first,
47
+ * `[orgId, ...ancestorIds]`) so a descendant inherits an ancestor's org-named
48
+ * price list. When absent, the chain is `[orgId]` (flat behavior, byte-for-byte).
49
+ */
50
+ resolveOrgChain,
51
+ /**
52
+ * Feature 075 Phase C — forwarded verbatim to the `PriceListService` this
53
+ * engine builds for the display-mode chain, whose category step reaches
54
+ * `catalog` over its read port. Absent, that step refuses rather than
55
+ * treating every category as unknown.
56
+ */
57
+ targetReads) {
58
+ this.emFactory = emFactory;
59
+ this.cache = cache;
60
+ this.resolveOrgChain = resolveOrgChain;
61
+ this.targetReads = targetReads;
62
+ }
63
+ /**
64
+ * `catalog`'s category read port, or a refusal — the same refusal
65
+ * `PriceListService.targets()` makes, for the same reason.
66
+ *
67
+ * Every category question this engine asks is asked here: which categories a
68
+ * product is in, and which products a category holds. Skipping one because
69
+ * nothing was wired would turn "this composition cannot reach `catalog`" into
70
+ * an answer — "this product is in no category", "this category rule applies
71
+ * to everything" — and both of those price a line the rule does not cover.
72
+ */
73
+ #categoryRead() {
74
+ const categoryRead = this.targetReads?.catalogCategoryRead;
75
+ if (!categoryRead) {
76
+ throw new Error('PricingService: this resolution asks `catalog` about category membership and this ' +
77
+ "composition constructed the engine without `catalog`'s category read port.");
78
+ }
79
+ return categoryRead;
80
+ }
81
+ /**
82
+ * `(product, category)` memberships for a set of products, read through
83
+ * `catalog`'s port (feature 075 / D-87) — the shape both resolution paths
84
+ * take, one call for the whole set.
85
+ *
86
+ * **Structural, deliberately** — no `activeOnly`. `evaluateApplicationRule`'s
87
+ * `category` criterion tests the raw membership, so narrowing to live
88
+ * categories here would price a product against a rule its memberships no
89
+ * longer satisfy the moment an operator deactivates a category.
90
+ *
91
+ * Every product asked about gets an entry — an uncategorised one answers an
92
+ * empty set rather than a missing key, which is what `resolveEngine`'s
93
+ * single-product read means by "no rows".
94
+ */
95
+ async #categoryMemberships(productIds) {
96
+ const out = new Map();
97
+ for (const id of productIds)
98
+ out.set(id, new Set());
99
+ if (productIds.length === 0)
100
+ return out;
101
+ for (const assignment of await this.#categoryRead().listAssignmentsForProducts(productIds)) {
102
+ out.get(assignment.productId)?.add(assignment.categoryId);
103
+ }
104
+ return out;
105
+ }
106
+ // ---- Engine resolver (US5 / FR-026..FR-032) ------------------------
107
+ /**
108
+ * New resolver pipeline composing the three pure helpers:
109
+ * - evaluateApplicationRule (US4)
110
+ * - pickPriorityChain (US5)
111
+ * - resolvePriceBracket (US3)
112
+ *
113
+ * Returns Base + (optional) Sale resolutions on the given context.
114
+ *
115
+ * Bracket-gap fall-through (FR-031): when the picked list has no
116
+ * bracket for the requested (product, currency, qty), the resolver
117
+ * advances to the next-priority list in the same partition and
118
+ * repeats. Default is the terminal fallback for the Base partition;
119
+ * the Sale partition simply yields null when nothing matches with a
120
+ * usable bracket.
121
+ *
122
+ * Display mode resolution is stubbed at this layer until US7 wires
123
+ * the override chain. The default mode `gross_only` is returned so
124
+ * the response shape is complete.
125
+ */
126
+ async resolveEngine(input) {
127
+ const em = this.emFactory();
128
+ const { product, context } = input;
129
+ const currencyCode = (context.currencyCode ?? context.salesChannel.defaultCurrency).toUpperCase();
130
+ const cacheKey = {
131
+ productId: product.id,
132
+ variantId: input.variantId ?? null,
133
+ quantity: context.quantity,
134
+ currencyCode,
135
+ salesChannelId: context.salesChannel.id,
136
+ organizationId: context.organization?.id ?? null,
137
+ customerGroupId: context.customerGroupId ?? context.organization?.customerGroupId ?? null,
138
+ };
139
+ const cached = this.cache?.get(cacheKey);
140
+ if (cached)
141
+ return cached;
142
+ // Build the resolution context.
143
+ const productCategoryIds = (await this.#categoryMemberships([product.id])).get(product.id) ?? new Set();
144
+ const orgId = context.organization?.id ?? null;
145
+ // Feature 056 — build the org inheritance chain (nearest-first). Flat when
146
+ // no resolver is wired or the org is a root.
147
+ const organizationChain = orgId !== null && this.resolveOrgChain ? await this.resolveOrgChain(orgId) : orgId !== null ? [orgId] : [];
148
+ const ctx = {
149
+ organizationId: orgId,
150
+ organizationChain,
151
+ customerGroupId: context.customerGroupId ?? context.organization?.customerGroupId ?? null,
152
+ salesChannelId: context.salesChannel.id,
153
+ currencyCode,
154
+ productCategoryIds,
155
+ };
156
+ // Load every active list and evaluate.
157
+ const activeLists = await em.find(PriceList, { status: 'active' });
158
+ const baseMatches = [];
159
+ const saleMatches = [];
160
+ for (const list of activeLists) {
161
+ const evaluation = evaluateApplicationRule(list.applicationRule, ctx);
162
+ if (!evaluation.matched)
163
+ continue;
164
+ const candidate = {
165
+ list,
166
+ evaluation,
167
+ modifiedAt: list.modifiedAt,
168
+ name: list.name,
169
+ isSystem: list.isSystem,
170
+ };
171
+ if (list.type === 'sale')
172
+ saleMatches.push({ list, candidate });
173
+ else
174
+ baseMatches.push({ list, candidate });
175
+ }
176
+ // For each partition: walk the priority chain, do bracket lookup, and
177
+ // fall through if the picked list has no usable bracket (FR-031).
178
+ const baseResult = await this.pickAndResolveBracket(em, baseMatches, product.id, currencyCode, context.quantity);
179
+ const saleResult = await this.pickAndResolveBracket(em, saleMatches, product.id, currencyCode, context.quantity);
180
+ // Display mode (US7): walk the override chain via the price-list service
181
+ // helper. Lazy-construct the helper here so PricingService does not pull
182
+ // PriceListService into its constructor signature (keeps the existing
183
+ // composition.ts wiring intact).
184
+ const { PriceListService } = await import('./price-list-service.js');
185
+ const priceListService = new PriceListService(this.emFactory, undefined, undefined, undefined, this.targetReads);
186
+ const displayMode = await priceListService.resolveDisplayMode({
187
+ productId: product.id,
188
+ organizationId: context.organization?.id ?? null,
189
+ salesChannelId: context.salesChannel.id,
190
+ customerKind: context.organization ? 'signed_in' : 'guest',
191
+ });
192
+ const result = {
193
+ base: {
194
+ listId: baseResult.list?.id ?? '',
195
+ listName: baseResult.list?.name ?? '',
196
+ bracket: baseResult.bracket,
197
+ },
198
+ sale: saleResult.list && saleResult.bracket
199
+ ? {
200
+ listId: saleResult.list.id,
201
+ listName: saleResult.list.name,
202
+ bracket: saleResult.bracket,
203
+ }
204
+ : null,
205
+ displayMode,
206
+ currencyCode,
207
+ };
208
+ this.cache?.set(cacheKey, result);
209
+ return result;
210
+ }
211
+ /**
212
+ * Convenience wrapper for cart/checkout/order callers (US6 / FR-034).
213
+ *
214
+ * Returns the line's effective unit price as a `{ amount, currency }`
215
+ * pair plus the source price-list ID and a flag indicating whether the
216
+ * Sale partition won. Callers persist the amount on the cart line and
217
+ * the source list ID for audit; if the response is `null`, the product
218
+ * has no bracket on any matching list and the caller should refuse the
219
+ * line (or, in the storefront's case, route to the Quote Request flow).
220
+ *
221
+ * Always charges the Sale price when present (FR-034). Falls back to
222
+ * the Base price; both come from `resolveEngine`.
223
+ */
224
+ async resolveLinePrice(input) {
225
+ return lineFromEngine(await this.resolveEngine(input));
226
+ }
227
+ /**
228
+ * Issue #132 — the price a catalogue listing may render, per product.
229
+ *
230
+ * The chain is `listingPriceFrom`; this method feeds it, so the listing paths
231
+ * make one call and get an answer whose type states which step of the chain
232
+ * produced it. Every requested product gets an entry.
233
+ *
234
+ * ## Why this is a batch and not a loop
235
+ *
236
+ * It used to be a loop over `resolveLinePrice`, which is **7 queries per
237
+ * product, sequentially**: a 50-item catalogue page — the platform's default
238
+ * page size and its busiest storefront read — cost 350 round trips and 315 ms
239
+ * of resolution on a warm local Postgres. Six of those seven asked a question
240
+ * whose answer does not vary across the page (the active price lists, the
241
+ * settings pair, the organization's override) or asked it one id at a time
242
+ * (the category memberships, the brackets, the product-scope overrides).
243
+ *
244
+ * So the set is resolved once and applied per product: the memberships in one
245
+ * `in (…)`, the active lists once, the organization's inheritance chain once,
246
+ * the brackets in **priority waves** (one query per fall-through level the
247
+ * page actually needs, not per product), and the display-mode chain through
248
+ * `resolveDisplayModes`, reusing the memberships already read.
249
+ *
250
+ * ## What is *not* batched, and why
251
+ *
252
+ * The application-rule evaluation and the priority walk stay per product, and
253
+ * they have to: `evaluateApplicationRule` reads the product's own category
254
+ * memberships, so a category rule matches some cards on a page and not
255
+ * others, and the winning list therefore differs per product. Both are pure
256
+ * in-memory functions over rows already loaded — the same
257
+ * `evaluateApplicationRule` / `pickPriorityChain` / `resolvePriceBracket`
258
+ * helpers `resolveEngine` composes — so the per-product part costs no I/O.
259
+ *
260
+ * The bracket wave is what keeps that honest. A single `in (…)` over every
261
+ * matched list would fetch brackets for lists most cards never reach; asking
262
+ * one wave at a time fetches exactly the (list, product) pairs the per-product
263
+ * walk would have asked for, in one query per level instead of one per card.
264
+ *
265
+ * ## Identity with the per-product path
266
+ *
267
+ * `resolveEngine` and `resolveLinePrice` are untouched: cart, checkout and the
268
+ * admin resolved-price probe take the same path they always did, and
269
+ * `test/integration/price_lists/listing-prices-batch.test.ts` asserts, product
270
+ * by product over a fixture with a bracket gap, a sale partition, a category
271
+ * rule and both unpriced arms, that this method answers exactly what looping
272
+ * that path answers. The LRU is keyed and populated identically, so a page
273
+ * whose products are already cached still issues no query at all.
274
+ */
275
+ async resolveListingPrices(input) {
276
+ const currencyCode = (input.context.currencyCode ?? input.context.salesChannel.defaultCurrency).toUpperCase();
277
+ const out = new Map();
278
+ if (input.products.length === 0)
279
+ return out;
280
+ const organization = input.context.organization ?? null;
281
+ const customerGroupId = input.context.customerGroupId ?? organization?.customerGroupId ?? null;
282
+ const salesChannel = input.context.salesChannel;
283
+ const cacheKeyFor = (productId) => ({
284
+ productId,
285
+ variantId: null,
286
+ quantity: 1,
287
+ currencyCode,
288
+ salesChannelId: salesChannel.id,
289
+ organizationId: organization?.id ?? null,
290
+ customerGroupId,
291
+ });
292
+ // A page may name the same product twice; the answer is the same, and the
293
+ // loop it replaces resolved it twice.
294
+ const requested = new Map();
295
+ for (const product of input.products) {
296
+ if (!requested.has(product.id))
297
+ requested.set(product.id, product);
298
+ }
299
+ const pending = [];
300
+ for (const product of requested.values()) {
301
+ const cached = this.cache?.get(cacheKeyFor(product.id));
302
+ if (cached)
303
+ out.set(product.id, listingPriceFrom(lineFromEngine(cached), product, currencyCode));
304
+ else
305
+ pending.push(product);
306
+ }
307
+ if (pending.length === 0)
308
+ return out;
309
+ const em = this.emFactory();
310
+ const productIds = pending.map((p) => p.id);
311
+ const categoryIdsByProduct = await this.#categoryMemberships(productIds);
312
+ const orgId = organization?.id ?? null;
313
+ // Feature 056 — one chain for the page: it depends on the acting org, which
314
+ // the listing context fixes for every product on it.
315
+ const organizationChain = orgId !== null && this.resolveOrgChain
316
+ ? await this.resolveOrgChain(orgId)
317
+ : orgId !== null
318
+ ? [orgId]
319
+ : [];
320
+ const activeLists = await em.find(PriceList, { status: 'active' });
321
+ const baseMatches = new Map();
322
+ const saleMatches = new Map();
323
+ for (const product of pending) {
324
+ const ctx = {
325
+ organizationId: orgId,
326
+ organizationChain,
327
+ customerGroupId,
328
+ salesChannelId: salesChannel.id,
329
+ currencyCode,
330
+ productCategoryIds: categoryIdsByProduct.get(product.id) ?? new Set(),
331
+ };
332
+ const base = [];
333
+ const sale = [];
334
+ for (const list of activeLists) {
335
+ const evaluation = evaluateApplicationRule(list.applicationRule, ctx);
336
+ if (!evaluation.matched)
337
+ continue;
338
+ const candidate = {
339
+ list,
340
+ evaluation,
341
+ modifiedAt: list.modifiedAt,
342
+ name: list.name,
343
+ isSystem: list.isSystem,
344
+ };
345
+ if (list.type === 'sale')
346
+ sale.push({ list, candidate });
347
+ else
348
+ base.push({ list, candidate });
349
+ }
350
+ baseMatches.set(product.id, base);
351
+ saleMatches.set(product.id, sale);
352
+ }
353
+ const baseResults = await this.pickAndResolveBracketsForSet(em, baseMatches, currencyCode, 1);
354
+ const saleResults = await this.pickAndResolveBracketsForSet(em, saleMatches, currencyCode, 1);
355
+ // Same lazy construction as `resolveEngine`, for the same reason: it keeps
356
+ // `PriceListService` out of this class's constructor signature.
357
+ const { PriceListService } = await import('./price-list-service.js');
358
+ const priceListService = new PriceListService(this.emFactory, undefined, undefined, undefined, this.targetReads);
359
+ const displayModes = await priceListService.resolveDisplayModes({
360
+ productIds,
361
+ organizationId: orgId,
362
+ salesChannelId: salesChannel.id,
363
+ customerKind: organization ? 'signed_in' : 'guest',
364
+ categoryIdsByProduct,
365
+ });
366
+ for (const product of pending) {
367
+ const base = baseResults.get(product.id) ?? { list: null, bracket: null };
368
+ const sale = saleResults.get(product.id) ?? { list: null, bracket: null };
369
+ const engine = {
370
+ base: {
371
+ listId: base.list?.id ?? '',
372
+ listName: base.list?.name ?? '',
373
+ bracket: base.bracket,
374
+ },
375
+ sale: sale.list && sale.bracket
376
+ ? { listId: sale.list.id, listName: sale.list.name, bracket: sale.bracket }
377
+ : null,
378
+ displayMode: displayModes.get(product.id) ?? 'gross_only',
379
+ currencyCode,
380
+ };
381
+ this.cache?.set(cacheKeyFor(product.id), engine);
382
+ out.set(product.id, listingPriceFrom(lineFromEngine(engine), product, currencyCode));
383
+ }
384
+ return out;
385
+ }
386
+ // ---- Feature 086 — the listing ordering slice ----------------------------
387
+ /**
388
+ * The catalogue in resolved-unit-price order, for one viewer, one chunk at a
389
+ * time — {@link ListingPriceOrderPort}.
390
+ *
391
+ * ## Identity with the card, which is the whole point
392
+ *
393
+ * The rows this answers are the rows `resolveListingPrices` would answer for
394
+ * the same viewer, sorted. Not "approximately": the candidates come from
395
+ * {@link buildCandidateVector}, which produces its order by running
396
+ * `pickPriorityChain` to exhaustion, and the sale partition leads the base one
397
+ * because `lineFromEngine` charges the sale price whenever the sale partition
398
+ * resolves anything. `test/integration/price_lists/unit-price-ordering.test.ts`
399
+ * asserts it product by product against the batch, the way
400
+ * `listing-prices-batch.test.ts` asserts the batch against the per-product
401
+ * path.
402
+ *
403
+ * ## What it does not know
404
+ *
405
+ * Visibility, channel membership, product status, archival, categories the
406
+ * *buyer* filtered by. Those are `catalog`'s facts about `catalog`'s rows, and
407
+ * a chunk from here may name products the viewer must never see — the caller
408
+ * intersects before anything reaches a page, a cursor or a count.
409
+ *
410
+ * ## Two statements, whatever the page size
411
+ *
412
+ * One probe of the leading stream, which fixes the watermark, and one merge of
413
+ * the rest. A single-candidate viewer costs one. The leading stream's rows are
414
+ * reused rather than re-read.
415
+ */
416
+ async orderByUnitPrice(input) {
417
+ const empty = { rows: [], exhausted: true, sourceRowsRead: 0 };
418
+ if (input.limit <= 0)
419
+ return { rows: [], exhausted: false, sourceRowsRead: 0 };
420
+ // An empty restriction means "nothing", which is a different answer from
421
+ // `undefined` meaning "the whole catalogue".
422
+ if (input.restrictToProductIds !== undefined && input.restrictToProductIds.length === 0) {
423
+ return empty;
424
+ }
425
+ const em = this.emFactory();
426
+ const currencyCode = this.#listingCurrency(input.context);
427
+ const streams = await this.#candidateStreams(em, input.context, currencyCode, input.restrictToProductIds);
428
+ if (streams.length === 0)
429
+ return empty;
430
+ const limit = input.limit;
431
+ const direction = input.direction;
432
+ const after = input.after;
433
+ const range = input.amountRange;
434
+ // Pass 1 — the leading stream alone. It carries no exclusion (nothing
435
+ // outranks it), so its window is exactly the cheapest `limit` rows any
436
+ // candidate can contribute below the watermark it defines.
437
+ const lead = buildUnitPriceMergeQuery({
438
+ streams: [streams[0]],
439
+ currencyCode,
440
+ direction,
441
+ after: after ?? null,
442
+ ...(range ? { amountRange: range } : {}),
443
+ perStreamLimit: limit,
444
+ watermark: null,
445
+ });
446
+ const leadRows = await em.execute(lead.sql, lead.params);
447
+ let sourceRowsRead = leadRows.length;
448
+ // The bound, and the one line that makes it exact: the leading stream has
449
+ // no exclusion, so a full window of `limit` rows at or below this amount
450
+ // means every row above it sorts after all of them and cannot be on this
451
+ // page. A short window is no bound at all.
452
+ const watermark = leadRows.length >= limit ? leadRows[leadRows.length - 1].amount : null;
453
+ const merged = leadRows.map((row) => ({
454
+ productId: row.product_id,
455
+ amount: row.amount,
456
+ streamIndex: 0,
457
+ }));
458
+ let everyOtherStreamShort = true;
459
+ if (streams.length > 1) {
460
+ const rest = buildUnitPriceMergeQuery({
461
+ streams,
462
+ currencyCode,
463
+ direction,
464
+ after: after ?? null,
465
+ ...(range ? { amountRange: range } : {}),
466
+ perStreamLimit: limit,
467
+ watermark,
468
+ skipStreams: 1,
469
+ });
470
+ const restRows = await em.execute(rest.sql, rest.params);
471
+ sourceRowsRead += restRows.length;
472
+ const perStream = new Map();
473
+ for (const row of restRows) {
474
+ perStream.set(row.stream_index, (perStream.get(row.stream_index) ?? 0) + 1);
475
+ merged.push({
476
+ productId: row.product_id,
477
+ amount: row.amount,
478
+ streamIndex: Number(row.stream_index),
479
+ });
480
+ }
481
+ for (const count of perStream.values())
482
+ if (count >= limit)
483
+ everyOtherStreamShort = false;
484
+ }
485
+ merged.sort((a, b) => compareOrderRows(a, b, direction));
486
+ const page = merged.slice(0, limit);
487
+ const rows = page.map((row) => ({
488
+ productId: row.productId,
489
+ amount: row.amount,
490
+ currency: currencyCode,
491
+ priceListId: streams[row.streamIndex].priceListId,
492
+ isSale: streams[row.streamIndex].isSale,
493
+ }));
494
+ // `exhausted` is not "a short chunk". It is "there is provably nothing
495
+ // left": no watermark cut anything off, no stream filled its own window,
496
+ // and the merge fitted inside the page.
497
+ const exhausted = watermark === null && everyOtherStreamShort && merged.length <= limit;
498
+ return { rows, exhausted, sourceRowsRead };
499
+ }
500
+ /**
501
+ * Which of `productIds` the viewer's candidate lists price at quantity 1.
502
+ *
503
+ * `catalog` composes the unpriced tail out of the complement, so this is a set
504
+ * membership answer and deliberately not a price: it says nothing about which
505
+ * list won or what it charges.
506
+ */
507
+ async pricedProductIds(input) {
508
+ const out = new Set();
509
+ if (input.productIds.length === 0)
510
+ return out;
511
+ const em = this.emFactory();
512
+ const currencyCode = this.#listingCurrency(input.context);
513
+ const streams = await this.#candidateStreams(em, input.context, currencyCode, input.productIds);
514
+ if (streams.length === 0)
515
+ return out;
516
+ const params = [currencyCode, uuidArrayLiteral([...new Set(input.productIds)])];
517
+ const perStream = streams.map((stream) => {
518
+ if (stream.productIds === null) {
519
+ params.push(stream.priceListId);
520
+ return `b."price_list_id" = ?`;
521
+ }
522
+ params.push(stream.priceListId, uuidArrayLiteral(stream.productIds));
523
+ return `(b."price_list_id" = ? and b."product_id" = any(?::uuid[]))`;
524
+ });
525
+ const rows = await em.execute(`select distinct b."product_id"::text as product_id
526
+ from "price_list_price_brackets" b
527
+ where b."currency_code" = ?
528
+ and b."min_quantity" = 1
529
+ and b."product_id" = any(?::uuid[])
530
+ and (${perStream.join(' or ')})`, params);
531
+ for (const row of rows)
532
+ out.add(row.product_id);
533
+ return out;
534
+ }
535
+ /**
536
+ * The display mode a *page* resolves to for this viewer — feature 086 /
537
+ * FR-016. See `PriceListService.resolvePageDisplayMode` for why the
538
+ * per-product and per-category steps are absent.
539
+ */
540
+ async pageDisplayMode(input) {
541
+ const { PriceListService } = await import('./price-list-service.js');
542
+ const priceListService = new PriceListService(this.emFactory, undefined, undefined, undefined, this.targetReads);
543
+ return priceListService.resolvePageDisplayMode({
544
+ organizationId: input.context.organization?.id ?? null,
545
+ salesChannelId: input.context.salesChannel.id,
546
+ customerKind: input.context.organization ? 'signed_in' : 'guest',
547
+ });
548
+ }
549
+ #listingCurrency(context) {
550
+ return (context.currencyCode ?? context.salesChannel.defaultCurrency).toUpperCase();
551
+ }
552
+ /**
553
+ * The viewer's candidate vector, resolved to streams: a price list, and the
554
+ * product ids the stream is restricted to.
555
+ *
556
+ * Two things restrict a stream, and they are intersected rather than chosen
557
+ * between: the caller's opaque id set, and the categories a candidate's own
558
+ * rule requires. The second is read through `catalog`'s category port — this
559
+ * module names `product_categories` in no statement of its own, and could not
560
+ * express the membership as a join into the merge anyway without putting
561
+ * another module's table in this module's SQL (D-87).
562
+ */
563
+ async #candidateStreams(em, context, currencyCode, restrictToProductIds) {
564
+ const organization = context.organization ?? null;
565
+ const orgId = organization?.id ?? null;
566
+ const organizationChain = orgId !== null && this.resolveOrgChain
567
+ ? await this.resolveOrgChain(orgId)
568
+ : orgId !== null
569
+ ? [orgId]
570
+ : [];
571
+ const viewer = {
572
+ organizationId: orgId,
573
+ organizationChain,
574
+ customerGroupId: context.customerGroupId ?? organization?.customerGroupId ?? null,
575
+ salesChannelId: context.salesChannel.id,
576
+ currencyCode,
577
+ };
578
+ const activeLists = await em.find(PriceList, { status: 'active' });
579
+ const candidates = buildCandidateVector(activeLists.map((list) => ({
580
+ id: list.id,
581
+ type: list.type,
582
+ applicationRule: list.applicationRule,
583
+ modifiedAt: list.modifiedAt,
584
+ name: list.name,
585
+ isSystem: list.isSystem,
586
+ })), viewer);
587
+ const restriction = restrictToProductIds ? new Set(restrictToProductIds) : null;
588
+ const categoryProducts = await this.#categoryProductIds(candidates);
589
+ const streams = [];
590
+ for (const candidate of candidates) {
591
+ let ids = restriction;
592
+ for (const group of candidate.requireAnyOf) {
593
+ const inGroup = new Set();
594
+ for (const categoryId of group) {
595
+ for (const productId of categoryProducts.get(categoryId) ?? [])
596
+ inGroup.add(productId);
597
+ }
598
+ ids = ids === null ? inGroup : new Set([...ids].filter((id) => inGroup.has(id)));
599
+ }
600
+ // A candidate whose categories hold nothing prices nothing: dropping it
601
+ // is not an optimisation, it is the empty stream written down.
602
+ if (ids !== null && ids.size === 0)
603
+ continue;
604
+ streams.push({
605
+ priceListId: candidate.priceListId,
606
+ isSale: candidate.isSale,
607
+ productIds: ids === null ? null : [...ids],
608
+ });
609
+ }
610
+ return streams;
611
+ }
612
+ /**
613
+ * Product ids per category id, for every category any candidate's rule names.
614
+ *
615
+ * Read through `catalog`'s port, structurally — `listProductIdsInCategory`
616
+ * filters neither `isActive` nor `deletedAt`, and neither does
617
+ * `evaluateApplicationRule`'s `category` criterion, which tests the raw
618
+ * membership. The two have to agree, so the read that agrees is the one to
619
+ * make.
620
+ *
621
+ * Nothing is read at all in the ordinary case: a rule that names no category
622
+ * produces no group, and the reference catalogue's rules name none.
623
+ */
624
+ async #categoryProductIds(candidates) {
625
+ const wanted = new Set();
626
+ for (const candidate of candidates) {
627
+ for (const group of candidate.requireAnyOf)
628
+ for (const id of group)
629
+ wanted.add(id);
630
+ }
631
+ const out = new Map();
632
+ if (wanted.size === 0)
633
+ return out;
634
+ // Refuses when nothing was wired — see `#categoryRead`. Skipping the
635
+ // membership would make this category rule apply to everything, which
636
+ // prices the products it excludes.
637
+ const categoryRead = this.#categoryRead();
638
+ for (const categoryId of wanted) {
639
+ out.set(categoryId, await categoryRead.listProductIdsInCategory(categoryId));
640
+ }
641
+ return out;
642
+ }
643
+ /**
644
+ * Issue #132 — the lowest-quantity bracket amount on one named price list.
645
+ *
646
+ * `product_feeds` used to run this `select` itself, which put another
647
+ * module's table in its SQL and let a feed publish prices while this module
648
+ * was refusing to serve them. The read is the same; owning it here is what
649
+ * makes the port gate apply.
650
+ */
651
+ async namedListPrices(input) {
652
+ const out = new Map();
653
+ if (input.productIds.length === 0)
654
+ return out;
655
+ const em = this.emFactory();
656
+ const placeholders = input.productIds.map(() => '?').join(',');
657
+ const rows = await em.getConnection().execute(`select distinct on ("product_id") "product_id", "amount"
658
+ from "price_list_price_brackets"
659
+ where "price_list_id" = ? and "currency_code" = ?
660
+ and "product_id" in (${placeholders})
661
+ order by "product_id" asc, "min_quantity" asc`, [input.priceListId, input.currencyCode.toUpperCase(), ...input.productIds], 'all', em.getTransactionContext());
662
+ for (const row of rows)
663
+ out.set(row.product_id, row.amount);
664
+ return out;
665
+ }
666
+ /**
667
+ * Feature 062 — distinct bracket start quantities (ascending) across every
668
+ * ACTIVE list for a (product, currency). Consumers probe `resolveLinePrice`
669
+ * at each quantity to derive the acting org's effective tier ladder; a
670
+ * quantity contributed by a non-matching list simply resolves to the same
671
+ * amount as its predecessor and is de-duplicated by the caller — no
672
+ * cross-org data leaks through this read.
673
+ */
674
+ async listBracketMinQuantities(productId, currencyCode) {
675
+ const em = this.emFactory();
676
+ const rows = await em.execute(`select distinct b.min_quantity
677
+ from price_list_price_brackets b
678
+ join price_lists l on l.id = b.price_list_id
679
+ where b.product_id = ? and b.currency_code = ? and l.status = 'active'
680
+ order by b.min_quantity asc`, [productId, currencyCode.toUpperCase()]);
681
+ return rows.map((r) => Number(r.min_quantity));
682
+ }
683
+ /**
684
+ * Walks the priority chain until a list yields a bracket for the
685
+ * requested (product, currency, quantity). Excluded candidates are
686
+ * ones that already failed the bracket lookup. Returns the first
687
+ * list+bracket pair, or {list: null, bracket: null} if every match
688
+ * has no usable bracket.
689
+ */
690
+ /**
691
+ * `pickAndResolveBracket` for a whole page, in **priority waves**.
692
+ *
693
+ * Each wave picks the top-priority remaining list per product — the same
694
+ * `pickPriorityChain` call the single-product walk makes — and reads the
695
+ * brackets for that wave's (list, product) pairs in one query. A product
696
+ * whose picked list has no usable bracket drops that list and enters the next
697
+ * wave, exactly as FR-031's fall-through does; a product with no candidate
698
+ * left answers `{ list: null, bracket: null }`, exactly as the single walk's
699
+ * exhausted `remaining` does.
700
+ *
701
+ * The number of queries is therefore the number of fall-through levels the
702
+ * page actually needs — one, normally — instead of one per product, and the
703
+ * rows fetched are the ones the per-product walk would have fetched rather
704
+ * than every matched list's brackets.
705
+ */
706
+ async pickAndResolveBracketsForSet(em, matchesByProduct, currencyCode, quantity) {
707
+ const out = new Map();
708
+ const remaining = new Map();
709
+ for (const [productId, matches] of matchesByProduct) {
710
+ if (matches.length === 0)
711
+ out.set(productId, { list: null, bracket: null });
712
+ else
713
+ remaining.set(productId, [...matches]);
714
+ }
715
+ while (remaining.size > 0) {
716
+ const picks = new Map();
717
+ for (const [productId, matches] of [...remaining]) {
718
+ const picked = pickPriorityChain(matches.map((m) => m.candidate));
719
+ if (!picked) {
720
+ out.set(productId, { list: null, bracket: null });
721
+ remaining.delete(productId);
722
+ continue;
723
+ }
724
+ picks.set(productId, picked.list);
725
+ }
726
+ if (picks.size === 0)
727
+ break;
728
+ const listIds = [...new Set([...picks.values()].map((list) => list.id))];
729
+ const brackets = await em.find(PriceListPriceBracket, {
730
+ priceListId: { $in: listIds },
731
+ productId: { $in: [...picks.keys()] },
732
+ });
733
+ const rowsByPair = new Map();
734
+ for (const b of brackets) {
735
+ const key = `${b.priceListId}|${b.productId}`;
736
+ const rows = rowsByPair.get(key);
737
+ const row = {
738
+ priceListId: b.priceListId,
739
+ productId: b.productId,
740
+ currencyCode: b.currencyCode,
741
+ minQuantity: b.minQuantity,
742
+ maxQuantity: b.maxQuantity ?? null,
743
+ amount: b.amount,
744
+ };
745
+ if (rows)
746
+ rows.push(row);
747
+ else
748
+ rowsByPair.set(key, [row]);
749
+ }
750
+ for (const [productId, list] of picks) {
751
+ const bracket = resolvePriceBracket(rowsByPair.get(`${list.id}|${productId}`) ?? [], currencyCode, quantity);
752
+ if (bracket) {
753
+ out.set(productId, { list, bracket });
754
+ remaining.delete(productId);
755
+ continue;
756
+ }
757
+ const matches = remaining.get(productId);
758
+ if (!matches)
759
+ continue;
760
+ const idx = matches.findIndex((m) => m.list.id === list.id);
761
+ if (idx >= 0)
762
+ matches.splice(idx, 1);
763
+ if (matches.length === 0) {
764
+ out.set(productId, { list: null, bracket: null });
765
+ remaining.delete(productId);
766
+ }
767
+ }
768
+ }
769
+ return out;
770
+ }
771
+ async pickAndResolveBracket(em, matches, productId, currencyCode, quantity) {
772
+ if (matches.length === 0)
773
+ return { list: null, bracket: null };
774
+ const remaining = [...matches];
775
+ while (remaining.length > 0) {
776
+ const picked = pickPriorityChain(remaining.map((m) => m.candidate));
777
+ if (!picked)
778
+ return { list: null, bracket: null };
779
+ const list = picked.list;
780
+ const brackets = await em.find(PriceListPriceBracket, {
781
+ priceListId: list.id,
782
+ productId,
783
+ });
784
+ const rows = brackets.map((b) => ({
785
+ priceListId: b.priceListId,
786
+ productId: b.productId,
787
+ currencyCode: b.currencyCode,
788
+ minQuantity: b.minQuantity,
789
+ maxQuantity: b.maxQuantity ?? null,
790
+ amount: b.amount,
791
+ }));
792
+ const bracket = resolvePriceBracket(rows, currencyCode, quantity);
793
+ if (bracket)
794
+ return { list, bracket };
795
+ // Fall through: drop the picked list from `remaining` and retry.
796
+ const idx = remaining.findIndex((m) => m.list.id === list.id);
797
+ if (idx >= 0)
798
+ remaining.splice(idx, 1);
799
+ }
800
+ return { list: null, bracket: null };
801
+ }
802
+ }
803
+ //# sourceMappingURL=pricing-service.js.map