@open-mercato/core 0.10.1-develop.7671.1.9117e4b0b3 → 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.
@@ -1,4 +1,4 @@
1
- [build:core] found 5992 entry points
1
+ [build:core] found 5993 entry points
2
2
  [build:core] built successfully
3
3
  [build:core:generated] found 266 entry points
4
4
  [build:core:generated] built successfully
@@ -1,6 +1,9 @@
1
1
  import { registerCommand } from "@open-mercato/shared/lib/commands";
2
2
  import { emitCrudSideEffects, emitCrudUndoSideEffects, buildChanges, requireId } from "@open-mercato/shared/lib/commands/helpers";
3
- import { CustomerTag, CustomerTagAssignment } from "../data/entities.js";
3
+ import { LockMode } from "@mikro-orm/core";
4
+ import { buildScopedWhere } from "@open-mercato/shared/lib/api/crud";
5
+ import { isUnrestrictedOrganizationScope } from "@open-mercato/shared/lib/auth/organizationAccess";
6
+ import { CustomerEntity, CustomerTag, CustomerTagAssignment } from "../data/entities.js";
4
7
  import {
5
8
  tagCreateSchema,
6
9
  tagUpdateSchema,
@@ -12,11 +15,13 @@ import {
12
15
  extractUndoPayload,
13
16
  requireCustomerEntity,
14
17
  ensureSameScope,
15
- loadEntityTagIds
18
+ loadEntityTagIds,
19
+ emitQueryIndexUpsertEvents
16
20
  } from "./shared.js";
17
21
  import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
18
- import { CrudHttpError, notFound } from "@open-mercato/shared/lib/crud/errors";
19
- import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
22
+ import { CrudHttpError, forbidden, notFound } from "@open-mercato/shared/lib/crud/errors";
23
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
24
+ import { invalidateCrudCache } from "@open-mercato/shared/lib/crud/cache";
20
25
  import { emitCustomersEvent } from "../events.js";
21
26
  import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
22
27
  import { makeCreateRedo } from "@open-mercato/shared/lib/commands/redo";
@@ -34,8 +39,33 @@ const tagCrudEvents = {
34
39
  tenantId: ctx.identifiers.tenantId
35
40
  })
36
41
  };
