@open-mercato/core 0.5.1-develop.3045.b4b3320cc2 → 0.6.0

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 (106) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +21 -1
  3. package/dist/modules/api_keys/api/keys/route.js +9 -0
  4. package/dist/modules/api_keys/api/keys/route.js.map +2 -2
  5. package/dist/modules/audit_logs/services/accessLogService.js +13 -0
  6. package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
  7. package/dist/modules/audit_logs/services/actionLogService.js +6 -5
  8. package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
  9. package/dist/modules/auth/api/roles/acl/route.js +27 -37
  10. package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
  11. package/dist/modules/auth/api/users/route.js +41 -28
  12. package/dist/modules/auth/api/users/route.js.map +3 -3
  13. package/dist/modules/auth/lib/grantChecks.js +160 -0
  14. package/dist/modules/auth/lib/grantChecks.js.map +7 -0
  15. package/dist/modules/configs/cli.js +11 -0
  16. package/dist/modules/configs/cli.js.map +2 -2
  17. package/dist/modules/configs/lib/touchGeneratedBarrels.js +46 -0
  18. package/dist/modules/configs/lib/touchGeneratedBarrels.js.map +7 -0
  19. package/dist/modules/customers/api/activities/route.js +1 -52
  20. package/dist/modules/customers/api/activities/route.js.map +2 -2
  21. package/dist/modules/customers/api/interactions/counts/route.js +2 -1
  22. package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
  23. package/dist/modules/customers/api/interactions/route.js +21 -1
  24. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  25. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -3
  26. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  27. package/dist/modules/customers/backend/customers/deals/[id]/page.js +5 -1
  28. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  29. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -3
  30. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  31. package/dist/modules/customers/components/detail/ActivitiesCard.js +62 -6
  32. package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
  33. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +21 -6
  34. package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
  35. package/dist/modules/customers/components/detail/ActivitiesSection.js +37 -5
  36. package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
  37. package/dist/modules/customers/components/detail/ActivityCard.js +69 -17
  38. package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
  39. package/dist/modules/customers/components/detail/ActivityHistorySection.js +94 -34
  40. package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
  41. package/dist/modules/customers/components/detail/ActivityLogTab.js +3 -1
  42. package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
  43. package/dist/modules/customers/components/detail/ActivityTimeline.js +41 -8
  44. package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
  45. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +19 -6
  46. package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
  47. package/dist/modules/customers/components/detail/ActivityTypeSelector.js +4 -3
  48. package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +2 -2
  49. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +80 -12
  50. package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
  51. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +65 -10
  52. package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
  53. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +10 -5
  54. package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
  55. package/dist/modules/customers/data/validators.js +74 -2
  56. package/dist/modules/customers/data/validators.js.map +2 -2
  57. package/dist/modules/customers/lib/legacyActivityBridge.js +61 -0
  58. package/dist/modules/customers/lib/legacyActivityBridge.js.map +7 -0
  59. package/dist/modules/integrations/data/validators.js +2 -2
  60. package/dist/modules/integrations/data/validators.js.map +2 -2
  61. package/dist/modules/integrations/lib/credentials-service.js +12 -1
  62. package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
  63. package/dist/modules/messages/commands/actions.js +29 -14
  64. package/dist/modules/messages/commands/actions.js.map +2 -2
  65. package/dist/modules/messages/lib/actions.js +24 -4
  66. package/dist/modules/messages/lib/actions.js.map +2 -2
  67. package/dist/modules/sales/api/documents/factory.js +49 -36
  68. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  69. package/package.json +9 -10
  70. package/src/modules/api_keys/api/keys/route.ts +9 -0
  71. package/src/modules/audit_logs/services/accessLogService.ts +20 -0
  72. package/src/modules/audit_logs/services/actionLogService.ts +13 -5
  73. package/src/modules/auth/api/roles/acl/route.ts +32 -46
  74. package/src/modules/auth/api/users/route.ts +48 -33
  75. package/src/modules/auth/lib/grantChecks.ts +234 -0
  76. package/src/modules/configs/cli.ts +11 -0
  77. package/src/modules/configs/lib/touchGeneratedBarrels.ts +61 -0
  78. package/src/modules/customers/api/activities/route.ts +1 -76
  79. package/src/modules/customers/api/interactions/counts/route.ts +2 -1
  80. package/src/modules/customers/api/interactions/route.ts +28 -1
  81. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +13 -3
  82. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +14 -2
  83. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -3
  84. package/src/modules/customers/components/detail/ActivitiesCard.tsx +92 -5
  85. package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +38 -6
  86. package/src/modules/customers/components/detail/ActivitiesSection.tsx +37 -3
  87. package/src/modules/customers/components/detail/ActivityCard.tsx +79 -14
  88. package/src/modules/customers/components/detail/ActivityHistorySection.tsx +102 -33
  89. package/src/modules/customers/components/detail/ActivityLogTab.tsx +7 -1
  90. package/src/modules/customers/components/detail/ActivityTimeline.tsx +39 -5
  91. package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +29 -7
  92. package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +3 -2
  93. package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +96 -13
  94. package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +50 -4
  95. package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +21 -5
  96. package/src/modules/customers/data/validators.ts +85 -2
  97. package/src/modules/customers/i18n/de.json +11 -0
  98. package/src/modules/customers/i18n/en.json +11 -0
  99. package/src/modules/customers/i18n/es.json +11 -0
  100. package/src/modules/customers/i18n/pl.json +11 -0
  101. package/src/modules/customers/lib/legacyActivityBridge.ts +106 -0
  102. package/src/modules/integrations/data/validators.ts +8 -6
  103. package/src/modules/integrations/lib/credentials-service.ts +15 -1
  104. package/src/modules/messages/commands/actions.ts +28 -13
  105. package/src/modules/messages/lib/actions.ts +34 -3
  106. package/src/modules/sales/api/documents/factory.ts +55 -38
