@open-mercato/core 0.9.1-develop.7532.1.8eac7e198f → 0.9.1-develop.7536.1.f9a5776a07

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/logout.js +8 -5
  3. package/dist/modules/auth/api/logout.js.map +2 -2
  4. package/dist/modules/currencies/backend/currencies/[id]/page.js +5 -2
  5. package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
  6. package/dist/modules/currencies/commands/currencies.js +399 -137
  7. package/dist/modules/currencies/commands/currencies.js.map +2 -2
  8. package/dist/modules/currencies/lib/seeds.js +2 -0
  9. package/dist/modules/currencies/lib/seeds.js.map +2 -2
  10. package/dist/modules/customers/commands/companies.js +10 -3
  11. package/dist/modules/customers/commands/companies.js.map +2 -2
  12. package/dist/modules/customers/commands/interactionRestoreDetails.js +75 -0
  13. package/dist/modules/customers/commands/interactionRestoreDetails.js.map +7 -0
  14. package/dist/modules/customers/commands/people.js +10 -3
  15. package/dist/modules/customers/commands/people.js.map +2 -2
  16. package/package.json +7 -7
  17. package/src/modules/auth/api/logout.ts +9 -5
  18. package/src/modules/currencies/backend/currencies/[id]/page.tsx +5 -1
  19. package/src/modules/currencies/commands/currencies.ts +475 -168
  20. package/src/modules/currencies/i18n/de.json +4 -0
  21. package/src/modules/currencies/i18n/en.json +4 -0
  22. package/src/modules/currencies/i18n/es.json +4 -0
  23. package/src/modules/currencies/i18n/ko.json +4 -0
  24. package/src/modules/currencies/i18n/pl.json +4 -0
  25. package/src/modules/currencies/lib/seeds.ts +2 -0
  26. package/src/modules/customers/commands/companies.ts +16 -4
  27. package/src/modules/customers/commands/interactionRestoreDetails.ts +110 -0
  28. package/src/modules/customers/commands/people.ts +16 -4
@@ -6,6 +6,9 @@
6
6
  "currencies.create.title": "Währung erstellen",
7
7
  "currencies.currencies.update": "Währung aktualisieren",
8
8
  "currencies.edit.title": "Währung bearbeiten",
9
+ "currencies.errors.baseCurrencyUnset": "Wählen Sie stattdessen eine andere Währung als Basiswährung, anstatt sie zu entfernen.",
10
+ "currencies.errors.undoHasRates": "Diese Änderung kann nicht rückgängig gemacht werden, da inzwischen Wechselkurse diese Währung verwenden. Löschen oder deaktivieren Sie diese Wechselkurse zuerst.",
11
+ "currencies.errors.undoWouldClearBase": "Diese Änderung kann nicht rückgängig gemacht werden, da die Organisation sonst keine Basiswährung hätte.",
9
12
  "currencies.exchange_rates.update": "Wechselkurs aktualisieren",
10
13
  "currencies.fetch.description": "Verwalten Sie den automatischen Abruf von Währungskursen von externen Anbietern",
11
14
  "currencies.fetch.disabled": "Deaktiviert",
@@ -54,6 +57,7 @@
54
57
  "currencies.form.field.decimalSeparator": "Dezimaltrennzeichen",
55
58
  "currencies.form.field.isActive": "Aktiv",
56
59
  "currencies.form.field.isBase": "Basiswährung",
60
+ "currencies.form.field.isBaseLocked": "Dies ist die Basiswährung. Um sie zu ändern, legen Sie eine andere Währung als Basis fest.",
57
61
  "currencies.form.field.name": "Währungsname",
58
62
  "currencies.form.field.namePlaceholder": "US-Dollar",
59
63
  "currencies.form.field.symbol": "Symbol",
@@ -6,6 +6,9 @@
6
6
  "currencies.create.title": "Create Currency",
7
7
  "currencies.currencies.update": "Update currency",
8
8
  "currencies.edit.title": "Edit Currency",
