@open-mercato/core 0.8.1-develop.7228.1.b7ea09d2d3 → 0.8.1-develop.7238.1.3abe669ea5

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 (173) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities/availability_policy/index.js +41 -0
  3. package/dist/generated/entities/availability_policy/index.js.map +7 -0
  4. package/dist/generated/entities.ids.generated.js +4 -0
  5. package/dist/generated/entities.ids.generated.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +21 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/attachments/data/entities.js +3 -3
  9. package/dist/modules/attachments/data/entities.js.map +2 -2
  10. package/dist/modules/availability/acl.js +16 -0
  11. package/dist/modules/availability/acl.js.map +7 -0
  12. package/dist/modules/availability/api/check/route.js +95 -0
  13. package/dist/modules/availability/api/check/route.js.map +7 -0
  14. package/dist/modules/availability/api/openapi.js +27 -0
  15. package/dist/modules/availability/api/openapi.js.map +7 -0
  16. package/dist/modules/availability/api/policies/resolve-preview/route.js +58 -0
  17. package/dist/modules/availability/api/policies/resolve-preview/route.js.map +7 -0
  18. package/dist/modules/availability/api/policies/route.js +188 -0
  19. package/dist/modules/availability/api/policies/route.js.map +7 -0
  20. package/dist/modules/availability/backend/availability/check/page.js +185 -0
  21. package/dist/modules/availability/backend/availability/check/page.js.map +7 -0
  22. package/dist/modules/availability/backend/availability/check/page.meta.js +16 -0
  23. package/dist/modules/availability/backend/availability/check/page.meta.js.map +7 -0
  24. package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js +79 -0
  25. package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js.map +7 -0
  26. package/dist/modules/availability/backend/availability/policies/ScopeFields.js +53 -0
  27. package/dist/modules/availability/backend/availability/policies/ScopeFields.js.map +7 -0
  28. package/dist/modules/availability/backend/availability/policies/[id]/page.js +152 -0
  29. package/dist/modules/availability/backend/availability/policies/[id]/page.js.map +7 -0
  30. package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js +12 -0
  31. package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js.map +7 -0
  32. package/dist/modules/availability/backend/availability/policies/create/page.js +94 -0
  33. package/dist/modules/availability/backend/availability/policies/create/page.js.map +7 -0
  34. package/dist/modules/availability/backend/availability/policies/create/page.meta.js +12 -0
  35. package/dist/modules/availability/backend/availability/policies/create/page.meta.js.map +7 -0
  36. package/dist/modules/availability/backend/availability/policies/formGroups.js +75 -0
  37. package/dist/modules/availability/backend/availability/policies/formGroups.js.map +7 -0
  38. package/dist/modules/availability/backend/availability/policies/page.js +176 -0
  39. package/dist/modules/availability/backend/availability/policies/page.js.map +7 -0
  40. package/dist/modules/availability/backend/availability/policies/page.meta.js +16 -0
  41. package/dist/modules/availability/backend/availability/policies/page.meta.js.map +7 -0
  42. package/dist/modules/availability/commands/policies.js +312 -0
  43. package/dist/modules/availability/commands/policies.js.map +7 -0
  44. package/dist/modules/availability/commands/scope.js +19 -0
  45. package/dist/modules/availability/commands/scope.js.map +7 -0
  46. package/dist/modules/availability/data/entities.js +95 -0
  47. package/dist/modules/availability/data/entities.js.map +7 -0
  48. package/dist/modules/availability/data/validators.js +53 -0
  49. package/dist/modules/availability/data/validators.js.map +7 -0
  50. package/dist/modules/availability/di.js +41 -0
  51. package/dist/modules/availability/di.js.map +7 -0
  52. package/dist/modules/availability/events.js +18 -0
  53. package/dist/modules/availability/events.js.map +7 -0
  54. package/dist/modules/availability/index.js +15 -0
  55. package/dist/modules/availability/index.js.map +7 -0
  56. package/dist/modules/availability/lib/policyResolution.js +155 -0
  57. package/dist/modules/availability/lib/policyResolution.js.map +7 -0
  58. package/dist/modules/availability/lib/tryResolve.js +11 -0
  59. package/dist/modules/availability/lib/tryResolve.js.map +7 -0
  60. package/dist/modules/availability/migrations/Migration20260922095844_availability.js +16 -0
  61. package/dist/modules/availability/migrations/Migration20260922095844_availability.js.map +7 -0
  62. package/dist/modules/availability/setup.js +12 -0
  63. package/dist/modules/availability/setup.js.map +7 -0
  64. package/dist/modules/entities/cli.js +33 -12
  65. package/dist/modules/entities/cli.js.map +2 -2
  66. package/dist/modules/inbox_ops/api/settings/route.js +7 -32
  67. package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
  68. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +2 -2
  69. package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
  70. package/dist/modules/inbox_ops/components/proposals/ActionCard.js +1 -1
  71. package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
  72. package/dist/modules/inbox_ops/lib/ensure-settings.js +47 -0
  73. package/dist/modules/inbox_ops/lib/ensure-settings.js.map +7 -0
  74. package/dist/modules/inbox_ops/lib/executionEngine.js.map +1 -1
  75. package/dist/modules/inbox_ops/lib/executionHelpers.js +0 -23
  76. package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
  77. package/dist/modules/inbox_ops/lib/payloadEnrichment.js +0 -3
  78. package/dist/modules/inbox_ops/lib/payloadEnrichment.js.map +2 -2
  79. package/dist/modules/inbox_ops/setup.js +2 -21
  80. package/dist/modules/inbox_ops/setup.js.map +2 -2
  81. package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +1 -1
  82. package/dist/modules/sales/components/documents/SalesDocumentForm.js +7 -0
  83. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  84. package/dist/modules/sales/data/entities.js +3 -1
  85. package/dist/modules/sales/data/entities.js.map +2 -2
  86. package/dist/modules/sales/inbox-actions.js +0 -12
  87. package/dist/modules/sales/inbox-actions.js.map +2 -2
  88. package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js +20 -0
  89. package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js.map +7 -0
  90. package/dist/modules/wms/di.js +3 -0
  91. package/dist/modules/wms/di.js.map +2 -2
  92. package/dist/modules/wms/lib/availabilityCache.js +85 -0
  93. package/dist/modules/wms/lib/availabilityCache.js.map +7 -0
  94. package/dist/modules/wms/lib/availabilityCalculation.js +178 -0
  95. package/dist/modules/wms/lib/availabilityCalculation.js.map +7 -0
  96. package/dist/modules/wms/lib/availabilityProvider.js +14 -0
  97. package/dist/modules/wms/lib/availabilityProvider.js.map +7 -0
  98. package/dist/modules/wms/lib/sqlInClause.js +9 -0
  99. package/dist/modules/wms/lib/sqlInClause.js.map +7 -0
  100. package/dist/modules/wms/lib/tryResolve.js +11 -0
  101. package/dist/modules/wms/lib/tryResolve.js.map +7 -0
  102. package/dist/modules/wms/subscribers/invalidate-availability-cache.js +16 -0
  103. package/dist/modules/wms/subscribers/invalidate-availability-cache.js.map +7 -0
  104. package/generated/entities/availability_policy/index.ts +19 -0
  105. package/generated/entities.ids.generated.ts +4 -0
  106. package/generated/entity-fields-registry.ts +21 -0
  107. package/package.json +7 -7
  108. package/src/modules/attachments/data/entities.ts +5 -3
  109. package/src/modules/auth/i18n/de.json +3 -0
  110. package/src/modules/auth/i18n/en.json +3 -0
  111. package/src/modules/auth/i18n/es.json +3 -0
  112. package/src/modules/auth/i18n/ko.json +3 -0
  113. package/src/modules/auth/i18n/pl.json +3 -0
  114. package/src/modules/availability/acl.ts +12 -0
  115. package/src/modules/availability/api/check/route.ts +114 -0
  116. package/src/modules/availability/api/openapi.ts +28 -0
  117. package/src/modules/availability/api/policies/resolve-preview/route.ts +67 -0
  118. package/src/modules/availability/api/policies/route.ts +224 -0
  119. package/src/modules/availability/backend/availability/check/page.meta.ts +12 -0
  120. package/src/modules/availability/backend/availability/check/page.tsx +223 -0
  121. package/src/modules/availability/backend/availability/policies/ResolutionPreviewPanel.tsx +106 -0
  122. package/src/modules/availability/backend/availability/policies/ScopeFields.tsx +55 -0
  123. package/src/modules/availability/backend/availability/policies/[id]/page.meta.ts +8 -0
  124. package/src/modules/availability/backend/availability/policies/[id]/page.tsx +182 -0
  125. package/src/modules/availability/backend/availability/policies/create/page.meta.ts +8 -0
  126. package/src/modules/availability/backend/availability/policies/create/page.tsx +105 -0
  127. package/src/modules/availability/backend/availability/policies/formGroups.ts +77 -0
  128. package/src/modules/availability/backend/availability/policies/page.meta.ts +12 -0
  129. package/src/modules/availability/backend/availability/policies/page.tsx +196 -0
  130. package/src/modules/availability/commands/policies.ts +358 -0
  131. package/src/modules/availability/commands/scope.ts +26 -0
  132. package/src/modules/availability/data/entities.ts +92 -0
  133. package/src/modules/availability/data/validators.ts +70 -0
  134. package/src/modules/availability/di.ts +54 -0
  135. package/src/modules/availability/events.ts +17 -0
  136. package/src/modules/availability/i18n/de.json +81 -0
  137. package/src/modules/availability/i18n/en.json +81 -0
  138. package/src/modules/availability/i18n/es.json +81 -0
  139. package/src/modules/availability/i18n/ko.json +81 -0
  140. package/src/modules/availability/i18n/pl.json +81 -0
  141. package/src/modules/availability/index.ts +13 -0
  142. package/src/modules/availability/lib/policyResolution.ts +264 -0
  143. package/src/modules/availability/lib/tryResolve.ts +8 -0
  144. package/src/modules/availability/migrations/.snapshot-open-mercato.json +375 -0
  145. package/src/modules/availability/migrations/Migration20260922095844_availability.ts +13 -0
  146. package/src/modules/availability/setup.ts +10 -0
  147. package/src/modules/entities/cli.ts +53 -13
  148. package/src/modules/inbox_ops/api/settings/route.ts +9 -32
  149. package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +2 -2
  150. package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +1 -1
  151. package/src/modules/inbox_ops/i18n/de.json +1 -1
  152. package/src/modules/inbox_ops/i18n/en.json +1 -1
  153. package/src/modules/inbox_ops/i18n/es.json +1 -1
  154. package/src/modules/inbox_ops/i18n/ko.json +1 -1
  155. package/src/modules/inbox_ops/i18n/pl.json +1 -1
  156. package/src/modules/inbox_ops/lib/ensure-settings.ts +77 -0
  157. package/src/modules/inbox_ops/lib/executionEngine.ts +1 -1
  158. package/src/modules/inbox_ops/lib/executionHelpers.ts +0 -27
  159. package/src/modules/inbox_ops/lib/payloadEnrichment.ts +1 -5
  160. package/src/modules/inbox_ops/setup.ts +2 -21
  161. package/src/modules/inbox_ops/subscribers/extractionWorker.ts +1 -1
  162. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +13 -0
  163. package/src/modules/sales/data/entities.ts +2 -0
  164. package/src/modules/sales/inbox-actions.ts +0 -20
  165. package/src/modules/sales/migrations/.snapshot-open-mercato.json +20 -0
  166. package/src/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.ts +74 -0
  167. package/src/modules/wms/di.ts +6 -0
  168. package/src/modules/wms/lib/availabilityCache.ts +136 -0
  169. package/src/modules/wms/lib/availabilityCalculation.ts +289 -0
  170. package/src/modules/wms/lib/availabilityProvider.ts +29 -0
  171. package/src/modules/wms/lib/sqlInClause.ts +21 -0
  172. package/src/modules/wms/lib/tryResolve.ts +8 -0
  173. package/src/modules/wms/subscribers/invalidate-availability-cache.ts +30 -0
