@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.
- package/.turbo/turbo-build.log +2 -2
- package/dist/generated/entities/availability_policy/index.js +41 -0
- package/dist/generated/entities/availability_policy/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +4 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +21 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -3
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/availability/acl.js +16 -0
- package/dist/modules/availability/acl.js.map +7 -0
- package/dist/modules/availability/api/check/route.js +95 -0
- package/dist/modules/availability/api/check/route.js.map +7 -0
- package/dist/modules/availability/api/openapi.js +27 -0
- package/dist/modules/availability/api/openapi.js.map +7 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js +58 -0
- package/dist/modules/availability/api/policies/resolve-preview/route.js.map +7 -0
- package/dist/modules/availability/api/policies/route.js +188 -0
- package/dist/modules/availability/api/policies/route.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.js +185 -0
- package/dist/modules/availability/backend/availability/check/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/check/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js +79 -0
- package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js +53 -0
- package/dist/modules/availability/backend/availability/policies/ScopeFields.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js +152 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js +94 -0
- package/dist/modules/availability/backend/availability/policies/create/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js +12 -0
- package/dist/modules/availability/backend/availability/policies/create/page.meta.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js +75 -0
- package/dist/modules/availability/backend/availability/policies/formGroups.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.js +176 -0
- package/dist/modules/availability/backend/availability/policies/page.js.map +7 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js +16 -0
- package/dist/modules/availability/backend/availability/policies/page.meta.js.map +7 -0
- package/dist/modules/availability/commands/policies.js +312 -0
- package/dist/modules/availability/commands/policies.js.map +7 -0
- package/dist/modules/availability/commands/scope.js +19 -0
- package/dist/modules/availability/commands/scope.js.map +7 -0
- package/dist/modules/availability/data/entities.js +95 -0
- package/dist/modules/availability/data/entities.js.map +7 -0
- package/dist/modules/availability/data/validators.js +53 -0
- package/dist/modules/availability/data/validators.js.map +7 -0
- package/dist/modules/availability/di.js +41 -0
- package/dist/modules/availability/di.js.map +7 -0
- package/dist/modules/availability/events.js +18 -0
- package/dist/modules/availability/events.js.map +7 -0
- package/dist/modules/availability/index.js +15 -0
- package/dist/modules/availability/index.js.map +7 -0
- package/dist/modules/availability/lib/policyResolution.js +155 -0
- package/dist/modules/availability/lib/policyResolution.js.map +7 -0
- package/dist/modules/availability/lib/tryResolve.js +11 -0
- package/dist/modules/availability/lib/tryResolve.js.map +7 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js +16 -0
- package/dist/modules/availability/migrations/Migration20260922095844_availability.js.map +7 -0
- package/dist/modules/availability/setup.js +12 -0
- package/dist/modules/availability/setup.js.map +7 -0
- package/dist/modules/entities/cli.js +33 -12
- package/dist/modules/entities/cli.js.map +2 -2
- package/dist/modules/inbox_ops/api/settings/route.js +7 -32
- package/dist/modules/inbox_ops/api/settings/route.js.map +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js +2 -2
- package/dist/modules/inbox_ops/backend/inbox-ops/settings/page.js.map +2 -2
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js +1 -1
- package/dist/modules/inbox_ops/components/proposals/ActionCard.js.map +2 -2
- package/dist/modules/inbox_ops/lib/ensure-settings.js +47 -0
- package/dist/modules/inbox_ops/lib/ensure-settings.js.map +7 -0
- package/dist/modules/inbox_ops/lib/executionEngine.js.map +1 -1
- package/dist/modules/inbox_ops/lib/executionHelpers.js +0 -23
- package/dist/modules/inbox_ops/lib/executionHelpers.js.map +2 -2
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js +0 -3
- package/dist/modules/inbox_ops/lib/payloadEnrichment.js.map +2 -2
- package/dist/modules/inbox_ops/setup.js +2 -21
- package/dist/modules/inbox_ops/setup.js.map +2 -2
- package/dist/modules/inbox_ops/subscribers/extractionWorker.js.map +1 -1
- package/dist/modules/sales/components/documents/SalesDocumentForm.js +7 -0
- package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
- package/dist/modules/sales/data/entities.js +3 -1
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/inbox-actions.js +0 -12
- package/dist/modules/sales/inbox-actions.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js +20 -0
- package/dist/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.js.map +7 -0
- package/dist/modules/wms/di.js +3 -0
- package/dist/modules/wms/di.js.map +2 -2
- package/dist/modules/wms/lib/availabilityCache.js +85 -0
- package/dist/modules/wms/lib/availabilityCache.js.map +7 -0
- package/dist/modules/wms/lib/availabilityCalculation.js +178 -0
- package/dist/modules/wms/lib/availabilityCalculation.js.map +7 -0
- package/dist/modules/wms/lib/availabilityProvider.js +14 -0
- package/dist/modules/wms/lib/availabilityProvider.js.map +7 -0
- package/dist/modules/wms/lib/sqlInClause.js +9 -0
- package/dist/modules/wms/lib/sqlInClause.js.map +7 -0
- package/dist/modules/wms/lib/tryResolve.js +11 -0
- package/dist/modules/wms/lib/tryResolve.js.map +7 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js +16 -0
- package/dist/modules/wms/subscribers/invalidate-availability-cache.js.map +7 -0
- package/generated/entities/availability_policy/index.ts +19 -0
- package/generated/entities.ids.generated.ts +4 -0
- package/generated/entity-fields-registry.ts +21 -0
- package/package.json +7 -7
- package/src/modules/attachments/data/entities.ts +5 -3
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/availability/acl.ts +12 -0
- package/src/modules/availability/api/check/route.ts +114 -0
- package/src/modules/availability/api/openapi.ts +28 -0
- package/src/modules/availability/api/policies/resolve-preview/route.ts +67 -0
- package/src/modules/availability/api/policies/route.ts +224 -0
- package/src/modules/availability/backend/availability/check/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/check/page.tsx +223 -0
- package/src/modules/availability/backend/availability/policies/ResolutionPreviewPanel.tsx +106 -0
- package/src/modules/availability/backend/availability/policies/ScopeFields.tsx +55 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/[id]/page.tsx +182 -0
- package/src/modules/availability/backend/availability/policies/create/page.meta.ts +8 -0
- package/src/modules/availability/backend/availability/policies/create/page.tsx +105 -0
- package/src/modules/availability/backend/availability/policies/formGroups.ts +77 -0
- package/src/modules/availability/backend/availability/policies/page.meta.ts +12 -0
- package/src/modules/availability/backend/availability/policies/page.tsx +196 -0
- package/src/modules/availability/commands/policies.ts +358 -0
- package/src/modules/availability/commands/scope.ts +26 -0
- package/src/modules/availability/data/entities.ts +92 -0
- package/src/modules/availability/data/validators.ts +70 -0
- package/src/modules/availability/di.ts +54 -0
- package/src/modules/availability/events.ts +17 -0
- package/src/modules/availability/i18n/de.json +81 -0
- package/src/modules/availability/i18n/en.json +81 -0
- package/src/modules/availability/i18n/es.json +81 -0
- package/src/modules/availability/i18n/ko.json +81 -0
- package/src/modules/availability/i18n/pl.json +81 -0
- package/src/modules/availability/index.ts +13 -0
- package/src/modules/availability/lib/policyResolution.ts +264 -0
- package/src/modules/availability/lib/tryResolve.ts +8 -0
- package/src/modules/availability/migrations/.snapshot-open-mercato.json +375 -0
- package/src/modules/availability/migrations/Migration20260922095844_availability.ts +13 -0
- package/src/modules/availability/setup.ts +10 -0
- package/src/modules/entities/cli.ts +53 -13
- package/src/modules/inbox_ops/api/settings/route.ts +9 -32
- package/src/modules/inbox_ops/backend/inbox-ops/settings/page.tsx +2 -2
- package/src/modules/inbox_ops/components/proposals/ActionCard.tsx +1 -1
- package/src/modules/inbox_ops/i18n/de.json +1 -1
- package/src/modules/inbox_ops/i18n/en.json +1 -1
- package/src/modules/inbox_ops/i18n/es.json +1 -1
- package/src/modules/inbox_ops/i18n/ko.json +1 -1
- package/src/modules/inbox_ops/i18n/pl.json +1 -1
- package/src/modules/inbox_ops/lib/ensure-settings.ts +77 -0
- package/src/modules/inbox_ops/lib/executionEngine.ts +1 -1
- package/src/modules/inbox_ops/lib/executionHelpers.ts +0 -27
- package/src/modules/inbox_ops/lib/payloadEnrichment.ts +1 -5
- package/src/modules/inbox_ops/setup.ts +2 -21
- package/src/modules/inbox_ops/subscribers/extractionWorker.ts +1 -1
- package/src/modules/sales/components/documents/SalesDocumentForm.tsx +13 -0
- package/src/modules/sales/data/entities.ts +2 -0
- package/src/modules/sales/inbox-actions.ts +0 -20
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +20 -0
- package/src/modules/sales/migrations/Migration20260916120000_sales_payment_allocation_order_idx.ts +74 -0
- package/src/modules/wms/di.ts +6 -0
- package/src/modules/wms/lib/availabilityCache.ts +136 -0
- package/src/modules/wms/lib/availabilityCalculation.ts +289 -0
- package/src/modules/wms/lib/availabilityProvider.ts +29 -0
- package/src/modules/wms/lib/sqlInClause.ts +21 -0
- package/src/modules/wms/lib/tryResolve.ts +8 -0
- package/src/modules/wms/subscribers/invalidate-availability-cache.ts +30 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `AvailabilityPolicy` resolution chain — six levels, most specific wins.
|
|
3
|
+
*
|
|
4
|
+
* @see .ai/specs/2026-08-14-availability-contract.md §5.2
|
|
5
|
+
*
|
|
6
|
+
* Resolution design (documented here because §5.2 does not fully spell it
|
|
7
|
+
* out — see PLAN.md § Key design decisions):
|
|
8
|
+
*
|
|
9
|
+
* - Boolean columns (`isStockManaged`, `allowBackorder`, `hideWhenOutOfStock`,
|
|
10
|
+
* `isActive`) are NOT NULL, so a matched row's own value is always
|
|
11
|
+
* concrete — there is no "unset" to fall through. The single most specific
|
|
12
|
+
* EXISTING row decides every boolean field.
|
|
13
|
+
* - Nullable columns (`backorderLeadTimeDays`, `preorderReleaseAt`,
|
|
14
|
+
* `lowStockThreshold`, `minOrderQuantity`, `maxOrderQuantity`,
|
|
15
|
+
* `quantityIncrement`) genuinely cascade: a matched row that leaves the
|
|
16
|
+
* field `null` defers to the next-less-specific row, per US-A2's "which
|
|
17
|
+
* level currently decides each field" requirement.
|
|
18
|
+
*
|
|
19
|
+
* `resolveMany()` exists so `wms`'s batched `check()` (§4.2/R4 — one policy
|
|
20
|
+
* lookup regardless of item count) can soft-resolve this service and still
|
|
21
|
+
* issue exactly one query for the whole batch, rather than one per item.
|
|
22
|
+
* `resolve()` is `resolveMany([scope])[0]`, so single-item callers (the admin
|
|
23
|
+
* check tool, the resolve-preview endpoint) are unaffected.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
27
|
+
import { AvailabilityPolicy } from '../data/entities'
|
|
28
|
+
import { tryResolve } from './tryResolve'
|
|
29
|
+
|
|
30
|
+
export type PolicyResolutionScope = {
|
|
31
|
+
tenantId: string
|
|
32
|
+
organizationId: string
|
|
33
|
+
storeId?: string | null
|
|
34
|
+
productId: string
|
|
35
|
+
variantId?: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ResolvedField<T> = {
|
|
39
|
+
value: T
|
|
40
|
+
/** The `AvailabilityPolicy` row id that decided, or `null` for the module default. */
|
|
41
|
+
policySourceId: string | null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type ResolvedAvailabilityPolicy = {
|
|
45
|
+
isStockManaged: ResolvedField<boolean>
|
|
46
|
+
allowBackorder: ResolvedField<boolean>
|
|
47
|
+
backorderLeadTimeDays: ResolvedField<number | null>
|
|
48
|
+
preorderReleaseAt: ResolvedField<Date | null>
|
|
49
|
+
lowStockThreshold: ResolvedField<number | null>
|
|
50
|
+
minOrderQuantity: ResolvedField<number | null>
|
|
51
|
+
maxOrderQuantity: ResolvedField<number | null>
|
|
52
|
+
quantityIncrement: ResolvedField<number | null>
|
|
53
|
+
hideWhenOutOfStock: ResolvedField<boolean>
|
|
54
|
+
isActive: ResolvedField<boolean>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The six chain levels, most specific first. `null` = "no row matched this level". */
|
|
58
|
+
export type PolicyChainLevel =
|
|
59
|
+
| 'variant_store'
|
|
60
|
+
| 'variant'
|
|
61
|
+
| 'product_store'
|
|
62
|
+
| 'product'
|
|
63
|
+
| 'store_default'
|
|
64
|
+
| 'module_default'
|
|
65
|
+
|
|
66
|
+
export const POLICY_CHAIN_LEVELS: PolicyChainLevel[] = [
|
|
67
|
+
'variant_store',
|
|
68
|
+
'variant',
|
|
69
|
+
'product_store',
|
|
70
|
+
'product',
|
|
71
|
+
'store_default',
|
|
72
|
+
'module_default',
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
const NULLABLE_FIELDS = [
|
|
76
|
+
'backorderLeadTimeDays',
|
|
77
|
+
'preorderReleaseAt',
|
|
78
|
+
'lowStockThreshold',
|
|
79
|
+
'minOrderQuantity',
|
|
80
|
+
'maxOrderQuantity',
|
|
81
|
+
'quantityIncrement',
|
|
82
|
+
] as const
|
|
83
|
+
|
|
84
|
+
const BOOLEAN_FIELDS = ['isStockManaged', 'allowBackorder', 'hideWhenOutOfStock', 'isActive'] as const
|
|
85
|
+
|
|
86
|
+
function candidateFilters(scope: PolicyResolutionScope): Array<Record<string, unknown>> {
|
|
87
|
+
const storeId = scope.storeId ?? null
|
|
88
|
+
const variantId = scope.variantId ?? null
|
|
89
|
+
const filters: Array<Record<string, unknown>> = []
|
|
90
|
+
if (variantId) {
|
|
91
|
+
filters.push({ variantId, storeId })
|
|
92
|
+
filters.push({ variantId, storeId: null })
|
|
93
|
+
}
|
|
94
|
+
filters.push({ productId: scope.productId, variantId: null, storeId })
|
|
95
|
+
filters.push({ productId: scope.productId, variantId: null, storeId: null })
|
|
96
|
+
filters.push({ productId: null, variantId: null, storeId })
|
|
97
|
+
if (storeId) filters.push({ productId: null, variantId: null, storeId: null })
|
|
98
|
+
return filters
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Picks the (up to) five candidate rows for one scope out of a shared, already-loaded candidate pool. */
|
|
102
|
+
function matchChainRows(scope: PolicyResolutionScope, pool: AvailabilityPolicy[]): Array<AvailabilityPolicy | null> {
|
|
103
|
+
const storeId = scope.storeId ?? null
|
|
104
|
+
const variantId = scope.variantId ?? null
|
|
105
|
+
const findMatch = (predicate: (row: AvailabilityPolicy) => boolean) => pool.find(predicate) ?? null
|
|
106
|
+
|
|
107
|
+
const rows: Array<AvailabilityPolicy | null> = []
|
|
108
|
+
if (variantId) {
|
|
109
|
+
rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === storeId))
|
|
110
|
+
rows.push(findMatch((r) => r.variantId === variantId && (r.storeId ?? null) === null))
|
|
111
|
+
}
|
|
112
|
+
rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === storeId))
|
|
113
|
+
rows.push(findMatch((r) => r.productId === scope.productId && !r.variantId && (r.storeId ?? null) === null))
|
|
114
|
+
rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === storeId))
|
|
115
|
+
// The store-default level cascades store-specific → all-stores, same as
|
|
116
|
+
// the variant/product levels above it ("store_id null = applies to all
|
|
117
|
+
// stores in the organization", §5.1) — otherwise an org-wide default row
|
|
118
|
+
// is invisible to any query that names a concrete store.
|
|
119
|
+
if (storeId) rows.push(findMatch((r) => !r.productId && !r.variantId && (r.storeId ?? null) === null))
|
|
120
|
+
return rows
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveFromRows(
|
|
124
|
+
rows: Array<AvailabilityPolicy | null>,
|
|
125
|
+
isStockManagedModuleDefault: boolean,
|
|
126
|
+
): ResolvedAvailabilityPolicy {
|
|
127
|
+
const firstRow = rows.find((row): row is AvailabilityPolicy => row != null) ?? null
|
|
128
|
+
const result = {} as ResolvedAvailabilityPolicy
|
|
129
|
+
|
|
130
|
+
for (const field of BOOLEAN_FIELDS) {
|
|
131
|
+
if (firstRow) {
|
|
132
|
+
;(result as any)[field] = { value: (firstRow as any)[field], policySourceId: firstRow.id }
|
|
133
|
+
} else {
|
|
134
|
+
;(result as any)[field] = { value: false, policySourceId: null }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// is_stock_managed's module default is dynamic (wms + profile existence), not a flat `false`.
|
|
139
|
+
if (!firstRow) result.isStockManaged = { value: isStockManagedModuleDefault, policySourceId: null }
|
|
140
|
+
// is_active's module default is "active" (nothing to deactivate).
|
|
141
|
+
if (!firstRow) result.isActive = { value: true, policySourceId: null }
|
|
142
|
+
|
|
143
|
+
for (const field of NULLABLE_FIELDS) {
|
|
144
|
+
let resolved: ResolvedField<unknown> = { value: null, policySourceId: null }
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
if (row && (row as any)[field] != null) {
|
|
147
|
+
resolved = { value: (row as any)[field], policySourceId: row.id }
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
;(result as any)[field] = resolved
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** `is_stock_managed`'s module default: true when `wms` is enabled and a `ProductInventoryProfile` exists for the item. */
|
|
158
|
+
export async function resolveIsStockManagedModuleDefault(
|
|
159
|
+
em: EntityManager,
|
|
160
|
+
container: { resolve: <T = unknown>(name: string) => T },
|
|
161
|
+
scope: { tenantId: string; organizationId: string; productId: string; variantId?: string | null },
|
|
162
|
+
): Promise<boolean> {
|
|
163
|
+
const ProductInventoryProfile = tryResolve<new () => unknown>(container, 'ProductInventoryProfile')
|
|
164
|
+
if (!ProductInventoryProfile) return false
|
|
165
|
+
|
|
166
|
+
const baseFilter = {
|
|
167
|
+
tenantId: scope.tenantId,
|
|
168
|
+
organizationId: scope.organizationId,
|
|
169
|
+
catalogProductId: scope.productId,
|
|
170
|
+
deletedAt: null,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (scope.variantId) {
|
|
174
|
+
const variantRow = await em.findOne(ProductInventoryProfile as any, { ...baseFilter, catalogVariantId: scope.variantId })
|
|
175
|
+
if (variantRow) return true
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const productRow = await em.findOne(ProductInventoryProfile as any, { ...baseFilter, catalogVariantId: null })
|
|
179
|
+
return !!productRow
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Batched module-default check for `resolveMany` — one query for every scope that might need it. */
|
|
183
|
+
async function resolveIsStockManagedModuleDefaultMany(
|
|
184
|
+
em: EntityManager,
|
|
185
|
+
container: { resolve: <T = unknown>(name: string) => T },
|
|
186
|
+
scopes: PolicyResolutionScope[],
|
|
187
|
+
): Promise<Map<string, boolean>> {
|
|
188
|
+
const ProductInventoryProfile = tryResolve<new () => unknown>(container, 'ProductInventoryProfile')
|
|
189
|
+
const results = new Map<string, boolean>()
|
|
190
|
+
if (!ProductInventoryProfile || scopes.length === 0) return results
|
|
191
|
+
|
|
192
|
+
const first = scopes[0]
|
|
193
|
+
const productIds = Array.from(new Set(scopes.map((s) => s.productId)))
|
|
194
|
+
const rows = (await em.find(ProductInventoryProfile as any, {
|
|
195
|
+
tenantId: first.tenantId,
|
|
196
|
+
organizationId: first.organizationId,
|
|
197
|
+
deletedAt: null,
|
|
198
|
+
catalogProductId: { $in: productIds },
|
|
199
|
+
} as any)) as Array<{ catalogProductId: string; catalogVariantId: string | null }>
|
|
200
|
+
|
|
201
|
+
const exists = new Set(rows.map((row) => `${row.catalogProductId}:${row.catalogVariantId ?? ''}`))
|
|
202
|
+
for (const scope of scopes) {
|
|
203
|
+
const variantHit = scope.variantId ? exists.has(`${scope.productId}:${scope.variantId}`) : false
|
|
204
|
+
const productHit = exists.has(`${scope.productId}:`)
|
|
205
|
+
results.set(scopeKey(scope), variantHit || productHit)
|
|
206
|
+
}
|
|
207
|
+
return results
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function scopeKey(scope: PolicyResolutionScope): string {
|
|
211
|
+
return `${scope.productId}:${scope.variantId ?? ''}:${scope.storeId ?? ''}`
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface PolicyResolutionService {
|
|
215
|
+
resolve(em: EntityManager, scope: PolicyResolutionScope): Promise<ResolvedAvailabilityPolicy>
|
|
216
|
+
resolveMany(em: EntityManager, scopes: PolicyResolutionScope[]): Promise<ResolvedAvailabilityPolicy[]>
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function createPolicyResolutionService(container: {
|
|
220
|
+
resolve: <T = unknown>(name: string) => T
|
|
221
|
+
}): PolicyResolutionService {
|
|
222
|
+
async function resolveMany(em: EntityManager, scopes: PolicyResolutionScope[]): Promise<ResolvedAvailabilityPolicy[]> {
|
|
223
|
+
if (scopes.length === 0) return []
|
|
224
|
+
|
|
225
|
+
const first = scopes[0]
|
|
226
|
+
const seen = new Set<string>()
|
|
227
|
+
const orFilters: Array<Record<string, unknown>> = []
|
|
228
|
+
for (const scope of scopes) {
|
|
229
|
+
for (const filter of candidateFilters(scope)) {
|
|
230
|
+
const key = JSON.stringify(filter)
|
|
231
|
+
if (seen.has(key)) continue
|
|
232
|
+
seen.add(key)
|
|
233
|
+
orFilters.push(filter)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const pool = orFilters.length
|
|
238
|
+
? await em.find(AvailabilityPolicy, {
|
|
239
|
+
tenantId: first.tenantId,
|
|
240
|
+
organizationId: first.organizationId,
|
|
241
|
+
deletedAt: null,
|
|
242
|
+
$or: orFilters as any,
|
|
243
|
+
})
|
|
244
|
+
: []
|
|
245
|
+
|
|
246
|
+
// Only scopes that resolve to no row at all need the dynamic is_stock_managed
|
|
247
|
+
// module default — batch just those.
|
|
248
|
+
const rowsByScope = scopes.map((scope) => matchChainRows(scope, pool))
|
|
249
|
+
const needsModuleDefault = scopes.filter((scope, index) => !rowsByScope[index].some((row) => row != null))
|
|
250
|
+
const moduleDefaults = await resolveIsStockManagedModuleDefaultMany(em, container, needsModuleDefault)
|
|
251
|
+
|
|
252
|
+
return scopes.map((scope, index) =>
|
|
253
|
+
resolveFromRows(rowsByScope[index], moduleDefaults.get(scopeKey(scope)) ?? false),
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
async resolve(em, scope) {
|
|
259
|
+
const [result] = await resolveMany(em, [scope])
|
|
260
|
+
return result
|
|
261
|
+
},
|
|
262
|
+
resolveMany,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Per-module local soft-resolve helper — see `packages/core/AGENTS.md` § Cross-Module Coupling. */
|
|
2
|
+
export function tryResolve<T>(resolver: { resolve: <R = unknown>(name: string) => R }, name: string): T | undefined {
|
|
3
|
+
try {
|
|
4
|
+
return resolver.resolve<T>(name)
|
|
5
|
+
} catch {
|
|
6
|
+
return undefined
|
|
7
|
+
}
|
|
8
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "public",
|
|
3
|
+
"namespaces": [
|
|
4
|
+
"public"
|
|
5
|
+
],
|
|
6
|
+
"tables": [
|
|
7
|
+
{
|
|
8
|
+
"name": "availability_policies",
|
|
9
|
+
"schema": "public",
|
|
10
|
+
"columns": {
|
|
11
|
+
"allow_backorder": {
|
|
12
|
+
"name": "allow_backorder",
|
|
13
|
+
"type": "boolean",
|
|
14
|
+
"unsigned": false,
|
|
15
|
+
"autoincrement": false,
|
|
16
|
+
"primary": false,
|
|
17
|
+
"nullable": false,
|
|
18
|
+
"unique": false,
|
|
19
|
+
"length": null,
|
|
20
|
+
"precision": null,
|
|
21
|
+
"scale": null,
|
|
22
|
+
"default": "false",
|
|
23
|
+
"comment": null,
|
|
24
|
+
"collation": null,
|
|
25
|
+
"enumItems": [],
|
|
26
|
+
"mappedType": "boolean"
|
|
27
|
+
},
|
|
28
|
+
"backorder_lead_time_days": {
|
|
29
|
+
"name": "backorder_lead_time_days",
|
|
30
|
+
"type": "int",
|
|
31
|
+
"unsigned": false,
|
|
32
|
+
"autoincrement": false,
|
|
33
|
+
"primary": false,
|
|
34
|
+
"nullable": true,
|
|
35
|
+
"unique": false,
|
|
36
|
+
"length": null,
|
|
37
|
+
"precision": null,
|
|
38
|
+
"scale": null,
|
|
39
|
+
"default": null,
|
|
40
|
+
"comment": null,
|
|
41
|
+
"collation": null,
|
|
42
|
+
"enumItems": [],
|
|
43
|
+
"mappedType": "integer"
|
|
44
|
+
},
|
|
45
|
+
"created_at": {
|
|
46
|
+
"name": "created_at",
|
|
47
|
+
"type": "timestamptz(6)",
|
|
48
|
+
"unsigned": false,
|
|
49
|
+
"autoincrement": false,
|
|
50
|
+
"primary": false,
|
|
51
|
+
"nullable": false,
|
|
52
|
+
"unique": false,
|
|
53
|
+
"length": 6,
|
|
54
|
+
"precision": null,
|
|
55
|
+
"scale": null,
|
|
56
|
+
"default": null,
|
|
57
|
+
"comment": null,
|
|
58
|
+
"collation": null,
|
|
59
|
+
"enumItems": [],
|
|
60
|
+
"mappedType": "datetime"
|
|
61
|
+
},
|
|
62
|
+
"deleted_at": {
|
|
63
|
+
"name": "deleted_at",
|
|
64
|
+
"type": "timestamptz(6)",
|
|
65
|
+
"unsigned": false,
|
|
66
|
+
"autoincrement": false,
|
|
67
|
+
"primary": false,
|
|
68
|
+
"nullable": true,
|
|
69
|
+
"unique": false,
|
|
70
|
+
"length": 6,
|
|
71
|
+
"precision": null,
|
|
72
|
+
"scale": null,
|
|
73
|
+
"default": null,
|
|
74
|
+
"comment": null,
|
|
75
|
+
"collation": null,
|
|
76
|
+
"enumItems": [],
|
|
77
|
+
"mappedType": "datetime"
|
|
78
|
+
},
|
|
79
|
+
"hide_when_out_of_stock": {
|
|
80
|
+
"name": "hide_when_out_of_stock",
|
|
81
|
+
"type": "boolean",
|
|
82
|
+
"unsigned": false,
|
|
83
|
+
"autoincrement": false,
|
|
84
|
+
"primary": false,
|
|
85
|
+
"nullable": false,
|
|
86
|
+
"unique": false,
|
|
87
|
+
"length": null,
|
|
88
|
+
"precision": null,
|
|
89
|
+
"scale": null,
|
|
90
|
+
"default": "false",
|
|
91
|
+
"comment": null,
|
|
92
|
+
"collation": null,
|
|
93
|
+
"enumItems": [],
|
|
94
|
+
"mappedType": "boolean"
|
|
95
|
+
},
|
|
96
|
+
"id": {
|
|
97
|
+
"name": "id",
|
|
98
|
+
"type": "uuid",
|
|
99
|
+
"unsigned": false,
|
|
100
|
+
"autoincrement": false,
|
|
101
|
+
"primary": true,
|
|
102
|
+
"nullable": false,
|
|
103
|
+
"unique": false,
|
|
104
|
+
"length": null,
|
|
105
|
+
"precision": null,
|
|
106
|
+
"scale": null,
|
|
107
|
+
"default": "gen_random_uuid()",
|
|
108
|
+
"comment": null,
|
|
109
|
+
"collation": null,
|
|
110
|
+
"enumItems": [],
|
|
111
|
+
"mappedType": "uuid"
|
|
112
|
+
},
|
|
113
|
+
"is_active": {
|
|
114
|
+
"name": "is_active",
|
|
115
|
+
"type": "boolean",
|
|
116
|
+
"unsigned": false,
|
|
117
|
+
"autoincrement": false,
|
|
118
|
+
"primary": false,
|
|
119
|
+
"nullable": false,
|
|
120
|
+
"unique": false,
|
|
121
|
+
"length": null,
|
|
122
|
+
"precision": null,
|
|
123
|
+
"scale": null,
|
|
124
|
+
"default": "true",
|
|
125
|
+
"comment": null,
|
|
126
|
+
"collation": null,
|
|
127
|
+
"enumItems": [],
|
|
128
|
+
"mappedType": "boolean"
|
|
129
|
+
},
|
|
130
|
+
"is_stock_managed": {
|
|
131
|
+
"name": "is_stock_managed",
|
|
132
|
+
"type": "boolean",
|
|
133
|
+
"unsigned": false,
|
|
134
|
+
"autoincrement": false,
|
|
135
|
+
"primary": false,
|
|
136
|
+
"nullable": false,
|
|
137
|
+
"unique": false,
|
|
138
|
+
"length": null,
|
|
139
|
+
"precision": null,
|
|
140
|
+
"scale": null,
|
|
141
|
+
"default": "false",
|
|
142
|
+
"comment": null,
|
|
143
|
+
"collation": null,
|
|
144
|
+
"enumItems": [],
|
|
145
|
+
"mappedType": "boolean"
|
|
146
|
+
},
|
|
147
|
+
"low_stock_threshold": {
|
|
148
|
+
"name": "low_stock_threshold",
|
|
149
|
+
"type": "int",
|
|
150
|
+
"unsigned": false,
|
|
151
|
+
"autoincrement": false,
|
|
152
|
+
"primary": false,
|
|
153
|
+
"nullable": true,
|
|
154
|
+
"unique": false,
|
|
155
|
+
"length": null,
|
|
156
|
+
"precision": null,
|
|
157
|
+
"scale": null,
|
|
158
|
+
"default": null,
|
|
159
|
+
"comment": null,
|
|
160
|
+
"collation": null,
|
|
161
|
+
"enumItems": [],
|
|
162
|
+
"mappedType": "integer"
|
|
163
|
+
},
|
|
164
|
+
"max_order_quantity": {
|
|
165
|
+
"name": "max_order_quantity",
|
|
166
|
+
"type": "int",
|
|
167
|
+
"unsigned": false,
|
|
168
|
+
"autoincrement": false,
|
|
169
|
+
"primary": false,
|
|
170
|
+
"nullable": true,
|
|
171
|
+
"unique": false,
|
|
172
|
+
"length": null,
|
|
173
|
+
"precision": null,
|
|
174
|
+
"scale": null,
|
|
175
|
+
"default": null,
|
|
176
|
+
"comment": null,
|
|
177
|
+
"collation": null,
|
|
178
|
+
"enumItems": [],
|
|
179
|
+
"mappedType": "integer"
|
|
180
|
+
},
|
|
181
|
+
"min_order_quantity": {
|
|
182
|
+
"name": "min_order_quantity",
|
|
183
|
+
"type": "int",
|
|
184
|
+
"unsigned": false,
|
|
185
|
+
"autoincrement": false,
|
|
186
|
+
"primary": false,
|
|
187
|
+
"nullable": true,
|
|
188
|
+
"unique": false,
|
|
189
|
+
"length": null,
|
|
190
|
+
"precision": null,
|
|
191
|
+
"scale": null,
|
|
192
|
+
"default": null,
|
|
193
|
+
"comment": null,
|
|
194
|
+
"collation": null,
|
|
195
|
+
"enumItems": [],
|
|
196
|
+
"mappedType": "integer"
|
|
197
|
+
},
|
|
198
|
+
"organization_id": {
|
|
199
|
+
"name": "organization_id",
|
|
200
|
+
"type": "uuid",
|
|
201
|
+
"unsigned": false,
|
|
202
|
+
"autoincrement": false,
|
|
203
|
+
"primary": false,
|
|
204
|
+
"nullable": false,
|
|
205
|
+
"unique": false,
|
|
206
|
+
"length": null,
|
|
207
|
+
"precision": null,
|
|
208
|
+
"scale": null,
|
|
209
|
+
"default": null,
|
|
210
|
+
"comment": null,
|
|
211
|
+
"collation": null,
|
|
212
|
+
"enumItems": [],
|
|
213
|
+
"mappedType": "uuid"
|
|
214
|
+
},
|
|
215
|
+
"preorder_release_at": {
|
|
216
|
+
"name": "preorder_release_at",
|
|
217
|
+
"type": "timestamptz(6)",
|
|
218
|
+
"unsigned": false,
|
|
219
|
+
"autoincrement": false,
|
|
220
|
+
"primary": false,
|
|
221
|
+
"nullable": true,
|
|
222
|
+
"unique": false,
|
|
223
|
+
"length": 6,
|
|
224
|
+
"precision": null,
|
|
225
|
+
"scale": null,
|
|
226
|
+
"default": null,
|
|
227
|
+
"comment": null,
|
|
228
|
+
"collation": null,
|
|
229
|
+
"enumItems": [],
|
|
230
|
+
"mappedType": "datetime"
|
|
231
|
+
},
|
|
232
|
+
"product_id": {
|
|
233
|
+
"name": "product_id",
|
|
234
|
+
"type": "uuid",
|
|
235
|
+
"unsigned": false,
|
|
236
|
+
"autoincrement": false,
|
|
237
|
+
"primary": false,
|
|
238
|
+
"nullable": true,
|
|
239
|
+
"unique": false,
|
|
240
|
+
"length": null,
|
|
241
|
+
"precision": null,
|
|
242
|
+
"scale": null,
|
|
243
|
+
"default": null,
|
|
244
|
+
"comment": null,
|
|
245
|
+
"collation": null,
|
|
246
|
+
"enumItems": [],
|
|
247
|
+
"mappedType": "uuid"
|
|
248
|
+
},
|
|
249
|
+
"quantity_increment": {
|
|
250
|
+
"name": "quantity_increment",
|
|
251
|
+
"type": "int",
|
|
252
|
+
"unsigned": false,
|
|
253
|
+
"autoincrement": false,
|
|
254
|
+
"primary": false,
|
|
255
|
+
"nullable": true,
|
|
256
|
+
"unique": false,
|
|
257
|
+
"length": null,
|
|
258
|
+
"precision": null,
|
|
259
|
+
"scale": null,
|
|
260
|
+
"default": null,
|
|
261
|
+
"comment": null,
|
|
262
|
+
"collation": null,
|
|
263
|
+
"enumItems": [],
|
|
264
|
+
"mappedType": "integer"
|
|
265
|
+
},
|
|
266
|
+
"store_id": {
|
|
267
|
+
"name": "store_id",
|
|
268
|
+
"type": "uuid",
|
|
269
|
+
"unsigned": false,
|
|
270
|
+
"autoincrement": false,
|
|
271
|
+
"primary": false,
|
|
272
|
+
"nullable": true,
|
|
273
|
+
"unique": false,
|
|
274
|
+
"length": null,
|
|
275
|
+
"precision": null,
|
|
276
|
+
"scale": null,
|
|
277
|
+
"default": null,
|
|
278
|
+
"comment": null,
|
|
279
|
+
"collation": null,
|
|
280
|
+
"enumItems": [],
|
|
281
|
+
"mappedType": "uuid"
|
|
282
|
+
},
|
|
283
|
+
"tenant_id": {
|
|
284
|
+
"name": "tenant_id",
|
|
285
|
+
"type": "uuid",
|
|
286
|
+
"unsigned": false,
|
|
287
|
+
"autoincrement": false,
|
|
288
|
+
"primary": false,
|
|
289
|
+
"nullable": false,
|
|
290
|
+
"unique": false,
|
|
291
|
+
"length": null,
|
|
292
|
+
"precision": null,
|
|
293
|
+
"scale": null,
|
|
294
|
+
"default": null,
|
|
295
|
+
"comment": null,
|
|
296
|
+
"collation": null,
|
|
297
|
+
"enumItems": [],
|
|
298
|
+
"mappedType": "uuid"
|
|
299
|
+
},
|
|
300
|
+
"updated_at": {
|
|
301
|
+
"name": "updated_at",
|
|
302
|
+
"type": "timestamptz(6)",
|
|
303
|
+
"unsigned": false,
|
|
304
|
+
"autoincrement": false,
|
|
305
|
+
"primary": false,
|
|
306
|
+
"nullable": false,
|
|
307
|
+
"unique": false,
|
|
308
|
+
"length": 6,
|
|
309
|
+
"precision": null,
|
|
310
|
+
"scale": null,
|
|
311
|
+
"default": null,
|
|
312
|
+
"comment": null,
|
|
313
|
+
"collation": null,
|
|
314
|
+
"enumItems": [],
|
|
315
|
+
"mappedType": "datetime"
|
|
316
|
+
},
|
|
317
|
+
"variant_id": {
|
|
318
|
+
"name": "variant_id",
|
|
319
|
+
"type": "uuid",
|
|
320
|
+
"unsigned": false,
|
|
321
|
+
"autoincrement": false,
|
|
322
|
+
"primary": false,
|
|
323
|
+
"nullable": true,
|
|
324
|
+
"unique": false,
|
|
325
|
+
"length": null,
|
|
326
|
+
"precision": null,
|
|
327
|
+
"scale": null,
|
|
328
|
+
"default": null,
|
|
329
|
+
"comment": null,
|
|
330
|
+
"collation": null,
|
|
331
|
+
"enumItems": [],
|
|
332
|
+
"mappedType": "uuid"
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
"indexes": [
|
|
336
|
+
{
|
|
337
|
+
"columnNames": [
|
|
338
|
+
"id"
|
|
339
|
+
],
|
|
340
|
+
"composite": false,
|
|
341
|
+
"constraint": true,
|
|
342
|
+
"keyName": "availability_policies_pkey",
|
|
343
|
+
"primary": true,
|
|
344
|
+
"unique": true
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
"columnNames": [
|
|
348
|
+
"tenant_id",
|
|
349
|
+
"organization_id"
|
|
350
|
+
],
|
|
351
|
+
"composite": true,
|
|
352
|
+
"constraint": false,
|
|
353
|
+
"keyName": "availability_policies_scope_idx",
|
|
354
|
+
"primary": false,
|
|
355
|
+
"unique": false
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"columnNames": [],
|
|
359
|
+
"composite": false,
|
|
360
|
+
"constraint": false,
|
|
361
|
+
"keyName": "availability_policies_scope_target_unique",
|
|
362
|
+
"primary": false,
|
|
363
|
+
"unique": false,
|
|
364
|
+
"expression": "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"
|
|
365
|
+
}
|
|
366
|
+
],
|
|
367
|
+
"checks": [],
|
|
368
|
+
"triggers": [],
|
|
369
|
+
"foreignKeys": {},
|
|
370
|
+
"comment": null
|
|
371
|
+
}
|
|
372
|
+
],
|
|
373
|
+
"views": [],
|
|
374
|
+
"nativeEnums": {}
|
|
375
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260922095844_availability extends Migration {
|
|
4
|
+
|
|
5
|
+
override name = 'Migration20260922095844';
|
|
6
|
+
|
|
7
|
+
override up(): void | Promise<void> {
|
|
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
|
+
}
|