@open-mercato/core 0.8.1-develop.7266.1.8e520bbe03 → 0.8.1-develop.7267.1.2e95af80a6

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 (124) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/generated/entities.ids.generated.js +0 -4
  3. package/dist/generated/entities.ids.generated.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +0 -21
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/modules/wms/di.js +0 -3
  7. package/dist/modules/wms/di.js.map +2 -2
  8. package/generated/entities.ids.generated.ts +0 -4
  9. package/generated/entity-fields-registry.ts +0 -21
  10. package/package.json +7 -7
  11. package/src/modules/auth/i18n/de.json +0 -3
  12. package/src/modules/auth/i18n/en.json +0 -3
  13. package/src/modules/auth/i18n/es.json +0 -3
  14. package/src/modules/auth/i18n/ko.json +0 -3
  15. package/src/modules/auth/i18n/pl.json +0 -3
  16. package/src/modules/wms/di.ts +0 -6
  17. package/dist/generated/entities/availability_policy/index.js +0 -41
  18. package/dist/generated/entities/availability_policy/index.js.map +0 -7
  19. package/dist/modules/availability/acl.js +0 -16
  20. package/dist/modules/availability/acl.js.map +0 -7
  21. package/dist/modules/availability/api/check/route.js +0 -95
  22. package/dist/modules/availability/api/check/route.js.map +0 -7
  23. package/dist/modules/availability/api/openapi.js +0 -27
  24. package/dist/modules/availability/api/openapi.js.map +0 -7
  25. package/dist/modules/availability/api/policies/resolve-preview/route.js +0 -58
  26. package/dist/modules/availability/api/policies/resolve-preview/route.js.map +0 -7
  27. package/dist/modules/availability/api/policies/route.js +0 -188
  28. package/dist/modules/availability/api/policies/route.js.map +0 -7
  29. package/dist/modules/availability/backend/availability/check/page.js +0 -185
  30. package/dist/modules/availability/backend/availability/check/page.js.map +0 -7
  31. package/dist/modules/availability/backend/availability/check/page.meta.js +0 -16
  32. package/dist/modules/availability/backend/availability/check/page.meta.js.map +0 -7
  33. package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js +0 -79
  34. package/dist/modules/availability/backend/availability/policies/ResolutionPreviewPanel.js.map +0 -7
  35. package/dist/modules/availability/backend/availability/policies/ScopeFields.js +0 -53
  36. package/dist/modules/availability/backend/availability/policies/ScopeFields.js.map +0 -7
  37. package/dist/modules/availability/backend/availability/policies/[id]/page.js +0 -152
  38. package/dist/modules/availability/backend/availability/policies/[id]/page.js.map +0 -7
  39. package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js +0 -12
  40. package/dist/modules/availability/backend/availability/policies/[id]/page.meta.js.map +0 -7
  41. package/dist/modules/availability/backend/availability/policies/create/page.js +0 -94
  42. package/dist/modules/availability/backend/availability/policies/create/page.js.map +0 -7
  43. package/dist/modules/availability/backend/availability/policies/create/page.meta.js +0 -12
  44. package/dist/modules/availability/backend/availability/policies/create/page.meta.js.map +0 -7
  45. package/dist/modules/availability/backend/availability/policies/formGroups.js +0 -75
  46. package/dist/modules/availability/backend/availability/policies/formGroups.js.map +0 -7
  47. package/dist/modules/availability/backend/availability/policies/page.js +0 -176
  48. package/dist/modules/availability/backend/availability/policies/page.js.map +0 -7
  49. package/dist/modules/availability/backend/availability/policies/page.meta.js +0 -16
  50. package/dist/modules/availability/backend/availability/policies/page.meta.js.map +0 -7
  51. package/dist/modules/availability/commands/policies.js +0 -312
  52. package/dist/modules/availability/commands/policies.js.map +0 -7
  53. package/dist/modules/availability/commands/scope.js +0 -19
  54. package/dist/modules/availability/commands/scope.js.map +0 -7
  55. package/dist/modules/availability/data/entities.js +0 -95
  56. package/dist/modules/availability/data/entities.js.map +0 -7
  57. package/dist/modules/availability/data/validators.js +0 -53
  58. package/dist/modules/availability/data/validators.js.map +0 -7
  59. package/dist/modules/availability/di.js +0 -41
  60. package/dist/modules/availability/di.js.map +0 -7
  61. package/dist/modules/availability/events.js +0 -18
  62. package/dist/modules/availability/events.js.map +0 -7
  63. package/dist/modules/availability/index.js +0 -15
  64. package/dist/modules/availability/index.js.map +0 -7
  65. package/dist/modules/availability/lib/policyResolution.js +0 -155
  66. package/dist/modules/availability/lib/policyResolution.js.map +0 -7
  67. package/dist/modules/availability/lib/tryResolve.js +0 -11
  68. package/dist/modules/availability/lib/tryResolve.js.map +0 -7
  69. package/dist/modules/availability/migrations/Migration20260922095844_availability.js +0 -16
  70. package/dist/modules/availability/migrations/Migration20260922095844_availability.js.map +0 -7
  71. package/dist/modules/availability/setup.js +0 -12
  72. package/dist/modules/availability/setup.js.map +0 -7
  73. package/dist/modules/wms/lib/availabilityCache.js +0 -85
  74. package/dist/modules/wms/lib/availabilityCache.js.map +0 -7
  75. package/dist/modules/wms/lib/availabilityCalculation.js +0 -178
  76. package/dist/modules/wms/lib/availabilityCalculation.js.map +0 -7
  77. package/dist/modules/wms/lib/availabilityProvider.js +0 -14
  78. package/dist/modules/wms/lib/availabilityProvider.js.map +0 -7
  79. package/dist/modules/wms/lib/sqlInClause.js +0 -9
  80. package/dist/modules/wms/lib/sqlInClause.js.map +0 -7
  81. package/dist/modules/wms/lib/tryResolve.js +0 -11
  82. package/dist/modules/wms/lib/tryResolve.js.map +0 -7
  83. package/dist/modules/wms/subscribers/invalidate-availability-cache.js +0 -16
  84. package/dist/modules/wms/subscribers/invalidate-availability-cache.js.map +0 -7
  85. package/generated/entities/availability_policy/index.ts +0 -19
  86. package/src/modules/availability/acl.ts +0 -12
  87. package/src/modules/availability/api/check/route.ts +0 -114
  88. package/src/modules/availability/api/openapi.ts +0 -28
  89. package/src/modules/availability/api/policies/resolve-preview/route.ts +0 -67
  90. package/src/modules/availability/api/policies/route.ts +0 -224
  91. package/src/modules/availability/backend/availability/check/page.meta.ts +0 -12
  92. package/src/modules/availability/backend/availability/check/page.tsx +0 -223
  93. package/src/modules/availability/backend/availability/policies/ResolutionPreviewPanel.tsx +0 -106
  94. package/src/modules/availability/backend/availability/policies/ScopeFields.tsx +0 -55
  95. package/src/modules/availability/backend/availability/policies/[id]/page.meta.ts +0 -8
  96. package/src/modules/availability/backend/availability/policies/[id]/page.tsx +0 -182
  97. package/src/modules/availability/backend/availability/policies/create/page.meta.ts +0 -8
  98. package/src/modules/availability/backend/availability/policies/create/page.tsx +0 -105
  99. package/src/modules/availability/backend/availability/policies/formGroups.ts +0 -77
  100. package/src/modules/availability/backend/availability/policies/page.meta.ts +0 -12
  101. package/src/modules/availability/backend/availability/policies/page.tsx +0 -196
  102. package/src/modules/availability/commands/policies.ts +0 -358
  103. package/src/modules/availability/commands/scope.ts +0 -26
  104. package/src/modules/availability/data/entities.ts +0 -92
  105. package/src/modules/availability/data/validators.ts +0 -70
  106. package/src/modules/availability/di.ts +0 -54
  107. package/src/modules/availability/events.ts +0 -17
  108. package/src/modules/availability/i18n/de.json +0 -81
  109. package/src/modules/availability/i18n/en.json +0 -81
  110. package/src/modules/availability/i18n/es.json +0 -81
  111. package/src/modules/availability/i18n/ko.json +0 -81
  112. package/src/modules/availability/i18n/pl.json +0 -81
  113. package/src/modules/availability/index.ts +0 -13
  114. package/src/modules/availability/lib/policyResolution.ts +0 -264
  115. package/src/modules/availability/lib/tryResolve.ts +0 -8
  116. package/src/modules/availability/migrations/.snapshot-open-mercato.json +0 -375
  117. package/src/modules/availability/migrations/Migration20260922095844_availability.ts +0 -13
  118. package/src/modules/availability/setup.ts +0 -10
  119. package/src/modules/wms/lib/availabilityCache.ts +0 -136
  120. package/src/modules/wms/lib/availabilityCalculation.ts +0 -289
  121. package/src/modules/wms/lib/availabilityProvider.ts +0 -29
  122. package/src/modules/wms/lib/sqlInClause.ts +0 -21
  123. package/src/modules/wms/lib/tryResolve.ts +0 -8
  124. package/src/modules/wms/subscribers/invalidate-availability-cache.ts +0 -30
@@ -1,358 +0,0 @@
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 }
@@ -1,26 +0,0 @@
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
- }
@@ -1,92 +0,0 @@
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
- }
@@ -1,70 +0,0 @@
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>
@@ -1,54 +0,0 @@
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
- }
@@ -1,17 +0,0 @@
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