@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7748.1.d745ba61aa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- package/dist/generated/entities/attachment_partition/index.js +2 -0
- package/dist/generated/entities/attachment_partition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
- package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
- package/dist/modules/api_keys/data/entities.js +7 -0
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +31 -9
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/attachments/api/file/[id]/route.js +16 -10
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +100 -56
- package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
- package/dist/modules/attachments/api/library/route.js +56 -17
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +2 -0
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +74 -34
- package/dist/modules/attachments/api/route.js.map +3 -3
- package/dist/modules/attachments/api/transfer/route.js +63 -37
- package/dist/modules/attachments/api/transfer/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -0
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +41 -0
- package/dist/modules/attachments/data/validators.js.map +7 -0
- package/dist/modules/attachments/di.js +5 -2
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/generators.js +29 -0
- package/dist/modules/attachments/generators.js.map +7 -0
- package/dist/modules/attachments/index.js +8 -1
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-errors.js +35 -0
- package/dist/modules/attachments/lib/access-errors.js.map +7 -0
- package/dist/modules/attachments/lib/access-mutation.js +23 -0
- package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
- package/dist/modules/attachments/lib/access-protection.js +73 -0
- package/dist/modules/attachments/lib/access-protection.js.map +7 -0
- package/dist/modules/attachments/lib/access-query.js +74 -0
- package/dist/modules/attachments/lib/access-query.js.map +7 -0
- package/dist/modules/attachments/lib/access-registry.js +87 -0
- package/dist/modules/attachments/lib/access-registry.js.map +7 -0
- package/dist/modules/attachments/lib/access-runner.js +161 -0
- package/dist/modules/attachments/lib/access-runner.js.map +7 -0
- package/dist/modules/attachments/lib/access-types.js +1 -0
- package/dist/modules/attachments/lib/access-types.js.map +7 -0
- package/dist/modules/attachments/lib/attachment-service.js +14 -6
- package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
- package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
- package/dist/modules/attachments/lib/partitions.js +3 -2
- package/dist/modules/attachments/lib/partitions.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/target-access-service.js +12 -3
- package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
- package/dist/modules/attachments/setup.js +10 -0
- package/dist/modules/attachments/setup.js.map +2 -2
- package/dist/modules/notifications/api/[id]/action/route.js +8 -0
- package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
- package/dist/modules/notifications/api/route.js +17 -0
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +1 -1
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +45 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +27 -0
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
- package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
- package/dist/modules/staff/notifications.js +4 -2
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +18 -4
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-types.js +2 -2
- package/dist/modules/workflows/lib/activity-types.js.map +2 -2
- package/dist/modules/workflows/lib/condition-handler.js +7 -1
- package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +13 -0
- package/dist/modules/workflows/lib/duration.js.map +2 -2
- package/dist/modules/workflows/lib/instance-status.js +16 -0
- package/dist/modules/workflows/lib/instance-status.js.map +7 -0
- package/dist/modules/workflows/lib/step-handler.js +9 -2
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-handler.js +16 -8
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-quick-action.js +13 -10
- package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
- package/dist/modules/workflows/lib/task-sla.js +10 -0
- package/dist/modules/workflows/lib/task-sla.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +2 -1
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +29 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/notifications.js +4 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entities/attachment_partition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
- package/src/modules/api_keys/data/entities.ts +25 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
- package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
- package/src/modules/api_keys/services/apiKeyService.ts +55 -9
- package/src/modules/attachments/api/file/[id]/route.ts +12 -9
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
- package/src/modules/attachments/api/library/[id]/route.ts +87 -58
- package/src/modules/attachments/api/library/route.ts +40 -11
- package/src/modules/attachments/api/partitions/route.ts +2 -0
- package/src/modules/attachments/api/route.ts +68 -35
- package/src/modules/attachments/api/transfer/route.ts +65 -42
- package/src/modules/attachments/data/entities.ts +4 -0
- package/src/modules/attachments/data/validators.ts +36 -0
- package/src/modules/attachments/di.ts +4 -1
- package/src/modules/attachments/generators.ts +27 -0
- package/src/modules/attachments/i18n/de.json +4 -0
- package/src/modules/attachments/i18n/en.json +4 -0
- package/src/modules/attachments/i18n/es.json +4 -0
- package/src/modules/attachments/i18n/ko.json +4 -0
- package/src/modules/attachments/i18n/pl.json +4 -0
- package/src/modules/attachments/index.ts +18 -0
- package/src/modules/attachments/lib/access-errors.ts +32 -0
- package/src/modules/attachments/lib/access-mutation.ts +26 -0
- package/src/modules/attachments/lib/access-protection.ts +83 -0
- package/src/modules/attachments/lib/access-query.ts +86 -0
- package/src/modules/attachments/lib/access-registry.ts +117 -0
- package/src/modules/attachments/lib/access-runner.ts +183 -0
- package/src/modules/attachments/lib/access-types.ts +78 -0
- package/src/modules/attachments/lib/attachment-service.ts +8 -4
- package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
- package/src/modules/attachments/lib/partitions.ts +3 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
- package/src/modules/attachments/lib/target-access-service.ts +10 -3
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
- package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
- package/src/modules/attachments/setup.ts +10 -0
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/notifications/api/[id]/action/route.ts +8 -0
- package/src/modules/notifications/api/route.ts +17 -0
- package/src/modules/notifications/data/validators.ts +1 -1
- package/src/modules/notifications/i18n/de.json +2 -0
- package/src/modules/notifications/i18n/en.json +2 -0
- package/src/modules/notifications/i18n/es.json +2 -0
- package/src/modules/notifications/i18n/ko.json +2 -0
- package/src/modules/notifications/i18n/pl.json +2 -0
- package/src/modules/notifications/lib/notificationService.ts +67 -1
- package/src/modules/notifications/lib/routeHelpers.ts +38 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
- package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
- package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
- package/src/modules/staff/notifications.ts +2 -0
- package/src/modules/workflows/AGENTS.md +2 -2
- package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
- package/src/modules/workflows/data/entities.ts +6 -0
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/ko.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
- package/src/modules/workflows/lib/activity-executor.ts +36 -4
- package/src/modules/workflows/lib/activity-types.ts +2 -2
- package/src/modules/workflows/lib/condition-handler.ts +16 -1
- package/src/modules/workflows/lib/duration.ts +25 -0
- package/src/modules/workflows/lib/instance-status.ts +29 -0
- package/src/modules/workflows/lib/step-handler.ts +16 -1
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-quick-action.ts +19 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- package/src/modules/workflows/lib/transition-handler.ts +2 -1
- package/src/modules/workflows/lib/workflow-executor.ts +44 -1
- package/src/modules/workflows/notifications.ts +4 -0
|
@@ -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 type NotificationPollData,\n} from '@open-mercato/shared/modules/notifications/types'\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\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 * 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 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 id: notification.sourceEntityId,\n ...input.payload,\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 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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,7 +4,9 @@ import { resolveOrganizationScopeForRequest } from "@open-mercato/core/modules/d
|
|
|
4
4
|
import { isCrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
5
5
|
import { runRouteMutationGuards } from "@open-mercato/shared/lib/crud/route-mutation-guard";
|
|
6
6
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
7
|
+
import { NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE } from "@open-mercato/shared/modules/notifications/types";
|
|
7
8
|
import { resolveNotificationService } from "./notificationService.js";
|
|
9
|
+
import { actionConflictResponseSchema } from "../api/openapi.js";
|
|
8
10
|
const NOTIFICATION_RESOURCE_KIND = "notifications.notification";
|
|
9
11
|
const NOTIFICATION_SETTINGS_RESOURCE_KIND = "notifications.settings";
|
|
10
12
|
const NOTIFICATION_PREFERENCE_RESOURCE_KIND = "notifications.preference";
|
|
@@ -23,6 +25,20 @@ async function notificationValidationErrorResponse(error) {
|
|
|
23
25
|
{ status: 400 }
|
|
24
26
|
);
|
|
25
27
|
}
|
|
28
|
+
async function rejectNotificationActionCommands(data) {
|
|
29
|
+
if (!data.actions?.some((action) => action.commandId)) return null;
|
|
30
|
+
const { t } = await resolveTranslations();
|
|
31
|
+
return Response.json(
|
|
32
|
+
{
|
|
33
|
+
error: t(
|
|
34
|
+
"notifications.error.actionCommandNotAllowed",
|
|
35
|
+
"Notification actions created through the API cannot run commands"
|
|
36
|
+
),
|
|
37
|
+
code: NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE
|
|
38
|
+
},
|
|
39
|
+
{ status: 400 }
|
|
40
|
+
);
|
|
41
|
+
}
|
|
26
42
|
function notificationCrudErrorResponse(error) {
|
|
27
43
|
if (!isCrudHttpError(error)) return null;
|
|
28
44
|
return Response.json(error.body ?? { error: "Notification request failed" }, { status: error.status });
|
|
@@ -113,6 +129,8 @@ function createBulkNotificationRoute(schema, serviceMethod) {
|
|
|
113
129
|
if (!parsed.success) {
|
|
114
130
|
return notificationValidationErrorResponse(parsed.error);
|
|
115
131
|
}
|
|
132
|
+
const commandRejection = await rejectNotificationActionCommands(parsed.data);
|
|
133
|
+
if (commandRejection) return commandRejection;
|
|
116
134
|
try {
|
|
117
135
|
const guarded = await runGuardedNotificationWrite(
|
|
118
136
|
ctx.container,
|
|
@@ -165,6 +183,14 @@ function createBulkNotificationOpenApi(schema, summary, description) {
|
|
|
165
183
|
})
|
|
166
184
|
}
|
|
167
185
|
}
|
|
186
|
+
},
|
|
187
|
+
400: {
|
|
188
|
+
description: `Invalid payload, or an action carries a \`commandId\` (code: ${NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE})`,
|
|
189
|
+
content: {
|
|
190
|
+
"application/json": {
|
|
191
|
+
schema: actionConflictResponseSchema
|
|
192
|
+
}
|
|
193
|
+
}
|
|
168
194
|
}
|
|
169
195
|
}
|
|
170
196
|
}
|
|
@@ -232,6 +258,7 @@ export {
|
|
|
232
258
|
createSingleNotificationActionRoute,
|
|
233
259
|
notificationCrudErrorResponse,
|
|
234
260
|
notificationValidationErrorResponse,
|
|
261
|
+
rejectNotificationActionCommands,
|
|
235
262
|
requireResolvedNotificationTenantScope,
|
|
236
263
|
resolveGuardedNotificationContext,
|
|
237
264
|
resolveNotificationContext,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/notifications/lib/routeHelpers.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport { resolveRequestContext } from '@open-mercato/shared/lib/api/context'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { resolveNotificationService, type NotificationService } from './notificationService'\n\n/**\n * Mutation-guard resource kind for notification rows.\n */\nexport const NOTIFICATION_RESOURCE_KIND = 'notifications.notification'\n\n/**\n * Mutation-guard resource kind for notification delivery settings.\n */\nexport const NOTIFICATION_SETTINGS_RESOURCE_KIND = 'notifications.settings'\n\n/**\n * Mutation-guard resource kind for a user's channel preferences.\n */\nexport const NOTIFICATION_PREFERENCE_RESOURCE_KIND = 'notifications.preference'\n\n/**\n * Notification scope context for service calls\n */\nexport interface NotificationScope {\n tenantId: string\n organizationId: string | null\n organizationIds?: string[] | null\n userId: string | null\n}\n\n/**\n * Resolved notification context from a request\n */\nexport interface NotificationRequestContext {\n service: NotificationService\n scope: NotificationScope\n ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx']\n}\n\nfunction formatZodIssues(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.length ? `${issue.path.join('.')}: ` : ''\n return `${path}${issue.message}`\n })\n .join('; ')\n}\n\nexport async function notificationValidationErrorResponse(error: z.ZodError): Promise<Response> {\n const { t } = await resolveTranslations()\n const prefix = t('api.errors.invalidPayload', 'Invalid request body')\n const details = formatZodIssues(error)\n return Response.json(\n { error: details ? `${prefix}: ${details}` : prefix },\n { status: 400 },\n )\n}\n\nexport function notificationCrudErrorResponse(error: unknown): Response | null {\n if (!isCrudHttpError(error)) return null\n return Response.json(error.body ?? { error: 'Notification request failed' }, { status: error.status })\n}\n\n/**\n * Machine-readable discriminator for the tenant-scope rejection, mirroring\n * `ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE`. Without it a client cannot tell an unresolved scope\n * apart from an ordinary permission denial, since both are a 403 carrying only a message.\n */\nexport const TENANT_SCOPE_REQUIRED_ERROR_CODE = 'tenant_scope_required'\n\n/**\n * Fail closed when a request cannot be resolved to a tenant.\n *\n * `resolveNotificationContext` falls back to `''` when neither the organization scope nor the auth\n * context yields a tenant \u2014 reachable for a genuinely tenant-less principal such as an unscoped\n * super-admin API key. `notifications.tenant_id` is a NOT NULL uuid, so that `''` makes the driver\n * reject every read and write built from the scope; dropping the tenant predicate instead would\n * leave `recipientUserId` as the only thing keeping a read inside one tenant.\n *\n * Unlike the audit-log read guard there is no `isSuperAdmin` escape hatch: notification rows are\n * per-recipient and per-tenant, so there is no cross-tenant read mode to preserve.\n *\n * Plain truthiness is deliberate \u2014 it matches the `?? ''` sentinel and also rejects a null or\n * omitted tenant reaching this helper from a caller that builds its own scope.\n */\nexport async function requireResolvedNotificationTenantScope(\n scope: { tenantId?: string | null },\n): Promise<Response | null> {\n if (scope.tenantId) return null\n const { t } = await resolveTranslations()\n return Response.json(\n {\n error: t('api.errors.forbidden', 'Forbidden'),\n code: TENANT_SCOPE_REQUIRED_ERROR_CODE,\n },\n { status: 403 },\n )\n}\n\n/**\n * Resolve notification service and scope from a request.\n * Centralizes the common pattern used across all notification API routes.\n */\nexport async function resolveNotificationContext(req: Request): Promise<NotificationRequestContext> {\n const { ctx } = await resolveRequestContext(req)\n const organizationScope = await resolveOrganizationScopeForRequest({\n container: ctx.container,\n auth: ctx.auth,\n request: req,\n ...(ctx.selectedOrganizationId === undefined\n ? {}\n : { selectedId: ctx.selectedOrganizationId }),\n })\n const tenantId = organizationScope.tenantId ?? ctx.auth?.tenantId ?? ''\n const organizationId = organizationScope.selectedId\n const organizationIds = organizationScope.filterIds\n ctx.organizationScope = organizationScope\n ctx.selectedOrganizationId = organizationId\n ctx.organizationIds = organizationIds\n return {\n service: resolveNotificationService(ctx.container),\n scope: {\n tenantId,\n // The *creator's* org (the shared resolver already falls back to the caller's own org when\n // nothing is selected), and the push strategy later scopes the recipient's device lookup to\n // it. Same-org and self-notify (the common paths) match; a cross-org notify \u2014 an admin in\n // org A notifying a user whose devices live in org B \u2014 won't match the recipient's devices,\n // so push is silently dropped for that case. Same for a deliberate all-organizations\n // selection, which stays tenant-level (org=null) and so only reaches null-org devices.\n // In-app delivery is unaffected. If cross-org push is ever required, scope the device\n // lookup by the recipient's org instead.\n organizationId,\n organizationIds,\n userId: ctx.auth?.sub ?? null,\n },\n ctx,\n }\n}\n\nexport type GuardedNotificationContext =\n | ({ ok: true } & NotificationRequestContext)\n | { ok: false; response: Response }\n\n/**\n * Resolve the notification context and reject the request when it has no tenant.\n *\n * Writes are structurally safe because every one of them funnels through\n * `runGuardedNotificationWrite`. Reads have no such choke point, so they resolve their context\n * through this wrapper instead: a route that forgets to check cannot compile past the discriminated\n * result, which makes the guard impossible to skip by omission rather than by convention.\n */\nexport async function resolveGuardedNotificationContext(req: Request): Promise<GuardedNotificationContext> {\n const resolved = await resolveNotificationContext(req)\n const tenantScopeGuard = await requireResolvedNotificationTenantScope(resolved.scope)\n if (tenantScopeGuard) return { ok: false, response: tenantScopeGuard }\n return { ok: true, ...resolved }\n}\n\n/**\n * Mutation-guard options for a notification write.\n */\nexport interface NotificationMutationGuardOptions {\n resourceKind: string\n resourceId?: string | null\n operation: 'create' | 'update' | 'delete' | 'custom'\n payload?: Record<string, unknown> | null\n}\n\nexport type GuardedNotificationWriteResult<T> =\n | { ok: true; result: T }\n | { ok: false; response: Response }\n\n/**\n * Run a notification write through the mutation guard lifecycle.\n * Validates before the mutation, performs the write, then runs after-success\n * hooks only when the write succeeded and the guard requested them. Returns the\n * guard's own block response when validation fails so authorization behavior and\n * conflict shapes are preserved.\n */\nexport async function runGuardedNotificationWrite<T>(\n container: AwilixContainer,\n scope: NotificationScope,\n req: Request,\n options: NotificationMutationGuardOptions,\n write: () => Promise<T>,\n): Promise<GuardedNotificationWriteResult<T>> {\n const tenantScopeGuard = await requireResolvedNotificationTenantScope(scope)\n if (tenantScopeGuard) {\n return { ok: false, response: tenantScopeGuard }\n }\n\n const guarded = await runRouteMutationGuards({\n container,\n req,\n auth: {\n userId: scope.userId ?? '',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n input: {\n resourceKind: options.resourceKind,\n resourceId: options.resourceId ?? null,\n operation: options.operation,\n mutationPayload: options.payload ?? null,\n },\n })\n if (!guarded.ok) {\n return { ok: false, response: guarded.response }\n }\n\n const result = await write()\n\n await guarded.runAfterSuccess()\n\n return { ok: true, result }\n}\n\n/**\n * Create a POST handler for bulk notification creation routes.\n * Used by batch, role, and feature notification endpoints.\n */\nexport function createBulkNotificationRoute<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n serviceMethod: 'createBatch' | 'createForRole' | 'createForFeature'\n) {\n return async function POST(req: Request) {\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n const body = await req.json().catch(() => ({}))\n const parsed = schema.safeParse(body)\n if (!parsed.success) {\n return notificationValidationErrorResponse(parsed.error)\n }\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n operation: 'create',\n payload: parsed.data as Record<string, unknown>,\n },\n () => service[serviceMethod](parsed.data as never, scope),\n )\n if (!guarded.ok) return guarded.response\n const notifications = guarded.result\n\n return Response.json({\n ok: true,\n count: notifications.length,\n ids: notifications.map((n) => n.id),\n }, { status: 201 })\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n }\n}\n\n/**\n * Create OpenAPI spec for bulk notification creation routes.\n */\nexport function createBulkNotificationOpenApi<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n summary: string,\n description?: string\n) {\n return {\n POST: {\n summary,\n description,\n tags: ['Notifications'],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema,\n },\n },\n },\n responses: {\n 201: {\n description: 'Notifications created',\n content: {\n 'application/json': {\n schema: z.object({\n ok: z.boolean(),\n count: z.number(),\n ids: z.array(z.string().uuid()),\n }),\n },\n },\n },\n },\n },\n }\n}\n\n/**\n * Create a PUT handler for single notification action routes.\n * Used by read and dismiss endpoints.\n */\nexport function createSingleNotificationActionRoute(\n serviceMethod: 'markAsRead' | 'dismiss'\n) {\n return async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {\n const { id } = await params\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n resourceId: id,\n operation: 'update',\n },\n () => service[serviceMethod](id, scope),\n )\n if (!guarded.ok) return guarded.response\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n\n return Response.json({ ok: true })\n }\n}\n\n/**\n * Create OpenAPI spec for single notification action routes.\n */\nexport function createSingleNotificationActionOpenApi(\n summary: string,\n description: string\n) {\n return {\n PUT: {\n summary,\n tags: ['Notifications'],\n parameters: [\n {\n name: 'id',\n in: 'path',\n required: true,\n schema: { type: 'string', format: 'uuid' },\n },\n ],\n responses: {\n 200: {\n description,\n content: {\n 'application/json': {\n schema: z.object({ ok: z.boolean() }),\n },\n },\n },\n },\n },\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAElB,SAAS,6BAA6B;AACtC,SAAS,0CAA0C;AACnD,SAAS,uBAAuB;AAChC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,kCAA4D;
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod'\nimport type { AwilixContainer } from 'awilix'\nimport { resolveRequestContext } from '@open-mercato/shared/lib/api/context'\nimport { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { runRouteMutationGuards } from '@open-mercato/shared/lib/crud/route-mutation-guard'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE } from '@open-mercato/shared/modules/notifications/types'\nimport { resolveNotificationService, type NotificationService } from './notificationService'\nimport { actionConflictResponseSchema } from '../api/openapi'\n\n/**\n * Mutation-guard resource kind for notification rows.\n */\nexport const NOTIFICATION_RESOURCE_KIND = 'notifications.notification'\n\n/**\n * Mutation-guard resource kind for notification delivery settings.\n */\nexport const NOTIFICATION_SETTINGS_RESOURCE_KIND = 'notifications.settings'\n\n/**\n * Mutation-guard resource kind for a user's channel preferences.\n */\nexport const NOTIFICATION_PREFERENCE_RESOURCE_KIND = 'notifications.preference'\n\n/**\n * Notification scope context for service calls\n */\nexport interface NotificationScope {\n tenantId: string\n organizationId: string | null\n organizationIds?: string[] | null\n userId: string | null\n}\n\n/**\n * Resolved notification context from a request\n */\nexport interface NotificationRequestContext {\n service: NotificationService\n scope: NotificationScope\n ctx: Awaited<ReturnType<typeof resolveRequestContext>>['ctx']\n}\n\nfunction formatZodIssues(error: z.ZodError): string {\n return error.issues\n .map((issue) => {\n const path = issue.path.length ? `${issue.path.join('.')}: ` : ''\n return `${path}${issue.message}`\n })\n .join('; ')\n}\n\nexport async function notificationValidationErrorResponse(error: z.ZodError): Promise<Response> {\n const { t } = await resolveTranslations()\n const prefix = t('api.errors.invalidPayload', 'Invalid request body')\n const details = formatZodIssues(error)\n return Response.json(\n { error: details ? `${prefix}: ${details}` : prefix },\n { status: 400 },\n )\n}\n\nexport type NotificationActionsPayload = { actions?: Array<{ commandId?: string | null }> | null }\n\n/**\n * Reject an API-supplied action that carries a `commandId`.\n *\n * Command-backed actions run with the clicking user's identity, so only server code may attach\n * them, from the notification type definition. Called after payload validation and before the\n * mutation guard or the service write.\n */\nexport async function rejectNotificationActionCommands(\n data: NotificationActionsPayload,\n): Promise<Response | null> {\n if (!data.actions?.some((action) => action.commandId)) return null\n const { t } = await resolveTranslations()\n return Response.json(\n {\n error: t(\n 'notifications.error.actionCommandNotAllowed',\n 'Notification actions created through the API cannot run commands',\n ),\n code: NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE,\n },\n { status: 400 },\n )\n}\n\nexport function notificationCrudErrorResponse(error: unknown): Response | null {\n if (!isCrudHttpError(error)) return null\n return Response.json(error.body ?? { error: 'Notification request failed' }, { status: error.status })\n}\n\n/**\n * Machine-readable discriminator for the tenant-scope rejection, mirroring\n * `ORGANIZATION_SCOPE_REQUIRED_ERROR_CODE`. Without it a client cannot tell an unresolved scope\n * apart from an ordinary permission denial, since both are a 403 carrying only a message.\n */\nexport const TENANT_SCOPE_REQUIRED_ERROR_CODE = 'tenant_scope_required'\n\n/**\n * Fail closed when a request cannot be resolved to a tenant.\n *\n * `resolveNotificationContext` falls back to `''` when neither the organization scope nor the auth\n * context yields a tenant \u2014 reachable for a genuinely tenant-less principal such as an unscoped\n * super-admin API key. `notifications.tenant_id` is a NOT NULL uuid, so that `''` makes the driver\n * reject every read and write built from the scope; dropping the tenant predicate instead would\n * leave `recipientUserId` as the only thing keeping a read inside one tenant.\n *\n * Unlike the audit-log read guard there is no `isSuperAdmin` escape hatch: notification rows are\n * per-recipient and per-tenant, so there is no cross-tenant read mode to preserve.\n *\n * Plain truthiness is deliberate \u2014 it matches the `?? ''` sentinel and also rejects a null or\n * omitted tenant reaching this helper from a caller that builds its own scope.\n */\nexport async function requireResolvedNotificationTenantScope(\n scope: { tenantId?: string | null },\n): Promise<Response | null> {\n if (scope.tenantId) return null\n const { t } = await resolveTranslations()\n return Response.json(\n {\n error: t('api.errors.forbidden', 'Forbidden'),\n code: TENANT_SCOPE_REQUIRED_ERROR_CODE,\n },\n { status: 403 },\n )\n}\n\n/**\n * Resolve notification service and scope from a request.\n * Centralizes the common pattern used across all notification API routes.\n */\nexport async function resolveNotificationContext(req: Request): Promise<NotificationRequestContext> {\n const { ctx } = await resolveRequestContext(req)\n const organizationScope = await resolveOrganizationScopeForRequest({\n container: ctx.container,\n auth: ctx.auth,\n request: req,\n ...(ctx.selectedOrganizationId === undefined\n ? {}\n : { selectedId: ctx.selectedOrganizationId }),\n })\n const tenantId = organizationScope.tenantId ?? ctx.auth?.tenantId ?? ''\n const organizationId = organizationScope.selectedId\n const organizationIds = organizationScope.filterIds\n ctx.organizationScope = organizationScope\n ctx.selectedOrganizationId = organizationId\n ctx.organizationIds = organizationIds\n return {\n service: resolveNotificationService(ctx.container),\n scope: {\n tenantId,\n // The *creator's* org (the shared resolver already falls back to the caller's own org when\n // nothing is selected), and the push strategy later scopes the recipient's device lookup to\n // it. Same-org and self-notify (the common paths) match; a cross-org notify \u2014 an admin in\n // org A notifying a user whose devices live in org B \u2014 won't match the recipient's devices,\n // so push is silently dropped for that case. Same for a deliberate all-organizations\n // selection, which stays tenant-level (org=null) and so only reaches null-org devices.\n // In-app delivery is unaffected. If cross-org push is ever required, scope the device\n // lookup by the recipient's org instead.\n organizationId,\n organizationIds,\n userId: ctx.auth?.sub ?? null,\n },\n ctx,\n }\n}\n\nexport type GuardedNotificationContext =\n | ({ ok: true } & NotificationRequestContext)\n | { ok: false; response: Response }\n\n/**\n * Resolve the notification context and reject the request when it has no tenant.\n *\n * Writes are structurally safe because every one of them funnels through\n * `runGuardedNotificationWrite`. Reads have no such choke point, so they resolve their context\n * through this wrapper instead: a route that forgets to check cannot compile past the discriminated\n * result, which makes the guard impossible to skip by omission rather than by convention.\n */\nexport async function resolveGuardedNotificationContext(req: Request): Promise<GuardedNotificationContext> {\n const resolved = await resolveNotificationContext(req)\n const tenantScopeGuard = await requireResolvedNotificationTenantScope(resolved.scope)\n if (tenantScopeGuard) return { ok: false, response: tenantScopeGuard }\n return { ok: true, ...resolved }\n}\n\n/**\n * Mutation-guard options for a notification write.\n */\nexport interface NotificationMutationGuardOptions {\n resourceKind: string\n resourceId?: string | null\n operation: 'create' | 'update' | 'delete' | 'custom'\n payload?: Record<string, unknown> | null\n}\n\nexport type GuardedNotificationWriteResult<T> =\n | { ok: true; result: T }\n | { ok: false; response: Response }\n\n/**\n * Run a notification write through the mutation guard lifecycle.\n * Validates before the mutation, performs the write, then runs after-success\n * hooks only when the write succeeded and the guard requested them. Returns the\n * guard's own block response when validation fails so authorization behavior and\n * conflict shapes are preserved.\n */\nexport async function runGuardedNotificationWrite<T>(\n container: AwilixContainer,\n scope: NotificationScope,\n req: Request,\n options: NotificationMutationGuardOptions,\n write: () => Promise<T>,\n): Promise<GuardedNotificationWriteResult<T>> {\n const tenantScopeGuard = await requireResolvedNotificationTenantScope(scope)\n if (tenantScopeGuard) {\n return { ok: false, response: tenantScopeGuard }\n }\n\n const guarded = await runRouteMutationGuards({\n container,\n req,\n auth: {\n userId: scope.userId ?? '',\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n },\n input: {\n resourceKind: options.resourceKind,\n resourceId: options.resourceId ?? null,\n operation: options.operation,\n mutationPayload: options.payload ?? null,\n },\n })\n if (!guarded.ok) {\n return { ok: false, response: guarded.response }\n }\n\n const result = await write()\n\n await guarded.runAfterSuccess()\n\n return { ok: true, result }\n}\n\n/**\n * Create a POST handler for bulk notification creation routes.\n * Used by batch, role, and feature notification endpoints.\n */\nexport function createBulkNotificationRoute<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n serviceMethod: 'createBatch' | 'createForRole' | 'createForFeature'\n) {\n return async function POST(req: Request) {\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n const body = await req.json().catch(() => ({}))\n const parsed = schema.safeParse(body)\n if (!parsed.success) {\n return notificationValidationErrorResponse(parsed.error)\n }\n const commandRejection = await rejectNotificationActionCommands(parsed.data as NotificationActionsPayload)\n if (commandRejection) return commandRejection\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n operation: 'create',\n payload: parsed.data as Record<string, unknown>,\n },\n () => service[serviceMethod](parsed.data as never, scope),\n )\n if (!guarded.ok) return guarded.response\n const notifications = guarded.result\n\n return Response.json({\n ok: true,\n count: notifications.length,\n ids: notifications.map((n) => n.id),\n }, { status: 201 })\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n }\n}\n\n/**\n * Create OpenAPI spec for bulk notification creation routes.\n */\nexport function createBulkNotificationOpenApi<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n summary: string,\n description?: string\n) {\n return {\n POST: {\n summary,\n description,\n tags: ['Notifications'],\n requestBody: {\n required: true,\n content: {\n 'application/json': {\n schema,\n },\n },\n },\n responses: {\n 201: {\n description: 'Notifications created',\n content: {\n 'application/json': {\n schema: z.object({\n ok: z.boolean(),\n count: z.number(),\n ids: z.array(z.string().uuid()),\n }),\n },\n },\n },\n 400: {\n description: `Invalid payload, or an action carries a \\`commandId\\` (code: ${NOTIFICATION_ACTION_COMMAND_NOT_ALLOWED_ERROR_CODE})`,\n content: {\n 'application/json': {\n schema: actionConflictResponseSchema,\n },\n },\n },\n },\n },\n }\n}\n\n/**\n * Create a PUT handler for single notification action routes.\n * Used by read and dismiss endpoints.\n */\nexport function createSingleNotificationActionRoute(\n serviceMethod: 'markAsRead' | 'dismiss'\n) {\n return async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {\n const { id } = await params\n const { service, scope, ctx } = await resolveNotificationContext(req)\n\n try {\n const guarded = await runGuardedNotificationWrite(\n ctx.container,\n scope,\n req,\n {\n resourceKind: NOTIFICATION_RESOURCE_KIND,\n resourceId: id,\n operation: 'update',\n },\n () => service[serviceMethod](id, scope),\n )\n if (!guarded.ok) return guarded.response\n } catch (error) {\n const errorResponse = notificationCrudErrorResponse(error)\n if (errorResponse) return errorResponse\n throw error\n }\n\n return Response.json({ ok: true })\n }\n}\n\n/**\n * Create OpenAPI spec for single notification action routes.\n */\nexport function createSingleNotificationActionOpenApi(\n summary: string,\n description: string\n) {\n return {\n PUT: {\n summary,\n tags: ['Notifications'],\n parameters: [\n {\n name: 'id',\n in: 'path',\n required: true,\n schema: { type: 'string', format: 'uuid' },\n },\n ],\n responses: {\n 200: {\n description,\n content: {\n 'application/json': {\n schema: z.object({ ok: z.boolean() }),\n },\n },\n },\n },\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAElB,SAAS,6BAA6B;AACtC,SAAS,0CAA0C;AACnD,SAAS,uBAAuB;AAChC,SAAS,8BAA8B;AACvC,SAAS,2BAA2B;AACpC,SAAS,0DAA0D;AACnE,SAAS,kCAA4D;AACrE,SAAS,oCAAoC;AAKtC,MAAM,6BAA6B;AAKnC,MAAM,sCAAsC;AAK5C,MAAM,wCAAwC;AAqBrD,SAAS,gBAAgB,OAA2B;AAClD,SAAO,MAAM,OACV,IAAI,CAAC,UAAU;AACd,UAAM,OAAO,MAAM,KAAK,SAAS,GAAG,MAAM,KAAK,KAAK,GAAG,CAAC,OAAO;AAC/D,WAAO,GAAG,IAAI,GAAG,MAAM,OAAO;AAAA,EAChC,CAAC,EACA,KAAK,IAAI;AACd;AAEA,eAAsB,oCAAoC,OAAsC;AAC9F,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,QAAM,SAAS,EAAE,6BAA6B,sBAAsB;AACpE,QAAM,UAAU,gBAAgB,KAAK;AACrC,SAAO,SAAS;AAAA,IACd,EAAE,OAAO,UAAU,GAAG,MAAM,KAAK,OAAO,KAAK,OAAO;AAAA,IACpD,EAAE,QAAQ,IAAI;AAAA,EAChB;AACF;AAWA,eAAsB,iCACpB,MAC0B;AAC1B,MAAI,CAAC,KAAK,SAAS,KAAK,CAAC,WAAW,OAAO,SAAS,EAAG,QAAO;AAC9D,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,SAAO,SAAS;AAAA,IACd;AAAA,MACE,OAAO;AAAA,QACL;AAAA,QACA;AAAA,MACF;AAAA,MACA,MAAM;AAAA,IACR;AAAA,IACA,EAAE,QAAQ,IAAI;AAAA,EAChB;AACF;AAEO,SAAS,8BAA8B,OAAiC;AAC7E,MAAI,CAAC,gBAAgB,KAAK,EAAG,QAAO;AACpC,SAAO,SAAS,KAAK,MAAM,QAAQ,EAAE,OAAO,8BAA8B,GAAG,EAAE,QAAQ,MAAM,OAAO,CAAC;AACvG;AAOO,MAAM,mCAAmC;AAiBhD,eAAsB,uCACpB,OAC0B;AAC1B,MAAI,MAAM,SAAU,QAAO;AAC3B,QAAM,EAAE,EAAE,IAAI,MAAM,oBAAoB;AACxC,SAAO,SAAS;AAAA,IACd;AAAA,MACE,OAAO,EAAE,wBAAwB,WAAW;AAAA,MAC5C,MAAM;AAAA,IACR;AAAA,IACA,EAAE,QAAQ,IAAI;AAAA,EAChB;AACF;AAMA,eAAsB,2BAA2B,KAAmD;AAClG,QAAM,EAAE,IAAI,IAAI,MAAM,sBAAsB,GAAG;AAC/C,QAAM,oBAAoB,MAAM,mCAAmC;AAAA,IACjE,WAAW,IAAI;AAAA,IACf,MAAM,IAAI;AAAA,IACV,SAAS;AAAA,IACT,GAAI,IAAI,2BAA2B,SAC/B,CAAC,IACD,EAAE,YAAY,IAAI,uBAAuB;AAAA,EAC/C,CAAC;AACD,QAAM,WAAW,kBAAkB,YAAY,IAAI,MAAM,YAAY;AACrE,QAAM,iBAAiB,kBAAkB;AACzC,QAAM,kBAAkB,kBAAkB;AAC1C,MAAI,oBAAoB;AACxB,MAAI,yBAAyB;AAC7B,MAAI,kBAAkB;AACtB,SAAO;AAAA,IACL,SAAS,2BAA2B,IAAI,SAAS;AAAA,IACjD,OAAO;AAAA,MACL;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASA;AAAA,MACA;AAAA,MACA,QAAQ,IAAI,MAAM,OAAO;AAAA,IAC3B;AAAA,IACA;AAAA,EACF;AACF;AAcA,eAAsB,kCAAkC,KAAmD;AACzG,QAAM,WAAW,MAAM,2BAA2B,GAAG;AACrD,QAAM,mBAAmB,MAAM,uCAAuC,SAAS,KAAK;AACpF,MAAI,iBAAkB,QAAO,EAAE,IAAI,OAAO,UAAU,iBAAiB;AACrE,SAAO,EAAE,IAAI,MAAM,GAAG,SAAS;AACjC;AAuBA,eAAsB,4BACpB,WACA,OACA,KACA,SACA,OAC4C;AAC5C,QAAM,mBAAmB,MAAM,uCAAuC,KAAK;AAC3E,MAAI,kBAAkB;AACpB,WAAO,EAAE,IAAI,OAAO,UAAU,iBAAiB;AAAA,EACjD;AAEA,QAAM,UAAU,MAAM,uBAAuB;AAAA,IAC3C;AAAA,IACA;AAAA,IACA,MAAM;AAAA,MACJ,QAAQ,MAAM,UAAU;AAAA,MACxB,UAAU,MAAM;AAAA,MAChB,gBAAgB,MAAM;AAAA,IACxB;AAAA,IACA,OAAO;AAAA,MACL,cAAc,QAAQ;AAAA,MACtB,YAAY,QAAQ,cAAc;AAAA,MAClC,WAAW,QAAQ;AAAA,MACnB,iBAAiB,QAAQ,WAAW;AAAA,IACtC;AAAA,EACF,CAAC;AACD,MAAI,CAAC,QAAQ,IAAI;AACf,WAAO,EAAE,IAAI,OAAO,UAAU,QAAQ,SAAS;AAAA,EACjD;AAEA,QAAM,SAAS,MAAM,MAAM;AAE3B,QAAM,QAAQ,gBAAgB;AAE9B,SAAO,EAAE,IAAI,MAAM,OAAO;AAC5B;AAMO,SAAS,4BACd,QACA,eACA;AACA,SAAO,eAAe,KAAK,KAAc;AACvC,UAAM,EAAE,SAAS,OAAO,IAAI,IAAI,MAAM,2BAA2B,GAAG;AAEpE,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC9C,UAAM,SAAS,OAAO,UAAU,IAAI;AACpC,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,oCAAoC,OAAO,KAAK;AAAA,IACzD;AACA,UAAM,mBAAmB,MAAM,iCAAiC,OAAO,IAAkC;AACzG,QAAI,iBAAkB,QAAO;AAE7B,QAAI;AACF,YAAM,UAAU,MAAM;AAAA,QACpB,IAAI;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,UACE,cAAc;AAAA,UACd,WAAW;AAAA,UACX,SAAS,OAAO;AAAA,QAClB;AAAA,QACA,MAAM,QAAQ,aAAa,EAAE,OAAO,MAAe,KAAK;AAAA,MAC1D;AACA,UAAI,CAAC,QAAQ,GAAI,QAAO,QAAQ;AAChC,YAAM,gBAAgB,QAAQ;AAE9B,aAAO,SAAS,KAAK;AAAA,QACnB,IAAI;AAAA,QACJ,OAAO,cAAc;AAAA,QACrB,KAAK,cAAc,IAAI,CAAC,MAAM,EAAE,EAAE;AAAA,MACpC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpB,SAAS,OAAO;AACd,YAAM,gBAAgB,8BAA8B,KAAK;AACzD,UAAI,cAAe,QAAO;AAC1B,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAKO,SAAS,8BACd,QACA,SACA,aACA;AACA,SAAO;AAAA,IACL,MAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,MAAM,CAAC,eAAe;AAAA,MACtB,aAAa;AAAA,QACX,UAAU;AAAA,QACV,SAAS;AAAA,UACP,oBAAoB;AAAA,YAClB;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MACA,WAAW;AAAA,QACT,KAAK;AAAA,UACH,aAAa;AAAA,UACb,SAAS;AAAA,YACP,oBAAoB;AAAA,cAClB,QAAQ,EAAE,OAAO;AAAA,gBACf,IAAI,EAAE,QAAQ;AAAA,gBACd,OAAO,EAAE,OAAO;AAAA,gBAChB,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC;AAAA,cAChC,CAAC;AAAA,YACH;AAAA,UACF;AAAA,QACF;AAAA,QACA,KAAK;AAAA,UACH,aAAa,gEAAgE,kDAAkD;AAAA,UAC/H,SAAS;AAAA,YACP,oBAAoB;AAAA,cAClB,QAAQ;AAAA,YACV;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAMO,SAAS,oCACd,eACA;AACA,SAAO,eAAe,IAAI,KAAc,EAAE,OAAO,GAAwC;AACvF,UAAM,EAAE,GAAG,IAAI,MAAM;AACrB,UAAM,EAAE,SAAS,OAAO,IAAI,IAAI,MAAM,2BAA2B,GAAG;AAEpE,QAAI;AACF,YAAM,UAAU,MAAM;AAAA,QACpB,IAAI;AAAA,QACJ;AAAA,QACA;AAAA,QACA;AAAA,UACE,cAAc;AAAA,UACd,YAAY;AAAA,UACZ,WAAW;AAAA,QACb;AAAA,QACA,MAAM,QAAQ,aAAa,EAAE,IAAI,KAAK;AAAA,MACxC;AACA,UAAI,CAAC,QAAQ,GAAI,QAAO,QAAQ;AAAA,IAClC,SAAS,OAAO;AACd,YAAM,gBAAgB,8BAA8B,KAAK;AACzD,UAAI,cAAe,QAAO;AAC1B,YAAM;AAAA,IACR;AAEA,WAAO,SAAS,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EACnC;AACF;AAKO,SAAS,sCACd,SACA,aACA;AACA,SAAO;AAAA,IACL,KAAK;AAAA,MACH;AAAA,MACA,MAAM,CAAC,eAAe;AAAA,MACtB,YAAY;AAAA,QACV;AAAA,UACE,MAAM;AAAA,UACN,IAAI;AAAA,UACJ,UAAU;AAAA,UACV,QAAQ,EAAE,MAAM,UAAU,QAAQ,OAAO;AAAA,QAC3C;AAAA,MACF;AAAA,MACA,WAAW;AAAA,QACT,KAAK;AAAA,UACH;AAAA,UACA,SAAS;AAAA,YACP,oBAAoB;AAAA,cAClB,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;AAAA,YACtC;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { QueryOrder } from "@mikro-orm/core";
|
|
2
|
+
import { LockMode, QueryOrder } from "@mikro-orm/core";
|
|
3
3
|
import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
4
4
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
5
5
|
import {
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "@open-mercato/shared/modules/payment_gateways/types";
|
|
8
8
|
import { conflict, CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
|
|
9
9
|
import { GatewaySessionInitialization, GatewayTransaction } from "../data/entities.js";
|
|
10
|
-
import { canApplyManualAction, isValidTransition } from "./status-machine.js";
|
|
10
|
+
import { canApplyManualAction, isValidTransition, resolveImpliedCaptureStatus } from "./status-machine.js";
|
|
11
11
|
import { emitPaymentGatewayEvent } from "../events.js";
|
|
12
12
|
import { readGatewayMetadata, readWebhookLog } from "./transaction-fields.js";
|
|
13
13
|
import { reconcileSessionAmountWithOrder } from "./order-amount-reconciliation.js";
|
|
@@ -62,7 +62,7 @@ function createPaymentGatewayService(deps) {
|
|
|
62
62
|
deps.sessionClaimOptions?.heartbeatIntervalMs ?? Math.floor(claimStaleAfterMs / 3)
|
|
63
63
|
);
|
|
64
64
|
const claimPollIntervalMs = Math.max(1, deps.sessionClaimOptions?.pollIntervalMs ?? PAYMENT_SESSION_WAIT_INTERVAL_MS);
|
|
65
|
-
async function findTransactionOrThrow(transactionId, scope, targetEm = em) {
|
|
65
|
+
async function findTransactionOrThrow(transactionId, scope, targetEm = em, options) {
|
|
66
66
|
const transaction = await findOneWithDecryption(
|
|
67
67
|
targetEm,
|
|
68
68
|
GatewayTransaction,
|
|
@@ -72,7 +72,7 @@ function createPaymentGatewayService(deps) {
|
|
|
72
72
|
tenantId: scope.tenantId,
|
|
73
73
|
deletedAt: null
|
|
74
74
|
},
|
|
75
|
-
|
|
75
|
+
options,
|
|
76
76
|
scope
|
|
77
77
|
);
|
|
78
78
|
if (!transaction) {
|
|
@@ -585,42 +585,57 @@ function createPaymentGatewayService(deps) {
|
|
|
585
585
|
return status;
|
|
586
586
|
},
|
|
587
587
|
async syncTransactionStatus(transactionId, update, scope) {
|
|
588
|
-
const transaction = await
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
588
|
+
const { transaction, previousStatus, impliedCaptureStatus, shouldApplyStatus } = await em.fork().transactional(async (tx) => {
|
|
589
|
+
const locked = await findTransactionOrThrow(transactionId, scope, tx, { lockMode: LockMode.PESSIMISTIC_WRITE });
|
|
590
|
+
const currentStatus = locked.unifiedStatus;
|
|
591
|
+
const implied = resolveImpliedCaptureStatus(currentStatus, update.unifiedStatus);
|
|
592
|
+
const canTransition = isValidTransition(currentStatus, update.unifiedStatus) || implied !== null;
|
|
593
|
+
const applies = canTransition && update.unifiedStatus !== currentStatus;
|
|
594
|
+
if (applies) {
|
|
595
|
+
if (implied) alignCapturedAmountWithStatus(locked, implied);
|
|
596
|
+
locked.unifiedStatus = update.unifiedStatus;
|
|
597
|
+
alignCapturedAmountWithStatus(locked, update.unifiedStatus);
|
|
598
|
+
}
|
|
599
|
+
if (update.providerStatus) {
|
|
600
|
+
locked.gatewayStatus = update.providerStatus;
|
|
601
|
+
}
|
|
602
|
+
if (update.providerData) {
|
|
603
|
+
locked.gatewayMetadata = { ...readGatewayMetadata(locked.gatewayMetadata), ...update.providerData };
|
|
604
|
+
}
|
|
605
|
+
if (update.webhookEvent) {
|
|
606
|
+
const webhookLog = readWebhookLog(locked.webhookLog);
|
|
607
|
+
webhookLog.push({
|
|
608
|
+
eventType: update.webhookEvent.eventType,
|
|
609
|
+
receivedAt: update.webhookEvent.receivedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
610
|
+
idempotencyKey: update.webhookEvent.idempotencyKey,
|
|
611
|
+
unifiedStatus: update.unifiedStatus,
|
|
612
|
+
processed: update.webhookEvent.processed
|
|
613
|
+
});
|
|
614
|
+
locked.webhookLog = webhookLog;
|
|
615
|
+
}
|
|
616
|
+
locked.lastWebhookAt = /* @__PURE__ */ new Date();
|
|
617
|
+
await tx.flush();
|
|
618
|
+
return {
|
|
619
|
+
transaction: locked,
|
|
620
|
+
previousStatus: currentStatus,
|
|
621
|
+
impliedCaptureStatus: applies ? implied : null,
|
|
622
|
+
shouldApplyStatus: applies
|
|
623
|
+
};
|
|
624
|
+
});
|
|
616
625
|
if (shouldApplyStatus) {
|
|
617
|
-
|
|
626
|
+
const eventPayload = {
|
|
618
627
|
transactionId: transaction.id,
|
|
619
628
|
paymentId: transaction.paymentId,
|
|
620
629
|
providerKey: transaction.providerKey,
|
|
621
|
-
previousStatus,
|
|
622
630
|
organizationId: transaction.organizationId,
|
|
623
631
|
tenantId: transaction.tenantId
|
|
632
|
+
};
|
|
633
|
+
if (impliedCaptureStatus) {
|
|
634
|
+
await emitStatusEvent(impliedCaptureStatus, { ...eventPayload, previousStatus });
|
|
635
|
+
}
|
|
636
|
+
await emitStatusEvent(update.unifiedStatus, {
|
|
637
|
+
...eventPayload,
|
|
638
|
+
previousStatus: impliedCaptureStatus ?? previousStatus
|
|
624
639
|
});
|
|
625
640
|
}
|
|
626
641
|
await writeTransactionLog(
|
|
@@ -631,6 +646,7 @@ function createPaymentGatewayService(deps) {
|
|
|
631
646
|
shouldApplyStatus ? "Payment status synchronized from webhook" : "Webhook received with no status transition",
|
|
632
647
|
{
|
|
633
648
|
previousStatus,
|
|
649
|
+
...impliedCaptureStatus ? { impliedStatus: impliedCaptureStatus } : {},
|
|
634
650
|
nextStatus: update.unifiedStatus,
|
|
635
651
|
providerStatus: update.providerStatus ?? null,
|
|
636
652
|
eventType: update.webhookEvent?.eventType ?? null,
|