37
- async function loadTagSnapshot(em, id) {
38
- const tag = await em.findOne(CustomerTag, { id });
42
+ function tagDeleteLookup(id, ctx) {
43
+ const isSuperAdmin = ctx.auth?.isSuperAdmin === true;
44
+ const organizationScope = ctx.organizationScope;
45
+ const tenantId = ctx.auth ? ctx.auth.tenantId ?? null : organizationScope?.tenantId ?? null;
46
+ if (ctx.auth && !isSuperAdmin && (!tenantId || organizationScope && !organizationScope.tenantId)) {
47
+ throw forbidden();
48
+ }
49
+ let organizationIds;
50
+ if (organizationScope) {
51
+ if (!isUnrestrictedOrganizationScope({ isSuperAdmin, allowedOrganizationIds: organizationScope.allowedIds })) {
52
+ organizationIds = organizationScope.allowedIds ?? [];
53
+ }
54
+ } else if (!isSuperAdmin) {
55
+ const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId;
56
+ if (organizationId) organizationIds = [organizationId];
57
+ }
58
+ return {
59
+ where: buildScopedWhere({ id }, {
60
+ tenantId: tenantId ?? void 0,
61
+ organizationIds,
62
+ softDeleteField: null
63
+ }),
64
+ scope: { tenantId, organizationId: organizationIds?.length === 1 ? organizationIds[0] : null }
65
+ };
66
+ }
67
+ async function loadTagSnapshot(em, id, lookup) {
68
+ const tag = lookup ? await findOneWithDecryption(em, CustomerTag, lookup.where, {}, lookup.scope) : await em.findOne(CustomerTag, { id });
39
69
  if (!tag) return null;
40
70
  return {
41
71
  id: tag.id,
@@ -47,6 +77,39 @@ async function loadTagSnapshot(em, id) {
47
77
  description: tag.description ?? null
48
78
  };
49
79
  }
80
+ async function loadTagAssignments(em, tag) {
81
+ const scope = { tenantId: tag.tenantId, organizationId: tag.organizationId };
82
+ return findWithDecryption(em, CustomerTagAssignment, { tag: tag.id, ...scope }, {}, scope);
83
+ }
84
+ async function refreshTagMemberships(ctx, tag, assignments, action) {
85
+ const dataEngine = ctx.container.resolve("dataEngine");
86
+ for (const assignment of assignments) {
87
+ await emitCrudSideEffects({
88
+ dataEngine,
89
+ action,
90
+ entity: assignment,
91
+ identifiers: { id: assignment.id, tenantId: tag.tenantId, organizationId: tag.organizationId },
92
+ indexer: { entityType: E.customers.customer_tag_assignment }
93
+ });
94
+ await emitCustomersEvent(action === "created" ? "customers.tag.assigned" : "customers.tag.removed", {
95
+ id: assignment.id,
96
+ tagId: tag.id,
97
+ entityId: assignment.entity.id,
98
+ organizationId: tag.organizationId,
99
+ tenantId: tag.tenantId
100
+ }, { persistent: true });
101
+ }
102
+ await emitQueryIndexUpsertEvents(ctx, assignments.map((assignment) => ({
103
+ entityType: E.customers.customer_entity,
104
+ recordId: assignment.entity.id,
105
+ tenantId: tag.tenantId,
106
+ organizationId: tag.organizationId
107
+ })));
108
+ await invalidateCrudCache(ctx.container, "customers.tagAssignment", {
109
+ tenantId: tag.tenantId,
110
+ organizationId: tag.organizationId
111
+ }, tag.tenantId, `customers.tags.delete.${action}`, ["customers.person", "customers.company"]);
112
+ }
50
113
  const createTagCommand = {
51
114
  id: "customers.tags.create",
52
115
  async execute(rawInput, ctx) {
@@ -235,38 +298,63 @@ const updateTagCommand = {
235
298
  };
236
299
  const deleteTagCommand = {
237
300
  id: "customers.tags.delete",
301
+ prepareSnapshotInsideTransaction: true,
238
302
  async prepare(input, ctx) {
239
303
  const id = requireId(input, "Tag id required");
240
- const em = ctx.container.resolve("em");
241
- const snapshot = await loadTagSnapshot(em, id);
242
- return snapshot ? { before: snapshot } : {};
304
+ const em = ctx.transactionalEm ?? ctx.container.resolve("em").fork();
305
+ const snapshot = await loadTagSnapshot(em, id, tagDeleteLookup(id, ctx));
306
+ if (!snapshot) return {};
307
+ ensureTenantScope(ctx, snapshot.tenantId);
308
+ ensureOrganizationScope(ctx, snapshot.organizationId);
309
+ const assignments = await findWithDecryption(em, CustomerTagAssignment, {
310
+ tag: id,
311
+ tenantId: snapshot.tenantId,
312
+ organizationId: snapshot.organizationId
313
+ }, {}, snapshot);
314
+ snapshot.assignments = assignments.map((assignment) => ({
315
+ id: assignment.id,
316
+ entityId: assignment.entity.id,
317
+ tenantId: assignment.tenantId,
318
+ organizationId: assignment.organizationId,
319
+ createdAt: assignment.createdAt.toISOString()
320
+ }));
321
+ return { before: snapshot };
243
322
  },
244
323
  async execute(input, ctx) {
245
324
  const id = requireId(input, "Tag id required");
325
+ const lookup = tagDeleteLookup(id, ctx);
246
326
  const em = ctx.container.resolve("em").fork();
247
- const tag = await em.findOne(CustomerTag, { id });
248
- if (!tag) throw notFound("Tag not found");
249
- ensureTenantScope(ctx, tag.tenantId);
250
- ensureOrganizationScope(ctx, tag.organizationId);
327
+ let tag = null;
328
+ let assignments = [];
251
329
  await withAtomicFlush(em, [
252
330
  async () => {
253
- await em.nativeDelete(CustomerTagAssignment, { tag });
331
+ tag = await findOneWithDecryption(em, CustomerTag, lookup.where, { lockMode: LockMode.PESSIMISTIC_WRITE }, lookup.scope);
332
+ if (!tag) throw notFound("Tag not found");
333
+ ensureTenantScope(ctx, tag.tenantId);
334
+ ensureOrganizationScope(ctx, tag.organizationId);
335
+ await ctx.beforeTransactionalWrite?.(em, input);
336
+ assignments = await loadTagAssignments(em, tag);
337
+ await em.nativeDelete(CustomerTagAssignment, { tag, tenantId: tag.tenantId, organizationId: tag.organizationId });
254
338
  em.remove(tag);
255
339
  }
256
- ], { transaction: true });
340
+ ], { transaction: true, label: "customers.tags.delete" });
341
+ if (!tag) throw notFound("Tag not found");
342
+ const deletedTag = tag;
257
343
  const de = ctx.container.resolve("dataEngine");
258
344
  await emitCrudSideEffects({
259
345
  dataEngine: de,
260
346
  action: "deleted",
261
- entity: tag,
347
+ entity: deletedTag,
262
348
  identifiers: {
263
- id: tag.id,
264
- organizationId: tag.organizationId,
265
- tenantId: tag.tenantId
349
+ id: deletedTag.id,
350
+ organizationId: deletedTag.organizationId,
351
+ tenantId: deletedTag.tenantId
266
352
  },
267
- events: tagCrudEvents
353
+ events: tagCrudEvents,
354
+ indexer: tagCrudIndexer
268
355
  });
269
- return { tagId: tag.id };
356
+ await refreshTagMemberships(ctx, deletedTag, assignments, "deleted");
357
+ return { tagId: deletedTag.id };
270
358
  },
271
359
  buildLog: async ({ snapshots }) => {
272
360
  const before = snapshots.before;
@@ -278,6 +366,7 @@ const deleteTagCommand = {
278
366
  resourceId: before.id,
279
367
  tenantId: before.tenantId,
280
368
  organizationId: before.organizationId,
369
+ context: { cacheAliases: ["customers.tagAssignment", "customers.person", "customers.company"] },
281
370
  snapshotBefore: before,
282
371
  payload: {
283
372
  undo: {
@@ -290,44 +379,65 @@ const deleteTagCommand = {
290
379
  const payload = extractUndoPayload(logEntry);
291
380
  const before = payload?.before;
292
381
  if (!before) return;
382
+ ensureTenantScope(ctx, before.tenantId);
383
+ ensureOrganizationScope(ctx, before.organizationId);
293
384
  const em = ctx.container.resolve("em").fork();
294
- let tag = await em.findOne(CustomerTag, { id: before.id });
295
- if (!tag) {
296
- tag = em.create(CustomerTag, {
297
- id: before.id,
298
- organizationId: before.organizationId,
299
- tenantId: before.tenantId,
300
- slug: before.slug,
301
- label: before.label,
302
- color: before.color,
303
- description: before.description
304
- });
305
- em.persist(tag);
306
- } else {
307
- tag.slug = before.slug;
308
- tag.label = before.label;
309
- tag.color = before.color;
310
- tag.description = before.description;
311
- }
312
- const restoredTag = tag;
385
+ const scope = { tenantId: before.tenantId, organizationId: before.organizationId };
386
+ const existingTag = await findOneWithDecryption(em, CustomerTag, { id: before.id, ...scope }, {}, scope);
387
+ const tag = existingTag ?? em.create(CustomerTag, { id: before.id, ...scope, slug: before.slug, label: before.label });
388
+ const restoredAssignments = [];
313
389
  await withAtomicFlush(em, [
314
390
  () => {
315
- em.persist(restoredTag);
391
+ tag.slug = before.slug;
392
+ tag.label = before.label;
393
+ tag.color = before.color;
394
+ tag.description = before.description;
395
+ em.persist(tag);
396
+ },
397
+ async () => {
398
+ const assignments = (before.assignments ?? []).filter(
399
+ (assignment) => assignment.tenantId === before.tenantId && assignment.organizationId === before.organizationId
400
+ );
401
+ if (!assignments.length) return;
402
+ const customers = await findWithDecryption(em, CustomerEntity, {
403
+ id: { $in: assignments.map((assignment) => assignment.entityId) },
404
+ ...scope,
405
+ deletedAt: null,
406
+ kind: { $in: ["person", "company"] }
407
+ }, {}, scope);
408
+ const existingAssignments = await loadTagAssignments(em, tag);
409
+ const assignedEntityIds = new Set(existingAssignments.map((assignment) => assignment.entity.id));
410
+ const customersById = new Map(customers.map((customer) => [customer.id, customer]));
411
+ for (const snapshot of assignments) {
412
+ const entity = customersById.get(snapshot.entityId);
413
+ if (!entity || assignedEntityIds.has(entity.id)) continue;
414
+ const assignment = em.create(CustomerTagAssignment, {
415
+ id: snapshot.id,
416
+ tag,
417
+ entity,
418
+ ...scope,
419
+ createdAt: new Date(snapshot.createdAt)
420
+ });
421
+ em.persist(assignment);
422
+ restoredAssignments.push(assignment);
423
+ assignedEntityIds.add(entity.id);
424
+ }
316
425
  }
317
- ], { transaction: true });
426
+ ], { transaction: true, label: "customers.tags.delete.undo" });
318
427
  const de = ctx.container.resolve("dataEngine");
319
428
  await emitCrudUndoSideEffects({
320
429
  dataEngine: de,
321
430
  action: "created",
322
431
  entity: tag,
323
432
  identifiers: {
324
- id: tag.id,
325
- organizationId: tag.organizationId,
326
- tenantId: tag.tenantId
433
+ id: before.id,
434
+ organizationId: before.organizationId,
435
+ tenantId: before.tenantId
327
436
  },
328
437
  events: tagCrudEvents,
329
438
  indexer: tagCrudIndexer
330
439
  });
440
+ await refreshTagMemberships(ctx, tag, restoredAssignments, "created");
331
441
  }
332
442
  };
333
443
  const assignTagCommand = {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/customers/commands/tags.ts"],
4
- "sourcesContent": ["import { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { emitCrudSideEffects, emitCrudUndoSideEffects, buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { CustomerTag, CustomerTagAssignment } from '../data/entities'\nimport {\n tagCreateSchema,\n tagUpdateSchema,\n tagAssignmentSchema,\n type TagCreateInput,\n type TagUpdateInput,\n type TagAssignmentInput,\n} from '../data/validators'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n extractUndoPayload,\n requireCustomerEntity,\n ensureSameScope,\n loadEntityTagIds,\n} from './shared'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { emitCustomersEvent } from '../events'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { E } from '#generated/entities.ids.generated'\n\n// The route (`api/tags/route.ts`) declares this same entity type and hands it down to the\n// execute path, but an undo runs through `CommandBus.undo()` where no route declaration is\n// active \u2014 so the undo marks carry the indexer explicitly, or a restored tag would stay\n// invisible to every index-backed list until the next full rebuild (#5741).\nconst tagCrudIndexer: CrudIndexerConfig<CustomerTag> = {\n entityType: E.customers.customer_tag,\n}\n\nconst tagCrudEvents: CrudEventsConfig = {\n module: 'customers',\n entity: 'tag',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\ntype TagSnapshot = {\n id: string\n organizationId: string\n tenantId: string\n slug: string\n label: string\n color: string | null\n description: string | null\n}\n\ntype TagUndoPayload = {\n before?: TagSnapshot | null\n after?: TagSnapshot | null\n}\n\ntype TagAssignmentSnapshot = {\n tagId: string\n entityId: string\n organizationId: string\n tenantId: string\n}\n\ntype TagAssignmentUndoPayload = {\n before?: TagAssignmentSnapshot | null\n}\n\nasync function loadTagSnapshot(em: EntityManager, id: string): Promise<TagSnapshot | null> {\n const tag = await em.findOne(CustomerTag, { id })\n if (!tag) return null\n return {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n slug: tag.slug,\n label: tag.label,\n color: tag.color ?? null,\n description: tag.description ?? null,\n }\n}\n\nconst createTagCommand: CommandHandler<TagCreateInput, { tagId: string }> = {\n id: 'customers.tags.create',\n async execute(rawInput, ctx) {\n const parsed = tagCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const duplicate = await em.findOne(CustomerTag, {\n slug: parsed.slug,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n })\n if (duplicate) throw new CrudHttpError(409, { error: 'Tag slug already exists' })\n\n const tag = em.create(CustomerTag, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n slug: parsed.slug,\n label: parsed.label,\n color: parsed.color ?? null,\n description: parsed.description ?? null,\n })\n em.persist(tag)\n await em.flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: tag,\n identifiers: {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n },\n events: tagCrudEvents,\n })\n\n return { tagId: tag.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return await loadTagSnapshot(em, result.tagId)\n },\n buildLog: async ({ result, ctx }) => {\n const { translate } = await resolveTranslations()\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTagSnapshot(em, result.tagId)\n return {\n actionLabel: translate('customers.audit.tags.create', 'Create tag'),\n resourceKind: 'customers.tag',\n resourceId: result.tagId,\n tenantId: snapshot?.tenantId ?? null,\n organizationId: snapshot?.organizationId ?? null,\n snapshotAfter: snapshot ?? null,\n payload: {\n undo: {\n after: snapshot ?? null,\n } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const tagId = logEntry?.resourceId ?? null\n if (!tagId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: tagId })\n if (tag) {\n await em.nativeDelete(CustomerTagAssignment, { tag })\n em.remove(tag)\n await em.flush()\n }\n },\n redo: makeCreateRedo<CustomerTag, TagSnapshot, TagCreateInput, { tagId: string }>({\n entityClass: CustomerTag,\n buildResult: (entity) => ({ tagId: entity.id }),\n events: tagCrudEvents,\n }),\n}\n\nconst updateTagCommand: CommandHandler<TagUpdateInput, { tagId: string }> = {\n id: 'customers.tags.update',\n async prepare(rawInput, ctx) {\n const parsed = tagUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadTagSnapshot(em, parsed.id)\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n const parsed = tagUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: parsed.id })\n if (!tag) throw notFound('Tag not found')\n ensureTenantScope(ctx, tag.tenantId)\n ensureOrganizationScope(ctx, tag.organizationId)\n\n if (parsed.slug !== undefined && parsed.slug !== tag.slug) {\n const duplicate = await em.findOne(CustomerTag, {\n slug: parsed.slug,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n id: { $ne: tag.id },\n })\n if (duplicate) throw new CrudHttpError(409, { error: 'Tag slug already exists' })\n tag.slug = parsed.slug\n }\n if (parsed.label !== undefined) tag.label = parsed.label\n if (parsed.color !== undefined) tag.color = parsed.color ?? null\n if (parsed.description !== undefined) tag.description = parsed.description ?? null\n\n await em.flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: tag,\n identifiers: {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n },\n events: tagCrudEvents,\n })\n\n return { tagId: tag.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as TagSnapshot | undefined\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const afterSnapshot = await loadTagSnapshot(em, before.id)\n const changes =\n afterSnapshot && before\n ? buildChanges(\n before as unknown as Record<string, unknown>,\n afterSnapshot as unknown as Record<string, unknown>,\n ['slug', 'label', 'color', 'description']\n )\n : {}\n return {\n actionLabel: translate('customers.audit.tags.update', 'Update tag'),\n resourceKind: 'customers.tag',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: afterSnapshot ?? null,\n changes,\n payload: {\n undo: {\n before,\n after: afterSnapshot ?? null,\n } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let tag = await em.findOne(CustomerTag, { id: before.id })\n if (!tag) {\n tag = em.create(CustomerTag, {\n id: before.id,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n slug: before.slug,\n label: before.label,\n color: before.color,\n description: before.description,\n })\n em.persist(tag)\n } else {\n tag.slug = before.slug\n tag.label = before.label\n tag.color = before.color\n tag.description = before.description\n }\n await em.flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudUndoSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: tag,\n identifiers: {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n },\n events: tagCrudEvents,\n indexer: tagCrudIndexer,\n })\n },\n}\n\nconst deleteTagCommand: CommandHandler<{ body?: Record<string, unknown>; query?: Record<string, unknown> }, { tagId: string }> = {\n id: 'customers.tags.delete',\n async prepare(input, ctx) {\n const id = requireId(input, 'Tag id required')\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadTagSnapshot(em, id)\n return snapshot ? { before: snapshot } : {}\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Tag id required')\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id })\n if (!tag) throw notFound('Tag not found')\n ensureTenantScope(ctx, tag.tenantId)\n ensureOrganizationScope(ctx, tag.organizationId)\n await withAtomicFlush(em, [\n async () => {\n await em.nativeDelete(CustomerTagAssignment, { tag })\n em.remove(tag)\n },\n ], { transaction: true })\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'deleted',\n entity: tag,\n identifiers: {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n },\n events: tagCrudEvents,\n })\n return { tagId: tag.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as TagSnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('customers.audit.tags.delete', 'Delete tag'),\n resourceKind: 'customers.tag',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let tag = await em.findOne(CustomerTag, { id: before.id })\n if (!tag) {\n tag = em.create(CustomerTag, {\n id: before.id,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n slug: before.slug,\n label: before.label,\n color: before.color,\n description: before.description,\n })\n em.persist(tag)\n } else {\n tag.slug = before.slug\n tag.label = before.label\n tag.color = before.color\n tag.description = before.description\n }\n const restoredTag = tag\n await withAtomicFlush(em, [\n () => {\n em.persist(restoredTag)\n },\n ], { transaction: true })\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudUndoSideEffects({\n dataEngine: de,\n action: 'created',\n entity: tag,\n identifiers: {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n },\n events: tagCrudEvents,\n indexer: tagCrudIndexer,\n })\n },\n}\n\nconst assignTagCommand: CommandHandler<TagAssignmentInput, { assignmentId: string }> = {\n id: 'customers.tags.assign',\n async execute(rawInput, ctx) {\n const parsed = tagAssignmentSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: parsed.tagId, tenantId: parsed.tenantId, organizationId: parsed.organizationId })\n if (!tag) throw notFound('Tag not found')\n const entity = await requireCustomerEntity(em, parsed.entityId, { tenantId: parsed.tenantId, organizationId: parsed.organizationId }, undefined, 'Customer not found')\n ensureSameScope(entity, parsed.organizationId, parsed.tenantId)\n const tagIds = await loadEntityTagIds(em, entity)\n if (tagIds.includes(parsed.tagId)) {\n throw new CrudHttpError(409, { error: 'Tag already assigned' })\n }\n\n const assignment = em.create(CustomerTagAssignment, {\n tag,\n entity,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n })\n em.persist(assignment)\n await em.flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: assignment,\n identifiers: {\n id: String(assignment.id),\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n },\n })\n\n await emitCustomersEvent('customers.tag.assigned', {\n id: String(assignment.id),\n tagId: parsed.tagId,\n entityId: parsed.entityId,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n }, { persistent: true })\n\n return { assignmentId: assignment.id }\n },\n buildLog: async ({ result, ctx }) => {\n const { translate } = await resolveTranslations()\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const assignment = await findOneWithDecryption(\n em,\n CustomerTagAssignment,\n { id: result.assignmentId },\n { populate: ['tag', 'entity'] },\n { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null },\n )\n if (!assignment) return null\n const tagId = typeof assignment.tag === 'string' ? assignment.tag : assignment.tag.id\n const entityId = typeof assignment.entity === 'string' ? assignment.entity : assignment.entity.id\n return {\n actionLabel: translate('customers.audit.tags.assign', 'Assign tag'),\n resourceKind: 'customers.tagAssignment',\n resourceId: result.assignmentId,\n tenantId: assignment.tenantId,\n organizationId: assignment.organizationId,\n payload: {\n undo: {\n before: {\n tagId,\n entityId,\n tenantId: assignment.tenantId,\n organizationId: assignment.organizationId,\n } satisfies TagAssignmentSnapshot,\n } satisfies TagAssignmentUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagAssignmentUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.nativeDelete(CustomerTagAssignment, {\n tag: before.tagId,\n entity: before.entityId,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n })\n },\n redo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagAssignmentUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) {\n throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for tag assign' })\n }\n const assignmentId = logEntry?.resourceId ?? null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: before.tagId })\n if (!tag) throw notFound('Tag not found')\n const entity = await requireCustomerEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId }, undefined, 'Customer not found')\n ensureSameScope(entity, before.organizationId, before.tenantId)\n\n let assignment = await em.findOne(CustomerTagAssignment, {\n tag,\n entity,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n })\n if (!assignment) {\n assignment = em.create(CustomerTagAssignment, {\n ...(assignmentId ? { id: assignmentId } : {}),\n tag,\n entity,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n })\n em.persist(assignment)\n await em.flush()\n }\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: assignment,\n identifiers: {\n id: String(assignment.id),\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n },\n })\n\n await emitCustomersEvent('customers.tag.assigned', {\n id: String(assignment.id),\n tagId: before.tagId,\n entityId: before.entityId,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n }, { persistent: true })\n\n return { assignmentId: assignment.id }\n },\n}\n\nconst unassignTagCommand: CommandHandler<TagAssignmentInput, { assignmentId: string | null }> = {\n id: 'customers.tags.unassign',\n async execute(rawInput, ctx) {\n const parsed = tagAssignmentSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const existing = await em.findOne(CustomerTagAssignment, {\n tag: parsed.tagId,\n entity: parsed.entityId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n if (!existing) throw notFound('Tag assignment not found')\n await em.remove(existing).flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: existing,\n identifiers: {\n id: String(existing.id),\n organizationId: existing.organizationId,\n tenantId: existing.tenantId,\n },\n })\n\n await emitCustomersEvent('customers.tag.removed', {\n id: String(existing.id),\n tagId: parsed.tagId,\n entityId: parsed.entityId,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n }, { persistent: true })\n\n return { assignmentId: existing.id ?? null }\n },\n buildLog: async ({ snapshots, input }) => {\n const { translate } = await resolveTranslations()\n const parsed = tagAssignmentSchema.parse(input)\n return {\n actionLabel: translate('customers.audit.tags.unassign', 'Unassign tag'),\n resourceKind: 'customers.tagAssignment',\n resourceId: parsed.tagId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n payload: {\n undo: {\n before: {\n tagId: parsed.tagId,\n entityId: parsed.entityId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n } satisfies TagAssignmentSnapshot,\n } satisfies TagAssignmentUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagAssignmentUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: before.tagId })\n const entity = await requireCustomerEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId }, undefined, 'Customer not found')\n ensureSameScope(entity, before.organizationId, before.tenantId)\n if (!tag) throw notFound('Tag not found')\n const existing = await em.findOne(CustomerTagAssignment, {\n tag,\n entity,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n })\n if (!existing) {\n const assignment = em.create(CustomerTagAssignment, {\n tag,\n entity,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n })\n em.persist(assignment)\n await em.flush()\n }\n },\n}\n\nregisterCommand(createTagCommand)\nregisterCommand(updateTagCommand)\nregisterCommand(deleteTagCommand)\nregisterCommand(assignTagCommand)\nregisterCommand(unassignTagCommand)\n"],
5
- "mappings": "AAAA,SAAS,uBAAuB;AAEhC,SAAS,qBAAqB,yBAAyB,cAAc,iBAAiB;AAGtF,SAAS,aAAa,6BAA6B;AACnD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC,SAAS,eAAe,gBAAgB;AAExC,SAAS,6BAA6B;AACtC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAC/B,SAAS,SAAS;AAMlB,MAAM,iBAAiD;AAAA,EACrD,YAAY,EAAE,UAAU;AAC1B;AAEA,MAAM,gBAAkC;AAAA,EACtC,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,cAAc,CAAC,SAAS;AAAA,IACtB,IAAI,IAAI,YAAY;AAAA,IACpB,gBAAgB,IAAI,YAAY;AAAA,IAChC,UAAU,IAAI,YAAY;AAAA,EAC5B;AACF;AA4BA,eAAe,gBAAgB,IAAmB,IAAyC;AACzF,QAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,GAAG,CAAC;AAChD,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,gBAAgB,IAAI;AAAA,IACpB,UAAU,IAAI;AAAA,IACd,MAAM,IAAI;AAAA,IACV,OAAO,IAAI;AAAA,IACX,OAAO,IAAI,SAAS;AAAA,IACpB,aAAa,IAAI,eAAe;AAAA,EAClC;AACF;AAEA,MAAM,mBAAsE;AAAA,EAC1E,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,gBAAgB,MAAM,QAAQ;AAC7C,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,YAAY,MAAM,GAAG,QAAQ,aAAa;AAAA,MAC9C,MAAM,OAAO;AAAA,MACb,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,CAAC;AACD,QAAI,UAAW,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,0BAA0B,CAAC;AAEhF,UAAM,MAAM,GAAG,OAAO,aAAa;AAAA,MACjC,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,MAAM,OAAO;AAAA,MACb,OAAO,OAAO;AAAA,MACd,OAAO,OAAO,SAAS;AAAA,MACvB,aAAa,OAAO,eAAe;AAAA,IACrC,CAAC;AACD,OAAG,QAAQ,GAAG;AACd,UAAM,GAAG,MAAM;AAEf,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,IAAI;AAAA,QACR,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,MAChB;AAAA,MACA,QAAQ;AAAA,IACV,CAAC;AAED,WAAO,EAAE,OAAO,IAAI,GAAG;AAAA,EACzB;AAAA,EACA,cAAc,OAAO,QAAQ,QAAQ,QAAQ;AAC3C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,WAAO,MAAM,gBAAgB,IAAI,OAAO,KAAK;AAAA,EAC/C;AAAA,EACA,UAAU,OAAO,EAAE,QAAQ,IAAI,MAAM;AACnC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,WAAW,MAAM,gBAAgB,IAAI,OAAO,KAAK;AACvD,WAAO;AAAA,MACL,aAAa,UAAU,+BAA+B,YAAY;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,UAAU,YAAY;AAAA,MAChC,gBAAgB,UAAU,kBAAkB;AAAA,MAC5C,eAAe,YAAY;AAAA,MAC3B,SAAS;AAAA,QACP,MAAM;AAAA,UACJ,OAAO,YAAY;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,QAAQ,UAAU,cAAc;AACtC,QAAI,CAAC,MAAO;AACZ,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,MAAM,CAAC;AACvD,QAAI,KAAK;AACP,YAAM,GAAG,aAAa,uBAAuB,EAAE,IAAI,CAAC;AACpD,SAAG,OAAO,GAAG;AACb,YAAM,GAAG,MAAM;AAAA,IACjB;AAAA,EACF;AAAA,EACA,MAAM,eAA4E;AAAA,IAChF,aAAa;AAAA,IACb,aAAa,CAAC,YAAY,EAAE,OAAO,OAAO,GAAG;AAAA,IAC7C,QAAQ;AAAA,EACV,CAAC;AACH;AAEA,MAAM,mBAAsE;AAAA,EAC1E,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,gBAAgB,MAAM,QAAQ;AAC7C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI;AACtC,UAAM,WAAW,MAAM,gBAAgB,IAAI,OAAO,EAAE;AACpD,WAAO,WAAW,EAAE,QAAQ,SAAS,IAAI,CAAC;AAAA,EAC5C;AAAA,EACA,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,gBAAgB,MAAM,QAAQ;AAC7C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,GAAG,CAAC;AAC3D,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,sBAAkB,KAAK,IAAI,QAAQ;AACnC,4BAAwB,KAAK,IAAI,cAAc;AAE/C,QAAI,OAAO,SAAS,UAAa,OAAO,SAAS,IAAI,MAAM;AACzD,YAAM,YAAY,MAAM,GAAG,QAAQ,aAAa;AAAA,QAC9C,MAAM,OAAO;AAAA,QACb,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,QACd,IAAI,EAAE,KAAK,IAAI,GAAG;AAAA,MACpB,CAAC;AACD,UAAI,UAAW,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,0BAA0B,CAAC;AAChF,UAAI,OAAO,OAAO;AAAA,IACpB;AACA,QAAI,OAAO,UAAU,OAAW,KAAI,QAAQ,OAAO;AACnD,QAAI,OAAO,UAAU,OAAW,KAAI,QAAQ,OAAO,SAAS;AAC5D,QAAI,OAAO,gBAAgB,OAAW,KAAI,cAAc,OAAO,eAAe;AAE9E,UAAM,GAAG,MAAM;AAEf,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,IAAI;AAAA,QACR,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,MAChB;AAAA,MACA,QAAQ;AAAA,IACV,CAAC;AAED,WAAO,EAAE,OAAO,IAAI,GAAG;AAAA,EACzB;AAAA,EACA,UAAU,OAAO,EAAE,WAAW,IAAI,MAAM;AACtC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,SAAS,UAAU;AACzB,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,gBAAgB,MAAM,gBAAgB,IAAI,OAAO,EAAE;AACzD,UAAM,UACJ,iBAAiB,SACb;AAAA,MACE;AAAA,MACA;AAAA,MACA,CAAC,QAAQ,SAAS,SAAS,aAAa;AAAA,IAC1C,IACA,CAAC;AACP,WAAO;AAAA,MACL,aAAa,UAAU,+BAA+B,YAAY;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,gBAAgB;AAAA,MAChB,eAAe,iBAAiB;AAAA,MAChC;AAAA,MACA,SAAS;AAAA,QACP,MAAM;AAAA,UACJ;AAAA,UACA,OAAO,iBAAiB;AAAA,QAC1B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAAmC,QAAQ;AAC3D,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,OAAQ;AACb,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,QAAI,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,GAAG,CAAC;AACzD,QAAI,CAAC,KAAK;AACR,YAAM,GAAG,OAAO,aAAa;AAAA,QAC3B,IAAI,OAAO;AAAA,QACX,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,QACjB,MAAM,OAAO;AAAA,QACb,OAAO,OAAO;AAAA,QACd,OAAO,OAAO;AAAA,QACd,aAAa,OAAO;AAAA,MACtB,CAAC;AACD,SAAG,QAAQ,GAAG;AAAA,IAChB,OAAO;AACL,UAAI,OAAO,OAAO;AAClB,UAAI,QAAQ,OAAO;AACnB,UAAI,QAAQ,OAAO;AACnB,UAAI,cAAc,OAAO;AAAA,IAC3B;AACA,UAAM,GAAG,MAAM;AAEf,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,wBAAwB;AAAA,MAC5B,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,IAAI;AAAA,QACR,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,MAChB;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AACF;AAEA,MAAM,mBAA2H;AAAA,EAC/H,IAAI;AAAA,EACJ,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,KAAK,UAAU,OAAO,iBAAiB;AAC7C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI;AACtC,UAAM,WAAW,MAAM,gBAAgB,IAAI,EAAE;AAC7C,WAAO,WAAW,EAAE,QAAQ,SAAS,IAAI,CAAC;AAAA,EAC5C;AAAA,EACA,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,KAAK,UAAU,OAAO,iBAAiB;AAC7C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,GAAG,CAAC;AAChD,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,sBAAkB,KAAK,IAAI,QAAQ;AACnC,4BAAwB,KAAK,IAAI,cAAc;AAC/C,UAAM,gBAAgB,IAAI;AAAA,MACxB,YAAY;AACV,cAAM,GAAG,aAAa,uBAAuB,EAAE,IAAI,CAAC;AACpD,WAAG,OAAO,GAAG;AAAA,MACf;AAAA,IACF,GAAG,EAAE,aAAa,KAAK,CAAC;AAExB,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,IAAI;AAAA,QACR,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,MAChB;AAAA,MACA,QAAQ;AAAA,IACV,CAAC;AACD,WAAO,EAAE,OAAO,IAAI,GAAG;AAAA,EACzB;AAAA,EACA,UAAU,OAAO,EAAE,UAAU,MAAM;AACjC,UAAM,SAAS,UAAU;AACzB,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,WAAO;AAAA,MACL,aAAa,UAAU,+BAA+B,YAAY;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,gBAAgB;AAAA,MAChB,SAAS;AAAA,QACP,MAAM;AAAA,UACJ;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAAmC,QAAQ;AAC3D,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,OAAQ;AACb,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,QAAI,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,GAAG,CAAC;AACzD,QAAI,CAAC,KAAK;AACR,YAAM,GAAG,OAAO,aAAa;AAAA,QAC3B,IAAI,OAAO;AAAA,QACX,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,QACjB,MAAM,OAAO;AAAA,QACb,OAAO,OAAO;AAAA,QACd,OAAO,OAAO;AAAA,QACd,aAAa,OAAO;AAAA,MACtB,CAAC;AACD,SAAG,QAAQ,GAAG;AAAA,IAChB,OAAO;AACL,UAAI,OAAO,OAAO;AAClB,UAAI,QAAQ,OAAO;AACnB,UAAI,QAAQ,OAAO;AACnB,UAAI,cAAc,OAAO;AAAA,IAC3B;AACA,UAAM,cAAc;AACpB,UAAM,gBAAgB,IAAI;AAAA,MACxB,MAAM;AACJ,WAAG,QAAQ,WAAW;AAAA,MACxB;AAAA,IACF,GAAG,EAAE,aAAa,KAAK,CAAC;AAExB,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,wBAAwB;AAAA,MAC5B,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,IAAI;AAAA,QACR,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,MAChB;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AACF;AAEA,MAAM,mBAAiF;AAAA,EACrF,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,oBAAoB,MAAM,QAAQ;AACjD,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AACjE,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,OAAO,UAAU,OAAO,UAAU,gBAAgB,OAAO,eAAe,CAAC;AAChI,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,UAAM,SAAS,MAAM,sBAAsB,IAAI,OAAO,UAAU,EAAE,UAAU,OAAO,UAAU,gBAAgB,OAAO,eAAe,GAAG,QAAW,oBAAoB;AACrK,oBAAgB,QAAQ,OAAO,gBAAgB,OAAO,QAAQ;AAC9D,UAAM,SAAS,MAAM,iBAAiB,IAAI,MAAM;AAC9C,QAAI,OAAO,SAAS,OAAO,KAAK,GAAG;AACjC,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,uBAAuB,CAAC;AAAA,IAChE;AAEA,UAAM,aAAa,GAAG,OAAO,uBAAuB;AAAA,MAClD;AAAA,MACA;AAAA,MACA,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,CAAC;AACD,OAAG,QAAQ,UAAU;AACrB,UAAM,GAAG,MAAM;AAEf,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO,WAAW,EAAE;AAAA,QACxB,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,MACnB;AAAA,IACF,CAAC;AAED,UAAM,mBAAmB,0BAA0B;AAAA,MACjD,IAAI,OAAO,WAAW,EAAE;AAAA,MACxB,OAAO,OAAO;AAAA,MACd,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,GAAG,EAAE,YAAY,KAAK,CAAC;AAEvB,WAAO,EAAE,cAAc,WAAW,GAAG;AAAA,EACvC;AAAA,EACA,UAAU,OAAO,EAAE,QAAQ,IAAI,MAAM;AACnC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,aAAa,MAAM;AAAA,MACvB;AAAA,MACA;AAAA,MACA,EAAE,IAAI,OAAO,aAAa;AAAA,MAC1B,EAAE,UAAU,CAAC,OAAO,QAAQ,EAAE;AAAA,MAC9B,EAAE,UAAU,IAAI,MAAM,YAAY,MAAM,gBAAgB,IAAI,0BAA0B,IAAI,MAAM,SAAS,KAAK;AAAA,IAChH;AACA,QAAI,CAAC,WAAY,QAAO;AACxB,UAAM,QAAQ,OAAO,WAAW,QAAQ,WAAW,WAAW,MAAM,WAAW,IAAI;AACnF,UAAM,WAAW,OAAO,WAAW,WAAW,WAAW,WAAW,SAAS,WAAW,OAAO;AAC/F,WAAO;AAAA,MACL,aAAa,UAAU,+BAA+B,YAAY;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,WAAW;AAAA,MACrB,gBAAgB,WAAW;AAAA,MAC3B,SAAS;AAAA,QACP,MAAM;AAAA,UACJ,QAAQ;AAAA,YACN;AAAA,YACA;AAAA,YACA,UAAU,WAAW;AAAA,YACrB,gBAAgB,WAAW;AAAA,UAC7B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAA6C,QAAQ;AACrE,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,OAAQ;AACb,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,GAAG,aAAa,uBAAuB;AAAA,MAC3C,KAAK,OAAO;AAAA,MACZ,QAAQ,OAAO;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AAAA,EACH;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAA6C,QAAQ;AACrE,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,sDAAsD,CAAC;AAAA,IAC/F;AACA,UAAM,eAAe,UAAU,cAAc;AAC7C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,MAAM,CAAC;AAC9D,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,UAAM,SAAS,MAAM,sBAAsB,IAAI,OAAO,UAAU,EAAE,UAAU,OAAO,UAAU,gBAAgB,OAAO,eAAe,GAAG,QAAW,oBAAoB;AACrK,oBAAgB,QAAQ,OAAO,gBAAgB,OAAO,QAAQ;AAE9D,QAAI,aAAa,MAAM,GAAG,QAAQ,uBAAuB;AAAA,MACvD;AAAA,MACA;AAAA,MACA,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AACD,QAAI,CAAC,YAAY;AACf,mBAAa,GAAG,OAAO,uBAAuB;AAAA,QAC5C,GAAI,eAAe,EAAE,IAAI,aAAa,IAAI,CAAC;AAAA,QAC3C;AAAA,QACA;AAAA,QACA,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,MACnB,CAAC;AACD,SAAG,QAAQ,UAAU;AACrB,YAAM,GAAG,MAAM;AAAA,IACjB;AAEA,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO,WAAW,EAAE;AAAA,QACxB,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,MACnB;AAAA,IACF,CAAC;AAED,UAAM,mBAAmB,0BAA0B;AAAA,MACjD,IAAI,OAAO,WAAW,EAAE;AAAA,MACxB,OAAO,OAAO;AAAA,MACd,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,GAAG,EAAE,YAAY,KAAK,CAAC;AAEvB,WAAO,EAAE,cAAc,WAAW,GAAG;AAAA,EACvC;AACF;AAEA,MAAM,qBAA0F;AAAA,EAC9F,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,oBAAoB,MAAM,QAAQ;AACjD,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,WAAW,MAAM,GAAG,QAAQ,uBAAuB;AAAA,MACvD,KAAK,OAAO;AAAA,MACZ,QAAQ,OAAO;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AACD,QAAI,CAAC,SAAU,OAAM,SAAS,0BAA0B;AACxD,UAAM,GAAG,OAAO,QAAQ,EAAE,MAAM;AAEhC,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO,SAAS,EAAE;AAAA,QACtB,gBAAgB,SAAS;AAAA,QACzB,UAAU,SAAS;AAAA,MACrB;AAAA,IACF,CAAC;AAED,UAAM,mBAAmB,yBAAyB;AAAA,MAChD,IAAI,OAAO,SAAS,EAAE;AAAA,MACtB,OAAO,OAAO;AAAA,MACd,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,GAAG,EAAE,YAAY,KAAK,CAAC;AAEvB,WAAO,EAAE,cAAc,SAAS,MAAM,KAAK;AAAA,EAC7C;AAAA,EACA,UAAU,OAAO,EAAE,WAAW,MAAM,MAAM;AACxC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,SAAS,oBAAoB,MAAM,KAAK;AAC9C,WAAO;AAAA,MACL,aAAa,UAAU,iCAAiC,cAAc;AAAA,MACtE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,SAAS;AAAA,QACP,MAAM;AAAA,UACJ,QAAQ;AAAA,YACN,OAAO,OAAO;AAAA,YACd,UAAU,OAAO;AAAA,YACjB,UAAU,OAAO;AAAA,YACjB,gBAAgB,OAAO;AAAA,UACzB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAA6C,QAAQ;AACrE,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,OAAQ;AACb,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,MAAM,CAAC;AAC9D,UAAM,SAAS,MAAM,sBAAsB,IAAI,OAAO,UAAU,EAAE,UAAU,OAAO,UAAU,gBAAgB,OAAO,eAAe,GAAG,QAAW,oBAAoB;AACrK,oBAAgB,QAAQ,OAAO,gBAAgB,OAAO,QAAQ;AAC9D,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,UAAM,WAAW,MAAM,GAAG,QAAQ,uBAAuB;AAAA,MACvD;AAAA,MACA;AAAA,MACA,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AACD,QAAI,CAAC,UAAU;AACb,YAAM,aAAa,GAAG,OAAO,uBAAuB;AAAA,QAClD;AAAA,QACA;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,MACzB,CAAC;AACD,SAAG,QAAQ,UAAU;AACrB,YAAM,GAAG,MAAM;AAAA,IACjB;AAAA,EACF;AACF;AAEA,gBAAgB,gBAAgB;AAChC,gBAAgB,gBAAgB;AAChC,gBAAgB,gBAAgB;AAChC,gBAAgB,gBAAgB;AAChC,gBAAgB,kBAAkB;",
4
+ "sourcesContent": ["import { registerCommand } from '@open-mercato/shared/lib/commands'\nimport type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { emitCrudSideEffects, emitCrudUndoSideEffects, buildChanges, requireId } from '@open-mercato/shared/lib/commands/helpers'\nimport type { DataEngine } from '@open-mercato/shared/lib/data/engine'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { LockMode, type FilterQuery } from '@mikro-orm/core'\nimport { buildScopedWhere } from '@open-mercato/shared/lib/api/crud'\nimport { isUnrestrictedOrganizationScope } from '@open-mercato/shared/lib/auth/organizationAccess'\nimport { CustomerEntity, CustomerTag, CustomerTagAssignment } from '../data/entities'\nimport {\n tagCreateSchema,\n tagUpdateSchema,\n tagAssignmentSchema,\n type TagCreateInput,\n type TagUpdateInput,\n type TagAssignmentInput,\n} from '../data/validators'\nimport {\n ensureOrganizationScope,\n ensureTenantScope,\n extractUndoPayload,\n requireCustomerEntity,\n ensureSameScope,\n loadEntityTagIds,\n emitQueryIndexUpsertEvents,\n} from './shared'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CrudHttpError, forbidden, notFound } from '@open-mercato/shared/lib/crud/errors'\nimport type { CrudEventsConfig, CrudIndexerConfig } from '@open-mercato/shared/lib/crud/types'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'\nimport { emitCustomersEvent } from '../events'\nimport { withAtomicFlush } from '@open-mercato/shared/lib/commands/flush'\nimport { makeCreateRedo } from '@open-mercato/shared/lib/commands/redo'\nimport { E } from '#generated/entities.ids.generated'\n\n// The route (`api/tags/route.ts`) declares this same entity type and hands it down to the\n// execute path, but an undo runs through `CommandBus.undo()` where no route declaration is\n// active \u2014 so the undo marks carry the indexer explicitly, or a restored tag would stay\n// invisible to every index-backed list until the next full rebuild (#5741).\nconst tagCrudIndexer: CrudIndexerConfig<CustomerTag> = {\n entityType: E.customers.customer_tag,\n}\n\nconst tagCrudEvents: CrudEventsConfig = {\n module: 'customers',\n entity: 'tag',\n persistent: true,\n buildPayload: (ctx) => ({\n id: ctx.identifiers.id,\n organizationId: ctx.identifiers.organizationId,\n tenantId: ctx.identifiers.tenantId,\n }),\n}\n\ntype TagSnapshot = {\n id: string\n organizationId: string\n tenantId: string\n slug: string\n label: string\n color: string | null\n description: string | null\n assignments?: TagMembershipSnapshot[]\n}\n\ntype TagMembershipSnapshot = {\n id: string\n entityId: string\n organizationId: string\n tenantId: string\n createdAt: string\n}\n\ntype TagUndoPayload = {\n before?: TagSnapshot | null\n after?: TagSnapshot | null\n}\n\ntype TagAssignmentSnapshot = {\n tagId: string\n entityId: string\n organizationId: string\n tenantId: string\n}\n\ntype TagAssignmentUndoPayload = {\n before?: TagAssignmentSnapshot | null\n}\n\nfunction tagDeleteLookup(id: string, ctx: CommandRuntimeContext) {\n const isSuperAdmin = ctx.auth?.isSuperAdmin === true\n const organizationScope = ctx.organizationScope\n const tenantId = ctx.auth ? (ctx.auth.tenantId ?? null) : (organizationScope?.tenantId ?? null)\n if (ctx.auth && !isSuperAdmin && (!tenantId || (organizationScope && !organizationScope.tenantId))) {\n throw forbidden()\n }\n let organizationIds: string[] | undefined\n if (organizationScope) {\n if (!isUnrestrictedOrganizationScope({ isSuperAdmin, allowedOrganizationIds: organizationScope.allowedIds })) {\n organizationIds = organizationScope.allowedIds ?? []\n }\n } else if (!isSuperAdmin) {\n const organizationId = ctx.selectedOrganizationId ?? ctx.auth?.orgId\n if (organizationId) organizationIds = [organizationId]\n }\n return {\n where: buildScopedWhere({ id }, {\n tenantId: tenantId ?? undefined,\n organizationIds,\n softDeleteField: null,\n }) as FilterQuery<CustomerTag>,\n scope: { tenantId, organizationId: organizationIds?.length === 1 ? organizationIds[0] : null },\n }\n}\n\nasync function loadTagSnapshot(\n em: EntityManager,\n id: string,\n lookup?: ReturnType<typeof tagDeleteLookup>,\n): Promise<TagSnapshot | null> {\n const tag = lookup\n ? await findOneWithDecryption(em, CustomerTag, lookup.where, {}, lookup.scope)\n : await em.findOne(CustomerTag, { id })\n if (!tag) return null\n return {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n slug: tag.slug,\n label: tag.label,\n color: tag.color ?? null,\n description: tag.description ?? null,\n }\n}\n\nasync function loadTagAssignments(em: EntityManager, tag: CustomerTag) {\n const scope = { tenantId: tag.tenantId, organizationId: tag.organizationId }\n return findWithDecryption(em, CustomerTagAssignment, { tag: tag.id, ...scope }, {}, scope)\n}\n\nasync function refreshTagMemberships(\n ctx: CommandRuntimeContext,\n tag: CustomerTag,\n assignments: CustomerTagAssignment[],\n action: 'created' | 'deleted',\n) {\n const dataEngine = ctx.container.resolve<DataEngine>('dataEngine')\n for (const assignment of assignments) {\n await emitCrudSideEffects({\n dataEngine,\n action,\n entity: assignment,\n identifiers: { id: assignment.id, tenantId: tag.tenantId, organizationId: tag.organizationId },\n indexer: { entityType: E.customers.customer_tag_assignment },\n })\n await emitCustomersEvent(action === 'created' ? 'customers.tag.assigned' : 'customers.tag.removed', {\n id: assignment.id,\n tagId: tag.id,\n entityId: assignment.entity.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n }, { persistent: true })\n }\n await emitQueryIndexUpsertEvents(ctx, assignments.map((assignment) => ({\n entityType: E.customers.customer_entity,\n recordId: assignment.entity.id,\n tenantId: tag.tenantId,\n organizationId: tag.organizationId,\n })))\n await invalidateCrudCache(ctx.container, 'customers.tagAssignment', {\n tenantId: tag.tenantId,\n organizationId: tag.organizationId,\n }, tag.tenantId, `customers.tags.delete.${action}`, ['customers.person', 'customers.company'])\n}\n\nconst createTagCommand: CommandHandler<TagCreateInput, { tagId: string }> = {\n id: 'customers.tags.create',\n async execute(rawInput, ctx) {\n const parsed = tagCreateSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const duplicate = await em.findOne(CustomerTag, {\n slug: parsed.slug,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n })\n if (duplicate) throw new CrudHttpError(409, { error: 'Tag slug already exists' })\n\n const tag = em.create(CustomerTag, {\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n slug: parsed.slug,\n label: parsed.label,\n color: parsed.color ?? null,\n description: parsed.description ?? null,\n })\n em.persist(tag)\n await em.flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'created',\n entity: tag,\n identifiers: {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n },\n events: tagCrudEvents,\n })\n\n return { tagId: tag.id }\n },\n captureAfter: async (_input, result, ctx) => {\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n return await loadTagSnapshot(em, result.tagId)\n },\n buildLog: async ({ result, ctx }) => {\n const { translate } = await resolveTranslations()\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTagSnapshot(em, result.tagId)\n return {\n actionLabel: translate('customers.audit.tags.create', 'Create tag'),\n resourceKind: 'customers.tag',\n resourceId: result.tagId,\n tenantId: snapshot?.tenantId ?? null,\n organizationId: snapshot?.organizationId ?? null,\n snapshotAfter: snapshot ?? null,\n payload: {\n undo: {\n after: snapshot ?? null,\n } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const tagId = logEntry?.resourceId ?? null\n if (!tagId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: tagId })\n if (tag) {\n await em.nativeDelete(CustomerTagAssignment, { tag })\n em.remove(tag)\n await em.flush()\n }\n },\n redo: makeCreateRedo<CustomerTag, TagSnapshot, TagCreateInput, { tagId: string }>({\n entityClass: CustomerTag,\n buildResult: (entity) => ({ tagId: entity.id }),\n events: tagCrudEvents,\n }),\n}\n\nconst updateTagCommand: CommandHandler<TagUpdateInput, { tagId: string }> = {\n id: 'customers.tags.update',\n async prepare(rawInput, ctx) {\n const parsed = tagUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager)\n const snapshot = await loadTagSnapshot(em, parsed.id)\n return snapshot ? { before: snapshot } : {}\n },\n async execute(rawInput, ctx) {\n const parsed = tagUpdateSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: parsed.id })\n if (!tag) throw notFound('Tag not found')\n ensureTenantScope(ctx, tag.tenantId)\n ensureOrganizationScope(ctx, tag.organizationId)\n\n if (parsed.slug !== undefined && parsed.slug !== tag.slug) {\n const duplicate = await em.findOne(CustomerTag, {\n slug: parsed.slug,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n id: { $ne: tag.id },\n })\n if (duplicate) throw new CrudHttpError(409, { error: 'Tag slug already exists' })\n tag.slug = parsed.slug\n }\n if (parsed.label !== undefined) tag.label = parsed.label\n if (parsed.color !== undefined) tag.color = parsed.color ?? null\n if (parsed.description !== undefined) tag.description = parsed.description ?? null\n\n await em.flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: tag,\n identifiers: {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n },\n events: tagCrudEvents,\n })\n\n return { tagId: tag.id }\n },\n buildLog: async ({ snapshots, ctx }) => {\n const { translate } = await resolveTranslations()\n const before = snapshots.before as TagSnapshot | undefined\n if (!before) return null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const afterSnapshot = await loadTagSnapshot(em, before.id)\n const changes =\n afterSnapshot && before\n ? buildChanges(\n before as unknown as Record<string, unknown>,\n afterSnapshot as unknown as Record<string, unknown>,\n ['slug', 'label', 'color', 'description']\n )\n : {}\n return {\n actionLabel: translate('customers.audit.tags.update', 'Update tag'),\n resourceKind: 'customers.tag',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n snapshotBefore: before,\n snapshotAfter: afterSnapshot ?? null,\n changes,\n payload: {\n undo: {\n before,\n after: afterSnapshot ?? null,\n } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let tag = await em.findOne(CustomerTag, { id: before.id })\n if (!tag) {\n tag = em.create(CustomerTag, {\n id: before.id,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n slug: before.slug,\n label: before.label,\n color: before.color,\n description: before.description,\n })\n em.persist(tag)\n } else {\n tag.slug = before.slug\n tag.label = before.label\n tag.color = before.color\n tag.description = before.description\n }\n await em.flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudUndoSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: tag,\n identifiers: {\n id: tag.id,\n organizationId: tag.organizationId,\n tenantId: tag.tenantId,\n },\n events: tagCrudEvents,\n indexer: tagCrudIndexer,\n })\n },\n}\n\nconst deleteTagCommand: CommandHandler<{ body?: Record<string, unknown>; query?: Record<string, unknown> }, { tagId: string }> = {\n id: 'customers.tags.delete',\n prepareSnapshotInsideTransaction: true,\n async prepare(input, ctx) {\n const id = requireId(input, 'Tag id required')\n const em = ctx.transactionalEm ?? (ctx.container.resolve('em') as EntityManager).fork()\n const snapshot = await loadTagSnapshot(em, id, tagDeleteLookup(id, ctx))\n if (!snapshot) return {}\n ensureTenantScope(ctx, snapshot.tenantId)\n ensureOrganizationScope(ctx, snapshot.organizationId)\n const assignments = await findWithDecryption(em, CustomerTagAssignment, {\n tag: id,\n tenantId: snapshot.tenantId,\n organizationId: snapshot.organizationId,\n }, {}, snapshot)\n snapshot.assignments = assignments.map((assignment) => ({\n id: assignment.id,\n entityId: assignment.entity.id,\n tenantId: assignment.tenantId,\n organizationId: assignment.organizationId,\n createdAt: assignment.createdAt.toISOString(),\n }))\n return { before: snapshot }\n },\n async execute(input, ctx) {\n const id = requireId(input, 'Tag id required')\n const lookup = tagDeleteLookup(id, ctx)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n let tag: CustomerTag | null = null\n let assignments: CustomerTagAssignment[] = []\n await withAtomicFlush(em, [\n async () => {\n tag = await findOneWithDecryption(em, CustomerTag, lookup.where, { lockMode: LockMode.PESSIMISTIC_WRITE }, lookup.scope)\n if (!tag) throw notFound('Tag not found')\n ensureTenantScope(ctx, tag.tenantId)\n ensureOrganizationScope(ctx, tag.organizationId)\n await ctx.beforeTransactionalWrite?.(em, input)\n assignments = await loadTagAssignments(em, tag)\n await em.nativeDelete(CustomerTagAssignment, { tag, tenantId: tag.tenantId, organizationId: tag.organizationId })\n em.remove(tag)\n },\n ], { transaction: true, label: 'customers.tags.delete' })\n if (!tag) throw notFound('Tag not found')\n const deletedTag = tag as CustomerTag\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'deleted',\n entity: deletedTag,\n identifiers: {\n id: deletedTag.id,\n organizationId: deletedTag.organizationId,\n tenantId: deletedTag.tenantId,\n },\n events: tagCrudEvents,\n indexer: tagCrudIndexer,\n })\n await refreshTagMemberships(ctx, deletedTag, assignments, 'deleted')\n return { tagId: deletedTag.id }\n },\n buildLog: async ({ snapshots }) => {\n const before = snapshots.before as TagSnapshot | undefined\n if (!before) return null\n const { translate } = await resolveTranslations()\n return {\n actionLabel: translate('customers.audit.tags.delete', 'Delete tag'),\n resourceKind: 'customers.tag',\n resourceId: before.id,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n context: { cacheAliases: ['customers.tagAssignment', 'customers.person', 'customers.company'] },\n snapshotBefore: before,\n payload: {\n undo: {\n before,\n } satisfies TagUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n ensureTenantScope(ctx, before.tenantId)\n ensureOrganizationScope(ctx, before.organizationId)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const scope = { tenantId: before.tenantId, organizationId: before.organizationId }\n const existingTag = await findOneWithDecryption(em, CustomerTag, { id: before.id, ...scope }, {}, scope)\n const tag = existingTag ?? em.create(CustomerTag, { id: before.id, ...scope, slug: before.slug, label: before.label })\n const restoredAssignments: CustomerTagAssignment[] = []\n await withAtomicFlush(em, [\n () => {\n tag.slug = before.slug\n tag.label = before.label\n tag.color = before.color\n tag.description = before.description\n em.persist(tag)\n },\n async () => {\n const assignments = (before.assignments ?? []).filter((assignment) =>\n assignment.tenantId === before.tenantId && assignment.organizationId === before.organizationId,\n )\n if (!assignments.length) return\n const customers = await findWithDecryption(em, CustomerEntity, {\n id: { $in: assignments.map((assignment) => assignment.entityId) },\n ...scope,\n deletedAt: null,\n kind: { $in: ['person', 'company'] },\n }, {}, scope)\n const existingAssignments = await loadTagAssignments(em, tag)\n const assignedEntityIds = new Set(existingAssignments.map((assignment) => assignment.entity.id))\n const customersById = new Map(customers.map((customer) => [customer.id, customer]))\n for (const snapshot of assignments) {\n const entity = customersById.get(snapshot.entityId)\n if (!entity || assignedEntityIds.has(entity.id)) continue\n const assignment = em.create(CustomerTagAssignment, {\n id: snapshot.id,\n tag,\n entity,\n ...scope,\n createdAt: new Date(snapshot.createdAt),\n })\n em.persist(assignment)\n restoredAssignments.push(assignment)\n assignedEntityIds.add(entity.id)\n }\n },\n ], { transaction: true, label: 'customers.tags.delete.undo' })\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudUndoSideEffects({\n dataEngine: de,\n action: 'created',\n entity: tag,\n identifiers: {\n id: before.id,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n },\n events: tagCrudEvents,\n indexer: tagCrudIndexer,\n })\n await refreshTagMemberships(ctx, tag, restoredAssignments, 'created')\n },\n}\n\nconst assignTagCommand: CommandHandler<TagAssignmentInput, { assignmentId: string }> = {\n id: 'customers.tags.assign',\n async execute(rawInput, ctx) {\n const parsed = tagAssignmentSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: parsed.tagId, tenantId: parsed.tenantId, organizationId: parsed.organizationId })\n if (!tag) throw notFound('Tag not found')\n const entity = await requireCustomerEntity(em, parsed.entityId, { tenantId: parsed.tenantId, organizationId: parsed.organizationId }, undefined, 'Customer not found')\n ensureSameScope(entity, parsed.organizationId, parsed.tenantId)\n const tagIds = await loadEntityTagIds(em, entity)\n if (tagIds.includes(parsed.tagId)) {\n throw new CrudHttpError(409, { error: 'Tag already assigned' })\n }\n\n const assignment = em.create(CustomerTagAssignment, {\n tag,\n entity,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n })\n em.persist(assignment)\n await em.flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: assignment,\n identifiers: {\n id: String(assignment.id),\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n },\n })\n\n await emitCustomersEvent('customers.tag.assigned', {\n id: String(assignment.id),\n tagId: parsed.tagId,\n entityId: parsed.entityId,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n }, { persistent: true })\n\n return { assignmentId: assignment.id }\n },\n buildLog: async ({ result, ctx }) => {\n const { translate } = await resolveTranslations()\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const assignment = await findOneWithDecryption(\n em,\n CustomerTagAssignment,\n { id: result.assignmentId },\n { populate: ['tag', 'entity'] },\n { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.selectedOrganizationId ?? ctx.auth?.orgId ?? null },\n )\n if (!assignment) return null\n const tagId = typeof assignment.tag === 'string' ? assignment.tag : assignment.tag.id\n const entityId = typeof assignment.entity === 'string' ? assignment.entity : assignment.entity.id\n return {\n actionLabel: translate('customers.audit.tags.assign', 'Assign tag'),\n resourceKind: 'customers.tagAssignment',\n resourceId: result.assignmentId,\n tenantId: assignment.tenantId,\n organizationId: assignment.organizationId,\n payload: {\n undo: {\n before: {\n tagId,\n entityId,\n tenantId: assignment.tenantId,\n organizationId: assignment.organizationId,\n } satisfies TagAssignmentSnapshot,\n } satisfies TagAssignmentUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagAssignmentUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n await em.nativeDelete(CustomerTagAssignment, {\n tag: before.tagId,\n entity: before.entityId,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n })\n },\n redo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagAssignmentUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) {\n throw new CrudHttpError(400, { error: '[internal] redo snapshot unavailable for tag assign' })\n }\n const assignmentId = logEntry?.resourceId ?? null\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: before.tagId })\n if (!tag) throw notFound('Tag not found')\n const entity = await requireCustomerEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId }, undefined, 'Customer not found')\n ensureSameScope(entity, before.organizationId, before.tenantId)\n\n let assignment = await em.findOne(CustomerTagAssignment, {\n tag,\n entity,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n })\n if (!assignment) {\n assignment = em.create(CustomerTagAssignment, {\n ...(assignmentId ? { id: assignmentId } : {}),\n tag,\n entity,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n })\n em.persist(assignment)\n await em.flush()\n }\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: assignment,\n identifiers: {\n id: String(assignment.id),\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n },\n })\n\n await emitCustomersEvent('customers.tag.assigned', {\n id: String(assignment.id),\n tagId: before.tagId,\n entityId: before.entityId,\n organizationId: before.organizationId,\n tenantId: before.tenantId,\n }, { persistent: true })\n\n return { assignmentId: assignment.id }\n },\n}\n\nconst unassignTagCommand: CommandHandler<TagAssignmentInput, { assignmentId: string | null }> = {\n id: 'customers.tags.unassign',\n async execute(rawInput, ctx) {\n const parsed = tagAssignmentSchema.parse(rawInput)\n ensureTenantScope(ctx, parsed.tenantId)\n ensureOrganizationScope(ctx, parsed.organizationId)\n\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const existing = await em.findOne(CustomerTagAssignment, {\n tag: parsed.tagId,\n entity: parsed.entityId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n })\n if (!existing) throw notFound('Tag assignment not found')\n await em.remove(existing).flush()\n\n const de = (ctx.container.resolve('dataEngine') as DataEngine)\n await emitCrudSideEffects({\n dataEngine: de,\n action: 'updated',\n entity: existing,\n identifiers: {\n id: String(existing.id),\n organizationId: existing.organizationId,\n tenantId: existing.tenantId,\n },\n })\n\n await emitCustomersEvent('customers.tag.removed', {\n id: String(existing.id),\n tagId: parsed.tagId,\n entityId: parsed.entityId,\n organizationId: parsed.organizationId,\n tenantId: parsed.tenantId,\n }, { persistent: true })\n\n return { assignmentId: existing.id ?? null }\n },\n buildLog: async ({ snapshots, input }) => {\n const { translate } = await resolveTranslations()\n const parsed = tagAssignmentSchema.parse(input)\n return {\n actionLabel: translate('customers.audit.tags.unassign', 'Unassign tag'),\n resourceKind: 'customers.tagAssignment',\n resourceId: parsed.tagId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n payload: {\n undo: {\n before: {\n tagId: parsed.tagId,\n entityId: parsed.entityId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n } satisfies TagAssignmentSnapshot,\n } satisfies TagAssignmentUndoPayload,\n },\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const payload = extractUndoPayload<TagAssignmentUndoPayload>(logEntry)\n const before = payload?.before\n if (!before) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const tag = await em.findOne(CustomerTag, { id: before.tagId })\n const entity = await requireCustomerEntity(em, before.entityId, { tenantId: before.tenantId, organizationId: before.organizationId }, undefined, 'Customer not found')\n ensureSameScope(entity, before.organizationId, before.tenantId)\n if (!tag) throw notFound('Tag not found')\n const existing = await em.findOne(CustomerTagAssignment, {\n tag,\n entity,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n })\n if (!existing) {\n const assignment = em.create(CustomerTagAssignment, {\n tag,\n entity,\n tenantId: before.tenantId,\n organizationId: before.organizationId,\n })\n em.persist(assignment)\n await em.flush()\n }\n },\n}\n\nregisterCommand(createTagCommand)\nregisterCommand(updateTagCommand)\nregisterCommand(deleteTagCommand)\nregisterCommand(assignTagCommand)\nregisterCommand(unassignTagCommand)\n"],
5
+ "mappings": "AAAA,SAAS,uBAAuB;AAEhC,SAAS,qBAAqB,yBAAyB,cAAc,iBAAiB;AAGtF,SAAS,gBAAkC;AAC3C,SAAS,wBAAwB;AACjC,SAAS,uCAAuC;AAChD,SAAS,gBAAgB,aAAa,6BAA6B;AACnE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,2BAA2B;AACpC,SAAS,eAAe,WAAW,gBAAgB;AAEnD,SAAS,uBAAuB,0BAA0B;AAC1D,SAAS,2BAA2B;AACpC,SAAS,0BAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,sBAAsB;AAC/B,SAAS,SAAS;AAMlB,MAAM,iBAAiD;AAAA,EACrD,YAAY,EAAE,UAAU;AAC1B;AAEA,MAAM,gBAAkC;AAAA,EACtC,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,cAAc,CAAC,SAAS;AAAA,IACtB,IAAI,IAAI,YAAY;AAAA,IACpB,gBAAgB,IAAI,YAAY;AAAA,IAChC,UAAU,IAAI,YAAY;AAAA,EAC5B;AACF;AAqCA,SAAS,gBAAgB,IAAY,KAA4B;AAC/D,QAAM,eAAe,IAAI,MAAM,iBAAiB;AAChD,QAAM,oBAAoB,IAAI;AAC9B,QAAM,WAAW,IAAI,OAAQ,IAAI,KAAK,YAAY,OAAS,mBAAmB,YAAY;AAC1F,MAAI,IAAI,QAAQ,CAAC,iBAAiB,CAAC,YAAa,qBAAqB,CAAC,kBAAkB,WAAY;AAClG,UAAM,UAAU;AAAA,EAClB;AACA,MAAI;AACJ,MAAI,mBAAmB;AACrB,QAAI,CAAC,gCAAgC,EAAE,cAAc,wBAAwB,kBAAkB,WAAW,CAAC,GAAG;AAC5G,wBAAkB,kBAAkB,cAAc,CAAC;AAAA,IACrD;AAAA,EACF,WAAW,CAAC,cAAc;AACxB,UAAM,iBAAiB,IAAI,0BAA0B,IAAI,MAAM;AAC/D,QAAI,eAAgB,mBAAkB,CAAC,cAAc;AAAA,EACvD;AACA,SAAO;AAAA,IACL,OAAO,iBAAiB,EAAE,GAAG,GAAG;AAAA,MAC9B,UAAU,YAAY;AAAA,MACtB;AAAA,MACA,iBAAiB;AAAA,IACnB,CAAC;AAAA,IACD,OAAO,EAAE,UAAU,gBAAgB,iBAAiB,WAAW,IAAI,gBAAgB,CAAC,IAAI,KAAK;AAAA,EAC/F;AACF;AAEA,eAAe,gBACb,IACA,IACA,QAC6B;AAC7B,QAAM,MAAM,SACR,MAAM,sBAAsB,IAAI,aAAa,OAAO,OAAO,CAAC,GAAG,OAAO,KAAK,IAC3E,MAAM,GAAG,QAAQ,aAAa,EAAE,GAAG,CAAC;AACxC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,gBAAgB,IAAI;AAAA,IACpB,UAAU,IAAI;AAAA,IACd,MAAM,IAAI;AAAA,IACV,OAAO,IAAI;AAAA,IACX,OAAO,IAAI,SAAS;AAAA,IACpB,aAAa,IAAI,eAAe;AAAA,EAClC;AACF;AAEA,eAAe,mBAAmB,IAAmB,KAAkB;AACrE,QAAM,QAAQ,EAAE,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AAC3E,SAAO,mBAAmB,IAAI,uBAAuB,EAAE,KAAK,IAAI,IAAI,GAAG,MAAM,GAAG,CAAC,GAAG,KAAK;AAC3F;AAEA,eAAe,sBACb,KACA,KACA,aACA,QACA;AACA,QAAM,aAAa,IAAI,UAAU,QAAoB,YAAY;AACjE,aAAW,cAAc,aAAa;AACpC,UAAM,oBAAoB;AAAA,MACxB;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,aAAa,EAAE,IAAI,WAAW,IAAI,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe;AAAA,MAC7F,SAAS,EAAE,YAAY,EAAE,UAAU,wBAAwB;AAAA,IAC7D,CAAC;AACD,UAAM,mBAAmB,WAAW,YAAY,2BAA2B,yBAAyB;AAAA,MAClG,IAAI,WAAW;AAAA,MACf,OAAO,IAAI;AAAA,MACX,UAAU,WAAW,OAAO;AAAA,MAC5B,gBAAgB,IAAI;AAAA,MACpB,UAAU,IAAI;AAAA,IAChB,GAAG,EAAE,YAAY,KAAK,CAAC;AAAA,EACzB;AACA,QAAM,2BAA2B,KAAK,YAAY,IAAI,CAAC,gBAAgB;AAAA,IACrE,YAAY,EAAE,UAAU;AAAA,IACxB,UAAU,WAAW,OAAO;AAAA,IAC5B,UAAU,IAAI;AAAA,IACd,gBAAgB,IAAI;AAAA,EACtB,EAAE,CAAC;AACH,QAAM,oBAAoB,IAAI,WAAW,2BAA2B;AAAA,IAClE,UAAU,IAAI;AAAA,IACd,gBAAgB,IAAI;AAAA,EACtB,GAAG,IAAI,UAAU,yBAAyB,MAAM,IAAI,CAAC,oBAAoB,mBAAmB,CAAC;AAC/F;AAEA,MAAM,mBAAsE;AAAA,EAC1E,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,gBAAgB,MAAM,QAAQ;AAC7C,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,YAAY,MAAM,GAAG,QAAQ,aAAa;AAAA,MAC9C,MAAM,OAAO;AAAA,MACb,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,CAAC;AACD,QAAI,UAAW,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,0BAA0B,CAAC;AAEhF,UAAM,MAAM,GAAG,OAAO,aAAa;AAAA,MACjC,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,MACjB,MAAM,OAAO;AAAA,MACb,OAAO,OAAO;AAAA,MACd,OAAO,OAAO,SAAS;AAAA,MACvB,aAAa,OAAO,eAAe;AAAA,IACrC,CAAC;AACD,OAAG,QAAQ,GAAG;AACd,UAAM,GAAG,MAAM;AAEf,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,IAAI;AAAA,QACR,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,MAChB;AAAA,MACA,QAAQ;AAAA,IACV,CAAC;AAED,WAAO,EAAE,OAAO,IAAI,GAAG;AAAA,EACzB;AAAA,EACA,cAAc,OAAO,QAAQ,QAAQ,QAAQ;AAC3C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,WAAO,MAAM,gBAAgB,IAAI,OAAO,KAAK;AAAA,EAC/C;AAAA,EACA,UAAU,OAAO,EAAE,QAAQ,IAAI,MAAM;AACnC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,WAAW,MAAM,gBAAgB,IAAI,OAAO,KAAK;AACvD,WAAO;AAAA,MACL,aAAa,UAAU,+BAA+B,YAAY;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,UAAU,YAAY;AAAA,MAChC,gBAAgB,UAAU,kBAAkB;AAAA,MAC5C,eAAe,YAAY;AAAA,MAC3B,SAAS;AAAA,QACP,MAAM;AAAA,UACJ,OAAO,YAAY;AAAA,QACrB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,QAAQ,UAAU,cAAc;AACtC,QAAI,CAAC,MAAO;AACZ,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,MAAM,CAAC;AACvD,QAAI,KAAK;AACP,YAAM,GAAG,aAAa,uBAAuB,EAAE,IAAI,CAAC;AACpD,SAAG,OAAO,GAAG;AACb,YAAM,GAAG,MAAM;AAAA,IACjB;AAAA,EACF;AAAA,EACA,MAAM,eAA4E;AAAA,IAChF,aAAa;AAAA,IACb,aAAa,CAAC,YAAY,EAAE,OAAO,OAAO,GAAG;AAAA,IAC7C,QAAQ;AAAA,EACV,CAAC;AACH;AAEA,MAAM,mBAAsE;AAAA,EAC1E,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,gBAAgB,MAAM,QAAQ;AAC7C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI;AACtC,UAAM,WAAW,MAAM,gBAAgB,IAAI,OAAO,EAAE;AACpD,WAAO,WAAW,EAAE,QAAQ,SAAS,IAAI,CAAC;AAAA,EAC5C;AAAA,EACA,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,gBAAgB,MAAM,QAAQ;AAC7C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,GAAG,CAAC;AAC3D,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,sBAAkB,KAAK,IAAI,QAAQ;AACnC,4BAAwB,KAAK,IAAI,cAAc;AAE/C,QAAI,OAAO,SAAS,UAAa,OAAO,SAAS,IAAI,MAAM;AACzD,YAAM,YAAY,MAAM,GAAG,QAAQ,aAAa;AAAA,QAC9C,MAAM,OAAO;AAAA,QACb,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,QACd,IAAI,EAAE,KAAK,IAAI,GAAG;AAAA,MACpB,CAAC;AACD,UAAI,UAAW,OAAM,IAAI,cAAc,KAAK,EAAE,OAAO,0BAA0B,CAAC;AAChF,UAAI,OAAO,OAAO;AAAA,IACpB;AACA,QAAI,OAAO,UAAU,OAAW,KAAI,QAAQ,OAAO;AACnD,QAAI,OAAO,UAAU,OAAW,KAAI,QAAQ,OAAO,SAAS;AAC5D,QAAI,OAAO,gBAAgB,OAAW,KAAI,cAAc,OAAO,eAAe;AAE9E,UAAM,GAAG,MAAM;AAEf,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,IAAI;AAAA,QACR,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,MAChB;AAAA,MACA,QAAQ;AAAA,IACV,CAAC;AAED,WAAO,EAAE,OAAO,IAAI,GAAG;AAAA,EACzB;AAAA,EACA,UAAU,OAAO,EAAE,WAAW,IAAI,MAAM;AACtC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,SAAS,UAAU;AACzB,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,gBAAgB,MAAM,gBAAgB,IAAI,OAAO,EAAE;AACzD,UAAM,UACJ,iBAAiB,SACb;AAAA,MACE;AAAA,MACA;AAAA,MACA,CAAC,QAAQ,SAAS,SAAS,aAAa;AAAA,IAC1C,IACA,CAAC;AACP,WAAO;AAAA,MACL,aAAa,UAAU,+BAA+B,YAAY;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,gBAAgB;AAAA,MAChB,eAAe,iBAAiB;AAAA,MAChC;AAAA,MACA,SAAS;AAAA,QACP,MAAM;AAAA,UACJ;AAAA,UACA,OAAO,iBAAiB;AAAA,QAC1B;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAAmC,QAAQ;AAC3D,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,OAAQ;AACb,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,QAAI,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,GAAG,CAAC;AACzD,QAAI,CAAC,KAAK;AACR,YAAM,GAAG,OAAO,aAAa;AAAA,QAC3B,IAAI,OAAO;AAAA,QACX,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,QACjB,MAAM,OAAO;AAAA,QACb,OAAO,OAAO;AAAA,QACd,OAAO,OAAO;AAAA,QACd,aAAa,OAAO;AAAA,MACtB,CAAC;AACD,SAAG,QAAQ,GAAG;AAAA,IAChB,OAAO;AACL,UAAI,OAAO,OAAO;AAClB,UAAI,QAAQ,OAAO;AACnB,UAAI,QAAQ,OAAO;AACnB,UAAI,cAAc,OAAO;AAAA,IAC3B;AACA,UAAM,GAAG,MAAM;AAEf,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,wBAAwB;AAAA,MAC5B,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,IAAI;AAAA,QACR,gBAAgB,IAAI;AAAA,QACpB,UAAU,IAAI;AAAA,MAChB;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AAAA,EACH;AACF;AAEA,MAAM,mBAA2H;AAAA,EAC/H,IAAI;AAAA,EACJ,kCAAkC;AAAA,EAClC,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,KAAK,UAAU,OAAO,iBAAiB;AAC7C,UAAM,KAAK,IAAI,mBAAoB,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AACtF,UAAM,WAAW,MAAM,gBAAgB,IAAI,IAAI,gBAAgB,IAAI,GAAG,CAAC;AACvE,QAAI,CAAC,SAAU,QAAO,CAAC;AACvB,sBAAkB,KAAK,SAAS,QAAQ;AACxC,4BAAwB,KAAK,SAAS,cAAc;AACpD,UAAM,cAAc,MAAM,mBAAmB,IAAI,uBAAuB;AAAA,MACtE,KAAK;AAAA,MACL,UAAU,SAAS;AAAA,MACnB,gBAAgB,SAAS;AAAA,IAC3B,GAAG,CAAC,GAAG,QAAQ;AACf,aAAS,cAAc,YAAY,IAAI,CAAC,gBAAgB;AAAA,MACtD,IAAI,WAAW;AAAA,MACf,UAAU,WAAW,OAAO;AAAA,MAC5B,UAAU,WAAW;AAAA,MACrB,gBAAgB,WAAW;AAAA,MAC3B,WAAW,WAAW,UAAU,YAAY;AAAA,IAC9C,EAAE;AACF,WAAO,EAAE,QAAQ,SAAS;AAAA,EAC5B;AAAA,EACA,MAAM,QAAQ,OAAO,KAAK;AACxB,UAAM,KAAK,UAAU,OAAO,iBAAiB;AAC7C,UAAM,SAAS,gBAAgB,IAAI,GAAG;AACtC,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,QAAI,MAA0B;AAC9B,QAAI,cAAuC,CAAC;AAC5C,UAAM,gBAAgB,IAAI;AAAA,MACxB,YAAY;AACV,cAAM,MAAM,sBAAsB,IAAI,aAAa,OAAO,OAAO,EAAE,UAAU,SAAS,kBAAkB,GAAG,OAAO,KAAK;AACvH,YAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,0BAAkB,KAAK,IAAI,QAAQ;AACnC,gCAAwB,KAAK,IAAI,cAAc;AAC/C,cAAM,IAAI,2BAA2B,IAAI,KAAK;AAC9C,sBAAc,MAAM,mBAAmB,IAAI,GAAG;AAC9C,cAAM,GAAG,aAAa,uBAAuB,EAAE,KAAK,UAAU,IAAI,UAAU,gBAAgB,IAAI,eAAe,CAAC;AAChH,WAAG,OAAO,GAAG;AAAA,MACf;AAAA,IACF,GAAG,EAAE,aAAa,MAAM,OAAO,wBAAwB,CAAC;AACxD,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,UAAM,aAAa;AAEnB,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,WAAW;AAAA,QACf,gBAAgB,WAAW;AAAA,QAC3B,UAAU,WAAW;AAAA,MACvB;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AACD,UAAM,sBAAsB,KAAK,YAAY,aAAa,SAAS;AACnE,WAAO,EAAE,OAAO,WAAW,GAAG;AAAA,EAChC;AAAA,EACA,UAAU,OAAO,EAAE,UAAU,MAAM;AACjC,UAAM,SAAS,UAAU;AACzB,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,WAAO;AAAA,MACL,aAAa,UAAU,+BAA+B,YAAY;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,SAAS,EAAE,cAAc,CAAC,2BAA2B,oBAAoB,mBAAmB,EAAE;AAAA,MAC9F,gBAAgB;AAAA,MAChB,SAAS;AAAA,QACP,MAAM;AAAA,UACJ;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAAmC,QAAQ;AAC3D,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,OAAQ;AACb,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAClD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,QAAQ,EAAE,UAAU,OAAO,UAAU,gBAAgB,OAAO,eAAe;AACjF,UAAM,cAAc,MAAM,sBAAsB,IAAI,aAAa,EAAE,IAAI,OAAO,IAAI,GAAG,MAAM,GAAG,CAAC,GAAG,KAAK;AACvG,UAAM,MAAM,eAAe,GAAG,OAAO,aAAa,EAAE,IAAI,OAAO,IAAI,GAAG,OAAO,MAAM,OAAO,MAAM,OAAO,OAAO,MAAM,CAAC;AACrH,UAAM,sBAA+C,CAAC;AACtD,UAAM,gBAAgB,IAAI;AAAA,MACxB,MAAM;AACJ,YAAI,OAAO,OAAO;AAClB,YAAI,QAAQ,OAAO;AACnB,YAAI,QAAQ,OAAO;AACnB,YAAI,cAAc,OAAO;AACzB,WAAG,QAAQ,GAAG;AAAA,MAChB;AAAA,MACA,YAAY;AACV,cAAM,eAAe,OAAO,eAAe,CAAC,GAAG;AAAA,UAAO,CAAC,eACrD,WAAW,aAAa,OAAO,YAAY,WAAW,mBAAmB,OAAO;AAAA,QAClF;AACA,YAAI,CAAC,YAAY,OAAQ;AACzB,cAAM,YAAY,MAAM,mBAAmB,IAAI,gBAAgB;AAAA,UAC7D,IAAI,EAAE,KAAK,YAAY,IAAI,CAAC,eAAe,WAAW,QAAQ,EAAE;AAAA,UAChE,GAAG;AAAA,UACH,WAAW;AAAA,UACX,MAAM,EAAE,KAAK,CAAC,UAAU,SAAS,EAAE;AAAA,QACrC,GAAG,CAAC,GAAG,KAAK;AACZ,cAAM,sBAAsB,MAAM,mBAAmB,IAAI,GAAG;AAC5D,cAAM,oBAAoB,IAAI,IAAI,oBAAoB,IAAI,CAAC,eAAe,WAAW,OAAO,EAAE,CAAC;AAC/F,cAAM,gBAAgB,IAAI,IAAI,UAAU,IAAI,CAAC,aAAa,CAAC,SAAS,IAAI,QAAQ,CAAC,CAAC;AAClF,mBAAW,YAAY,aAAa;AAClC,gBAAM,SAAS,cAAc,IAAI,SAAS,QAAQ;AAClD,cAAI,CAAC,UAAU,kBAAkB,IAAI,OAAO,EAAE,EAAG;AACjD,gBAAM,aAAa,GAAG,OAAO,uBAAuB;AAAA,YAClD,IAAI,SAAS;AAAA,YACb;AAAA,YACA;AAAA,YACA,GAAG;AAAA,YACH,WAAW,IAAI,KAAK,SAAS,SAAS;AAAA,UACxC,CAAC;AACD,aAAG,QAAQ,UAAU;AACrB,8BAAoB,KAAK,UAAU;AACnC,4BAAkB,IAAI,OAAO,EAAE;AAAA,QACjC;AAAA,MACF;AAAA,IACF,GAAG,EAAE,aAAa,MAAM,OAAO,6BAA6B,CAAC;AAE7D,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,wBAAwB;AAAA,MAC5B,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO;AAAA,QACX,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,MACnB;AAAA,MACA,QAAQ;AAAA,MACR,SAAS;AAAA,IACX,CAAC;AACD,UAAM,sBAAsB,KAAK,KAAK,qBAAqB,SAAS;AAAA,EACtE;AACF;AAEA,MAAM,mBAAiF;AAAA,EACrF,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,oBAAoB,MAAM,QAAQ;AACjD,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AACjE,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,OAAO,UAAU,OAAO,UAAU,gBAAgB,OAAO,eAAe,CAAC;AAChI,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,UAAM,SAAS,MAAM,sBAAsB,IAAI,OAAO,UAAU,EAAE,UAAU,OAAO,UAAU,gBAAgB,OAAO,eAAe,GAAG,QAAW,oBAAoB;AACrK,oBAAgB,QAAQ,OAAO,gBAAgB,OAAO,QAAQ;AAC9D,UAAM,SAAS,MAAM,iBAAiB,IAAI,MAAM;AAC9C,QAAI,OAAO,SAAS,OAAO,KAAK,GAAG;AACjC,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,uBAAuB,CAAC;AAAA,IAChE;AAEA,UAAM,aAAa,GAAG,OAAO,uBAAuB;AAAA,MAClD;AAAA,MACA;AAAA,MACA,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,CAAC;AACD,OAAG,QAAQ,UAAU;AACrB,UAAM,GAAG,MAAM;AAEf,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO,WAAW,EAAE;AAAA,QACxB,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,MACnB;AAAA,IACF,CAAC;AAED,UAAM,mBAAmB,0BAA0B;AAAA,MACjD,IAAI,OAAO,WAAW,EAAE;AAAA,MACxB,OAAO,OAAO;AAAA,MACd,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,GAAG,EAAE,YAAY,KAAK,CAAC;AAEvB,WAAO,EAAE,cAAc,WAAW,GAAG;AAAA,EACvC;AAAA,EACA,UAAU,OAAO,EAAE,QAAQ,IAAI,MAAM;AACnC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,aAAa,MAAM;AAAA,MACvB;AAAA,MACA;AAAA,MACA,EAAE,IAAI,OAAO,aAAa;AAAA,MAC1B,EAAE,UAAU,CAAC,OAAO,QAAQ,EAAE;AAAA,MAC9B,EAAE,UAAU,IAAI,MAAM,YAAY,MAAM,gBAAgB,IAAI,0BAA0B,IAAI,MAAM,SAAS,KAAK;AAAA,IAChH;AACA,QAAI,CAAC,WAAY,QAAO;AACxB,UAAM,QAAQ,OAAO,WAAW,QAAQ,WAAW,WAAW,MAAM,WAAW,IAAI;AACnF,UAAM,WAAW,OAAO,WAAW,WAAW,WAAW,WAAW,SAAS,WAAW,OAAO;AAC/F,WAAO;AAAA,MACL,aAAa,UAAU,+BAA+B,YAAY;AAAA,MAClE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,WAAW;AAAA,MACrB,gBAAgB,WAAW;AAAA,MAC3B,SAAS;AAAA,QACP,MAAM;AAAA,UACJ,QAAQ;AAAA,YACN;AAAA,YACA;AAAA,YACA,UAAU,WAAW;AAAA,YACrB,gBAAgB,WAAW;AAAA,UAC7B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAA6C,QAAQ;AACrE,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,OAAQ;AACb,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,GAAG,aAAa,uBAAuB;AAAA,MAC3C,KAAK,OAAO;AAAA,MACZ,QAAQ,OAAO;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AAAA,EACH;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAA6C,QAAQ;AACrE,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,cAAc,KAAK,EAAE,OAAO,sDAAsD,CAAC;AAAA,IAC/F;AACA,UAAM,eAAe,UAAU,cAAc;AAC7C,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,MAAM,CAAC;AAC9D,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,UAAM,SAAS,MAAM,sBAAsB,IAAI,OAAO,UAAU,EAAE,UAAU,OAAO,UAAU,gBAAgB,OAAO,eAAe,GAAG,QAAW,oBAAoB;AACrK,oBAAgB,QAAQ,OAAO,gBAAgB,OAAO,QAAQ;AAE9D,QAAI,aAAa,MAAM,GAAG,QAAQ,uBAAuB;AAAA,MACvD;AAAA,MACA;AAAA,MACA,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AACD,QAAI,CAAC,YAAY;AACf,mBAAa,GAAG,OAAO,uBAAuB;AAAA,QAC5C,GAAI,eAAe,EAAE,IAAI,aAAa,IAAI,CAAC;AAAA,QAC3C;AAAA,QACA;AAAA,QACA,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,MACnB,CAAC;AACD,SAAG,QAAQ,UAAU;AACrB,YAAM,GAAG,MAAM;AAAA,IACjB;AAEA,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO,WAAW,EAAE;AAAA,QACxB,gBAAgB,OAAO;AAAA,QACvB,UAAU,OAAO;AAAA,MACnB;AAAA,IACF,CAAC;AAED,UAAM,mBAAmB,0BAA0B;AAAA,MACjD,IAAI,OAAO,WAAW,EAAE;AAAA,MACxB,OAAO,OAAO;AAAA,MACd,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,GAAG,EAAE,YAAY,KAAK,CAAC;AAEvB,WAAO,EAAE,cAAc,WAAW,GAAG;AAAA,EACvC;AACF;AAEA,MAAM,qBAA0F;AAAA,EAC9F,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,SAAS,oBAAoB,MAAM,QAAQ;AACjD,sBAAkB,KAAK,OAAO,QAAQ;AACtC,4BAAwB,KAAK,OAAO,cAAc;AAElD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,WAAW,MAAM,GAAG,QAAQ,uBAAuB;AAAA,MACvD,KAAK,OAAO;AAAA,MACZ,QAAQ,OAAO;AAAA,MACf,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AACD,QAAI,CAAC,SAAU,OAAM,SAAS,0BAA0B;AACxD,UAAM,GAAG,OAAO,QAAQ,EAAE,MAAM;AAEhC,UAAM,KAAM,IAAI,UAAU,QAAQ,YAAY;AAC9C,UAAM,oBAAoB;AAAA,MACxB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,aAAa;AAAA,QACX,IAAI,OAAO,SAAS,EAAE;AAAA,QACtB,gBAAgB,SAAS;AAAA,QACzB,UAAU,SAAS;AAAA,MACrB;AAAA,IACF,CAAC;AAED,UAAM,mBAAmB,yBAAyB;AAAA,MAChD,IAAI,OAAO,SAAS,EAAE;AAAA,MACtB,OAAO,OAAO;AAAA,MACd,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,GAAG,EAAE,YAAY,KAAK,CAAC;AAEvB,WAAO,EAAE,cAAc,SAAS,MAAM,KAAK;AAAA,EAC7C;AAAA,EACA,UAAU,OAAO,EAAE,WAAW,MAAM,MAAM;AACxC,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,UAAM,SAAS,oBAAoB,MAAM,KAAK;AAC9C,WAAO;AAAA,MACL,aAAa,UAAU,iCAAiC,cAAc;AAAA,MACtE,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,SAAS;AAAA,QACP,MAAM;AAAA,UACJ,QAAQ;AAAA,YACN,OAAO,OAAO;AAAA,YACd,UAAU,OAAO;AAAA,YACjB,UAAU,OAAO;AAAA,YACjB,gBAAgB,OAAO;AAAA,UACzB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,UAAU,mBAA6C,QAAQ;AACrE,UAAM,SAAS,SAAS;AACxB,QAAI,CAAC,OAAQ;AACb,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,MAAM,MAAM,GAAG,QAAQ,aAAa,EAAE,IAAI,OAAO,MAAM,CAAC;AAC9D,UAAM,SAAS,MAAM,sBAAsB,IAAI,OAAO,UAAU,EAAE,UAAU,OAAO,UAAU,gBAAgB,OAAO,eAAe,GAAG,QAAW,oBAAoB;AACrK,oBAAgB,QAAQ,OAAO,gBAAgB,OAAO,QAAQ;AAC9D,QAAI,CAAC,IAAK,OAAM,SAAS,eAAe;AACxC,UAAM,WAAW,MAAM,GAAG,QAAQ,uBAAuB;AAAA,MACvD;AAAA,MACA;AAAA,MACA,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,IACzB,CAAC;AACD,QAAI,CAAC,UAAU;AACb,YAAM,aAAa,GAAG,OAAO,uBAAuB;AAAA,QAClD;AAAA,QACA;AAAA,QACA,UAAU,OAAO;AAAA,QACjB,gBAAgB,OAAO;AAAA,MACzB,CAAC;AACD,SAAG,QAAQ,UAAU;AACrB,YAAM,GAAG,MAAM;AAAA,IACjB;AAAA,EACF;AACF;AAEA,gBAAgB,gBAAgB;AAChC,gBAAgB,gBAAgB;AAChC,gBAAgB,gBAAgB;AAChC,gBAAgB,gBAAgB;AAChC,gBAAgB,kBAAkB;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7671.1.9117e4b0b3",
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.7671.1.9117e4b0b3",
269
- "@open-mercato/shared": "0.10.1-develop.7671.1.9117e4b0b3",
270
- "@open-mercato/ui": "0.10.1-develop.7671.1.9117e4b0b3",
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.7671.1.9117e4b0b3",
276
- "@open-mercato/shared": "0.10.1-develop.7671.1.9117e4b0b3",
277
- "@open-mercato/ui": "0.10.1-develop.7671.1.9117e4b0b3",
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 { CustomerTag, CustomerTagAssignment } from '../data/entities'
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
- async function loadTagSnapshot(em: EntityManager, id: string): Promise<TagSnapshot | null> {
78
- const tag = await em.findOne(CustomerTag, { id })
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
- return snapshot ? { before: snapshot } : {}
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
- const tag = await em.findOne(CustomerTag, { id })
303
- if (!tag) throw notFound('Tag not found')
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.nativeDelete(CustomerTagAssignment, { tag })
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: tag,
426
+ entity: deletedTag,
318
427
  identifiers: {
319
- id: tag.id,
320
- organizationId: tag.organizationId,
321
- tenantId: tag.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
- return { tagId: tag.id }
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
- let tag = await em.findOne(CustomerTag, { id: before.id })
351
- if (!tag) {
352
- tag = em.create(CustomerTag, {
353
- id: before.id,
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
- em.persist(restoredTag)
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
- ], { transaction: true })
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: tag.id,
382
- organizationId: tag.organizationId,
383
- tenantId: tag.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