@@ -0,0 +1,18 @@
1
+ import { createModuleEvents } from "@open-mercato/shared/modules/events";
2
+ const events = [
3
+ { id: "availability.policy.created", label: "Availability Policy Created", entity: "policy", category: "crud" },
4
+ { id: "availability.policy.updated", label: "Availability Policy Updated", entity: "policy", category: "crud" },
5
+ { id: "availability.policy.deleted", label: "Availability Policy Deleted", entity: "policy", category: "crud" }
6
+ ];
7
+ const eventsConfig = createModuleEvents({
8
+ moduleId: "availability",
9
+ events
10
+ });
11
+ const emitAvailabilityEvent = eventsConfig.emit;
12
+ var events_default = eventsConfig;
13
+ export {
14
+ events_default as default,
15
+ emitAvailabilityEvent,
16
+ eventsConfig
17
+ };
18
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/availability/events.ts"],
4
+ "sourcesContent": ["import { createModuleEvents } from '@open-mercato/shared/modules/events'\n\nconst events = [\n { id: 'availability.policy.created', label: 'Availability Policy Created', entity: 'policy', category: 'crud' },\n { id: 'availability.policy.updated', label: 'Availability Policy Updated', entity: 'policy', category: 'crud' },\n { id: 'availability.policy.deleted', label: 'Availability Policy Deleted', entity: 'policy', category: 'crud' },\n] as const\n\nexport const eventsConfig = createModuleEvents({\n moduleId: 'availability',\n events,\n})\n\nexport const emitAvailabilityEvent = eventsConfig.emit\nexport type AvailabilityEventId = typeof events[number]['id']\n\nexport default eventsConfig\n"],
5
+ "mappings": "AAAA,SAAS,0BAA0B;AAEnC,MAAM,SAAS;AAAA,EACb,EAAE,IAAI,+BAA+B,OAAO,+BAA+B,QAAQ,UAAU,UAAU,OAAO;AAAA,EAC9G,EAAE,IAAI,+BAA+B,OAAO,+BAA+B,QAAQ,UAAU,UAAU,OAAO;AAAA,EAC9G,EAAE,IAAI,+BAA+B,OAAO,+BAA+B,QAAQ,UAAU,UAAU,OAAO;AAChH;AAEO,MAAM,eAAe,mBAAmB;AAAA,EAC7C,UAAU;AAAA,EACV;AACF,CAAC;AAEM,MAAM,wBAAwB,aAAa;AAGlD,IAAO,iBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,15 @@
1
+ const metadata = {
2
+ name: "availability",
3
+ title: "Availability",
4
+ version: "0.1.0",
5
+ description: "Sell-policy layer (stock-managed, backorder, preorder, thresholds) on top of the shared availability contract.",
6
+ author: "Open Mercato Team",
7
+ license: "MIT",
8
+ ejectable: true
9
+ };
10
+ import { features } from "./acl.js";
11
+ export {
12
+ features,
13
+ metadata
14
+ };
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/availability/index.ts"],
4
+ "sourcesContent": ["import type { ModuleInfo } from '@open-mercato/shared/modules/registry'\n\nexport const metadata: ModuleInfo = {\n name: 'availability',\n title: 'Availability',\n version: '0.1.0',\n description: 'Sell-policy layer (stock-managed, backorder, preorder, thresholds) on top of the shared availability contract.',\n author: 'Open Mercato Team',\n license: 'MIT',\n ejectable: true,\n}\n\nexport { features } from './acl'\n"],
5
+ "mappings": "AAEO,MAAM,WAAuB;AAAA,EAClC,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,aAAa;AAAA,EACb,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,WAAW;AACb;AAEA,SAAS,gBAAgB;",
6
+ "names": []
7
+ }
@@ -0,0 +1,155 @@
1
+ import { AvailabilityPolicy } from "../data/entities.js";
2
+ import { tryResolve } from "./tryResolve.js";
3
+ const POLICY_CHAIN_LEVELS = [
4
+ "variant_store",
5
+ "variant",
6
+ "product_store",
7
+ "product",
8
+ "store_default",
9
+ "module_default"
10
+ ];
11
+ const NULLABLE_FIELDS = [
12
+ "backorderLeadTimeDays",
13
+ "preorderReleaseAt",
14
+ "lowStockThreshold",
15
+ "minOrderQuantity",
16
+ "maxOrderQuantity",
17
+ "quantityIncrement"
18
+ ];
19
+ const BOOLEAN_FIELDS = ["isStockManaged", "allowBackorder", "hideWhenOutOfStock", "isActive"];
20
+ function candidateFilters(scope) {
21
+ const storeId = scope.storeId ?? null;
22
+ const variantId = scope.variantId ?? null;
23
+ const filters = [];
24
+ if (variantId) {
25
+ filters.push({ variantId, storeId });
26
+ filters.push({ variantId, storeId: null });
27
+ }
28
+ filters.push({ productId: scope.productId, variantId: null, storeId });
29
+ filters.push({ productId: scope.productId, variantId: null, storeId: null });
30
+ filters.push({ productId: null, variantId: null, storeId });
31
+ if (storeId) filters.push({ productId: null, variantId: null, storeId: null });
32
+ return filters;
33
+ }
34
+ function matchChainRows(scope, pool) {
35
+ const storeId = scope.storeId ?? null;
36
+ const variantId = scope.variantId ?? null;
37
+ const findMatch = (predicate) => pool.find(predicate) ?? null;
38
+ const rows = [];
39
+ if (variantId) {
40
+ rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === storeId));
41
+ rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === null));
42
+ }
43
+ rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === storeId));
44
+ rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === null));
45
+ rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === storeId));
46
+ if (storeId) rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === null));
47
+ return rows;
48
+ }
49
+ function resolveFromRows(rows, isStockManagedModuleDefault) {
50
+ const firstRow = rows.find((row) => row != null) ?? null;
51
+ const result = {};
52
+ for (const field of BOOLEAN_FIELDS) {
53
+ if (firstRow) {
54
+ ;
55
+ result[field] = { value: firstRow[field], policySourceId: firstRow.id };
56
+ } else {
57
+ ;
58
+ result[field] = { value: false, policySourceId: null };
59
+ }
60
+ }
61
+ if (!firstRow) result.isStockManaged = { value: isStockManagedModuleDefault, policySourceId: null };
62
+ if (!firstRow) result.isActive = { value: true, policySourceId: null };
63
+ for (const field of NULLABLE_FIELDS) {
64
+ let resolved = { value: null, policySourceId: null };
65
+ for (const row of rows) {
66
+ if (row && row[field] != null) {
67
+ resolved = { value: row[field], policySourceId: row.id };
68
+ break;
69
+ }
70
+ }
71
+ ;
72
+ result[field] = resolved;
73
+ }
74
+ return result;
75
+ }
76
+ async function resolveIsStockManagedModuleDefault(em, container, scope) {
77
+ const ProductInventoryProfile = tryResolve(container, "ProductInventoryProfile");
78
+ if (!ProductInventoryProfile) return false;
79
+ const baseFilter = {
80
+ tenantId: scope.tenantId,
81
+ organizationId: scope.organizationId,
82
+ catalogProductId: scope.productId,
83
+ deletedAt: null
84
+ };
85
+ if (scope.variantId) {
86
+ const variantRow = await em.findOne(ProductInventoryProfile, { ...baseFilter, catalogVariantId: scope.variantId });
87
+ if (variantRow) return true;
88
+ }
89
+ const productRow = await em.findOne(ProductInventoryProfile, { ...baseFilter, catalogVariantId: null });
90
+ return !!productRow;
91
+ }
92
+ async function resolveIsStockManagedModuleDefaultMany(em, container, scopes) {
93
+ const ProductInventoryProfile = tryResolve(container, "ProductInventoryProfile");
94
+ const results = /* @__PURE__ */ new Map();
95
+ if (!ProductInventoryProfile || scopes.length === 0) return results;
96
+ const first = scopes[0];
97
+ const productIds = Array.from(new Set(scopes.map((s) => s.productId)));
98
+ const rows = await em.find(ProductInventoryProfile, {
99
+ tenantId: first.tenantId,
100
+ organizationId: first.organizationId,
101
+ deletedAt: null,
102
+ catalogProductId: { $in: productIds }
103
+ });
104
+ const exists = new Set(rows.map((row) => `${row.catalogProductId}:${row.catalogVariantId ?? ""}`));
105
+ for (const scope of scopes) {
106
+ const variantHit = scope.variantId ? exists.has(`${scope.productId}:${scope.variantId}`) : false;
107
+ const productHit = exists.has(`${scope.productId}:`);
108
+ results.set(scopeKey(scope), variantHit || productHit);
109
+ }
110
+ return results;
111
+ }
112
+ function scopeKey(scope) {
113
+ return `${scope.productId}:${scope.variantId ?? ""}:${scope.storeId ?? ""}`;
114
+ }
115
+ function createPolicyResolutionService(container) {
116
+ async function resolveMany(em, scopes) {
117
+ if (scopes.length === 0) return [];
118
+ const first = scopes[0];
119
+ const seen = /* @__PURE__ */ new Set();
120
+ const orFilters = [];
121
+ for (const scope of scopes) {
122
+ for (const filter of candidateFilters(scope)) {
123
+ const key = JSON.stringify(filter);
124
+ if (seen.has(key)) continue;
125
+ seen.add(key);
126
+ orFilters.push(filter);
127
+ }
128
+ }
129
+ const pool = orFilters.length ? await em.find(AvailabilityPolicy, {
130
+ tenantId: first.tenantId,
131
+ organizationId: first.organizationId,
132
+ deletedAt: null,
133
+ $or: orFilters
134
+ }) : [];
135
+ const rowsByScope = scopes.map((scope) => matchChainRows(scope, pool));
136
+ const needsModuleDefault = scopes.filter((scope, index) => !rowsByScope[index].some((row) => row != null));
137
+ const moduleDefaults = await resolveIsStockManagedModuleDefaultMany(em, container, needsModuleDefault);
138
+ return scopes.map(
139
+ (scope, index) => resolveFromRows(rowsByScope[index], moduleDefaults.get(scopeKey(scope)) ?? false)
140
+ );
141
+ }
142
+ return {
143
+ async resolve(em, scope) {
144
+ const [result] = await resolveMany(em, [scope]);
145
+ return result;
146
+ },
147
+ resolveMany
148
+ };
149
+ }
150
+ export {
151
+ POLICY_CHAIN_LEVELS,
152
+ createPolicyResolutionService,
153
+ resolveIsStockManagedModuleDefault
154
+ };
155
+ //# sourceMappingURL=policyResolution.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/availability/lib/policyResolution.ts"],
4
+ "sourcesContent": ["/**\n * `AvailabilityPolicy` resolution chain \u2014 six levels, most specific wins.\n *\n * @see .ai/specs/2026-08-14-availability-contract.md \u00A75.2\n *\n * Resolution design (documented here because \u00A75.2 does not fully spell it\n * out \u2014 see PLAN.md \u00A7 Key design decisions):\n *\n * - Boolean columns (`isStockManaged`, `allowBackorder`, `hideWhenOutOfStock`,\n * `isActive`) are NOT NULL, so a matched row's own value is always\n * concrete \u2014 there is no \"unset\" to fall through. The single most specific\n * EXISTING row decides every boolean field.\n * - Nullable columns (`backorderLeadTimeDays`, `preorderReleaseAt`,\n * `lowStockThreshold`, `minOrderQuantity`, `maxOrderQuantity`,\n * `quantityIncrement`) genuinely cascade: a matched row that leaves the\n * field `null` defers to the next-less-specific row, per US-A2's \"which\n * level currently decides each field\" requirement.\n *\n * `resolveMany()` exists so `wms`'s batched `check()` (\u00A74.2/R4 \u2014 one policy\n * lookup regardless of item count) can soft-resolve this service and still\n * issue exactly one query for the whole batch, rather than one per item.\n * `resolve()` is `resolveMany([scope])[0]`, so single-item callers (the admin\n * check tool, the resolve-preview endpoint) are unaffected.\n */\n\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { AvailabilityPolicy } from '../data/entities'\nimport { tryResolve } from './tryResolve'\n\nexport type PolicyResolutionScope = {\n tenantId: string\n organizationId: string\n storeId?: string | null\n productId: string\n variantId?: string | null\n}\n\nexport type ResolvedField<T> = {\n value: T\n /** The `AvailabilityPolicy` row id that decided, or `null` for the module default. */\n policySourceId: string | null\n}\n\nexport type ResolvedAvailabilityPolicy = {\n isStockManaged: ResolvedField<boolean>\n allowBackorder: ResolvedField<boolean>\n backorderLeadTimeDays: ResolvedField<number | null>\n preorderReleaseAt: ResolvedField<Date | null>\n lowStockThreshold: ResolvedField<number | null>\n minOrderQuantity: ResolvedField<number | null>\n maxOrderQuantity: ResolvedField<number | null>\n quantityIncrement: ResolvedField<number | null>\n hideWhenOutOfStock: ResolvedField<boolean>\n isActive: ResolvedField<boolean>\n}\n\n/** The six chain levels, most specific first. `null` = \"no row matched this level\". */\nexport type PolicyChainLevel =\n | 'variant_store'\n | 'variant'\n | 'product_store'\n | 'product'\n | 'store_default'\n | 'module_default'\n\nexport const POLICY_CHAIN_LEVELS: PolicyChainLevel[] = [\n 'variant_store',\n 'variant',\n 'product_store',\n 'product',\n 'store_default',\n 'module_default',\n]\n\nconst NULLABLE_FIELDS = [\n 'backorderLeadTimeDays',\n 'preorderReleaseAt',\n 'lowStockThreshold',\n 'minOrderQuantity',\n 'maxOrderQuantity',\n 'quantityIncrement',\n] as const\n\nconst BOOLEAN_FIELDS = ['isStockManaged', 'allowBackorder', 'hideWhenOutOfStock', 'isActive'] as const\n\nfunction candidateFilters(scope: PolicyResolutionScope): Array<Record<string, unknown>> {\n const storeId = scope.storeId ?? null\n const variantId = scope.variantId ?? null\n const filters: Array<Record<string, unknown>> = []\n if (variantId) {\n filters.push({ variantId, storeId })\n filters.push({ variantId, storeId: null })\n }\n filters.push({ productId: scope.productId, variantId: null, storeId })\n filters.push({ productId: scope.productId, variantId: null, storeId: null })\n filters.push({ productId: null, variantId: null, storeId })\n if (storeId) filters.push({ productId: null, variantId: null, storeId: null })\n return filters\n}\n\n/** Picks the (up to) five candidate rows for one scope out of a shared, already-loaded candidate pool. */\nfunction matchChainRows(scope: PolicyResolutionScope, pool: AvailabilityPolicy[]): Array<AvailabilityPolicy | null> {\n const storeId = scope.storeId ?? null\n const variantId = scope.variantId ?? null\n const findMatch = (predicate: (row: AvailabilityPolicy) => boolean) => pool.find(predicate) ?? null\n\n const rows: Array<AvailabilityPolicy | null> = []\n if (variantId) {\n rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === storeId))\n rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === null))\n }\n rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === storeId))\n rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === null))\n rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === storeId))\n // The store-default level cascades store-specific \u2192 all-stores, same as\n // the variant/product levels above it (\"store_id null = applies to all\n // stores in the organization\", \u00A75.1) \u2014 otherwise an org-wide default row\n // is invisible to any query that names a concrete store.\n if (storeId) rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === null))\n return rows\n}\n\nfunction resolveFromRows(\n rows: Array<AvailabilityPolicy | null>,\n isStockManagedModuleDefault: boolean,\n): ResolvedAvailabilityPolicy {\n const firstRow = rows.find((row): row is AvailabilityPolicy => row != null) ?? null\n const result = {} as ResolvedAvailabilityPolicy\n\n for (const field of BOOLEAN_FIELDS) {\n if (firstRow) {\n ;(result as any)[field] = { value: (firstRow as any)[field], policySourceId: firstRow.id }\n } else {\n ;(result as any)[field] = { value: false, policySourceId: null }\n }\n }\n\n // is_stock_managed's module default is dynamic (wms + profile existence), not a flat `false`.\n if (!firstRow) result.isStockManaged = { value: isStockManagedModuleDefault, policySourceId: null }\n // is_active's module default is \"active\" (nothing to deactivate).\n if (!firstRow) result.isActive = { value: true, policySourceId: null }\n\n for (const field of NULLABLE_FIELDS) {\n let resolved: ResolvedField<unknown> = { value: null, policySourceId: null }\n for (const row of rows) {\n if (row && (row as any)[field] != null) {\n resolved = { value: (row as any)[field], policySourceId: row.id }\n break\n }\n }\n ;(result as any)[field] = resolved\n }\n\n return result\n}\n\n/** `is_stock_managed`'s module default: true when `wms` is enabled and a `ProductInventoryProfile` exists for the item. */\nexport async function resolveIsStockManagedModuleDefault(\n em: EntityManager,\n container: { resolve: <T = unknown>(name: string) => T },\n scope: { tenantId: string; organizationId: string; productId: string; variantId?: string | null },\n): Promise<boolean> {\n const ProductInventoryProfile = tryResolve<new () => unknown>(container, 'ProductInventoryProfile')\n if (!ProductInventoryProfile) return false\n\n const baseFilter = {\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n catalogProductId: scope.productId,\n deletedAt: null,\n }\n\n if (scope.variantId) {\n const variantRow = await em.findOne(ProductInventoryProfile as any, { ...baseFilter, catalogVariantId: scope.variantId })\n if (variantRow) return true\n }\n\n const productRow = await em.findOne(ProductInventoryProfile as any, { ...baseFilter, catalogVariantId: null })\n return !!productRow\n}\n\n/** Batched module-default check for `resolveMany` \u2014 one query for every scope that might need it. */\nasync function resolveIsStockManagedModuleDefaultMany(\n em: EntityManager,\n container: { resolve: <T = unknown>(name: string) => T },\n scopes: PolicyResolutionScope[],\n): Promise<Map<string, boolean>> {\n const ProductInventoryProfile = tryResolve<new () => unknown>(container, 'ProductInventoryProfile')\n const results = new Map<string, boolean>()\n if (!ProductInventoryProfile || scopes.length === 0) return results\n\n const first = scopes[0]\n const productIds = Array.from(new Set(scopes.map((s) => s.productId)))\n const rows = (await em.find(ProductInventoryProfile as any, {\n tenantId: first.tenantId,\n organizationId: first.organizationId,\n deletedAt: null,\n catalogProductId: { $in: productIds },\n } as any)) as Array<{ catalogProductId: string; catalogVariantId: string | null }>\n\n const exists = new Set(rows.map((row) => `${row.catalogProductId}:${row.catalogVariantId ?? ''}`))\n for (const scope of scopes) {\n const variantHit = scope.variantId ? exists.has(`${scope.productId}:${scope.variantId}`) : false\n const productHit = exists.has(`${scope.productId}:`)\n results.set(scopeKey(scope), variantHit || productHit)\n }\n return results\n}\n\nfunction scopeKey(scope: PolicyResolutionScope): string {\n return `${scope.productId}:${scope.variantId ?? ''}:${scope.storeId ?? ''}`\n}\n\nexport interface PolicyResolutionService {\n resolve(em: EntityManager, scope: PolicyResolutionScope): Promise<ResolvedAvailabilityPolicy>\n resolveMany(em: EntityManager, scopes: PolicyResolutionScope[]): Promise<ResolvedAvailabilityPolicy[]>\n}\n\nexport function createPolicyResolutionService(container: {\n resolve: <T = unknown>(name: string) => T\n}): PolicyResolutionService {\n async function resolveMany(em: EntityManager, scopes: PolicyResolutionScope[]): Promise<ResolvedAvailabilityPolicy[]> {\n if (scopes.length === 0) return []\n\n const first = scopes[0]\n const seen = new Set<string>()\n const orFilters: Array<Record<string, unknown>> = []\n for (const scope of scopes) {\n for (const filter of candidateFilters(scope)) {\n const key = JSON.stringify(filter)\n if (seen.has(key)) continue\n seen.add(key)\n orFilters.push(filter)\n }\n }\n\n const pool = orFilters.length\n ? await em.find(AvailabilityPolicy, {\n tenantId: first.tenantId,\n organizationId: first.organizationId,\n deletedAt: null,\n $or: orFilters as any,\n })\n : []\n\n // Only scopes that resolve to no row at all need the dynamic is_stock_managed\n // module default \u2014 batch just those.\n const rowsByScope = scopes.map((scope) => matchChainRows(scope, pool))\n const needsModuleDefault = scopes.filter((scope, index) => !rowsByScope[index].some((row) => row != null))\n const moduleDefaults = await resolveIsStockManagedModuleDefaultMany(em, container, needsModuleDefault)\n\n return scopes.map((scope, index) =>\n resolveFromRows(rowsByScope[index], moduleDefaults.get(scopeKey(scope)) ?? false),\n )\n }\n\n return {\n async resolve(em, scope) {\n const [result] = await resolveMany(em, [scope])\n return result\n },\n resolveMany,\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,11 @@
1
+ function tryResolve(resolver, name) {
2
+ try {
3
+ return resolver.resolve(name);
4
+ } catch {
5
+ return void 0;
6
+ }
7
+ }
8
+ export {
9
+ tryResolve
10
+ };
11
+ //# sourceMappingURL=tryResolve.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/availability/lib/tryResolve.ts"],
4
+ "sourcesContent": ["/** Per-module local soft-resolve helper \u2014 see `packages/core/AGENTS.md` \u00A7 Cross-Module Coupling. */\nexport function tryResolve<T>(resolver: { resolve: <R = unknown>(name: string) => R }, name: string): T | undefined {\n try {\n return resolver.resolve<T>(name)\n } catch {\n return undefined\n }\n}\n"],
5
+ "mappings": "AACO,SAAS,WAAc,UAAyD,MAA6B;AAClH,MAAI;AACF,WAAO,SAAS,QAAW,IAAI;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,16 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20260922095844_availability extends Migration {
3
+ constructor() {
4
+ super(...arguments);
5
+ this.name = "Migration20260922095844";
6
+ }
7
+ up() {
8
+ this.addSql(`create table "availability_policies" ("id" uuid not null default gen_random_uuid(), "organization_id" uuid not null, "tenant_id" uuid not null, "store_id" uuid null, "product_id" uuid null, "variant_id" uuid null, "is_stock_managed" boolean not null default false, "allow_backorder" boolean not null default false, "backorder_lead_time_days" int null, "preorder_release_at" timestamptz null, "low_stock_threshold" int null, "min_order_quantity" int null, "max_order_quantity" int null, "quantity_increment" int null, "hide_when_out_of_stock" boolean not null default false, "is_active" boolean not null default true, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, primary key ("id"));`);
9
+ this.addSql(`create unique index "availability_policies_scope_target_unique" on "availability_policies" ("tenant_id", "organization_id", "store_id", "product_id", "variant_id") nulls not distinct where deleted_at is null;`);
10
+ this.addSql(`create index "availability_policies_scope_idx" on "availability_policies" ("tenant_id", "organization_id");`);
11
+ }
12
+ }
13
+ export {
14
+ Migration20260922095844_availability
15
+ };
16
+ //# sourceMappingURL=Migration20260922095844_availability.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/availability/migrations/Migration20260922095844_availability.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations';\n\nexport class Migration20260922095844_availability extends Migration {\n\n override name = 'Migration20260922095844';\n\n override up(): void | Promise<void> {\n this.addSql(`create table \"availability_policies\" (\"id\" uuid not null default gen_random_uuid(), \"organization_id\" uuid not null, \"tenant_id\" uuid not null, \"store_id\" uuid null, \"product_id\" uuid null, \"variant_id\" uuid null, \"is_stock_managed\" boolean not null default false, \"allow_backorder\" boolean not null default false, \"backorder_lead_time_days\" int null, \"preorder_release_at\" timestamptz null, \"low_stock_threshold\" int null, \"min_order_quantity\" int null, \"max_order_quantity\" int null, \"quantity_increment\" int null, \"hide_when_out_of_stock\" boolean not null default false, \"is_active\" boolean not null default true, \"created_at\" timestamptz not null, \"updated_at\" timestamptz not null, \"deleted_at\" timestamptz null, primary key (\"id\"));`);\n this.addSql(`create unique index \"availability_policies_scope_target_unique\" on \"availability_policies\" (\"tenant_id\", \"organization_id\", \"store_id\", \"product_id\", \"variant_id\") nulls not distinct where deleted_at is null;`);\n this.addSql(`create index \"availability_policies_scope_idx\" on \"availability_policies\" (\"tenant_id\", \"organization_id\");`);\n }\n\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,6CAA6C,UAAU;AAAA,EAA7D;AAAA;AAEL,SAAS,OAAO;AAAA;AAAA,EAEP,KAA2B;AAClC,SAAK,OAAO,ouBAAouB;AAChvB,SAAK,OAAO,kNAAkN;AAC9N,SAAK,OAAO,6GAA6G;AAAA,EAC3H;AAEF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,12 @@
1
+ const setup = {
2
+ defaultRoleFeatures: {
3
+ admin: ["availability.*"],
4
+ employee: ["availability.policies.view"]
5
+ }
6
+ };
7
+ var setup_default = setup;
8
+ export {
9
+ setup_default as default,
10
+ setup
11
+ };
12
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/modules/availability/setup.ts"],
4
+ "sourcesContent": ["import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'\n\nexport const setup: ModuleSetupConfig = {\n defaultRoleFeatures: {\n admin: ['availability.*'],\n employee: ['availability.policies.view'],\n },\n}\n\nexport default setup\n"],
5
+ "mappings": "AAEO,MAAM,QAA2B;AAAA,EACtC,qBAAqB;AAAA,IACnB,OAAO,CAAC,gBAAgB;AAAA,IACxB,UAAU,CAAC,4BAA4B;AAAA,EACzC;AACF;AAEA,IAAO,gBAAQ;",
6
+ "names": []
7
+ }
@@ -14,6 +14,7 @@ import { createKmsService } from "@open-mercato/shared/lib/encryption/kms";
14
14
  import {
15
15
  decryptWithAesGcm,
16
16
  decryptWithAesGcmStrict,
17
+ isEncryptedPayloadShape,
17
18
  TenantDataEncryptionError,
18
19
  TenantDataEncryptionErrorCode
19
20
  } from "@open-mercato/shared/lib/encryption/aes";