9
+ "currencies.errors.baseCurrencyUnset": "Choose another currency as the base instead of clearing it.",
10
+ "currencies.errors.undoHasRates": "This change can't be undone because exchange rates now use this currency. Delete or deactivate those exchange rates first.",
11
+ "currencies.errors.undoWouldClearBase": "This change can't be undone because it would leave the organization without a base currency.",
9
12
  "currencies.exchange_rates.update": "Update exchange rate",
10
13
  "currencies.fetch.description": "Manage automatic currency exchange rate fetching from external providers",
11
14
  "currencies.fetch.disabled": "Disabled",
@@ -54,6 +57,7 @@
54
57
  "currencies.form.field.decimalSeparator": "Decimal Separator",
55
58
  "currencies.form.field.isActive": "Active",
56
59
  "currencies.form.field.isBase": "Base Currency",
60
+ "currencies.form.field.isBaseLocked": "This is the base currency. To change it, set another currency as the base.",
57
61
  "currencies.form.field.name": "Currency Name",
58
62
  "currencies.form.field.namePlaceholder": "US Dollar",
59
63
  "currencies.form.field.symbol": "Symbol",
@@ -6,6 +6,9 @@
6
6
  "currencies.create.title": "Crear Moneda",
7
7
  "currencies.currencies.update": "Actualizar moneda",
8
8
  "currencies.edit.title": "Editar Moneda",
9
+ "currencies.errors.baseCurrencyUnset": "Elija otra moneda como base en lugar de quitarla.",
10
+ "currencies.errors.undoHasRates": "Este cambio no se puede deshacer porque ahora hay tipos de cambio que usan esta moneda. Elimine o desactive primero esos tipos de cambio.",
11
+ "currencies.errors.undoWouldClearBase": "Este cambio no se puede deshacer porque dejaría a la organización sin moneda base.",
9
12
  "currencies.exchange_rates.update": "Actualizar tipo de cambio",
10
13
  "currencies.fetch.description": "Gestionar la obtención automática de tipos de cambio desde proveedores externos",
11
14
  "currencies.fetch.disabled": "Deshabilitado",
@@ -54,6 +57,7 @@
54
57
  "currencies.form.field.decimalSeparator": "Separador Decimal",
55
58
  "currencies.form.field.isActive": "Activa",
56
59
  "currencies.form.field.isBase": "Moneda Base",
60
+ "currencies.form.field.isBaseLocked": "Esta es la moneda base. Para cambiarla, establezca otra moneda como base.",
57
61
  "currencies.form.field.name": "Nombre de Moneda",
58
62
  "currencies.form.field.namePlaceholder": "Dólar estadounidense",
59
63
  "currencies.form.field.symbol": "Símbolo",
@@ -6,6 +6,9 @@
6
6
  "currencies.create.title": "통화 생성",
7
7
  "currencies.currencies.update": "통화 수정",
8
8
  "currencies.edit.title": "통화 편집",
9
+ "currencies.errors.baseCurrencyUnset": "기준 통화를 해제하는 대신 다른 통화를 기준 통화로 선택하세요.",
10
+ "currencies.errors.undoHasRates": "이제 이 통화를 사용하는 환율이 있어 이 변경을 취소할 수 없습니다. 먼저 해당 환율을 삭제하거나 비활성화하세요.",
11
+ "currencies.errors.undoWouldClearBase": "이 변경을 취소하면 조직에 기준 통화가 없게 되므로 취소할 수 없습니다.",
9
12
  "currencies.exchange_rates.update": "환율 수정",
10
13
  "currencies.fetch.description": "외부 제공업체로부터 자동 환율 가져오기를 관리합니다",
11
14
  "currencies.fetch.disabled": "비활성화됨",
@@ -54,6 +57,7 @@
54
57
  "currencies.form.field.decimalSeparator": "소수 구분 기호",
55
58
  "currencies.form.field.isActive": "활성",
56
59
  "currencies.form.field.isBase": "기준 통화",
60
+ "currencies.form.field.isBaseLocked": "기준 통화입니다. 변경하려면 다른 통화를 기준 통화로 설정하세요.",
57
61
  "currencies.form.field.name": "통화 이름",
