@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,47 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ import type { PriceListService } from './services/price-list-service.js';
4
+ import type { PricingServiceContract } from './services/pricing-service.interface.js';
5
+ import { type CatalogProductReadPort, type OrganizationDetailsPort } from '@endora-commerce/contracts';
6
+ export interface StorefrontPricingRoutesDeps {
7
+ priceListService: PriceListService;
8
+ pricingService: PricingServiceContract;
9
+ emFactory: () => EntityManager;
10
+ /**
11
+ * Feature 075 Phase C — the product these two routes price, over `catalog`'s
12
+ * read port instead of its `Product` entity. It fails closed when `catalog`
13
+ * is off, which is the answer a storefront price probe should get: quoting a
14
+ * price for a product the platform will not serve is worse than refusing.
15
+ */
16
+ catalogProductRead: CatalogProductReadPort;
17
+ /**
18
+ * The buying organisation behind a signed-in request (issue #265). The same
19
+ * port the admin resolved-price probe in `routes.ts` reads, for the same
20
+ * reason: a price is resolved against an Organization, and the request only
21
+ * carries its id.
22
+ */
23
+ organizationDetails: OrganizationDetailsPort;
24
+ }
25
+ /**
26
+ * Storefront pricing endpoints (T025/T026 / contracts/pricing-resolution).
27
+ *
28
+ * GET /api/v1/storefront/products/:id/resolved-price
29
+ * GET /api/v1/storefront/pricing/display-mode/:productId
30
+ *
31
+ * Mounted from `plugin.ts` under the platform's existing
32
+ * `/api/v1/storefront/` prefix. Both routes are reachable without a credential
33
+ * and both **read** one when it is there, through the single
34
+ * {@link pricingViewerOf} above.
35
+ *
36
+ * The header above used to say auth was "intentionally absent", and the handler
37
+ * below passed `organization: null` to the engine behind a comment promising
38
+ * the organisation would be derived "when wired" (issue #265). It never was —
39
+ * so the one endpoint on the platform whose name promises a per-customer answer
40
+ * quoted every caller the channel price, from feature 011 until now. The
41
+ * ruling MR !796 settled for the catalogue applies here unchanged: an anonymous
42
+ * visitor sees the channel price, a signed-in buyer sees their own — and since
43
+ * issue #271 the display mode beside it is resolved for the same viewer, so the
44
+ * two cannot answer "net or gross for this buyer?" differently.
45
+ */
46
+ export declare function registerStorefrontPricingRoutes(app: FastifyInstance, deps: StorefrontPricingRoutesDeps): Promise<void>;
47
+ //# sourceMappingURL=routes.storefront.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.storefront.d.ts","sourceRoot":"","sources":["../../src/backend/routes.storefront.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAkB,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AACzE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yCAAyC,CAAC;AACtF,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAG7B,MAAM,4BAA4B,CAAC;AAIpC,MAAM,WAAW,2BAA2B;IAC1C,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,cAAc,EAAE,sBAAsB,CAAC;IACvC,SAAS,EAAE,MAAM,aAAa,CAAC;IAC/B;;;;;OAKG;IACH,kBAAkB,EAAE,sBAAsB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,EAAE,uBAAuB,CAAC;CAC9C;AA2CD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,+BAA+B,CACnD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,2BAA2B,GAChC,OAAO,CAAC,IAAI,CAAC,CA+Gf"}
@@ -0,0 +1,131 @@
1
+ import { isProductVisibleTo, } from '@endora-commerce/contracts';
2
+ import { getResolvedChannel } from '@endora-commerce/platform/kernel';
3
+ import { markPersonalisedPricing, productAudienceOf } from '@endora-commerce/platform/http';
4
+ async function pricingViewerOf(request, organizationDetails) {
5
+ const audience = productAudienceOf(request);
6
+ // `null` covers three callers whose answers happen to agree: the anonymous
7
+ // visitor, the authenticated caller with no Organization (feature 026's
8
+ // guest-style account, an unbound API key), and the buyer whose Organization
9
+ // row is gone.
10
+ const organization = audience.organizationId === null
11
+ ? null
12
+ : await organizationDetails.findById(audience.organizationId);
13
+ return { audience, organization, customerKind: organization ? 'signed_in' : 'guest' };
14
+ }
15
+ /**
16
+ * Storefront pricing endpoints (T025/T026 / contracts/pricing-resolution).
17
+ *
18
+ * GET /api/v1/storefront/products/:id/resolved-price
19
+ * GET /api/v1/storefront/pricing/display-mode/:productId
20
+ *
21
+ * Mounted from `plugin.ts` under the platform's existing
22
+ * `/api/v1/storefront/` prefix. Both routes are reachable without a credential
23
+ * and both **read** one when it is there, through the single
24
+ * {@link pricingViewerOf} above.
25
+ *
26
+ * The header above used to say auth was "intentionally absent", and the handler
27
+ * below passed `organization: null` to the engine behind a comment promising
28
+ * the organisation would be derived "when wired" (issue #265). It never was —
29
+ * so the one endpoint on the platform whose name promises a per-customer answer
30
+ * quoted every caller the channel price, from feature 011 until now. The
31
+ * ruling MR !796 settled for the catalogue applies here unchanged: an anonymous
32
+ * visitor sees the channel price, a signed-in buyer sees their own — and since
33
+ * issue #271 the display mode beside it is resolved for the same viewer, so the
34
+ * two cannot answer "net or gross for this buyer?" differently.
35
+ */
36
+ export async function registerStorefrontPricingRoutes(app, deps) {
37
+ const { priceListService, pricingService, catalogProductRead, organizationDetails } = deps;
38
+ app.get('/api/v1/storefront/pricing/display-mode/:productId', async (request, reply) => {
39
+ const viewer = await pricingViewerOf(request, organizationDetails);
40
+ // Before the read, as the resolved price below does: this answer is
41
+ // derived for the caller once the caller names an Organization, so it must
42
+ // not enter a cache another caller can read. The anonymous answer stays
43
+ // unstamped and is still the representation the storefront's shared
44
+ // window and a crawler hold (Principle VII).
45
+ markPersonalisedPricing(reply, viewer.audience);
46
+ const product = await catalogProductRead.findById(request.params.productId);
47
+ // Issue #227 — this route took a bare product id and answered about it.
48
+ // Whether a price is even displayed for a product an operator restricted
49
+ // to one distributor is that operator's answer, so the row has to pass the
50
+ // audience test before this route says anything about it at all.
51
+ if (!product || !isProductVisibleTo(product, viewer.audience)) {
52
+ reply.status(404);
53
+ return { error: { code: 'NOT_FOUND', message: 'Product not found.' } };
54
+ }
55
+ // Feature 053 / FR-002: read the channel resolved once by the canonical
56
+ // middleware (`getResolvedChannel()`) — no header re-parse, no re-query.
57
+ const channel = getResolvedChannel(request);
58
+ // Issue #271 — the viewer's, matching the price. This route used to pass
59
+ // `organizationId: null, customerKind: 'guest'` unconditionally, so the
60
+ // cart that reads it got the `unauthenticated_display_mode` key while the
61
+ // product page's resolved price carried the signed-in one: the same buyer,
62
+ // the same product, two conventions. The chain the resolver walks is
63
+ // unchanged — Product → Category → Organization → Settings, the
64
+ // organisation's say being the `price_display_mode_overrides` row it
65
+ // already consults — and only the viewer handed to it moved.
66
+ const mode = await priceListService.resolveDisplayMode({
67
+ productId: product.id,
68
+ organizationId: viewer.organization?.id ?? null,
69
+ salesChannelId: channel.id,
70
+ customerKind: viewer.customerKind,
71
+ });
72
+ return { data: { displayMode: mode } };
73
+ });
74
+ app.get('/api/v1/storefront/products/:id/resolved-price', async (request, reply) => {
75
+ const viewer = await pricingViewerOf(request, organizationDetails);
76
+ // Before the read, as `catalog`'s public routes do: this response's price
77
+ // is resolved for the caller, and so is the 404 below, so a response that
78
+ // named an organisation must not enter a shared cache whatever it turned
79
+ // out to be. Anonymous responses are left unstamped and unchanged — still
80
+ // the representation a crawler indexes.
81
+ markPersonalisedPricing(reply, viewer.audience);
82
+ const product = await catalogProductRead.findById(request.params.id);
83
+ // Issue #227 — see the display-mode route above. A resolved price is the
84
+ // most direct disclosure this module has.
85
+ if (!product || !isProductVisibleTo(product, viewer.audience)) {
86
+ reply.status(404);
87
+ return { error: { code: 'NOT_FOUND', message: 'Product not found.' } };
88
+ }
89
+ const channel = getResolvedChannel(request);
90
+ const quantity = Math.max(1, Number(request.query.quantity ?? '1') || 1);
91
+ const currency = request.query.currency?.toUpperCase();
92
+ const variantId = request.query.variantId ?? null;
93
+ // The engine reads `customerGroupId` off the record `pricingViewerOf`
94
+ // loaded, so the organisation's group travels with it; a customer's *own*
95
+ // group override (feature 040 R6) is not resolved here, exactly as it is
96
+ // not on the catalogue listing this page's cards come from.
97
+ const out = await pricingService.resolveEngine({
98
+ product,
99
+ variantId,
100
+ context: {
101
+ quantity,
102
+ organization: viewer.organization,
103
+ salesChannel: channel,
104
+ ...(currency ? { currencyCode: currency } : {}),
105
+ },
106
+ });
107
+ return {
108
+ data: {
109
+ resolvedPrice: {
110
+ baseListId: out.base.listId,
111
+ basePrice: out.base.bracket
112
+ ? { amount: out.base.bracket.amount, currency: out.currencyCode }
113
+ : null,
114
+ saleListId: out.sale?.listId ?? null,
115
+ salePrice: out.sale
116
+ ? { amount: out.sale.bracket.amount, currency: out.currencyCode }
117
+ : null,
118
+ displayMode: out.displayMode,
119
+ currencyCode: out.currencyCode,
120
+ quantityBracket: out.base.bracket
121
+ ? {
122
+ minQuantity: out.base.bracket.minQuantity,
123
+ maxQuantity: out.base.bracket.maxQuantity ?? null,
124
+ }
125
+ : null,
126
+ },
127
+ },
128
+ };
129
+ });
130
+ }
131
+ //# sourceMappingURL=routes.storefront.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.storefront.js","sourceRoot":"","sources":["../../src/backend/routes.storefront.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure rule evaluator (feature 011 / FR-022, FR-023, FR-026, FR-029).
3
+ *
4
+ * The implementation moved to `@endora-commerce/contracts` in feature 075's Phase P
5
+ * (FR-013): it is a pure function over an `ApplicationRule` — a shape that
6
+ * package already declares — and a context of plain ids, so switching
7
+ * `price_lists` off cannot change whether a rule matches and a gated port
8
+ * answering 503 would be a bug rather than a degrade.
9
+ *
10
+ * There is a second reason it had to be one implementation rather than a port:
11
+ * `organizations` evaluates the same rules to work out an organisation's
12
+ * effective price lists, and it must reach the *same* answer the resolver
13
+ * does. Two copies that drifted would show an operator one set of lists on the
14
+ * organisation screen and price from another at checkout.
15
+ *
16
+ * The two type names widen when they leave the module (`ResolutionContext` and
17
+ * `RuleEvaluation` are too generic for a package every module imports), so
18
+ * they are aliased back here. Both this shim and the aliases go with the last
19
+ * consumer Phase C rewires.
20
+ */
21
+ import type { PriceListResolutionContext, PriceListRuleEvaluation } from '@endora-commerce/contracts';
22
+ export { evaluateApplicationRule } from '@endora-commerce/contracts';
23
+ export type ResolutionContext = PriceListResolutionContext;
24
+ export type RuleEvaluation = PriceListRuleEvaluation;
25
+ //# sourceMappingURL=application-rule-evaluator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-rule-evaluator.d.ts","sourceRoot":"","sources":["../../../src/backend/services/application-rule-evaluator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EACV,0BAA0B,EAC1B,uBAAuB,EACxB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC;AAErE,MAAM,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AAC3D,MAAM,MAAM,cAAc,GAAG,uBAAuB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { evaluateApplicationRule } from '@endora-commerce/contracts';
2
+ //# sourceMappingURL=application-rule-evaluator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"application-rule-evaluator.js","sourceRoot":"","sources":["../../../src/backend/services/application-rule-evaluator.ts"],"names":[],"mappings":"AAyBA,OAAO,EAAE,uBAAuB,EAAE,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { AuditReferenceRegistryPort } from '@endora-commerce/contracts';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ /**
4
+ * What a `price_list` audit row is called, and where the admin app shows it
5
+ * (feature 075, D-87 drain).
6
+ *
7
+ * `audit_logs` used to run `select id, name, code from price_lists` and spell
8
+ * `/price-lists/{id}` into its own switch statement.
9
+ */
10
+ export declare function registerPriceListAuditReferences(registry: AuditReferenceRegistryPort, emFactory: () => EntityManager): void;
11
+ //# sourceMappingURL=audit-references.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-references.d.ts","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAG3D;;;;;;GAMG;AACH,wBAAgB,gCAAgC,CAC9C,QAAQ,EAAE,0BAA0B,EACpC,SAAS,EAAE,MAAM,aAAa,GAC7B,IAAI,CAiBN"}
@@ -0,0 +1,23 @@
1
+ import { PriceList } from '../entities/price-list.entity.js';
2
+ /**
3
+ * What a `price_list` audit row is called, and where the admin app shows it
4
+ * (feature 075, D-87 drain).
5
+ *
6
+ * `audit_logs` used to run `select id, name, code from price_lists` and spell
7
+ * `/price-lists/{id}` into its own switch statement.
8
+ */
9
+ export function registerPriceListAuditReferences(registry, emFactory) {
10
+ registry.register({
11
+ ownerModuleId: 'price_lists',
12
+ referenceType: 'price_list',
13
+ resolve: async (ids) => {
14
+ const rows = await emFactory().find(PriceList, { id: { $in: [...ids] } }, { fields: ['id', 'name', 'code'] });
15
+ return rows.map((row) => ({
16
+ id: row.id,
17
+ label: row.name.length > 0 ? row.name : row.code,
18
+ url: `/price-lists/${row.id}`,
19
+ }));
20
+ },
21
+ });
22
+ }
23
+ //# sourceMappingURL=audit-references.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-references.js","sourceRoot":"","sources":["../../../src/backend/services/audit-references.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAE7D;;;;;;GAMG;AACH,MAAM,UAAU,gCAAgC,CAC9C,QAAoC,EACpC,SAA8B;IAE9B,QAAQ,CAAC,QAAQ,CAAC;QAChB,aAAa,EAAE,aAAa;QAC5B,aAAa,EAAE,YAAY;QAC3B,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;YACrB,MAAM,IAAI,GAAG,MAAM,SAAS,EAAE,CAAC,IAAI,CACjC,SAAS,EACT,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,EACzB,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CACnC,CAAC;YACF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBACxB,EAAE,EAAE,GAAG,CAAC,EAAE;gBACV,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI;gBAChD,GAAG,EAAE,gBAAgB,GAAG,CAAC,EAAE,EAAE;aAC9B,CAAC,CAAC,CAAC;QACN,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,70 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { CatalogProductReadPort } from '@endora-commerce/contracts';
3
+ /**
4
+ * Default price list migration helper (feature 011 / US1, FR-002, FR-003,
5
+ * research §R7).
6
+ *
7
+ * Surfaced as a callable service so:
8
+ * - migration `031` invokes it inline at upgrade time (preserved here),
9
+ * - an admin "repair" command can re-run it idempotently,
10
+ * - integration tests can re-seed `Default` after a truncate.
11
+ *
12
+ * Returns a structured report: how many products got fresh assignments, how
13
+ * many bracket rows were written, and which products received an identity
14
+ * copy across more than one currency (so administrators can revisit and set
15
+ * proper per-currency values per Q3).
16
+ */
17
+ export declare const DEFAULT_PRICE_LIST_ID = "00000000-0000-4000-8000-00000000d51b";
18
+ export interface MigrationReport {
19
+ /** Products newly assigned to the Default list during this run. */
20
+ productsAssigned: number;
21
+ /** Bracket rows newly inserted during this run. */
22
+ bracketRowsInserted: number;
23
+ /** Products that ended up with > 1 currency bracket (multi-currency identity copy). */
24
+ productsWithMultiCurrencyIdentityCopy: Array<{
25
+ productId: string;
26
+ currencies: string[];
27
+ legacyAmount: string;
28
+ }>;
29
+ /** Products skipped because the legacy field was unparseable. */
30
+ productsSkipped: Array<{
31
+ productId: string;
32
+ reason: string;
33
+ }>;
34
+ }
35
+ export declare class DefaultPriceListMigrator {
36
+ private readonly emFactory;
37
+ constructor(emFactory: () => EntityManager);
38
+ /**
39
+ * Idempotent upsert of the seeded `Default` price list. Re-running is a
40
+ * no-op when the row already exists. Used by both migration 031 and any
41
+ * test fixture that truncates `price_lists` and needs to restore Default.
42
+ */
43
+ seedDefault(): Promise<void>;
44
+ /**
45
+ * Idempotent re-run of the legacy `attributeValues.defaultPrice` migration.
46
+ * Re-runs are no-ops because every UPSERT keys on natural primary keys.
47
+ * Does NOT strip the legacy `attributeValues.defaultPrice` / `price` keys
48
+ * (per the expand→migrate→contract rollout — see Phase 2 strategy note in
49
+ * tasks.md).
50
+ *
51
+ * Auto-seeds the Default row first so this method works against a fresh
52
+ * test fixture that just truncated `price_lists`.
53
+ *
54
+ * Feature 075 Phase C — the product list arrives over `catalog`'s published
55
+ * read port instead of `em.find(Product, {})`.
56
+ *
57
+ * The port is a **parameter of this method**, not of the constructor, for two
58
+ * reasons. `seedDefault` needs nothing of `catalog`, and eighteen of this
59
+ * class's twenty callers only ever call that — a constructor argument would
60
+ * make all of them declare a dependency two of them have. And the read has to
61
+ * happen on the **caller's** unit of work: a backfill run inside an open
62
+ * transaction, as the dev seed and the migration tests do, must see the
63
+ * products that transaction has just written, which a port bound to another
64
+ * fork cannot. Passing it per call is what lets the caller say which
65
+ * `EntityManager` the read runs on, and the previous `em.find(Product, …)`
66
+ * only got that right by accident of sharing this class's own factory.
67
+ */
68
+ run(catalogProductRead: CatalogProductReadPort): Promise<MigrationReport>;
69
+ }
70
+ //# sourceMappingURL=default-price-list-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-price-list-migration.d.ts","sourceRoot":"","sources":["../../../src/backend/services/default-price-list-migration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAMzE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,yCAAyC,CAAC;AAE5E,MAAM,WAAW,eAAe;IAC9B,mEAAmE;IACnE,gBAAgB,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uFAAuF;IACvF,qCAAqC,EAAE,KAAK,CAAC;QAC3C,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,EAAE,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC,CAAC;IACH,iEAAiE;IACjE,eAAe,EAAE,KAAK,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/D;AAED,qBAAa,wBAAwB;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAE3D;;;;OAIG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAgClC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,GAAG,CAAC,kBAAkB,EAAE,sBAAsB,GAAG,OAAO,CAAC,eAAe,CAAC;CAiGhF"}
@@ -0,0 +1,177 @@
1
+ import { SalesChannel } from '@endora-commerce/platform/kernel';
2
+ import { PriceList } from '../entities/price-list.entity.js';
3
+ import { PriceListProduct } from '../entities/price-list-product.entity.js';
4
+ import { PriceListPriceBracket } from '../entities/price-list-price-bracket.entity.js';
5
+ /**
6
+ * Default price list migration helper (feature 011 / US1, FR-002, FR-003,
7
+ * research §R7).
8
+ *
9
+ * Surfaced as a callable service so:
10
+ * - migration `031` invokes it inline at upgrade time (preserved here),
11
+ * - an admin "repair" command can re-run it idempotently,
12
+ * - integration tests can re-seed `Default` after a truncate.
13
+ *
14
+ * Returns a structured report: how many products got fresh assignments, how
15
+ * many bracket rows were written, and which products received an identity
16
+ * copy across more than one currency (so administrators can revisit and set
17
+ * proper per-currency values per Q3).
18
+ */
19
+ export const DEFAULT_PRICE_LIST_ID = '00000000-0000-4000-8000-00000000d51b';
20
+ export class DefaultPriceListMigrator {
21
+ emFactory;
22
+ constructor(emFactory) {
23
+ this.emFactory = emFactory;
24
+ }
25
+ /**
26
+ * Idempotent upsert of the seeded `Default` price list. Re-running is a
27
+ * no-op when the row already exists. Used by both migration 031 and any
28
+ * test fixture that truncates `price_lists` and needs to restore Default.
29
+ */
30
+ async seedDefault() {
31
+ // command-coverage-ignore: one-time system bootstrap that seeds the Default
32
+ // price list at install/migration time — not an admin action.
33
+ const em = this.emFactory();
34
+ const existing = await em.findOne(PriceList, { id: DEFAULT_PRICE_LIST_ID });
35
+ if (existing) {
36
+ // Keep engine columns coherent if they were left unset on an older row.
37
+ existing.name = 'Default';
38
+ existing.type = 'base';
39
+ existing.status = 'active';
40
+ existing.applicationRule = { kind: 'all' };
41
+ existing.isSystem = true;
42
+ existing.modifiedAt = new Date();
43
+ await em.flush();
44
+ return;
45
+ }
46
+ const row = em.create(PriceList, {
47
+ id: DEFAULT_PRICE_LIST_ID,
48
+ code: 'default',
49
+ name: 'Default',
50
+ currency: 'PLN',
51
+ isDefault: true,
52
+ priority: 0,
53
+ type: 'base',
54
+ status: 'active',
55
+ applicationRule: { kind: 'all' },
56
+ isSystem: true,
57
+ modifiedAt: new Date(),
58
+ });
59
+ await em.persistAndFlush(row);
60
+ }
61
+ /**
62
+ * Idempotent re-run of the legacy `attributeValues.defaultPrice` migration.
63
+ * Re-runs are no-ops because every UPSERT keys on natural primary keys.
64
+ * Does NOT strip the legacy `attributeValues.defaultPrice` / `price` keys
65
+ * (per the expand→migrate→contract rollout — see Phase 2 strategy note in
66
+ * tasks.md).
67
+ *
68
+ * Auto-seeds the Default row first so this method works against a fresh
69
+ * test fixture that just truncated `price_lists`.
70
+ *
71
+ * Feature 075 Phase C — the product list arrives over `catalog`'s published
72
+ * read port instead of `em.find(Product, {})`.
73
+ *
74
+ * The port is a **parameter of this method**, not of the constructor, for two
75
+ * reasons. `seedDefault` needs nothing of `catalog`, and eighteen of this
76
+ * class's twenty callers only ever call that — a constructor argument would
77
+ * make all of them declare a dependency two of them have. And the read has to
78
+ * happen on the **caller's** unit of work: a backfill run inside an open
79
+ * transaction, as the dev seed and the migration tests do, must see the
80
+ * products that transaction has just written, which a port bound to another
81
+ * fork cannot. Passing it per call is what lets the caller say which
82
+ * `EntityManager` the read runs on, and the previous `em.find(Product, …)`
83
+ * only got that right by accident of sharing this class's own factory.
84
+ */
85
+ async run(catalogProductRead) {
86
+ // command-coverage-ignore: one-time data migration (backfills the Default
87
+ // price list + assignments) run at install/upgrade — not an admin action.
88
+ await this.seedDefault();
89
+ const em = this.emFactory();
90
+ // Discover the currencies exposed by any sales channel + always include PLN
91
+ // as the platform's default currency (matches migration 031's fallback).
92
+ const channels = await em.find(SalesChannel, {});
93
+ const currencies = new Set(['PLN']);
94
+ for (const ch of channels) {
95
+ for (const c of ch.currencies ?? []) {
96
+ if (typeof c === 'string' && c.length === 3) {
97
+ currencies.add(c.toUpperCase());
98
+ }
99
+ }
100
+ }
101
+ // Discover legacy-priced products through `catalog`'s read port.
102
+ const products = await catalogProductRead.listAll();
103
+ const skipped = [];
104
+ const candidates = [];
105
+ for (const p of products) {
106
+ const av = p.attributeValues;
107
+ if (!av)
108
+ continue;
109
+ const raw = av['defaultPrice'] ?? av['price'];
110
+ if (raw == null)
111
+ continue;
112
+ const amount = Number(raw);
113
+ if (!Number.isFinite(amount) || amount < 0) {
114
+ skipped.push({ productId: p.id, reason: 'legacy_price_invalid' });
115
+ continue;
116
+ }
117
+ candidates.push({ productId: p.id, legacyAmount: amount.toFixed(4) });
118
+ }
119
+ let productsAssigned = 0;
120
+ let bracketRowsInserted = 0;
121
+ const multiCurrency = [];
122
+ const currencyList = [...currencies].sort();
123
+ // Pass 1: assignments — flush before inserting brackets so the FK from
124
+ // bracket → assignment is satisfied at insert time.
125
+ for (const c of candidates) {
126
+ const existingAssignment = await em.findOne(PriceListProduct, {
127
+ priceListId: DEFAULT_PRICE_LIST_ID,
128
+ productId: c.productId,
129
+ });
130
+ if (!existingAssignment) {
131
+ em.create(PriceListProduct, {
132
+ priceListId: DEFAULT_PRICE_LIST_ID,
133
+ productId: c.productId,
134
+ });
135
+ productsAssigned += 1;
136
+ }
137
+ }
138
+ await em.flush();
139
+ // Pass 2: brackets per currency.
140
+ for (const c of candidates) {
141
+ for (const cur of currencyList) {
142
+ const existingBracket = await em.findOne(PriceListPriceBracket, {
143
+ priceListId: DEFAULT_PRICE_LIST_ID,
144
+ productId: c.productId,
145
+ currencyCode: cur,
146
+ minQuantity: 1,
147
+ });
148
+ if (!existingBracket) {
149
+ em.create(PriceListPriceBracket, {
150
+ priceListId: DEFAULT_PRICE_LIST_ID,
151
+ productId: c.productId,
152
+ currencyCode: cur,
153
+ minQuantity: 1,
154
+ maxQuantity: null,
155
+ amount: c.legacyAmount,
156
+ });
157
+ bracketRowsInserted += 1;
158
+ }
159
+ }
160
+ if (currencyList.length > 1) {
161
+ multiCurrency.push({
162
+ productId: c.productId,
163
+ currencies: currencyList,
164
+ legacyAmount: c.legacyAmount,
165
+ });
166
+ }
167
+ }
168
+ await em.flush();
169
+ return {
170
+ productsAssigned,
171
+ bracketRowsInserted,
172
+ productsWithMultiCurrencyIdentityCopy: multiCurrency,
173
+ productsSkipped: skipped,
174
+ };
175
+ }
176
+ }
177
+ //# sourceMappingURL=default-price-list-migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"default-price-list-migration.js","sourceRoot":"","sources":["../../../src/backend/services/default-price-list-migration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import type { DisplayMode } from '@endora-commerce/contracts';
2
+ /**
3
+ * Pure display-mode resolver (feature 011 / FR-039).
4
+ *
5
+ * Walks the chain Product → Category → Organization → Settings (default for
6
+ * signed-in, unauthenticated for guests) and returns the first non-`inherit`
7
+ * value. Guests carry `organizationId === null`, in which case the
8
+ * Organization-level lookup is skipped.
9
+ *
10
+ * The category-level lookup picks the most specific category override the
11
+ * product belongs to — defined as the deepest category in the tree, with
12
+ * a deterministic tie-break on `(sort_order asc, id asc)`. The category
13
+ * depth + tie-break logic lives at the SQL layer; this pure resolver just
14
+ * receives the already-picked override row (or null).
15
+ */
16
+ export type CustomerKind = 'guest' | 'signed_in';
17
+ export type ResolvedFrom = 'product' | 'category' | 'organization' | 'settings.default' | 'settings.unauthenticated';
18
+ export interface DisplayModeOverrideLookup {
19
+ /** Product-level override row or null. */
20
+ productOverride: DisplayMode | null;
21
+ /** Most-specific category-level override the product belongs to, or null. */
22
+ categoryOverride: DisplayMode | null;
23
+ /** Organization-level override or null (also null for guests). */
24
+ organizationOverride: DisplayMode | null;
25
+ }
26
+ export interface SettingsDefaults {
27
+ defaultDisplayMode: DisplayMode;
28
+ unauthenticatedDisplayMode: DisplayMode;
29
+ }
30
+ export interface ResolvedDisplayMode {
31
+ mode: DisplayMode;
32
+ resolvedFrom: ResolvedFrom;
33
+ }
34
+ export declare function resolveDisplayMode(customer: CustomerKind, overrides: DisplayModeOverrideLookup, settings: SettingsDefaults): ResolvedDisplayMode;
35
+ /**
36
+ * One category-scope override, with the three fields the chain ranks it by.
37
+ *
38
+ * `resolveDisplayMode` above receives the already-picked override; the ranking
39
+ * that picks it used to live inline in `PriceListService`, which is why the
40
+ * batched path could have ranked differently from the per-product one. It is
41
+ * here now, so both hand their candidates to the same comparator.
42
+ */
43
+ export interface DisplayModeCategoryCandidate {
44
+ mode: DisplayMode;
45
+ /** Ancestors above the category itself — deeper is more specific. */
46
+ depth: number;
47
+ sortOrder: number;
48
+ categoryId: string;
49
+ }
50
+ /** Most specific first: deepest, then `(sort_order asc, id asc)` as tie-break. */
51
+ export declare function pickCategoryOverride(candidates: readonly DisplayModeCategoryCandidate[]): DisplayModeCategoryCandidate | null;
52
+ export type DisplayModeDecision = {
53
+ source: 'product' | 'category' | 'organization';
54
+ mode: DisplayMode;
55
+ } | {
56
+ source: 'settings';
57
+ };
58
+ /**
59
+ * The chain's decision with the settings tier left **unread**.
60
+ *
61
+ * The eager `resolveDisplayMode` above wants both settings values in hand,
62
+ * which is one query per product on a listing page and two more on a cart line
63
+ * that an override already answered. This form names the settings step instead
64
+ * of taking its value, so the per-product path reads the pair only when it is
65
+ * reached and the batched path reads it once for the whole page.
66
+ *
67
+ * `organizationOverride` is the caller's answer to "is this a signed-in
68
+ * customer with an organization" — a guest passes `null`, exactly as the
69
+ * eager resolver's `customer === 'signed_in'` guard requires.
70
+ */
71
+ export declare function decideDisplayMode(inputs: {
72
+ productOverride: DisplayMode | null;
73
+ categoryCandidates: readonly DisplayModeCategoryCandidate[];
74
+ organizationOverride: DisplayMode | null;
75
+ }): DisplayModeDecision;
76
+ /** Which `pricing.*` settings key answers for this customer kind. */
77
+ export declare function settingsDisplayModeKey(customer: CustomerKind): 'default_display_mode' | 'unauthenticated_display_mode';
78
+ //# sourceMappingURL=display-mode-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"display-mode-resolver.d.ts","sourceRoot":"","sources":["../../../src/backend/services/display-mode-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAE9D;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,WAAW,CAAC;AAEjD,MAAM,MAAM,YAAY,GACpB,SAAS,GACT,UAAU,GACV,cAAc,GACd,kBAAkB,GAClB,0BAA0B,CAAC;AAE/B,MAAM,WAAW,yBAAyB;IACxC,0CAA0C;IAC1C,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;IACpC,6EAA6E;IAC7E,gBAAgB,EAAE,WAAW,GAAG,IAAI,CAAC;IACrC,kEAAkE;IAClE,oBAAoB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC1C;AAED,MAAM,WAAW,gBAAgB;IAC/B,kBAAkB,EAAE,WAAW,CAAC;IAChC,0BAA0B,EAAE,WAAW,CAAC;CACzC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,YAAY,CAAC;CAC5B;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,EACtB,SAAS,EAAE,yBAAyB,EACpC,QAAQ,EAAE,gBAAgB,GACzB,mBAAmB,CAiBrB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,4BAA4B;IAC3C,IAAI,EAAE,WAAW,CAAC;IAClB,qEAAqE;IACrE,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,kFAAkF;AAClF,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,SAAS,4BAA4B,EAAE,GAClD,4BAA4B,GAAG,IAAI,CAkBrC;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,cAAc,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GACtE;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,CAAC;AAE3B;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IACxC,eAAe,EAAE,WAAW,GAAG,IAAI,CAAC;IACpC,kBAAkB,EAAE,SAAS,4BAA4B,EAAE,CAAC;IAC5D,oBAAoB,EAAE,WAAW,GAAG,IAAI,CAAC;CAC1C,GAAG,mBAAmB,CAUtB;AAED,qEAAqE;AACrE,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,YAAY,GACrB,sBAAsB,GAAG,8BAA8B,CAEzD"}