@@ -381,11 +382,6 @@ function resolveMapMeta(map, metaByEntityId, warn = () => {
381
382
  if (!tenantId) return null;
382
383
  return { entityId, meta, fields, tenantId };
383
384
  }
384
- function isEncryptedPayload(value) {
385
- if (typeof value !== "string") return false;
386
- const parts = value.split(":");
387
- return parts.length === 4 && parts[3] === "v1";
388
- }
389
385
  function formatValueForColumn(prop, value) {
390
386
  if (value === null || value === void 0) return value;
391
387
  const types = Array.isArray(prop?.columnTypes) ? prop.columnTypes : [];
@@ -474,6 +470,7 @@ const rotateEncryptionKey = {
474
470
  return scopes;
475
471
  };
476
472
  const oldDekCache = /* @__PURE__ */ new Map();
473
+ const unrecoverableRows = [];
477
474
  const dekAvailability = /* @__PURE__ */ new Map();
478
475
  const hasExistingDek = async (tenantId) => {
479
476
  const cached = dekAvailability.get(tenantId);
@@ -510,6 +507,7 @@ const rotateEncryptionKey = {
510
507
  const list = Array.isArray(rows) ? rows : [];
511
508
  if (!list.length) return 0;
512
509
  const dekAvailable = dryRun ? await hasExistingDek(scope.tenantId) : true;
510
+ const currentDek = rotate && oldKms ? await encryptionService.getDek(scope.tenantId) : null;
513
511
  let updated = 0;
514
512
  for (const row of list) {
515
513
  const payload = {};
@@ -518,7 +516,7 @@ const rotateEncryptionKey = {
518
516
  const col = resolved?.columnName;
519
517
  if (!col) continue;
520
518
  const rawValue = row[col];
521
- if (rotate && !isEncryptedPayload(rawValue)) {
519
+ if (isEncryptedPayloadShape(rawValue) ? !rotate : rotate) {
522
520
  continue;
523
521
  }
524
522
  payload[rule.field] = rawValue;
@@ -539,10 +537,19 @@ const rotateEncryptionKey = {
539
537
  }
540
538
  for (const rule of fields) {
541
539
  const value = payload[rule.field];
542
- if (typeof value !== "string" || !isEncryptedPayload(value)) continue;
540
+ if (typeof value !== "string" || !isEncryptedPayloadShape(value)) continue;
543
541
  const decrypted = decryptWithOldKey(value, oldDek);
544
- if (decrypted === null) continue;
545
- payload[rule.field] = parseDecryptedFieldValue(decrypted);
542
+ if (decrypted !== null) {
543
+ payload[rule.field] = parseDecryptedFieldValue(decrypted);
544
+ continue;
545
+ }
546
+ if (currentDek && decryptWithAesGcm(value, currentDek.key) !== null) continue;
547
+ delete payload[rule.field];
548
+ if (rule.hashField) delete payload[rule.hashField];
549
+ unrecoverableRows.push({ entityId, field: rule.field, rowId: row[pk] });
550
+ console.warn(
551
+ `Skipping ${entityId}.${rule.field} for row ${row[pk]}: its ciphertext opens under neither --old-key nor the current tenant key. Re-run with the key that sealed it.`
552
+ );
546
553
  }
547
554
  }
548
555
  if (!dekAvailable) {
@@ -551,7 +558,7 @@ const rotateEncryptionKey = {
551
558
  if (!col) return false;
552
559
  const value = row[col];
553
560
  if (value === null || value === void 0) return false;
554
- return rotate ? isEncryptedPayload(value) : !isEncryptedPayload(value);
561
+ return rotate ? isEncryptedPayloadShape(value) : !isEncryptedPayloadShape(value);
555
562
  });
556
563
  if (wouldChange) updated += 1;
557
564
  continue;
@@ -570,7 +577,7 @@ const rotateEncryptionKey = {
570
577
  if (!col) continue;
571
578
  const nextValue = encrypted[rule.field];
572
579
  if (nextValue !== void 0 && nextValue !== row[col]) {
573
- if (!rotate && isEncryptedPayload(row[col])) continue;
580
+ if (!rotate && isEncryptedPayloadShape(row[col])) continue;
574
581
  updates[col] = formatValueForColumn(resolved?.prop, nextValue);
575
582
  }
576
583
  if (rule.hashField) {
@@ -615,6 +622,19 @@ const rotateEncryptionKey = {
615
622
  } else {
616
623
  console.log("All mapped entity fields already encrypted for the selected scope.");
617
624
  }
625
+ if (unrecoverableRows.length) {
626
+ const maxListed = 20;
627
+ console.log(
628
+ `
629
+ \u26A0\uFE0F ${unrecoverableRows.length} field(s) could not be rotated \u2014 their ciphertext opens under neither --old-key nor the current tenant key, so they were left untouched. See "Key management" in apps/docs/docs/architecture/data-encryption.mdx for how to resolve these manually.`
630
+ );
631
+ for (const row of unrecoverableRows.slice(0, maxListed)) {
632
+ console.log(` - ${row.entityId}.${row.field} row=${String(row.rowId)}`);
633
+ }
634
+ if (unrecoverableRows.length > maxListed) {
635
+ console.log(` ... and ${unrecoverableRows.length - maxListed} more`);
636
+ }
637
+ }
618
638
  }
619
639
  };
620
640
  const decryptDatabase = {
@@ -1044,8 +1064,9 @@ const backfillSystemEncryption = {
1044
1064
  const resolved = resolveProperty(meta, rule.field);
1045
1065
  if (!resolved.columnName) continue;
1046
1066
  const rawValue = row[resolved.columnName];
1067
+ if (rawValue !== null && rawValue !== void 0 && isEncryptedPayloadShape(rawValue)) continue;
1047
1068
  payload[rule.field] = rawValue;
1048
- if (rawValue !== null && rawValue !== void 0 && !isEncryptedPayload(rawValue)) hasPlaintext = true;
1069
+ if (rawValue !== null && rawValue !== void 0) hasPlaintext = true;
1049
1070
  if (rule.hashField) {
1050
1071
  const resolvedHash = resolveProperty(meta, rule.hashField);
1051
1072
  if (resolvedHash.columnName) payload[rule.hashField] = row[resolvedHash.columnName];