@open-mercato/core 0.10.1-develop.7670.1.7b5ad74924 → 0.10.1-develop.7672.1.c2e4c0b5df
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 +1 -1
- package/dist/modules/customers/commands/tags.js +155 -45
- package/dist/modules/customers/commands/tags.js.map +2 -2
- package/dist/modules/warranty_claims/commands/claims.js +2 -2
- package/dist/modules/warranty_claims/commands/claims.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customers/commands/tags.ts +177 -44
- package/src/modules/warranty_claims/commands/claims.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7672.1.c2e4c0b5df",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -265,16 +265,16 @@
|
|
|
265
265
|
"zod": "^4.4.3"
|
|
266
266
|
},
|
|
267
267
|
"peerDependencies": {
|
|
268
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
269
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
268
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7672.1.c2e4c0b5df",
|
|
269
|
+
"@open-mercato/shared": "0.10.1-develop.7672.1.c2e4c0b5df",
|
|
270
|
+
"@open-mercato/ui": "0.10.1-develop.7672.1.c2e4c0b5df",
|
|
271
271
|
"react": "^19.0.0",
|
|
272
272
|
"react-dom": "^19.0.0"
|
|
273
273
|
},
|
|
274
274
|
"devDependencies": {
|
|
275
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
276
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
275
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7672.1.c2e4c0b5df",
|
|
276
|
+
"@open-mercato/shared": "0.10.1-develop.7672.1.c2e4c0b5df",
|
|
277
|
+
"@open-mercato/ui": "0.10.1-develop.7672.1.c2e4c0b5df",
|
|
278
278
|
"@testing-library/dom": "^10.4.1",
|
|
279
279
|
"@testing-library/jest-dom": "^7.0.1",
|
|
280
280
|
"@testing-library/react": "^16.3.3",
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { registerCommand } from '@open-mercato/shared/lib/commands'
|
|
2
|
-
import type { CommandHandler } from '@open-mercato/shared/lib/commands'
|
|
2
|
+
import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
|
|
3
3
|
import { emitCrudSideEffects, emitCrudUndoSideEffects, buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'
|
|
4
4
|
import type { DataEngine } from '@open-mercato/shared/lib/data/engine'
|
|
5
5
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
6
|
-
import {
|
|
6
|
+
import { LockMode, type FilterQuery } from '@mikro-orm/core'
|
|
7
|
+
import { buildScopedWhere } from '@open-mercato/shared/lib/api/crud'
|
|
8
|
+
import { isUnrestrictedOrganizationScope } from '@open-mercato/shared/lib/auth/organizationAccess'
|
|
9
|
+
import { CustomerEntity, CustomerTag, CustomerTagAssignment } from '../data/entities'
|
|
7
10
|
import {
|
|
8
11
|
tagCreateSchema,
|
|
9
12
|
tagUpdateSchema,
|
|
@@ -19,11 +22,13 @@ import {
|
|
|
19
22
|
requireCustomerEntity,
|
|
20
23
|
ensureSameScope,
|
|
21
24
|
loadEntityTagIds,
|
|
25
|
+
emitQueryIndexUpsertEvents,
|
|
22
26
|
} from './shared'
|
|
23
27
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
24
|
-
import { CrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'
|
|
28
|
+
import { CrudHttpError, forbidden, notFound } from '@open-mercato/shared/lib/crud/errors'
|
|
25
29
|
import type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'
|
|
26
|
-
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
30
|
+
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
31
|
+
import { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'
|
|
27
32
|
import { emitCustomersEvent } from '../events'
|
|
28
33
|
import { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'
|
|
29
34
|
import { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'
|
|
@@ -56,6 +61,15 @@ type TagSnapshot = {
|
|
|
56
61
|
label: string
|
|
57
62
|
color: string | null
|
|
58
63
|
description: string | null
|
|
64
|
+
assignments?: TagMembershipSnapshot[]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
type TagMembershipSnapshot = {
|
|
68
|
+
id: string
|
|
69
|
+
entityId: string
|
|
70
|
+
organizationId: string
|
|
71
|
+
tenantId: string
|
|
72
|
+
createdAt: string
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
type TagUndoPayload = {
|
|
@@ -74,8 +88,40 @@ type TagAssignmentUndoPayload = {
|
|
|
74
88
|
before?: TagAssignmentSnapshot | null
|
|
75
89
|
}
|
|
76
90
|
|
|
77
|
-
|
|
78
|
-
const
|
|
91
|
+
function tagDeleteLookup(id: string, ctx: CommandRuntimeContext) {
|
|
92
|
+
const isSuperAdmin = ctx.auth?.isSuperAdmin === true
|
|
93
|
+
const organizationScope = ctx.organizationScope
|
|
94
|
+
const tenantId = ctx.auth ? (ctx.auth.tenantId ?? null) : (organizationScope?.tenantId ?? null)
|
|
95
|
+
if (ctx.auth && !isSuperAdmin && (!tenantId || (organizationScope && !organizationScope.tenantId))) {
|
|
96
|
+
throw forbidden()
|
|
97
|
+
}
|
|
98
|
+
let organizationIds: string[] | undefined
|
|
99
|
+
if (organizationScope) {
|
|
100
|
+
if (!isUnrestrictedOrganizationScope({ isSuperAdmin, allowedOrganizationIds: organizationScope.allowedIds })) {
|
|
101
|
+
organizationIds = organizationScope.allowedIds ?? []
|
|
102
|
+
}
|
|
103
|
+
} else if (!isSuperAdmin) {
|
|
104
|
+
const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId
|
|
105
|
+
if (organizationId) organizationIds = [organizationId]
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
where: buildScopedWhere({ id }, {
|
|
109
|
+
tenantId: tenantId ?? undefined,
|
|
110
|
+
organizationIds,
|
|
111
|
+
softDeleteField: null,
|
|
112
|
+
}) as FilterQuery<CustomerTag>,
|
|
113
|
+
scope: { tenantId, organizationId: organizationIds?.length === 1 ? organizationIds[0] : null },
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function loadTagSnapshot(
|
|
118
|
+
em: EntityManager,
|
|
119
|
+
id: string,
|
|
120
|
+
lookup?: ReturnType<typeof tagDeleteLookup>,
|
|
121
|
+
): Promise<TagSnapshot | null> {
|
|
122
|
+
const tag = lookup
|
|
123
|
+
? await findOneWithDecryption(em, CustomerTag, lookup.where, {}, lookup.scope)
|
|
124
|
+
: await em.findOne(CustomerTag, { id })
|
|
79
125
|
if (!tag) return null
|
|
80
126
|
return {
|
|
81
127
|
id: tag.id,
|
|
@@ -88,6 +134,46 @@ async function loadTagSnapshot(em: EntityManager, id: string): Promise<TagSnapsh
|
|
|
88
134
|
}
|
|
89
135
|
}
|
|
90
136
|
|
|
137
|
+
async function loadTagAssignments(em: EntityManager, tag: CustomerTag) {
|
|
138
|
+
const scope = { tenantId: tag.tenantId, organizationId: tag.organizationId }
|
|
139
|
+
return findWithDecryption(em, CustomerTagAssignment, { tag: tag.id, ...scope }, {}, scope)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function refreshTagMemberships(
|
|
143
|
+
ctx: CommandRuntimeContext,
|
|
144
|
+
tag: CustomerTag,
|
|
145
|
+
assignments: CustomerTagAssignment[],
|
|
146
|
+
action: 'created' | 'deleted',
|
|
147
|
+
) {
|
|
148
|
+
const dataEngine = ctx.container.resolve<DataEngine>('dataEngine')
|
|
149
|
+
for (const assignment of assignments) {
|
|
150
|
+
await emitCrudSideEffects({
|
|
151
|
+
dataEngine,
|
|
152
|
+
action,
|
|
153
|
+
entity: assignment,
|
|
154
|
+
identifiers: { id: assignment.id, tenantId: tag.tenantId, organizationId: tag.organizationId },
|
|
155
|
+
indexer: { entityType: E.customers.customer_tag_assignment },
|
|
156
|
+
})
|
|
157
|
+
await emitCustomersEvent(action === 'created' ? 'customers.tag.assigned' : 'customers.tag.removed', {
|
|
158
|
+
id: assignment.id,
|
|
159
|
+
tagId: tag.id,
|
|
160
|
+
entityId: assignment.entity.id,
|
|
161
|
+
organizationId: tag.organizationId,
|
|
162
|
+
tenantId: tag.tenantId,
|
|
163
|
+
}, { persistent: true })
|
|
164
|
+
}
|
|
165
|
+
await emitQueryIndexUpsertEvents(ctx, assignments.map((assignment) => ({
|
|
166
|
+
entityType: E.customers.customer_entity,
|
|
167
|
+
recordId: assignment.entity.id,
|
|
168
|
+
tenantId: tag.tenantId,
|
|
169
|
+
organizationId: tag.organizationId,
|
|
170
|
+
})))
|
|
171
|
+
await invalidateCrudCache(ctx.container, 'customers.tagAssignment', {
|
|
172
|
+
tenantId: tag.tenantId,
|
|
173
|
+
organizationId: tag.organizationId,
|
|
174
|
+
}, tag.tenantId, `customers.tags.delete.${action}`, ['customers.person', 'customers.company'])
|
|
175
|
+
}
|
|
176
|
+
|
|
91
177
|
const createTagCommand: CommandHandler<TagCreateInput, { tagId: string }> = {
|
|
92
178
|
id: 'customers.tags.create',
|
|
93
179
|
async execute(rawInput, ctx) {
|
|
@@ -290,39 +376,64 @@ const updateTagCommand: CommandHandler<TagUpdateInput, { tagId: string }> = {
|
|
|
290
376
|
|
|
291
377
|
const deleteTagCommand: CommandHandler<{ body?: Record<string, unknown>; query?: Record<string, unknown> }, { tagId: string }> = {
|
|
292
378
|
id: 'customers.tags.delete',
|
|
379
|
+
prepareSnapshotInsideTransaction: true,
|
|
293
380
|
async prepare(input, ctx) {
|
|
294
381
|
const id = requireId(input, 'Tag id required')
|
|
295
|
-
const em = (ctx.container.resolve('em') as EntityManager)
|
|
296
|
-
const snapshot = await loadTagSnapshot(em, id)
|
|
297
|
-
|
|
382
|
+
const em = ctx.transactionalEm ?? (ctx.container.resolve('em') as EntityManager).fork()
|
|
383
|
+
const snapshot = await loadTagSnapshot(em, id, tagDeleteLookup(id, ctx))
|
|
384
|
+
if (!snapshot) return {}
|
|
385
|
+
ensureTenantScope(ctx, snapshot.tenantId)
|
|
386
|
+
ensureOrganizationScope(ctx, snapshot.organizationId)
|
|
387
|
+
const assignments = await findWithDecryption(em, CustomerTagAssignment, {
|
|
388
|
+
tag: id,
|
|
389
|
+
tenantId: snapshot.tenantId,
|
|
390
|
+
organizationId: snapshot.organizationId,
|
|
391
|
+
}, {}, snapshot)
|
|
392
|
+
snapshot.assignments = assignments.map((assignment) => ({
|
|
393
|
+
id: assignment.id,
|
|
394
|
+
entityId: assignment.entity.id,
|
|
395
|
+
tenantId: assignment.tenantId,
|
|
396
|
+
organizationId: assignment.organizationId,
|
|
397
|
+
createdAt: assignment.createdAt.toISOString(),
|
|
398
|
+
}))
|
|
399
|
+
return { before: snapshot }
|
|
298
400
|
},
|
|
299
401
|
async execute(input, ctx) {
|
|
300
402
|
const id = requireId(input, 'Tag id required')
|
|
403
|
+
const lookup = tagDeleteLookup(id, ctx)
|
|
301
404
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
ensureTenantScope(ctx, tag.tenantId)
|
|
305
|
-
ensureOrganizationScope(ctx, tag.organizationId)
|
|
405
|
+
let tag: CustomerTag | null = null
|
|
406
|
+
let assignments: CustomerTagAssignment[] = []
|
|
306
407
|
await withAtomicFlush(em, [
|
|
307
408
|
async () => {
|
|
308
|
-
await em.
|
|
409
|
+
tag = await findOneWithDecryption(em, CustomerTag, lookup.where, { lockMode: LockMode.PESSIMISTIC_WRITE }, lookup.scope)
|
|
410
|
+
if (!tag) throw notFound('Tag not found')
|
|
411
|
+
ensureTenantScope(ctx, tag.tenantId)
|
|
412
|
+
ensureOrganizationScope(ctx, tag.organizationId)
|
|
413
|
+
await ctx.beforeTransactionalWrite?.(em, input)
|
|
414
|
+
assignments = await loadTagAssignments(em, tag)
|
|
415
|
+
await em.nativeDelete(CustomerTagAssignment, { tag, tenantId: tag.tenantId, organizationId: tag.organizationId })
|
|
309
416
|
em.remove(tag)
|
|
310
417
|
},
|
|
311
|
-
], { transaction: true })
|
|
418
|
+
], { transaction: true, label: 'customers.tags.delete' })
|
|
419
|
+
if (!tag) throw notFound('Tag not found')
|
|
420
|
+
const deletedTag = tag as CustomerTag
|
|
312
421
|
|
|
313
422
|
const de = (ctx.container.resolve('dataEngine') as DataEngine)
|
|
314
423
|
await emitCrudSideEffects({
|
|
315
424
|
dataEngine: de,
|
|
316
425
|
action: 'deleted',
|
|
317
|
-
entity:
|
|
426
|
+
entity: deletedTag,
|
|
318
427
|
identifiers: {
|
|
319
|
-
id:
|
|
320
|
-
organizationId:
|
|
321
|
-
tenantId:
|
|
428
|
+
id: deletedTag.id,
|
|
429
|
+
organizationId: deletedTag.organizationId,
|
|
430
|
+
tenantId: deletedTag.tenantId,
|
|
322
431
|
},
|
|
323
432
|
events: tagCrudEvents,
|
|
433
|
+
indexer: tagCrudIndexer,
|
|
324
434
|
})
|
|
325
|
-
|
|
435
|
+
await refreshTagMemberships(ctx, deletedTag, assignments, 'deleted')
|
|
436
|
+
return { tagId: deletedTag.id }
|
|
326
437
|
},
|
|
327
438
|
buildLog: async ({ snapshots }) => {
|
|
328
439
|
const before = snapshots.before as TagSnapshot | undefined
|
|
@@ -334,6 +445,7 @@ const deleteTagCommand: CommandHandler<{ body?: Record<string, unknown>; query?:
|
|
|
334
445
|
resourceId: before.id,
|
|
335
446
|
tenantId: before.tenantId,
|
|
336
447
|
organizationId: before.organizationId,
|
|
448
|
+
context: { cacheAliases: ['customers.tagAssignment', 'customers.person', 'customers.company'] },
|
|
337
449
|
snapshotBefore: before,
|
|
338
450
|
payload: {
|
|
339
451
|
undo: {
|
|
@@ -346,31 +458,51 @@ const deleteTagCommand: CommandHandler<{ body?: Record<string, unknown>; query?:
|
|
|
346
458
|
const payload = extractUndoPayload<TagUndoPayload>(logEntry)
|
|
347
459
|
const before = payload?.before
|
|
348
460
|
if (!before) return
|
|
461
|
+
ensureTenantScope(ctx, before.tenantId)
|
|
462
|
+
ensureOrganizationScope(ctx, before.organizationId)
|
|
349
463
|
const em = (ctx.container.resolve('em') as EntityManager).fork()
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
organizationId: before.organizationId,
|
|
355
|
-
tenantId: before.tenantId,
|
|
356
|
-
slug: before.slug,
|
|
357
|
-
label: before.label,
|
|
358
|
-
color: before.color,
|
|
359
|
-
description: before.description,
|
|
360
|
-
})
|
|
361
|
-
em.persist(tag)
|
|
362
|
-
} else {
|
|
363
|
-
tag.slug = before.slug
|
|
364
|
-
tag.label = before.label
|
|
365
|
-
tag.color = before.color
|
|
366
|
-
tag.description = before.description
|
|
367
|
-
}
|
|
368
|
-
const restoredTag = tag
|
|
464
|
+
const scope = { tenantId: before.tenantId, organizationId: before.organizationId }
|
|
465
|
+
const existingTag = await findOneWithDecryption(em, CustomerTag, { id: before.id, ...scope }, {}, scope)
|
|
466
|
+
const tag = existingTag ?? em.create(CustomerTag, { id: before.id, ...scope, slug: before.slug, label: before.label })
|
|
467
|
+
const restoredAssignments: CustomerTagAssignment[] = []
|
|
369
468
|
await withAtomicFlush(em, [
|
|
370
469
|
() => {
|
|
371
|
-
|
|
470
|
+
tag.slug = before.slug
|
|
471
|
+
tag.label = before.label
|
|
472
|
+
tag.color = before.color
|
|
473
|
+
tag.description = before.description
|
|
474
|
+
em.persist(tag)
|
|
372
475
|
},
|
|
373
|
-
|
|
476
|
+
async () => {
|
|
477
|
+
const assignments = (before.assignments ?? []).filter((assignment) =>
|
|
478
|
+
assignment.tenantId === before.tenantId && assignment.organizationId === before.organizationId,
|
|
479
|
+
)
|
|
480
|
+
if (!assignments.length) return
|
|
481
|
+
const customers = await findWithDecryption(em, CustomerEntity, {
|
|
482
|
+
id: { $in: assignments.map((assignment) => assignment.entityId) },
|
|
483
|
+
...scope,
|
|
484
|
+
deletedAt: null,
|
|
485
|
+
kind: { $in: ['person', 'company'] },
|
|
486
|
+
}, {}, scope)
|
|
487
|
+
const existingAssignments = await loadTagAssignments(em, tag)
|
|
488
|
+
const assignedEntityIds = new Set(existingAssignments.map((assignment) => assignment.entity.id))
|
|
489
|
+
const customersById = new Map(customers.map((customer) => [customer.id, customer]))
|
|
490
|
+
for (const snapshot of assignments) {
|
|
491
|
+
const entity = customersById.get(snapshot.entityId)
|
|
492
|
+
if (!entity || assignedEntityIds.has(entity.id)) continue
|
|
493
|
+
const assignment = em.create(CustomerTagAssignment, {
|
|
494
|
+
id: snapshot.id,
|
|
495
|
+
tag,
|
|
496
|
+
entity,
|
|
497
|
+
...scope,
|
|
498
|
+
createdAt: new Date(snapshot.createdAt),
|
|
499
|
+
})
|
|
500
|
+
em.persist(assignment)
|
|
501
|
+
restoredAssignments.push(assignment)
|
|
502
|
+
assignedEntityIds.add(entity.id)
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
], { transaction: true, label: 'customers.tags.delete.undo' })
|
|
374
506
|
|
|
375
507
|
const de = (ctx.container.resolve('dataEngine') as DataEngine)
|
|
376
508
|
await emitCrudUndoSideEffects({
|
|
@@ -378,13 +510,14 @@ const deleteTagCommand: CommandHandler<{ body?: Record<string, unknown>; query?:
|
|
|
378
510
|
action: 'created',
|
|
379
511
|
entity: tag,
|
|
380
512
|
identifiers: {
|
|
381
|
-
id:
|
|
382
|
-
organizationId:
|
|
383
|
-
tenantId:
|
|
513
|
+
id: before.id,
|
|
514
|
+
organizationId: before.organizationId,
|
|
515
|
+
tenantId: before.tenantId,
|
|
384
516
|
},
|
|
385
517
|
events: tagCrudEvents,
|
|
386
518
|
indexer: tagCrudIndexer,
|
|
387
519
|
})
|
|
520
|
+
await refreshTagMemberships(ctx, tag, restoredAssignments, 'created')
|
|
388
521
|
},
|
|
389
522
|
}
|
|
390
523
|
|
|
@@ -1422,7 +1422,7 @@ function assertClaimUpdateFields(claim: WarrantyClaim, input: ClaimUpdateInput):
|
|
|
1422
1422
|
|
|
1423
1423
|
function applyClaimUpdate(claim: WarrantyClaim, input: ClaimUpdateInput, customerName: string | null, orderNumber: string | null): void {
|
|
1424
1424
|
if (hasOwn(input, 'customerId')) claim.customerId = input.customerId ?? null
|
|
1425
|
-
if (hasOwn(input, 'customerName')) claim.customerName = customerName
|
|
1425
|
+
if (hasOwn(input, 'customerId') || hasOwn(input, 'customerName')) claim.customerName = customerName
|
|
1426
1426
|
if (hasOwn(input, 'orderId')) {
|
|
1427
1427
|
claim.orderId = input.orderId ?? null
|
|
1428
1428
|
claim.orderNumber = orderNumber
|
|
@@ -1673,7 +1673,7 @@ const updateClaimCommand: CommandHandler<ClaimUpdateInput, { claimId: string }>
|
|
|
1673
1673
|
const customerChanged = hasOwn(input, 'customerId') && (input.customerId ?? null) !== (claim.customerId ?? null)
|
|
1674
1674
|
const shouldRefreshCustomerName = hasOwn(input, 'customerId') || hasOwn(input, 'customerName')
|
|
1675
1675
|
const customerName = shouldRefreshCustomerName
|
|
1676
|
-
? await resolveCustomerName(ctx,
|
|
1676
|
+
? await resolveCustomerName(ctx, effectiveCustomerId, scope, customerChanged ? input.customerName : (input.customerName ?? claim.customerName), { strict: customerChanged })
|
|
1677
1677
|
: claim.customerName ?? null
|
|
1678
1678
|
const orderNumber = hasOwn(input, 'orderId')
|
|
1679
1679
|
? await resolveOrderNumber(ctx, input.orderId, scope, (input.orderId ?? null) === (claim.orderId ?? null) ? claim.orderNumber : null)
|