@open-mercato/core 0.7.1-develop.7149.1.7efa6e1612 → 0.7.1-develop.7151.1.00d0391847
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/helpers/integration/communicationChannelsFixtures.js +77 -1
- package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
- package/dist/helpers/integration/crmFixtures.js +3 -0
- package/dist/helpers/integration/crmFixtures.js.map +2 -2
- package/dist/modules/auth/api/reset.js +7 -1
- package/dist/modules/auth/api/reset.js.map +2 -2
- package/dist/modules/auth/api/users/resend-invite/route.js +7 -1
- package/dist/modules/auth/api/users/resend-invite/route.js.map +2 -2
- package/dist/modules/auth/commands/users.js +7 -1
- package/dist/modules/auth/commands/users.js.map +2 -2
- package/dist/modules/communication_channels/api/post/test-seed/route.js +101 -26
- package/dist/modules/communication_channels/api/post/test-seed/route.js.map +3 -3
- package/dist/modules/communication_channels/di.js +8 -0
- package/dist/modules/communication_channels/di.js.map +2 -2
- package/dist/modules/communication_channels/lib/ensure-system-email-channel.js +57 -0
- package/dist/modules/communication_channels/lib/ensure-system-email-channel.js.map +7 -0
- package/dist/modules/communication_channels/lib/system-email-provider-config.js +32 -0
- package/dist/modules/communication_channels/lib/system-email-provider-config.js.map +7 -0
- package/dist/modules/communication_channels/lib/system-email.js +190 -0
- package/dist/modules/communication_channels/lib/system-email.js.map +7 -0
- package/dist/modules/communication_channels/lib/test-seed.js +130 -1
- package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
- package/dist/modules/communication_channels/setup.js +56 -1
- package/dist/modules/communication_channels/setup.js.map +2 -2
- package/dist/modules/customer_accounts/api/admin/users-invite.js +1 -0
- package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/portal/users-invite.js +1 -0
- package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
- package/dist/modules/customer_accounts/api/signup.js +8 -4
- package/dist/modules/customer_accounts/api/signup.js.map +2 -2
- package/dist/modules/customer_accounts/lib/invitationEmail.js +3 -1
- package/dist/modules/customer_accounts/lib/invitationEmail.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/commands/tags.js +8 -2
- package/dist/modules/customers/commands/tags.js.map +2 -2
- package/dist/modules/messages/lib/email-sender.js +16 -14
- package/dist/modules/messages/lib/email-sender.js.map +2 -2
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +3 -1
- package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +2 -2
- package/dist/modules/sales/api/quotes/accept/route.js +3 -1
- package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
- package/dist/modules/sales/api/quotes/send/route.js +13 -4
- package/dist/modules/sales/api/quotes/send/route.js.map +3 -3
- package/dist/modules/staff/commands/timesheets-entries.js +4 -2
- package/dist/modules/staff/commands/timesheets-entries.js.map +2 -2
- package/dist/modules/staff/commands/timesheets-projects.js +4 -2
- package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
- package/package.json +7 -7
- package/src/helpers/integration/communicationChannelsFixtures.ts +129 -0
- package/src/helpers/integration/crmFixtures.ts +4 -1
- package/src/modules/auth/api/reset.ts +7 -1
- package/src/modules/auth/api/users/resend-invite/route.ts +7 -1
- package/src/modules/auth/commands/users.ts +7 -1
- package/src/modules/auth/i18n/de.json +4 -0
- package/src/modules/auth/i18n/en.json +4 -0
- package/src/modules/auth/i18n/es.json +4 -0
- package/src/modules/auth/i18n/ko.json +4 -0
- package/src/modules/auth/i18n/pl.json +4 -0
- package/src/modules/communication_channels/api/post/test-seed/route.ts +121 -29
- package/src/modules/communication_channels/di.ts +8 -0
- package/src/modules/communication_channels/lib/ensure-system-email-channel.ts +85 -0
- package/src/modules/communication_channels/lib/system-email-provider-config.ts +62 -0
- package/src/modules/communication_channels/lib/system-email.ts +316 -0
- package/src/modules/communication_channels/lib/test-seed.ts +198 -0
- package/src/modules/communication_channels/setup.ts +91 -1
- package/src/modules/customer_accounts/api/admin/users-invite.ts +1 -0
- package/src/modules/customer_accounts/api/portal/users-invite.ts +1 -0
- package/src/modules/customer_accounts/api/signup.ts +6 -2
- package/src/modules/customer_accounts/lib/invitationEmail.ts +3 -0
- package/src/modules/customers/api/interactions/route.ts +4 -0
- package/src/modules/customers/commands/tags.ts +12 -1
- package/src/modules/messages/lib/email-sender.ts +18 -16
- package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +3 -1
- package/src/modules/sales/api/quotes/accept/route.ts +2 -0
- package/src/modules/sales/api/quotes/send/route.ts +16 -3
- package/src/modules/staff/commands/timesheets-entries.ts +2 -0
- package/src/modules/staff/commands/timesheets-projects.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.7.1-develop.
|
|
3
|
+
"version": "0.7.1-develop.7151.1.00d0391847",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -252,16 +252,16 @@
|
|
|
252
252
|
"zod": "^4.4.3"
|
|
253
253
|
},
|
|
254
254
|
"peerDependencies": {
|
|
255
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
256
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
257
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
255
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7151.1.00d0391847",
|
|
256
|
+
"@open-mercato/shared": "0.7.1-develop.7151.1.00d0391847",
|
|
257
|
+
"@open-mercato/ui": "0.7.1-develop.7151.1.00d0391847",
|
|
258
258
|
"react": "^19.0.0",
|
|
259
259
|
"react-dom": "^19.0.0"
|
|
260
260
|
},
|
|
261
261
|
"devDependencies": {
|
|
262
|
-
"@open-mercato/ai-assistant": "0.7.1-develop.
|
|
263
|
-
"@open-mercato/shared": "0.7.1-develop.
|
|
264
|
-
"@open-mercato/ui": "0.7.1-develop.
|
|
262
|
+
"@open-mercato/ai-assistant": "0.7.1-develop.7151.1.00d0391847",
|
|
263
|
+
"@open-mercato/shared": "0.7.1-develop.7151.1.00d0391847",
|
|
264
|
+
"@open-mercato/ui": "0.7.1-develop.7151.1.00d0391847",
|
|
265
265
|
"@testing-library/dom": "^10.4.1",
|
|
266
266
|
"@testing-library/jest-dom": "^7.0.0",
|
|
267
267
|
"@testing-library/react": "^16.3.1",
|
|
@@ -16,8 +16,39 @@ export type SeedAddressField =
|
|
|
16
16
|
| { address: string; name?: string }
|
|
17
17
|
| Array<string | { address: string; name?: string }>;
|
|
18
18
|
|
|
19
|
+
export type CapturedTestSeedEmail = {
|
|
20
|
+
capturedAt: string;
|
|
21
|
+
externalMessageId: string;
|
|
22
|
+
conversationId?: string;
|
|
23
|
+
content: {
|
|
24
|
+
text?: string;
|
|
25
|
+
html?: string;
|
|
26
|
+
bodyFormat?: 'text' | 'markdown' | 'html';
|
|
27
|
+
attachments?: Array<{ url: string; mimeType: string; fileName: string; fileSize?: number; inline?: boolean }>;
|
|
28
|
+
raw?: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
credentials: Record<string, unknown>;
|
|
31
|
+
scope: {
|
|
32
|
+
tenantId: string;
|
|
33
|
+
organizationId: string;
|
|
34
|
+
};
|
|
35
|
+
metadata?: Record<string, unknown>;
|
|
36
|
+
};
|
|
37
|
+
|
|
19
38
|
const TEST_SEED_PATH = '/api/communication_channels/test-seed';
|
|
20
39
|
|
|
40
|
+
function resolveSystemCaptureCredentials(): {
|
|
41
|
+
accessToken: string;
|
|
42
|
+
captureCorrelationToken: string;
|
|
43
|
+
} {
|
|
44
|
+
const accessToken = process.env.OM_TEST_EMAIL_CAPTURE_ACCESS_TOKEN?.trim();
|
|
45
|
+
const captureCorrelationToken = process.env.OM_TEST_EMAIL_CAPTURE_CORRELATION_TOKEN?.trim();
|
|
46
|
+
if (!accessToken || !captureCorrelationToken) {
|
|
47
|
+
throw new Error('[internal] System email capture credentials are not configured by the integration harness');
|
|
48
|
+
}
|
|
49
|
+
return { accessToken, captureCorrelationToken };
|
|
50
|
+
}
|
|
51
|
+
|
|
21
52
|
/**
|
|
22
53
|
* Probe whether the env-gated test-seed endpoint is enabled in the target app.
|
|
23
54
|
* Returns false when the route answers 404 (flag off) so callers can `test.skip`.
|
|
@@ -80,6 +111,104 @@ export async function seedConnectedChannel(
|
|
|
80
111
|
return expectId(body?.channelId, 'connect-channel response should include channelId');
|
|
81
112
|
}
|
|
82
113
|
|
|
114
|
+
export async function seedSystemEmailChannel(
|
|
115
|
+
request: APIRequestContext,
|
|
116
|
+
token: string,
|
|
117
|
+
input: { displayName?: string; externalIdentifier?: string } = {},
|
|
118
|
+
): Promise<string> {
|
|
119
|
+
const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {
|
|
120
|
+
token,
|
|
121
|
+
data: { action: 'seed-system-channel', ...input },
|
|
122
|
+
});
|
|
123
|
+
expect(
|
|
124
|
+
response.status(),
|
|
125
|
+
'POST /api/communication_channels/test-seed (seed-system-channel) should return 201',
|
|
126
|
+
).toBe(201);
|
|
127
|
+
const body = await readJsonSafe<{ channelId?: string }>(response);
|
|
128
|
+
return expectId(body?.channelId, 'seed-system-channel response should include channelId');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function clearCapturedSystemEmails(
|
|
132
|
+
request: APIRequestContext,
|
|
133
|
+
token: string,
|
|
134
|
+
options: { systemRecipient?: string } = {},
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const captureCredentials = options.systemRecipient ? resolveSystemCaptureCredentials() : null;
|
|
137
|
+
const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {
|
|
138
|
+
token,
|
|
139
|
+
data: {
|
|
140
|
+
action: 'clear-capture',
|
|
141
|
+
...options,
|
|
142
|
+
...(captureCredentials
|
|
143
|
+
? { captureCorrelationToken: captureCredentials.captureCorrelationToken }
|
|
144
|
+
: {}),
|
|
145
|
+
},
|
|
146
|
+
headers: captureCredentials
|
|
147
|
+
? { 'x-om-test-email-capture-access-token': captureCredentials.accessToken }
|
|
148
|
+
: undefined,
|
|
149
|
+
});
|
|
150
|
+
expect(response.ok(), 'POST /api/communication_channels/test-seed (clear-capture) should succeed').toBeTruthy();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function listCapturedSystemEmails(
|
|
154
|
+
request: APIRequestContext,
|
|
155
|
+
token: string,
|
|
156
|
+
options: { systemRecipient?: string } = {},
|
|
157
|
+
): Promise<CapturedTestSeedEmail[]> {
|
|
158
|
+
const captureCredentials = options.systemRecipient ? resolveSystemCaptureCredentials() : null;
|
|
159
|
+
const response = await apiRequest(request, 'POST', TEST_SEED_PATH, {
|
|
160
|
+
token,
|
|
161
|
+
data: {
|
|
162
|
+
action: 'list-capture',
|
|
163
|
+
...options,
|
|
164
|
+
...(captureCredentials
|
|
165
|
+
? { captureCorrelationToken: captureCredentials.captureCorrelationToken }
|
|
166
|
+
: {}),
|
|
167
|
+
},
|
|
168
|
+
headers: captureCredentials
|
|
169
|
+
? { 'x-om-test-email-capture-access-token': captureCredentials.accessToken }
|
|
170
|
+
: undefined,
|
|
171
|
+
});
|
|
172
|
+
expect(response.ok(), 'POST /api/communication_channels/test-seed (list-capture) should succeed').toBeTruthy();
|
|
173
|
+
const body = await readJsonSafe<{ items?: CapturedTestSeedEmail[] }>(response);
|
|
174
|
+
return Array.isArray(body?.items) ? body.items : [];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export async function waitForCapturedSystemEmail(
|
|
178
|
+
request: APIRequestContext,
|
|
179
|
+
token: string,
|
|
180
|
+
predicate: (email: CapturedTestSeedEmail) => boolean,
|
|
181
|
+
options: {
|
|
182
|
+
timeoutMs?: number;
|
|
183
|
+
intervalMs?: number;
|
|
184
|
+
description?: string;
|
|
185
|
+
systemRecipient?: string;
|
|
186
|
+
} = {},
|
|
187
|
+
): Promise<CapturedTestSeedEmail> {
|
|
188
|
+
const timeoutMs = options.timeoutMs ?? 20_000;
|
|
189
|
+
const intervalMs = options.intervalMs ?? 1_000;
|
|
190
|
+
const deadline = Date.now() + timeoutMs;
|
|
191
|
+
let lastItems: CapturedTestSeedEmail[] = [];
|
|
192
|
+
while (Date.now() < deadline) {
|
|
193
|
+
lastItems = await listCapturedSystemEmails(request, token, {
|
|
194
|
+
systemRecipient: options.systemRecipient,
|
|
195
|
+
});
|
|
196
|
+
const found = lastItems.find(predicate);
|
|
197
|
+
if (found) return found;
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
199
|
+
}
|
|
200
|
+
const summary = lastItems.map((item) => ({
|
|
201
|
+
to: item.metadata?.to,
|
|
202
|
+
from: item.metadata?.from,
|
|
203
|
+
subject: item.metadata?.subject,
|
|
204
|
+
scope: item.scope,
|
|
205
|
+
bodyFormat: item.content?.bodyFormat,
|
|
206
|
+
}));
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Timed out waiting for captured system email${options.description ? `: ${options.description}` : ''}. Captured ${lastItems.length} message(s): ${JSON.stringify(summary)}`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
83
212
|
/**
|
|
84
213
|
* Seed an inbound `MessageChannelLink` for `channelId` and emit
|
|
85
214
|
* `communication_channels.message.received` through the real event bus. The
|
|
@@ -55,7 +55,7 @@ export async function createCompanyFixture(
|
|
|
55
55
|
export async function createPersonFixture(
|
|
56
56
|
request: APIRequestContext,
|
|
57
57
|
token: string,
|
|
58
|
-
input: { firstName: string; lastName: string; displayName: string; companyEntityId?: string },
|
|
58
|
+
input: { firstName: string; lastName: string; displayName: string; companyEntityId?: string; primaryEmail?: string },
|
|
59
59
|
): Promise<string> {
|
|
60
60
|
const data: Record<string, unknown> = {
|
|
61
61
|
firstName: input.firstName,
|
|
@@ -65,6 +65,9 @@ export async function createPersonFixture(
|
|
|
65
65
|
if (input.companyEntityId) {
|
|
66
66
|
data.companyEntityId = input.companyEntityId;
|
|
67
67
|
}
|
|
68
|
+
if (input.primaryEmail) {
|
|
69
|
+
data.primaryEmail = input.primaryEmail;
|
|
70
|
+
}
|
|
68
71
|
return createEntity(request, token, '/api/customers/people', data, ['id', 'entityId', 'personId']);
|
|
69
72
|
}
|
|
70
73
|
|
|
@@ -76,7 +76,13 @@ export async function POST(req: Request) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
try {
|
|
79
|
-
await sendEmail({
|
|
79
|
+
await sendEmail({
|
|
80
|
+
to: user.email,
|
|
81
|
+
subject,
|
|
82
|
+
react: ResetPasswordEmail({ resetUrl, copy }),
|
|
83
|
+
tenantId: user.tenantId ? String(user.tenantId) : undefined,
|
|
84
|
+
organizationId: user.organizationId ? String(user.organizationId) : null,
|
|
85
|
+
})
|
|
80
86
|
} catch (err) {
|
|
81
87
|
logger.error('Failed to send reset email', { err })
|
|
82
88
|
}
|
|
@@ -175,7 +175,13 @@ export async function POST(req: Request) {
|
|
|
175
175
|
|
|
176
176
|
let emailSent = true
|
|
177
177
|
try {
|
|
178
|
-
await sendEmail({
|
|
178
|
+
await sendEmail({
|
|
179
|
+
to: user.email,
|
|
180
|
+
subject,
|
|
181
|
+
react: InviteUserEmail({ inviteUrl, copy }),
|
|
182
|
+
tenantId: user.tenantId ? String(user.tenantId) : auth.tenantId ?? undefined,
|
|
183
|
+
organizationId: user.organizationId ? String(user.organizationId) : null,
|
|
184
|
+
})
|
|
179
185
|
} catch (err) {
|
|
180
186
|
logger.error('Failed to send invitation email', { err })
|
|
181
187
|
emailSent = false
|
|
@@ -504,7 +504,13 @@ async function sendInviteToUser(
|
|
|
504
504
|
|
|
505
505
|
let emailSent = true
|
|
506
506
|
try {
|
|
507
|
-
await sendEmail({
|
|
507
|
+
await sendEmail({
|
|
508
|
+
to: user.email,
|
|
509
|
+
subject,
|
|
510
|
+
react: InviteUserEmail({ inviteUrl, copy }),
|
|
511
|
+
tenantId: user.tenantId ? String(user.tenantId) : undefined,
|
|
512
|
+
organizationId: user.organizationId ? String(user.organizationId) : null,
|
|
513
|
+
})
|
|
508
514
|
} catch (err) {
|
|
509
515
|
logger.error('Failed to send invitation email', { err })
|
|
510
516
|
emailSent = false
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
"auth.acl.features.channel_gmail.view": "View Gmail channel configuration",
|
|
67
67
|
"auth.acl.features.channel_imap.configure": "Configure IMAP channel defaults",
|
|
68
68
|
"auth.acl.features.channel_imap.view": "View IMAP channel configuration",
|
|
69
|
+
"auth.acl.features.channel_resend.configure": "Configure Resend channel defaults",
|
|
70
|
+
"auth.acl.features.channel_resend.view": "View Resend channel configuration",
|
|
71
|
+
"auth.acl.features.channel_ses.configure": "Configure Amazon SES channel defaults",
|
|
72
|
+
"auth.acl.features.channel_ses.view": "View Amazon SES channel configuration",
|
|
69
73
|
"auth.acl.features.checkout.create": "Create checkout links and templates",
|
|
70
74
|
"auth.acl.features.checkout.delete": "Delete checkout links and templates",
|
|
71
75
|
"auth.acl.features.checkout.edit": "Edit checkout links and templates",
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
"auth.acl.features.channel_gmail.view": "View Gmail channel configuration",
|
|
67
67
|
"auth.acl.features.channel_imap.configure": "Configure IMAP channel defaults",
|
|
68
68
|
"auth.acl.features.channel_imap.view": "View IMAP channel configuration",
|
|
69
|
+
"auth.acl.features.channel_resend.configure": "Configure Resend channel defaults",
|
|
70
|
+
"auth.acl.features.channel_resend.view": "View Resend channel configuration",
|
|
71
|
+
"auth.acl.features.channel_ses.configure": "Configure Amazon SES channel defaults",
|
|
72
|
+
"auth.acl.features.channel_ses.view": "View Amazon SES channel configuration",
|
|
69
73
|
"auth.acl.features.checkout.create": "Create checkout links and templates",
|
|
70
74
|
"auth.acl.features.checkout.delete": "Delete checkout links and templates",
|
|
71
75
|
"auth.acl.features.checkout.edit": "Edit checkout links and templates",
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
"auth.acl.features.channel_gmail.view": "View Gmail channel configuration",
|
|
67
67
|
"auth.acl.features.channel_imap.configure": "Configure IMAP channel defaults",
|
|
68
68
|
"auth.acl.features.channel_imap.view": "View IMAP channel configuration",
|
|
69
|
+
"auth.acl.features.channel_resend.configure": "Configure Resend channel defaults",
|
|
70
|
+
"auth.acl.features.channel_resend.view": "View Resend channel configuration",
|
|
71
|
+
"auth.acl.features.channel_ses.configure": "Configure Amazon SES channel defaults",
|
|
72
|
+
"auth.acl.features.channel_ses.view": "View Amazon SES channel configuration",
|
|
69
73
|
"auth.acl.features.checkout.create": "Create checkout links and templates",
|
|
70
74
|
"auth.acl.features.checkout.delete": "Delete checkout links and templates",
|
|
71
75
|
"auth.acl.features.checkout.edit": "Edit checkout links and templates",
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
"auth.acl.features.channel_gmail.view": "View Gmail channel configuration",
|
|
67
67
|
"auth.acl.features.channel_imap.configure": "Configure IMAP channel defaults",
|
|
68
68
|
"auth.acl.features.channel_imap.view": "View IMAP channel configuration",
|
|
69
|
+
"auth.acl.features.channel_resend.configure": "Configure Resend channel defaults",
|
|
70
|
+
"auth.acl.features.channel_resend.view": "View Resend channel configuration",
|
|
71
|
+
"auth.acl.features.channel_ses.configure": "Configure Amazon SES channel defaults",
|
|
72
|
+
"auth.acl.features.channel_ses.view": "View Amazon SES channel configuration",
|
|
69
73
|
"auth.acl.features.checkout.create": "Create checkout links and templates",
|
|
70
74
|
"auth.acl.features.checkout.delete": "Delete checkout links and templates",
|
|
71
75
|
"auth.acl.features.checkout.edit": "Edit checkout links and templates",
|
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
"auth.acl.features.channel_gmail.view": "Wyświetl konfigurację kanału Gmail",
|
|
67
67
|
"auth.acl.features.channel_imap.configure": "Konfiguruj domyślne ustawienia kanału IMAP",
|
|
68
68
|
"auth.acl.features.channel_imap.view": "Wyświetl konfigurację kanału IMAP",
|
|
69
|
+
"auth.acl.features.channel_resend.configure": "Konfiguruj domyślne ustawienia kanału Resend",
|
|
70
|
+
"auth.acl.features.channel_resend.view": "Wyświetl konfigurację kanału Resend",
|
|
71
|
+
"auth.acl.features.channel_ses.configure": "Konfiguruj domyślne ustawienia kanału Amazon SES",
|
|
72
|
+
"auth.acl.features.channel_ses.view": "Wyświetl konfigurację kanału Amazon SES",
|
|
69
73
|
"auth.acl.features.checkout.create": "Twórz linki i szablony płatności",
|
|
70
74
|
"auth.acl.features.checkout.delete": "Usuwaj linki i szablony płatności",
|
|
71
75
|
"auth.acl.features.checkout.edit": "Edytuj linki i szablony płatności",
|
|
@@ -22,8 +22,12 @@ import { emitCommunicationChannelsEvent } from '../../../events'
|
|
|
22
22
|
import {
|
|
23
23
|
TEST_SEED_CHAT_PROVIDER_KEY,
|
|
24
24
|
TEST_SEED_PROVIDER_KEY,
|
|
25
|
+
clearTestSeedCapturedMessages,
|
|
26
|
+
createTestSeedPlatformMessage,
|
|
25
27
|
ensureTestSeedAdapterRegistered,
|
|
28
|
+
isTestEmailCaptureAccessAuthorized,
|
|
26
29
|
isTestChannelSeedingEnabled,
|
|
30
|
+
listTestSeedCapturedMessages,
|
|
27
31
|
} from '../../../lib/test-seed'
|
|
28
32
|
import {
|
|
29
33
|
COMMUNICATION_CHANNELS_INGEST_INBOUND_COMMAND_ID,
|
|
@@ -135,6 +139,24 @@ const ingestInboundSchema = z.object({
|
|
|
135
139
|
externalConversationId: z.string().min(1).max(255),
|
|
136
140
|
})
|
|
137
141
|
|
|
142
|
+
const seedSystemChannelSchema = z.object({
|
|
143
|
+
action: z.literal('seed-system-channel'),
|
|
144
|
+
displayName: z.string().min(1).max(255).optional(),
|
|
145
|
+
externalIdentifier: z.string().min(1).max(255).optional(),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
const clearCaptureSchema = z.object({
|
|
149
|
+
action: z.literal('clear-capture'),
|
|
150
|
+
systemRecipient: z.string().email().max(320).optional(),
|
|
151
|
+
captureCorrelationToken: z.string().min(32).max(512).optional(),
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const listCaptureSchema = z.object({
|
|
155
|
+
action: z.literal('list-capture'),
|
|
156
|
+
systemRecipient: z.string().email().max(320).optional(),
|
|
157
|
+
captureCorrelationToken: z.string().min(32).max(512).optional(),
|
|
158
|
+
})
|
|
159
|
+
|
|
138
160
|
const emitInboundSchema = z.object({
|
|
139
161
|
action: z.literal('emit-inbound'),
|
|
140
162
|
/** Channel that owns the inbound message; controls authorUserId + default visibility. */
|
|
@@ -172,6 +194,9 @@ const bodySchema = z.discriminatedUnion('action', [
|
|
|
172
194
|
connectChannelSchema,
|
|
173
195
|
ingestInboundSchema,
|
|
174
196
|
emitInboundSchema,
|
|
197
|
+
seedSystemChannelSchema,
|
|
198
|
+
clearCaptureSchema,
|
|
199
|
+
listCaptureSchema,
|
|
175
200
|
])
|
|
176
201
|
|
|
177
202
|
export async function POST(req: Request): Promise<Response> {
|
|
@@ -196,14 +221,97 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
196
221
|
)
|
|
197
222
|
}
|
|
198
223
|
|
|
224
|
+
const tenantId = auth.tenantId as string
|
|
225
|
+
const organizationId = (auth as { orgId?: string | null }).orgId ?? null
|
|
226
|
+
const userId = auth.sub as string
|
|
227
|
+
const captureScope = { tenantId, organizationId }
|
|
228
|
+
|
|
229
|
+
if (
|
|
230
|
+
(body.action === 'clear-capture' || body.action === 'list-capture')
|
|
231
|
+
&& body.systemRecipient
|
|
232
|
+
&& (
|
|
233
|
+
!body.captureCorrelationToken
|
|
234
|
+
|| !isTestEmailCaptureAccessAuthorized(req.headers.get('x-om-test-email-capture-access-token'))
|
|
235
|
+
)
|
|
236
|
+
) {
|
|
237
|
+
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (body.action === 'clear-capture') {
|
|
241
|
+
await clearTestSeedCapturedMessages(captureScope, {
|
|
242
|
+
systemRecipient: body.systemRecipient,
|
|
243
|
+
captureCorrelationToken: body.captureCorrelationToken,
|
|
244
|
+
})
|
|
245
|
+
return NextResponse.json({ ok: true })
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (body.action === 'list-capture') {
|
|
249
|
+
return NextResponse.json({
|
|
250
|
+
items: (await listTestSeedCapturedMessages(captureScope, {
|
|
251
|
+
systemRecipient: body.systemRecipient,
|
|
252
|
+
captureCorrelationToken: body.captureCorrelationToken,
|
|
253
|
+
})).map(({ captureCorrelationToken: _captureCorrelationToken, ...message }) => message),
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
|
|
199
257
|
const container = await createRequestContainer()
|
|
200
258
|
// Defensive: make sure the stub adapter is registered for this process even if
|
|
201
259
|
// a worker-only node skipped module di registration.
|
|
202
260
|
ensureTestSeedAdapterRegistered()
|
|
203
261
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
262
|
+
if (body.action === 'seed-system-channel') {
|
|
263
|
+
const em = (container.resolve('em') as EntityManager).fork()
|
|
264
|
+
const credentialsService = container.resolve('integrationCredentialsService') as {
|
|
265
|
+
save: (
|
|
266
|
+
integrationId: string,
|
|
267
|
+
credentials: Record<string, unknown>,
|
|
268
|
+
scope: { organizationId: string; tenantId: string; userId?: string | null },
|
|
269
|
+
) => Promise<void>
|
|
270
|
+
}
|
|
271
|
+
await credentialsService.save(
|
|
272
|
+
`channel_${TEST_SEED_PROVIDER_KEY}`,
|
|
273
|
+
{ testSeed: true },
|
|
274
|
+
{ tenantId, organizationId: organizationId ?? tenantId, userId: null },
|
|
275
|
+
)
|
|
276
|
+
let channel = await findOneWithDecryption(
|
|
277
|
+
em,
|
|
278
|
+
CommunicationChannel,
|
|
279
|
+
{
|
|
280
|
+
providerKey: TEST_SEED_PROVIDER_KEY,
|
|
281
|
+
channelType: 'email',
|
|
282
|
+
tenantId,
|
|
283
|
+
organizationId,
|
|
284
|
+
userId: null,
|
|
285
|
+
deletedAt: null,
|
|
286
|
+
},
|
|
287
|
+
undefined,
|
|
288
|
+
{ tenantId, organizationId },
|
|
289
|
+
)
|
|
290
|
+
if (!channel) {
|
|
291
|
+
const stamp = Date.now()
|
|
292
|
+
channel = em.create(CommunicationChannel, {
|
|
293
|
+
providerKey: TEST_SEED_PROVIDER_KEY,
|
|
294
|
+
channelType: 'email',
|
|
295
|
+
displayName: body.displayName ?? `Test Seed System Email ${stamp}`,
|
|
296
|
+
externalIdentifier: body.externalIdentifier ?? `system-${stamp}@test-seed.local`,
|
|
297
|
+
userId: null,
|
|
298
|
+
isPrimary: false,
|
|
299
|
+
isActive: true,
|
|
300
|
+
status: 'connected',
|
|
301
|
+
tenantId,
|
|
302
|
+
organizationId,
|
|
303
|
+
})
|
|
304
|
+
em.persist(channel)
|
|
305
|
+
await em.flush()
|
|
306
|
+
} else if (!channel.isActive || channel.status !== 'connected') {
|
|
307
|
+
channel.isActive = true
|
|
308
|
+
channel.status = 'connected'
|
|
309
|
+
channel.lastError = null
|
|
310
|
+
await em.flush()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return NextResponse.json({ channelId: channel.id }, { status: 201 })
|
|
314
|
+
}
|
|
207
315
|
|
|
208
316
|
if (body.action === 'connect-channel') {
|
|
209
317
|
const stamp = Date.now()
|
|
@@ -424,32 +532,16 @@ export async function POST(req: Request): Promise<Response> {
|
|
|
424
532
|
em.persist(conversation)
|
|
425
533
|
await em.flush()
|
|
426
534
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
(?, ?, ?, ?, ?, 'text', 'normal', 'sent',
|
|
438
|
-
false, now(), 'public', 'communication_channels.test_seed_inbound', ?,
|
|
439
|
-
?, ?, now(), now())
|
|
440
|
-
RETURNING id`,
|
|
441
|
-
[
|
|
442
|
-
`channel.${providerKey}`,
|
|
443
|
-
body.messageThreadId ?? null,
|
|
444
|
-
userId,
|
|
445
|
-
body.subject ?? '(no subject)',
|
|
446
|
-
body.bodyText ?? '',
|
|
447
|
-
body.channelId,
|
|
448
|
-
tenantId,
|
|
449
|
-
organizationId,
|
|
450
|
-
],
|
|
451
|
-
)) as Array<{ id: string }>
|
|
452
|
-
const messageId = messageRows[0]?.id
|
|
535
|
+
const messageId = await createTestSeedPlatformMessage(em, {
|
|
536
|
+
providerKey,
|
|
537
|
+
threadId: body.messageThreadId,
|
|
538
|
+
senderUserId: userId,
|
|
539
|
+
subject: body.subject,
|
|
540
|
+
bodyText: body.bodyText,
|
|
541
|
+
channelId: body.channelId,
|
|
542
|
+
tenantId,
|
|
543
|
+
organizationId,
|
|
544
|
+
})
|
|
453
545
|
if (!messageId) {
|
|
454
546
|
return NextResponse.json({ error: '[internal] failed to seed message row' }, { status: 500 })
|
|
455
547
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { asValue } from 'awilix'
|
|
2
2
|
import type { AppContainer } from '@open-mercato/shared/lib/di/container'
|
|
3
|
+
import { registerEmailTransport } from '@open-mercato/shared/lib/email/transport'
|
|
3
4
|
import {
|
|
4
5
|
CommunicationChannel,
|
|
5
6
|
ExternalConversation,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
import { getChannelAdapterRegistry } from './lib/adapter-registry-singleton'
|
|
12
13
|
import { ensureTestSeedAdapterRegistered } from './lib/test-seed'
|
|
13
14
|
import { sendAsUser } from './lib/send-as-user'
|
|
15
|
+
import { isSystemEmailTransportConfigured, sendSystemEmail } from './lib/system-email'
|
|
14
16
|
import { resolveChannelTypeSafely } from './lib/resolve-channel-type'
|
|
15
17
|
|
|
16
18
|
export function register(container: AppContainer) {
|
|
@@ -19,6 +21,11 @@ export function register(container: AppContainer) {
|
|
|
19
21
|
// integration harness connect a channel + complete the outbound send chain.
|
|
20
22
|
// See lib/test-seed.ts.
|
|
21
23
|
ensureTestSeedAdapterRegistered()
|
|
24
|
+
registerEmailTransport({
|
|
25
|
+
id: 'communication_channels',
|
|
26
|
+
send: (payload) => sendSystemEmail(container, payload),
|
|
27
|
+
isConfigured: isSystemEmailTransportConfigured,
|
|
28
|
+
})
|
|
22
29
|
|
|
23
30
|
container.register({
|
|
24
31
|
// Entity class registrations (for EntityManager lookups by string)
|
|
@@ -37,6 +44,7 @@ export function register(container: AppContainer) {
|
|
|
37
44
|
// In-process send-as-user facade. Cross-module callers (e.g. the customers
|
|
38
45
|
// compose route) resolve this instead of making an HTTP self-call.
|
|
39
46
|
communicationChannelsSendAsUser: asValue(sendAsUser),
|
|
47
|
+
communicationChannelsSendSystemEmail: asValue(sendSystemEmail),
|
|
40
48
|
|
|
41
49
|
// Cross-module channel-type lookup. The messages compose route resolves this
|
|
42
50
|
// to decide whether an external correspondent must carry an email address
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
3
|
+
import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
|
|
4
|
+
import { CommunicationChannel } from '../data/entities'
|
|
5
|
+
|
|
6
|
+
const logger = createLogger('communication_channels')
|
|
7
|
+
|
|
8
|
+
export type EnsureSystemEmailChannelInput = {
|
|
9
|
+
tenantId: string
|
|
10
|
+
organizationId: string
|
|
11
|
+
providerKey: string
|
|
12
|
+
externalIdentifier: string
|
|
13
|
+
displayName?: string
|
|
14
|
+
capabilities?: Record<string, unknown>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type EnsureSystemEmailChannelResult = 'created' | 'updated' | 'failed'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Idempotently create or refresh a tenant's system email channel row.
|
|
21
|
+
*
|
|
22
|
+
* Shared by the two places that need the same row: the provider env preset, which runs at tenant setup
|
|
23
|
+
* and again on every `yarn mercato seed:defaults --module channel_<provider>` used to migrate tenants
|
|
24
|
+
* that predate the Communications Hub, and the lazy repair in `sendSystemEmail` for a tenant whose
|
|
25
|
+
* credentials were saved through the integrations admin UI (which stores credentials but has no hook
|
|
26
|
+
* to create the channel).
|
|
27
|
+
*
|
|
28
|
+
* Never throws: a channel row is a convenience for the Hub UI and for later lookups, so a failure to
|
|
29
|
+
* write one must not take down the caller — an email send in particular still has usable credentials.
|
|
30
|
+
*/
|
|
31
|
+
export async function ensureSystemEmailChannel(
|
|
32
|
+
em: EntityManager,
|
|
33
|
+
input: EnsureSystemEmailChannelInput,
|
|
34
|
+
): Promise<EnsureSystemEmailChannelResult> {
|
|
35
|
+
const { tenantId, organizationId, providerKey, externalIdentifier } = input
|
|
36
|
+
const displayName = input.displayName ?? `${providerKey} system email`
|
|
37
|
+
const dscope = { tenantId, organizationId }
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const existing = await findOneWithDecryption(
|
|
41
|
+
em,
|
|
42
|
+
CommunicationChannel,
|
|
43
|
+
{
|
|
44
|
+
providerKey,
|
|
45
|
+
channelType: 'email',
|
|
46
|
+
tenantId,
|
|
47
|
+
organizationId,
|
|
48
|
+
userId: null,
|
|
49
|
+
deletedAt: null,
|
|
50
|
+
},
|
|
51
|
+
undefined,
|
|
52
|
+
dscope,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (existing) {
|
|
56
|
+
existing.displayName = displayName
|
|
57
|
+
existing.externalIdentifier = externalIdentifier
|
|
58
|
+
if (input.capabilities) existing.capabilities = { ...input.capabilities }
|
|
59
|
+
existing.isActive = true
|
|
60
|
+
existing.status = 'connected'
|
|
61
|
+
existing.lastError = null
|
|
62
|
+
await em.flush()
|
|
63
|
+
return 'updated'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const channel = em.create(CommunicationChannel, {
|
|
67
|
+
providerKey,
|
|
68
|
+
channelType: 'email',
|
|
69
|
+
displayName,
|
|
70
|
+
externalIdentifier,
|
|
71
|
+
capabilities: { ...(input.capabilities ?? {}) },
|
|
72
|
+
isActive: true,
|
|
73
|
+
status: 'connected',
|
|
74
|
+
userId: null,
|
|
75
|
+
pollIntervalSeconds: null,
|
|
76
|
+
tenantId,
|
|
77
|
+
organizationId,
|
|
78
|
+
})
|
|
79
|
+
await em.persist(channel).flush()
|
|
80
|
+
return 'created'
|
|
81
|
+
} catch (err) {
|
|
82
|
+
logger.warn('Could not ensure system email channel', { err, tenantId, providerKey })
|
|
83
|
+
return 'failed'
|
|
84
|
+
}
|
|
85
|
+
}
|