58
62
  "currencies.form.field.namePlaceholder": "미국 달러",
59
63
  "currencies.form.field.symbol": "기호",
@@ -6,6 +6,9 @@
6
6
  "currencies.create.title": "Utwórz Walutę",
7
7
  "currencies.currencies.update": "Aktualizuj walutę",
8
8
  "currencies.edit.title": "Edytuj Walutę",
9
+ "currencies.errors.baseCurrencyUnset": "Zamiast odznaczać walutę bazową, wybierz inną walutę jako bazową.",
10
+ "currencies.errors.undoHasRates": "Tej zmiany nie można cofnąć, ponieważ kursy wymiany korzystają teraz z tej waluty. Najpierw usuń lub dezaktywuj te kursy wymiany.",
11
+ "currencies.errors.undoWouldClearBase": "Tej zmiany nie można cofnąć, ponieważ organizacja zostałaby bez waluty bazowej.",
9
12
  "currencies.exchange_rates.update": "Aktualizuj kurs wymiany",
10
13
  "currencies.fetch.description": "Zarządzaj automatycznym pobieraniem kursów walut od zewnętrznych dostawców",
11
14
  "currencies.fetch.disabled": "Wyłączony",
@@ -54,6 +57,7 @@
54
57
  "currencies.form.field.decimalSeparator": "Separator Dziesiętny",
55
58
  "currencies.form.field.isActive": "Aktywna",
56
59
  "currencies.form.field.isBase": "Waluta Bazowa",
60
+ "currencies.form.field.isBaseLocked": "To jest waluta bazowa. Aby ją zmienić, ustaw inną walutę jako bazową.",
57
61
  "currencies.form.field.name": "Nazwa Waluty",
58
62
  "currencies.form.field.namePlaceholder": "Dolar amerykański",
59
63
  "currencies.form.field.symbol": "Symbol",