@@ -4,6 +4,9 @@
4
4
  "audit_logs.resource_kind.customers.comment": "Komentarz",
5
5
  "audit_logs.resource_kind.customers.todoLink": "Zadanie",
6
6
  "backend.nav.configuration": "Konfiguracja",
7
+ "customers.activities.actions.markDone": "Oznacz jako wykonane",
8
+ "customers.activities.actions.markDoneError": "Nie udało się oznaczyć aktywności jako wykonanej",
9
+ "customers.activities.actions.markDoneSuccess": "Aktywność oznaczona jako wykonana",
7
10
  "customers.activities.add.call": "Zarejestruj rozmowę",
8
11
  "customers.activities.add.email": "Napisz e-mail",
9
12
  "customers.activities.add.meeting": "Nowe spotkanie",
@@ -24,6 +27,10 @@
24
27
  "customers.activities.card.empty": "Nic nie zaplanowano na ten dzień.",
25
28
  "customers.activities.card.overdue": "{count} zaległych",
26
29
  "customers.activities.card.title": "Aktywności",
30
+ "customers.activities.errors.dateRequired": "Data jest wymagana",
31
+ "customers.activities.errors.phoneInvalid": "Wprowadź prawidłowy numer telefonu z numerem kierunkowym kraju (np. +48 123 456 789)",
32
+ "customers.activities.errors.phoneRequired": "Numer telefonu jest wymagany dla aktywności typu Połączenie",
33
+ "customers.activities.errors.timeRequired": "Godzina jest wymagana",
27
34
  "customers.activities.filters.clearAll": "Clear filters",
28
35
  "customers.activities.filters.dateRange": "Date range",
29
36
  "customers.activities.loadFailed": "Nie udało się załadować aktywności.",
@@ -52,6 +59,7 @@
52
59
  "customers.activityComposer.types.email": "E-mail",
53
60
  "customers.activityComposer.types.meeting": "Spotkanie",
54
61
  "customers.activityComposer.types.note": "Notatka",
62
+ "customers.activityComposer.types.task": "Zadanie",
55
63
  "customers.activityComposer.validation.descriptionRequired": "Opis jest wymagany",
56
64
  "customers.activityComposer.validation.typeRequired": "Select an activity type",
57
65
  "customers.activityComposer.weekPreviewTitle": "Ten tydzień",
@@ -59,6 +67,8 @@
59
67
  "customers.activityLog.direction.with": "z",
