@open-mercato/core 0.7.1-develop.7150.1.c1941e0c22 → 0.7.1-develop.7152.1.a69e92f9c9
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/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/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/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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/api/signup.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { signupSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { CustomerRole, CustomerUserRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport CustomerSignupVerificationEmail from '@open-mercato/core/modules/customer_accounts/emails/CustomerSignupVerificationEmail'\nimport CustomerExistingAccountEmail from '@open-mercato/core/modules/customer_accounts/emails/CustomerExistingAccountEmail'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerSignupRateLimitConfig,\n customerSignupIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\nimport { findOrganizationInTenant } from '@open-mercato/core/modules/customer_accounts/lib/organizationLookup'\nimport { getSecurityEmailBaseUrl, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\nimport { urlForCustomerOrg } from '@open-mercato/core/modules/customer_accounts/lib/customerUrl'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customer_accounts').child({ component: 'signup' })\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the existing-user and new-user signup branches so the endpoint's\n// 202-for-both contract is not undone by a timing side channel.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\nfunction resolvePortalLoginUrl(baseUrl: string, organizationSlug?: string | null): string {\n return organizationSlug\n ? `${baseUrl}/${organizationSlug}/portal/login`\n : `${baseUrl}/portal/login`\n}\n\nfunction resolvePortalVerifyUrl(baseUrl: string, token: string, organizationSlug?: string | null): string {\n const route = organizationSlug\n ? `${baseUrl}/${organizationSlug}/portal/verify`\n : `${baseUrl}/portal/verify`\n return `${route}?token=${encodeURIComponent(token)}`\n}\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerSignupIpRateLimitConfig,\n compoundConfig: customerSignupRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = signupSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const { email, password, displayName } = parsed.data\n let tenantId: string\n let organizationId: string | null\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n organizationId = context.organizationId ?? parsed.data.organizationId ?? null\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n if (!organizationId) {\n return NextResponse.json({ ok: false, error: 'organizationId is required' }, { status: 400 })\n }\n\n let baseUrl: string\n try {\n baseUrl = getSecurityEmailBaseUrl(req)\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'customer_accounts.signup',\n configMessage: 'Customer signup is not configured',\n })\n if (mapped) return NextResponse.json(mapped.body, { status: mapped.status })\n throw error\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const { translate } = await resolveTranslations()\n\n const orgRow = await findOrganizationInTenant(em, organizationId, tenantId)\n if (!orgRow) {\n return NextResponse.json({ ok: false, error: 'Registration could not be completed' }, { status: 400 })\n }\n\n const existing = await customerUserService.findByEmail(email, tenantId)\n if (existing) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n const existingOrg = await findOrganizationInTenant(em, existing.organizationId, tenantId)\n // Prefer the org's active custom domain when available; fall back to the\n // platform login URL preserved by `resolvePortalLoginUrl`.\n let loginUrl = resolvePortalLoginUrl(baseUrl, existingOrg?.slug ?? null)\n try {\n loginUrl = await urlForCustomerOrg(existing.organizationId, '/login', { container })\n } catch {\n // Fall back to platform URL on any resolution issue.\n }\n const subject = translate('customer_accounts.signup.existing.subject', 'You already have a portal account')\n const copy = {\n preview: translate('customer_accounts.signup.existing.preview', 'A sign-up attempt was made for an email that already has a portal account.'),\n title: translate('customer_accounts.signup.existing.title', 'You already have a portal account'),\n body: translate(\n 'customer_accounts.signup.existing.body',\n 'A sign-up request was made for this email address. You can sign in with your existing account. If you forgot your password, use the password reset option on the sign-in page.',\n ),\n cta: translate('customer_accounts.signup.existing.cta', 'Open sign-in page'),\n hint: translate(\n 'customer_accounts.signup.existing.hint',\n 'If this was not you, you can ignore this message. No new portal account was created.',\n ),\n }\n\n void sendEmail({\n to: existing.email,\n subject,\n react: CustomerExistingAccountEmail({ loginUrl, copy }),\n }).catch((error) => {\n logger.error('Existing-account email failed', { err: error })\n })\n\n return NextResponse.json({ ok: true }, { status: 202 })\n }\n\n const user = await customerUserService.createUser(email, password, displayName, { tenantId, organizationId })\n\n const defaultRole = await em.findOne(CustomerRole, {\n tenantId,\n isDefault: true,\n deletedAt: null,\n })\n if (defaultRole) {\n const userRole = em.create(CustomerUserRole, {\n user,\n role: defaultRole,\n createdAt: new Date(),\n } as any)\n em.persist(userRole)\n }\n\n await em.persist(user).flush()\n\n const verificationToken = await customerTokenService.createEmailVerification(user.id, tenantId)\n let verifyUrl = resolvePortalVerifyUrl(baseUrl, verificationToken, orgRow.slug)\n try {\n verifyUrl = await urlForCustomerOrg(organizationId, `/verify?token=${encodeURIComponent(verificationToken)}`, {\n container,\n })\n } catch {\n // Fall back to the platform-derived URL on any resolution issue.\n }\n const subject = translate('customer_accounts.signup.created.subject', 'Verify your portal account')\n const copy = {\n preview: translate('customer_accounts.signup.created.preview', 'Verify your portal account to finish sign-up.'),\n title: translate('customer_accounts.signup.created.title', 'Verify your portal account'),\n body: translate(\n 'customer_accounts.signup.created.body',\n 'Your account request was accepted. Confirm your email address to finish setting up portal access.',\n ),\n cta: translate('customer_accounts.signup.created.cta', 'Verify email address'),\n hint: translate(\n 'customer_accounts.signup.created.hint',\n 'This verification link expires in 24 hours. If you did not request this, you can ignore this email.',\n ),\n }\n\n void sendEmail({\n to: user.email,\n subject,\n react: CustomerSignupVerificationEmail({ verifyUrl, copy }),\n }).catch((error) => {\n logger.error('Verification email failed', { err: error })\n })\n\n void emitCustomerAccountsEvent('customer_accounts.user.created', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId,\n }).catch(() => undefined)\n\n return NextResponse.json({ ok: true }, { status: 202 })\n}\n\nconst signupAcceptedSchema = z.object({ ok: z.literal(true) })\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Register a new customer account',\n description: 'Accepts a signup request and always returns 202 to prevent account enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: signupSchema,\n description: 'Signup payload with email, password, and display name.',\n },\n responses: [\n { status: 202, description: 'Signup accepted', schema: signupAcceptedSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed or invalid request origin', schema: errorSchema },\n { status: 429, description: 'Too many signup attempts', schema: rateLimitErrorSchema },\n { status: 500, description: 'Signup email origin is not configured', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer account registration',\n description: 'Handles customer self-registration without revealing whether the email already exists.',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { compare as bcryptCompare } from 'bcryptjs'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc, OpenApiMethodDoc } from '@open-mercato/shared/lib/openapi'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { signupSchema } from '@open-mercato/core/modules/customer_accounts/data/validators'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { CustomerUserService } from '@open-mercato/core/modules/customer_accounts/services/customerUserService'\nimport { CustomerTokenService } from '@open-mercato/core/modules/customer_accounts/services/customerTokenService'\nimport { CustomerRole, CustomerUserRole } from '@open-mercato/core/modules/customer_accounts/data/entities'\nimport { emitCustomerAccountsEvent } from '@open-mercato/core/modules/customer_accounts/events'\nimport CustomerSignupVerificationEmail from '@open-mercato/core/modules/customer_accounts/emails/CustomerSignupVerificationEmail'\nimport CustomerExistingAccountEmail from '@open-mercato/core/modules/customer_accounts/emails/CustomerExistingAccountEmail'\nimport { rateLimitErrorSchema } from '@open-mercato/shared/lib/ratelimit/helpers'\nimport {\n checkAuthRateLimit,\n customerSignupRateLimitConfig,\n customerSignupIpRateLimitConfig,\n} from '@open-mercato/core/modules/customer_accounts/lib/rateLimiter'\nimport { readNormalizedEmailFromJsonRequest } from '@open-mercato/core/modules/customer_accounts/lib/rateLimitIdentifier'\nimport { findOrganizationInTenant } from '@open-mercato/core/modules/customer_accounts/lib/organizationLookup'\nimport { getSecurityEmailBaseUrl, mapSecurityEmailUrlError } from '@open-mercato/shared/lib/url'\nimport {\n resolveTenantContext,\n TenantResolutionError,\n} from '@open-mercato/core/modules/customer_accounts/lib/resolveTenantContext'\nimport { urlForCustomerOrg } from '@open-mercato/core/modules/customer_accounts/lib/customerUrl'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customer_accounts').child({ component: 'signup' })\n\nexport const metadata: { path?: string; requireAuth?: boolean } = { requireAuth: false }\n\n// Precomputed bcrypt cost-10 hash of an unknowable random 32-byte input; used to equalize\n// response latency between the existing-user and new-user signup branches so the endpoint's\n// 202-for-both contract is not undone by a timing side channel.\nconst TIMING_EQUALIZATION_HASH = '$2b$10$.F2A6UHFzk.d8trNdfqt4OLz05Nf3IOuMmN6VJKflhD4.rz.prR8i'\nfunction resolvePortalLoginUrl(baseUrl: string, organizationSlug?: string | null): string {\n return organizationSlug\n ? `${baseUrl}/${organizationSlug}/portal/login`\n : `${baseUrl}/portal/login`\n}\n\nfunction resolvePortalVerifyUrl(baseUrl: string, token: string, organizationSlug?: string | null): string {\n const route = organizationSlug\n ? `${baseUrl}/${organizationSlug}/portal/verify`\n : `${baseUrl}/portal/verify`\n return `${route}?token=${encodeURIComponent(token)}`\n}\n\nexport async function POST(req: Request) {\n const rateLimitEmail = await readNormalizedEmailFromJsonRequest(req)\n const { error: rateLimitError } = await checkAuthRateLimit({\n req,\n ipConfig: customerSignupIpRateLimitConfig,\n compoundConfig: customerSignupRateLimitConfig,\n compoundIdentifier: rateLimitEmail,\n })\n if (rateLimitError) return rateLimitError\n\n let body: unknown\n try {\n body = await req.json()\n } catch {\n return NextResponse.json({ ok: false, error: 'Invalid request body' }, { status: 400 })\n }\n\n const parsed = signupSchema.safeParse(body)\n if (!parsed.success) {\n return NextResponse.json({ ok: false, error: 'Validation failed', details: parsed.error.flatten().fieldErrors }, { status: 400 })\n }\n\n const { email, password, displayName } = parsed.data\n let tenantId: string\n let organizationId: string | null\n try {\n const context = await resolveTenantContext(req, parsed.data.tenantId, {\n organizationId: parsed.data.organizationId ?? null,\n })\n tenantId = context.tenantId\n organizationId = context.organizationId ?? parsed.data.organizationId ?? null\n } catch (err) {\n if (err instanceof TenantResolutionError) {\n return NextResponse.json({ ok: false, error: err.message }, { status: err.status })\n }\n throw err\n }\n if (!organizationId) {\n return NextResponse.json({ ok: false, error: 'organizationId is required' }, { status: 400 })\n }\n\n let baseUrl: string\n try {\n baseUrl = getSecurityEmailBaseUrl(req)\n } catch (error) {\n const mapped = mapSecurityEmailUrlError(error, {\n scope: 'customer_accounts.signup',\n configMessage: 'Customer signup is not configured',\n })\n if (mapped) return NextResponse.json(mapped.body, { status: mapped.status })\n throw error\n }\n\n const container = await createRequestContainer()\n const customerUserService = container.resolve('customerUserService') as CustomerUserService\n const customerTokenService = container.resolve('customerTokenService') as CustomerTokenService\n const em = container.resolve('em') as import('@mikro-orm/postgresql').EntityManager\n const { translate } = await resolveTranslations()\n\n const orgRow = await findOrganizationInTenant(em, organizationId, tenantId)\n if (!orgRow) {\n return NextResponse.json({ ok: false, error: 'Registration could not be completed' }, { status: 400 })\n }\n\n const existing = await customerUserService.findByEmail(email, tenantId)\n if (existing) {\n await bcryptCompare(password, TIMING_EQUALIZATION_HASH)\n const existingOrg = await findOrganizationInTenant(em, existing.organizationId, tenantId)\n // Prefer the org's active custom domain when available; fall back to the\n // platform login URL preserved by `resolvePortalLoginUrl`.\n let loginUrl = resolvePortalLoginUrl(baseUrl, existingOrg?.slug ?? null)\n try {\n loginUrl = await urlForCustomerOrg(existing.organizationId, '/login', { container })\n } catch {\n // Fall back to platform URL on any resolution issue.\n }\n const subject = translate('customer_accounts.signup.existing.subject', 'You already have a portal account')\n const copy = {\n preview: translate('customer_accounts.signup.existing.preview', 'A sign-up attempt was made for an email that already has a portal account.'),\n title: translate('customer_accounts.signup.existing.title', 'You already have a portal account'),\n body: translate(\n 'customer_accounts.signup.existing.body',\n 'A sign-up request was made for this email address. You can sign in with your existing account. If you forgot your password, use the password reset option on the sign-in page.',\n ),\n cta: translate('customer_accounts.signup.existing.cta', 'Open sign-in page'),\n hint: translate(\n 'customer_accounts.signup.existing.hint',\n 'If this was not you, you can ignore this message. No new portal account was created.',\n ),\n }\n\n await sendEmail({\n to: existing.email,\n subject,\n react: CustomerExistingAccountEmail({ loginUrl, copy }),\n tenantId,\n organizationId,\n }).catch((error) => {\n logger.error('Existing-account email failed', { err: error })\n })\n\n return NextResponse.json({ ok: true }, { status: 202 })\n }\n\n const user = await customerUserService.createUser(email, password, displayName, { tenantId, organizationId })\n\n const defaultRole = await em.findOne(CustomerRole, {\n tenantId,\n isDefault: true,\n deletedAt: null,\n })\n if (defaultRole) {\n const userRole = em.create(CustomerUserRole, {\n user,\n role: defaultRole,\n createdAt: new Date(),\n } as any)\n em.persist(userRole)\n }\n\n await em.persist(user).flush()\n\n const verificationToken = await customerTokenService.createEmailVerification(user.id, tenantId)\n let verifyUrl = resolvePortalVerifyUrl(baseUrl, verificationToken, orgRow.slug)\n try {\n verifyUrl = await urlForCustomerOrg(organizationId, `/verify?token=${encodeURIComponent(verificationToken)}`, {\n container,\n })\n } catch {\n // Fall back to the platform-derived URL on any resolution issue.\n }\n const subject = translate('customer_accounts.signup.created.subject', 'Verify your portal account')\n const copy = {\n preview: translate('customer_accounts.signup.created.preview', 'Verify your portal account to finish sign-up.'),\n title: translate('customer_accounts.signup.created.title', 'Verify your portal account'),\n body: translate(\n 'customer_accounts.signup.created.body',\n 'Your account request was accepted. Confirm your email address to finish setting up portal access.',\n ),\n cta: translate('customer_accounts.signup.created.cta', 'Verify email address'),\n hint: translate(\n 'customer_accounts.signup.created.hint',\n 'This verification link expires in 24 hours. If you did not request this, you can ignore this email.',\n ),\n }\n\n await sendEmail({\n to: user.email,\n subject,\n react: CustomerSignupVerificationEmail({ verifyUrl, copy }),\n tenantId,\n organizationId,\n }).catch((error) => {\n logger.error('Verification email failed', { err: error })\n })\n\n void emitCustomerAccountsEvent('customer_accounts.user.created', {\n id: user.id,\n email: user.email,\n tenantId,\n organizationId,\n }).catch(() => undefined)\n\n return NextResponse.json({ ok: true }, { status: 202 })\n}\n\nconst signupAcceptedSchema = z.object({ ok: z.literal(true) })\n\nconst errorSchema = z.object({\n ok: z.literal(false),\n error: z.string(),\n})\n\nconst methodDoc: OpenApiMethodDoc = {\n summary: 'Register a new customer account',\n description: 'Accepts a signup request and always returns 202 to prevent account enumeration.',\n tags: ['Customer Authentication'],\n requestBody: {\n schema: signupSchema,\n description: 'Signup payload with email, password, and display name.',\n },\n responses: [\n { status: 202, description: 'Signup accepted', schema: signupAcceptedSchema },\n ],\n errors: [\n { status: 400, description: 'Validation failed or invalid request origin', schema: errorSchema },\n { status: 429, description: 'Too many signup attempts', schema: rateLimitErrorSchema },\n { status: 500, description: 'Signup email origin is not configured', schema: errorSchema },\n ],\n}\n\nexport const openApi: OpenApiRouteDoc = {\n summary: 'Customer account registration',\n description: 'Handles customer self-registration without revealing whether the email already exists.',\n methods: { POST: methodDoc },\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["subject", "copy"]
|
|
7
7
|
}
|
|
@@ -26,7 +26,9 @@ async function sendCustomerInvitationEmail(input) {
|
|
|
26
26
|
await sendEmail({
|
|
27
27
|
to: input.email,
|
|
28
28
|
subject,
|
|
29
|
-
react: CustomerInvitationEmail({ inviteUrl, copy })
|
|
29
|
+
react: CustomerInvitationEmail({ inviteUrl, copy }),
|
|
30
|
+
tenantId: input.tenantId,
|
|
31
|
+
organizationId: input.organizationId
|
|
30
32
|
});
|
|
31
33
|
}
|
|
32
34
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/customer_accounts/lib/invitationEmail.ts"],
|
|
4
|
-
"sourcesContent": ["import type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport CustomerInvitationEmail from '@open-mercato/core/modules/customer_accounts/emails/CustomerInvitationEmail'\nimport { urlForCustomerOrg } from '@open-mercato/core/modules/customer_accounts/lib/customerUrl'\n\nexport type CustomerInvitationEmailInput = {\n container: AppContainer\n organizationId: string\n email: string\n rawToken: string\n}\n\nexport async function sendCustomerInvitationEmail(input: CustomerInvitationEmailInput): Promise<void> {\n const { translate } = await resolveTranslations()\n const inviteUrl = await urlForCustomerOrg(\n input.organizationId,\n `/invite?token=${encodeURIComponent(input.rawToken)}`,\n { container: input.container },\n )\n\n const subject = translate('customer_accounts.invitation.email.subject', 'You have been invited to the customer portal')\n const copy = {\n preview: translate('customer_accounts.invitation.email.preview', 'Accept your invitation to finish setting up portal access.'),\n title: translate('customer_accounts.invitation.email.title', 'You have been invited'),\n body: translate(\n 'customer_accounts.invitation.email.body',\n 'Use this secure invitation link to create your password and access the customer portal.',\n ),\n cta: translate('customer_accounts.invitation.email.cta', 'Accept invitation'),\n hint: translate(\n 'customer_accounts.invitation.email.hint',\n 'This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.',\n ),\n }\n\n await sendEmail({\n to: input.email,\n subject,\n react: CustomerInvitationEmail({ inviteUrl, copy }),\n })\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,2BAA2B;AACpC,SAAS,iBAAiB;AAC1B,OAAO,6BAA6B;AACpC,SAAS,yBAAyB;
|
|
4
|
+
"sourcesContent": ["import type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport CustomerInvitationEmail from '@open-mercato/core/modules/customer_accounts/emails/CustomerInvitationEmail'\nimport { urlForCustomerOrg } from '@open-mercato/core/modules/customer_accounts/lib/customerUrl'\n\nexport type CustomerInvitationEmailInput = {\n container: AppContainer\n tenantId?: string\n organizationId: string\n email: string\n rawToken: string\n}\n\nexport async function sendCustomerInvitationEmail(input: CustomerInvitationEmailInput): Promise<void> {\n const { translate } = await resolveTranslations()\n const inviteUrl = await urlForCustomerOrg(\n input.organizationId,\n `/invite?token=${encodeURIComponent(input.rawToken)}`,\n { container: input.container },\n )\n\n const subject = translate('customer_accounts.invitation.email.subject', 'You have been invited to the customer portal')\n const copy = {\n preview: translate('customer_accounts.invitation.email.preview', 'Accept your invitation to finish setting up portal access.'),\n title: translate('customer_accounts.invitation.email.title', 'You have been invited'),\n body: translate(\n 'customer_accounts.invitation.email.body',\n 'Use this secure invitation link to create your password and access the customer portal.',\n ),\n cta: translate('customer_accounts.invitation.email.cta', 'Accept invitation'),\n hint: translate(\n 'customer_accounts.invitation.email.hint',\n 'This invitation link expires in 72 hours. If you did not expect this invitation, you can ignore this email.',\n ),\n }\n\n await sendEmail({\n to: input.email,\n subject,\n react: CustomerInvitationEmail({ inviteUrl, copy }),\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n })\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,2BAA2B;AACpC,SAAS,iBAAiB;AAC1B,OAAO,6BAA6B;AACpC,SAAS,yBAAyB;AAUlC,eAAsB,4BAA4B,OAAoD;AACpG,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,QAAM,YAAY,MAAM;AAAA,IACtB,MAAM;AAAA,IACN,iBAAiB,mBAAmB,MAAM,QAAQ,CAAC;AAAA,IACnD,EAAE,WAAW,MAAM,UAAU;AAAA,EAC/B;AAEA,QAAM,UAAU,UAAU,8CAA8C,8CAA8C;AACtH,QAAM,OAAO;AAAA,IACX,SAAS,UAAU,8CAA8C,4DAA4D;AAAA,IAC7H,OAAO,UAAU,4CAA4C,uBAAuB;AAAA,IACpF,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,IACA,KAAK,UAAU,0CAA0C,mBAAmB;AAAA,IAC5E,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU;AAAA,IACd,IAAI,MAAM;AAAA,IACV;AAAA,IACA,OAAO,wBAAwB,EAAE,WAAW,KAAK,CAAC;AAAA,IAClD,UAAU,MAAM;AAAA,IAChB,gBAAgB,MAAM;AAAA,EACxB,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -219,6 +219,7 @@ const INTERACTION_LIST_COLUMNS = [
|
|
|
219
219
|
"priority",
|
|
220
220
|
"author_user_id",
|
|
221
221
|
"owner_user_id",
|
|
222
|
+
"external_message_id",
|
|
222
223
|
"appearance_icon",
|
|
223
224
|
"appearance_color",
|
|
224
225
|
"source",
|
|
@@ -478,6 +479,7 @@ async function GET(req) {
|
|
|
478
479
|
priority: row.priority ?? null,
|
|
479
480
|
authorUserId: row.author_user_id ?? null,
|
|
480
481
|
ownerUserId: row.owner_user_id ?? null,
|
|
482
|
+
externalMessageId: row.external_message_id ?? null,
|
|
481
483
|
appearanceIcon: row.appearance_icon ?? null,
|
|
482
484
|
appearanceColor: row.appearance_color ?? null,
|
|
483
485
|
source: row.source ?? null,
|
|
@@ -552,6 +554,7 @@ const interactionListItemSchema = z.object({
|
|
|
552
554
|
priority: z.number().nullable(),
|
|
553
555
|
authorUserId: z.string().uuid().nullable(),
|
|
554
556
|
ownerUserId: z.string().uuid().nullable(),
|
|
557
|
+
externalMessageId: z.string().uuid().nullable().optional(),
|
|
555
558
|
appearanceIcon: z.string().nullable().optional(),
|
|
556
559
|
appearanceColor: z.string().nullable().optional(),
|
|
557
560
|
source: z.string().nullable().optional(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/api/interactions/route.ts"],
|
|
4
|
-
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { applyResponseEnrichers } from '@open-mercato/shared/lib/crud/enricher-runner'\nimport type { EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEncryptedSortFields, resolveEncryptedSortMaxRows } from '@open-mercato/shared/lib/query/encrypted-sort'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CustomerDeal, CustomerInteraction } from '../../data/entities'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { interactionCreateSchema, interactionUpdateSchema } from '../../data/validators'\nimport { parseScopedCommandInput } from '../utils'\nimport {\n createCustomersCrudOpenApi,\n defaultOkResponseSchema,\n} from '../openapi'\nimport { CUSTOMER_INTERACTION_ENTITY_ID } from '../../lib/interactionCompatibility'\nimport { applyEmailVisibilityFilter } from '../../lib/visibilityFilter'\nimport { resolveEncryptedSortPage } from './encryptedSortPage'\nimport { resolveCanonicalActivityTargetId } from '../../lib/legacyActivityBridge'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst interactionSortFieldSchema = z.enum([\n 'scheduledAt',\n 'occurredAt',\n 'createdAt',\n 'updatedAt',\n 'status',\n 'priority',\n 'interactionType',\n 'title',\n])\n\nexport const listSchema = z\n .object({\n limit: z.coerce.number().min(1).max(100).default(25),\n cursor: z.string().optional(),\n entityId: z.string().uuid().optional(),\n dealId: z.string().uuid().optional(),\n status: z.string().optional(),\n interactionType: z.string().optional(),\n type: z.string().optional(),\n excludeInteractionType: z.string().optional(),\n search: z.string().trim().min(1).optional(),\n from: z.coerce.date().optional(),\n to: z.coerce.date().optional(),\n recurrenceMasters: z.enum(['true', 'false']).optional(),\n pinned: z.enum(['true', 'false']).optional(),\n sortField: interactionSortFieldSchema.optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.interactions.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CustomerInteraction,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n enrichers: { entityId: 'customers.interaction' },\n indexer: {\n entityType: 'customers:customer_interaction',\n },\n actions: {\n create: {\n commandId: 'customers.interactions.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(interactionCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({ id: result?.interactionId ?? result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'customers.interactions.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const parsed = parseScopedCommandInput(interactionUpdateSchema, raw ?? {}, ctx, translate)\n // Bridge legacy `customer_activities` rows into `customer_interactions`\n // before the canonical update runs so historical activities (#1807)\n // remain editable through the new dialog. No-op when the canonical\n // record already exists.\n const tenantId = ctx.auth?.tenantId ?? null\n if (typeof parsed.id === 'string' && tenantId) {\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const commandContext: CommandRuntimeContext = {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: ctx.organizationScope ?? null,\n selectedOrganizationId: ctx.selectedOrganizationId ?? null,\n organizationIds: ctx.organizationIds ?? null,\n request: ctx.request,\n }\n await resolveCanonicalActivityTargetId(em, commandBus, commandContext, parsed.id, tenantId)\n } catch (err) {\n // Bridging is best-effort; downstream lookup will surface a 404\n // when neither canonical nor legacy rows exist.\n logger.warn('Legacy interaction bridge failed', { component: 'interactions.put', id: parsed.id, err })\n }\n }\n return parsed\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'customers.interactions.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n if (!id) {\n throw new CrudHttpError(400, {\n error: translate('customers.errors.interaction_required', 'Interaction id is required'),\n })\n }\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\n\nexport { POST, PUT, DELETE }\n\ntype InteractionListRow = {\n id: string\n entity_id: string\n deal_id: string | null\n interaction_type: string\n title: string | null\n body: string | null\n status: string\n scheduled_at: Date | null\n occurred_at: Date | null\n priority: number | null\n author_user_id: string | null\n owner_user_id: string | null\n appearance_icon: string | null\n appearance_color: string | null\n source: string | null\n duration_minutes: number | null\n location: string | null\n all_day: boolean | null\n recurrence_rule: string | null\n recurrence_end: Date | null\n participants: Array<{ userId?: string; name?: string; email?: string; status?: string }> | null\n reminder_minutes: number | null\n visibility: string | null\n linked_entities: Array<{ id: string; type: string; label: string }> | null\n guest_permissions: { canInviteOthers?: boolean; canModify?: boolean; canSeeList?: boolean } | null\n pinned: boolean\n organization_id: string\n tenant_id: string\n created_at: Date\n updated_at: Date\n __sort_value: string | number | Date | null\n}\n\ntype CursorPayload = {\n id: string\n sortValue: string | number | null\n}\n\ntype RbacServiceLike = {\n getGrantedFeatures?: (userId: string, input: { tenantId: string | null; organizationId: string | null }) => Promise<string[]>\n}\n\nconst cursorSchema = z.object({\n id: z.string().uuid(),\n sortValue: z.union([z.string(), z.number(), z.null()]),\n})\n\nconst interactionSortConfig = {\n scheduledAt: { column: 'scheduled_at', type: 'date' as const, defaultDir: 'asc' as const },\n occurredAt: { column: 'occurred_at', type: 'date' as const, defaultDir: 'desc' as const },\n createdAt: { column: 'created_at', type: 'date' as const, defaultDir: 'desc' as const },\n updatedAt: { column: 'updated_at', type: 'date' as const, defaultDir: 'desc' as const },\n status: { column: 'status', type: 'text' as const, defaultDir: 'asc' as const },\n priority: { column: 'priority', type: 'number' as const, defaultDir: 'desc' as const },\n interactionType: { column: 'interaction_type', type: 'text' as const, defaultDir: 'asc' as const },\n title: { column: 'title', type: 'text' as const, defaultDir: 'asc' as const },\n} as const\n\nfunction toIsoString(value: unknown): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value.toISOString()\n }\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return null\n const parsed = new Date(trimmed)\n return Number.isNaN(parsed.getTime()) ? trimmed : parsed.toISOString()\n }\n return null\n}\n\nfunction normalizeCursorValue(\n value: string | number | Date | null,\n type: 'date' | 'number' | 'text',\n): string | number | null {\n if (value == null) return null\n if (type === 'number') {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isNaN(parsed) ? null : parsed\n }\n return null\n }\n if (type === 'date') {\n return toIsoString(value)\n }\n if (typeof value === 'string') return value\n if (value instanceof Date) return value.toISOString()\n return String(value)\n}\n\nfunction encodeCursor(payload: CursorPayload): string {\n return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64')\n}\n\nfunction decodeCursor(token: string | undefined, type: 'date' | 'number' | 'text'): CursorPayload | null {\n if (!token) return null\n try {\n const decoded = Buffer.from(token, 'base64').toString('utf8')\n const parsed = cursorSchema.parse(JSON.parse(decoded))\n return {\n id: parsed.id,\n sortValue: normalizeCursorValue(parsed.sortValue, type),\n }\n } catch {\n return null\n }\n}\n\nfunction buildSortSql(\n sortField: keyof typeof interactionSortConfig,\n sortDir: 'asc' | 'desc',\n): string {\n const config = interactionSortConfig[sortField]\n if (config.type === 'date') {\n const sentinel =\n sortDir === 'asc'\n ? \"timestamp with time zone '9999-12-31T23:59:59.999Z'\"\n : \"timestamp with time zone '0001-01-01T00:00:00.000Z'\"\n return `coalesce(${config.column}, ${sentinel})`\n }\n if (config.type === 'number') {\n const sentinel = sortDir === 'asc' ? '2147483647' : '-2147483648'\n return `coalesce(${config.column}, ${sentinel})`\n }\n const sentinel = sortDir === 'asc' ? \"'~~~~~~~~~~'\" : \"''\"\n return `coalesce(${config.column}, ${sentinel})`\n}\n\nconst INTERACTION_LIST_COLUMNS = [\n 'id',\n 'entity_id',\n 'deal_id',\n 'interaction_type',\n 'title',\n 'body',\n 'status',\n 'scheduled_at',\n 'occurred_at',\n 'priority',\n 'author_user_id',\n 'owner_user_id',\n 'appearance_icon',\n 'appearance_color',\n 'source',\n 'duration_minutes',\n 'location',\n 'all_day',\n 'recurrence_rule',\n 'recurrence_end',\n 'participants',\n 'reminder_minutes',\n 'visibility',\n 'linked_entities',\n 'guest_permissions',\n 'pinned',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n] as const\n\n// Shared by both the SQL keyset path and the encrypted-sort candidate scan\n// so the two paths never drift on which rows are in scope.\nfunction applyInteractionListFilters(\n baseQuery: any,\n params: {\n tenantId: string\n organizationIds: string[]\n query: z.infer<typeof listSchema>\n },\n): any {\n let q = baseQuery.where('deleted_at', 'is', null).where('tenant_id', '=', params.tenantId)\n if (params.organizationIds.length > 0) q = q.where('organization_id', 'in', params.organizationIds)\n const { query } = params\n if (query.entityId) q = q.where('entity_id', '=', query.entityId)\n if (query.dealId) q = q.where('deal_id', '=', query.dealId)\n if (query.status) q = q.where('status', '=', query.status)\n if (query.interactionType) q = q.where('interaction_type', '=', query.interactionType)\n if (query.type) {\n const types = query.type.split(',').map((t) => t.trim()).filter(Boolean)\n if (types.length > 0) q = q.where('interaction_type', 'in', types)\n }\n if (query.pinned === 'true') {\n q = q.where('pinned', '=', true)\n } else if (query.pinned === 'false') {\n q = q.where('pinned', '=', false)\n }\n if (query.excludeInteractionType) q = q.where('interaction_type', '!=', query.excludeInteractionType)\n if (query.search) {\n // NOTE: for tenants with data encryption enabled, `title`/`body` are\n // ciphertext at rest (see encryption.ts), so this ILIKE matches encrypted\n // bytes and returns no rows \u2014 substring search over encrypted free-text\n // columns is unsupported, the same documented limitation as\n // customer_activity / customer_comment. The returned page's title/body are\n // still decrypted for display further below.\n const searchTerm = `%${escapeLikePattern(query.search)}%`\n q = q.where(sql<boolean>`coalesce(title, '') ilike ${searchTerm} or coalesce(body, '') ilike ${searchTerm}`)\n }\n if (query.recurrenceMasters === 'true') {\n q = q.where('recurrence_rule', 'is not', null)\n if (query.from) {\n q = q.where(sql<boolean>`recurrence_end is null or recurrence_end >= ${query.from}`)\n }\n if (query.to) {\n q = q.where(sql<boolean>`coalesce(occurred_at, scheduled_at, created_at) <= ${query.to}`)\n }\n } else {\n if (query.from) {\n q = q.where(sql<boolean>`coalesce(occurred_at, scheduled_at, created_at) >= ${query.from}`)\n }\n if (query.to) {\n q = q.where(sql<boolean>`coalesce(occurred_at, scheduled_at, created_at) <= ${query.to}`)\n }\n }\n return q\n}\n\nasync function resolveUserFeatures(\n container: { resolve: (name: string) => unknown },\n userId: string,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string[] | undefined> {\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getGrantedFeatures) return undefined\n return await rbac.getGrantedFeatures(userId, { tenantId, organizationId })\n } catch {\n return undefined\n }\n}\n\nasync function buildEnricherContext(\n container: { resolve: (name: string) => unknown },\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>,\n organizationId: string | null,\n precomputedUserFeatures?: { userId: string; features: string[] | undefined },\n): Promise<EnricherContext> {\n const userId =\n (typeof auth.sub === 'string' && auth.sub.trim().length > 0\n ? auth.sub\n : typeof auth.userId === 'string' && auth.userId.trim().length > 0\n ? auth.userId\n : typeof auth.keyId === 'string' && auth.keyId.trim().length > 0\n ? auth.keyId\n : 'system')\n\n // Reuse features already resolved for this same user (the GET handler resolves\n // them once for the visibility filter) to avoid a second RBAC lookup per request.\n const userFeatures =\n precomputedUserFeatures && precomputedUserFeatures.userId === userId\n ? precomputedUserFeatures.features\n : await resolveUserFeatures(container, userId, auth.tenantId ?? null, organizationId)\n\n return {\n organizationId: organizationId ?? '',\n tenantId: auth.tenantId ?? '',\n userId,\n em: container.resolve('em'),\n container,\n userFeatures,\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const queryUrl = new URL(req.url)\n const query = listSchema.parse(Object.fromEntries(queryUrl.searchParams))\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, {\n error: translate('customers.errors.unauthorized', 'Unauthorized'),\n })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationIds = Array.isArray(scope?.filterIds) && scope.filterIds.length > 0\n ? scope.filterIds\n : auth.orgId\n ? [auth.orgId]\n : []\n const selectedOrganizationId = scope?.selectedId ?? auth.orgId ?? organizationIds[0] ?? null\n const em = (container.resolve('em') as EntityManager).fork()\n const db = em.getKysely<any>() as any\n\n const requestedSortField = query.sortField ?? 'scheduledAt'\n const sortConfig = interactionSortConfig[requestedSortField]\n const sortDir = query.sortDir ?? sortConfig.defaultDir\n const sortSql = buildSortSql(requestedSortField, sortDir)\n const cursor = decodeCursor(query.cursor, sortConfig.type)\n if (query.cursor && !cursor) {\n throw new CrudHttpError(400, {\n error: translate('customers.interactions.cursor.invalid', 'Invalid cursor'),\n })\n }\n\n // \u2500\u2500 Email visibility filter (2026-05-27) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Non-email interactions pass through; email rows with visibility='private'\n // are filtered out unless the caller is the author or has admin bypass.\n // API-key callers have no user identity (`auth.sub` undefined): resolve the\n // viewer to null so they never gain the author bypass and only see shared\n // emails (fail-closed). Mirrors counts/people/activities routes.\n const viewerUserId = auth.isApiKey ? null : (auth.sub ?? null)\n const encryptionService = resolveTenantEncryptionService(em)\n // Encrypted sort columns can't use SQL keyset ordering on ciphertext, so an\n // encrypted sort field takes a bounded candidate-scan + in-memory-sort path\n // instead of the SQL path below. Resolved alongside the independent\n // visibility-feature lookup rather than after it.\n const [callerUserFeatures, encryptedSortFields] = await Promise.all([\n viewerUserId\n ? resolveUserFeatures(container, viewerUserId, auth.tenantId ?? null, selectedOrganizationId)\n : Promise.resolve(undefined),\n resolveEncryptedSortFields(\n encryptionService,\n CUSTOMER_INTERACTION_ENTITY_ID,\n [sortConfig.column],\n auth.tenantId,\n selectedOrganizationId,\n ),\n ])\n const sortFieldIsEncrypted = encryptedSortFields.has(sortConfig.column)\n\n let pageRows: InteractionListRow[]\n let hasMore: boolean\n\n if (sortFieldIsEncrypted) {\n let candidateQuery = applyInteractionListFilters(\n db.selectFrom('customer_interactions').select(['id', sortConfig.column]),\n { tenantId: auth.tenantId, organizationIds, query },\n )\n candidateQuery = applyEmailVisibilityFilter(candidateQuery as any, {\n currentUserId: viewerUserId,\n userFeatures: callerUserFeatures,\n })\n const cap = resolveEncryptedSortMaxRows()\n if (cap !== null) {\n candidateQuery = candidateQuery.limit(cap).orderBy('id', 'asc')\n }\n const candidateRows = await candidateQuery.execute() as Array<{ id: string } & Record<string, unknown>>\n if (cap !== null && candidateRows.length >= cap) {\n logger.warn('Encrypted sort candidate scan hit OM_ENCRYPTED_SORT_MAX_ROWS cap; results may be incomplete', {\n component: 'interactions.GET',\n cap,\n sortField: sortConfig.column,\n tenantId: auth.tenantId,\n })\n }\n\n const decryptPayload = encryptionService?.decryptEntityPayload?.bind(encryptionService)\n const { pageIds, hasMore: encryptedHasMore } = await resolveEncryptedSortPage({\n candidates: candidateRows,\n decryptRow: async (row) => {\n if (!decryptPayload) return row\n try {\n const decrypted = await decryptPayload(CUSTOMER_INTERACTION_ENTITY_ID, row, auth.tenantId, selectedOrganizationId)\n return { ...row, ...decrypted }\n } catch (err) {\n logger.error('error decrypting sort candidate', { component: 'interactions.GET', err })\n return row\n }\n },\n sortField: sortConfig.column,\n sortDir,\n cursorId: cursor?.id ?? null,\n limit: query.limit,\n })\n hasMore = encryptedHasMore\n\n if (pageIds.length === 0) {\n pageRows = []\n } else {\n let pageQuery = applyInteractionListFilters(\n db.selectFrom('customer_interactions').select([...INTERACTION_LIST_COLUMNS, sql`${sql.raw(sortSql)}`.as('__sort_value')]),\n { tenantId: auth.tenantId, organizationIds, query },\n )\n pageQuery = applyEmailVisibilityFilter(pageQuery as any, {\n currentUserId: viewerUserId,\n userFeatures: callerUserFeatures,\n })\n pageQuery = pageQuery.where('id', 'in', pageIds)\n const rawPageRows = await pageQuery.execute() as InteractionListRow[]\n const byId = new Map(rawPageRows.map((row) => [row.id, row]))\n pageRows = pageIds\n .map((id) => byId.get(id))\n .filter((row): row is InteractionListRow => row != null)\n }\n } else {\n let rowsQuery = applyInteractionListFilters(\n db\n .selectFrom('customer_interactions')\n .select([...INTERACTION_LIST_COLUMNS, sql`${sql.raw(sortSql)}`.as('__sort_value')])\n .limit(query.limit + 1),\n { tenantId: auth.tenantId, organizationIds, query },\n )\n\n if (cursor) {\n const op = sortDir === 'asc' ? '>' : '<'\n const opRaw = sql.raw(op)\n const sortRaw = sql.raw(sortSql)\n rowsQuery = rowsQuery.where((eb: any) => eb.or([\n sql<boolean>`${sortRaw} ${opRaw} ${cursor.sortValue}`,\n eb.and([\n sql<boolean>`${sortRaw} = ${cursor.sortValue}`,\n eb('id', op, cursor.id),\n ]),\n ]))\n }\n\n rowsQuery = applyEmailVisibilityFilter(rowsQuery as any, {\n currentUserId: viewerUserId,\n userFeatures: callerUserFeatures,\n })\n\n rowsQuery = rowsQuery.orderBy(sql`${sql.raw(sortSql)} ${sql.raw(sortDir)}`).orderBy('id', sortDir)\n\n const rows = await rowsQuery.execute() as InteractionListRow[]\n pageRows = rows.slice(0, query.limit)\n hasMore = rows.length > query.limit\n }\n\n const authorIds = Array.from(\n new Set(\n pageRows\n .map((row) => (typeof row.author_user_id === 'string' ? row.author_user_id : null))\n .filter((value): value is string => !!value),\n ),\n )\n const dealIds = Array.from(\n new Set(\n pageRows\n .map((row) => (typeof row.deal_id === 'string' ? row.deal_id : null))\n .filter((value): value is string => !!value),\n ),\n )\n const interactionIds = pageRows.map((row) => row.id)\n\n const [users, deals, customFieldValues, interactionRecords] = await Promise.all([\n authorIds.length > 0 ? findWithDecryption(em, User, { id: { $in: authorIds } }, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId }) : Promise.resolve([]),\n dealIds.length > 0 ? findWithDecryption(em, CustomerDeal, { id: { $in: dealIds } }, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId }) : Promise.resolve([]),\n interactionIds.length > 0\n ? loadCustomFieldValues({\n em,\n entityId: CUSTOMER_INTERACTION_ENTITY_ID,\n recordIds: interactionIds,\n tenantIdByRecord: Object.fromEntries(pageRows.map((row) => [row.id, row.tenant_id])),\n organizationIdByRecord: Object.fromEntries(pageRows.map((row) => [row.id, row.organization_id])),\n tenantFallbacks: [auth.tenantId].filter((value): value is string => !!value),\n })\n : Promise.resolve<Record<string, Record<string, unknown>>>({}),\n interactionIds.length > 0\n ? findWithDecryption(em, CustomerInteraction, { id: { $in: interactionIds } } as never, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId })\n : Promise.resolve([]),\n ])\n\n const userMap = new Map(\n users.map((user) => [\n user.id,\n {\n name: user.name ?? null,\n email: user.email ?? null,\n },\n ]),\n )\n const dealMap = new Map(\n deals.map((deal) => [deal.id, deal.title]),\n )\n // title/body are encrypted at rest (see encryption.ts). The kysely rows above\n // carry ciphertext when tenant encryption is enabled, so override them with the\n // decrypted values from findWithDecryption for the returned page.\n const interactionContentMap = new Map(\n (interactionRecords as Array<{ id: string; title?: string | null; body?: string | null }>).map(\n (record) => [record.id, { title: record.title ?? null, body: record.body ?? null }],\n ),\n )\n\n const baseItems = pageRows.map((row) => ({\n id: row.id,\n entityId: row.entity_id,\n dealId: row.deal_id ?? null,\n interactionType: row.interaction_type,\n title: (interactionContentMap.has(row.id) ? interactionContentMap.get(row.id)!.title : row.title) ?? null,\n body: (interactionContentMap.has(row.id) ? interactionContentMap.get(row.id)!.body : row.body) ?? null,\n status: row.status,\n scheduledAt: toIsoString(row.scheduled_at),\n occurredAt: toIsoString(row.occurred_at),\n priority: row.priority ?? null,\n authorUserId: row.author_user_id ?? null,\n ownerUserId: row.owner_user_id ?? null,\n appearanceIcon: row.appearance_icon ?? null,\n appearanceColor: row.appearance_color ?? null,\n source: row.source ?? null,\n duration: row.duration_minutes ?? null,\n durationMinutes: row.duration_minutes ?? null,\n location: row.location ?? null,\n allDay: row.all_day ?? null,\n recurrenceRule: row.recurrence_rule ?? null,\n recurrenceEnd: toIsoString(row.recurrence_end),\n participants: row.participants ?? null,\n reminderMinutes: row.reminder_minutes ?? null,\n visibility: row.visibility ?? null,\n linkedEntities: row.linked_entities ?? null,\n guestPermissions: row.guest_permissions ?? null,\n pinned: row.pinned ?? false,\n organizationId: row.organization_id,\n tenantId: row.tenant_id,\n createdAt: toIsoString(row.created_at) ?? new Date().toISOString(),\n updatedAt: toIsoString(row.updated_at) ?? new Date().toISOString(),\n authorName: row.author_user_id ? userMap.get(row.author_user_id)?.name ?? null : null,\n authorEmail: row.author_user_id ? userMap.get(row.author_user_id)?.email ?? null : null,\n dealTitle: row.deal_id ? dealMap.get(row.deal_id) ?? null : null,\n customValues: normalizeCustomFieldResponse(customFieldValues[row.id]) ?? null,\n }))\n\n const enricherContext = await buildEnricherContext(\n container,\n auth,\n selectedOrganizationId,\n viewerUserId ? { userId: viewerUserId, features: callerUserFeatures } : undefined,\n )\n const enriched = await applyResponseEnrichers(baseItems, 'customers.interaction', enricherContext)\n\n let nextCursor: string | undefined\n if (hasMore && pageRows.length > 0) {\n const last = pageRows[pageRows.length - 1]\n nextCursor = encodeCursor({\n id: last.id,\n sortValue: normalizeCursorValue(last.__sort_value, sortConfig.type),\n })\n }\n\n return NextResponse.json({\n items: enriched.items,\n nextCursor,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: 'Validation failed', details: err.issues },\n { status: 400 },\n )\n }\n logger.error('customers.interactions.get failed', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json(\n { error: translate('customers.interactions.load.error', 'Failed to load interactions.') },\n { status: 500 },\n )\n }\n}\n\nconst interactionListItemSchema = z\n .object({\n id: z.string().uuid(),\n entityId: z.string().uuid().nullable(),\n dealId: z.string().uuid().nullable(),\n interactionType: z.string(),\n title: z.string().nullable(),\n body: z.string().nullable(),\n status: z.string(),\n scheduledAt: z.string().nullable(),\n occurredAt: z.string().nullable(),\n priority: z.number().nullable(),\n authorUserId: z.string().uuid().nullable(),\n ownerUserId: z.string().uuid().nullable(),\n appearanceIcon: z.string().nullable().optional(),\n appearanceColor: z.string().nullable().optional(),\n source: z.string().nullable().optional(),\n duration: z.number().nullable().optional(),\n durationMinutes: z.number().nullable().optional(),\n location: z.string().nullable().optional(),\n allDay: z.boolean().nullable().optional(),\n recurrenceRule: z.string().nullable().optional(),\n recurrenceEnd: z.string().nullable().optional(),\n participants: z.array(\n z.object({\n userId: z\n .string()\n .uuid()\n .optional()\n .describe(\n 'Absent for an external guest, who has no person/customer/staff record and is identified by email instead. Identify a participant by userId when present, otherwise by its normalized email.',\n ),\n name: z.string().optional(),\n email: z.string().optional(),\n status: z.string().optional(),\n }),\n ).nullable().optional(),\n reminderMinutes: z.number().nullable().optional(),\n visibility: z.string().nullable().optional(),\n linkedEntities: z.array(\n z.object({\n id: z.string().uuid(),\n type: z.string(),\n label: z.string(),\n }),\n ).nullable().optional(),\n guestPermissions: z\n .object({\n canInviteOthers: z.boolean().optional(),\n canModify: z.boolean().optional(),\n canSeeList: z.boolean().optional(),\n })\n .nullable()\n .optional(),\n organizationId: z.string().uuid().nullable().optional(),\n tenantId: z.string().uuid().nullable().optional(),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n authorName: z.string().nullable().optional(),\n authorEmail: z.string().nullable().optional(),\n dealTitle: z.string().nullable().optional(),\n customValues: z.record(z.string(), z.unknown()).nullable().optional(),\n _integrations: z.record(z.string(), z.unknown()).optional(),\n })\n .passthrough()\n\nconst interactionListResponseSchema = z.object({\n items: z.array(interactionListItemSchema),\n nextCursor: z.string().optional(),\n})\n\nconst interactionCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nexport const openApi = createCustomersCrudOpenApi({\n resourceName: 'Interaction',\n querySchema: listSchema,\n listResponseSchema: interactionListResponseSchema,\n create: {\n schema: interactionCreateSchema,\n responseSchema: interactionCreateResponseSchema,\n description: 'Creates a new interaction linked to a customer entity or deal.',\n },\n update: {\n schema: interactionUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates fields for an existing interaction.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes an interaction identified by `id`. Accepts id via body or query string.',\n },\n})\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { NextResponse } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sql } from 'kysely'\nimport { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'\nimport { CrudHttpError, isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { loadCustomFieldValues } from '@open-mercato/shared/lib/crud/custom-fields'\nimport { normalizeCustomFieldResponse } from '@open-mercato/shared/lib/custom-fields/normalize'\nimport { applyResponseEnrichers } from '@open-mercato/shared/lib/crud/enricher-runner'\nimport type { EnricherContext } from '@open-mercato/shared/lib/crud/response-enricher'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { resolveTenantEncryptionService } from '@open-mercato/shared/lib/encryption/customFieldValues'\nimport { resolveEncryptedSortFields, resolveEncryptedSortMaxRows } from '@open-mercato/shared/lib/query/encrypted-sort'\nimport { escapeLikePattern } from '@open-mercato/shared/lib/db/escapeLikePattern'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { CustomerDeal, CustomerInteraction } from '../../data/entities'\nimport { User } from '@open-mercato/core/modules/auth/data/entities'\nimport { interactionCreateSchema, interactionUpdateSchema } from '../../data/validators'\nimport { parseScopedCommandInput } from '../utils'\nimport {\n createCustomersCrudOpenApi,\n defaultOkResponseSchema,\n} from '../openapi'\nimport { CUSTOMER_INTERACTION_ENTITY_ID } from '../../lib/interactionCompatibility'\nimport { applyEmailVisibilityFilter } from '../../lib/visibilityFilter'\nimport { resolveEncryptedSortPage } from './encryptedSortPage'\nimport { resolveCanonicalActivityTargetId } from '../../lib/legacyActivityBridge'\nimport type { CommandBus, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\nconst rawBodySchema = z.object({}).passthrough()\n\nconst interactionSortFieldSchema = z.enum([\n 'scheduledAt',\n 'occurredAt',\n 'createdAt',\n 'updatedAt',\n 'status',\n 'priority',\n 'interactionType',\n 'title',\n])\n\nexport const listSchema = z\n .object({\n limit: z.coerce.number().min(1).max(100).default(25),\n cursor: z.string().optional(),\n entityId: z.string().uuid().optional(),\n dealId: z.string().uuid().optional(),\n status: z.string().optional(),\n interactionType: z.string().optional(),\n type: z.string().optional(),\n excludeInteractionType: z.string().optional(),\n search: z.string().trim().min(1).optional(),\n from: z.coerce.date().optional(),\n to: z.coerce.date().optional(),\n recurrenceMasters: z.enum(['true', 'false']).optional(),\n pinned: z.enum(['true', 'false']).optional(),\n sortField: interactionSortFieldSchema.optional(),\n sortDir: z.enum(['asc', 'desc']).optional(),\n })\n .passthrough()\n\nconst routeMetadata = {\n GET: { requireAuth: true, requireFeatures: ['customers.interactions.view'] },\n POST: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n PUT: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['customers.interactions.manage'] },\n}\n\nexport const metadata = routeMetadata\n\nconst crud = makeCrudRoute({\n metadata: routeMetadata,\n orm: {\n entity: CustomerInteraction,\n idField: 'id',\n orgField: 'organizationId',\n tenantField: 'tenantId',\n softDeleteField: 'deletedAt',\n },\n enrichers: { entityId: 'customers.interaction' },\n indexer: {\n entityType: 'customers:customer_interaction',\n },\n actions: {\n create: {\n commandId: 'customers.interactions.create',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n return parseScopedCommandInput(interactionCreateSchema, raw ?? {}, ctx, translate)\n },\n response: ({ result }) => ({ id: result?.interactionId ?? result?.id ?? null }),\n status: 201,\n },\n update: {\n commandId: 'customers.interactions.update',\n schema: rawBodySchema,\n mapInput: async ({ raw, ctx }) => {\n const { translate } = await resolveTranslations()\n const parsed = parseScopedCommandInput(interactionUpdateSchema, raw ?? {}, ctx, translate)\n // Bridge legacy `customer_activities` rows into `customer_interactions`\n // before the canonical update runs so historical activities (#1807)\n // remain editable through the new dialog. No-op when the canonical\n // record already exists.\n const tenantId = ctx.auth?.tenantId ?? null\n if (typeof parsed.id === 'string' && tenantId) {\n try {\n const em = ctx.container.resolve('em') as EntityManager\n const commandBus = ctx.container.resolve('commandBus') as CommandBus\n const commandContext: CommandRuntimeContext = {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: ctx.organizationScope ?? null,\n selectedOrganizationId: ctx.selectedOrganizationId ?? null,\n organizationIds: ctx.organizationIds ?? null,\n request: ctx.request,\n }\n await resolveCanonicalActivityTargetId(em, commandBus, commandContext, parsed.id, tenantId)\n } catch (err) {\n // Bridging is best-effort; downstream lookup will surface a 404\n // when neither canonical nor legacy rows exist.\n logger.warn('Legacy interaction bridge failed', { component: 'interactions.put', id: parsed.id, err })\n }\n }\n return parsed\n },\n response: () => ({ ok: true }),\n },\n delete: {\n commandId: 'customers.interactions.delete',\n schema: rawBodySchema,\n mapInput: async ({ parsed, ctx }) => {\n const { translate } = await resolveTranslations()\n const id =\n parsed?.body?.id ??\n parsed?.id ??\n parsed?.query?.id ??\n (ctx.request ? new URL(ctx.request.url).searchParams.get('id') : null)\n if (!id) {\n throw new CrudHttpError(400, {\n error: translate('customers.errors.interaction_required', 'Interaction id is required'),\n })\n }\n return { id }\n },\n response: () => ({ ok: true }),\n },\n },\n})\n\nconst { POST, PUT, DELETE } = crud\n\nexport { POST, PUT, DELETE }\n\ntype InteractionListRow = {\n id: string\n entity_id: string\n deal_id: string | null\n interaction_type: string\n title: string | null\n body: string | null\n status: string\n scheduled_at: Date | null\n occurred_at: Date | null\n priority: number | null\n author_user_id: string | null\n owner_user_id: string | null\n external_message_id: string | null\n appearance_icon: string | null\n appearance_color: string | null\n source: string | null\n duration_minutes: number | null\n location: string | null\n all_day: boolean | null\n recurrence_rule: string | null\n recurrence_end: Date | null\n participants: Array<{ userId?: string; name?: string; email?: string; status?: string }> | null\n reminder_minutes: number | null\n visibility: string | null\n linked_entities: Array<{ id: string; type: string; label: string }> | null\n guest_permissions: { canInviteOthers?: boolean; canModify?: boolean; canSeeList?: boolean } | null\n pinned: boolean\n organization_id: string\n tenant_id: string\n created_at: Date\n updated_at: Date\n __sort_value: string | number | Date | null\n}\n\ntype CursorPayload = {\n id: string\n sortValue: string | number | null\n}\n\ntype RbacServiceLike = {\n getGrantedFeatures?: (userId: string, input: { tenantId: string | null; organizationId: string | null }) => Promise<string[]>\n}\n\nconst cursorSchema = z.object({\n id: z.string().uuid(),\n sortValue: z.union([z.string(), z.number(), z.null()]),\n})\n\nconst interactionSortConfig = {\n scheduledAt: { column: 'scheduled_at', type: 'date' as const, defaultDir: 'asc' as const },\n occurredAt: { column: 'occurred_at', type: 'date' as const, defaultDir: 'desc' as const },\n createdAt: { column: 'created_at', type: 'date' as const, defaultDir: 'desc' as const },\n updatedAt: { column: 'updated_at', type: 'date' as const, defaultDir: 'desc' as const },\n status: { column: 'status', type: 'text' as const, defaultDir: 'asc' as const },\n priority: { column: 'priority', type: 'number' as const, defaultDir: 'desc' as const },\n interactionType: { column: 'interaction_type', type: 'text' as const, defaultDir: 'asc' as const },\n title: { column: 'title', type: 'text' as const, defaultDir: 'asc' as const },\n} as const\n\nfunction toIsoString(value: unknown): string | null {\n if (value == null) return null\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value.toISOString()\n }\n if (typeof value === 'string') {\n const trimmed = value.trim()\n if (!trimmed.length) return null\n const parsed = new Date(trimmed)\n return Number.isNaN(parsed.getTime()) ? trimmed : parsed.toISOString()\n }\n return null\n}\n\nfunction normalizeCursorValue(\n value: string | number | Date | null,\n type: 'date' | 'number' | 'text',\n): string | number | null {\n if (value == null) return null\n if (type === 'number') {\n if (typeof value === 'number' && Number.isFinite(value)) return value\n if (typeof value === 'string') {\n const parsed = Number(value)\n return Number.isNaN(parsed) ? null : parsed\n }\n return null\n }\n if (type === 'date') {\n return toIsoString(value)\n }\n if (typeof value === 'string') return value\n if (value instanceof Date) return value.toISOString()\n return String(value)\n}\n\nfunction encodeCursor(payload: CursorPayload): string {\n return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64')\n}\n\nfunction decodeCursor(token: string | undefined, type: 'date' | 'number' | 'text'): CursorPayload | null {\n if (!token) return null\n try {\n const decoded = Buffer.from(token, 'base64').toString('utf8')\n const parsed = cursorSchema.parse(JSON.parse(decoded))\n return {\n id: parsed.id,\n sortValue: normalizeCursorValue(parsed.sortValue, type),\n }\n } catch {\n return null\n }\n}\n\nfunction buildSortSql(\n sortField: keyof typeof interactionSortConfig,\n sortDir: 'asc' | 'desc',\n): string {\n const config = interactionSortConfig[sortField]\n if (config.type === 'date') {\n const sentinel =\n sortDir === 'asc'\n ? \"timestamp with time zone '9999-12-31T23:59:59.999Z'\"\n : \"timestamp with time zone '0001-01-01T00:00:00.000Z'\"\n return `coalesce(${config.column}, ${sentinel})`\n }\n if (config.type === 'number') {\n const sentinel = sortDir === 'asc' ? '2147483647' : '-2147483648'\n return `coalesce(${config.column}, ${sentinel})`\n }\n const sentinel = sortDir === 'asc' ? \"'~~~~~~~~~~'\" : \"''\"\n return `coalesce(${config.column}, ${sentinel})`\n}\n\nconst INTERACTION_LIST_COLUMNS = [\n 'id',\n 'entity_id',\n 'deal_id',\n 'interaction_type',\n 'title',\n 'body',\n 'status',\n 'scheduled_at',\n 'occurred_at',\n 'priority',\n 'author_user_id',\n 'owner_user_id',\n 'external_message_id',\n 'appearance_icon',\n 'appearance_color',\n 'source',\n 'duration_minutes',\n 'location',\n 'all_day',\n 'recurrence_rule',\n 'recurrence_end',\n 'participants',\n 'reminder_minutes',\n 'visibility',\n 'linked_entities',\n 'guest_permissions',\n 'pinned',\n 'organization_id',\n 'tenant_id',\n 'created_at',\n 'updated_at',\n] as const\n\n// Shared by both the SQL keyset path and the encrypted-sort candidate scan\n// so the two paths never drift on which rows are in scope.\nfunction applyInteractionListFilters(\n baseQuery: any,\n params: {\n tenantId: string\n organizationIds: string[]\n query: z.infer<typeof listSchema>\n },\n): any {\n let q = baseQuery.where('deleted_at', 'is', null).where('tenant_id', '=', params.tenantId)\n if (params.organizationIds.length > 0) q = q.where('organization_id', 'in', params.organizationIds)\n const { query } = params\n if (query.entityId) q = q.where('entity_id', '=', query.entityId)\n if (query.dealId) q = q.where('deal_id', '=', query.dealId)\n if (query.status) q = q.where('status', '=', query.status)\n if (query.interactionType) q = q.where('interaction_type', '=', query.interactionType)\n if (query.type) {\n const types = query.type.split(',').map((t) => t.trim()).filter(Boolean)\n if (types.length > 0) q = q.where('interaction_type', 'in', types)\n }\n if (query.pinned === 'true') {\n q = q.where('pinned', '=', true)\n } else if (query.pinned === 'false') {\n q = q.where('pinned', '=', false)\n }\n if (query.excludeInteractionType) q = q.where('interaction_type', '!=', query.excludeInteractionType)\n if (query.search) {\n // NOTE: for tenants with data encryption enabled, `title`/`body` are\n // ciphertext at rest (see encryption.ts), so this ILIKE matches encrypted\n // bytes and returns no rows \u2014 substring search over encrypted free-text\n // columns is unsupported, the same documented limitation as\n // customer_activity / customer_comment. The returned page's title/body are\n // still decrypted for display further below.\n const searchTerm = `%${escapeLikePattern(query.search)}%`\n q = q.where(sql<boolean>`coalesce(title, '') ilike ${searchTerm} or coalesce(body, '') ilike ${searchTerm}`)\n }\n if (query.recurrenceMasters === 'true') {\n q = q.where('recurrence_rule', 'is not', null)\n if (query.from) {\n q = q.where(sql<boolean>`recurrence_end is null or recurrence_end >= ${query.from}`)\n }\n if (query.to) {\n q = q.where(sql<boolean>`coalesce(occurred_at, scheduled_at, created_at) <= ${query.to}`)\n }\n } else {\n if (query.from) {\n q = q.where(sql<boolean>`coalesce(occurred_at, scheduled_at, created_at) >= ${query.from}`)\n }\n if (query.to) {\n q = q.where(sql<boolean>`coalesce(occurred_at, scheduled_at, created_at) <= ${query.to}`)\n }\n }\n return q\n}\n\nasync function resolveUserFeatures(\n container: { resolve: (name: string) => unknown },\n userId: string,\n tenantId: string | null,\n organizationId: string | null,\n): Promise<string[] | undefined> {\n try {\n const rbac = container.resolve('rbacService') as RbacServiceLike | undefined\n if (!rbac?.getGrantedFeatures) return undefined\n return await rbac.getGrantedFeatures(userId, { tenantId, organizationId })\n } catch {\n return undefined\n }\n}\n\nasync function buildEnricherContext(\n container: { resolve: (name: string) => unknown },\n auth: NonNullable<Awaited<ReturnType<typeof getAuthFromRequest>>>,\n organizationId: string | null,\n precomputedUserFeatures?: { userId: string; features: string[] | undefined },\n): Promise<EnricherContext> {\n const userId =\n (typeof auth.sub === 'string' && auth.sub.trim().length > 0\n ? auth.sub\n : typeof auth.userId === 'string' && auth.userId.trim().length > 0\n ? auth.userId\n : typeof auth.keyId === 'string' && auth.keyId.trim().length > 0\n ? auth.keyId\n : 'system')\n\n // Reuse features already resolved for this same user (the GET handler resolves\n // them once for the visibility filter) to avoid a second RBAC lookup per request.\n const userFeatures =\n precomputedUserFeatures && precomputedUserFeatures.userId === userId\n ? precomputedUserFeatures.features\n : await resolveUserFeatures(container, userId, auth.tenantId ?? null, organizationId)\n\n return {\n organizationId: organizationId ?? '',\n tenantId: auth.tenantId ?? '',\n userId,\n em: container.resolve('em'),\n container,\n userFeatures,\n }\n}\n\nexport async function GET(req: Request) {\n try {\n const queryUrl = new URL(req.url)\n const query = listSchema.parse(Object.fromEntries(queryUrl.searchParams))\n const container = await createRequestContainer()\n const auth = await getAuthFromRequest(req)\n const { translate } = await resolveTranslations()\n\n if (!auth || !auth.tenantId) {\n throw new CrudHttpError(401, {\n error: translate('customers.errors.unauthorized', 'Unauthorized'),\n })\n }\n\n const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })\n const organizationIds = Array.isArray(scope?.filterIds) && scope.filterIds.length > 0\n ? scope.filterIds\n : auth.orgId\n ? [auth.orgId]\n : []\n const selectedOrganizationId = scope?.selectedId ?? auth.orgId ?? organizationIds[0] ?? null\n const em = (container.resolve('em') as EntityManager).fork()\n const db = em.getKysely<any>() as any\n\n const requestedSortField = query.sortField ?? 'scheduledAt'\n const sortConfig = interactionSortConfig[requestedSortField]\n const sortDir = query.sortDir ?? sortConfig.defaultDir\n const sortSql = buildSortSql(requestedSortField, sortDir)\n const cursor = decodeCursor(query.cursor, sortConfig.type)\n if (query.cursor && !cursor) {\n throw new CrudHttpError(400, {\n error: translate('customers.interactions.cursor.invalid', 'Invalid cursor'),\n })\n }\n\n // \u2500\u2500 Email visibility filter (2026-05-27) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n // Non-email interactions pass through; email rows with visibility='private'\n // are filtered out unless the caller is the author or has admin bypass.\n // API-key callers have no user identity (`auth.sub` undefined): resolve the\n // viewer to null so they never gain the author bypass and only see shared\n // emails (fail-closed). Mirrors counts/people/activities routes.\n const viewerUserId = auth.isApiKey ? null : (auth.sub ?? null)\n const encryptionService = resolveTenantEncryptionService(em)\n // Encrypted sort columns can't use SQL keyset ordering on ciphertext, so an\n // encrypted sort field takes a bounded candidate-scan + in-memory-sort path\n // instead of the SQL path below. Resolved alongside the independent\n // visibility-feature lookup rather than after it.\n const [callerUserFeatures, encryptedSortFields] = await Promise.all([\n viewerUserId\n ? resolveUserFeatures(container, viewerUserId, auth.tenantId ?? null, selectedOrganizationId)\n : Promise.resolve(undefined),\n resolveEncryptedSortFields(\n encryptionService,\n CUSTOMER_INTERACTION_ENTITY_ID,\n [sortConfig.column],\n auth.tenantId,\n selectedOrganizationId,\n ),\n ])\n const sortFieldIsEncrypted = encryptedSortFields.has(sortConfig.column)\n\n let pageRows: InteractionListRow[]\n let hasMore: boolean\n\n if (sortFieldIsEncrypted) {\n let candidateQuery = applyInteractionListFilters(\n db.selectFrom('customer_interactions').select(['id', sortConfig.column]),\n { tenantId: auth.tenantId, organizationIds, query },\n )\n candidateQuery = applyEmailVisibilityFilter(candidateQuery as any, {\n currentUserId: viewerUserId,\n userFeatures: callerUserFeatures,\n })\n const cap = resolveEncryptedSortMaxRows()\n if (cap !== null) {\n candidateQuery = candidateQuery.limit(cap).orderBy('id', 'asc')\n }\n const candidateRows = await candidateQuery.execute() as Array<{ id: string } & Record<string, unknown>>\n if (cap !== null && candidateRows.length >= cap) {\n logger.warn('Encrypted sort candidate scan hit OM_ENCRYPTED_SORT_MAX_ROWS cap; results may be incomplete', {\n component: 'interactions.GET',\n cap,\n sortField: sortConfig.column,\n tenantId: auth.tenantId,\n })\n }\n\n const decryptPayload = encryptionService?.decryptEntityPayload?.bind(encryptionService)\n const { pageIds, hasMore: encryptedHasMore } = await resolveEncryptedSortPage({\n candidates: candidateRows,\n decryptRow: async (row) => {\n if (!decryptPayload) return row\n try {\n const decrypted = await decryptPayload(CUSTOMER_INTERACTION_ENTITY_ID, row, auth.tenantId, selectedOrganizationId)\n return { ...row, ...decrypted }\n } catch (err) {\n logger.error('error decrypting sort candidate', { component: 'interactions.GET', err })\n return row\n }\n },\n sortField: sortConfig.column,\n sortDir,\n cursorId: cursor?.id ?? null,\n limit: query.limit,\n })\n hasMore = encryptedHasMore\n\n if (pageIds.length === 0) {\n pageRows = []\n } else {\n let pageQuery = applyInteractionListFilters(\n db.selectFrom('customer_interactions').select([...INTERACTION_LIST_COLUMNS, sql`${sql.raw(sortSql)}`.as('__sort_value')]),\n { tenantId: auth.tenantId, organizationIds, query },\n )\n pageQuery = applyEmailVisibilityFilter(pageQuery as any, {\n currentUserId: viewerUserId,\n userFeatures: callerUserFeatures,\n })\n pageQuery = pageQuery.where('id', 'in', pageIds)\n const rawPageRows = await pageQuery.execute() as InteractionListRow[]\n const byId = new Map(rawPageRows.map((row) => [row.id, row]))\n pageRows = pageIds\n .map((id) => byId.get(id))\n .filter((row): row is InteractionListRow => row != null)\n }\n } else {\n let rowsQuery = applyInteractionListFilters(\n db\n .selectFrom('customer_interactions')\n .select([...INTERACTION_LIST_COLUMNS, sql`${sql.raw(sortSql)}`.as('__sort_value')])\n .limit(query.limit + 1),\n { tenantId: auth.tenantId, organizationIds, query },\n )\n\n if (cursor) {\n const op = sortDir === 'asc' ? '>' : '<'\n const opRaw = sql.raw(op)\n const sortRaw = sql.raw(sortSql)\n rowsQuery = rowsQuery.where((eb: any) => eb.or([\n sql<boolean>`${sortRaw} ${opRaw} ${cursor.sortValue}`,\n eb.and([\n sql<boolean>`${sortRaw} = ${cursor.sortValue}`,\n eb('id', op, cursor.id),\n ]),\n ]))\n }\n\n rowsQuery = applyEmailVisibilityFilter(rowsQuery as any, {\n currentUserId: viewerUserId,\n userFeatures: callerUserFeatures,\n })\n\n rowsQuery = rowsQuery.orderBy(sql`${sql.raw(sortSql)} ${sql.raw(sortDir)}`).orderBy('id', sortDir)\n\n const rows = await rowsQuery.execute() as InteractionListRow[]\n pageRows = rows.slice(0, query.limit)\n hasMore = rows.length > query.limit\n }\n\n const authorIds = Array.from(\n new Set(\n pageRows\n .map((row) => (typeof row.author_user_id === 'string' ? row.author_user_id : null))\n .filter((value): value is string => !!value),\n ),\n )\n const dealIds = Array.from(\n new Set(\n pageRows\n .map((row) => (typeof row.deal_id === 'string' ? row.deal_id : null))\n .filter((value): value is string => !!value),\n ),\n )\n const interactionIds = pageRows.map((row) => row.id)\n\n const [users, deals, customFieldValues, interactionRecords] = await Promise.all([\n authorIds.length > 0 ? findWithDecryption(em, User, { id: { $in: authorIds } }, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId }) : Promise.resolve([]),\n dealIds.length > 0 ? findWithDecryption(em, CustomerDeal, { id: { $in: dealIds } }, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId }) : Promise.resolve([]),\n interactionIds.length > 0\n ? loadCustomFieldValues({\n em,\n entityId: CUSTOMER_INTERACTION_ENTITY_ID,\n recordIds: interactionIds,\n tenantIdByRecord: Object.fromEntries(pageRows.map((row) => [row.id, row.tenant_id])),\n organizationIdByRecord: Object.fromEntries(pageRows.map((row) => [row.id, row.organization_id])),\n tenantFallbacks: [auth.tenantId].filter((value): value is string => !!value),\n })\n : Promise.resolve<Record<string, Record<string, unknown>>>({}),\n interactionIds.length > 0\n ? findWithDecryption(em, CustomerInteraction, { id: { $in: interactionIds } } as never, undefined, { tenantId: auth.tenantId, organizationId: selectedOrganizationId })\n : Promise.resolve([]),\n ])\n\n const userMap = new Map(\n users.map((user) => [\n user.id,\n {\n name: user.name ?? null,\n email: user.email ?? null,\n },\n ]),\n )\n const dealMap = new Map(\n deals.map((deal) => [deal.id, deal.title]),\n )\n // title/body are encrypted at rest (see encryption.ts). The kysely rows above\n // carry ciphertext when tenant encryption is enabled, so override them with the\n // decrypted values from findWithDecryption for the returned page.\n const interactionContentMap = new Map(\n (interactionRecords as Array<{ id: string; title?: string | null; body?: string | null }>).map(\n (record) => [record.id, { title: record.title ?? null, body: record.body ?? null }],\n ),\n )\n\n const baseItems = pageRows.map((row) => ({\n id: row.id,\n entityId: row.entity_id,\n dealId: row.deal_id ?? null,\n interactionType: row.interaction_type,\n title: (interactionContentMap.has(row.id) ? interactionContentMap.get(row.id)!.title : row.title) ?? null,\n body: (interactionContentMap.has(row.id) ? interactionContentMap.get(row.id)!.body : row.body) ?? null,\n status: row.status,\n scheduledAt: toIsoString(row.scheduled_at),\n occurredAt: toIsoString(row.occurred_at),\n priority: row.priority ?? null,\n authorUserId: row.author_user_id ?? null,\n ownerUserId: row.owner_user_id ?? null,\n externalMessageId: row.external_message_id ?? null,\n appearanceIcon: row.appearance_icon ?? null,\n appearanceColor: row.appearance_color ?? null,\n source: row.source ?? null,\n duration: row.duration_minutes ?? null,\n durationMinutes: row.duration_minutes ?? null,\n location: row.location ?? null,\n allDay: row.all_day ?? null,\n recurrenceRule: row.recurrence_rule ?? null,\n recurrenceEnd: toIsoString(row.recurrence_end),\n participants: row.participants ?? null,\n reminderMinutes: row.reminder_minutes ?? null,\n visibility: row.visibility ?? null,\n linkedEntities: row.linked_entities ?? null,\n guestPermissions: row.guest_permissions ?? null,\n pinned: row.pinned ?? false,\n organizationId: row.organization_id,\n tenantId: row.tenant_id,\n createdAt: toIsoString(row.created_at) ?? new Date().toISOString(),\n updatedAt: toIsoString(row.updated_at) ?? new Date().toISOString(),\n authorName: row.author_user_id ? userMap.get(row.author_user_id)?.name ?? null : null,\n authorEmail: row.author_user_id ? userMap.get(row.author_user_id)?.email ?? null : null,\n dealTitle: row.deal_id ? dealMap.get(row.deal_id) ?? null : null,\n customValues: normalizeCustomFieldResponse(customFieldValues[row.id]) ?? null,\n }))\n\n const enricherContext = await buildEnricherContext(\n container,\n auth,\n selectedOrganizationId,\n viewerUserId ? { userId: viewerUserId, features: callerUserFeatures } : undefined,\n )\n const enriched = await applyResponseEnrichers(baseItems, 'customers.interaction', enricherContext)\n\n let nextCursor: string | undefined\n if (hasMore && pageRows.length > 0) {\n const last = pageRows[pageRows.length - 1]\n nextCursor = encodeCursor({\n id: last.id,\n sortValue: normalizeCursorValue(last.__sort_value, sortConfig.type),\n })\n }\n\n return NextResponse.json({\n items: enriched.items,\n nextCursor,\n })\n } catch (err) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n if (err instanceof z.ZodError) {\n return NextResponse.json(\n { error: 'Validation failed', details: err.issues },\n { status: 400 },\n )\n }\n logger.error('customers.interactions.get failed', { err })\n const { translate } = await resolveTranslations()\n return NextResponse.json(\n { error: translate('customers.interactions.load.error', 'Failed to load interactions.') },\n { status: 500 },\n )\n }\n}\n\nconst interactionListItemSchema = z\n .object({\n id: z.string().uuid(),\n entityId: z.string().uuid().nullable(),\n dealId: z.string().uuid().nullable(),\n interactionType: z.string(),\n title: z.string().nullable(),\n body: z.string().nullable(),\n status: z.string(),\n scheduledAt: z.string().nullable(),\n occurredAt: z.string().nullable(),\n priority: z.number().nullable(),\n authorUserId: z.string().uuid().nullable(),\n ownerUserId: z.string().uuid().nullable(),\n externalMessageId: z.string().uuid().nullable().optional(),\n appearanceIcon: z.string().nullable().optional(),\n appearanceColor: z.string().nullable().optional(),\n source: z.string().nullable().optional(),\n duration: z.number().nullable().optional(),\n durationMinutes: z.number().nullable().optional(),\n location: z.string().nullable().optional(),\n allDay: z.boolean().nullable().optional(),\n recurrenceRule: z.string().nullable().optional(),\n recurrenceEnd: z.string().nullable().optional(),\n participants: z.array(\n z.object({\n userId: z\n .string()\n .uuid()\n .optional()\n .describe(\n 'Absent for an external guest, who has no person/customer/staff record and is identified by email instead. Identify a participant by userId when present, otherwise by its normalized email.',\n ),\n name: z.string().optional(),\n email: z.string().optional(),\n status: z.string().optional(),\n }),\n ).nullable().optional(),\n reminderMinutes: z.number().nullable().optional(),\n visibility: z.string().nullable().optional(),\n linkedEntities: z.array(\n z.object({\n id: z.string().uuid(),\n type: z.string(),\n label: z.string(),\n }),\n ).nullable().optional(),\n guestPermissions: z\n .object({\n canInviteOthers: z.boolean().optional(),\n canModify: z.boolean().optional(),\n canSeeList: z.boolean().optional(),\n })\n .nullable()\n .optional(),\n organizationId: z.string().uuid().nullable().optional(),\n tenantId: z.string().uuid().nullable().optional(),\n createdAt: z.string().nullable(),\n updatedAt: z.string().nullable(),\n authorName: z.string().nullable().optional(),\n authorEmail: z.string().nullable().optional(),\n dealTitle: z.string().nullable().optional(),\n customValues: z.record(z.string(), z.unknown()).nullable().optional(),\n _integrations: z.record(z.string(), z.unknown()).optional(),\n })\n .passthrough()\n\nconst interactionListResponseSchema = z.object({\n items: z.array(interactionListItemSchema),\n nextCursor: z.string().optional(),\n})\n\nconst interactionCreateResponseSchema = z.object({\n id: z.string().uuid().nullable(),\n})\n\nexport const openApi = createCustomersCrudOpenApi({\n resourceName: 'Interaction',\n querySchema: listSchema,\n listResponseSchema: interactionListResponseSchema,\n create: {\n schema: interactionCreateSchema,\n responseSchema: interactionCreateResponseSchema,\n description: 'Creates a new interaction linked to a customer entity or deal.',\n },\n update: {\n schema: interactionUpdateSchema,\n responseSchema: defaultOkResponseSchema,\n description: 'Updates fields for an existing interaction.',\n },\n del: {\n schema: z.object({ id: z.string().uuid() }),\n responseSchema: defaultOkResponseSchema,\n description: 'Soft-deletes an interaction identified by `id`. Accepts id via body or query string.',\n },\n})\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["sentinel"]
|
|
7
7
|
}
|
|
@@ -33,7 +33,7 @@ function resolveObjectLabels(objects) {
|
|
|
33
33
|
return objects.map((item) => `${item.entityModule}.${item.entityType} (${item.entityId})`);
|
|
34
34
|
}
|
|
35
35
|
async function mapAttachmentsForEmail(messageId, attachments) {
|
|
36
|
-
const
|
|
36
|
+
const emailAttachments = [];
|
|
37
37
|
let totalBytes = 0;
|
|
38
38
|
for (const attachment of attachments.slice(0, MAX_EMAIL_ATTACHMENTS)) {
|
|
39
39
|
const absolutePath = resolveAttachmentAbsolutePath(
|
|
@@ -61,13 +61,13 @@ async function mapAttachmentsForEmail(messageId, attachments) {
|
|
|
61
61
|
continue;
|
|
62
62
|
}
|
|
63
63
|
totalBytes += buffer.length;
|
|
64
|
-
|
|
64
|
+
emailAttachments.push({
|
|
65
65
|
filename: attachment.fileName,
|
|
66
66
|
content: buffer.toString("base64"),
|
|
67
67
|
contentType: attachment.mimeType || void 0
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
|
-
return
|
|
70
|
+
return emailAttachments;
|
|
71
71
|
}
|
|
72
72
|
async function renderMarkdownEmailBody(body) {
|
|
73
73
|
const ReactMarkdownModule = await import("react-markdown");
|
|
@@ -127,14 +127,13 @@ async function sendMessageEmailToRecipient(params) {
|
|
|
127
127
|
}
|
|
128
128
|
const copy = await buildEmailCopy(message.sentAt ?? /* @__PURE__ */ new Date());
|
|
129
129
|
const bodyHtml = await buildEmailBodyHtml(message);
|
|
130
|
-
const
|
|
131
|
-
logDebug("Sending recipient email
|
|
130
|
+
const emailAttachments = await mapAttachmentsForEmail(message.id, attachments);
|
|
131
|
+
logDebug("Sending recipient email", {
|
|
132
132
|
messageId: message.id,
|
|
133
133
|
recipientUserId,
|
|
134
134
|
recipientEmail,
|
|
135
135
|
hasViewUrl: Boolean(viewUrl),
|
|
136
|
-
attachmentsCount:
|
|
137
|
-
hasApiKey: Boolean(process.env.RESEND_API_KEY),
|
|
136
|
+
attachmentsCount: emailAttachments.length,
|
|
138
137
|
from: resolveDefaultEmailFromAddress() ?? null
|
|
139
138
|
});
|
|
140
139
|
await sendEmail({
|
|
@@ -151,19 +150,20 @@ async function sendMessageEmailToRecipient(params) {
|
|
|
151
150
|
attachmentNames: attachments.map((item) => item.fileName),
|
|
152
151
|
objectLabels: resolveObjectLabels(objects)
|
|
153
152
|
}),
|
|
154
|
-
attachments:
|
|
153
|
+
attachments: emailAttachments,
|
|
154
|
+
tenantId: message.tenantId,
|
|
155
|
+
organizationId: message.organizationId ?? null
|
|
155
156
|
});
|
|
156
157
|
}
|
|
157
158
|
async function sendMessageEmailToExternal(params) {
|
|
158
159
|
const { message, email, sender, objects, attachments } = params;
|
|
159
160
|
const copy = await buildEmailCopy(message.sentAt ?? /* @__PURE__ */ new Date());
|
|
160
161
|
const bodyHtml = await buildEmailBodyHtml(message);
|
|
161
|
-
const
|
|
162
|
-
logDebug("Sending external email
|
|
162
|
+
const emailAttachments = await mapAttachmentsForEmail(message.id, attachments);
|
|
163
|
+
logDebug("Sending external email", {
|
|
163
164
|
messageId: message.id,
|
|
164
165
|
email,
|
|
165
|
-
attachmentsCount:
|
|
166
|
-
hasApiKey: Boolean(process.env.RESEND_API_KEY),
|
|
166
|
+
attachmentsCount: emailAttachments.length,
|
|
167
167
|
from: resolveDefaultEmailFromAddress() ?? null
|
|
168
168
|
});
|
|
169
169
|
await sendEmail({
|
|
@@ -180,9 +180,11 @@ async function sendMessageEmailToExternal(params) {
|
|
|
180
180
|
attachmentNames: attachments.map((item) => item.fileName),
|
|
181
181
|
objectLabels: resolveObjectLabels(objects)
|
|
182
182
|
}),
|
|
183
|
-
attachments:
|
|
183
|
+
attachments: emailAttachments,
|
|
184
|
+
tenantId: message.tenantId,
|
|
185
|
+
organizationId: message.organizationId ?? null
|
|
184
186
|
});
|
|
185
|
-
logDebug("External email sent
|
|
187
|
+
logDebug("External email sent", {
|
|
186
188
|
messageId: message.id,
|
|
187
189
|
email
|
|
188
190
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/messages/lib/email-sender.ts"],
|
|
4
|
-
"sourcesContent": ["import * as React from 'react'\nimport { promises as fs } from 'fs'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveDefaultEmailFromAddress } from '@open-mercato/shared/lib/email/config'\nimport { loadDictionary } from '@open-mercato/shared/lib/i18n/server'\nimport { defaultLocale } from '@open-mercato/shared/lib/i18n/config'\nimport { createFallbackTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport type { Message, MessageObject } from '../data/entities'\nimport { MessageAccessToken } from '../data/entities'\nimport MessageEmail from '../emails/MessageEmail'\nimport { resolveAttachmentAbsolutePath } from '../../attachments/lib/storage'\nimport { generateAuthToken, hashAuthToken } from '../../auth/lib/tokenHash'\nimport type { MessageEmailAttachment } from './attachments'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'email-sender' })\n\nconst ACCESS_TOKEN_EXPIRY_HOURS = 24 * 7\nconst MAX_EMAIL_ATTACHMENTS = 10\nconst MAX_TOTAL_ATTACHMENT_BYTES = 20 * 1024 * 1024\n\nexport type SenderIdentity = {\n name: string | null\n email: string | null\n}\n\nfunction logDebug(message: string, details?: Record<string, unknown>) {\n logger.debug(message, details)\n}\n\nfunction resolveAppUrl(): string | null {\n const raw = process.env.APP_URL?.trim()\n if (!raw) return null\n return raw.replace(/\\/$/, '')\n}\n\nfunction buildSenderLabel(sender: SenderIdentity): string {\n const name = sender.name?.trim()\n if (name) return name\n const email = sender.email?.trim()\n if (email) return email\n return 'System'\n}\n\nfunction resolveObjectLabels(objects: MessageObject[]): string[] {\n return objects.map((item) => `${item.entityModule}.${item.entityType} (${item.entityId})`)\n}\n\ntype
|
|
5
|
-
"mappings": "AAAA,YAAY,WAAW;AACvB,SAAS,YAAY,UAAU;AAE/B,SAAS,iBAAiB;AAC1B,SAAS,sCAAsC;AAC/C,SAAS,sBAAsB;AAC/B,SAAS,qBAAqB;AAC9B,SAAS,gCAAgC;AAEzC,SAAS,0BAA0B;AACnC,OAAO,kBAAkB;AACzB,SAAS,qCAAqC;AAC9C,SAAS,mBAAmB,qBAAqB;AAEjD,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,eAAe,CAAC;AAE3E,MAAM,4BAA4B,KAAK;AACvC,MAAM,wBAAwB;AAC9B,MAAM,6BAA6B,KAAK,OAAO;AAO/C,SAAS,SAAS,SAAiB,SAAmC;AACpE,SAAO,MAAM,SAAS,OAAO;AAC/B;AAEA,SAAS,gBAA+B;AACtC,QAAM,MAAM,QAAQ,IAAI,SAAS,KAAK;AACtC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,IAAI,QAAQ,OAAO,EAAE;AAC9B;AAEA,SAAS,iBAAiB,QAAgC;AACxD,QAAM,OAAO,OAAO,MAAM,KAAK;AAC/B,MAAI,KAAM,QAAO;AACjB,QAAM,QAAQ,OAAO,OAAO,KAAK;AACjC,MAAI,MAAO,QAAO;AAClB,SAAO;AACT;AAEA,SAAS,oBAAoB,SAAoC;AAC/D,SAAO,QAAQ,IAAI,CAAC,SAAS,GAAG,KAAK,YAAY,IAAI,KAAK,UAAU,KAAK,KAAK,QAAQ,GAAG;AAC3F;AAQA,eAAe,uBACb,WACA,
|
|
4
|
+
"sourcesContent": ["import * as React from 'react'\nimport { promises as fs } from 'fs'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { sendEmail } from '@open-mercato/shared/lib/email/send'\nimport { resolveDefaultEmailFromAddress } from '@open-mercato/shared/lib/email/config'\nimport { loadDictionary } from '@open-mercato/shared/lib/i18n/server'\nimport { defaultLocale } from '@open-mercato/shared/lib/i18n/config'\nimport { createFallbackTranslator } from '@open-mercato/shared/lib/i18n/translate'\nimport type { Message, MessageObject } from '../data/entities'\nimport { MessageAccessToken } from '../data/entities'\nimport MessageEmail from '../emails/MessageEmail'\nimport { resolveAttachmentAbsolutePath } from '../../attachments/lib/storage'\nimport { generateAuthToken, hashAuthToken } from '../../auth/lib/tokenHash'\nimport type { MessageEmailAttachment } from './attachments'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'email-sender' })\n\nconst ACCESS_TOKEN_EXPIRY_HOURS = 24 * 7\nconst MAX_EMAIL_ATTACHMENTS = 10\nconst MAX_TOTAL_ATTACHMENT_BYTES = 20 * 1024 * 1024\n\nexport type SenderIdentity = {\n name: string | null\n email: string | null\n}\n\nfunction logDebug(message: string, details?: Record<string, unknown>) {\n logger.debug(message, details)\n}\n\nfunction resolveAppUrl(): string | null {\n const raw = process.env.APP_URL?.trim()\n if (!raw) return null\n return raw.replace(/\\/$/, '')\n}\n\nfunction buildSenderLabel(sender: SenderIdentity): string {\n const name = sender.name?.trim()\n if (name) return name\n const email = sender.email?.trim()\n if (email) return email\n return 'System'\n}\n\nfunction resolveObjectLabels(objects: MessageObject[]): string[] {\n return objects.map((item) => `${item.entityModule}.${item.entityType} (${item.entityId})`)\n}\n\ntype EmailAttachment = {\n filename: string\n content: string\n contentType?: string\n}\n\nasync function mapAttachmentsForEmail(\n messageId: string,\n attachments: MessageEmailAttachment[],\n): Promise<EmailAttachment[]> {\n const emailAttachments: EmailAttachment[] = []\n let totalBytes = 0\n\n for (const attachment of attachments.slice(0, MAX_EMAIL_ATTACHMENTS)) {\n const absolutePath = resolveAttachmentAbsolutePath(\n attachment.partitionCode,\n attachment.storagePath,\n attachment.storageDriver,\n )\n\n let buffer: Buffer\n try {\n buffer = await fs.readFile(absolutePath)\n } catch (error) {\n logDebug('Attachment skipped: file read failed', {\n messageId,\n fileName: attachment.fileName,\n error: error instanceof Error ? error.message : String(error),\n })\n continue\n }\n\n if ((totalBytes + buffer.length) > MAX_TOTAL_ATTACHMENT_BYTES) {\n logDebug('Attachment skipped: total size limit exceeded', {\n messageId,\n fileName: attachment.fileName,\n nextTotalBytes: totalBytes + buffer.length,\n })\n continue\n }\n\n totalBytes += buffer.length\n emailAttachments.push({\n filename: attachment.fileName,\n content: buffer.toString('base64'),\n contentType: attachment.mimeType || undefined,\n })\n }\n\n return emailAttachments\n}\n\nasync function renderMarkdownEmailBody(body: string) {\n const ReactMarkdownModule = await import('react-markdown')\n const remarkGfmModule = await import('remark-gfm')\n const ReactMarkdown =\n (ReactMarkdownModule.default ?? ReactMarkdownModule) as React.ComponentType<{\n remarkPlugins?: unknown\n children?: React.ReactNode\n }>\n const remarkGfmPlugin = remarkGfmModule.default ?? remarkGfmModule\n const { renderToStaticMarkup } = await import('react-dom/server')\n\n return renderToStaticMarkup(\n React.createElement(ReactMarkdown, { remarkPlugins: [remarkGfmPlugin] }, body),\n )\n}\n\nasync function buildEmailBodyHtml(message: Message): Promise<string | undefined> {\n if (message.bodyFormat !== 'markdown') return undefined\n if (!message.body) return undefined\n return renderMarkdownEmailBody(message.body)\n}\n\nasync function buildEmailCopy(sentAt: Date) {\n const dict = await loadDictionary(defaultLocale)\n const t = createFallbackTranslator(dict)\n return {\n preview: t('messages.email.preview', 'You received a message in Open Mercato'),\n heading: t('messages.email.heading', 'New message'),\n from: t('messages.email.from', 'From'),\n sentAt: t('messages.email.sentAt', 'Sent'),\n sentAtLabel: sentAt.toISOString(),\n viewCta: t('messages.email.viewCta', 'View message'),\n attachmentsLabel: t('messages.email.attachments', 'Attachments'),\n objectsLabel: t('messages.email.objects', 'Related records'),\n footer: t('messages.email.footer', 'Open Mercato messages'),\n }\n}\n\nexport async function createMessageAccessToken(\n em: EntityManager,\n messageId: string,\n recipientUserId: string,\n): Promise<string> {\n const rawToken = generateAuthToken()\n const expiresAt = new Date(Date.now() + ACCESS_TOKEN_EXPIRY_HOURS * 60 * 60 * 1000)\n const record = em.create(MessageAccessToken, {\n messageId,\n recipientUserId,\n token: hashAuthToken(rawToken),\n expiresAt,\n useCount: 0,\n })\n await em.persist(record).flush()\n logDebug('Created access token', {\n messageId,\n recipientUserId,\n expiresAt: expiresAt.toISOString(),\n })\n return rawToken\n}\n\nexport async function sendMessageEmailToRecipient(params: {\n em: EntityManager\n message: Message\n recipientUserId: string\n recipientEmail: string\n sender: SenderIdentity\n objects: MessageObject[]\n attachments: MessageEmailAttachment[]\n}): Promise<void> {\n const { em, message, recipientUserId, recipientEmail, sender, objects, attachments } = params\n const token = await createMessageAccessToken(em, message.id, recipientUserId)\n const appUrl = resolveAppUrl()\n const viewUrl = appUrl ? `${appUrl}/messages/view/${token}` : null\n if (!appUrl) {\n logDebug('APP_URL missing - email link omitted', { messageId: message.id })\n }\n const copy = await buildEmailCopy(message.sentAt ?? new Date())\n const bodyHtml = await buildEmailBodyHtml(message)\n const emailAttachments = await mapAttachmentsForEmail(message.id, attachments)\n logDebug('Sending recipient email', {\n messageId: message.id,\n recipientUserId,\n recipientEmail,\n hasViewUrl: Boolean(viewUrl),\n attachmentsCount: emailAttachments.length,\n from: resolveDefaultEmailFromAddress() ?? null,\n })\n\n await sendEmail({\n to: recipientEmail,\n subject: message.subject,\n react: MessageEmail({\n subject: message.subject,\n body: message.body,\n bodyHtml,\n senderName: buildSenderLabel(sender),\n sentAtLabel: copy.sentAtLabel,\n viewUrl,\n copy,\n attachmentNames: attachments.map((item) => item.fileName),\n objectLabels: resolveObjectLabels(objects),\n }),\n attachments: emailAttachments,\n tenantId: message.tenantId,\n organizationId: message.organizationId ?? null,\n })\n}\n\nexport async function sendMessageEmailToExternal(params: {\n message: Message\n email: string\n sender: SenderIdentity\n objects: MessageObject[]\n attachments: MessageEmailAttachment[]\n}): Promise<void> {\n const { message, email, sender, objects, attachments } = params\n const copy = await buildEmailCopy(message.sentAt ?? new Date())\n const bodyHtml = await buildEmailBodyHtml(message)\n const emailAttachments = await mapAttachmentsForEmail(message.id, attachments)\n logDebug('Sending external email', {\n messageId: message.id,\n email,\n attachmentsCount: emailAttachments.length,\n from: resolveDefaultEmailFromAddress() ?? null,\n })\n\n await sendEmail({\n to: email,\n subject: message.subject,\n react: MessageEmail({\n subject: message.subject,\n body: message.body,\n bodyHtml,\n senderName: buildSenderLabel(sender),\n sentAtLabel: copy.sentAtLabel,\n viewUrl: null,\n copy,\n attachmentNames: attachments.map((item) => item.fileName),\n objectLabels: resolveObjectLabels(objects),\n }),\n attachments: emailAttachments,\n tenantId: message.tenantId,\n organizationId: message.organizationId ?? null,\n })\n logDebug('External email sent', {\n messageId: message.id,\n email,\n })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -12,7 +12,7 @@ const emailDeliveryStrategy = {
|
|
|
12
12
|
defaultEnabled: true,
|
|
13
13
|
isConfigured: (ctx) => ctx.deliveryConfig.strategies.email.enabled === true,
|
|
14
14
|
async deliver(ctx) {
|
|
15
|
-
const { recipient, panelLink, title, body, actionLinks, deliveryConfig, t } = ctx;
|
|
15
|
+
const { notification, recipient, panelLink, title, body, actionLinks, deliveryConfig, t } = ctx;
|
|
16
16
|
if (!recipient?.email || !panelLink) {
|
|
17
17
|
debug("email skipped: missing recipient email or panelLink");
|
|
18
18
|
return;
|
|
@@ -34,6 +34,8 @@ const emailDeliveryStrategy = {
|
|
|
34
34
|
subject,
|
|
35
35
|
from: deliveryConfig.strategies.email.from,
|
|
36
36
|
replyTo: deliveryConfig.strategies.email.replyTo,
|
|
37
|
+
tenantId: notification.tenantId,
|
|
38
|
+
organizationId: notification.organizationId ?? null,
|
|
37
39
|
react: NotificationEmail({
|
|
38
40
|
title,
|
|
39
41
|
body,
|