@open-mercato/core 0.10.1-develop.7768.1.90fa052c11 → 0.10.1-develop.7770.1.970b396ebb
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/customer_accounts/search.js +5 -9
- package/dist/modules/customer_accounts/search.js.map +2 -2
- package/dist/modules/customers/search.js +71 -50
- package/dist/modules/customers/search.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +19 -1
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/customer_accounts/search.ts +4 -10
- package/src/modules/customers/search.ts +82 -52
- package/src/modules/notifications/lib/notificationService.ts +24 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/notifications/lib/notificationService.ts"],
|
|
4
|
-
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'\nimport { Notification, type NotificationStatus } from '../data/entities'\nimport type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'\nimport {\n NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,\n NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,\n type NotificationAction,\n type NotificationPollData,\n type NotificationTypeAction,\n} from '@open-mercato/shared/modules/notifications/types'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from './events'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildNotificationEntity,\n emitNotificationCreated,\n emitNotificationCreatedBatch,\n type NotificationContentInput,\n type NotificationTenantContext,\n} from './notificationFactory'\nimport { toNotificationDto } from './notificationMapper'\nimport { buildNotificationReadScopeWhere } from './notificationScope'\nimport {\n getRecipientUserIdsForFeature,\n getRecipientUserIdsForRole,\n getScopedNotificationRecipientUserIds,\n} from './notificationRecipients'\nimport { assertSafeNotificationHref, sanitizeNotificationActions } from './safeHref'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getNotificationType } from './notification-type-registry'\nimport { getNotificationTypeOverrides, type NotificationTypeOverrides } from './typeOverrides'\nimport { getNotificationDeliveryStrategies } from './deliveryStrategies'\nimport { resolveEffectiveChannels } from './shouldDeliver'\nimport {\n createNotificationPreferenceService,\n type NotificationPreferenceService,\n} from './notificationPreferenceService'\nimport { inAppVisibleFilter, inAppVisibleSql, isInAppVisible } from './notificationVisibility'\n\nconst logger = createLogger('notifications').child({ component: 'service' })\n\nfunction debug(message: string, ...details: unknown[]): void {\n logger.debug(message, details.length ? { details } : undefined)\n}\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\nconst UNIQUE_NOTIFICATION_ACTIVE_STATUSES: NotificationStatus[] = ['unread', 'read', 'actioned']\nconst NOTIFICATION_RESOURCE_KIND = 'notifications.notification'\n\nfunction normalizeOrgScope(organizationId: string | null | undefined): string | null {\n return organizationId ?? null\n}\n\nasync function invalidateNotificationCache(\n container: NotificationServiceDeps['container'],\n ctx: Pick<NotificationServiceContext, 'tenantId' | 'organizationId'>,\n reason: string,\n notificationId?: string,\n): Promise<void> {\n if (!container) return\n await invalidateCrudCache(\n container as Parameters<typeof invalidateCrudCache>[0],\n NOTIFICATION_RESOURCE_KIND,\n {\n id: notificationId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n },\n ctx.tenantId,\n reason,\n )\n}\n\nasync function assertNotificationRecipientsInScope(\n em: EntityManager,\n recipientUserIds: string[],\n ctx: NotificationServiceContext,\n): Promise<void> {\n const scopedRecipientUserIds = await getScopedNotificationRecipientUserIds(\n getDb(em),\n ctx.tenantId,\n normalizeOrgScope(ctx.organizationId),\n recipientUserIds,\n )\n\n if (scopedRecipientUserIds.length !== recipientUserIds.length) {\n throw new CrudHttpError(404, { error: 'Notification recipient not found' })\n }\n}\n\nfunction actionAlreadyExecuted(): CrudHttpError {\n return new CrudHttpError(409, {\n error: 'Notification action already executed',\n code: NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,\n })\n}\n\nasync function actionForbidden(): Promise<CrudHttpError> {\n const { translate } = await resolveTranslations()\n return new CrudHttpError(403, {\n error: translate('notifications.error.actionForbidden', 'You are not allowed to run this notification action'),\n code: NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,\n })\n}\n\n/**\n * The command action the notification's registered type declares for a stored action: same action\n * id (across `actions` and `optionalActions`) and same `commandId`. Anything else is not dispatched.\n */\nfunction findDeclaredCommandAction(type: string, action: NotificationAction): NotificationTypeAction | null {\n const definition = getNotificationType(type)\n if (!definition) return null\n const declared = [...definition.actions, ...(definition.optionalActions ?? [])]\n .find((candidate) => candidate.id === action.id)\n if (!declared?.commandId || declared.commandId !== action.commandId) return null\n return declared\n}\n\nfunction applyNotificationContent(\n notification: Notification,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n channels: string[] | null,\n) {\n const actions = sanitizeNotificationActions(input.actions)\n const linkHref = assertSafeNotificationHref(input.linkHref)\n\n notification.recipientUserId = recipientUserId\n notification.type = input.type\n notification.titleKey = input.titleKey\n notification.bodyKey = input.bodyKey\n notification.titleVariables = input.titleVariables\n notification.bodyVariables = input.bodyVariables\n notification.title = input.title || input.titleKey || ''\n notification.body = input.body\n notification.icon = input.icon\n notification.severity = input.severity ?? 'info'\n notification.actionData = actions\n ? {\n actions,\n primaryActionId: input.primaryActionId,\n }\n : null\n notification.sourceModule = input.sourceModule\n notification.sourceEntityType = input.sourceEntityType\n notification.sourceEntityId = input.sourceEntityId\n notification.linkHref = linkHref\n notification.groupKey = input.groupKey\n notification.channels = channels\n notification.expiresAt = input.expiresAt ? new Date(input.expiresAt) : null\n notification.tenantId = ctx.tenantId\n notification.organizationId = normalizeOrgScope(ctx.organizationId)\n notification.status = 'unread'\n notification.readAt = null\n notification.actionedAt = null\n notification.dismissedAt = null\n notification.actionTaken = null\n notification.actionResult = null\n notification.createdAt = new Date()\n}\n\nasync function findScopedNotificationOrThrow(\n em: EntityManager,\n notificationId: string,\n ctx: NotificationServiceContext,\n): Promise<Notification> {\n const notification = await findOneWithDecryption(\n em,\n Notification,\n {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n },\n undefined,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!notification) {\n throw new CrudHttpError(404, { error: 'Notification not found' })\n }\n return notification\n}\n\nasync function emitNotificationSseEvents(\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> },\n notifications: Notification[],\n ctx: NotificationServiceContext,\n): Promise<void> {\n // Live bell updates only for notifications actually delivered to the in-app channel; the others\n // exist as records but must not bump anyone's badge/inbox.\n const visible = notifications.filter((notification) => isInAppVisible(notification.channels))\n if (visible.length === 0) return\n\n const visibleRecipientUserIds = Array.from(new Set(visible.map((n) => n.recipientUserId)))\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.BATCH_CREATED, {\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n recipientUserIds: visibleRecipientUserIds,\n count: visible.length,\n })\n\n for (const notification of visible) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n}\n\nasync function createOrRefreshNotification(\n em: EntityManager,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n channels: string[] | null,\n): Promise<Notification> {\n if (input.groupKey && input.groupKey.trim().length > 0) {\n const orgScope = normalizeOrgScope(ctx.organizationId) ?? 'global'\n const lockKey = `notifications:${ctx.tenantId}:${orgScope}:${recipientUserId}:${input.type}:${input.groupKey}`\n try {\n const db = getDb(em)\n await sql`select pg_advisory_xact_lock(hashtext(${lockKey}))`.execute(db)\n } catch {\n // If advisory locks are unavailable, continue with best-effort dedupe.\n }\n\n const existing = await em.findOne(Notification, {\n recipientUserId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n type: input.type,\n groupKey: input.groupKey,\n status: { $in: UNIQUE_NOTIFICATION_ACTIVE_STATUSES },\n }, {\n orderBy: { createdAt: 'desc' },\n })\n\n if (existing) {\n applyNotificationContent(existing, input, recipientUserId, ctx, channels)\n return existing\n }\n }\n\n return buildNotificationEntity(em, input, recipientUserId, ctx, channels)\n}\n\nexport interface NotificationServiceContext {\n tenantId: string\n organizationId?: string | null\n organizationIds?: string[] | null\n userId?: string | null\n}\n\nexport type CreateFeatureNotificationServiceInput = CreateFeatureNotificationInput & {\n restrictRecipientsToOrganization?: boolean\n}\n\nexport interface NotificationService {\n create(input: CreateNotificationInput, ctx: NotificationServiceContext): Promise<Notification>\n createBatch(input: CreateBatchNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForRole(input: CreateRoleNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForFeature(input: CreateFeatureNotificationServiceInput, ctx: NotificationServiceContext): Promise<Notification[]>\n markAsRead(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n markAllAsRead(ctx: NotificationServiceContext): Promise<number>\n dismiss(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n restoreDismissed(\n notificationId: string,\n status: 'read' | 'unread' | undefined,\n ctx: NotificationServiceContext\n ): Promise<Notification>\n executeAction(\n notificationId: string,\n input: ExecuteActionInput,\n ctx: NotificationServiceContext\n ): Promise<{ notification: Notification; result: unknown }>\n getUnreadCount(ctx: NotificationServiceContext): Promise<number>\n getPollData(ctx: NotificationServiceContext, since?: string): Promise<NotificationPollData>\n cleanupExpired(): Promise<number>\n deleteBySource(\n sourceEntityType: string,\n sourceEntityId: string,\n ctx: NotificationServiceContext\n ): Promise<number>\n}\n\nexport interface NotificationServiceDeps {\n em: EntityManager\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> }\n commandBus?: {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown }>\n }\n container?: { resolve: (name: string) => unknown }\n}\n\nexport function createNotificationService(deps: NotificationServiceDeps): NotificationService {\n const { em: rootEm, eventBus, commandBus, container } = deps\n\n /**\n * Whether the clicking user holds every feature a declared notification action requires.\n * Fails closed: no user, no container, or a failing RBAC check denies the action.\n */\n const userHoldsActionFeatures = async (\n requiredFeatures: string[] | undefined,\n scopeCtx: NotificationServiceContext,\n ): Promise<boolean> => {\n if (!requiredFeatures || requiredFeatures.length === 0) return true\n if (!scopeCtx.userId || !container) return false\n try {\n const rbacService = container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n const allowed = await rbacService.userHasAllFeatures(\n scopeCtx.userId,\n requiredFeatures,\n { tenantId: scopeCtx.tenantId, organizationId: scopeCtx.organizationId ?? null },\n )\n return allowed === true\n } catch (err) {\n getTelemetryRuntime()?.reportError(err, {\n module: 'notifications',\n code: 'notifications.action_feature_check_failed',\n })\n return false\n }\n }\n\n /**\n * Resolves the authoritative delivery-channel set for one recipient at create time \u2014 the single\n * gate that folds per-send target, per-type eligibility, registered strategies, and the\n * recipient's per-channel preferences (`nonOptOut` bypasses opt-out). Stored on the row and\n * replayed by the dispatcher; `in_app` membership also drives bell/inbox visibility.\n *\n * Returns `null` (\u21D2 \"all channels\", legacy behavior) when no delivery strategies are registered\n * (e.g. a minimal bootstrap/test), so notifications never become silently undeliverable/invisible\n * in an environment that simply hasn't wired the seam.\n */\n const resolveChannelsFor = async (\n content: NotificationContentInput,\n recipientUserId: string,\n scopeCtx: NotificationServiceContext,\n preferences: NotificationPreferenceService = createNotificationPreferenceService({ em: rootEm.fork() }),\n typeOverrides?: NotificationTypeOverrides | null,\n ): Promise<string[] | null> => {\n const registeredChannels = getNotificationDeliveryStrategies().map((strategy) => strategy.id)\n if (registeredChannels.length === 0) return null\n // Treat an empty target as \"no restriction\" (all channels) rather than \"no deliverable channel\":\n // a programmatic caller that computed an empty array should not silently black-hole the\n // notification. The HTTP layer rejects an empty `channels` outright (see validators.ts).\n const targetChannels = content.channels && content.channels.length > 0 ? content.channels : null\n const overrides = typeOverrides === undefined\n ? (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null\n : typeOverrides\n return resolveEffectiveChannels({\n typeId: content.type,\n type: getNotificationType(content.type),\n scope: { tenantId: scopeCtx.tenantId, userId: recipientUserId },\n targetChannels,\n registeredChannels,\n preferences,\n channelsOverride: overrides?.channels ?? null,\n nonOptOutOverride: overrides?.nonOptOut ?? null,\n })\n }\n\n /**\n * Broadcast counterpart of {@link resolveChannelsFor}: resolves the channel set for every recipient\n * up front, BEFORE the write transaction opens, reusing a single forked EM / preference service\n * across the whole set. This keeps the preference reads out of the write transaction (matching\n * `create`, which resolves before its transaction) and avoids one EM fork per recipient on large\n * role/feature broadcasts.\n */\n const resolveChannelsForRecipients = async (\n content: NotificationContentInput,\n recipientUserIds: string[],\n scopeCtx: NotificationServiceContext,\n ): Promise<Array<{ recipientUserId: string; channels: string[] | null }>> => {\n const preferences = createNotificationPreferenceService({ em: rootEm.fork() })\n // Stored overrides are per-type (not per-recipient) \u2014 read once for the whole broadcast.\n const typeOverrides =\n (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null\n const resolved: Array<{ recipientUserId: string; channels: string[] | null }> = []\n for (const recipientUserId of recipientUserIds) {\n resolved.push({\n recipientUserId,\n channels: await resolveChannelsFor(content, recipientUserId, scopeCtx, preferences, typeOverrides),\n })\n }\n return resolved\n }\n\n return {\n async create(input, ctx) {\n const { recipientUserId, ...content } = input\n const channels = await resolveChannelsFor(content, recipientUserId, ctx)\n const writeEm = rootEm.fork()\n const notification = await writeEm.transactional(async (tx) => {\n await assertNotificationRecipientsInScope(tx, [recipientUserId], ctx)\n const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n await tx.flush()\n return entity\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n // Always emit the domain event (drives the deliver subscriber \u2192 push/email/\u2026). Only bump the\n // live in-app bell when this notification is actually visible in-app.\n await emitNotificationCreated(eventBus, notification, ctx)\n if (isInAppVisible(notification.channels)) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return notification\n },\n\n async createBatch(input, ctx) {\n const recipientUserIds = Array.from(new Set(input.recipientUserIds))\n const { recipientUserIds: _recipientUserIds, ...content } = input\n const notifications: Notification[] = []\n const resolved = await resolveChannelsForRecipients(content, recipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n await assertNotificationRecipientsInScope(tx, recipientUserIds, ctx)\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForRole(input, ctx) {\n const em = rootEm.fork()\n\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForRole(db, ctx.tenantId, input.roleId)\n if (recipientUserIds.length === 0) {\n return []\n }\n\n const { roleId: _roleId, ...content } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))\n const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForFeature(input, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForFeature(db, ctx.tenantId, input.requiredFeature)\n\n if (recipientUserIds.length === 0) {\n debug('No users found with feature:', input.requiredFeature, 'in tenant:', ctx.tenantId)\n return []\n }\n\n let authorizedRecipientUserIds = recipientUserIds\n if (input.restrictRecipientsToOrganization) {\n const uniqueCandidateUserIds = Array.from(new Set(recipientUserIds))\n if (!ctx.organizationId) {\n logger.warn('Organization-restricted feature fan-out skipped because organization scope is missing', {\n tenantId: ctx.tenantId,\n requiredFeature: input.requiredFeature,\n })\n return []\n }\n\n if (uniqueCandidateUserIds.length > 200) {\n logger.warn('Organization-restricted feature fan-out skipped because the candidate cap was exceeded', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n candidateCount: uniqueCandidateUserIds.length,\n candidateCap: 200,\n })\n return []\n }\n\n if (!container) {\n logger.warn('Organization-restricted feature fan-out skipped because the DI container is unavailable', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n })\n return []\n }\n\n try {\n const rbacService = container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n const allowedRecipientUserIds: string[] = []\n for (let offset = 0; offset < uniqueCandidateUserIds.length; offset += 10) {\n const candidates = uniqueCandidateUserIds.slice(offset, offset + 10)\n const results = await Promise.all(candidates.map(async (recipientUserId) => ({\n recipientUserId,\n allowed: await rbacService.userHasAllFeatures(\n recipientUserId,\n [input.requiredFeature],\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId ?? null },\n ),\n })))\n for (const result of results) {\n if (result.allowed) allowedRecipientUserIds.push(result.recipientUserId)\n }\n }\n authorizedRecipientUserIds = allowedRecipientUserIds\n } catch (err) {\n logger.warn('Organization-restricted feature fan-out skipped because RBAC filtering failed', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n err,\n })\n return []\n }\n\n if (authorizedRecipientUserIds.length === 0) {\n debug('No users found with feature in organization:', input.requiredFeature, ctx.organizationId)\n return []\n }\n }\n\n debug('Creating notifications for', authorizedRecipientUserIds.length, 'user(s) with feature:', input.requiredFeature)\n\n const {\n requiredFeature: _requiredFeature,\n restrictRecipientsToOrganization: _restrictRecipientsToOrganization,\n ...content\n } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(authorizedRecipientUserIds))\n const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async markAsRead(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status === 'unread') {\n notification.status = 'read'\n notification.readAt = new Date()\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n }\n\n return notification\n },\n\n async markAllAsRead(ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const applyScope = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '=', 'unread')\n // Only in-app-visible rows count toward the badge (see getUnreadCount),\n // so \"mark all as read\" must scope to the SAME set \u2014 otherwise it flips\n // push/email-only rows the user never saw and SSE-broadcasts a read for\n // notifications that were never in the bell, inflating the returned\n // count past what the badge showed.\n .where(inAppVisibleSql() as any)\n if (ctx.organizationId) {\n chain = chain.where('organization_id' as any, '=', ctx.organizationId)\n }\n return chain\n }\n\n const targetRows = await applyScope(\n db\n .selectFrom('notifications' as any)\n .select([\n 'id' as any,\n 'organization_id' as any,\n 'recipient_user_id' as any,\n ]),\n ).execute() as Array<{ id: string }>\n\n if (!targetRows.length) {\n return 0\n }\n\n const updateResult = await applyScope(\n db.updateTable('notifications' as any).set({\n status: 'read',\n read_at: sql`now()`,\n } as any) as any,\n ).executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n const result = Number(updateResult?.numUpdatedRows ?? targetRows.length)\n\n await invalidateNotificationCache(container, ctx, 'updated')\n\n const notifications = await findWithDecryption(em, Notification, {\n id: { $in: targetRows.map((row) => row.id) },\n }, undefined, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n })\n\n for (const notification of notifications) {\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return result\n },\n\n async dismiss(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n notification.status = 'dismissed'\n notification.dismissedAt = new Date()\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.DISMISSED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return notification\n },\n\n async restoreDismissed(notificationId, status, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status !== 'dismissed') {\n return notification\n }\n\n // A dismissed notification whose action already ran comes back as `actioned`,\n // whatever the caller asked for: `actioned_at` outlives the dismissal.\n const targetStatus: NotificationStatus = notification.actionedAt != null ? 'actioned' : status ?? 'read'\n notification.status = targetStatus\n notification.dismissedAt = null\n\n if (targetStatus === 'unread') {\n notification.readAt = null\n } else if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.RESTORED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n status: targetStatus,\n })\n\n return notification\n },\n\n async executeAction(notificationId, input, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n const actionData = notification.actionData\n const action = actionData?.actions?.find((a) => a.id === input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n // Reject an already-actioned notification before dispatching the command,\n // so a retry or double-click cannot re-run the side effect. `actioned_at` is\n // the durable marker: `status` is overwritten by a dismissal.\n if (notification.status === 'actioned' || notification.actionedAt != null) {\n throw actionAlreadyExecuted()\n }\n\n if (action.commandId) {\n const declared = findDeclaredCommandAction(notification.type, action)\n if (!declared || !(await userHoldsActionFeatures(declared.requiredFeatures, ctx))) {\n throw await actionForbidden()\n }\n }\n\n const actionedAt = new Date()\n const previousStatus = notification.status\n const previousActionedAt = notification.actionedAt ?? null\n const previousActionTaken = notification.actionTaken ?? null\n\n // Atomically claim the notification so only one concurrent request can run\n // the side-effecting command. The conditional UPDATE matches only while the\n // notification has not been actioned yet; a losing request updates 0 rows.\n const claimResult = (await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: 'actioned',\n actioned_at: actionedAt,\n action_taken: input.actionId,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '!=', 'actioned')\n .where('actioned_at' as any, 'is', null)\n .executeTakeFirst()) as { numUpdatedRows?: bigint | number } | undefined\n\n if (Number(claimResult?.numUpdatedRows ?? 0) === 0) {\n throw actionAlreadyExecuted()\n }\n\n // The claim is provisional: if the side-effecting command fails, the action\n // never actually completed, so release the claim to its prior state. This\n // lets the user retry the action instead of the notification being locked as\n // `actioned` forever. The claim is owned through its own `actioned_at` and\n // `action_taken`, not through `status`: a dismissal that lands while the\n // command runs overwrites `status`, and the release must still clear the\n // claim then \u2014 while leaving that dismissal in place.\n const releaseClaim = async () => {\n await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: sql`case when status = 'actioned' then ${previousStatus} else status end`,\n actioned_at: previousActionedAt,\n action_taken: previousActionTaken,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('actioned_at' as any, '=', actionedAt)\n .where('action_taken' as any, '=', input.actionId)\n .executeTakeFirst()\n }\n\n let result: unknown = null\n\n if (action.commandId && commandBus && container) {\n const commandInput = {\n ...input.payload,\n id: notification.sourceEntityId,\n }\n\n // Build a CommandRuntimeContext from the notification service context\n const commandCtx = {\n container,\n auth: {\n sub: ctx.userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: ctx.organizationId ?? null,\n organizationIds: ctx.organizationId ? [ctx.organizationId] : null,\n }\n\n let commandResult: { result: unknown }\n try {\n commandResult = await commandBus.execute(action.commandId, {\n input: commandInput,\n ctx: commandCtx,\n metadata: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n resourceKind: 'notifications',\n },\n })\n } catch (err) {\n // Never let a rollback failure mask the original command error \u2014 the\n // caller needs the real failure to decide whether to retry.\n try {\n await releaseClaim()\n } catch (releaseErr) {\n debug('failed to release notification action claim', releaseErr)\n }\n throw err\n }\n\n result = commandResult.result\n }\n\n notification.status = 'actioned'\n notification.actionedAt = actionedAt\n notification.actionTaken = input.actionId\n notification.actionResult = result as Record<string, unknown>\n\n if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.ACTIONED, {\n notificationId: notification.id,\n actionId: input.actionId,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return { notification, result }\n },\n\n async getUnreadCount(ctx) {\n const em = rootEm.fork()\n return em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n })\n },\n\n async getPollData(ctx, since) {\n const em = rootEm.fork()\n const filters: Record<string, unknown> = {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n }\n\n if (since) {\n filters.createdAt = { $gt: new Date(since) }\n }\n\n const [notifications, unreadCount] = await Promise.all([\n em.find(Notification, filters, {\n orderBy: { createdAt: 'desc' },\n limit: 50,\n }),\n em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n }),\n ])\n\n const recent = notifications.map(toNotificationDto)\n const hasNew = since ? recent.length > 0 : false\n\n return {\n unreadCount,\n recent,\n hasNew,\n lastId: recent[0]?.id,\n }\n },\n\n async cleanupExpired() {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const affectedScopes = await db\n .selectFrom('notifications' as any)\n .select([\n 'tenant_id' as any,\n 'organization_id' as any,\n ])\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .distinct()\n .execute() as Array<{ tenant_id: string; organization_id: string | null }>\n\n if (!affectedScopes.length) return 0\n\n const updateResult = await db\n .updateTable('notifications' as any)\n .set({\n status: 'dismissed',\n dismissed_at: sql`now()`,\n } as any)\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n\n for (const scope of affectedScopes) {\n await invalidateNotificationCache(container, {\n tenantId: scope.tenant_id,\n organizationId: scope.organization_id,\n }, 'updated')\n }\n\n return Number(updateResult?.numUpdatedRows ?? 0)\n },\n\n async deleteBySource(sourceEntityType, sourceEntityId, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const deleteResult = await db\n .deleteFrom('notifications' as any)\n .where('source_entity_type' as any, '=', sourceEntityType)\n .where('source_entity_id' as any, '=', sourceEntityId)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .executeTakeFirst() as { numDeletedRows?: bigint | number } | undefined\n\n const deletedCount = Number(deleteResult?.numDeletedRows ?? 0)\n if (deletedCount > 0) {\n await invalidateNotificationCache(container, ctx, 'deleted')\n }\n\n return deletedCount\n },\n }\n}\n\n/**\n * Helper to create notification service from a DI container.\n * Use this in API routes and commands to avoid DI resolution issues.\n */\nexport function resolveNotificationService(container: {\n resolve: (name: string) => unknown\n}): NotificationService {\n const em = container.resolve('em') as EntityManager\n const eventBus = container.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n\n // commandBus may not be registered in all contexts, so resolve it safely\n let commandBus: NotificationServiceDeps['commandBus']\n try {\n commandBus = container.resolve('commandBus') as typeof commandBus\n } catch {\n // commandBus not available - actions with commandId won't work\n commandBus = undefined\n }\n\n return createNotificationService({ em, eventBus, commandBus, container })\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { type Kysely, sql } from 'kysely'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { invalidateCrudCache } from '@open-mercato/shared/lib/crud/cache'\nimport { Notification, type NotificationStatus } from '../data/entities'\nimport type { CreateNotificationInput, CreateBatchNotificationInput, CreateRoleNotificationInput, CreateFeatureNotificationInput, ExecuteActionInput } from '../data/validators'\nimport {\n NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,\n NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,\n type NotificationAction,\n type NotificationPollData,\n type NotificationTypeAction,\n} from '@open-mercato/shared/modules/notifications/types'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { NOTIFICATION_EVENTS, NOTIFICATION_SSE_EVENTS } from './events'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n buildNotificationEntity,\n emitNotificationCreated,\n emitNotificationCreatedBatch,\n type NotificationContentInput,\n type NotificationTenantContext,\n} from './notificationFactory'\nimport { toNotificationDto } from './notificationMapper'\nimport { buildNotificationReadScopeWhere } from './notificationScope'\nimport {\n getRecipientUserIdsForFeature,\n getRecipientUserIdsForRole,\n getScopedNotificationRecipientUserIds,\n} from './notificationRecipients'\nimport { assertSafeNotificationHref, sanitizeNotificationActions } from './safeHref'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { getNotificationType } from './notification-type-registry'\nimport { getNotificationTypeOverrides, type NotificationTypeOverrides } from './typeOverrides'\nimport { getNotificationDeliveryStrategies } from './deliveryStrategies'\nimport { resolveEffectiveChannels } from './shouldDeliver'\nimport {\n createNotificationPreferenceService,\n type NotificationPreferenceService,\n} from './notificationPreferenceService'\nimport { inAppVisibleFilter, inAppVisibleSql, isInAppVisible } from './notificationVisibility'\n\nconst logger = createLogger('notifications').child({ component: 'service' })\n\nfunction debug(message: string, ...details: unknown[]): void {\n logger.debug(message, details.length ? { details } : undefined)\n}\n\nfunction getDb(em: EntityManager): Kysely<any> {\n return em.getKysely<any>()\n}\n\nconst UNIQUE_NOTIFICATION_ACTIVE_STATUSES: NotificationStatus[] = ['unread', 'read', 'actioned']\nconst NOTIFICATION_RESOURCE_KIND = 'notifications.notification'\n\nfunction normalizeOrgScope(organizationId: string | null | undefined): string | null {\n return organizationId ?? null\n}\n\nasync function invalidateNotificationCache(\n container: NotificationServiceDeps['container'],\n ctx: Pick<NotificationServiceContext, 'tenantId' | 'organizationId'>,\n reason: string,\n notificationId?: string,\n): Promise<void> {\n if (!container) return\n await invalidateCrudCache(\n container as Parameters<typeof invalidateCrudCache>[0],\n NOTIFICATION_RESOURCE_KIND,\n {\n id: notificationId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n },\n ctx.tenantId,\n reason,\n )\n}\n\nasync function assertNotificationRecipientsInScope(\n em: EntityManager,\n recipientUserIds: string[],\n ctx: NotificationServiceContext,\n): Promise<void> {\n const scopedRecipientUserIds = await getScopedNotificationRecipientUserIds(\n getDb(em),\n ctx.tenantId,\n normalizeOrgScope(ctx.organizationId),\n recipientUserIds,\n )\n\n if (scopedRecipientUserIds.length !== recipientUserIds.length) {\n throw new CrudHttpError(404, { error: 'Notification recipient not found' })\n }\n}\n\nfunction actionAlreadyExecuted(): CrudHttpError {\n return new CrudHttpError(409, {\n error: 'Notification action already executed',\n code: NOTIFICATION_ACTION_ALREADY_EXECUTED_ERROR_CODE,\n })\n}\n\nasync function actionForbidden(): Promise<CrudHttpError> {\n const { translate } = await resolveTranslations()\n return new CrudHttpError(403, {\n error: translate('notifications.error.actionForbidden', 'You are not allowed to run this notification action'),\n code: NOTIFICATION_ACTION_FORBIDDEN_ERROR_CODE,\n })\n}\n\n/**\n * The command action the notification's registered type declares for a stored action: same action\n * id (across `actions` and `optionalActions`) and same `commandId`. Anything else is not dispatched.\n */\nfunction findDeclaredCommandAction(type: string, action: NotificationAction): NotificationTypeAction | null {\n const definition = getNotificationType(type)\n if (!definition) return null\n const declared = [...definition.actions, ...(definition.optionalActions ?? [])]\n .find((candidate) => candidate.id === action.id)\n if (!declared?.commandId || declared.commandId !== action.commandId) return null\n return declared\n}\n\nfunction applyNotificationContent(\n notification: Notification,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n channels: string[] | null,\n) {\n const actions = sanitizeNotificationActions(input.actions)\n const linkHref = assertSafeNotificationHref(input.linkHref)\n\n notification.recipientUserId = recipientUserId\n notification.type = input.type\n notification.titleKey = input.titleKey\n notification.bodyKey = input.bodyKey\n notification.titleVariables = input.titleVariables\n notification.bodyVariables = input.bodyVariables\n notification.title = input.title || input.titleKey || ''\n notification.body = input.body\n notification.icon = input.icon\n notification.severity = input.severity ?? 'info'\n notification.actionData = actions\n ? {\n actions,\n primaryActionId: input.primaryActionId,\n }\n : null\n notification.sourceModule = input.sourceModule\n notification.sourceEntityType = input.sourceEntityType\n notification.sourceEntityId = input.sourceEntityId\n notification.linkHref = linkHref\n notification.groupKey = input.groupKey\n notification.channels = channels\n notification.expiresAt = input.expiresAt ? new Date(input.expiresAt) : null\n notification.tenantId = ctx.tenantId\n notification.organizationId = normalizeOrgScope(ctx.organizationId)\n notification.status = 'unread'\n notification.readAt = null\n notification.actionedAt = null\n notification.dismissedAt = null\n notification.actionTaken = null\n notification.actionResult = null\n notification.createdAt = new Date()\n}\n\nasync function findScopedNotificationOrThrow(\n em: EntityManager,\n notificationId: string,\n ctx: NotificationServiceContext,\n): Promise<Notification> {\n const notification = await findOneWithDecryption(\n em,\n Notification,\n {\n id: notificationId,\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n },\n undefined,\n {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n },\n )\n if (!notification) {\n throw new CrudHttpError(404, { error: 'Notification not found' })\n }\n return notification\n}\n\nasync function emitNotificationSseEvents(\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> },\n notifications: Notification[],\n ctx: NotificationServiceContext,\n): Promise<void> {\n // Live bell updates only for notifications actually delivered to the in-app channel; the others\n // exist as records but must not bump anyone's badge/inbox.\n const visible = notifications.filter((notification) => isInAppVisible(notification.channels))\n if (visible.length === 0) return\n\n const visibleRecipientUserIds = Array.from(new Set(visible.map((n) => n.recipientUserId)))\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.BATCH_CREATED, {\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n recipientUserIds: visibleRecipientUserIds,\n count: visible.length,\n })\n\n for (const notification of visible) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n}\n\nasync function createOrRefreshNotification(\n em: EntityManager,\n input: NotificationContentInput,\n recipientUserId: string,\n ctx: NotificationTenantContext,\n channels: string[] | null,\n): Promise<Notification> {\n if (input.groupKey && input.groupKey.trim().length > 0) {\n const orgScope = normalizeOrgScope(ctx.organizationId) ?? 'global'\n const lockKey = `notifications:${ctx.tenantId}:${orgScope}:${recipientUserId}:${input.type}:${input.groupKey}`\n try {\n const db = getDb(em)\n await sql`select pg_advisory_xact_lock(hashtext(${lockKey}))`.execute(db)\n } catch {\n // If advisory locks are unavailable, continue with best-effort dedupe.\n }\n\n const existing = await em.findOne(Notification, {\n recipientUserId,\n tenantId: ctx.tenantId,\n organizationId: normalizeOrgScope(ctx.organizationId),\n type: input.type,\n groupKey: input.groupKey,\n status: { $in: UNIQUE_NOTIFICATION_ACTIVE_STATUSES },\n }, {\n orderBy: { createdAt: 'desc' },\n })\n\n if (existing) {\n applyNotificationContent(existing, input, recipientUserId, ctx, channels)\n return existing\n }\n }\n\n return buildNotificationEntity(em, input, recipientUserId, ctx, channels)\n}\n\nexport interface NotificationServiceContext {\n tenantId: string\n organizationId?: string | null\n organizationIds?: string[] | null\n userId?: string | null\n}\n\nexport type CreateFeatureNotificationServiceInput = CreateFeatureNotificationInput & {\n restrictRecipientsToOrganization?: boolean\n}\n\nexport type CreateBatchNotificationOptions = {\n skipExistingGroup?: boolean\n}\n\nexport interface NotificationService {\n create(input: CreateNotificationInput, ctx: NotificationServiceContext): Promise<Notification>\n createBatch(input: CreateBatchNotificationInput, ctx: NotificationServiceContext, options?: CreateBatchNotificationOptions): Promise<Notification[]>\n createForRole(input: CreateRoleNotificationInput, ctx: NotificationServiceContext): Promise<Notification[]>\n createForFeature(input: CreateFeatureNotificationServiceInput, ctx: NotificationServiceContext): Promise<Notification[]>\n markAsRead(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n markAllAsRead(ctx: NotificationServiceContext): Promise<number>\n dismiss(notificationId: string, ctx: NotificationServiceContext): Promise<Notification>\n restoreDismissed(\n notificationId: string,\n status: 'read' | 'unread' | undefined,\n ctx: NotificationServiceContext\n ): Promise<Notification>\n executeAction(\n notificationId: string,\n input: ExecuteActionInput,\n ctx: NotificationServiceContext\n ): Promise<{ notification: Notification; result: unknown }>\n getUnreadCount(ctx: NotificationServiceContext): Promise<number>\n getPollData(ctx: NotificationServiceContext, since?: string): Promise<NotificationPollData>\n cleanupExpired(): Promise<number>\n deleteBySource(\n sourceEntityType: string,\n sourceEntityId: string,\n ctx: NotificationServiceContext\n ): Promise<number>\n}\n\nexport interface NotificationServiceDeps {\n em: EntityManager\n eventBus: { emit: (event: string, payload: unknown) => Promise<void> }\n commandBus?: {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown }>\n }\n container?: { resolve: (name: string) => unknown }\n}\n\nexport function createNotificationService(deps: NotificationServiceDeps): NotificationService {\n const { em: rootEm, eventBus, commandBus, container } = deps\n\n /**\n * Whether the clicking user holds every feature a declared notification action requires.\n * Fails closed: no user, no container, or a failing RBAC check denies the action.\n */\n const userHoldsActionFeatures = async (\n requiredFeatures: string[] | undefined,\n scopeCtx: NotificationServiceContext,\n ): Promise<boolean> => {\n if (!requiredFeatures || requiredFeatures.length === 0) return true\n if (!scopeCtx.userId || !container) return false\n try {\n const rbacService = container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n const allowed = await rbacService.userHasAllFeatures(\n scopeCtx.userId,\n requiredFeatures,\n { tenantId: scopeCtx.tenantId, organizationId: scopeCtx.organizationId ?? null },\n )\n return allowed === true\n } catch (err) {\n getTelemetryRuntime()?.reportError(err, {\n module: 'notifications',\n code: 'notifications.action_feature_check_failed',\n })\n return false\n }\n }\n\n /**\n * Resolves the authoritative delivery-channel set for one recipient at create time \u2014 the single\n * gate that folds per-send target, per-type eligibility, registered strategies, and the\n * recipient's per-channel preferences (`nonOptOut` bypasses opt-out). Stored on the row and\n * replayed by the dispatcher; `in_app` membership also drives bell/inbox visibility.\n *\n * Returns `null` (\u21D2 \"all channels\", legacy behavior) when no delivery strategies are registered\n * (e.g. a minimal bootstrap/test), so notifications never become silently undeliverable/invisible\n * in an environment that simply hasn't wired the seam.\n */\n const resolveChannelsFor = async (\n content: NotificationContentInput,\n recipientUserId: string,\n scopeCtx: NotificationServiceContext,\n preferences: NotificationPreferenceService = createNotificationPreferenceService({ em: rootEm.fork() }),\n typeOverrides?: NotificationTypeOverrides | null,\n ): Promise<string[] | null> => {\n const registeredChannels = getNotificationDeliveryStrategies().map((strategy) => strategy.id)\n if (registeredChannels.length === 0) return null\n // Treat an empty target as \"no restriction\" (all channels) rather than \"no deliverable channel\":\n // a programmatic caller that computed an empty array should not silently black-hole the\n // notification. The HTTP layer rejects an empty `channels` outright (see validators.ts).\n const targetChannels = content.channels && content.channels.length > 0 ? content.channels : null\n const overrides = typeOverrides === undefined\n ? (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null\n : typeOverrides\n return resolveEffectiveChannels({\n typeId: content.type,\n type: getNotificationType(content.type),\n scope: { tenantId: scopeCtx.tenantId, userId: recipientUserId },\n targetChannels,\n registeredChannels,\n preferences,\n channelsOverride: overrides?.channels ?? null,\n nonOptOutOverride: overrides?.nonOptOut ?? null,\n })\n }\n\n /**\n * Broadcast counterpart of {@link resolveChannelsFor}: resolves the channel set for every recipient\n * up front, BEFORE the write transaction opens, reusing a single forked EM / preference service\n * across the whole set. This keeps the preference reads out of the write transaction (matching\n * `create`, which resolves before its transaction) and avoids one EM fork per recipient on large\n * role/feature broadcasts.\n */\n const resolveChannelsForRecipients = async (\n content: NotificationContentInput,\n recipientUserIds: string[],\n scopeCtx: NotificationServiceContext,\n ): Promise<Array<{ recipientUserId: string; channels: string[] | null }>> => {\n const preferences = createNotificationPreferenceService({ em: rootEm.fork() })\n // Stored overrides are per-type (not per-recipient) \u2014 read once for the whole broadcast.\n const typeOverrides =\n (await getNotificationTypeOverrides(rootEm.fork(), scopeCtx.tenantId, [content.type])).get(content.type) ?? null\n const resolved: Array<{ recipientUserId: string; channels: string[] | null }> = []\n for (const recipientUserId of recipientUserIds) {\n resolved.push({\n recipientUserId,\n channels: await resolveChannelsFor(content, recipientUserId, scopeCtx, preferences, typeOverrides),\n })\n }\n return resolved\n }\n\n return {\n async create(input, ctx) {\n const { recipientUserId, ...content } = input\n const channels = await resolveChannelsFor(content, recipientUserId, ctx)\n const writeEm = rootEm.fork()\n const notification = await writeEm.transactional(async (tx) => {\n await assertNotificationRecipientsInScope(tx, [recipientUserId], ctx)\n const entity = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n await tx.flush()\n return entity\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n // Always emit the domain event (drives the deliver subscriber \u2192 push/email/\u2026). Only bump the\n // live in-app bell when this notification is actually visible in-app.\n await emitNotificationCreated(eventBus, notification, ctx)\n if (isInAppVisible(notification.channels)) {\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return notification\n },\n\n async createBatch(input, ctx, options) {\n if (options?.skipExistingGroup && !input.groupKey?.trim()) {\n throw new Error('[internal] Notification group key is required for deduplicated batch delivery')\n }\n const recipientUserIds = Array.from(new Set(input.recipientUserIds))\n if (options?.skipExistingGroup) recipientUserIds.sort((left, right) => left.localeCompare(right))\n const { recipientUserIds: _recipientUserIds, ...content } = input\n const notifications: Notification[] = []\n const resolved = await resolveChannelsForRecipients(content, recipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n await assertNotificationRecipientsInScope(tx, recipientUserIds, ctx)\n for (const { recipientUserId, channels } of resolved) {\n if (options?.skipExistingGroup) {\n const organizationId = normalizeOrgScope(ctx.organizationId)\n const lockKey = `notifications:${ctx.tenantId}:${organizationId ?? 'global'}:${recipientUserId}:${content.type}:${content.groupKey}`\n await sql`select pg_advisory_xact_lock(hashtext(${lockKey}))`.execute(getDb(tx))\n const existing = await tx.findOne(Notification, {\n recipientUserId,\n tenantId: ctx.tenantId,\n organizationId,\n type: content.type,\n groupKey: content.groupKey,\n })\n if (existing) continue\n }\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n if (options?.skipExistingGroup && notifications.length === 0) return []\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForRole(input, ctx) {\n const em = rootEm.fork()\n\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForRole(db, ctx.tenantId, input.roleId)\n if (recipientUserIds.length === 0) {\n return []\n }\n\n const { roleId: _roleId, ...content } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(recipientUserIds))\n const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async createForFeature(input, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const recipientUserIds = await getRecipientUserIdsForFeature(db, ctx.tenantId, input.requiredFeature)\n\n if (recipientUserIds.length === 0) {\n debug('No users found with feature:', input.requiredFeature, 'in tenant:', ctx.tenantId)\n return []\n }\n\n let authorizedRecipientUserIds = recipientUserIds\n if (input.restrictRecipientsToOrganization) {\n const uniqueCandidateUserIds = Array.from(new Set(recipientUserIds))\n if (!ctx.organizationId) {\n logger.warn('Organization-restricted feature fan-out skipped because organization scope is missing', {\n tenantId: ctx.tenantId,\n requiredFeature: input.requiredFeature,\n })\n return []\n }\n\n if (uniqueCandidateUserIds.length > 200) {\n logger.warn('Organization-restricted feature fan-out skipped because the candidate cap was exceeded', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n candidateCount: uniqueCandidateUserIds.length,\n candidateCap: 200,\n })\n return []\n }\n\n if (!container) {\n logger.warn('Organization-restricted feature fan-out skipped because the DI container is unavailable', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n })\n return []\n }\n\n try {\n const rbacService = container.resolve('rbacService') as {\n userHasAllFeatures: (\n userId: string,\n features: string[],\n scope: { tenantId: string | null; organizationId: string | null },\n ) => Promise<boolean>\n }\n const allowedRecipientUserIds: string[] = []\n for (let offset = 0; offset < uniqueCandidateUserIds.length; offset += 10) {\n const candidates = uniqueCandidateUserIds.slice(offset, offset + 10)\n const results = await Promise.all(candidates.map(async (recipientUserId) => ({\n recipientUserId,\n allowed: await rbacService.userHasAllFeatures(\n recipientUserId,\n [input.requiredFeature],\n { tenantId: ctx.tenantId, organizationId: ctx.organizationId ?? null },\n ),\n })))\n for (const result of results) {\n if (result.allowed) allowedRecipientUserIds.push(result.recipientUserId)\n }\n }\n authorizedRecipientUserIds = allowedRecipientUserIds\n } catch (err) {\n logger.warn('Organization-restricted feature fan-out skipped because RBAC filtering failed', {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n requiredFeature: input.requiredFeature,\n err,\n })\n return []\n }\n\n if (authorizedRecipientUserIds.length === 0) {\n debug('No users found with feature in organization:', input.requiredFeature, ctx.organizationId)\n return []\n }\n }\n\n debug('Creating notifications for', authorizedRecipientUserIds.length, 'user(s) with feature:', input.requiredFeature)\n\n const {\n requiredFeature: _requiredFeature,\n restrictRecipientsToOrganization: _restrictRecipientsToOrganization,\n ...content\n } = input\n const notifications: Notification[] = []\n const uniqueRecipientUserIds = Array.from(new Set(authorizedRecipientUserIds))\n const resolved = await resolveChannelsForRecipients(content, uniqueRecipientUserIds, ctx)\n const writeEm = rootEm.fork()\n\n await writeEm.transactional(async (tx) => {\n for (const { recipientUserId, channels } of resolved) {\n const notification = await createOrRefreshNotification(tx, content, recipientUserId, ctx, channels)\n notifications.push(notification)\n }\n await tx.flush()\n })\n\n await invalidateNotificationCache(container, ctx, 'created')\n await emitNotificationCreatedBatch(eventBus, notifications, ctx)\n await emitNotificationSseEvents(eventBus, notifications, ctx)\n\n return notifications\n },\n\n async markAsRead(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status === 'unread') {\n notification.status = 'read'\n notification.readAt = new Date()\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n }\n\n return notification\n },\n\n async markAllAsRead(ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n const applyScope = <QB extends { where: (...args: any[]) => QB }>(q: QB): QB => {\n let chain = q\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '=', 'unread')\n // Only in-app-visible rows count toward the badge (see getUnreadCount),\n // so \"mark all as read\" must scope to the SAME set \u2014 otherwise it flips\n // push/email-only rows the user never saw and SSE-broadcasts a read for\n // notifications that were never in the bell, inflating the returned\n // count past what the badge showed.\n .where(inAppVisibleSql() as any)\n if (ctx.organizationId) {\n chain = chain.where('organization_id' as any, '=', ctx.organizationId)\n }\n return chain\n }\n\n const targetRows = await applyScope(\n db\n .selectFrom('notifications' as any)\n .select([\n 'id' as any,\n 'organization_id' as any,\n 'recipient_user_id' as any,\n ]),\n ).execute() as Array<{ id: string }>\n\n if (!targetRows.length) {\n return 0\n }\n\n const updateResult = await applyScope(\n db.updateTable('notifications' as any).set({\n status: 'read',\n read_at: sql`now()`,\n } as any) as any,\n ).executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n const result = Number(updateResult?.numUpdatedRows ?? targetRows.length)\n\n await invalidateNotificationCache(container, ctx, 'updated')\n\n const notifications = await findWithDecryption(em, Notification, {\n id: { $in: targetRows.map((row) => row.id) },\n }, undefined, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n })\n\n for (const notification of notifications) {\n await eventBus.emit(NOTIFICATION_EVENTS.READ, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n await eventBus.emit(NOTIFICATION_SSE_EVENTS.CREATED, {\n tenantId: notification.tenantId,\n organizationId: notification.organizationId ?? null,\n recipientUserId: notification.recipientUserId,\n notification: toNotificationDto(notification),\n })\n }\n\n return result\n },\n\n async dismiss(notificationId, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n notification.status = 'dismissed'\n notification.dismissedAt = new Date()\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.DISMISSED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return notification\n },\n\n async restoreDismissed(notificationId, status, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n if (notification.status !== 'dismissed') {\n return notification\n }\n\n // A dismissed notification whose action already ran comes back as `actioned`,\n // whatever the caller asked for: `actioned_at` outlives the dismissal.\n const targetStatus: NotificationStatus = notification.actionedAt != null ? 'actioned' : status ?? 'read'\n notification.status = targetStatus\n notification.dismissedAt = null\n\n if (targetStatus === 'unread') {\n notification.readAt = null\n } else if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.RESTORED, {\n notificationId: notification.id,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n status: targetStatus,\n })\n\n return notification\n },\n\n async executeAction(notificationId, input, ctx) {\n const em = rootEm.fork()\n const notification = await findScopedNotificationOrThrow(em, notificationId, ctx)\n\n const actionData = notification.actionData\n const action = actionData?.actions?.find((a) => a.id === input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n // Reject an already-actioned notification before dispatching the command,\n // so a retry or double-click cannot re-run the side effect. `actioned_at` is\n // the durable marker: `status` is overwritten by a dismissal.\n if (notification.status === 'actioned' || notification.actionedAt != null) {\n throw actionAlreadyExecuted()\n }\n\n if (action.commandId) {\n const declared = findDeclaredCommandAction(notification.type, action)\n if (!declared || !(await userHoldsActionFeatures(declared.requiredFeatures, ctx))) {\n throw await actionForbidden()\n }\n }\n\n const actionedAt = new Date()\n const previousStatus = notification.status\n const previousActionedAt = notification.actionedAt ?? null\n const previousActionTaken = notification.actionTaken ?? null\n\n // Atomically claim the notification so only one concurrent request can run\n // the side-effecting command. The conditional UPDATE matches only while the\n // notification has not been actioned yet; a losing request updates 0 rows.\n const claimResult = (await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: 'actioned',\n actioned_at: actionedAt,\n action_taken: input.actionId,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('status' as any, '!=', 'actioned')\n .where('actioned_at' as any, 'is', null)\n .executeTakeFirst()) as { numUpdatedRows?: bigint | number } | undefined\n\n if (Number(claimResult?.numUpdatedRows ?? 0) === 0) {\n throw actionAlreadyExecuted()\n }\n\n // The claim is provisional: if the side-effecting command fails, the action\n // never actually completed, so release the claim to its prior state. This\n // lets the user retry the action instead of the notification being locked as\n // `actioned` forever. The claim is owned through its own `actioned_at` and\n // `action_taken`, not through `status`: a dismissal that lands while the\n // command runs overwrites `status`, and the release must still clear the\n // claim then \u2014 while leaving that dismissal in place.\n const releaseClaim = async () => {\n await getDb(em)\n .updateTable('notifications' as any)\n .set({\n status: sql`case when status = 'actioned' then ${previousStatus} else status end`,\n actioned_at: previousActionedAt,\n action_taken: previousActionTaken,\n } as any)\n .where('id' as any, '=', notification.id)\n .where('recipient_user_id' as any, '=', ctx.userId as any)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .where('actioned_at' as any, '=', actionedAt)\n .where('action_taken' as any, '=', input.actionId)\n .executeTakeFirst()\n }\n\n let result: unknown = null\n\n if (action.commandId && commandBus && container) {\n const commandInput = {\n ...input.payload,\n id: notification.sourceEntityId,\n }\n\n // Build a CommandRuntimeContext from the notification service context\n const commandCtx = {\n container,\n auth: {\n sub: ctx.userId,\n tenantId: ctx.tenantId,\n orgId: ctx.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: ctx.organizationId ?? null,\n organizationIds: ctx.organizationId ? [ctx.organizationId] : null,\n }\n\n let commandResult: { result: unknown }\n try {\n commandResult = await commandBus.execute(action.commandId, {\n input: commandInput,\n ctx: commandCtx,\n metadata: {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n resourceKind: 'notifications',\n },\n })\n } catch (err) {\n // Never let a rollback failure mask the original command error \u2014 the\n // caller needs the real failure to decide whether to retry.\n try {\n await releaseClaim()\n } catch (releaseErr) {\n debug('failed to release notification action claim', releaseErr)\n }\n throw err\n }\n\n result = commandResult.result\n }\n\n notification.status = 'actioned'\n notification.actionedAt = actionedAt\n notification.actionTaken = input.actionId\n notification.actionResult = result as Record<string, unknown>\n\n if (!notification.readAt) {\n notification.readAt = new Date()\n }\n\n await em.flush()\n\n await invalidateNotificationCache(container, ctx, 'updated', notification.id)\n await eventBus.emit(NOTIFICATION_EVENTS.ACTIONED, {\n notificationId: notification.id,\n actionId: input.actionId,\n userId: ctx.userId,\n tenantId: ctx.tenantId,\n })\n\n return { notification, result }\n },\n\n async getUnreadCount(ctx) {\n const em = rootEm.fork()\n return em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n })\n },\n\n async getPollData(ctx, since) {\n const em = rootEm.fork()\n const filters: Record<string, unknown> = {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n }\n\n if (since) {\n filters.createdAt = { $gt: new Date(since) }\n }\n\n const [notifications, unreadCount] = await Promise.all([\n em.find(Notification, filters, {\n orderBy: { createdAt: 'desc' },\n limit: 50,\n }),\n em.count(Notification, {\n recipientUserId: ctx.userId,\n tenantId: ctx.tenantId,\n status: 'unread',\n $and: [\n buildNotificationReadScopeWhere(ctx),\n inAppVisibleFilter(),\n ],\n }),\n ])\n\n const recent = notifications.map(toNotificationDto)\n const hasNew = since ? recent.length > 0 : false\n\n return {\n unreadCount,\n recent,\n hasNew,\n lastId: recent[0]?.id,\n }\n },\n\n async cleanupExpired() {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const affectedScopes = await db\n .selectFrom('notifications' as any)\n .select([\n 'tenant_id' as any,\n 'organization_id' as any,\n ])\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .distinct()\n .execute() as Array<{ tenant_id: string; organization_id: string | null }>\n\n if (!affectedScopes.length) return 0\n\n const updateResult = await db\n .updateTable('notifications' as any)\n .set({\n status: 'dismissed',\n dismissed_at: sql`now()`,\n } as any)\n .where('expires_at' as any, '<', sql`now()`)\n .where('status' as any, 'not in', ['actioned', 'dismissed'])\n .executeTakeFirst() as { numUpdatedRows?: bigint | number } | undefined\n\n for (const scope of affectedScopes) {\n await invalidateNotificationCache(container, {\n tenantId: scope.tenant_id,\n organizationId: scope.organization_id,\n }, 'updated')\n }\n\n return Number(updateResult?.numUpdatedRows ?? 0)\n },\n\n async deleteBySource(sourceEntityType, sourceEntityId, ctx) {\n const em = rootEm.fork()\n const db = getDb(em)\n\n const deleteResult = await db\n .deleteFrom('notifications' as any)\n .where('source_entity_type' as any, '=', sourceEntityType)\n .where('source_entity_id' as any, '=', sourceEntityId)\n .where('tenant_id' as any, '=', ctx.tenantId)\n .executeTakeFirst() as { numDeletedRows?: bigint | number } | undefined\n\n const deletedCount = Number(deleteResult?.numDeletedRows ?? 0)\n if (deletedCount > 0) {\n await invalidateNotificationCache(container, ctx, 'deleted')\n }\n\n return deletedCount\n },\n }\n}\n\n/**\n * Helper to create notification service from a DI container.\n * Use this in API routes and commands to avoid DI resolution issues.\n */\nexport function resolveNotificationService(container: {\n resolve: (name: string) => unknown\n}): NotificationService {\n const em = container.resolve('em') as EntityManager\n const eventBus = container.resolve('eventBus') as { emit: (event: string, payload: unknown) => Promise<void> }\n\n // commandBus may not be registered in all contexts, so resolve it safely\n let commandBus: NotificationServiceDeps['commandBus']\n try {\n commandBus = container.resolve('commandBus') as typeof commandBus\n } catch {\n // commandBus not available - actions with commandId won't work\n commandBus = undefined\n }\n\n return createNotificationService({ em, eventBus, commandBus, container })\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.7770.1.970b396ebb",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -266,16 +266,16 @@
|
|
|
266
266
|
"zod": "^4.4.3"
|
|
267
267
|
},
|
|
268
268
|
"peerDependencies": {
|
|
269
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
270
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
271
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
269
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7770.1.970b396ebb",
|
|
270
|
+
"@open-mercato/shared": "0.10.1-develop.7770.1.970b396ebb",
|
|
271
|
+
"@open-mercato/ui": "0.10.1-develop.7770.1.970b396ebb",
|
|
272
272
|
"react": "^19.0.0",
|
|
273
273
|
"react-dom": "^19.0.0"
|
|
274
274
|
},
|
|
275
275
|
"devDependencies": {
|
|
276
|
-
"@open-mercato/ai-assistant": "0.10.1-develop.
|
|
277
|
-
"@open-mercato/shared": "0.10.1-develop.
|
|
278
|
-
"@open-mercato/ui": "0.10.1-develop.
|
|
276
|
+
"@open-mercato/ai-assistant": "0.10.1-develop.7770.1.970b396ebb",
|
|
277
|
+
"@open-mercato/shared": "0.10.1-develop.7770.1.970b396ebb",
|
|
278
|
+
"@open-mercato/ui": "0.10.1-develop.7770.1.970b396ebb",
|
|
279
279
|
"@testing-library/dom": "^10.4.1",
|
|
280
280
|
"@testing-library/jest-dom": "^7.0.1",
|
|
281
281
|
"@testing-library/react": "^16.3.3",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
SearchModuleConfig,
|
|
3
|
-
SearchBuildContext,
|
|
4
3
|
SearchResultPresenter,
|
|
5
4
|
SearchIndexSource,
|
|
6
5
|
} from '@open-mercato/shared/modules/search'
|
|
@@ -40,7 +39,6 @@ function formatSubtitle(...parts: Array<unknown>): string | undefined {
|
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
function buildIndexSource(
|
|
43
|
-
ctx: SearchBuildContext,
|
|
44
42
|
presenter: SearchResultPresenter,
|
|
45
43
|
lines: string[],
|
|
46
44
|
): SearchIndexSource | null {
|
|
@@ -48,7 +46,7 @@ function buildIndexSource(
|
|
|
48
46
|
return {
|
|
49
47
|
text: lines,
|
|
50
48
|
presenter,
|
|
51
|
-
checksumSource: {
|
|
49
|
+
checksumSource: { text: lines, presenter },
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
|
|
@@ -63,13 +61,10 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
63
61
|
const { t } = await resolveTranslations()
|
|
64
62
|
const record = ctx.record
|
|
65
63
|
const lines: string[] = []
|
|
66
|
-
appendLine(lines, '
|
|
67
|
-
appendLine(lines, 'Email', record.email)
|
|
64
|
+
appendLine(lines, 'Customer user ID', record.id ?? 'customer_accounts:customer_user')
|
|
68
65
|
return buildIndexSource(
|
|
69
|
-
ctx,
|
|
70
66
|
{
|
|
71
67
|
title: pickString(record.display_name, record.displayName) ?? String(record.id),
|
|
72
|
-
subtitle: formatSubtitle(record.email),
|
|
73
68
|
icon: 'user',
|
|
74
69
|
badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),
|
|
75
70
|
},
|
|
@@ -81,14 +76,14 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
81
76
|
const record = ctx.record
|
|
82
77
|
return {
|
|
83
78
|
title: pickString(record.display_name, record.displayName) ?? String(record.id),
|
|
84
|
-
subtitle: formatSubtitle(record.email),
|
|
85
79
|
icon: 'user',
|
|
86
80
|
badge: t('customer_accounts.search.badge.customerUser', 'Customer User'),
|
|
87
81
|
}
|
|
88
82
|
},
|
|
89
83
|
resolveUrl: async (ctx) => `/backend/customer-accounts/users/${encodeURIComponent(String(ctx.record.id))}`,
|
|
90
84
|
fieldPolicy: {
|
|
91
|
-
searchable: ['display_name'
|
|
85
|
+
searchable: ['display_name'],
|
|
86
|
+
hashOnly: ['email'],
|
|
92
87
|
excluded: ['password_hash', 'email_hash'],
|
|
93
88
|
},
|
|
94
89
|
},
|
|
@@ -104,7 +99,6 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
104
99
|
appendLine(lines, 'Name', record.name)
|
|
105
100
|
appendLine(lines, 'Description', record.description)
|
|
106
101
|
return buildIndexSource(
|
|
107
|
-
ctx,
|
|
108
102
|
{
|
|
109
103
|
title: pickString(record.name) ?? String(record.id),
|
|
110
104
|
subtitle: formatSubtitle(snippet(record.description)),
|
|
@@ -10,6 +10,9 @@ import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
|
10
10
|
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
11
|
import { CUSTOMER_INTERACTION_TASK_SOURCE, EXAMPLE_TODO_SOURCE } from './lib/interactionCompatibility'
|
|
12
12
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
13
|
+
import { defaultEncryptionMaps } from './encryption'
|
|
14
|
+
import type { TenantDataEncryptionService } from '@open-mercato/shared/lib/encryption/tenantDataEncryptionService'
|
|
15
|
+
import { isTenantDataEncryptionEnabled } from '@open-mercato/shared/lib/encryption/toggles'
|
|
13
16
|
|
|
14
17
|
const logger = createLogger('customers')
|
|
15
18
|
|
|
@@ -414,6 +417,61 @@ function appendCustomFieldLines(lines: string[], customFields: Record<string, un
|
|
|
414
417
|
}
|
|
415
418
|
}
|
|
416
419
|
|
|
420
|
+
function normalizeSearchFieldKey(key: string): string {
|
|
421
|
+
return key.replace(/^cf[:_]/, '').replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase()
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function resolveProfileEncryptedFields(ctx: SearchContext, kind: CustomerProfileKind, entity: Record<string, unknown> | null): Promise<string[]> {
|
|
425
|
+
if (!isTenantDataEncryptionEnabled()) {
|
|
426
|
+
throw new Error('[internal] Customer vector indexing requires available encryption declarations')
|
|
427
|
+
}
|
|
428
|
+
const container = ctx.container as { resolve?: (name: string) => unknown } | undefined
|
|
429
|
+
if (typeof container?.resolve !== 'function') {
|
|
430
|
+
throw new Error('[internal] Customer search encryption policy container is unavailable')
|
|
431
|
+
}
|
|
432
|
+
const service = container.resolve('tenantEncryptionService') as Pick<TenantDataEncryptionService, 'getEncryptedFieldNames'> | undefined
|
|
433
|
+
if (typeof service?.getEncryptedFieldNames !== 'function') {
|
|
434
|
+
throw new Error('[internal] Customer search encryption policy service is unavailable')
|
|
435
|
+
}
|
|
436
|
+
const organizationIds = new Set([ctx.organizationId])
|
|
437
|
+
for (const record of [ctx.record, entity]) {
|
|
438
|
+
const organizationId = record?.organization_id ?? record?.organizationId
|
|
439
|
+
if (typeof organizationId === 'string' && organizationId) organizationIds.add(organizationId)
|
|
440
|
+
}
|
|
441
|
+
const fields = await Promise.all(Array.from(organizationIds).flatMap((organizationId) => [
|
|
442
|
+
`customers:customer_${kind}_profile`,
|
|
443
|
+
'customers:customer_entity',
|
|
444
|
+
].map((entityId) => service.getEncryptedFieldNames(entityId, ctx.tenantId, organizationId, { ignoreRuntimeHealth: true }))))
|
|
445
|
+
return fields.flat()
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function selectProfileSearchFields(
|
|
449
|
+
source: Record<string, unknown> | null,
|
|
450
|
+
kind: CustomerProfileKind,
|
|
451
|
+
options: { forEmbedding?: boolean; customerEntity?: boolean; encryptedFields?: string[] } = {},
|
|
452
|
+
): Record<string, unknown> {
|
|
453
|
+
const entityId = `customers:customer_${kind}_profile`
|
|
454
|
+
const policy = searchConfig.entities.find((entry) => entry.entityId === entityId)?.fieldPolicy
|
|
455
|
+
const allowed = new Set((options.customerEntity
|
|
456
|
+
? ['display_name', 'description', 'status', 'lifecycle_stage']
|
|
457
|
+
: policy?.searchable ?? []).map(normalizeSearchFieldKey))
|
|
458
|
+
const excluded = new Set([...(policy?.hashOnly ?? []), ...(policy?.excluded ?? [])].map(normalizeSearchFieldKey))
|
|
459
|
+
if (options.forEmbedding) {
|
|
460
|
+
for (const field of options.encryptedFields ?? []) excluded.add(normalizeSearchFieldKey(field))
|
|
461
|
+
for (const map of defaultEncryptionMaps) {
|
|
462
|
+
if (map.entityId !== entityId && map.entityId !== 'customers:customer_entity') continue
|
|
463
|
+
for (const entry of map.fields) excluded.add(normalizeSearchFieldKey(entry.field))
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const fields: Record<string, unknown> = {}
|
|
467
|
+
for (const [key, value] of Object.entries(source ?? {})) {
|
|
468
|
+
if (value !== null && typeof value === 'object') continue
|
|
469
|
+
const field = normalizeSearchFieldKey(key)
|
|
470
|
+
if (allowed.has(field) && !excluded.has(field)) fields[field] = value
|
|
471
|
+
}
|
|
472
|
+
return fields
|
|
473
|
+
}
|
|
474
|
+
|
|
417
475
|
function pickValue(source: Record<string, unknown> | null | undefined, ...keys: string[]): unknown {
|
|
418
476
|
if (!source) return undefined
|
|
419
477
|
for (const key of keys) {
|
|
@@ -444,12 +502,8 @@ function pickLabel(...candidates: Array<unknown>): string | null {
|
|
|
444
502
|
function appendCustomerEntityLines(
|
|
445
503
|
lines: string[],
|
|
446
504
|
entity: Record<string, unknown> | null,
|
|
447
|
-
contactLabel: 'Customer' | 'Primary' = 'Customer',
|
|
448
505
|
) {
|
|
449
506
|
if (!entity) return
|
|
450
|
-
appendLine(lines, 'Customer', pickValue(entity, 'display_name', 'displayName') ?? entity.id)
|
|
451
|
-
appendLine(lines, `${contactLabel} email`, pickValue(entity, 'primary_email', 'primaryEmail'))
|
|
452
|
-
appendLine(lines, `${contactLabel} phone`, pickValue(entity, 'primary_phone', 'primaryPhone'))
|
|
453
507
|
appendLine(lines, 'Lifecycle stage', pickValue(entity, 'lifecycle_stage', 'lifecycleStage'))
|
|
454
508
|
appendLine(lines, 'Status', pickValue(entity, 'status'))
|
|
455
509
|
}
|
|
@@ -488,6 +542,9 @@ function resolvePersonPresenter(
|
|
|
488
542
|
customFields: Record<string, unknown>,
|
|
489
543
|
): SearchResultPresenter {
|
|
490
544
|
const fallbackEntityId = resolveCustomerEntityId(record)
|
|
545
|
+
record = { id: record.id, ...selectProfileSearchFields(record, 'person') }
|
|
546
|
+
entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, 'person', { customerEntity: true }) } : null
|
|
547
|
+
customFields = selectProfileSearchFields(customFields, 'person')
|
|
491
548
|
const firstName = record.first_name ?? record.firstName ?? customFields.first_name ?? customFields.firstName ?? ''
|
|
492
549
|
const lastName = record.last_name ?? record.lastName ?? customFields.last_name ?? customFields.lastName ?? ''
|
|
493
550
|
const nameParts = [firstName, lastName].filter(Boolean).join(' ')
|
|
@@ -504,10 +561,6 @@ function resolvePersonPresenter(
|
|
|
504
561
|
if (jobTitle) subtitlePieces.push(String(jobTitle))
|
|
505
562
|
const department = record.department ?? customFields.department
|
|
506
563
|
if (department) subtitlePieces.push(String(department))
|
|
507
|
-
const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')
|
|
508
|
-
if (primaryEmail) subtitlePieces.push(String(primaryEmail))
|
|
509
|
-
const primaryPhone = pickValue(entity, 'primary_phone', 'primaryPhone')
|
|
510
|
-
if (primaryPhone) subtitlePieces.push(String(primaryPhone))
|
|
511
564
|
const summary = snippet(
|
|
512
565
|
(pickValue(entity, 'description') as string | undefined) ??
|
|
513
566
|
(customFields.summary as string | undefined) ??
|
|
@@ -531,6 +584,9 @@ function resolveCompanyPresenter(
|
|
|
531
584
|
customFields: Record<string, unknown>,
|
|
532
585
|
): SearchResultPresenter {
|
|
533
586
|
const fallbackEntityId = resolveCustomerEntityId(record)
|
|
587
|
+
record = { id: record.id, ...selectProfileSearchFields(record, 'company') }
|
|
588
|
+
entity = entity ? { id: entity.id, ...selectProfileSearchFields(entity, 'company', { customerEntity: true }) } : null
|
|
589
|
+
customFields = selectProfileSearchFields(customFields, 'company')
|
|
534
590
|
const title =
|
|
535
591
|
(pickValue(entity, 'display_name', 'displayName') as string | undefined) ??
|
|
536
592
|
(customFields.display_name as string | undefined) ??
|
|
@@ -549,10 +605,6 @@ function resolveCompanyPresenter(
|
|
|
549
605
|
if (industry) subtitlePieces.push(String(industry))
|
|
550
606
|
const sizeBucket = record.size_bucket ?? record.sizeBucket
|
|
551
607
|
if (sizeBucket) subtitlePieces.push(String(sizeBucket))
|
|
552
|
-
if (entity) {
|
|
553
|
-
const primaryEmail = pickValue(entity, 'primary_email', 'primaryEmail')
|
|
554
|
-
if (primaryEmail) subtitlePieces.push(String(primaryEmail))
|
|
555
|
-
}
|
|
556
608
|
const summary = snippet(
|
|
557
609
|
(pickValue(entity, 'description') as string | undefined) ??
|
|
558
610
|
(customFields.summary as string | undefined) ??
|
|
@@ -616,21 +668,14 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
616
668
|
const { t } = await resolveTranslations()
|
|
617
669
|
const lines: string[] = []
|
|
618
670
|
const record = ctx.record
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
appendLine(lines, 'Last name', record.last_name ?? record.lastName ?? ctx.customFields.last_name)
|
|
622
|
-
appendLine(lines, 'Job title', record.job_title ?? record.jobTitle ?? ctx.customFields.job_title)
|
|
623
|
-
appendLine(lines, 'Department', record.department ?? record.department_name ?? record.departmentName ?? ctx.customFields.department)
|
|
624
|
-
appendLine(lines, 'Seniority', record.seniority ?? record.seniority_level ?? record.seniorityLevel ?? ctx.customFields.seniority)
|
|
625
|
-
appendLine(lines, 'Timezone', record.timezone ?? record.time_zone ?? record.timeZone ?? ctx.customFields.timezone)
|
|
626
|
-
appendLine(lines, 'LinkedIn', record.linked_in_url ?? record.linkedInUrl ?? ctx.customFields.linked_in_url)
|
|
627
|
-
appendLine(lines, 'Twitter', record.twitter_url ?? record.twitterUrl ?? ctx.customFields.twitter_url)
|
|
628
|
-
|
|
629
|
-
const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =
|
|
671
|
+
|
|
672
|
+
const { entity, entityId, profileCustomFields, entityOnlyCustomFields } =
|
|
630
673
|
await hydrateProfileContext(ctx, 'person')
|
|
631
|
-
|
|
674
|
+
const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, 'person', entity) }
|
|
675
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(record, 'person', embeddingOptions), '')
|
|
676
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, 'person', embeddingOptions), 'Person custom')
|
|
632
677
|
if (Object.keys(entityOnlyCustomFields).length) {
|
|
633
|
-
appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')
|
|
678
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, 'person', embeddingOptions), 'Customer custom')
|
|
634
679
|
}
|
|
635
680
|
if (!entity) {
|
|
636
681
|
logger.warn('Failed to load customer entity for person profile', {
|
|
@@ -640,8 +685,8 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
640
685
|
recordKeys: Object.keys(record),
|
|
641
686
|
})
|
|
642
687
|
}
|
|
643
|
-
appendCustomerEntityLines(lines, entity, '
|
|
644
|
-
ensureFallbackLines(lines, record)
|
|
688
|
+
appendCustomerEntityLines(lines, selectProfileSearchFields(entity, 'person', { ...embeddingOptions, customerEntity: true }))
|
|
689
|
+
ensureFallbackLines(lines, { id: record.id ?? entityId ?? 'customers:customer_person_profile' })
|
|
645
690
|
if (!lines.length) return null
|
|
646
691
|
|
|
647
692
|
if (!entityId) {
|
|
@@ -667,12 +712,7 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
667
712
|
text: lines,
|
|
668
713
|
presenter,
|
|
669
714
|
links,
|
|
670
|
-
checksumSource: {
|
|
671
|
-
record: ctx.record,
|
|
672
|
-
customFields: profileCustomFields,
|
|
673
|
-
entity,
|
|
674
|
-
entityCustomFields,
|
|
675
|
-
},
|
|
715
|
+
checksumSource: { text: lines, presenter, links },
|
|
676
716
|
}
|
|
677
717
|
},
|
|
678
718
|
|
|
@@ -728,22 +768,17 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
728
768
|
const { t } = await resolveTranslations()
|
|
729
769
|
const lines: string[] = []
|
|
730
770
|
const record = ctx.record
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
appendLine(lines, 'Domain', record.domain ?? record.website_domain ?? record.websiteDomain ?? ctx.customFields.domain)
|
|
734
|
-
appendLine(lines, 'Website', record.website_url ?? record.websiteUrl ?? ctx.customFields.website_url)
|
|
735
|
-
appendLine(lines, 'Industry', record.industry ?? ctx.customFields.industry)
|
|
736
|
-
appendLine(lines, 'Company size', record.size_bucket ?? record.sizeBucket ?? ctx.customFields.size_bucket)
|
|
737
|
-
appendLine(lines, 'Annual revenue', record.annual_revenue ?? record.annualRevenue ?? ctx.customFields.annual_revenue)
|
|
738
|
-
|
|
739
|
-
const { entity, entityId, profileCustomFields, entityCustomFields, entityOnlyCustomFields } =
|
|
771
|
+
|
|
772
|
+
const { entity, entityId, profileCustomFields, entityOnlyCustomFields } =
|
|
740
773
|
await hydrateProfileContext(ctx, 'company')
|
|
741
|
-
|
|
774
|
+
const embeddingOptions = { forEmbedding: true, encryptedFields: await resolveProfileEncryptedFields(ctx, 'company', entity) }
|
|
775
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(record, 'company', embeddingOptions), '')
|
|
776
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(profileCustomFields, 'company', embeddingOptions), 'Company custom')
|
|
742
777
|
if (Object.keys(entityOnlyCustomFields).length) {
|
|
743
|
-
appendCustomFieldLines(lines, entityOnlyCustomFields, 'Customer custom')
|
|
778
|
+
appendCustomFieldLines(lines, selectProfileSearchFields(entityOnlyCustomFields, 'company', embeddingOptions), 'Customer custom')
|
|
744
779
|
}
|
|
745
|
-
appendCustomerEntityLines(lines, entity, '
|
|
746
|
-
ensureFallbackLines(lines, record)
|
|
780
|
+
appendCustomerEntityLines(lines, selectProfileSearchFields(entity, 'company', { ...embeddingOptions, customerEntity: true }))
|
|
781
|
+
ensureFallbackLines(lines, { id: record.id ?? entityId ?? 'customers:customer_company_profile' })
|
|
747
782
|
if (!lines.length) return null
|
|
748
783
|
|
|
749
784
|
const presenter = resolveCompanyPresenter(t, record, entity, ctx.customFields)
|
|
@@ -761,12 +796,7 @@ export const searchConfig: SearchModuleConfig = {
|
|
|
761
796
|
text: lines,
|
|
762
797
|
presenter,
|
|
763
798
|
links,
|
|
764
|
-
checksumSource: {
|
|
765
|
-
record: ctx.record,
|
|
766
|
-
customFields: profileCustomFields,
|
|
767
|
-
entity,
|
|
768
|
-
entityCustomFields,
|
|
769
|
-
},
|
|
799
|
+
checksumSource: { text: lines, presenter, links },
|
|
770
800
|
}
|
|
771
801
|
},
|
|
772
802
|
|