@open-mercato/core 0.9.0 → 0.9.1-develop.7459.1.99e3accd30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/customer_dictionary_entry/index.js +2 -0
- package/dist/generated/entities/customer_dictionary_entry/index.js.map +2 -2
- package/dist/generated/entities/customer_interaction/index.js +2 -0
- package/dist/generated/entities/customer_interaction/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/catalog/commands/categories.js +1 -1
- package/dist/modules/catalog/commands/categories.js.map +2 -2
- package/dist/modules/catalog/commands/productDeleteChildren.js +389 -0
- package/dist/modules/catalog/commands/productDeleteChildren.js.map +7 -0
- package/dist/modules/catalog/commands/products.js +52 -17
- package/dist/modules/catalog/commands/products.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js +4 -6
- package/dist/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.js.map +2 -2
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js +47 -11
- package/dist/modules/communication_channels/api/post/oauth/[provider]/initiate/route.js.map +3 -3
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js +5 -1
- package/dist/modules/communication_channels/commands/deliver-outbound-message.js.map +2 -2
- package/dist/modules/communication_channels/lib/inbound-message-origin.js +1 -1
- package/dist/modules/communication_channels/lib/inbound-message-origin.js.map +2 -2
- package/dist/modules/communication_channels/lib/oauth-state.js +4 -1
- package/dist/modules/communication_channels/lib/oauth-state.js.map +2 -2
- package/dist/modules/communication_channels/lib/use-connect-channel.js +10 -0
- package/dist/modules/communication_channels/lib/use-connect-channel.js.map +2 -2
- package/dist/modules/customers/api/activity-types/route.js +151 -0
- package/dist/modules/customers/api/activity-types/route.js.map +7 -0
- package/dist/modules/customers/api/deals/[id]/people/route.js +58 -5
- package/dist/modules/customers/api/deals/[id]/people/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js +13 -5
- package/dist/modules/customers/api/dictionaries/[kind]/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -5
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/route.js +3 -0
- package/dist/modules/customers/api/interactions/route.js.map +2 -2
- package/dist/modules/customers/api/interactions/tasks/route.js +1 -0
- package/dist/modules/customers/api/interactions/tasks/route.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +18 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js +0 -23
- package/dist/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +15 -17
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/calendar-event-types.js +548 -0
- package/dist/modules/customers/calendar-event-types.js.map +7 -0
- package/dist/modules/customers/commands/dictionaries.js +70 -10
- package/dist/modules/customers/commands/dictionaries.js.map +2 -2
- package/dist/modules/customers/commands/interactions.js +187 -22
- package/dist/modules/customers/commands/interactions.js.map +3 -3
- package/dist/modules/customers/components/ActivityTypeEditor.js +288 -0
- package/dist/modules/customers/components/ActivityTypeEditor.js.map +7 -0
- package/dist/modules/customers/components/DictionarySettings.js +3 -1
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js +391 -297
- package/dist/modules/customers/components/calendar/CalendarEventEditor.js.map +3 -3
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js +9 -74
- package/dist/modules/customers/components/calendar/CalendarSettingsModal.js.map +2 -2
- package/dist/modules/customers/components/calendar/TimeGrid.js +20 -16
- package/dist/modules/customers/components/calendar/TimeGrid.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js +218 -0
- package/dist/modules/customers/components/calendar/editor/EventTypePanel.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/LocationField.js +7 -3
- package/dist/modules/customers/components/calendar/editor/LocationField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js +5 -2
- package/dist/modules/customers/components/calendar/editor/PeopleField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/PriorityField.js +3 -1
- package/dist/modules/customers/components/calendar/editor/PriorityField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/RepeatField.js +9 -1
- package/dist/modules/customers/components/calendar/editor/RepeatField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js +4 -1
- package/dist/modules/customers/components/calendar/editor/ResourcesField.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js +15 -7
- package/dist/modules/customers/components/calendar/editor/ScheduleSection.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js +3 -1
- package/dist/modules/customers/components/calendar/editor/SegmentGroup.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/hooks.js +28 -13
- package/dist/modules/customers/components/calendar/editor/hooks.js.map +3 -3
- package/dist/modules/customers/components/calendar/editor/inputs.js +14 -6
- package/dist/modules/customers/components/calendar/editor/inputs.js.map +2 -2
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js +54 -0
- package/dist/modules/customers/components/calendar/editor/useCalendarCustomFields.js.map +7 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js +82 -0
- package/dist/modules/customers/components/calendar/editor/useEventTypeCatalog.js.map +7 -0
- package/dist/modules/customers/components/calendar/types.js +1 -0
- package/dist/modules/customers/components/calendar/types.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js +78 -343
- package/dist/modules/customers/components/detail/CompanyPeopleSection.js.map +2 -2
- package/dist/modules/customers/components/detail/CreatePersonDialog.js +12 -11
- package/dist/modules/customers/components/detail/CreatePersonDialog.js.map +2 -2
- package/dist/modules/customers/components/detail/DealForm.js +7 -3
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/components/detail/DealPeopleSection.js +206 -0
- package/dist/modules/customers/components/detail/DealPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js +342 -0
- package/dist/modules/customers/components/detail/LinkedPeopleSection.js.map +7 -0
- package/dist/modules/customers/components/detail/PersonCard.js +5 -2
- package/dist/modules/customers/components/detail/PersonCard.js.map +2 -2
- package/dist/modules/customers/components/linking/adapters/personAdapter.js +3 -0
- package/dist/modules/customers/components/linking/adapters/personAdapter.js.map +2 -2
- package/dist/modules/customers/data/entities.js +6 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +39 -10
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/di.js +20 -1
- package/dist/modules/customers/di.js.map +2 -2
- package/dist/modules/customers/extension-points.js +13 -0
- package/dist/modules/customers/extension-points.js.map +2 -2
- package/dist/modules/customers/lib/calendar/conflicts.js +59 -16
- package/dist/modules/customers/lib/calendar/conflicts.js.map +2 -2
- package/dist/modules/customers/lib/calendar/editorPayload.js +34 -16
- package/dist/modules/customers/lib/calendar/editorPayload.js.map +2 -2
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js +197 -0
- package/dist/modules/customers/lib/calendar/eventTypeResolver.js.map +7 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js +67 -0
- package/dist/modules/customers/lib/calendar/interactionApplicability.js.map +7 -0
- package/dist/modules/customers/lib/calendar/mapItem.js +5 -2
- package/dist/modules/customers/lib/calendar/mapItem.js.map +2 -2
- package/dist/modules/customers/lib/calendar/recurrence.js +62 -1
- package/dist/modules/customers/lib/calendar/recurrence.js.map +2 -2
- package/dist/modules/customers/lib/calendar/timezone.js +111 -0
- package/dist/modules/customers/lib/calendar/timezone.js.map +7 -0
- package/dist/modules/customers/lib/interactionReadModel.js +1 -0
- package/dist/modules/customers/lib/interactionReadModel.js.map +2 -2
- package/dist/modules/customers/lib/todoCompatibility.js +1 -0
- package/dist/modules/customers/lib/todoCompatibility.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260928175058_customers.js.map +7 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js +17 -0
- package/dist/modules/customers/migrations/Migration20260930110235_customers.js.map +7 -0
- package/dist/modules/dictionaries/components/DictionaryTable.js +60 -51
- package/dist/modules/dictionaries/components/DictionaryTable.js.map +2 -2
- package/dist/modules/messages/subscribers/message-notification.js +13 -1
- package/dist/modules/messages/subscribers/message-notification.js.map +2 -2
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js +201 -53
- package/dist/modules/payment_gateways/api/webhook/[provider]/route.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +37 -11
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/planner/lib/availabilityMerge.js +101 -38
- package/dist/modules/planner/lib/availabilityMerge.js.map +3 -3
- package/dist/modules/planner/services/plannerAvailabilityService.js.map +2 -2
- package/dist/modules/sales/components/documents/LineItemDialog.js +159 -81
- package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
- package/dist/modules/sales/components/documents/lineItemUtils.js +7 -0
- package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js +12 -7
- package/dist/modules/staff/backend/staff/time-tracking/projects/[id]/page.js.map +2 -2
- package/dist/modules/sync_excel/lib/adapters/customers.js +28 -25
- package/dist/modules/sync_excel/lib/adapters/customers.js.map +2 -2
- package/generated/entities/customer_dictionary_entry/index.ts +1 -0
- package/generated/entities/customer_interaction/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +9 -8
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/catalog/commands/categories.ts +1 -1
- package/src/modules/catalog/commands/productDeleteChildren.ts +557 -0
- package/src/modules/catalog/commands/products.ts +58 -17
- package/src/modules/communication_channels/api/post/channels/connect/credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts +4 -6
- package/src/modules/communication_channels/api/post/oauth/[provider]/initiate/route.ts +46 -10
- package/src/modules/communication_channels/commands/deliver-outbound-message.ts +6 -1
- package/src/modules/communication_channels/i18n/de.json +4 -0
- package/src/modules/communication_channels/i18n/en.json +4 -0
- package/src/modules/communication_channels/i18n/es.json +4 -0
- package/src/modules/communication_channels/i18n/ko.json +4 -0
- package/src/modules/communication_channels/i18n/pl.json +4 -0
- package/src/modules/communication_channels/lib/inbound-message-origin.ts +2 -6
- package/src/modules/communication_channels/lib/oauth-state.ts +4 -1
- package/src/modules/communication_channels/lib/use-connect-channel.ts +10 -0
- package/src/modules/customers/api/activity-types/route.ts +154 -0
- package/src/modules/customers/api/deals/[id]/people/route.ts +82 -3
- package/src/modules/customers/api/dictionaries/[kind]/[id]/route.ts +9 -1
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +18 -2
- package/src/modules/customers/api/interactions/route.ts +4 -0
- package/src/modules/customers/api/interactions/tasks/route.ts +1 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +20 -0
- package/src/modules/customers/backend/customers/deals/[id]/hooks/useDealAssociations.ts +0 -29
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +15 -17
- package/src/modules/customers/calendar-event-types.ts +674 -0
- package/src/modules/customers/commands/dictionaries.ts +84 -8
- package/src/modules/customers/commands/interactions.ts +244 -21
- package/src/modules/customers/components/ActivityTypeEditor.tsx +239 -0
- package/src/modules/customers/components/DictionarySettings.tsx +5 -3
- package/src/modules/customers/components/calendar/CalendarEventEditor.tsx +347 -218
- package/src/modules/customers/components/calendar/CalendarSettingsModal.tsx +18 -96
- package/src/modules/customers/components/calendar/TimeGrid.tsx +20 -18
- package/src/modules/customers/components/calendar/editor/EventTypePanel.tsx +230 -0
- package/src/modules/customers/components/calendar/editor/LocationField.tsx +7 -1
- package/src/modules/customers/components/calendar/editor/PeopleField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/PriorityField.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/RepeatField.tsx +10 -0
- package/src/modules/customers/components/calendar/editor/ResourcesField.tsx +4 -0
- package/src/modules/customers/components/calendar/editor/ScheduleSection.tsx +16 -4
- package/src/modules/customers/components/calendar/editor/SegmentGroup.tsx +3 -0
- package/src/modules/customers/components/calendar/editor/hooks.ts +29 -13
- package/src/modules/customers/components/calendar/editor/inputs.tsx +17 -5
- package/src/modules/customers/components/calendar/editor/useCalendarCustomFields.ts +49 -0
- package/src/modules/customers/components/calendar/editor/useEventTypeCatalog.ts +100 -0
- package/src/modules/customers/components/calendar/types.ts +1 -0
- package/src/modules/customers/components/detail/CompanyPeopleSection.tsx +94 -388
- package/src/modules/customers/components/detail/CreatePersonDialog.tsx +39 -19
- package/src/modules/customers/components/detail/DealForm.tsx +7 -3
- package/src/modules/customers/components/detail/DealPeopleSection.tsx +256 -0
- package/src/modules/customers/components/detail/LinkedPeopleSection.tsx +474 -0
- package/src/modules/customers/components/detail/PersonCard.tsx +12 -4
- package/src/modules/customers/components/detail/types.ts +1 -0
- package/src/modules/customers/components/linking/adapters/personAdapter.tsx +4 -0
- package/src/modules/customers/data/entities.ts +8 -1
- package/src/modules/customers/data/validators.ts +42 -7
- package/src/modules/customers/di.ts +21 -0
- package/src/modules/customers/extension-points.ts +13 -0
- package/src/modules/customers/i18n/de.json +104 -8
- package/src/modules/customers/i18n/en.json +104 -8
- package/src/modules/customers/i18n/es.json +104 -8
- package/src/modules/customers/i18n/ko.json +104 -8
- package/src/modules/customers/i18n/pl.json +104 -8
- package/src/modules/customers/lib/calendar/conflicts.ts +106 -27
- package/src/modules/customers/lib/calendar/editorPayload.ts +39 -19
- package/src/modules/customers/lib/calendar/eventTypeResolver.ts +258 -0
- package/src/modules/customers/lib/calendar/interactionApplicability.ts +99 -0
- package/src/modules/customers/lib/calendar/mapItem.ts +6 -2
- package/src/modules/customers/lib/calendar/recurrence.ts +65 -1
- package/src/modules/customers/lib/calendar/timezone.ts +117 -0
- package/src/modules/customers/lib/interactionReadModel.ts +1 -0
- package/src/modules/customers/lib/todoCompatibility.ts +2 -0
- package/src/modules/customers/migrations/.snapshot-open-mercato.json +34 -0
- package/src/modules/customers/migrations/Migration20260928175058_customers.ts +15 -0
- package/src/modules/customers/migrations/Migration20260930110235_customers.ts +15 -0
- package/src/modules/dictionaries/components/DictionaryTable.tsx +63 -56
- package/src/modules/messages/subscribers/message-notification.ts +13 -3
- package/src/modules/payment_gateways/api/webhook/[provider]/route.ts +243 -62
- package/src/modules/payment_gateways/lib/gateway-service.ts +39 -12
- package/src/modules/planner/lib/availabilityMerge.ts +106 -41
- package/src/modules/planner/services/plannerAvailabilityService.ts +4 -0
- package/src/modules/sales/components/documents/LineItemDialog.tsx +194 -111
- package/src/modules/sales/components/documents/lineItemUtils.ts +18 -0
- package/src/modules/staff/backend/staff/time-tracking/projects/[id]/page.tsx +25 -13
- package/src/modules/sync_excel/lib/adapters/customers.ts +33 -27
|
@@ -33,16 +33,14 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
33
33
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const json = await readJsonSafe(req, null)
|
|
39
|
-
body = bodySchema.parse(json)
|
|
40
|
-
} catch (err) {
|
|
36
|
+
const parsed = bodySchema.safeParse(await readJsonSafe(req, null))
|
|
37
|
+
if (!parsed.success) {
|
|
41
38
|
return NextResponse.json(
|
|
42
|
-
{ error:
|
|
39
|
+
{ error: 'Invalid request body', details: parsed.error.issues },
|
|
43
40
|
{ status: 422 },
|
|
44
41
|
)
|
|
45
42
|
}
|
|
43
|
+
const body = parsed.data
|
|
46
44
|
|
|
47
45
|
const container = await createRequestContainer()
|
|
48
46
|
|
package/src/modules/communication_channels/api/post/channels/connect/tenant-credentials/route.ts
CHANGED
|
@@ -33,16 +33,14 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
33
33
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const json = await readJsonSafe(req, null)
|
|
39
|
-
body = bodySchema.parse(json)
|
|
40
|
-
} catch (err) {
|
|
36
|
+
const parsed = bodySchema.safeParse(await readJsonSafe(req, null))
|
|
37
|
+
if (!parsed.success) {
|
|
41
38
|
return NextResponse.json(
|
|
42
|
-
{ error:
|
|
39
|
+
{ error: 'Invalid request body', details: parsed.error.issues },
|
|
43
40
|
{ status: 422 },
|
|
44
41
|
)
|
|
45
42
|
}
|
|
43
|
+
const body = parsed.data
|
|
46
44
|
|
|
47
45
|
const container = await createRequestContainer()
|
|
48
46
|
|
|
@@ -4,6 +4,8 @@ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
|
4
4
|
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
5
5
|
import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'
|
|
6
6
|
import { toAbsoluteUrl } from '@open-mercato/shared/lib/url'
|
|
7
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
8
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
7
9
|
import { getChannelAdapter } from '../../../../../lib/adapter-registry-singleton'
|
|
8
10
|
import { resolveOAuthClientCredentials } from '../../../../../lib/oauth-client-config'
|
|
9
11
|
import {
|
|
@@ -16,6 +18,8 @@ import {
|
|
|
16
18
|
OAuthStateError,
|
|
17
19
|
} from '../../../../../lib/oauth-state'
|
|
18
20
|
|
|
21
|
+
const logger = createLogger('communication_channels').child({ component: 'oauth-initiate' })
|
|
22
|
+
|
|
19
23
|
export const metadata = {
|
|
20
24
|
path: '/communication_channels/oauth/[provider]/initiate',
|
|
21
25
|
POST: {
|
|
@@ -56,6 +60,21 @@ function defaultRedirectUri(req: Request, providerKey: string): string {
|
|
|
56
60
|
return toAbsoluteUrl(req, `/api/communication_channels/oauth/${providerKey}/callback`)
|
|
57
61
|
}
|
|
58
62
|
|
|
63
|
+
function oauthStateErrorResponse(err: unknown): Response | null {
|
|
64
|
+
if (err instanceof OAuthStateError) {
|
|
65
|
+
if (err.code === 'missing_secret') {
|
|
66
|
+
logger.error('OAuth state secret is not configured', { err })
|
|
67
|
+
getTelemetryRuntime()?.reportError(err, {
|
|
68
|
+
module: 'communication_channels',
|
|
69
|
+
code: 'communication_channels.missing_secret',
|
|
70
|
+
})
|
|
71
|
+
return NextResponse.json({ error: 'OAuth is not available', code: err.code }, { status: 500 })
|
|
72
|
+
}
|
|
73
|
+
return NextResponse.json({ error: err.message, code: err.code }, { status: 500 })
|
|
74
|
+
}
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
export async function POST(req: Request, context: RouteContext): Promise<Response> {
|
|
60
79
|
const { provider } = await context.params
|
|
61
80
|
if (!/^[a-z0-9_-]+$/i.test(provider)) {
|
|
@@ -121,13 +140,20 @@ export async function POST(req: Request, context: RouteContext): Promise<Respons
|
|
|
121
140
|
}
|
|
122
141
|
|
|
123
142
|
const redirectUri = defaultRedirectUri(req, provider)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
143
|
+
let stateEnvelope: ReturnType<typeof createOAuthState>
|
|
144
|
+
try {
|
|
145
|
+
stateEnvelope = createOAuthState({
|
|
146
|
+
userId: auth.sub as string,
|
|
147
|
+
tenantId: auth.tenantId as string,
|
|
148
|
+
organizationId: (auth as { orgId?: string | null }).orgId ?? null,
|
|
149
|
+
providerKey: provider,
|
|
150
|
+
returnUrl: normalizeOAuthReturnUrl(body.returnUrl, DEFAULT_OAUTH_RETURN_URL),
|
|
151
|
+
})
|
|
152
|
+
} catch (err) {
|
|
153
|
+
const response = oauthStateErrorResponse(err)
|
|
154
|
+
if (response) return response
|
|
155
|
+
throw err
|
|
156
|
+
}
|
|
131
157
|
|
|
132
158
|
let result
|
|
133
159
|
try {
|
|
@@ -153,12 +179,21 @@ export async function POST(req: Request, context: RouteContext): Promise<Respons
|
|
|
153
179
|
|
|
154
180
|
// If the adapter packed extras (PKCE verifier, scopes), bake them into the
|
|
155
181
|
// state cookie now so the callback handler can pass them to exchangeOAuthCode.
|
|
156
|
-
|
|
157
|
-
|
|
182
|
+
let finalCookie: string
|
|
183
|
+
if (result.extra) {
|
|
184
|
+
try {
|
|
185
|
+
finalCookie = (await import('../../../../../lib/oauth-state')).encryptOAuthState({
|
|
158
186
|
...stateEnvelope.payload,
|
|
159
187
|
extra: { ...(stateEnvelope.payload.extra ?? {}), ...result.extra },
|
|
160
188
|
})
|
|
161
|
-
|
|
189
|
+
} catch (err) {
|
|
190
|
+
const response = oauthStateErrorResponse(err)
|
|
191
|
+
if (response) return response
|
|
192
|
+
throw err
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
finalCookie = stateEnvelope.cookie
|
|
196
|
+
}
|
|
162
197
|
|
|
163
198
|
const response = NextResponse.json({ authorizeUrl: result.authorizeUrl })
|
|
164
199
|
response.cookies.set({
|
|
@@ -184,6 +219,7 @@ export const openApi = {
|
|
|
184
219
|
{ status: 400, description: 'Invalid provider or unsupported (no OAuth)' },
|
|
185
220
|
{ status: 401, description: 'Unauthorized' },
|
|
186
221
|
{ status: 422, description: 'Invalid request body' },
|
|
222
|
+
{ status: 500, description: 'OAuth state secret missing or state encryption failed' },
|
|
187
223
|
{ status: 502, description: 'Adapter failed to build authorize URL' },
|
|
188
224
|
],
|
|
189
225
|
},
|
|
@@ -76,6 +76,11 @@ export type DeliverOutboundMessageResult =
|
|
|
76
76
|
export const COMMUNICATION_CHANNELS_DELIVER_OUTBOUND_COMMAND_ID =
|
|
77
77
|
'communication_channels.message.deliver_outbound'
|
|
78
78
|
|
|
79
|
+
function omitRawMessage(metadata: Record<string, unknown> | undefined): Record<string, unknown> {
|
|
80
|
+
if (!metadata) return {}
|
|
81
|
+
return Object.fromEntries(Object.entries(metadata).filter(([key]) => key !== 'rawMessage'))
|
|
82
|
+
}
|
|
83
|
+
|
|
79
84
|
type CredentialsServiceLike = {
|
|
80
85
|
resolve: (
|
|
81
86
|
integrationId: string,
|
|
@@ -500,7 +505,7 @@ const deliverOutboundMessageCommand: CommandHandler<
|
|
|
500
505
|
link.externalMessageId = externalMessageRowId
|
|
501
506
|
link.channelMetadata = {
|
|
502
507
|
...((link.channelMetadata as Record<string, unknown> | undefined) ?? {}),
|
|
503
|
-
...(converted.metadata
|
|
508
|
+
...omitRawMessage(converted.metadata),
|
|
504
509
|
externalMessageId: sendResult.externalMessageId,
|
|
505
510
|
// Always persist the RFC2822 Message-ID so inbound reply matching (JWZ
|
|
506
511
|
// strategy in lib/thread-matcher) and sent-folder dedup can resolve this
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
"communication_channels.profile.connect.connected": "Kanal verbunden.",
|
|
72
72
|
"communication_channels.profile.connect.connecting": "Wird verbunden...",
|
|
73
73
|
"communication_channels.profile.connect.credentialsFailed": "Postfach konnte nicht verbunden werden.",
|
|
74
|
+
"communication_channels.profile.connect.errors.invalidEmail": "Geben Sie eine gültige E-Mail-Adresse ein.",
|
|
75
|
+
"communication_channels.profile.connect.errors.invalidPort": "Geben Sie einen Port zwischen 1 und 65535 ein.",
|
|
76
|
+
"communication_channels.profile.connect.errors.required": "Dieses Feld ist erforderlich.",
|
|
74
77
|
"communication_channels.profile.connect.fields.displayName": "Anzeigename",
|
|
75
78
|
"communication_channels.profile.connect.fields.fromAddress": "Absenderadresse",
|
|
76
79
|
"communication_channels.profile.connect.fields.imapHost": "IMAP-Host",
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
"communication_channels.profile.connect.imapTitle": "IMAP-Postfach verbinden",
|
|
89
92
|
"communication_channels.profile.connect.mailboxAlreadyConnected": "Dieses Postfach ist bereits über einen anderen Anbieter verbunden. Trennen Sie es zuerst, um es mit einem anderen Anbieter erneut zu verbinden.",
|
|
90
93
|
"communication_channels.profile.connect.menu": "Kanal verbinden",
|
|
94
|
+
"communication_channels.profile.connect.notAvailable": "Die Verbindung mit einem Postfach ist nicht verfügbar. Bitten Sie einen Administrator, den OAuth-Zustandsschlüssel zu konfigurieren.",
|
|
91
95
|
"communication_channels.profile.connect.notConfigured": "Dieser Anbieter ist noch nicht konfiguriert. Bitten Sie einen Administrator, die OAuth-Client-ID und das Secret unter Integrationen zu hinterlegen, bevor ein Postfach verbunden wird.",
|
|
92
96
|
"communication_channels.profile.connect.oauthFailed": "OAuth-Verbindung konnte nicht gestartet werden.",
|
|
93
97
|
"communication_channels.profile.connect.save": "Verbinden",
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
"communication_channels.profile.connect.connected": "Channel connected.",
|
|
72
72
|
"communication_channels.profile.connect.connecting": "Connecting...",
|
|
73
73
|
"communication_channels.profile.connect.credentialsFailed": "Could not connect mailbox.",
|
|
74
|
+
"communication_channels.profile.connect.errors.invalidEmail": "Enter a valid email address.",
|
|
75
|
+
"communication_channels.profile.connect.errors.invalidPort": "Enter a port between 1 and 65535.",
|
|
76
|
+
"communication_channels.profile.connect.errors.required": "This field is required.",
|
|
74
77
|
"communication_channels.profile.connect.fields.displayName": "Display name",
|
|
75
78
|
"communication_channels.profile.connect.fields.fromAddress": "From address",
|
|
76
79
|
"communication_channels.profile.connect.fields.imapHost": "IMAP host",
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
"communication_channels.profile.connect.imapTitle": "Connect IMAP mailbox",
|
|
89
92
|
"communication_channels.profile.connect.mailboxAlreadyConnected": "This mailbox is already connected through another provider. Disconnect it first to reconnect it with a different one.",
|
|
90
93
|
"communication_channels.profile.connect.menu": "Connect channel",
|
|
94
|
+
"communication_channels.profile.connect.notAvailable": "Mailbox connection is not available. Ask an administrator to configure the OAuth state key.",
|
|
91
95
|
"communication_channels.profile.connect.notConfigured": "This provider is not configured yet. Ask an administrator to add the OAuth Client ID and Secret under Integrations before connecting a mailbox.",
|
|
92
96
|
"communication_channels.profile.connect.oauthFailed": "Could not start OAuth connection.",
|
|
93
97
|
"communication_channels.profile.connect.save": "Connect",
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
"communication_channels.profile.connect.connected": "Canal conectado.",
|
|
72
72
|
"communication_channels.profile.connect.connecting": "Conectando...",
|
|
73
73
|
"communication_channels.profile.connect.credentialsFailed": "No se pudo conectar el buzón.",
|
|
74
|
+
"communication_channels.profile.connect.errors.invalidEmail": "Introduce una dirección de correo electrónico válida.",
|
|
75
|
+
"communication_channels.profile.connect.errors.invalidPort": "Introduce un puerto entre 1 y 65535.",
|
|
76
|
+
"communication_channels.profile.connect.errors.required": "Este campo es obligatorio.",
|
|
74
77
|
"communication_channels.profile.connect.fields.displayName": "Nombre visible",
|
|
75
78
|
"communication_channels.profile.connect.fields.fromAddress": "Dirección de remitente",
|
|
76
79
|
"communication_channels.profile.connect.fields.imapHost": "Host IMAP",
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
"communication_channels.profile.connect.imapTitle": "Conectar buzón IMAP",
|
|
89
92
|
"communication_channels.profile.connect.mailboxAlreadyConnected": "Este buzón ya está conectado a través de otro proveedor. Desconéctalo primero para volver a conectarlo con uno diferente.",
|
|
90
93
|
"communication_channels.profile.connect.menu": "Conectar canal",
|
|
94
|
+
"communication_channels.profile.connect.notAvailable": "La conexión del buzón no está disponible. Pide a un administrador que configure la clave de estado de OAuth.",
|
|
91
95
|
"communication_channels.profile.connect.notConfigured": "Este proveedor aún no está configurado. Pide a un administrador que añada el ID de cliente y el secreto de OAuth en Integraciones antes de conectar un buzón.",
|
|
92
96
|
"communication_channels.profile.connect.oauthFailed": "No se pudo iniciar la conexión OAuth.",
|
|
93
97
|
"communication_channels.profile.connect.save": "Conectar",
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
"communication_channels.profile.connect.connected": "채널이 연결되었습니다.",
|
|
72
72
|
"communication_channels.profile.connect.connecting": "연결 중...",
|
|
73
73
|
"communication_channels.profile.connect.credentialsFailed": "메일함에 연결할 수 없습니다.",
|
|
74
|
+
"communication_channels.profile.connect.errors.invalidEmail": "유효한 이메일 주소를 입력하세요.",
|
|
75
|
+
"communication_channels.profile.connect.errors.invalidPort": "1에서 65535 사이의 포트를 입력하세요.",
|
|
76
|
+
"communication_channels.profile.connect.errors.required": "필수 입력 항목입니다.",
|
|
74
77
|
"communication_channels.profile.connect.fields.displayName": "표시 이름",
|
|
75
78
|
"communication_channels.profile.connect.fields.fromAddress": "보내는 주소",
|
|
76
79
|
"communication_channels.profile.connect.fields.imapHost": "IMAP 호스트",
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
"communication_channels.profile.connect.imapTitle": "IMAP 메일함 연결",
|
|
89
92
|
"communication_channels.profile.connect.mailboxAlreadyConnected": "이 메일함은 이미 다른 제공업체를 통해 연결되어 있습니다. 다른 제공업체로 다시 연결하려면 먼저 연결을 해제하세요.",
|
|
90
93
|
"communication_channels.profile.connect.menu": "채널 연결",
|
|
94
|
+
"communication_channels.profile.connect.notAvailable": "메일함 연결을 사용할 수 없습니다. 관리자에게 OAuth 상태 키를 구성하도록 요청하세요.",
|
|
91
95
|
"communication_channels.profile.connect.notConfigured": "이 제공업체는 아직 구성되지 않았습니다. 메일함을 연결하기 전에 관리자에게 통합에서 OAuth 클라이언트 ID와 시크릿을 추가하도록 요청하세요.",
|
|
92
96
|
"communication_channels.profile.connect.oauthFailed": "OAuth 연결을 시작할 수 없습니다.",
|
|
93
97
|
"communication_channels.profile.connect.save": "연결",
|
|
@@ -71,6 +71,9 @@
|
|
|
71
71
|
"communication_channels.profile.connect.connected": "Kanał połączony.",
|
|
72
72
|
"communication_channels.profile.connect.connecting": "Łączenie...",
|
|
73
73
|
"communication_channels.profile.connect.credentialsFailed": "Nie udało się połączyć skrzynki pocztowej.",
|
|
74
|
+
"communication_channels.profile.connect.errors.invalidEmail": "Wprowadź prawidłowy adres e-mail.",
|
|
75
|
+
"communication_channels.profile.connect.errors.invalidPort": "Wprowadź port z zakresu od 1 do 65535.",
|
|
76
|
+
"communication_channels.profile.connect.errors.required": "To pole jest wymagane.",
|
|
74
77
|
"communication_channels.profile.connect.fields.displayName": "Nazwa wyświetlana",
|
|
75
78
|
"communication_channels.profile.connect.fields.fromAddress": "Adres nadawcy",
|
|
76
79
|
"communication_channels.profile.connect.fields.imapHost": "Host IMAP",
|
|
@@ -88,6 +91,7 @@
|
|
|
88
91
|
"communication_channels.profile.connect.imapTitle": "Połącz skrzynkę IMAP",
|
|
89
92
|
"communication_channels.profile.connect.mailboxAlreadyConnected": "Ta skrzynka jest już połączona przez innego dostawcę. Najpierw ją rozłącz, aby połączyć ją z innym dostawcą.",
|
|
90
93
|
"communication_channels.profile.connect.menu": "Połącz kanał",
|
|
94
|
+
"communication_channels.profile.connect.notAvailable": "Połączenie skrzynki pocztowej jest niedostępne. Poproś administratora o skonfigurowanie klucza stanu OAuth.",
|
|
91
95
|
"communication_channels.profile.connect.notConfigured": "Ten dostawca nie został jeszcze skonfigurowany. Poproś administratora o dodanie identyfikatora klienta i sekretu OAuth w sekcji Integracje przed połączeniem skrzynki.",
|
|
92
96
|
"communication_channels.profile.connect.oauthFailed": "Nie udało się rozpocząć połączenia OAuth.",
|
|
93
97
|
"communication_channels.profile.connect.save": "Połącz",
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
2
|
import { resolveCommunicationChannelsSystemUserId } from './system-user'
|
|
3
|
+
import { EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE } from '../../messages/lib/messageSourceEntityTypes'
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
* Entity type a channel-backed conversation is exposed under on a message's
|
|
6
|
-
* `sourceEntityType`. Written by `commands/ingest-inbound-message.ts` and
|
|
7
|
-
* inherited by every operator message composed on the same conversation.
|
|
8
|
-
*/
|
|
9
|
-
export const EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE = 'communication_channels.external_conversation'
|
|
5
|
+
export { EXTERNAL_CONVERSATION_SOURCE_ENTITY_TYPE }
|
|
10
6
|
|
|
11
7
|
/**
|
|
12
8
|
* Dedup key `ingest-inbound-message` stamps on the platform message it composes,
|
|
@@ -138,7 +138,10 @@ function getSecret(): string {
|
|
|
138
138
|
// state cookies, bypassing the userId/tenant binding. In non-production we fall
|
|
139
139
|
// back to JWT_SECRET so dev/test envs that only configure one secret still work.
|
|
140
140
|
if (process.env.NODE_ENV === 'production') {
|
|
141
|
-
throw new
|
|
141
|
+
throw new OAuthStateError(
|
|
142
|
+
'[internal] OM_HUB_OAUTH_STATE_KEY or KMS_MASTER_KEY required in production',
|
|
143
|
+
'missing_secret',
|
|
144
|
+
)
|
|
142
145
|
}
|
|
143
146
|
const fallback = process.env.JWT_SECRET
|
|
144
147
|
if (!fallback) {
|
|
@@ -46,6 +46,16 @@ export function useConnectChannel(options: {
|
|
|
46
46
|
})
|
|
47
47
|
const body = response.result as InitiateResponse | undefined
|
|
48
48
|
if (!response.ok || !body?.authorizeUrl) {
|
|
49
|
+
if (body?.code === 'missing_secret') {
|
|
50
|
+
flash(
|
|
51
|
+
t(
|
|
52
|
+
'communication_channels.profile.connect.notAvailable',
|
|
53
|
+
'Mailbox connection is not available. Ask an administrator to configure the OAuth state key.',
|
|
54
|
+
),
|
|
55
|
+
'error',
|
|
56
|
+
)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
49
59
|
if (body?.code === 'oauth_client_not_configured') {
|
|
50
60
|
flash(
|
|
51
61
|
t(
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
4
|
+
import { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
5
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
6
|
+
import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
|
|
7
|
+
import type { OrganizationHierarchyService } from '@open-mercato/shared/lib/auth/principal-service'
|
|
8
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
9
|
+
import { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'
|
|
10
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
|
+
import { resolveDictionaryRouteContext } from '../dictionaries/context'
|
|
12
|
+
import { createDictionaryCacheTags, DICTIONARY_CACHE_TTL_MS } from '../dictionaries/cache'
|
|
13
|
+
import { getInjectionRegistryVersion } from '@open-mercato/shared/modules/widgets/injection-loader'
|
|
14
|
+
import {
|
|
15
|
+
resolveScopedCalendarEventTypes,
|
|
16
|
+
} from '../../lib/calendar/eventTypeResolver'
|
|
17
|
+
import { calendarEventTypeBehaviorSchema } from '../../calendar-event-types'
|
|
18
|
+
import { getCalendarEventTypeRegistryVersion } from '../../calendar-event-types'
|
|
19
|
+
|
|
20
|
+
const logger = createLogger('customers')
|
|
21
|
+
|
|
22
|
+
function reportActivityTypeError(error: unknown, code: string, attributes?: Record<string, string>): void {
|
|
23
|
+
try {
|
|
24
|
+
getTelemetryRuntime()?.reportError(error, { module: 'customers', code, attributes })
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const querySchema = z.object({ organizationId: z.string().uuid().optional() })
|
|
29
|
+
const responseSchema = z.object({
|
|
30
|
+
fallbackKey: z.literal('meeting'),
|
|
31
|
+
items: z.array(z.object({
|
|
32
|
+
key: z.string(),
|
|
33
|
+
label: z.string(),
|
|
34
|
+
labelKey: z.string().optional(),
|
|
35
|
+
icon: z.string().nullable().optional(),
|
|
36
|
+
color: z.string().nullable().optional(),
|
|
37
|
+
behavior: calendarEventTypeBehaviorSchema,
|
|
38
|
+
selectable: z.boolean(),
|
|
39
|
+
panelKey: z.string().optional(),
|
|
40
|
+
source: z.string(),
|
|
41
|
+
provenance: z.record(z.string(), z.unknown()),
|
|
42
|
+
historical: z.boolean(),
|
|
43
|
+
fallbackReason: z.enum(['tombstoned', 'module-unavailable', 'unknown']).optional(),
|
|
44
|
+
adminConfigurable: z.boolean(),
|
|
45
|
+
isInherited: z.boolean(),
|
|
46
|
+
isLocalOverride: z.boolean(),
|
|
47
|
+
updatedAt: z.string().nullable(),
|
|
48
|
+
missingCustomFieldsetIds: z.array(z.string()),
|
|
49
|
+
inactiveDictionaryOverride: z.boolean().optional(),
|
|
50
|
+
})),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
export const metadata = {
|
|
54
|
+
GET: { requireAuth: true },
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function GET(req: Request) {
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(req.url)
|
|
60
|
+
const query = querySchema.parse({ organizationId: url.searchParams.get('organizationId') ?? undefined })
|
|
61
|
+
const context = await resolveDictionaryRouteContext(req, { selectedId: query.organizationId })
|
|
62
|
+
if (!context.organizationId) {
|
|
63
|
+
return NextResponse.json({ error: context.translate('customers.errors.organization_required', 'Organization context is required') }, { status: 400 })
|
|
64
|
+
}
|
|
65
|
+
const organizationId = context.organizationId
|
|
66
|
+
const rbac = context.container.resolve('rbacService') as RbacService | undefined
|
|
67
|
+
const actorId = context.auth?.sub
|
|
68
|
+
if (!rbac || !actorId) {
|
|
69
|
+
throw new CrudHttpError(403, { error: context.translate('customers.calendar.activityTypes.errors.forbidden', 'You do not have permission to view activity types.') })
|
|
70
|
+
}
|
|
71
|
+
const permissionScope = { tenantId: context.tenantId, organizationId }
|
|
72
|
+
const canViewInteractions = await rbac.userHasAllFeatures(actorId, ['customers.interactions.view'], permissionScope)
|
|
73
|
+
const canManageSettings = canViewInteractions || await rbac.userHasAllFeatures(actorId, ['customers.settings.manage'], permissionScope)
|
|
74
|
+
if (!canManageSettings) {
|
|
75
|
+
throw new CrudHttpError(403, { error: context.translate('customers.calendar.activityTypes.errors.forbidden', 'You do not have permission to view activity types.') })
|
|
76
|
+
}
|
|
77
|
+
const hierarchy = context.container.resolve('organizationHierarchyService') as OrganizationHierarchyService
|
|
78
|
+
const ancestors = await hierarchy.resolveAncestorIds({ tenantId: context.tenantId, organizationId })
|
|
79
|
+
if (ancestors === null) {
|
|
80
|
+
throw new CrudHttpError(403, { error: context.translate('customers.errors.organization_forbidden', 'Organization not accessible') })
|
|
81
|
+
}
|
|
82
|
+
const scopeIds = Array.from(new Set([organizationId, ...ancestors.slice().reverse()]))
|
|
83
|
+
const cacheKey = `customers:activity-types:${context.tenantId}:org=${organizationId}:scope=${scopeIds.join('|')}:widgets=${getInjectionRegistryVersion()}:registry=${getCalendarEventTypeRegistryVersion()}`
|
|
84
|
+
const cached = await context.cache?.get(cacheKey)
|
|
85
|
+
if (cached) return NextResponse.json(cached)
|
|
86
|
+
|
|
87
|
+
let catalog
|
|
88
|
+
try {
|
|
89
|
+
catalog = await resolveScopedCalendarEventTypes({
|
|
90
|
+
em: context.em,
|
|
91
|
+
tenantId: context.tenantId,
|
|
92
|
+
organizationId,
|
|
93
|
+
readableOrganizationIds: scopeIds,
|
|
94
|
+
})
|
|
95
|
+
} catch (err) {
|
|
96
|
+
logger.error('customers.activity_types.resolve failed', {
|
|
97
|
+
err,
|
|
98
|
+
tenantId: context.tenantId,
|
|
99
|
+
organizationId,
|
|
100
|
+
})
|
|
101
|
+
reportActivityTypeError(err, 'customers.activity_type_catalog_resolution_failed', {
|
|
102
|
+
tenantId: context.tenantId,
|
|
103
|
+
organizationId,
|
|
104
|
+
})
|
|
105
|
+
return NextResponse.json({
|
|
106
|
+
error: context.translate('customers.calendar.activityTypes.errors.loadFailed', 'Failed to load activity types'),
|
|
107
|
+
code: 'activity_type_catalog_unavailable',
|
|
108
|
+
}, { status: 503 })
|
|
109
|
+
}
|
|
110
|
+
if (context.cache) {
|
|
111
|
+
await context.cache.set(cacheKey, catalog, {
|
|
112
|
+
ttl: DICTIONARY_CACHE_TTL_MS,
|
|
113
|
+
tags: createDictionaryCacheTags({
|
|
114
|
+
tenantId: context.tenantId,
|
|
115
|
+
mappedKind: 'activity_type',
|
|
116
|
+
organizationIds: scopeIds,
|
|
117
|
+
}),
|
|
118
|
+
}).catch((err) => {
|
|
119
|
+
logger.warn('customers.activity_types cache write failed', { err })
|
|
120
|
+
reportActivityTypeError(err, 'customers.activity_type_catalog_cache_write_failed', {
|
|
121
|
+
tenantId: context.tenantId,
|
|
122
|
+
organizationId,
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
return NextResponse.json(catalog)
|
|
127
|
+
} catch (err) {
|
|
128
|
+
if (isCrudHttpError(err)) return NextResponse.json(err.body, { status: err.status })
|
|
129
|
+
logger.error('customers.activity_types request failed', { err })
|
|
130
|
+
reportActivityTypeError(err, 'customers.activity_type_catalog_request_failed')
|
|
131
|
+
const { translate } = await resolveTranslations()
|
|
132
|
+
return NextResponse.json({
|
|
133
|
+
error: translate('customers.calendar.activityTypes.errors.loadFailed', 'Failed to load activity types'),
|
|
134
|
+
}, { status: err instanceof z.ZodError ? 400 : 500 })
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const openApi: OpenApiRouteDoc = {
|
|
139
|
+
tag: 'Customers',
|
|
140
|
+
summary: 'Effective calendar activity types',
|
|
141
|
+
methods: {
|
|
142
|
+
GET: {
|
|
143
|
+
summary: 'List effective calendar activity types',
|
|
144
|
+
description: 'Returns the tenant and organization scoped calendar activity-type catalog. Requires customers.interactions.view or customers.settings.manage.',
|
|
145
|
+
responses: [{ status: 200, description: 'Effective activity-type catalog', schema: responseSchema }],
|
|
146
|
+
errors: [
|
|
147
|
+
{ status: 400, description: 'Organization scope is required', schema: z.object({ error: z.string() }) },
|
|
148
|
+
{ status: 401, description: 'Unauthorized', schema: z.object({ error: z.string() }) },
|
|
149
|
+
{ status: 403, description: 'Forbidden', schema: z.object({ error: z.string() }) },
|
|
150
|
+
{ status: 500, description: 'Unexpected server error', schema: z.object({ error: z.string() }) },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
@@ -10,7 +10,12 @@ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
|
10
10
|
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
11
11
|
import { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
12
12
|
import { isOrganizationReadAccessAllowed } from '@open-mercato/core/modules/directory/utils/organizationScopeGuard'
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
CustomerDeal,
|
|
15
|
+
CustomerDealPersonLink,
|
|
16
|
+
CustomerEntity,
|
|
17
|
+
CustomerPersonProfile,
|
|
18
|
+
} from '../../../../data/entities'
|
|
14
19
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
15
20
|
|
|
16
21
|
const logger = createLogger('customers')
|
|
@@ -41,12 +46,36 @@ type DealPersonItem = {
|
|
|
41
46
|
kind: 'person'
|
|
42
47
|
linkedAt: string
|
|
43
48
|
isPrimary: boolean
|
|
49
|
+
// Added for the shared linked-people card, which renders the same shape the company
|
|
50
|
+
// endpoint already returns. Purely additive: every key above keeps its meaning, so
|
|
51
|
+
// `DealLinkedEntitiesTab` and any third-party consumer are unaffected.
|
|
52
|
+
displayName: string
|
|
53
|
+
primaryEmail: string | null
|
|
54
|
+
primaryPhone: string | null
|
|
55
|
+
status: string | null
|
|
56
|
+
lifecycleStage: string | null
|
|
57
|
+
jobTitle: string | null
|
|
58
|
+
department: string | null
|
|
59
|
+
createdAt: string
|
|
60
|
+
organizationId: string
|
|
61
|
+
temperature: string | null
|
|
62
|
+
source: string | null
|
|
44
63
|
}
|
|
45
64
|
|
|
46
65
|
function matchesSearch(item: DealPersonItem, query: string): boolean {
|
|
47
66
|
const normalized = query.trim().toLowerCase()
|
|
48
67
|
if (!normalized.length) return true
|
|
49
|
-
return [
|
|
68
|
+
return [
|
|
69
|
+
item.label,
|
|
70
|
+
item.subtitle,
|
|
71
|
+
item.primaryEmail,
|
|
72
|
+
item.primaryPhone,
|
|
73
|
+
item.jobTitle,
|
|
74
|
+
item.department,
|
|
75
|
+
item.status,
|
|
76
|
+
item.lifecycleStage,
|
|
77
|
+
item.source,
|
|
78
|
+
]
|
|
50
79
|
.filter((value): value is string => typeof value === 'string' && value.length > 0)
|
|
51
80
|
.some((value) => value.toLowerCase().includes(normalized))
|
|
52
81
|
}
|
|
@@ -119,17 +148,56 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
119
148
|
entityScope,
|
|
120
149
|
)
|
|
121
150
|
|
|
151
|
+
// One batched read for the profile-only columns, keyed by every linked person — not one
|
|
152
|
+
// query per row. The handler filters, sorts and paginates in memory below, so the batch
|
|
153
|
+
// covers the whole link set rather than the page; that is fine at the expected scale of
|
|
154
|
+
// tens of contacts per deal, and the in-memory pipeline is what to revisit first if it
|
|
155
|
+
// ever is not.
|
|
156
|
+
const personIds = links
|
|
157
|
+
.map((link) => (link.person as CustomerEntity | null)?.id)
|
|
158
|
+
.filter((personId): personId is string => typeof personId === 'string' && personId.length > 0)
|
|
159
|
+
|
|
160
|
+
const profiles = personIds.length > 0
|
|
161
|
+
? await findWithDecryption(
|
|
162
|
+
em,
|
|
163
|
+
CustomerPersonProfile,
|
|
164
|
+
{
|
|
165
|
+
entity: { $in: personIds },
|
|
166
|
+
tenantId: deal.tenantId,
|
|
167
|
+
organizationId: deal.organizationId,
|
|
168
|
+
},
|
|
169
|
+
{},
|
|
170
|
+
entityScope,
|
|
171
|
+
)
|
|
172
|
+
: []
|
|
173
|
+
const profileByPersonId = new Map(
|
|
174
|
+
profiles.map((profile) => [(profile.entity as { id: string }).id, profile]),
|
|
175
|
+
)
|
|
176
|
+
|
|
122
177
|
const items = links
|
|
123
178
|
.map((link) => {
|
|
124
179
|
const person = link.person as CustomerEntity | null
|
|
125
180
|
if (!person?.id) return null
|
|
181
|
+
const profile = profileByPersonId.get(person.id) ?? null
|
|
182
|
+
const displayName = person.displayName ?? person.primaryEmail ?? person.id
|
|
126
183
|
return {
|
|
127
184
|
id: person.id,
|
|
128
|
-
label:
|
|
185
|
+
label: displayName,
|
|
129
186
|
subtitle: person.primaryEmail ?? person.primaryPhone ?? null,
|
|
130
187
|
kind: 'person',
|
|
131
188
|
linkedAt: link.createdAt.toISOString(),
|
|
132
189
|
isPrimary: link.isPrimary === true,
|
|
190
|
+
displayName,
|
|
191
|
+
primaryEmail: person.primaryEmail ?? null,
|
|
192
|
+
primaryPhone: person.primaryPhone ?? null,
|
|
193
|
+
status: person.status ?? null,
|
|
194
|
+
lifecycleStage: person.lifecycleStage ?? null,
|
|
195
|
+
jobTitle: profile?.jobTitle ?? null,
|
|
196
|
+
department: profile?.department ?? null,
|
|
197
|
+
createdAt: person.createdAt.toISOString(),
|
|
198
|
+
organizationId: person.organizationId,
|
|
199
|
+
temperature: person.temperature ?? null,
|
|
200
|
+
source: person.source ?? null,
|
|
133
201
|
} satisfies DealPersonItem
|
|
134
202
|
})
|
|
135
203
|
.filter((item): item is DealPersonItem => item !== null)
|
|
@@ -176,6 +244,17 @@ export const openApi: OpenApiRouteDoc = {
|
|
|
176
244
|
kind: z.literal('person'),
|
|
177
245
|
linkedAt: z.string(),
|
|
178
246
|
isPrimary: z.boolean(),
|
|
247
|
+
displayName: z.string(),
|
|
248
|
+
primaryEmail: z.string().nullable(),
|
|
249
|
+
primaryPhone: z.string().nullable(),
|
|
250
|
+
status: z.string().nullable(),
|
|
251
|
+
lifecycleStage: z.string().nullable(),
|
|
252
|
+
jobTitle: z.string().nullable(),
|
|
253
|
+
department: z.string().nullable(),
|
|
254
|
+
createdAt: z.string(),
|
|
255
|
+
organizationId: z.string().uuid().nullable(),
|
|
256
|
+
temperature: z.string().nullable(),
|
|
257
|
+
source: z.string().nullable(),
|
|
179
258
|
}),
|
|
180
259
|
),
|
|
181
260
|
total: z.number().int().nonnegative(),
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from '@open-mercato/shared/lib/crud/mutation-guard'
|
|
18
18
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
19
19
|
import { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'
|
|
20
|
+
import { calendarEventTypeBehaviorSchema } from '../../../../calendar-event-types'
|
|
20
21
|
|
|
21
22
|
const logger = createLogger('customers')
|
|
22
23
|
|
|
@@ -30,8 +31,9 @@ const patchSchema = z
|
|
|
30
31
|
label: z.string().trim().max(150).optional(),
|
|
31
32
|
color: z.union([z.string().trim(), z.null()]).optional(),
|
|
32
33
|
icon: z.union([z.string().trim(), z.null()]).optional(),
|
|
34
|
+
behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
|
|
33
35
|
})
|
|
34
|
-
.refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined, {
|
|
36
|
+
.refine((input) => input.value !== undefined || input.label !== undefined || input.color !== undefined || input.icon !== undefined || input.behavior !== undefined, {
|
|
35
37
|
message: 'No changes provided',
|
|
36
38
|
})
|
|
37
39
|
|
|
@@ -81,6 +83,7 @@ export async function PATCH(req: Request, ctx: { params?: { kind?: string; id?:
|
|
|
81
83
|
label: payload.label,
|
|
82
84
|
color: payload.color,
|
|
83
85
|
icon: payload.icon,
|
|
86
|
+
behavior: payload.behavior,
|
|
84
87
|
},
|
|
85
88
|
ctx: routeContext.ctx,
|
|
86
89
|
})) as CommandExecuteResult<{ entryId: string; changed: boolean }>
|
|
@@ -103,6 +106,7 @@ export async function PATCH(req: Request, ctx: { params?: { kind?: string; id?:
|
|
|
103
106
|
),
|
|
104
107
|
})
|
|
105
108
|
}
|
|
109
|
+
if (typeof err.body.code === 'string') throw err
|
|
106
110
|
throw new CrudHttpError(409, { error: routeContext.translate('customers.config.dictionaries.errors.duplicate', 'An entry with this value already exists') })
|
|
107
111
|
}
|
|
108
112
|
if (err.status === 400) {
|
|
@@ -141,8 +145,10 @@ export async function PATCH(req: Request, ctx: { params?: { kind?: string; id?:
|
|
|
141
145
|
label: entry.label,
|
|
142
146
|
color: entry.color,
|
|
143
147
|
icon: entry.icon,
|
|
148
|
+
...(mappedKind === 'activity_type' ? { behavior: entry.activityTypeBehavior ?? null } : {}),
|
|
144
149
|
organizationId: entry.organizationId,
|
|
145
150
|
isInherited: false,
|
|
151
|
+
updatedAt: entry.updatedAt,
|
|
146
152
|
})
|
|
147
153
|
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
148
154
|
response.headers.set(
|
|
@@ -297,8 +303,10 @@ const dictionaryEntrySchema = z.object({
|
|
|
297
303
|
label: z.string().nullable().optional(),
|
|
298
304
|
color: z.string().nullable().optional(),
|
|
299
305
|
icon: z.string().nullable().optional(),
|
|
306
|
+
behavior: calendarEventTypeBehaviorSchema.nullable().optional(),
|
|
300
307
|
organizationId: z.string().uuid().nullable().optional(),
|
|
301
308
|
isInherited: z.boolean().optional(),
|
|
309
|
+
updatedAt: z.date().or(z.string()).optional(),
|
|
302
310
|
})
|
|
303
311
|
|
|
304
312
|
const dictionaryDeleteResponseSchema = z.object({
|