60
68
  "customers.activityLog.emptyDescription": "Poszerz zakres dat albo usuń część filtrów.",
61
69
  "customers.activityLog.error": "Nie udało się wczytać historii aktywności",
70
+ "customers.activityLog.filters.dateRangeLabel": "Filtruj według zakresu dat",
71
+ "customers.activityLog.filters.sortLabel": "Sortuj aktywności",
62
72
  "customers.activityLog.searchPlaceholder": "Szukaj po tytule, notatce lub autorze",
63
73
  "customers.activityLog.sort.recent": "Sortuj: najnowsze",
64
74
  "customers.activityLog.sort.titleAsc": "Sortuj: nazwa A-Z",
@@ -2022,6 +2032,7 @@
2022
2032
  "customers.timeline.filter.from": "Od daty",
2023
2033
  "customers.timeline.filter.meeting": "Spotkanie",
2024
2034
  "customers.timeline.filter.note": "Notatka",
2035
+ "customers.timeline.filter.task": "Zadanie",
2025
2036
  "customers.timeline.filter.to": "Do daty",
2026
2037
  "customers.timeline.history.filtered": "filtered: {{types}} · {{count}} wyników",
2027
2038
  "customers.timeline.history.searchAriaLabel": "Szukaj w historii interakcji",
@@ -0,0 +1,106 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql'
2
+ import type { CommandBus } from '@open-mercato/shared/lib/commands'
3
+ import { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'
4
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import { CustomerActivity, CustomerInteraction } from '../data/entities'
6
+ import { CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE } from './interactionCompatibility'
7
+
8
+ type CommandContext = Parameters<CommandBus['execute']>[1]['ctx']
9
+
10
+ async function loadLegacyActivityCustomValues(
11
+ em: EntityManager,
12
+ activity: CustomerActivity,
13
+ ): Promise<Record<string, unknown> | null> {
14
+ const values = await loadCustomFieldValues({
15
+ em,
16
+ entityId: 'customers:customer_activity',
17
+ recordIds: [activity.id],
18
+ tenantIdByRecord: { [activity.id]: activity.tenantId },
19
+ organizationIdByRecord: { [activity.id]: activity.organizationId },
20
+ tenantFallbacks: [activity.tenantId],
21
+ })
22
+ return values[activity.id] ?? null
23
+ }
24
+
25
+ /**
26
+ * If the canonical `customer_interactions` row for `activity` does not yet
27
+ * exist, create it via the `customers.interactions.create` command using the
28
+ * legacy activity's primary key. Returns the canonical id (always equal to
29
+ * `activity.id` in this scheme).
30
+ *
31
+ * Mirrors the bridge in /api/customers/activities so the dialog editing flow
32
+ * can edit historical activities that still live only in `customer_activities`
33
+ * (root cause of #1807 PUT 404 "Interaction not found").
34
+ */
35
+ export async function ensureCanonicalActivityBridge(
36
+ em: EntityManager,
37
+ commandBus: CommandBus,
38
+ commandContext: CommandContext,
39
+ activity: CustomerActivity,
40
+ ): Promise<string> {
41
+ const existing = await em.findOne(CustomerInteraction, { id: activity.id, tenantId: activity.tenantId })
42
+ if (existing) return existing.id
43
+
44
+ const entityId = typeof activity.entity === 'string' ? activity.entity : activity.entity.id
45
+ const dealId = activity.deal
46
+ ? (typeof activity.deal === 'string' ? activity.deal : activity.deal.id)
47
+ : null
48
+ const customValues = await loadLegacyActivityCustomValues(em, activity)
49
+
50
+ await commandBus.execute('customers.interactions.create', {
51
+ input: {
52
+ id: activity.id,
53
+ tenantId: activity.tenantId,
54
+ organizationId: activity.organizationId,
55
+ entityId,
56
+ interactionType: activity.activityType,
57
+ title: activity.subject ?? null,
58
+ body: activity.body ?? null,
59
+ occurredAt: activity.occurredAt ?? null,
60
+ status: activity.occurredAt ? 'done' : 'planned',
61
+ dealId,
62
+ authorUserId: activity.authorUserId ?? null,
63
+ appearanceIcon: activity.appearanceIcon ?? null,
64
+ appearanceColor: activity.appearanceColor ?? null,
65
+ source: CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE,
66
+ ...(customValues ? { customValues } : {}),
67
+ },
68
+ ctx: commandContext,
69
+ })
70
+
71
+ return activity.id
72
+ }
73
+
74
+ /**
75
+ * Returns the canonical `customer_interactions.id` for the given target id. If
76
+ * the canonical record already exists, returns it directly. Otherwise looks
77
+ * the id up in the legacy `customer_activities` table and bridges the row into
78
+ * `customer_interactions` via {@link ensureCanonicalActivityBridge}. If
79
+ * neither exists, returns the original id unchanged so downstream lookups can
80
+ * surface a normal 404.
81
+ */
82
+ export async function resolveCanonicalActivityTargetId(
83
+ em: EntityManager,
84
+ commandBus: CommandBus,
85
+ commandContext: CommandContext,
86
+ targetId: string,
87
+ tenantId: string,
88
+ ): Promise<string> {
89
+ const existing = await em.findOne(CustomerInteraction, { id: targetId, tenantId })
90
+ if (existing) return existing.id
91
+
92
+ // Reads encrypted scalar fields (subject, body, appearanceIcon, appearanceColor)
93
+ // that are forwarded into `customers.interactions.create`. Use the decryption
94
+ // helper so the bridged interaction inherits plaintext values rather than
95
+ // ciphertext when tenant data encryption is enabled.
96
+ const legacy = await findOneWithDecryption(
97
+ em,
98
+ CustomerActivity,
99
+ { id: targetId, tenantId } as any,
100
+ { populate: ['entity', 'deal'] } as any,
101
+ { tenantId },
102
+ )
103
+ if (!legacy) return targetId
104
+
105
+ return ensureCanonicalActivityBridge(em, commandBus, commandContext, legacy)
106
+ }
@@ -40,12 +40,14 @@ export const listIntegrationLogsQuerySchema = z.object({
40
40
 
41
41
  export type ListIntegrationLogsQuery = z.infer<typeof listIntegrationLogsQuerySchema>
42
42
 
43
- const optionalBooleanQuery = z.preprocess((value) => {
44
- if (value === undefined || value === '' || value === null) return undefined
45
- if (value === true || value === 'true' || value === '1') return true
46
- if (value === false || value === 'false' || value === '0') return false
47
- return value
48
- }, z.boolean().optional()).optional()
43
+ const optionalBooleanQuery = z.union([z.boolean(), z.string(), z.null(), z.undefined()])
44
+ .transform((value) => {
45
+ if (value === undefined || value === '' || value === null) return undefined
46
+ if (value === true || value === 'true' || value === '1') return true
47
+ if (value === false || value === 'false' || value === '0') return false
48
+ return value
49
+ })
50
+ .pipe(z.boolean().optional())
49
51
 
50
52
  export const integrationMarketplaceHealthStatusSchema = z.enum(['healthy', 'degraded', 'unhealthy', 'unconfigured'])
51
53
 
@@ -3,6 +3,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
3
3
  import { decryptWithAesGcm, encryptWithAesGcm } from '@open-mercato/shared/lib/encryption/aes'
4
4
  import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
5
  import { createKmsService } from '@open-mercato/shared/lib/encryption/kms'
6
+ import { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
6
7
  import {
7
8
  getBundle,
8
9
  getIntegration,
@@ -15,6 +16,18 @@ import { IntegrationCredentials } from '../data/entities'
15
16
  const ENCRYPTED_CREDENTIALS_BLOB_KEY = '__om_encrypted_credentials_blob_v1'
16
17
  const DERIVED_KEY_CONTEXT = 'integrations.credentials'
17
18
 
19
+ function isRecordValue(value: unknown): value is Record<string, unknown> {
20
+ return !!value && typeof value === 'object' && !Array.isArray(value)
21
+ }
22
+
23
+ function normalizeCredentialsRecord(value: unknown): Record<string, unknown> {
24
+ if (isRecordValue(value)) return value
25
+ if (typeof value !== 'string') return {}
26
+
27
+ const parsed = parseDecryptedFieldValue(value)
28
+ return isRecordValue(parsed) ? parsed : {}
29
+ }
30
+
18
31
  function resolveFallbackEncryptionSecret(): string {
19
32
  const candidates = [
20
33
  process.env.TENANT_DATA_ENCRYPTION_FALLBACK_KEY,
@@ -100,9 +113,10 @@ export function createCredentialsService(em: EntityManager) {
100
113
  }
101
114
 
102
115
  async function decryptCredentialsBlob(
103
- credentials: Record<string, unknown>,
116
+ credentialsInput: unknown,
104
117
  scope: IntegrationScope,
105
118
  ): Promise<Record<string, unknown>> {
119
+ const credentials = normalizeCredentialsRecord(credentialsInput)
106
120
  const encrypted = credentials[ENCRYPTED_CREDENTIALS_BLOB_KEY]
107
121
  if (typeof encrypted !== 'string' || !encrypted) return credentials
108
122
 
@@ -2,6 +2,7 @@ import type { EntityManager } from '@mikro-orm/postgresql'
2
2
  import { z } from 'zod'
3
3
  import { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'
4
4
  import { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'
5
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
6
  import { Message, MessageObject, MessageRecipient } from '../data/entities'
6
7
  import { emitMessagesEvent } from '../events'
7
8
  import {
@@ -9,6 +10,7 @@ import {
9
10
  isTerminalMessageAction,
10
11
  resolveActionCommandInput,
11
12
  resolveActionHref,
13
+ resolveMessageActionData,
12
14
  } from '../lib/actions'
13
15
  import { getMessageType } from '../lib/message-types-registry'
14
16
  import { assertOrganizationAccess, type MessageScopeInput } from './shared'
@@ -52,11 +54,17 @@ function toDate(value: string | null | undefined): Date | null {
52
54
  }
53
55
 
54
56
  async function requireActionTarget(em: EntityManager, input: RecordTerminalActionInput) {
55
- const message = await em.findOne(Message, {
56
- id: input.messageId,
57
- tenantId: input.tenantId,
58
- deletedAt: null,
59
- })
57
+ const message = await findOneWithDecryption(
58
+ em,
59
+ Message,
60
+ {
61
+ id: input.messageId,
62
+ tenantId: input.tenantId,
63
+ deletedAt: null,
64
+ },
65
+ undefined,
66
+ { tenantId: input.tenantId, organizationId: input.organizationId },
67
+ )
60
68
  if (!message) throw new Error('Message not found')
61
69
  assertOrganizationAccess(input as MessageScopeInput, message)
62
70
 
@@ -70,11 +78,17 @@ async function requireActionTarget(em: EntityManager, input: RecordTerminalActio
70
78
  }
71
79
 
72
80
  async function requireActionMessage(em: EntityManager, input: ExecuteActionInput) {
73
- const message = await em.findOne(Message, {
74
- id: input.messageId,
75
- tenantId: input.tenantId,
76
- deletedAt: null,
77
- })
81
+ const message = await findOneWithDecryption(
82
+ em,
83
+ Message,
84
+ {
85
+ id: input.messageId,
86
+ tenantId: input.tenantId,
87
+ deletedAt: null,
88
+ },
89
+ undefined,
90
+ { tenantId: input.tenantId, organizationId: input.organizationId },
91
+ )
78
92
  if (!message) throw new Error('Message not found')
79
93
  assertOrganizationAccess(input as MessageScopeInput, message)
80
94
  const recipient = await em.findOne(MessageRecipient, {
@@ -148,7 +162,7 @@ const recordTerminalActionCommand: CommandHandler<unknown, { ok: true }> = {
148
162
  const messageId = logEntry?.resourceId as string | null
149
163
  if (!messageId) return
150
164
  const em = (ctx.container.resolve('em') as EntityManager).fork()
151
- const message = await em.findOne(Message, { id: messageId })
165
+ const message = await findOneWithDecryption(em, Message, { id: messageId })
152
166
  if (!message) return
153
167
  message.actionTaken = before.actionTaken
154
168
  message.actionTakenByUserId = before.actionTakenByUserId
@@ -180,8 +194,9 @@ const executeActionCommand: CommandHandler<
180
194
 
181
195
  const shouldRecordActionTaken = isTerminalMessageAction(action)
182
196
 
183
- if (message.actionData?.expiresAt) {
184
- if (new Date(message.actionData.expiresAt) < new Date()) {
197
+ const actionData = resolveMessageActionData(message)
198
+ if (actionData?.expiresAt) {
199
+ if (new Date(actionData.expiresAt) < new Date()) {
185
200
  throw new Error('Actions have expired')
186
201
  }
187
202
  } else {
@@ -1,3 +1,4 @@
1
+ import { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
1
2
  import type { Message, MessageAction, MessageActionData, MessageObject } from '../data/entities'
2
3
  import { getMessageObjectType } from './message-objects-registry'
3
4
  import { getMessageType } from './message-types-registry'
@@ -22,6 +23,34 @@ export type MessageActionResolutionContext = {
22
23
  userId: string
23
24
  }
24
25
 
26
+ function isRecordValue(value: unknown): value is Record<string, unknown> {
27
+ return !!value && typeof value === 'object' && !Array.isArray(value)
28
+ }
29
+
30
+ export function resolveMessageActionData(message: Pick<Message, 'actionData'>): MessageActionData | null {
31
+ const rawActionData = message.actionData as unknown
32
+ const parsedActionData = typeof rawActionData === 'string'
33
+ ? parseDecryptedFieldValue(rawActionData)
34
+ : rawActionData
35
+ if (!isRecordValue(parsedActionData)) return null
36
+
37
+ const actions = Array.isArray(parsedActionData.actions)
38
+ ? parsedActionData.actions.filter(isRecordValue) as MessageAction[]
39
+ : []
40
+ const primaryActionId = typeof parsedActionData.primaryActionId === 'string'
41
+ ? parsedActionData.primaryActionId
42
+ : undefined
43
+ const expiresAt = typeof parsedActionData.expiresAt === 'string'
44
+ ? parsedActionData.expiresAt
45
+ : undefined
46
+
47
+ return {
48
+ actions,
49
+ ...(primaryActionId ? { primaryActionId } : {}),
50
+ ...(expiresAt ? { expiresAt } : {}),
51
+ }
52
+ }
53
+
25
54
  function normalizeActionLabel(
26
55
  action: Pick<MessageAction, 'label' | 'id'>,
27
56
  fallback?: string | null,
@@ -49,7 +78,8 @@ export function buildMessageObjectActionId(objectId: string, actionId: string):
49
78
  }
50
79
 
51
80
  function readActionDataExpiry(message: Message): string | undefined {
52
- if (message.actionData?.expiresAt) return message.actionData.expiresAt
81
+ const actionData = resolveMessageActionData(message)
82
+ if (actionData?.expiresAt) return actionData.expiresAt
53
83
  const messageType = getMessageType(message.type)
54
84
  if (!messageType?.actionsExpireAfterHours || !message.sentAt) return undefined
55
85
  return new Date(
@@ -63,6 +93,7 @@ export function buildResolvedMessageActions(
63
93
  ): MessageActionData | null {
64
94
  const resolved: ResolvedMessageAction[] = []
65
95
  const usedIds = new Set<string>()
96
+ const actionData = resolveMessageActionData(message)
66
97
 
67
98
  const pushAction = (
68
99
  action: MessageAction,
@@ -83,7 +114,7 @@ export function buildResolvedMessageActions(
83
114
  })
84
115
  }
85
116
 
86
- for (const action of message.actionData?.actions ?? []) {
117
+ for (const action of actionData?.actions ?? []) {
87
118
  pushAction(action, 'message')
88
119
  }
89
120
 
@@ -128,7 +159,7 @@ export function buildResolvedMessageActions(
128
159
  return null
129
160
  }
130
161
 
131
- const configuredPrimaryActionId = message.actionData?.primaryActionId
162
+ const configuredPrimaryActionId = actionData?.primaryActionId
132
163
  const primaryActionId = configuredPrimaryActionId && resolved.some((entry) => entry.id === configuredPrimaryActionId)
133
164
  ? configuredPrimaryActionId
134
165
  : resolved[0]?.id
@@ -21,6 +21,7 @@ import { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern
21
21
  import { parseBooleanToken } from '@open-mercato/shared/lib/boolean'
22
22
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
23
23
  import { recalculateOrderTotalsForDisplay } from '../../commands/returns'
24
+ import { parseDecryptedFieldValue } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
24
25
 
25
26
  type DocumentKind = 'order' | 'quote'
26
27
 
@@ -38,6 +39,17 @@ type DocumentBinding = {
38
39
 
39
40
  const rawBodySchema = z.object({}).passthrough()
40
41
 
42
+ const normalizeJsonRecord = (value: unknown): Record<string, unknown> | null => {
43
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
44
+ return value as Record<string, unknown>
45
+ }
46
+ if (typeof value !== 'string') return null
47
+ const parsed = parseDecryptedFieldValue(value)
48
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
49
+ ? parsed as Record<string, unknown>
50
+ : null
51
+ }
52
+
41
53
  const resolveCustomerName = (snapshot: Record<string, unknown> | null, fallback?: string | null) => {
42
54
  if (!snapshot) return fallback ?? null
43
55
  const customer = snapshot.customer as Record<string, unknown> | undefined
@@ -155,38 +167,43 @@ function buildSortMap(numberColumn: string) {
155
167
  }
156
168
  }
157
169
 
158
- const mapUpdateResponse = (entity: any) => ({
159
- id: entity?.id ?? null,
160
- orderNumber: entity?.orderNumber ?? null,
161
- quoteNumber: entity?.quoteNumber ?? null,
162
- customerEntityId: entity?.customerEntityId ?? null,
163
- customerContactId: entity?.customerContactId ?? null,
164
- customerSnapshot: entity?.customerSnapshot ?? null,
165
- metadata: entity?.metadata ?? null,
166
- externalReference: entity?.externalReference ?? null,
167
- customerReference: entity?.customerReference ?? null,
168
- comment: entity?.comments ?? null,
169
- statusEntryId: (entity as any)?.statusEntryId ?? null,
170
- status: (entity as any)?.status ?? null,
171
- channelId: (entity as any)?.channelId ?? null,
172
- customerName: resolveCustomerName(entity?.customerSnapshot ?? null, entity?.customerEntityId ?? null),
173
- contactEmail:
174
- resolveCustomerEmail(entity?.customerSnapshot ?? null) ??
175
- (typeof entity?.metadata?.customerEmail === 'string' ? entity.metadata.customerEmail : null),
176
- currencyCode: entity?.currencyCode ?? null,
177
- placedAt: entity?.placedAt ? entity.placedAt.toISOString() : null,
178
- expectedDeliveryAt: entity?.expectedDeliveryAt ? entity.expectedDeliveryAt.toISOString() : null,
179
- shippingAddressId: entity?.shippingAddressId ?? null,
180
- billingAddressId: entity?.billingAddressId ?? null,
181
- shippingAddressSnapshot: entity?.shippingAddressSnapshot ?? null,
182
- billingAddressSnapshot: entity?.billingAddressSnapshot ?? null,
183
- shippingMethodId: entity?.shippingMethodId ?? null,
184
- shippingMethodCode: entity?.shippingMethodCode ?? null,
185
- shippingMethodSnapshot: entity?.shippingMethodSnapshot ?? null,
186
- paymentMethodId: entity?.paymentMethodId ?? null,
187
- paymentMethodCode: entity?.paymentMethodCode ?? null,
188
- paymentMethodSnapshot: entity?.paymentMethodSnapshot ?? null,
189
- })
170
+ const mapUpdateResponse = (entity: any) => {
171
+ const customerSnapshot = normalizeJsonRecord(entity?.customerSnapshot)
172
+ const metadata = normalizeJsonRecord(entity?.metadata)
173
+
174
+ return {
175
+ id: entity?.id ?? null,
176
+ orderNumber: entity?.orderNumber ?? null,
177
+ quoteNumber: entity?.quoteNumber ?? null,
178
+ customerEntityId: entity?.customerEntityId ?? null,
179
+ customerContactId: entity?.customerContactId ?? null,
180
+ customerSnapshot,
181
+ metadata,
182
+ externalReference: entity?.externalReference ?? null,
183
+ customerReference: entity?.customerReference ?? null,
184
+ comment: entity?.comments ?? null,
185
+ statusEntryId: (entity as any)?.statusEntryId ?? null,
186
+ status: (entity as any)?.status ?? null,
187
+ channelId: (entity as any)?.channelId ?? null,
188
+ customerName: resolveCustomerName(customerSnapshot, entity?.customerEntityId ?? null),
189
+ contactEmail:
190
+ resolveCustomerEmail(customerSnapshot) ??
191
+ (typeof metadata?.customerEmail === 'string' ? metadata.customerEmail : null),
192
+ currencyCode: entity?.currencyCode ?? null,
193
+ placedAt: entity?.placedAt ? entity.placedAt.toISOString() : null,
194
+ expectedDeliveryAt: entity?.expectedDeliveryAt ? entity.expectedDeliveryAt.toISOString() : null,
195
+ shippingAddressId: entity?.shippingAddressId ?? null,
196
+ billingAddressId: entity?.billingAddressId ?? null,
197
+ shippingAddressSnapshot: normalizeJsonRecord(entity?.shippingAddressSnapshot),
198
+ billingAddressSnapshot: normalizeJsonRecord(entity?.billingAddressSnapshot),
199
+ shippingMethodId: entity?.shippingMethodId ?? null,
200
+ shippingMethodCode: entity?.shippingMethodCode ?? null,
201
+ shippingMethodSnapshot: normalizeJsonRecord(entity?.shippingMethodSnapshot),
202
+ paymentMethodId: entity?.paymentMethodId ?? null,
203
+ paymentMethodCode: entity?.paymentMethodCode ?? null,
204
+ paymentMethodSnapshot: normalizeJsonRecord(entity?.paymentMethodSnapshot),
205
+ }
206
+ }
190
207
 
191
208
  const attachTags = async (payload: any, ctx: any) => {
192
209
  const items = Array.isArray(payload?.items) ? (payload.items as Array<Record<string, any>>) : []
@@ -369,10 +386,10 @@ export function buildDocumentCrudOptions(binding: DocumentBinding) {
369
386
  shippingAddressId: item.shipping_address_id ?? null,
370
387
  shippingMethodId: item.shipping_method_id ?? null,
371
388
  shippingMethodCode: item.shipping_method_code ?? null,
372
- shippingMethodSnapshot: item.shipping_method_snapshot ?? null,
389
+ shippingMethodSnapshot: normalizeJsonRecord(item.shipping_method_snapshot),
373
390
  paymentMethodId: item.payment_method_id ?? null,
374
391
  paymentMethodCode: item.payment_method_code ?? null,
375
- paymentMethodSnapshot: item.payment_method_snapshot ?? null,
392
+ paymentMethodSnapshot: normalizeJsonRecord(item.payment_method_snapshot),
376
393
  currencyCode: item.currency_code ?? null,
377
394
  channelId: item.channel_id ?? null,
378
395
  externalReference: item.external_reference ?? null,
@@ -395,10 +412,10 @@ export function buildDocumentCrudOptions(binding: DocumentBinding) {
395
412
  paidTotalAmount: toNumber(item.paid_total_amount),
396
413
  refundedTotalAmount: toNumber(item.refunded_total_amount),
397
414
  outstandingAmount: toNumber(item.outstanding_amount),
398
- customerSnapshot: item.customer_snapshot ?? null,
399
- billingAddressSnapshot: item.billing_address_snapshot ?? null,
400
- shippingAddressSnapshot: item.shipping_address_snapshot ?? null,
401
- metadata: item.metadata ?? null,
415
+ customerSnapshot: normalizeJsonRecord(item.customer_snapshot),
416
+ billingAddressSnapshot: normalizeJsonRecord(item.billing_address_snapshot),
417
+ shippingAddressSnapshot: normalizeJsonRecord(item.shipping_address_snapshot),
418
+ metadata: normalizeJsonRecord(item.metadata),
402
419
  organizationId: item.organization_id ?? null,
403
420
  tenantId: item.tenant_id ?? null,
404
421
  createdAt: item.created_at,