@@ -126,6 +126,7 @@ export async function seedExampleCurrencies(em: EntityManager, scope: CurrencySe
126
126
  for (const entry of existingEntries) {
127
127
  existingMap.set(entry.code, entry)
128
128
  }
129
+ const hasLiveBase = existingEntries.some((entry) => entry.isBase && !entry.deletedAt)
129
130
 
130
131
  let touched = false
131
132
  for (const curr of SEED_CURRENCIES) {
@@ -143,6 +144,7 @@ export async function seedExampleCurrencies(em: EntityManager, scope: CurrencySe
143
144
  tenantId: scope.tenantId,
144
145
  organizationId: scope.organizationId,
145
146
  ...curr,
147
+ isBase: curr.isBase && !hasLiveBase,
146
148
  createdAt: new Date(),
147
149
  updatedAt: new Date(),
148
150
  })
@@ -26,6 +26,12 @@ import {
26
26
  CustomerPersonProfile,
27
27
  CustomerTagAssignment,
28
28
  } from '../data/entities'
29
+ import {
30
+ captureInteractionRestoreDetails,
31
+ omitInteractionRestoreDetails,
32
+ restoreInteractionRestoreDetails,
33
+ type InteractionRestoreDetails,
34
+ } from './interactionRestoreDetails'
29
35
  import {
30
36
  companyCreateSchema,
31
37
  companyUpdateSchema,
@@ -189,7 +195,7 @@ type CompanyInteractionSnapshot = {
189
195
  updatedAt: Date
190
196
  deletedAt: Date | null
191
197
  custom?: Record<string, unknown>
192
- }
198
+ } & InteractionRestoreDetails
193
199
 
194
200
  type CompanySnapshot = {
195
201
  entity: {
@@ -246,7 +252,11 @@ type CompanyUndoPayload = {
246
252
  after?: CompanySnapshot | null
247
253
  }
248
254
 
249
- async function loadCompanySnapshot(em: EntityManager, id: string): Promise<CompanySnapshot | null> {
255
+ async function loadCompanySnapshot(
256
+ em: EntityManager,
257
+ id: string,
258
+ options: { includeInteractionRestoreDetails?: boolean } = {},
259
+ ): Promise<CompanySnapshot | null> {
250
260
  const entity = await em.findOne(CustomerEntity, { id, deletedAt: null })
251
261
  if (!entity || entity.kind !== 'company') return null
252
262
  const profile = await em.findOne(CustomerCompanyProfile, { entity })
@@ -408,6 +418,7 @@ async function loadCompanySnapshot(em: EntityManager, id: string): Promise<Compa
408
418
  createdAt: interaction.createdAt,
409
419
  updatedAt: interaction.updatedAt,
410
420
  deletedAt: interaction.deletedAt ?? null,
421
+ ...(options.includeInteractionRestoreDetails ? captureInteractionRestoreDetails(interaction) : {}),
411
422
  custom: await loadCustomFieldSnapshot(em, {
412
423
  entityId: INTERACTION_ENTITY_ID,
413
424
  recordId: interaction.id,
@@ -998,7 +1009,7 @@ const deleteCompanyCommand: CommandHandler<{ body?: Record<string, unknown>; que
998
1009
  async prepare(input, ctx) {
999
1010
  const id = requireId(input, 'Company id required')
1000
1011
  const em = (ctx.container.resolve('em') as EntityManager)
1001
- const snapshot = await loadCompanySnapshot(em, id)
1012
+ const snapshot = await loadCompanySnapshot(em, id, { includeInteractionRestoreDetails: true })
1002
1013
  return snapshot ? { before: snapshot } : {}
1003
1014
  },
1004
1015
  async execute(input, ctx) {
@@ -1182,7 +1193,7 @@ const deleteCompanyCommand: CommandHandler<{ body?: Record<string, unknown>; que
1182
1193
  resourceId: before.entity.id,
1183
1194
  tenantId: before.entity.tenantId,
1184
1195
  organizationId: before.entity.organizationId,
1185
- snapshotBefore: before,
1196
+ snapshotBefore: omitInteractionRestoreDetails(before),
1186
1197
  payload: {
1187
1198
  undo: {
1188
1199
  before,
@@ -1421,6 +1432,7 @@ const deleteCompanyCommand: CommandHandler<{ body?: Record<string, unknown>; que
1421
1432
  source: interaction.source,
1422
1433
  appearanceIcon: interaction.appearanceIcon,
1423
1434
  appearanceColor: interaction.appearanceColor,
1435
+ ...restoreInteractionRestoreDetails(interaction),
1424
1436
  createdAt: interaction.createdAt,
1425
1437
  updatedAt: interaction.updatedAt,
1426
1438
  deletedAt: interaction.deletedAt,
@@ -0,0 +1,110 @@
1
+ import { reviveSnapshotDates } from '@open-mercato/shared/lib/commands/undo'
2
+ import type { CustomerInteraction } from '../data/entities'
3
+
4
+ /**
5
+ * Interaction columns a Person/Company delete snapshot must carry so that undo can recreate each interaction
6
+ * exactly as it was. Every key is optional because delete logs written before these columns were captured
7
+ * do not contain them.
8
+ */
9
+ export type InteractionRestoreDetails = {
10
+ externalMessageId?: string | null
11
+ channelProviderKey?: string | null
12
+ channelId?: string | null
13
+ durationMinutes?: number | null
14
+ location?: string | null
15
+ allDay?: boolean | null
16
+ recurrenceRule?: string | null
17
+ recurrenceEnd?: Date | string | null
18
+ participants?: CustomerInteraction['participants']
19
+ reminderMinutes?: number | null
20
+ visibility?: string | null
21
+ linkedEntities?: CustomerInteraction['linkedEntities']
22
+ guestPermissions?: CustomerInteraction['guestPermissions']
23
+ pinned?: boolean
24
+ }
25
+
26
+ const INTERACTION_RESTORE_DETAIL_KEYS = [
27
+ 'externalMessageId',
28
+ 'channelProviderKey',
29
+ 'channelId',
30
+ 'durationMinutes',
31
+ 'location',
32
+ 'allDay',
33
+ 'recurrenceRule',
34
+ 'recurrenceEnd',
35
+ 'participants',
36
+ 'reminderMinutes',
37
+ 'visibility',
38
+ 'linkedEntities',
39
+ 'guestPermissions',
40
+ 'pinned',
41
+ ] as const satisfies ReadonlyArray<keyof InteractionRestoreDetails>
42
+
43
+ export function captureInteractionRestoreDetails(interaction: CustomerInteraction): Required<InteractionRestoreDetails> {
44
+ return {
45
+ externalMessageId: interaction.externalMessageId ?? null,
46
+ channelProviderKey: interaction.channelProviderKey ?? null,
47
+ channelId: interaction.channelId ?? null,
48
+ durationMinutes: interaction.durationMinutes ?? null,
49
+ location: interaction.location ?? null,
50
+ allDay: interaction.allDay ?? null,
51
+ recurrenceRule: interaction.recurrenceRule ?? null,
52
+ recurrenceEnd: interaction.recurrenceEnd ?? null,
53
+ participants: interaction.participants ?? null,
54
+ reminderMinutes: interaction.reminderMinutes ?? null,
55
+ visibility: interaction.visibility ?? null,
56
+ linkedEntities: interaction.linkedEntities ?? null,
57
+ guestPermissions: interaction.guestPermissions ?? null,
58
+ pinned: interaction.pinned === true,
59
+ }
60
+ }
61
+
62
+ /**
63
+ * A delete log written before visibility was captured cannot tell whether an email was private. An email that
64
+ * has an author is restored private: its author still reads it and can share it again. An email without an
65
+ * author keeps the previous behaviour, because a private row without an author is readable and changeable by
66
+ * nobody.
67
+ */
68
+ function resolveRestoredVisibility(snapshot: { interactionType: string; authorUserId: string | null } & InteractionRestoreDetails): string | null {
69
+ if (Object.prototype.hasOwnProperty.call(snapshot, 'visibility')) return snapshot.visibility ?? null
70
+ return snapshot.interactionType === 'email' && snapshot.authorUserId ? 'private' : null
71
+ }
72
+
73
+ export function restoreInteractionRestoreDetails(
74
+ snapshot: { interactionType: string; authorUserId: string | null } & InteractionRestoreDetails,
75
+ ) {
76
+ const { recurrenceEnd } = reviveSnapshotDates({ recurrenceEnd: snapshot.recurrenceEnd ?? null }, ['recurrenceEnd'])
77
+ return {
78
+ externalMessageId: snapshot.externalMessageId ?? null,
79
+ channelProviderKey: snapshot.channelProviderKey ?? null,
80
+ channelId: snapshot.channelId ?? null,
81
+ durationMinutes: snapshot.durationMinutes ?? null,
82
+ location: snapshot.location ?? null,
83
+ allDay: snapshot.allDay ?? null,
84
+ recurrenceRule: snapshot.recurrenceRule ?? null,
85
+ recurrenceEnd: recurrenceEnd as Date | null,
86
+ participants: snapshot.participants ?? null,
87
+ reminderMinutes: snapshot.reminderMinutes ?? null,
88
+ visibility: resolveRestoredVisibility(snapshot),
89
+ linkedEntities: snapshot.linkedEntities ?? null,
90
+ guestPermissions: snapshot.guestPermissions ?? null,
91
+ pinned: snapshot.pinned === true,
92
+ }
93
+ }
94
+
95
+ /**
96
+ * The delete log's `snapshotBefore` is what the audit log shows; the restore details stay in the undo payload only.
97
+ */
98
+ export function omitInteractionRestoreDetails<TSnapshot extends { interactions?: ReadonlyArray<InteractionRestoreDetails> }>(
99
+ snapshot: TSnapshot,
100
+ ): TSnapshot {
101
+ if (!Array.isArray(snapshot.interactions)) return snapshot
102
+ return {
103
+ ...snapshot,
104
+ interactions: snapshot.interactions.map((interaction) => {
105
+ const visible: Record<string, unknown> = { ...interaction }
106
+ for (const key of INTERACTION_RESTORE_DETAIL_KEYS) delete visible[key]
107
+ return visible
108
+ }),
109
+ }
110
+ }
@@ -24,6 +24,12 @@ import {
24
24
  CustomerPersonProfile,
25
25
  CustomerTagAssignment,
26
26
  } from '../data/entities'
27
+ import {
28
+ captureInteractionRestoreDetails,
29
+ omitInteractionRestoreDetails,
30
+ restoreInteractionRestoreDetails,
31
+ type InteractionRestoreDetails,
32
+ } from './interactionRestoreDetails'
27
33
  import { resolvePersonCustomFieldRouting, CUSTOMER_ENTITY_ID, PERSON_ENTITY_ID } from '../lib/customFieldRouting'
28
34
  import { CustomFieldValue } from '@open-mercato/core/modules/entities/data/entities'
29
35
  import {
@@ -133,7 +139,7 @@ type PersonInteractionSnapshot = {
133
139
  updatedAt: Date
134
140
  deletedAt: Date | null
135
141
  custom?: Record<string, unknown>
136
- }
142
+ } & InteractionRestoreDetails
137
143
 
138
144
  type PersonSnapshot = {
139
145
  entity: {
@@ -380,7 +386,11 @@ function serializePersonSnapshot(
380
386
  }
381
387
  }
382
388
 
383
- async function loadPersonSnapshot(em: EntityManager, entityId: string): Promise<PersonSnapshot | null> {
389
+ async function loadPersonSnapshot(
390
+ em: EntityManager,
391
+ entityId: string,
392
+ options: { includeInteractionRestoreDetails?: boolean } = {},
393
+ ): Promise<PersonSnapshot | null> {
384
394
  const entity = await em.findOne(CustomerEntity, { id: entityId, deletedAt: null })
385
395
  if (!entity || entity.kind !== 'person') return null
386
396
  const profile = await findOneWithDecryption(
@@ -455,6 +465,7 @@ async function loadPersonSnapshot(em: EntityManager, entityId: string): Promise<
455
465
  createdAt: interaction.createdAt,
456
466
  updatedAt: interaction.updatedAt,
457
467
  deletedAt: interaction.deletedAt ?? null,
468
+ ...(options.includeInteractionRestoreDetails ? captureInteractionRestoreDetails(interaction) : {}),
458
469
  custom: await loadCustomFieldSnapshot(em, {
459
470
  entityId: INTERACTION_ENTITY_ID,
460
471
  recordId: interaction.id,
@@ -1231,7 +1242,7 @@ const deletePersonCommand: CommandHandler<{ body?: Record<string, unknown>; quer
1231
1242
  async prepare(input, ctx) {
1232
1243
  const id = requireId(input, 'Person id required')
1233
1244
  const em = (ctx.container.resolve('em') as EntityManager).fork()
1234
- const snapshot = await loadPersonSnapshot(em, id)
1245
+ const snapshot = await loadPersonSnapshot(em, id, { includeInteractionRestoreDetails: true })
1235
1246
  return snapshot ? { before: snapshot } : {}
1236
1247
  },
1237
1248
  async execute(input, ctx) {
@@ -1355,7 +1366,7 @@ const deletePersonCommand: CommandHandler<{ body?: Record<string, unknown>; quer
1355
1366
  resourceId: before.entity.id,
1356
1367
  tenantId: before.entity.tenantId,
1357
1368
  organizationId: before.entity.organizationId,
1358
- snapshotBefore: before,
1369
+ snapshotBefore: omitInteractionRestoreDetails(before),
1359
1370
  payload: {
1360
1371
  undo: {
1361
1372
  before,
@@ -1646,6 +1657,7 @@ const deletePersonCommand: CommandHandler<{ body?: Record<string, unknown>; quer
1646
1657
  source: interaction.source,
1647
1658
  appearanceIcon: interaction.appearanceIcon,
1648
1659
  appearanceColor: interaction.appearanceColor,
1660
+ ...restoreInteractionRestoreDetails(interaction),
1649
1661
  createdAt: interaction.createdAt,
1650
1662
  updatedAt: interaction.updatedAt,
1651
1663
  deletedAt: interaction.deletedAt,