@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,358 @@
|
|
|
1
|
+
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
2
|
+
import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
3
|
+
import { requireId } from '@open-mercato/shared/lib/commands/helpers'
|
|
4
|
+
import { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'
|
|
5
|
+
import { runCrudCommandWrite } from '@open-mercato/shared/lib/commands/runCrudCommandWrite'
|
|
6
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
7
|
+
import { CrudHttpError, conflict, isUniqueViolation } from '@open-mercato/shared/lib/crud/errors'
|
|
8
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
9
|
+
import type { CrudEventsConfig } from '@open-mercato/shared/lib/crud/types'
|
|
10
|
+
import { E } from '#generated/entities.ids.generated'
|
|
11
|
+
import { AvailabilityPolicy } from '../data/entities'
|
|
12
|
+
import {
|
|
13
|
+
availabilityPolicyCreateSchema,
|
|
14
|
+
availabilityPolicyUpdateSchema,
|
|
15
|
+
availabilityPolicyDeleteSchema,
|
|
16
|
+
availabilityPolicyMergedConstraintsSchema,
|
|
17
|
+
type AvailabilityPolicyCreateInput,
|
|
18
|
+
type AvailabilityPolicyUpdateInput,
|
|
19
|
+
} from '../data/validators'
|
|
20
|
+
import { buildAvailabilityPolicyCommandWhere, ensureAvailabilityPolicyCommandScope } from './scope'
|
|
21
|
+
|
|
22
|
+
const AVAILABILITY_POLICY_ENTITY_ID = E.availability.availability_policy
|
|
23
|
+
|
|
24
|
+
const policyCrudEvents: CrudEventsConfig<AvailabilityPolicy> = {
|
|
25
|
+
module: 'availability',
|
|
26
|
+
entity: 'policy',
|
|
27
|
+
persistent: true,
|
|
28
|
+
buildPayload: (ctx) => ({
|
|
29
|
+
id: ctx.identifiers.id,
|
|
30
|
+
organizationId: ctx.identifiers.organizationId,
|
|
31
|
+
tenantId: ctx.identifiers.tenantId,
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type AvailabilityPolicySnapshot = {
|
|
36
|
+
id: string
|
|
37
|
+
organizationId: string
|
|
38
|
+
tenantId: string
|
|
39
|
+
storeId: string | null
|
|
40
|
+
productId: string | null
|
|
41
|
+
variantId: string | null
|
|
42
|
+
isStockManaged: boolean
|
|
43
|
+
allowBackorder: boolean
|
|
44
|
+
backorderLeadTimeDays: number | null
|
|
45
|
+
preorderReleaseAt: string | null
|
|
46
|
+
lowStockThreshold: number | null
|
|
47
|
+
minOrderQuantity: number | null
|
|
48
|
+
maxOrderQuantity: number | null
|
|
49
|
+
quantityIncrement: number | null
|
|
50
|
+
hideWhenOutOfStock: boolean
|
|
51
|
+
isActive: boolean
|
|
52
|
+
createdAt: string
|
|
53
|
+
updatedAt: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type AvailabilityPolicyUndoPayload = UndoPayload<AvailabilityPolicySnapshot>
|
|
57
|
+
|
|
58
|
+
function toSnapshot(record: AvailabilityPolicy): AvailabilityPolicySnapshot {
|
|
59
|
+
return {
|
|
60
|
+
id: record.id,
|
|
61
|
+
organizationId: record.organizationId,
|
|
62
|
+
tenantId: record.tenantId,
|
|
63
|
+
storeId: record.storeId ?? null,
|
|
64
|
+
productId: record.productId ?? null,
|
|
65
|
+
variantId: record.variantId ?? null,
|
|
66
|
+
isStockManaged: !!record.isStockManaged,
|
|
67
|
+
allowBackorder: !!record.allowBackorder,
|
|
68
|
+
backorderLeadTimeDays: record.backorderLeadTimeDays ?? null,
|
|
69
|
+
preorderReleaseAt: record.preorderReleaseAt ? record.preorderReleaseAt.toISOString() : null,
|
|
70
|
+
lowStockThreshold: record.lowStockThreshold ?? null,
|
|
71
|
+
minOrderQuantity: record.minOrderQuantity ?? null,
|
|
72
|
+
maxOrderQuantity: record.maxOrderQuantity ?? null,
|
|
73
|
+
quantityIncrement: record.quantityIncrement ?? null,
|
|
74
|
+
hideWhenOutOfStock: !!record.hideWhenOutOfStock,
|
|
75
|
+
isActive: !!record.isActive,
|
|
76
|
+
createdAt: record.createdAt.toISOString(),
|
|
77
|
+
updatedAt: record.updatedAt.toISOString(),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function loadSnapshot(
|
|
82
|
+
em: EntityManager,
|
|
83
|
+
id: string,
|
|
84
|
+
ctx: CommandRuntimeContext,
|
|
85
|
+
): Promise<AvailabilityPolicySnapshot | null> {
|
|
86
|
+
const record = await em.findOne(AvailabilityPolicy, buildAvailabilityPolicyCommandWhere<AvailabilityPolicy>(ctx, { id }))
|
|
87
|
+
if (!record) return null
|
|
88
|
+
ensureAvailabilityPolicyCommandScope(ctx, record)
|
|
89
|
+
return toSnapshot(record)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function applyUndoSnapshot(record: AvailabilityPolicy, snapshot: AvailabilityPolicySnapshot): void {
|
|
93
|
+
record.storeId = snapshot.storeId
|
|
94
|
+
record.productId = snapshot.productId
|
|
95
|
+
record.variantId = snapshot.variantId
|
|
96
|
+
record.isStockManaged = snapshot.isStockManaged
|
|
97
|
+
record.allowBackorder = snapshot.allowBackorder
|
|
98
|
+
record.backorderLeadTimeDays = snapshot.backorderLeadTimeDays
|
|
99
|
+
record.preorderReleaseAt = snapshot.preorderReleaseAt ? new Date(snapshot.preorderReleaseAt) : null
|
|
100
|
+
record.lowStockThreshold = snapshot.lowStockThreshold
|
|
101
|
+
record.minOrderQuantity = snapshot.minOrderQuantity
|
|
102
|
+
record.maxOrderQuantity = snapshot.maxOrderQuantity
|
|
103
|
+
record.quantityIncrement = snapshot.quantityIncrement
|
|
104
|
+
record.hideWhenOutOfStock = snapshot.hideWhenOutOfStock
|
|
105
|
+
record.isActive = snapshot.isActive
|
|
106
|
+
record.updatedAt = new Date()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const createPolicyCommand: CommandHandler<AvailabilityPolicyCreateInput, { policyId: string }> = {
|
|
110
|
+
id: 'availability.policies.create',
|
|
111
|
+
async execute(input, ctx) {
|
|
112
|
+
const parsed = availabilityPolicyCreateSchema.parse(input)
|
|
113
|
+
ensureAvailabilityPolicyCommandScope(ctx, parsed)
|
|
114
|
+
|
|
115
|
+
const record = new AvailabilityPolicy()
|
|
116
|
+
record.organizationId = parsed.organizationId
|
|
117
|
+
record.tenantId = parsed.tenantId
|
|
118
|
+
record.storeId = parsed.storeId ?? null
|
|
119
|
+
record.productId = parsed.productId ?? null
|
|
120
|
+
record.variantId = parsed.variantId ?? null
|
|
121
|
+
record.isStockManaged = parsed.isStockManaged ?? false
|
|
122
|
+
record.allowBackorder = parsed.allowBackorder ?? false
|
|
123
|
+
record.backorderLeadTimeDays = parsed.backorderLeadTimeDays ?? null
|
|
124
|
+
record.preorderReleaseAt = parsed.preorderReleaseAt ?? null
|
|
125
|
+
record.lowStockThreshold = parsed.lowStockThreshold ?? null
|
|
126
|
+
record.minOrderQuantity = parsed.minOrderQuantity ?? null
|
|
127
|
+
record.maxOrderQuantity = parsed.maxOrderQuantity ?? null
|
|
128
|
+
record.quantityIncrement = parsed.quantityIncrement ?? null
|
|
129
|
+
record.hideWhenOutOfStock = parsed.hideWhenOutOfStock ?? false
|
|
130
|
+
record.isActive = parsed.isActive ?? true
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await runCrudCommandWrite({
|
|
134
|
+
ctx,
|
|
135
|
+
entityId: AVAILABILITY_POLICY_ENTITY_ID,
|
|
136
|
+
action: 'created',
|
|
137
|
+
scope: { tenantId: record.tenantId, organizationId: record.organizationId },
|
|
138
|
+
events: policyCrudEvents,
|
|
139
|
+
sideEffect: () => ({
|
|
140
|
+
entity: record,
|
|
141
|
+
identifiers: { id: record.id, organizationId: record.organizationId, tenantId: record.tenantId },
|
|
142
|
+
}),
|
|
143
|
+
phases: [({ em }) => { em.persist(record) }],
|
|
144
|
+
})
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (isUniqueViolation(err, 'availability_policies_scope_target_unique')) {
|
|
147
|
+
const { translate } = await resolveTranslations()
|
|
148
|
+
throw conflict(translate('availability.policies.errors.duplicateTarget', 'A policy already exists for this store/product/variant combination.'))
|
|
149
|
+
}
|
|
150
|
+
throw err
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { policyId: record.id }
|
|
154
|
+
},
|
|
155
|
+
captureAfter: async (_input, result, ctx) => {
|
|
156
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
157
|
+
return loadSnapshot(em, result.policyId, ctx)
|
|
158
|
+
},
|
|
159
|
+
buildLog: async ({ snapshots }) => {
|
|
160
|
+
const after = snapshots.after as AvailabilityPolicySnapshot | undefined
|
|
161
|
+
if (!after) return null
|
|
162
|
+
const { translate } = await resolveTranslations()
|
|
163
|
+
return {
|
|
164
|
+
actionLabel: translate('availability.audit.create', 'Create availability policy'),
|
|
165
|
+
resourceKind: 'availability.policy',
|
|
166
|
+
resourceId: after.id,
|
|
167
|
+
tenantId: after.tenantId,
|
|
168
|
+
organizationId: after.organizationId,
|
|
169
|
+
snapshotAfter: after,
|
|
170
|
+
payload: { undo: { after } },
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
undo: async ({ logEntry, ctx }) => {
|
|
174
|
+
const payload = extractUndoPayload<AvailabilityPolicyUndoPayload>(logEntry)
|
|
175
|
+
const after = payload?.after ?? null
|
|
176
|
+
if (!after) return
|
|
177
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
178
|
+
const record = await em.findOne(AvailabilityPolicy, { id: after.id })
|
|
179
|
+
if (!record) return
|
|
180
|
+
record.deletedAt = new Date()
|
|
181
|
+
await em.flush()
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const updatePolicyCommand: CommandHandler<AvailabilityPolicyUpdateInput, { policyId: string }> = {
|
|
186
|
+
id: 'availability.policies.update',
|
|
187
|
+
async prepare(input, ctx) {
|
|
188
|
+
requireId(input.id, 'Availability policy ID is required')
|
|
189
|
+
const em = ctx.container.resolve('em') as EntityManager
|
|
190
|
+
const before = await loadSnapshot(em, input.id, ctx)
|
|
191
|
+
return { before }
|
|
192
|
+
},
|
|
193
|
+
async execute(input, ctx) {
|
|
194
|
+
const parsed = availabilityPolicyUpdateSchema.parse(input)
|
|
195
|
+
requireId(parsed.id, 'Availability policy ID is required')
|
|
196
|
+
|
|
197
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
198
|
+
const record = await em.findOne(AvailabilityPolicy, buildAvailabilityPolicyCommandWhere<AvailabilityPolicy>(ctx, { id: parsed.id }))
|
|
199
|
+
if (!record) throw new CrudHttpError(404, { error: 'Availability policy not found' })
|
|
200
|
+
ensureAvailabilityPolicyCommandScope(ctx, record)
|
|
201
|
+
|
|
202
|
+
const merged = {
|
|
203
|
+
storeId: parsed.storeId !== undefined ? parsed.storeId : record.storeId,
|
|
204
|
+
productId: parsed.productId !== undefined ? parsed.productId : record.productId,
|
|
205
|
+
variantId: parsed.variantId !== undefined ? parsed.variantId : record.variantId,
|
|
206
|
+
isStockManaged: parsed.isStockManaged !== undefined ? parsed.isStockManaged : record.isStockManaged,
|
|
207
|
+
allowBackorder: parsed.allowBackorder !== undefined ? parsed.allowBackorder : record.allowBackorder,
|
|
208
|
+
backorderLeadTimeDays:
|
|
209
|
+
parsed.backorderLeadTimeDays !== undefined ? parsed.backorderLeadTimeDays : record.backorderLeadTimeDays,
|
|
210
|
+
minOrderQuantity: parsed.minOrderQuantity !== undefined ? parsed.minOrderQuantity : record.minOrderQuantity,
|
|
211
|
+
maxOrderQuantity: parsed.maxOrderQuantity !== undefined ? parsed.maxOrderQuantity : record.maxOrderQuantity,
|
|
212
|
+
}
|
|
213
|
+
availabilityPolicyMergedConstraintsSchema.parse(merged)
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
await runCrudCommandWrite({
|
|
217
|
+
ctx,
|
|
218
|
+
em,
|
|
219
|
+
entityId: AVAILABILITY_POLICY_ENTITY_ID,
|
|
220
|
+
action: 'updated',
|
|
221
|
+
scope: { tenantId: record.tenantId, organizationId: record.organizationId },
|
|
222
|
+
events: policyCrudEvents,
|
|
223
|
+
sideEffect: () => ({
|
|
224
|
+
entity: record,
|
|
225
|
+
identifiers: { id: record.id, organizationId: record.organizationId, tenantId: record.tenantId },
|
|
226
|
+
}),
|
|
227
|
+
phases: [
|
|
228
|
+
({ em: forkedEm }) => {
|
|
229
|
+
const target = forkedEm.getReference(AvailabilityPolicy, record.id)
|
|
230
|
+
if (parsed.storeId !== undefined) target.storeId = parsed.storeId
|
|
231
|
+
if (parsed.productId !== undefined) target.productId = parsed.productId
|
|
232
|
+
if (parsed.variantId !== undefined) target.variantId = parsed.variantId
|
|
233
|
+
if (parsed.isStockManaged !== undefined) target.isStockManaged = parsed.isStockManaged
|
|
234
|
+
if (parsed.allowBackorder !== undefined) target.allowBackorder = parsed.allowBackorder
|
|
235
|
+
if (parsed.backorderLeadTimeDays !== undefined) target.backorderLeadTimeDays = parsed.backorderLeadTimeDays
|
|
236
|
+
if (parsed.preorderReleaseAt !== undefined) target.preorderReleaseAt = parsed.preorderReleaseAt
|
|
237
|
+
if (parsed.lowStockThreshold !== undefined) target.lowStockThreshold = parsed.lowStockThreshold
|
|
238
|
+
if (parsed.minOrderQuantity !== undefined) target.minOrderQuantity = parsed.minOrderQuantity
|
|
239
|
+
if (parsed.maxOrderQuantity !== undefined) target.maxOrderQuantity = parsed.maxOrderQuantity
|
|
240
|
+
if (parsed.quantityIncrement !== undefined) target.quantityIncrement = parsed.quantityIncrement
|
|
241
|
+
if (parsed.hideWhenOutOfStock !== undefined) target.hideWhenOutOfStock = parsed.hideWhenOutOfStock
|
|
242
|
+
if (parsed.isActive !== undefined) target.isActive = parsed.isActive
|
|
243
|
+
target.updatedAt = new Date()
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
})
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (isUniqueViolation(err, 'availability_policies_scope_target_unique')) {
|
|
249
|
+
const { translate } = await resolveTranslations()
|
|
250
|
+
throw conflict(translate('availability.policies.errors.duplicateTarget', 'A policy already exists for this store/product/variant combination.'))
|
|
251
|
+
}
|
|
252
|
+
throw err
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return { policyId: record.id }
|
|
256
|
+
},
|
|
257
|
+
captureAfter: async (_input, result, ctx) => {
|
|
258
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
259
|
+
return loadSnapshot(em, result.policyId, ctx)
|
|
260
|
+
},
|
|
261
|
+
buildLog: async ({ snapshots }) => {
|
|
262
|
+
const before = snapshots.before as AvailabilityPolicySnapshot | undefined
|
|
263
|
+
const after = snapshots.after as AvailabilityPolicySnapshot | undefined
|
|
264
|
+
if (!after) return null
|
|
265
|
+
const { translate } = await resolveTranslations()
|
|
266
|
+
return {
|
|
267
|
+
actionLabel: translate('availability.audit.update', 'Update availability policy'),
|
|
268
|
+
resourceKind: 'availability.policy',
|
|
269
|
+
resourceId: after.id,
|
|
270
|
+
tenantId: after.tenantId,
|
|
271
|
+
organizationId: after.organizationId,
|
|
272
|
+
snapshotBefore: before ?? undefined,
|
|
273
|
+
snapshotAfter: after,
|
|
274
|
+
payload: { undo: { before, after } },
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
undo: async ({ logEntry, ctx }) => {
|
|
278
|
+
const payload = extractUndoPayload<AvailabilityPolicyUndoPayload>(logEntry)
|
|
279
|
+
const before = payload?.before ?? null
|
|
280
|
+
if (!before) return
|
|
281
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
282
|
+
const record = await em.findOne(AvailabilityPolicy, { id: before.id })
|
|
283
|
+
if (!record) return
|
|
284
|
+
applyUndoSnapshot(record, before)
|
|
285
|
+
await em.flush()
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const deletePolicyCommand: CommandHandler<{ id: string; organizationId: string; tenantId: string }, { policyId: string }> = {
|
|
290
|
+
id: 'availability.policies.delete',
|
|
291
|
+
async prepare(input, ctx) {
|
|
292
|
+
requireId(input.id, 'Availability policy ID is required')
|
|
293
|
+
const em = ctx.container.resolve('em') as EntityManager
|
|
294
|
+
const before = await loadSnapshot(em, input.id, ctx)
|
|
295
|
+
return { before }
|
|
296
|
+
},
|
|
297
|
+
async execute(input, ctx) {
|
|
298
|
+
const parsed = availabilityPolicyDeleteSchema.parse(input)
|
|
299
|
+
requireId(parsed.id, 'Availability policy ID is required')
|
|
300
|
+
|
|
301
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
302
|
+
const record = await em.findOne(AvailabilityPolicy, buildAvailabilityPolicyCommandWhere<AvailabilityPolicy>(ctx, { id: parsed.id }))
|
|
303
|
+
if (!record) throw new CrudHttpError(404, { error: 'Availability policy not found' })
|
|
304
|
+
ensureAvailabilityPolicyCommandScope(ctx, record)
|
|
305
|
+
|
|
306
|
+
await runCrudCommandWrite({
|
|
307
|
+
ctx,
|
|
308
|
+
em,
|
|
309
|
+
entityId: AVAILABILITY_POLICY_ENTITY_ID,
|
|
310
|
+
action: 'deleted',
|
|
311
|
+
scope: { tenantId: record.tenantId, organizationId: record.organizationId },
|
|
312
|
+
events: policyCrudEvents,
|
|
313
|
+
sideEffect: () => ({
|
|
314
|
+
entity: record,
|
|
315
|
+
identifiers: { id: record.id, organizationId: record.organizationId, tenantId: record.tenantId },
|
|
316
|
+
}),
|
|
317
|
+
phases: [
|
|
318
|
+
({ em: forkedEm }) => {
|
|
319
|
+
const target = forkedEm.getReference(AvailabilityPolicy, record.id)
|
|
320
|
+
target.deletedAt = new Date()
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
return { policyId: record.id }
|
|
326
|
+
},
|
|
327
|
+
buildLog: async ({ snapshots }) => {
|
|
328
|
+
const before = snapshots.before as AvailabilityPolicySnapshot | undefined
|
|
329
|
+
if (!before) return null
|
|
330
|
+
const { translate } = await resolveTranslations()
|
|
331
|
+
return {
|
|
332
|
+
actionLabel: translate('availability.audit.delete', 'Delete availability policy'),
|
|
333
|
+
resourceKind: 'availability.policy',
|
|
334
|
+
resourceId: before.id,
|
|
335
|
+
tenantId: before.tenantId,
|
|
336
|
+
organizationId: before.organizationId,
|
|
337
|
+
snapshotBefore: before,
|
|
338
|
+
payload: { undo: { before } },
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
undo: async ({ logEntry, ctx }) => {
|
|
342
|
+
const payload = extractUndoPayload<AvailabilityPolicyUndoPayload>(logEntry)
|
|
343
|
+
const before = payload?.before ?? null
|
|
344
|
+
if (!before) return
|
|
345
|
+
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
346
|
+
const record = await em.findOne(AvailabilityPolicy, { id: before.id })
|
|
347
|
+
if (!record) return
|
|
348
|
+
record.deletedAt = null
|
|
349
|
+
record.updatedAt = new Date()
|
|
350
|
+
await em.flush()
|
|
351
|
+
},
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
registerCommand(createPolicyCommand)
|
|
355
|
+
registerCommand(updatePolicyCommand)
|
|
356
|
+
registerCommand(deletePolicyCommand)
|
|
357
|
+
|
|
358
|
+
export { createPolicyCommand, updatePolicyCommand, deletePolicyCommand }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { FilterQuery } from '@mikro-orm/core'
|
|
2
|
+
import type { CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
3
|
+
import { buildScopedWhere } from '@open-mercato/shared/lib/api/crud'
|
|
4
|
+
import { ensureOrganizationScope, ensureTenantScope } from '@open-mercato/shared/lib/commands/scope'
|
|
5
|
+
|
|
6
|
+
type AvailabilityPolicyScopedRecord = { organizationId: string; tenantId: string }
|
|
7
|
+
|
|
8
|
+
export function buildAvailabilityPolicyCommandWhere<T extends object>(
|
|
9
|
+
ctx: CommandRuntimeContext,
|
|
10
|
+
base: FilterQuery<T>,
|
|
11
|
+
): FilterQuery<T> {
|
|
12
|
+
const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? undefined
|
|
13
|
+
return buildScopedWhere(base as Record<string, unknown>, {
|
|
14
|
+
organizationId,
|
|
15
|
+
organizationIds: ctx.organizationIds ?? undefined,
|
|
16
|
+
tenantId: ctx.auth?.tenantId ?? undefined,
|
|
17
|
+
}) as FilterQuery<T>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function ensureAvailabilityPolicyCommandScope(
|
|
21
|
+
ctx: CommandRuntimeContext,
|
|
22
|
+
record: AvailabilityPolicyScopedRecord,
|
|
23
|
+
): void {
|
|
24
|
+
ensureTenantScope(ctx, record.tenantId)
|
|
25
|
+
ensureOrganizationScope(ctx, record.organizationId)
|
|
26
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { OptionalProps } from '@mikro-orm/core'
|
|
2
|
+
import { Entity, Index, PrimaryKey, Property } from '@mikro-orm/decorators/legacy'
|
|
3
|
+
|
|
4
|
+
@Entity({ tableName: 'availability_policies' })
|
|
5
|
+
@Index({ name: 'availability_policies_scope_idx', properties: ['tenantId', 'organizationId'] })
|
|
6
|
+
@Index({
|
|
7
|
+
// Exactly one policy row per (store, product, variant) combination — nullable columns mean
|
|
8
|
+
// "applies to all stores" / "product-level" / "store-level default". Postgres treats NULLs as
|
|
9
|
+
// distinct by default, so the index is declared NULLS NOT DISTINCT (mirrors user_devices).
|
|
10
|
+
name: 'availability_policies_scope_target_unique',
|
|
11
|
+
expression:
|
|
12
|
+
'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',
|
|
13
|
+
})
|
|
14
|
+
export class AvailabilityPolicy {
|
|
15
|
+
[OptionalProps]?:
|
|
16
|
+
| 'storeId'
|
|
17
|
+
| 'productId'
|
|
18
|
+
| 'variantId'
|
|
19
|
+
| 'isStockManaged'
|
|
20
|
+
| 'allowBackorder'
|
|
21
|
+
| 'backorderLeadTimeDays'
|
|
22
|
+
| 'preorderReleaseAt'
|
|
23
|
+
| 'lowStockThreshold'
|
|
24
|
+
| 'minOrderQuantity'
|
|
25
|
+
| 'maxOrderQuantity'
|
|
26
|
+
| 'quantityIncrement'
|
|
27
|
+
| 'hideWhenOutOfStock'
|
|
28
|
+
| 'isActive'
|
|
29
|
+
| 'createdAt'
|
|
30
|
+
| 'updatedAt'
|
|
31
|
+
| 'deletedAt'
|
|
32
|
+
|
|
33
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
34
|
+
id!: string
|
|
35
|
+
|
|
36
|
+
@Property({ name: 'organization_id', type: 'uuid' })
|
|
37
|
+
organizationId!: string
|
|
38
|
+
|
|
39
|
+
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
40
|
+
tenantId!: string
|
|
41
|
+
|
|
42
|
+
/** `catalog.EcommerceStore`-shaped id (not yet a real entity in this repo) — null = all stores. */
|
|
43
|
+
@Property({ name: 'store_id', type: 'uuid', nullable: true })
|
|
44
|
+
storeId?: string | null
|
|
45
|
+
|
|
46
|
+
/** `catalog.CatalogProduct.id`. */
|
|
47
|
+
@Property({ name: 'product_id', type: 'uuid', nullable: true })
|
|
48
|
+
productId?: string | null
|
|
49
|
+
|
|
50
|
+
/** `catalog.CatalogProductVariant.id`. Non-null requires `productId` non-null (validated). */
|
|
51
|
+
@Property({ name: 'variant_id', type: 'uuid', nullable: true })
|
|
52
|
+
variantId?: string | null
|
|
53
|
+
|
|
54
|
+
@Property({ name: 'is_stock_managed', type: 'boolean', default: false })
|
|
55
|
+
isStockManaged: boolean = false
|
|
56
|
+
|
|
57
|
+
@Property({ name: 'allow_backorder', type: 'boolean', default: false })
|
|
58
|
+
allowBackorder: boolean = false
|
|
59
|
+
|
|
60
|
+
@Property({ name: 'backorder_lead_time_days', type: 'integer', nullable: true })
|
|
61
|
+
backorderLeadTimeDays?: number | null
|
|
62
|
+
|
|
63
|
+
@Property({ name: 'preorder_release_at', type: Date, nullable: true })
|
|
64
|
+
preorderReleaseAt?: Date | null
|
|
65
|
+
|
|
66
|
+
@Property({ name: 'low_stock_threshold', type: 'integer', nullable: true })
|
|
67
|
+
lowStockThreshold?: number | null
|
|
68
|
+
|
|
69
|
+
@Property({ name: 'min_order_quantity', type: 'integer', nullable: true })
|
|
70
|
+
minOrderQuantity?: number | null
|
|
71
|
+
|
|
72
|
+
@Property({ name: 'max_order_quantity', type: 'integer', nullable: true })
|
|
73
|
+
maxOrderQuantity?: number | null
|
|
74
|
+
|
|
75
|
+
@Property({ name: 'quantity_increment', type: 'integer', nullable: true })
|
|
76
|
+
quantityIncrement?: number | null
|
|
77
|
+
|
|
78
|
+
@Property({ name: 'hide_when_out_of_stock', type: 'boolean', default: false })
|
|
79
|
+
hideWhenOutOfStock: boolean = false
|
|
80
|
+
|
|
81
|
+
@Property({ name: 'is_active', type: 'boolean', default: true })
|
|
82
|
+
isActive: boolean = true
|
|
83
|
+
|
|
84
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
85
|
+
createdAt: Date = new Date()
|
|
86
|
+
|
|
87
|
+
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
88
|
+
updatedAt: Date = new Date()
|
|
89
|
+
|
|
90
|
+
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
91
|
+
deletedAt?: Date | null
|
|
92
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
|
|
3
|
+
const uuid = () => z.string().uuid()
|
|
4
|
+
|
|
5
|
+
const scopedSchema = z.object({
|
|
6
|
+
organizationId: uuid(),
|
|
7
|
+
tenantId: uuid(),
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const nonNegativeInt = () => z.number().int().min(0)
|
|
11
|
+
|
|
12
|
+
const policyFieldsSchema = z.object({
|
|
13
|
+
storeId: uuid().nullable().optional(),
|
|
14
|
+
productId: uuid().nullable().optional(),
|
|
15
|
+
variantId: uuid().nullable().optional(),
|
|
16
|
+
isStockManaged: z.boolean().optional(),
|
|
17
|
+
allowBackorder: z.boolean().optional(),
|
|
18
|
+
backorderLeadTimeDays: nonNegativeInt().nullable().optional(),
|
|
19
|
+
preorderReleaseAt: z.coerce.date().nullable().optional(),
|
|
20
|
+
lowStockThreshold: nonNegativeInt().nullable().optional(),
|
|
21
|
+
minOrderQuantity: nonNegativeInt().nullable().optional(),
|
|
22
|
+
maxOrderQuantity: nonNegativeInt().nullable().optional(),
|
|
23
|
+
quantityIncrement: nonNegativeInt().nullable().optional(),
|
|
24
|
+
hideWhenOutOfStock: z.boolean().optional(),
|
|
25
|
+
isActive: z.boolean().optional(),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
export const AVAILABILITY_POLICY_VARIANT_REQUIRES_PRODUCT_MESSAGE_KEY =
|
|
29
|
+
'availability.policies.errors.variantRequiresProduct'
|
|
30
|
+
export const AVAILABILITY_POLICY_BACKORDER_REQUIRES_LEAD_TIME_MESSAGE_KEY =
|
|
31
|
+
'availability.policies.errors.backorderRequiresLeadTime'
|
|
32
|
+
export const AVAILABILITY_POLICY_MAX_BELOW_MIN_MESSAGE_KEY = 'availability.policies.errors.maxBelowMin'
|
|
33
|
+
|
|
34
|
+
function refinePolicyConstraints<T extends z.ZodTypeAny>(schema: T) {
|
|
35
|
+
return schema
|
|
36
|
+
.refine((val: any) => !(val.variantId && !val.productId), {
|
|
37
|
+
message: AVAILABILITY_POLICY_VARIANT_REQUIRES_PRODUCT_MESSAGE_KEY,
|
|
38
|
+
path: ['variantId'],
|
|
39
|
+
})
|
|
40
|
+
.refine((val: any) => !(val.allowBackorder && val.backorderLeadTimeDays == null), {
|
|
41
|
+
message: AVAILABILITY_POLICY_BACKORDER_REQUIRES_LEAD_TIME_MESSAGE_KEY,
|
|
42
|
+
path: ['backorderLeadTimeDays'],
|
|
43
|
+
})
|
|
44
|
+
.refine(
|
|
45
|
+
(val: any) =>
|
|
46
|
+
!(val.minOrderQuantity != null && val.maxOrderQuantity != null && val.maxOrderQuantity < val.minOrderQuantity),
|
|
47
|
+
{ message: AVAILABILITY_POLICY_MAX_BELOW_MIN_MESSAGE_KEY, path: ['maxOrderQuantity'] },
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const availabilityPolicyCreateSchema = refinePolicyConstraints(
|
|
52
|
+
scopedSchema.extend(policyFieldsSchema.shape),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
// Cross-field constraints (variant requires product, backorder requires lead time, max >= min)
|
|
56
|
+
// are NOT re-checked here at the zod layer: a partial update payload may touch only one side of
|
|
57
|
+
// a constraint while the other side's value already satisfies it in the stored row. The update
|
|
58
|
+
// command (commands/policies.ts) re-validates the same constraints against the merged
|
|
59
|
+
// (existing-row + patch) record, where the full picture is available.
|
|
60
|
+
export const availabilityPolicyUpdateSchema = z
|
|
61
|
+
.object({ id: uuid() })
|
|
62
|
+
.merge(scopedSchema.extend(policyFieldsSchema.shape).partial())
|
|
63
|
+
|
|
64
|
+
export const availabilityPolicyDeleteSchema = scopedSchema.extend({ id: uuid() })
|
|
65
|
+
|
|
66
|
+
/** Re-applies the create-time cross-field constraints to a merged (existing + patch) record. Used by the update command. */
|
|
67
|
+
export const availabilityPolicyMergedConstraintsSchema = refinePolicyConstraints(policyFieldsSchema)
|
|
68
|
+
|
|
69
|
+
export type AvailabilityPolicyCreateInput = z.infer<typeof availabilityPolicyCreateSchema>
|
|
70
|
+
export type AvailabilityPolicyUpdateInput = z.infer<typeof availabilityPolicyUpdateSchema>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { asValue } from 'awilix'
|
|
2
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
3
|
+
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { setCatalogOnlyPolicyLookup, availabilityItemKey } from '@open-mercato/shared/lib/availability'
|
|
5
|
+
import type { CatalogOnlyPolicyOverride } from '@open-mercato/shared/lib/availability'
|
|
6
|
+
import { AvailabilityPolicy } from './data/entities'
|
|
7
|
+
import { createPolicyResolutionService } from './lib/policyResolution'
|
|
8
|
+
|
|
9
|
+
export function register(container: AppContainer) {
|
|
10
|
+
container.register({
|
|
11
|
+
AvailabilityPolicy: asValue(AvailabilityPolicy),
|
|
12
|
+
policyResolutionService: {
|
|
13
|
+
resolve: (c) => createPolicyResolutionService(c),
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// Wires the shared catalog-only fallback's optional policy hook (decision 2,
|
|
18
|
+
// PLAN.md § Key design decisions) — closure captures the boot container, the
|
|
19
|
+
// same technique `wms/di.ts` uses for its own provider registration.
|
|
20
|
+
setCatalogOnlyPolicyLookup(async (query) => {
|
|
21
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
22
|
+
const service = createPolicyResolutionService(container)
|
|
23
|
+
const overrides: Record<string, CatalogOnlyPolicyOverride | null> = {}
|
|
24
|
+
|
|
25
|
+
// Batched (R4): one resolveMany() call for the whole item set, not one
|
|
26
|
+
// resolve() per item — the same discipline wms's provider follows.
|
|
27
|
+
const resolved = await service.resolveMany(
|
|
28
|
+
em,
|
|
29
|
+
query.items.map((item) => ({
|
|
30
|
+
tenantId: query.tenantId,
|
|
31
|
+
organizationId: query.organizationId,
|
|
32
|
+
storeId: query.storeId ?? null,
|
|
33
|
+
productId: item.catalogProductId,
|
|
34
|
+
variantId: item.catalogVariantId ?? null,
|
|
35
|
+
})),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
query.items.forEach((item, index) => {
|
|
39
|
+
const policy = resolved[index]
|
|
40
|
+
overrides[availabilityItemKey(item)] = {
|
|
41
|
+
isStockManaged: policy.isStockManaged.value,
|
|
42
|
+
isActive: policy.isActive.value,
|
|
43
|
+
preorderReleaseAt: policy.preorderReleaseAt.value ? policy.preorderReleaseAt.value.toISOString() : null,
|
|
44
|
+
policySourceId:
|
|
45
|
+
policy.preorderReleaseAt.policySourceId
|
|
46
|
+
?? policy.isActive.policySourceId
|
|
47
|
+
?? policy.isStockManaged.policySourceId
|
|
48
|
+
?? null,
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
return overrides
|
|
53
|
+
})
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createModuleEvents } from '@open-mercato/shared/modules/events'
|
|
2
|
+
|
|
3
|
+
const events = [
|
|
4
|
+
{ id: 'availability.policy.created', label: 'Availability Policy Created', entity: 'policy', category: 'crud' },
|
|
5
|
+
{ id: 'availability.policy.updated', label: 'Availability Policy Updated', entity: 'policy', category: 'crud' },
|
|
6
|
+
{ id: 'availability.policy.deleted', label: 'Availability Policy Deleted', entity: 'policy', category: 'crud' },
|
|
7
|
+
] as const
|
|
8
|
+
|
|
9
|
+
export const eventsConfig = createModuleEvents({
|
|
10
|
+
moduleId: 'availability',
|
|
11
|
+
events,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const emitAvailabilityEvent = eventsConfig.emit
|
|
15
|
+
export type AvailabilityEventId = typeof events[number]['id']
|
|
16
|
+
|
|
17
|
+
export default eventsConfig
|