@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.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/admin/components/ApplicationRuleBuilder.d.ts +24 -0
- package/dist/admin/components/ApplicationRuleBuilder.d.ts.map +1 -0
- package/dist/admin/components/ApplicationRuleBuilder.js +249 -0
- package/dist/admin/components/ApplicationRuleBuilder.js.map +1 -0
- package/dist/admin/components/BracketGrid.d.ts +29 -0
- package/dist/admin/components/BracketGrid.d.ts.map +1 -0
- package/dist/admin/components/BracketGrid.js +252 -0
- package/dist/admin/components/BracketGrid.js.map +1 -0
- package/dist/admin/components/DisplayModeOverrideRow.d.ts +46 -0
- package/dist/admin/components/DisplayModeOverrideRow.d.ts.map +1 -0
- package/dist/admin/components/DisplayModeOverrideRow.js +109 -0
- package/dist/admin/components/DisplayModeOverrideRow.js.map +1 -0
- package/dist/admin/components/LinkedPriceListsPanel.d.ts +55 -0
- package/dist/admin/components/LinkedPriceListsPanel.d.ts.map +1 -0
- package/dist/admin/components/LinkedPriceListsPanel.js +131 -0
- package/dist/admin/components/LinkedPriceListsPanel.js.map +1 -0
- package/dist/admin/index.d.ts +34 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +119 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/DisplayModeOverridesPage.d.ts +25 -0
- package/dist/admin/pages/DisplayModeOverridesPage.d.ts.map +1 -0
- package/dist/admin/pages/DisplayModeOverridesPage.js +266 -0
- package/dist/admin/pages/DisplayModeOverridesPage.js.map +1 -0
- package/dist/admin/pages/PriceListDetailPage.d.ts +23 -0
- package/dist/admin/pages/PriceListDetailPage.d.ts.map +1 -0
- package/dist/admin/pages/PriceListDetailPage.js +421 -0
- package/dist/admin/pages/PriceListDetailPage.js.map +1 -0
- package/dist/admin/pages/PriceListsPage.d.ts +17 -0
- package/dist/admin/pages/PriceListsPage.d.ts.map +1 -0
- package/dist/admin/pages/PriceListsPage.js +198 -0
- package/dist/admin/pages/PriceListsPage.js.map +1 -0
- package/dist/admin/zones/CategoryDisplayMode.d.ts +27 -0
- package/dist/admin/zones/CategoryDisplayMode.d.ts.map +1 -0
- package/dist/admin/zones/CategoryDisplayMode.js +11 -0
- package/dist/admin/zones/CategoryDisplayMode.js.map +1 -0
- package/dist/admin/zones/OrganizationDisplayMode.d.ts +36 -0
- package/dist/admin/zones/OrganizationDisplayMode.d.ts.map +1 -0
- package/dist/admin/zones/OrganizationDisplayMode.js +12 -0
- package/dist/admin/zones/OrganizationDisplayMode.js.map +1 -0
- package/dist/admin/zones/ProductLinkedPriceLists.d.ts +16 -0
- package/dist/admin/zones/ProductLinkedPriceLists.d.ts.map +1 -0
- package/dist/admin/zones/ProductLinkedPriceLists.js +7 -0
- package/dist/admin/zones/ProductLinkedPriceLists.js.map +1 -0
- package/dist/backend/entities/price-display-mode-override.entity.d.ts +23 -0
- package/dist/backend/entities/price-display-mode-override.entity.d.ts.map +1 -0
- package/dist/backend/entities/price-display-mode-override.entity.js +58 -0
- package/dist/backend/entities/price-display-mode-override.entity.js.map +1 -0
- package/dist/backend/entities/price-list-price-bracket.entity.d.ts +26 -0
- package/dist/backend/entities/price-list-price-bracket.entity.d.ts.map +1 -0
- package/dist/backend/entities/price-list-price-bracket.entity.js +73 -0
- package/dist/backend/entities/price-list-price-bracket.entity.js.map +1 -0
- package/dist/backend/entities/price-list-product.entity.d.ts +15 -0
- package/dist/backend/entities/price-list-product.entity.d.ts.map +1 -0
- package/dist/backend/entities/price-list-product.entity.js +43 -0
- package/dist/backend/entities/price-list-product.entity.js.map +1 -0
- package/dist/backend/entities/price-list.entity.d.ts +34 -0
- package/dist/backend/entities/price-list.entity.d.ts.map +1 -0
- package/dist/backend/entities/price-list.entity.js +115 -0
- package/dist/backend/entities/price-list.entity.js.map +1 -0
- package/dist/backend/index.d.ts +125 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +179 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/plugin.d.ts +68 -0
- package/dist/backend/plugin.d.ts.map +1 -0
- package/dist/backend/plugin.js +88 -0
- package/dist/backend/plugin.js.map +1 -0
- package/dist/backend/routes.d.ts +40 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +447 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/routes.storefront.d.ts +47 -0
- package/dist/backend/routes.storefront.d.ts.map +1 -0
- package/dist/backend/routes.storefront.js +131 -0
- package/dist/backend/routes.storefront.js.map +1 -0
- package/dist/backend/services/application-rule-evaluator.d.ts +25 -0
- package/dist/backend/services/application-rule-evaluator.d.ts.map +1 -0
- package/dist/backend/services/application-rule-evaluator.js +2 -0
- package/dist/backend/services/application-rule-evaluator.js.map +1 -0
- package/dist/backend/services/audit-references.d.ts +11 -0
- package/dist/backend/services/audit-references.d.ts.map +1 -0
- package/dist/backend/services/audit-references.js +23 -0
- package/dist/backend/services/audit-references.js.map +1 -0
- package/dist/backend/services/default-price-list-migration.d.ts +70 -0
- package/dist/backend/services/default-price-list-migration.d.ts.map +1 -0
- package/dist/backend/services/default-price-list-migration.js +177 -0
- package/dist/backend/services/default-price-list-migration.js.map +1 -0
- package/dist/backend/services/display-mode-resolver.d.ts +78 -0
- package/dist/backend/services/display-mode-resolver.d.ts.map +1 -0
- package/dist/backend/services/display-mode-resolver.js +71 -0
- package/dist/backend/services/display-mode-resolver.js.map +1 -0
- package/dist/backend/services/listing-price-chain.d.ts +26 -0
- package/dist/backend/services/listing-price-chain.d.ts.map +1 -0
- package/dist/backend/services/listing-price-chain.js +55 -0
- package/dist/backend/services/listing-price-chain.js.map +1 -0
- package/dist/backend/services/price-bracket-resolver.d.ts +24 -0
- package/dist/backend/services/price-bracket-resolver.d.ts.map +1 -0
- package/dist/backend/services/price-bracket-resolver.js +16 -0
- package/dist/backend/services/price-bracket-resolver.js.map +1 -0
- package/dist/backend/services/price-list-candidate-vector.d.ts +116 -0
- package/dist/backend/services/price-list-candidate-vector.d.ts.map +1 -0
- package/dist/backend/services/price-list-candidate-vector.js +133 -0
- package/dist/backend/services/price-list-candidate-vector.js.map +1 -0
- package/dist/backend/services/price-list-currency-reference.d.ts +12 -0
- package/dist/backend/services/price-list-currency-reference.d.ts.map +1 -0
- package/dist/backend/services/price-list-currency-reference.js +24 -0
- package/dist/backend/services/price-list-currency-reference.js.map +1 -0
- package/dist/backend/services/price-list-read-port.d.ts +29 -0
- package/dist/backend/services/price-list-read-port.d.ts.map +1 -0
- package/dist/backend/services/price-list-read-port.js +64 -0
- package/dist/backend/services/price-list-read-port.js.map +1 -0
- package/dist/backend/services/price-list-resolver.d.ts +26 -0
- package/dist/backend/services/price-list-resolver.d.ts.map +1 -0
- package/dist/backend/services/price-list-resolver.js +37 -0
- package/dist/backend/services/price-list-resolver.js.map +1 -0
- package/dist/backend/services/price-list-service.d.ts +431 -0
- package/dist/backend/services/price-list-service.d.ts.map +1 -0
- package/dist/backend/services/price-list-service.js +1238 -0
- package/dist/backend/services/price-list-service.js.map +1 -0
- package/dist/backend/services/price-list-status-worker.d.ts +28 -0
- package/dist/backend/services/price-list-status-worker.d.ts.map +1 -0
- package/dist/backend/services/price-list-status-worker.js +38 -0
- package/dist/backend/services/price-list-status-worker.js.map +1 -0
- package/dist/backend/services/pricing-cache.d.ts +53 -0
- package/dist/backend/services/pricing-cache.d.ts.map +1 -0
- package/dist/backend/services/pricing-cache.js +88 -0
- package/dist/backend/services/pricing-cache.js.map +1 -0
- package/dist/backend/services/pricing-service.d.ts +304 -0
- package/dist/backend/services/pricing-service.d.ts.map +1 -0
- package/dist/backend/services/pricing-service.interface.d.ts +151 -0
- package/dist/backend/services/pricing-service.interface.d.ts.map +1 -0
- package/dist/backend/services/pricing-service.interface.js +10 -0
- package/dist/backend/services/pricing-service.interface.js.map +1 -0
- package/dist/backend/services/pricing-service.js +803 -0
- package/dist/backend/services/pricing-service.js.map +1 -0
- package/dist/backend/services/unit-price-ordering.d.ts +121 -0
- package/dist/backend/services/unit-price-ordering.d.ts.map +1 -0
- package/dist/backend/services/unit-price-ordering.js +164 -0
- package/dist/backend/services/unit-price-ordering.js.map +1 -0
- package/dist/manifest.d.ts +218 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +228 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260426T075235_price_lists_pricing_init.d.ts +24 -0
- package/dist/migrations/20260426T075235_price_lists_pricing_init.d.ts.map +1 -0
- package/dist/migrations/20260426T075235_price_lists_pricing_init.js +89 -0
- package/dist/migrations/20260426T075235_price_lists_pricing_init.js.map +1 -0
- package/dist/migrations/20260504T125655_price_lists_engine.d.ts +6 -0
- package/dist/migrations/20260504T125655_price_lists_engine.d.ts.map +1 -0
- package/dist/migrations/20260504T125655_price_lists_engine.js +319 -0
- package/dist/migrations/20260504T125655_price_lists_engine.js.map +1 -0
- package/dist/migrations/20260817T055457_price_lists_single_system_price_list.d.ts +15 -0
- package/dist/migrations/20260817T055457_price_lists_single_system_price_list.d.ts.map +1 -0
- package/dist/migrations/20260817T055457_price_lists_single_system_price_list.js +58 -0
- package/dist/migrations/20260817T055457_price_lists_single_system_price_list.js.map +1 -0
- package/dist/migrations/20260821T135907_price_lists_unit_price_amount_index.d.ts +40 -0
- package/dist/migrations/20260821T135907_price_lists_unit_price_amount_index.d.ts.map +1 -0
- package/dist/migrations/20260821T135907_price_lists_unit_price_amount_index.js +46 -0
- package/dist/migrations/20260821T135907_price_lists_unit_price_amount_index.js.map +1 -0
- package/dist/migrations/index.d.ts +30 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +35 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/price_lists.md +278 -0
- package/i18n/en.json +14 -0
- package/i18n/pl.json +14 -0
- package/package.json +99 -0
- 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=¤cy=&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";
|