@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,278 @@
1
+ ---
2
+ title: price_lists
3
+ description: Customer / group / default pricing with volume tiers + per-category adjustments
4
+ ---
5
+
6
+ # `price_lists`
7
+
8
+ Pricing engine — a reshape on top of the original pricing foundation.
9
+ Owns:
10
+
11
+ - **CustomerGroup** — addressable bucket of Organizations sharing pricing.
12
+ - **PriceList** — a named pricing artefact with a lifecycle status
13
+ (`draft`, `scheduled`, `active`, `expired`), a `type` (`base` or
14
+ `sale`), an optional active window (`startsAt` / `endsAt`), and an
15
+ `applicationRule` AST that decides which customer / org / channel
16
+ the list applies to.
17
+ - **PriceListProduct** — composite-PK assignment of a Product to a
18
+ PriceList.
19
+ - **PriceListPriceBracket** — composite-PK row keyed
20
+ `(priceListId, productId, currencyCode, minQuantity)` carrying the
21
+ per-currency unit price for a quantity bracket. Bracket gaps are
22
+ allowed; the resolver falls through to the next-priority list.
23
+ - **PriceDisplayModeOverride** — composite-PK row keyed
24
+ `(scope, targetId)` overriding the resolver's display-mode chain at
25
+ Organization / Category / Product scope. The chain root is two
26
+ Settings keys (`pricing.default_display_mode`,
27
+ `pricing.unauthenticated_display_mode`).
28
+
29
+ The legacy `PriceListItem` and `PriceListAssignment` tables (and the
30
+ `code` / `currency` / `priority` / `isDefault` columns on `price_lists`)
31
+ are kept by migration 031 only as a transitional shim during the
32
+ expand → migrate → contract rollout. Newly written code MUST consume
33
+ the engine schema via `@endora-commerce/contracts`.
34
+
35
+ ## Lifecycle
36
+
37
+ ```text
38
+ draft ──activate──▶ scheduled ──auto on startsAt──▶ active ──auto on endsAt──▶ expired
39
+ ▲ │ │ │
40
+ └──── draftify ───────┴────── draftify ──────────────┴────── activate (resets) ┘
41
+ ```
42
+
43
+ - `activate` flips a `draft` (or `expired`) list to `scheduled` if a
44
+ future `startsAt` is set, otherwise straight to `active`.
45
+ - `draftify` returns any non-system list to `draft`.
46
+ - `PriceListStatusWorker.sweep()` runs the two clock-driven transitions
47
+ (`scheduled → active` at `startsAt`, `active → expired` at `endsAt`).
48
+ In production it's invoked by a 5-min BullMQ repeatable job; in tests
49
+ the same `sweep()` is exposed via `POST /api/v1/admin/price-lists-engine/internal/sweep`.
50
+
51
+ The seeded `Default` list (`isSystem = true`) refuses every state
52
+ change, every delete, and every non-empty `applicationRule`.
53
+ Migration 031 also seeds bracket rows on it from each
54
+ product's legacy `attributeValues.defaultPrice`, so the platform always
55
+ has a usable terminal-fallback price.
56
+
57
+ ## Application Rule (AST)
58
+
59
+ The `applicationRule` JSONB column stores a discriminated union:
60
+
61
+ ```ts
62
+ type ApplicationRule =
63
+ | { kind: 'all' }
64
+ | {
65
+ kind: 'criterion';
66
+ type: 'salesChannel' | 'customerGroup' | 'organization' | 'category' | 'currency';
67
+ values: string[]; // UUIDs (or ISO 4217 for currency)
68
+ }
69
+ | {
70
+ kind: 'group';
71
+ op: 'AND' | 'OR';
72
+ children: ApplicationRule[]; // 1..20
73
+ };
74
+ ```
75
+
76
+ Constraints (enforced both client-side in the rule builder and by the
77
+ service-layer normaliser):
78
+
79
+ - Depth ≤ 5 nested groups.
80
+ - Per-criterion value lists are deduplicated; currency codes uppercased.
81
+ - Empty groups are dropped; if the root group ends up empty, it is
82
+ replaced with `{ kind: 'all' }` — but `{ kind: 'all' }` is only valid
83
+ on the `Default` system list. Any other list with an empty rule is
84
+ refused at activation time (`400 empty_rule_on_non_default`).
85
+ - Unknown target IDs (channel / customer-group / organization /
86
+ category) are refused with `400 unknown_target { type, values }`.
87
+
88
+ ## Resolver
89
+
90
+ `PricingService.resolveEngine({ product, variantId?, context })`
91
+ returns:
92
+
93
+ ```ts
94
+ {
95
+ base: { listId, bracket, listName }, // never null — Default is the floor
96
+ sale: { listId, bracket, listName } | null, // optional second tier
97
+ displayMode: 'gross_only' | 'net_only' | 'both' | 'none',
98
+ currencyCode: string
99
+ }
100
+ ```
101
+
102
+ Algorithm:
103
+
104
+ 1. Load every `status='active'` price list.
105
+ 2. Evaluate each list's `applicationRule` against the resolution
106
+ context; keep matchers, partition by `type`.
107
+ 3. For each partition, walk the **priority chain**:
108
+ - Level 1: explicit organization match.
109
+ - Level 2: explicit customer-group match.
110
+ - Level 3: explicit category match.
111
+ - Level 4: explicit sales-channel match.
112
+ - Level 5: any other matching list.
113
+ 4. Pick the winner per level via `tieBreak()` — most recent
114
+ `modifiedAt` first, lexicographic `name` ASC second.
115
+ 5. Look up the bracket for `(productId, currencyCode, quantity)`. If
116
+ no bracket matches (gap), fall through to the next-priority list in
117
+ the same partition; the Default list is always at level 5 of the
118
+ Base partition and serves as the terminal floor.
119
+ 6. Resolve the display mode independently via
120
+ `displayModeResolver(product, organization, customerKind)` along
121
+ the chain Settings → Organization → Category → Product.
122
+
123
+ Determinism: same inputs → same outputs (no clock, no randomness, total
124
+ tie-break order). The resolver is consumed by the storefront
125
+ (`GET /api/v1/storefront/products/:id/resolved-price`) and by the cart
126
+ + order-placement code paths.
127
+
128
+ ## Display modes
129
+
130
+ Four values: `gross_only`, `net_only`, `both`, `none`. The first three
131
+ control the column layout on every storefront price-bearing surface;
132
+ `none` hides every price element and replaces Add-to-cart with the
133
+ existing Quote Request CTA. The cart-line and
134
+ order-placement endpoints additionally refuse the line with
135
+ `400 product_quote_only` when the resolved mode is `none` for the
136
+ `(product, organization, channel)` tuple — defence in depth.
137
+
138
+ ## Public surface
139
+
140
+ ### Engine endpoints (admin)
141
+
142
+ | Verb + Path | Purpose |
143
+ | --- | --- |
144
+ | `GET /api/v1/admin/price-lists-engine?status=&type=&search=` | List engine-shape lists with optional filters |
145
+ | `POST /api/v1/admin/price-lists-engine` | Create a draft list |
146
+ | `GET /api/v1/admin/price-lists-engine/:id` | Read one list |
147
+ | `PATCH /api/v1/admin/price-lists-engine/:id` | Update name / type / dates / applicationRule |
148
+ | `POST /api/v1/admin/price-lists-engine/:id/activate` | Lifecycle → scheduled or active |
149
+ | `POST /api/v1/admin/price-lists-engine/:id/draftify` | Lifecycle → draft |
150
+ | `POST /api/v1/admin/price-lists-engine/:id/duplicate` | Clone (resets dates and status) |
151
+ | `GET /api/v1/admin/price-lists-engine/:id/products` | List roster + per-currency brackets |
152
+ | `PUT /api/v1/admin/price-lists-engine/:id/products` | Replace roster (delta) |
153
+ | `POST /api/v1/admin/price-lists-engine/:id/products` | Append one product |
154
+ | `DELETE /api/v1/admin/price-lists-engine/:id/products/:productId` | Remove (cascade brackets) |
155
+ | `GET /api/v1/admin/price-lists-engine/:id/products/:productId/brackets` | Read brackets for one product |
156
+ | `PUT /api/v1/admin/price-lists-engine/:id/products/:productId/brackets` | Replace brackets (`{ bracketsByCurrency }`) |
157
+ | `POST /api/v1/admin/price-lists-engine/:id/products/:productId/brackets/copy` | Identity copy of one currency to N others |
158
+ | `POST /api/v1/admin/price-lists-engine/internal/sweep` | Test-only worker tick |
159
+
160
+ ### Rule-builder pickers (admin)
161
+
162
+ | Verb + Path | Purpose |
163
+ | --- | --- |
164
+ | `GET /api/v1/admin/pricing/rule-targets/sales-channels` | Channel options for the rule builder |
165
+ | `GET /api/v1/admin/pricing/rule-targets/customer-groups` | Customer-group options |
166
+ | `GET /api/v1/admin/pricing/rule-targets/organizations?search=&limit=` | Paginated org options |
167
+ | `GET /api/v1/admin/pricing/rule-targets/categories` | Full category tree |
168
+ | `GET /api/v1/admin/pricing/rule-targets/currencies` | Currencies exposed by any sales channel |
169
+
170
+ ### Display-mode admin
171
+
172
+ | Verb + Path | Purpose |
173
+ | --- | --- |
174
+ | `GET /api/v1/admin/pricing/display-mode-overrides?scope=` | List overrides |
175
+ | `GET /api/v1/admin/pricing/display-mode-overrides/:scope/:targetId` | Read one |
176
+ | `PUT /api/v1/admin/pricing/display-mode-overrides/:scope/:targetId` | Upsert (`{ mode }` or `{ mode: 'inherit' }` to delete) |
177
+
178
+ ### Linked price-lists panel (admin)
179
+
180
+ | Verb + Path | Purpose |
181
+ | --- | --- |
182
+ | `GET /api/v1/admin/products/:productId/price-lists` | Lists every price list a product is part of, with per-currency bracket summary and a deep-link path |
183
+
184
+ ### Storefront (public)
185
+
186
+ | Verb + Path | Purpose |
187
+ | --- | --- |
188
+ | `GET /api/v1/storefront/products/:id/resolved-price?quantity=&currency=&variantId=` | Per-customer Base + Sale + display mode |
189
+ | `GET /api/v1/storefront/pricing/display-mode/:productId` | Display mode only (used by the cart, and by batch surfaces that resolve the price separately) |
190
+
191
+ Both storefront reads are resolved **for the viewer**: they take the buyer's
192
+ session when one is there and answer as the public when it is not. They also
193
+ derive that viewer through one function, so the display mode carried inside a
194
+ resolved price and the one this endpoint returns cannot disagree for the same
195
+ caller — they once could, and a signed-in buyer read net on the
196
+ product page and gross in the cart wherever `pricing.default_display_mode` and
197
+ `pricing.unauthenticated_display_mode` were set differently. A response resolved
198
+ for an Organization carries `Cache-Control: private, no-store`; the anonymous
199
+ one is unstamped and stays the representation a crawler and the storefront's
200
+ shared window hold.
201
+
202
+ ### Legacy (still served until every reader migrates)
203
+
204
+ | Verb + Path | Purpose |
205
+ | --- | --- |
206
+ | `GET / PUT / DELETE /api/v1/admin/price-lists{,/:code,/:id}` | Legacy CRUD over the old shape |
207
+ | `GET / POST / DELETE /api/v1/admin/price-lists/:id/items{,/:itemId}` | Legacy items CRUD |
208
+ | `GET / POST / DELETE /api/v1/admin/price-lists/:id/assignments{,/:assignmentId}` | Legacy assignments CRUD |
209
+ | `GET /api/v1/admin/price-lists/preview?productSku=&quantity=&organizationId=&salesChannelCode=` | Legacy preview |
210
+
211
+ ## Migration notes (031)
212
+
213
+ `031_price_lists_engine.ts` runs an 8-step transactional reshape:
214
+
215
+ 1. Acquire an advisory lock so concurrent migrations bail out cleanly.
216
+ 2. Add the new columns on `price_lists` (`type`, `status`, `startsAt`,
217
+ `endsAt`, `modifiedAt`, `isSystem`, `applicationRule` JSONB).
218
+ 3. Create the three new tables (`price_list_products`,
219
+ `price_list_price_brackets`, `price_display_mode_overrides`).
220
+ 4. Seed the `Default` list with a deterministic UUID.
221
+ 5. Walk every `Product` whose `attributeValues` carries `defaultPrice`
222
+ (or `price` as fallback) and upsert one bracket row per currency
223
+ exposed by any sales channel. Identity copy across currencies is
224
+ flagged in the migration report.
225
+ 6. Strip the legacy `attributeValues.defaultPrice` and
226
+ `attributeValues.price` keys.
227
+ 7. Emit the report to `backend/var/migration-reports/011_price_lists_seed.json`.
228
+ 8. Release the advisory lock.
229
+
230
+ The migration is **additive** to the legacy schema — the
231
+ `price_list_items` and `price_list_assignments` tables and the
232
+ `code`/`currency`/`priority`/`isDefault` columns on `price_lists`
233
+ remain in place until the readers in `cart-service`, `comparison-service`,
234
+ `catalog-query`, `search-query`, and `product-link.service` swap to the
235
+ resolver. A follow-up migration drops the legacy columns once that
236
+ audit lands.
237
+
238
+ The migration helper (`default-price-list-migration.ts`) is idempotent
239
+ and can be re-run as a repair command.
240
+
241
+ ## Storefront integration
242
+
243
+ - `GET /api/v1/storefront/products/:id/resolved-price` is consumed by
244
+ `storefront/lib/api/pricing.ts` (`getResolvedPrice` /
245
+ `getResolvedPricesBulk`); the bulk wrapper fans out to the singular
246
+ endpoint with bounded concurrency until a backend POST batch lands.
247
+ - `BaseSalePriceBlock`, `PriceTag`, and `ProductCard` consume the
248
+ `resolvedPrice` envelope; `displayMode === 'none'` hides every price
249
+ element and surfaces the `QuoteRequestCta` (which routes through
250
+ `AddToRfqForm`).
251
+ - The PDP fetches the resolver in parallel with stock and swaps the
252
+ Add-to-cart row for the QuoteRequest CTA when the mode is `none`.
253
+
254
+ ## Admin integration
255
+
256
+ - `/price-lists` (engine list) and `/price-lists/:id` (editor with
257
+ Details / Products & brackets / Application rule tabs).
258
+ - `/price-lists/display-modes` — overrides browser plus the two
259
+ `pricing.*` settings keys.
260
+ - `DisplayModeOverrideRow` is embedded in the Organization editor,
261
+ the Categories tree EditForm, and the LinkedPriceListsPanel that
262
+ replaces the old `PricingPlaceholder` on the catalog Product editor.
263
+
264
+ ## Extension points
265
+
266
+ - **In-memory LRU cache** around the resolver
267
+ (deferred — the storefront's 60-s revalidate window is sufficient
268
+ for the MVP). Bookkeeping: every write path on `PriceListService`
269
+ should emit `pricing.invalidate.v1`.
270
+ - **Cart-service / order-placement swap** — the foundation cart and
271
+ order-placement code paths still read `attributeValues.defaultPrice`.
272
+ The next iteration introduces `PricingService.resolveLinePrice()`
273
+ and swaps the cart-service constructor to depend on it; the
274
+ defence-in-depth check for `displayMode === 'none'` lands in the
275
+ same change.
276
+ - **Status worker BullMQ wiring** — `PriceListStatusWorker.sweep()` is
277
+ ready but the BullMQ repeatable-job registration (mirroring the
278
+ RFQ expiry worker) is queued for a follow-up.
package/i18n/en.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "activity.verb.price_list.activate": "activated price list",
3
+ "activity.verb.price_list.bracket_update": "updated price bracket on",
4
+ "activity.verb.price_list.create": "created price list",
5
+ "activity.verb.price_list.draftify": "moved price list to draft",
6
+ "activity.verb.price_list.duplicate": "duplicated price list",
7
+ "activity.verb.price_list.expire": "expired price list",
8
+ "activity.verb.price_list.products_replace": "replaced products on price list",
9
+ "activity.verb.price_list.update": "updated price list",
10
+
11
+ "nav.priceLists.label": "Price lists",
12
+ "actions.openPriceLists.label": "Price lists",
13
+ "actions.openPriceLists.description": "Pricing rules + assignments"
14
+ }
package/i18n/pl.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "activity.verb.price_list.activate": "aktywował(a) cennik",
3
+ "activity.verb.price_list.bracket_update": "zaktualizował(a) próg cenowy na",
4
+ "activity.verb.price_list.create": "utworzył(a) cennik",
5
+ "activity.verb.price_list.draftify": "przeniósł(esła) cennik do wersji roboczej",
6
+ "activity.verb.price_list.duplicate": "zduplikował(a) cennik",
7
+ "activity.verb.price_list.expire": "zakończył(a) cennik",
8
+ "activity.verb.price_list.products_replace": "zaktualizował(a) produkty na cenniku",
9
+ "activity.verb.price_list.update": "zaktualizował(a) cennik",
10
+
11
+ "nav.priceLists.label": "Cenniki",
12
+ "actions.openPriceLists.label": "Cenniki",
13
+ "actions.openPriceLists.description": "Reguły cenowe + przypisania"
14
+ }
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@endora-commerce/mod-price-lists",
3
+ "version": "0.100.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "description": "Customer-group pricing, brackets, display modes, and rule-based engine.",
7
+ "license": "MIT",
8
+ "endora": {
9
+ "type": "module",
10
+ "id": "price_lists"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/endora-commerce/endora-commerce.git",
15
+ "directory": "packages/modules/price_lists"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/manifest.d.ts",
23
+ "default": "./dist/manifest.js"
24
+ },
25
+ "./backend": {
26
+ "types": "./dist/backend/index.d.ts",
27
+ "default": "./dist/backend/index.js"
28
+ },
29
+ "./migrations": {
30
+ "types": "./dist/migrations/index.d.ts",
31
+ "default": "./dist/migrations/index.js"
32
+ },
33
+ "./admin": {
34
+ "types": "./dist/admin/index.d.ts",
35
+ "default": "./dist/admin/index.js"
36
+ },
37
+ "./tailwind.css": "./tailwind.css",
38
+ "./package.json": "./package.json"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "i18n",
43
+ "docs",
44
+ "tailwind.css"
45
+ ],
46
+ "engines": {
47
+ "node": ">=22.18.0"
48
+ },
49
+ "peerDependencies": {
50
+ "@mikro-orm/core": "^6",
51
+ "@mikro-orm/migrations": "^6",
52
+ "@mikro-orm/postgresql": "^6",
53
+ "fastify": "^5",
54
+ "lucide-react": "^1",
55
+ "react": "^19",
56
+ "react-router-dom": "^7",
57
+ "zod": "^4",
58
+ "@endora-commerce/admin-kit": "0.100.0",
59
+ "@endora-commerce/contracts": "0.100.0",
60
+ "@endora-commerce/platform": "0.100.0"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@endora-commerce/admin-kit": {
64
+ "optional": true
65
+ },
66
+ "lucide-react": {
67
+ "optional": true
68
+ },
69
+ "react": {
70
+ "optional": true
71
+ },
72
+ "react-router-dom": {
73
+ "optional": true
74
+ }
75
+ },
76
+ "devDependencies": {
77
+ "@mikro-orm/core": "^6.6.13",
78
+ "@mikro-orm/migrations": "^6.6.13",
79
+ "@mikro-orm/postgresql": "^6.6.13",
80
+ "@types/node": "^22.9.0",
81
+ "@types/react": "^19.2.14",
82
+ "fastify": "^5.12.5",
83
+ "lucide-react": "^1.11.0",
84
+ "react": "^19.2.5",
85
+ "react-router-dom": "^7.18.2",
86
+ "typescript": "^5.9.3",
87
+ "vitest": "^4.1.11",
88
+ "zod": "^4.2.0",
89
+ "@endora-commerce/admin-kit": "0.100.0",
90
+ "@endora-commerce/contracts": "0.100.0",
91
+ "@endora-commerce/platform": "0.100.0"
92
+ },
93
+ "scripts": {
94
+ "build": "tsc -p tsconfig.build.json && tsc -p tsconfig.ui.json",
95
+ "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.ui.json --noEmit",
96
+ "lint": "eslint src",
97
+ "test": "vitest run"
98
+ }
99
+ }
package/tailwind.css ADDED
@@ -0,0 +1,14 @@
1
+ /* @endora-commerce/mod-price-lists — AUTO-GENERATED by `pnpm --filter backend run manifests:generate`.
2
+ *
3
+ * The `@source` directives this package asks its host to scan
4
+ * (`specs/110-instance-repository/contracts/admin-stylesheet-composition.md` R1).
5
+ * They resolve relative to **this file**, so they hold wherever the package is
6
+ * installed — a workspace link here, `node_modules` in a client's instance.
7
+ *
8
+ * The `dist` line is what a published tarball ships and is what an instance
9
+ * scans; the `src` line is inert there and is what keeps `pnpm --filter admin
10
+ * run dev` reading source in this repository. Do not edit: run
11
+ * `pnpm --filter backend run manifests:generate`.
12
+ */
13
+ @source "./dist/admin";
14
+ @source "./src/admin";