@open-mercato/core 0.7.1-develop.7150.1.c1941e0c22 → 0.7.1-develop.7151.1.00d0391847

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/helpers/integration/communicationChannelsFixtures.js +77 -1
  3. package/dist/helpers/integration/communicationChannelsFixtures.js.map +2 -2
  4. package/dist/helpers/integration/crmFixtures.js +3 -0
  5. package/dist/helpers/integration/crmFixtures.js.map +2 -2
  6. package/dist/modules/auth/api/reset.js +7 -1
  7. package/dist/modules/auth/api/reset.js.map +2 -2
  8. package/dist/modules/auth/api/users/resend-invite/route.js +7 -1
  9. package/dist/modules/auth/api/users/resend-invite/route.js.map +2 -2
  10. package/dist/modules/auth/commands/users.js +7 -1
  11. package/dist/modules/auth/commands/users.js.map +2 -2
  12. package/dist/modules/communication_channels/api/post/test-seed/route.js +101 -26
  13. package/dist/modules/communication_channels/api/post/test-seed/route.js.map +3 -3
  14. package/dist/modules/communication_channels/di.js +8 -0
  15. package/dist/modules/communication_channels/di.js.map +2 -2
  16. package/dist/modules/communication_channels/lib/ensure-system-email-channel.js +57 -0
  17. package/dist/modules/communication_channels/lib/ensure-system-email-channel.js.map +7 -0
  18. package/dist/modules/communication_channels/lib/system-email-provider-config.js +32 -0
  19. package/dist/modules/communication_channels/lib/system-email-provider-config.js.map +7 -0
  20. package/dist/modules/communication_channels/lib/system-email.js +190 -0
  21. package/dist/modules/communication_channels/lib/system-email.js.map +7 -0
  22. package/dist/modules/communication_channels/lib/test-seed.js +130 -1
  23. package/dist/modules/communication_channels/lib/test-seed.js.map +2 -2
  24. package/dist/modules/communication_channels/setup.js +56 -1
  25. package/dist/modules/communication_channels/setup.js.map +2 -2
  26. package/dist/modules/customer_accounts/api/admin/users-invite.js +1 -0
  27. package/dist/modules/customer_accounts/api/admin/users-invite.js.map +2 -2
  28. package/dist/modules/customer_accounts/api/portal/users-invite.js +1 -0
  29. package/dist/modules/customer_accounts/api/portal/users-invite.js.map +2 -2
  30. package/dist/modules/customer_accounts/api/signup.js +8 -4
  31. package/dist/modules/customer_accounts/api/signup.js.map +2 -2
  32. package/dist/modules/customer_accounts/lib/invitationEmail.js +3 -1
  33. package/dist/modules/customer_accounts/lib/invitationEmail.js.map +2 -2
  34. package/dist/modules/customers/api/interactions/route.js +3 -0
  35. package/dist/modules/customers/api/interactions/route.js.map +2 -2
  36. package/dist/modules/messages/lib/email-sender.js +16 -14
  37. package/dist/modules/messages/lib/email-sender.js.map +2 -2
  38. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js +3 -1
  39. package/dist/modules/notifications/lib/strategies/email-delivery-strategy.js.map +2 -2
  40. package/dist/modules/sales/api/quotes/accept/route.js +3 -1
  41. package/dist/modules/sales/api/quotes/accept/route.js.map +2 -2
  42. package/dist/modules/sales/api/quotes/send/route.js +13 -4
  43. package/dist/modules/sales/api/quotes/send/route.js.map +3 -3
  44. package/package.json +7 -7
  45. package/src/helpers/integration/communicationChannelsFixtures.ts +129 -0
  46. package/src/helpers/integration/crmFixtures.ts +4 -1
  47. package/src/modules/auth/api/reset.ts +7 -1
  48. package/src/modules/auth/api/users/resend-invite/route.ts +7 -1
  49. package/src/modules/auth/commands/users.ts +7 -1
  50. package/src/modules/auth/i18n/de.json +4 -0
  51. package/src/modules/auth/i18n/en.json +4 -0
  52. package/src/modules/auth/i18n/es.json +4 -0
  53. package/src/modules/auth/i18n/ko.json +4 -0
  54. package/src/modules/auth/i18n/pl.json +4 -0
  55. package/src/modules/communication_channels/api/post/test-seed/route.ts +121 -29
  56. package/src/modules/communication_channels/di.ts +8 -0
  57. package/src/modules/communication_channels/lib/ensure-system-email-channel.ts +85 -0
  58. package/src/modules/communication_channels/lib/system-email-provider-config.ts +62 -0
  59. package/src/modules/communication_channels/lib/system-email.ts +316 -0
  60. package/src/modules/communication_channels/lib/test-seed.ts +198 -0
  61. package/src/modules/communication_channels/setup.ts +91 -1
  62. package/src/modules/customer_accounts/api/admin/users-invite.ts +1 -0
  63. package/src/modules/customer_accounts/api/portal/users-invite.ts +1 -0
  64. package/src/modules/customer_accounts/api/signup.ts +6 -2
  65. package/src/modules/customer_accounts/lib/invitationEmail.ts +3 -0
  66. package/src/modules/customers/api/interactions/route.ts +4 -0
  67. package/src/modules/messages/lib/email-sender.ts +18 -16
  68. package/src/modules/notifications/lib/strategies/email-delivery-strategy.ts +3 -1
  69. package/src/modules/sales/api/quotes/accept/route.ts +2 -0
  70. 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;AASlC,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,EACpD,CAAC;AACH;",
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 resendAttachments = [];
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
- resendAttachments.push({
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 resendAttachments;
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 resendAttachments = await mapAttachmentsForEmail(message.id, attachments);
131
- logDebug("Sending recipient email via Resend", {
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: resendAttachments.length,
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: resendAttachments
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 resendAttachments = await mapAttachmentsForEmail(message.id, attachments);
162
- logDebug("Sending external email via Resend", {
162
+ const emailAttachments = await mapAttachmentsForEmail(message.id, attachments);
163
+ logDebug("Sending external email", {
163
164
  messageId: message.id,
164
165
  email,
165
- attachmentsCount: resendAttachments.length,
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: resendAttachments
183
+ attachments: emailAttachments,
184
+ tenantId: message.tenantId,
185
+ organizationId: message.organizationId ?? null
184
186
  });
185
- logDebug("External email sent via Resend", {
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 ResendAttachment = {\n filename: string\n content: string\n contentType?: string\n}\n\nasync function mapAttachmentsForEmail(\n messageId: string,\n attachments: MessageEmailAttachment[],\n): Promise<ResendAttachment[]> {\n const resendAttachments: ResendAttachment[] = []\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 resendAttachments.push({\n filename: attachment.fileName,\n content: buffer.toString('base64'),\n contentType: attachment.mimeType || undefined,\n })\n }\n\n return resendAttachments\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 resendAttachments = await mapAttachmentsForEmail(message.id, attachments)\n logDebug('Sending recipient email via Resend', {\n messageId: message.id,\n recipientUserId,\n recipientEmail,\n hasViewUrl: Boolean(viewUrl),\n attachmentsCount: resendAttachments.length,\n hasApiKey: Boolean(process.env.RESEND_API_KEY),\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: resendAttachments,\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 resendAttachments = await mapAttachmentsForEmail(message.id, attachments)\n logDebug('Sending external email via Resend', {\n messageId: message.id,\n email,\n attachmentsCount: resendAttachments.length,\n hasApiKey: Boolean(process.env.RESEND_API_KEY),\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: resendAttachments,\n })\n logDebug('External email sent via Resend', {\n messageId: message.id,\n email,\n })\n}\n"],
5
- "mappings": "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",
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,