@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +21 -1
- package/dist/modules/api_keys/api/keys/route.js +9 -0
- package/dist/modules/api_keys/api/keys/route.js.map +2 -2
- package/dist/modules/audit_logs/services/accessLogService.js +13 -0
- package/dist/modules/audit_logs/services/accessLogService.js.map +3 -3
- package/dist/modules/audit_logs/services/actionLogService.js +6 -5
- package/dist/modules/audit_logs/services/actionLogService.js.map +2 -2
- package/dist/modules/auth/api/roles/acl/route.js +27 -37
- package/dist/modules/auth/api/roles/acl/route.js.map +2 -2
- package/dist/modules/auth/api/users/route.js +41 -28
- package/dist/modules/auth/api/users/route.js.map +3 -3
- package/dist/modules/auth/lib/grantChecks.js +160 -0
- package/dist/modules/auth/lib/grantChecks.js.map +7 -0
- package/dist/modules/configs/cli.js +11 -0
- package/dist/modules/configs/cli.js.map +2 -2
- package/dist/modules/configs/lib/touchGeneratedBarrels.js +46 -0
- package/dist/modules/configs/lib/touchGeneratedBarrels.js.map +7 -0
- package/dist/modules/customers/api/activities/route.js +1 -52
- package/dist/modules/customers/api/activities/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/counts/route.js +2 -1
- package/dist/modules/customers/api/interactions/counts/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +21 -1
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +5 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +7 -3
- package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js +62 -6
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +21 -6
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesSection.js +37 -5
- package/dist/modules/customers/components/detail/ActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityCard.js +69 -17
- package/dist/modules/customers/components/detail/ActivityCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityHistorySection.js +94 -34
- package/dist/modules/customers/components/detail/ActivityHistorySection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityLogTab.js +3 -1
- package/dist/modules/customers/components/detail/ActivityLogTab.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimeline.js +41 -8
- package/dist/modules/customers/components/detail/ActivityTimeline.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js +19 -6
- package/dist/modules/customers/components/detail/ActivityTimelineFilters.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js +4 -3
- package/dist/modules/customers/components/detail/ActivityTypeSelector.js.map +2 -2
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js +80 -12
- package/dist/modules/customers/components/detail/ScheduleActivityDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +65 -10
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js +10 -5
- package/dist/modules/customers/components/detail/schedule/useScheduleFormState.js.map +2 -2
- package/dist/modules/customers/data/validators.js +74 -2
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/lib/legacyActivityBridge.js +61 -0
- package/dist/modules/customers/lib/legacyActivityBridge.js.map +7 -0
- package/dist/modules/integrations/data/validators.js +2 -2
- package/dist/modules/integrations/data/validators.js.map +2 -2
- package/dist/modules/integrations/lib/credentials-service.js +12 -1
- package/dist/modules/integrations/lib/credentials-service.js.map +2 -2
- package/dist/modules/messages/commands/actions.js +29 -14
- package/dist/modules/messages/commands/actions.js.map +2 -2
- package/dist/modules/messages/lib/actions.js +24 -4
- package/dist/modules/messages/lib/actions.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +49 -36
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/package.json +9 -10
- package/src/modules/api_keys/api/keys/route.ts +9 -0
- package/src/modules/audit_logs/services/accessLogService.ts +20 -0
- package/src/modules/audit_logs/services/actionLogService.ts +13 -5
- package/src/modules/auth/api/roles/acl/route.ts +32 -46
- package/src/modules/auth/api/users/route.ts +48 -33
- package/src/modules/auth/lib/grantChecks.ts +234 -0
- package/src/modules/configs/cli.ts +11 -0
- package/src/modules/configs/lib/touchGeneratedBarrels.ts +61 -0
- package/src/modules/customers/api/activities/route.ts +1 -76
- package/src/modules/customers/api/interactions/counts/route.ts +2 -1
- package/src/modules/customers/api/interactions/route.ts +28 -1
- package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +14 -2
- package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +13 -3
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +92 -5
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +38 -6
- package/src/modules/customers/components/detail/ActivitiesSection.tsx +37 -3
- package/src/modules/customers/components/detail/ActivityCard.tsx +79 -14
- package/src/modules/customers/components/detail/ActivityHistorySection.tsx +102 -33
- package/src/modules/customers/components/detail/ActivityLogTab.tsx +7 -1
- package/src/modules/customers/components/detail/ActivityTimeline.tsx +39 -5
- package/src/modules/customers/components/detail/ActivityTimelineFilters.tsx +29 -7
- package/src/modules/customers/components/detail/ActivityTypeSelector.tsx +3 -2
- package/src/modules/customers/components/detail/ScheduleActivityDialog.tsx +96 -13
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +50 -4
- package/src/modules/customers/components/detail/schedule/useScheduleFormState.ts +21 -5
- package/src/modules/customers/data/validators.ts +85 -2
- package/src/modules/customers/i18n/de.json +11 -0
- package/src/modules/customers/i18n/en.json +11 -0
- package/src/modules/customers/i18n/es.json +11 -0
- package/src/modules/customers/i18n/pl.json +11 -0
- package/src/modules/customers/lib/legacyActivityBridge.ts +106 -0
- package/src/modules/integrations/data/validators.ts +8 -6
- package/src/modules/integrations/lib/credentials-service.ts +15 -1
- package/src/modules/messages/commands/actions.ts +28 -13
- package/src/modules/messages/lib/actions.ts +34 -3
- 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.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
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
|
|
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
|
|
399
|
-
billingAddressSnapshot: item.billing_address_snapshot
|
|
400
|
-
shippingAddressSnapshot: item.shipping_address_snapshot
|
|
401
|
-
metadata: item.metadata
|
|
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,
|