@open-mercato/core 0.10.1-develop.7679.1.cb2c13f49e → 0.10.1-develop.7687.1.e1a6f9de51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/users/route.js +1 -1
  3. package/dist/modules/auth/api/users/route.js.map +2 -2
  4. package/dist/modules/customer_accounts/api/magic-link/request.js +15 -1
  5. package/dist/modules/customer_accounts/api/magic-link/request.js.map +2 -2
  6. package/dist/modules/customer_accounts/api/password/reset-request.js +15 -1
  7. package/dist/modules/customer_accounts/api/password/reset-request.js.map +2 -2
  8. package/dist/modules/customer_accounts/emails/CustomerAuthLinkEmail.js +26 -0
  9. package/dist/modules/customer_accounts/emails/CustomerAuthLinkEmail.js.map +7 -0
  10. package/dist/modules/customer_accounts/lib/authLinkEmails.js +67 -0
  11. package/dist/modules/customer_accounts/lib/authLinkEmails.js.map +7 -0
  12. package/dist/modules/messages/commands/actions.js +8 -1
  13. package/dist/modules/messages/commands/actions.js.map +2 -2
  14. package/dist/modules/payment_gateways/data/entities.js +4 -0
  15. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  16. package/dist/modules/payment_gateways/lib/session-idempotency.js +29 -5
  17. package/dist/modules/payment_gateways/lib/session-idempotency.js.map +2 -2
  18. package/dist/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.js +19 -0
  19. package/dist/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.js.map +7 -0
  20. package/dist/modules/payment_gateways/workers/session-initialization-prune.js +3 -3
  21. package/dist/modules/payment_gateways/workers/session-initialization-prune.js.map +2 -2
  22. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.js +59 -0
  23. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.js.map +7 -0
  24. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.js +11 -0
  25. package/dist/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.js.map +7 -0
  26. package/dist/modules/query_index/lib/engine.js +60 -13
  27. package/dist/modules/query_index/lib/engine.js.map +2 -2
  28. package/package.json +7 -7
  29. package/src/modules/auth/api/users/route.ts +1 -1
  30. package/src/modules/customer_accounts/api/magic-link/request.ts +16 -3
  31. package/src/modules/customer_accounts/api/password/reset-request.ts +16 -3
  32. package/src/modules/customer_accounts/emails/CustomerAuthLinkEmail.tsx +45 -0
  33. package/src/modules/customer_accounts/i18n/de.json +12 -0
  34. package/src/modules/customer_accounts/i18n/en.json +12 -0
  35. package/src/modules/customer_accounts/i18n/es.json +12 -0
  36. package/src/modules/customer_accounts/i18n/ko.json +12 -0
  37. package/src/modules/customer_accounts/i18n/pl.json +12 -0
  38. package/src/modules/customer_accounts/lib/authLinkEmails.ts +77 -0
  39. package/src/modules/messages/commands/actions.ts +8 -1
  40. package/src/modules/payment_gateways/data/entities.ts +4 -0
  41. package/src/modules/payment_gateways/lib/session-idempotency.ts +41 -5
  42. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +14 -1
  43. package/src/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.ts +18 -0
  44. package/src/modules/payment_gateways/workers/session-initialization-prune.ts +3 -3
  45. package/src/modules/portal/frontend/[orgSlug]/portal/magic-link/page.meta.ts +9 -0
  46. package/src/modules/portal/frontend/[orgSlug]/portal/magic-link/page.tsx +71 -0
  47. package/src/modules/portal/i18n/de.json +7 -0
  48. package/src/modules/portal/i18n/en.json +7 -0
  49. package/src/modules/portal/i18n/es.json +7 -0
  50. package/src/modules/portal/i18n/ko.json +7 -0
  51. package/src/modules/portal/i18n/pl.json +7 -0
  52. package/src/modules/query_index/lib/engine.ts +70 -12
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/messages/commands/actions.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Message, MessageObject, MessageRecipient } from '../data/entities'\nimport { emitMessagesEvent } from '../events'\nimport { MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND } from '../lib/constants'\nimport {\n findResolvedMessageActionById,\n isMessageSafeCommandId,\n isTerminalMessageAction,\n resolveActionCommandInput,\n resolveActionHref,\n resolveMessageActionData,\n} from '../lib/actions'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { assertOrganizationAccess, type MessageScopeInput } from './shared'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'actions' })\n\nconst actionStateSnapshotSchema = z.object({\n actionTaken: z.string().nullable(),\n actionTakenByUserId: z.string().nullable(),\n actionTakenAt: z.string().nullable(),\n actionResult: z.record(z.string(), z.unknown()).nullable(),\n})\n\nconst recordTerminalActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n result: z.record(z.string(), z.unknown()),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n // Optional pre-claim snapshot supplied by `messages.actions.execute` so the\n // undo log records the true (un-taken) state even though the terminal action\n // was atomically claimed before this finalizer runs.\n previousState: actionStateSnapshotSchema.optional(),\n})\n\ntype RecordTerminalActionInput = z.infer<typeof recordTerminalActionSchema>\n\nconst executeActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).optional(),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n})\n\ntype ExecuteActionInput = z.infer<typeof executeActionSchema>\n\ntype ActionStateSnapshot = {\n actionTaken: string | null\n actionTakenByUserId: string | null\n actionTakenAt: string | null\n actionResult: Record<string, unknown> | null\n}\n\nfunction toIso(value: Date | null | undefined): string | null {\n return value ? value.toISOString() : null\n}\n\nfunction toDate(value: string | null | undefined): Date | null {\n if (!value) return null\n return new Date(value)\n}\n\nasync function requireActionTarget(em: EntityManager, input: RecordTerminalActionInput) {\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: input.messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n if (!message) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, message)\n\n const recipient = await em.findOne(MessageRecipient, {\n messageId: input.messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (!recipient) throw new Error('Access denied')\n return message\n}\n\nasync function requireActionMessage(em: EntityManager, input: ExecuteActionInput) {\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: input.messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n if (!message) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, message)\n const recipient = await em.findOne(MessageRecipient, {\n messageId: input.messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (!recipient) throw new Error('Access denied')\n return message\n}\n\nfunction snapshotActionState(message: Message): ActionStateSnapshot {\n return {\n actionTaken: message.actionTaken ?? null,\n actionTakenByUserId: message.actionTakenByUserId ?? null,\n actionTakenAt: toIso(message.actionTakenAt),\n actionResult: message.actionResult ?? null,\n }\n}\n\nconst recordTerminalActionCommand: CommandHandler<unknown, { ok: true }> = {\n id: 'messages.actions.record_terminal',\n async prepare(rawInput, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n return { before: snapshotActionState(message) }\n },\n async execute(rawInput, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n const claimedByCaller =\n message.actionTaken === input.actionId && message.actionTakenByUserId === input.userId\n if (!claimedByCaller) {\n if (message.actionTaken) {\n throw new Error('Action already taken')\n }\n // Atomic compare-and-set: only one concurrent request transitions the\n // message out of the un-taken state. nativeUpdate runs as a single\n // `UPDATE ... WHERE action_taken IS NULL` so the loser matches 0 rows.\n const claimedRows = await em.nativeUpdate(\n Message,\n { id: input.messageId, tenantId: input.tenantId, actionTaken: null, deletedAt: null },\n {\n actionTaken: input.actionId,\n actionTakenByUserId: input.userId,\n actionTakenAt: new Date(),\n },\n )\n if (claimedRows === 0) {\n throw new Error('Action already taken')\n }\n message.actionTaken = input.actionId\n message.actionTakenByUserId = input.userId\n }\n if (!message.actionTakenAt) {\n message.actionTakenAt = new Date()\n }\n // action_result is an encrypted column, so it must be written through the\n // flush path (the encryption subscriber) rather than nativeUpdate.\n message.actionResult = input.result\n await em.flush()\n return { ok: true }\n },\n async captureAfter(rawInput, _result, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n return snapshotActionState(message)\n },\n buildLog: async ({ input, snapshots }) => {\n const parsed = recordTerminalActionSchema.parse(input)\n // When the action was pre-claimed by `messages.actions.execute`, the\n // prepare-captured snapshot already reflects the claimed state, so prefer\n // the explicit pre-claim snapshot for the undo baseline.\n const before =\n (parsed.previousState as ActionStateSnapshot | undefined) ??\n (snapshots.before as ActionStateSnapshot | undefined) ??\n null\n return {\n actionLabel: 'Execute message terminal action',\n resourceKind: 'messages.message',\n resourceId: parsed.messageId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n payload: {\n undo: {\n before,\n after: (snapshots.after as ActionStateSnapshot | undefined) ?? null,\n } satisfies UndoPayload<ActionStateSnapshot>,\n },\n snapshotBefore: before,\n snapshotAfter: snapshots.after ?? null,\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<UndoPayload<ActionStateSnapshot>>(logEntry)\n const before = undo?.before\n if (!before) return\n const messageId = logEntry?.resourceId as string | null\n if (!messageId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await findOneWithDecryption(em, Message, { id: messageId })\n if (!message) return\n message.actionTaken = before.actionTaken\n message.actionTakenByUserId = before.actionTakenByUserId\n message.actionTakenAt = toDate(before.actionTakenAt)\n message.actionResult = before.actionResult\n await em.flush()\n },\n}\n\nconst executeActionCommand: CommandHandler<\n unknown,\n { ok: true; actionId: string; result: Record<string, unknown>; operationLogEntry: unknown | null }\n> = {\n id: 'messages.actions.execute',\n async execute(rawInput, ctx) {\n const input = executeActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionMessage(em, input)\n\n // Reject a stale action: if the message changed since the actor's tab loaded\n // it (and the client sent the expected version), fail with the structured 409\n // conflict instead of acting on an out-of-date aggregate. Strictly additive \u2014\n // a no-op for callers that don't send the optimistic-lock header.\n enforceCommandOptimisticLock({\n resourceKind: MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND,\n resourceId: message.id,\n current: message.updatedAt,\n request: ctx.request ?? null,\n })\n\n const objects = await em.find(MessageObject, { messageId: message.id })\n const action = findResolvedMessageActionById(message, objects, input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n // Confused-deputy guard: a composer-controlled action must not dispatch an\n // arbitrary registered command with the recipient's auth. Only command ids\n // declared by code-side message types / object types are dispatchable.\n if (action.commandId && !isMessageSafeCommandId(action.commandId)) {\n throw new Error('Action command is not allowed')\n }\n\n if (message.actionTaken) {\n throw Object.assign(new Error('Action already taken'), { actionTaken: message.actionTaken })\n }\n\n const shouldRecordActionTaken = isTerminalMessageAction(action)\n\n const actionData = resolveMessageActionData(message)\n if (actionData?.expiresAt) {\n if (new Date(actionData.expiresAt) < new Date()) {\n throw new Error('Actions have expired')\n }\n } else {\n const messageType = getMessageType(message.type)\n if (messageType?.actionsExpireAfterHours && message.sentAt) {\n const expiry = new Date(message.sentAt.getTime() + messageType.actionsExpireAfterHours * 60 * 60 * 1000)\n if (expiry < new Date()) {\n throw new Error('Actions have expired')\n }\n }\n }\n\n // Capture the un-taken state for the undo log before reserving the action.\n const previousState = snapshotActionState(message)\n let claimedTerminal = false\n const releaseTerminalClaim = async () => {\n if (!claimedTerminal) return\n claimedTerminal = false\n await em.nativeUpdate(\n Message,\n {\n id: message.id,\n tenantId: input.tenantId,\n actionTaken: action.id,\n actionTakenByUserId: input.userId,\n },\n { actionTaken: null, actionTakenByUserId: null, actionTakenAt: null },\n )\n }\n\n if (shouldRecordActionTaken) {\n // Atomically reserve the terminal action BEFORE running the target\n // command so concurrent requests cannot both execute it. The losing\n // request matches 0 rows and surfaces the existing 409 response.\n const claimedRows = await em.nativeUpdate(\n Message,\n { id: message.id, tenantId: input.tenantId, actionTaken: null, deletedAt: null },\n {\n actionTaken: action.id,\n actionTakenByUserId: input.userId,\n actionTakenAt: new Date(),\n },\n )\n if (claimedRows === 0) {\n const current = await findOneWithDecryption(\n em,\n Message,\n { id: message.id, tenantId: input.tenantId, deletedAt: null },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n throw Object.assign(new Error('Action already taken'), {\n actionTaken: current?.actionTaken ?? message.actionTaken ?? action.id,\n })\n }\n claimedTerminal = true\n }\n\n const commandBus = ctx.container.resolve('commandBus') as {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown; logEntry?: unknown }>\n }\n\n let result: Record<string, unknown> = {}\n let operationLogEntry: unknown | null = null\n\n if (action.commandId) {\n try {\n const actionInput = resolveActionCommandInput(\n action,\n message,\n {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n },\n input.payload ?? {},\n )\n\n if (actionInput.id == null) {\n const fallbackId = action.objectRef?.entityId ?? message.sourceEntityId ?? null\n if (typeof fallbackId === 'string' && fallbackId.trim().length > 0) {\n actionInput.id = fallbackId\n }\n }\n\n const commandResult = await commandBus.execute(action.commandId, {\n input: actionInput,\n ctx: {\n container: ctx.container,\n auth: {\n sub: input.userId,\n tenantId: input.tenantId,\n orgId: input.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: input.organizationId,\n organizationIds: input.organizationId ? [input.organizationId] : null,\n },\n metadata: {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n resourceKind: 'messages',\n },\n })\n\n result = (commandResult.result as Record<string, unknown>) ?? {}\n operationLogEntry = commandResult.logEntry ?? null\n } catch (err) {\n logger.error('executeActionCommand sub-command failed', { err })\n // The target command never completed \u2014 release the reservation so the\n // action stays retryable, matching the pre-claim failure semantics.\n await releaseTerminalClaim()\n throw new Error('Action failed')\n }\n } else if (action.href) {\n const safeRedirect = resolveActionHref(action, message, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n })\n if (!safeRedirect) {\n await releaseTerminalClaim()\n throw new Error('Action has an unsafe redirect target')\n }\n result = { redirect: safeRedirect }\n } else {\n await releaseTerminalClaim()\n throw new Error('Action has no executable target')\n }\n\n if (shouldRecordActionTaken) {\n const terminalResult = await commandBus.execute('messages.actions.record_terminal', {\n input: {\n messageId: message.id,\n actionId: action.id,\n result,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n previousState,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: input.organizationId,\n organizationIds: input.organizationId ? [input.organizationId] : null,\n },\n })\n if (!operationLogEntry) {\n operationLogEntry = terminalResult.logEntry ?? null\n }\n await emitMessagesEvent(\n 'messages.message.action_taken',\n {\n messageId: message.id,\n actionId: action.id,\n userId: input.userId,\n recipientUserId: input.userId,\n result,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n },\n { persistent: true }\n )\n }\n\n return {\n ok: true,\n actionId: action.id,\n result,\n operationLogEntry,\n }\n },\n}\n\nregisterCommand(recordTerminalActionCommand)\nregisterCommand(executeActionCommand)\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { registerCommand, type CommandHandler } from '@open-mercato/shared/lib/commands'\nimport { extractUndoPayload, type UndoPayload } from '@open-mercato/shared/lib/commands/undo'\nimport { enforceCommandOptimisticLock } from '@open-mercato/shared/lib/crud/optimistic-lock-command'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { getCommandInterceptorHttpRejection } from '@open-mercato/shared/lib/commands/errors'\nimport { getTelemetryRuntime } from '@open-mercato/shared/lib/telemetry/runtime'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Message, MessageObject, MessageRecipient } from '../data/entities'\nimport { emitMessagesEvent } from '../events'\nimport { MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND } from '../lib/constants'\nimport {\n findResolvedMessageActionById,\n isMessageSafeCommandId,\n isTerminalMessageAction,\n resolveActionCommandInput,\n resolveActionHref,\n resolveMessageActionData,\n} from '../lib/actions'\nimport { getMessageType } from '../lib/message-types-registry'\nimport { assertOrganizationAccess, type MessageScopeInput } from './shared'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('messages').child({ component: 'actions' })\n\nconst actionStateSnapshotSchema = z.object({\n actionTaken: z.string().nullable(),\n actionTakenByUserId: z.string().nullable(),\n actionTakenAt: z.string().nullable(),\n actionResult: z.record(z.string(), z.unknown()).nullable(),\n})\n\nconst recordTerminalActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n result: z.record(z.string(), z.unknown()),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n // Optional pre-claim snapshot supplied by `messages.actions.execute` so the\n // undo log records the true (un-taken) state even though the terminal action\n // was atomically claimed before this finalizer runs.\n previousState: actionStateSnapshotSchema.optional(),\n})\n\ntype RecordTerminalActionInput = z.infer<typeof recordTerminalActionSchema>\n\nconst executeActionSchema = z.object({\n messageId: z.string().uuid(),\n actionId: z.string().min(1),\n payload: z.record(z.string(), z.unknown()).optional(),\n tenantId: z.string().uuid(),\n organizationId: z.string().uuid().nullable(),\n userId: z.string().uuid(),\n})\n\ntype ExecuteActionInput = z.infer<typeof executeActionSchema>\n\ntype ActionStateSnapshot = {\n actionTaken: string | null\n actionTakenByUserId: string | null\n actionTakenAt: string | null\n actionResult: Record<string, unknown> | null\n}\n\nfunction toIso(value: Date | null | undefined): string | null {\n return value ? value.toISOString() : null\n}\n\nfunction toDate(value: string | null | undefined): Date | null {\n if (!value) return null\n return new Date(value)\n}\n\nasync function requireActionTarget(em: EntityManager, input: RecordTerminalActionInput) {\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: input.messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n if (!message) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, message)\n\n const recipient = await em.findOne(MessageRecipient, {\n messageId: input.messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (!recipient) throw new Error('Access denied')\n return message\n}\n\nasync function requireActionMessage(em: EntityManager, input: ExecuteActionInput) {\n const message = await findOneWithDecryption(\n em,\n Message,\n {\n id: input.messageId,\n tenantId: input.tenantId,\n deletedAt: null,\n },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n if (!message) throw new Error('Message not found')\n assertOrganizationAccess(input as MessageScopeInput, message)\n const recipient = await em.findOne(MessageRecipient, {\n messageId: input.messageId,\n recipientUserId: input.userId,\n deletedAt: null,\n })\n if (!recipient) throw new Error('Access denied')\n return message\n}\n\nfunction snapshotActionState(message: Message): ActionStateSnapshot {\n return {\n actionTaken: message.actionTaken ?? null,\n actionTakenByUserId: message.actionTakenByUserId ?? null,\n actionTakenAt: toIso(message.actionTakenAt),\n actionResult: message.actionResult ?? null,\n }\n}\n\nconst recordTerminalActionCommand: CommandHandler<unknown, { ok: true }> = {\n id: 'messages.actions.record_terminal',\n async prepare(rawInput, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n return { before: snapshotActionState(message) }\n },\n async execute(rawInput, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n const claimedByCaller =\n message.actionTaken === input.actionId && message.actionTakenByUserId === input.userId\n if (!claimedByCaller) {\n if (message.actionTaken) {\n throw new Error('Action already taken')\n }\n // Atomic compare-and-set: only one concurrent request transitions the\n // message out of the un-taken state. nativeUpdate runs as a single\n // `UPDATE ... WHERE action_taken IS NULL` so the loser matches 0 rows.\n const claimedRows = await em.nativeUpdate(\n Message,\n { id: input.messageId, tenantId: input.tenantId, actionTaken: null, deletedAt: null },\n {\n actionTaken: input.actionId,\n actionTakenByUserId: input.userId,\n actionTakenAt: new Date(),\n },\n )\n if (claimedRows === 0) {\n throw new Error('Action already taken')\n }\n message.actionTaken = input.actionId\n message.actionTakenByUserId = input.userId\n }\n if (!message.actionTakenAt) {\n message.actionTakenAt = new Date()\n }\n // action_result is an encrypted column, so it must be written through the\n // flush path (the encryption subscriber) rather than nativeUpdate.\n message.actionResult = input.result\n await em.flush()\n return { ok: true }\n },\n async captureAfter(rawInput, _result, ctx) {\n const input = recordTerminalActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionTarget(em, input)\n return snapshotActionState(message)\n },\n buildLog: async ({ input, snapshots }) => {\n const parsed = recordTerminalActionSchema.parse(input)\n // When the action was pre-claimed by `messages.actions.execute`, the\n // prepare-captured snapshot already reflects the claimed state, so prefer\n // the explicit pre-claim snapshot for the undo baseline.\n const before =\n (parsed.previousState as ActionStateSnapshot | undefined) ??\n (snapshots.before as ActionStateSnapshot | undefined) ??\n null\n return {\n actionLabel: 'Execute message terminal action',\n resourceKind: 'messages.message',\n resourceId: parsed.messageId,\n tenantId: parsed.tenantId,\n organizationId: parsed.organizationId,\n payload: {\n undo: {\n before,\n after: (snapshots.after as ActionStateSnapshot | undefined) ?? null,\n } satisfies UndoPayload<ActionStateSnapshot>,\n },\n snapshotBefore: before,\n snapshotAfter: snapshots.after ?? null,\n }\n },\n undo: async ({ logEntry, ctx }) => {\n const undo = extractUndoPayload<UndoPayload<ActionStateSnapshot>>(logEntry)\n const before = undo?.before\n if (!before) return\n const messageId = logEntry?.resourceId as string | null\n if (!messageId) return\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await findOneWithDecryption(em, Message, { id: messageId })\n if (!message) return\n message.actionTaken = before.actionTaken\n message.actionTakenByUserId = before.actionTakenByUserId\n message.actionTakenAt = toDate(before.actionTakenAt)\n message.actionResult = before.actionResult\n await em.flush()\n },\n}\n\nconst executeActionCommand: CommandHandler<\n unknown,\n { ok: true; actionId: string; result: Record<string, unknown>; operationLogEntry: unknown | null }\n> = {\n id: 'messages.actions.execute',\n async execute(rawInput, ctx) {\n const input = executeActionSchema.parse(rawInput)\n const em = (ctx.container.resolve('em') as EntityManager).fork()\n const message = await requireActionMessage(em, input)\n\n // Reject a stale action: if the message changed since the actor's tab loaded\n // it (and the client sent the expected version), fail with the structured 409\n // conflict instead of acting on an out-of-date aggregate. Strictly additive \u2014\n // a no-op for callers that don't send the optimistic-lock header.\n enforceCommandOptimisticLock({\n resourceKind: MESSAGE_OPTIMISTIC_LOCK_RESOURCE_KIND,\n resourceId: message.id,\n current: message.updatedAt,\n request: ctx.request ?? null,\n })\n\n const objects = await em.find(MessageObject, { messageId: message.id })\n const action = findResolvedMessageActionById(message, objects, input.actionId)\n\n if (!action) {\n throw new Error('Action not found')\n }\n\n // Confused-deputy guard: a composer-controlled action must not dispatch an\n // arbitrary registered command with the recipient's auth. Only command ids\n // declared by code-side message types / object types are dispatchable.\n if (action.commandId && !isMessageSafeCommandId(action.commandId)) {\n throw new Error('Action command is not allowed')\n }\n\n if (message.actionTaken) {\n throw Object.assign(new Error('Action already taken'), { actionTaken: message.actionTaken })\n }\n\n const shouldRecordActionTaken = isTerminalMessageAction(action)\n\n const actionData = resolveMessageActionData(message)\n if (actionData?.expiresAt) {\n if (new Date(actionData.expiresAt) < new Date()) {\n throw new Error('Actions have expired')\n }\n } else {\n const messageType = getMessageType(message.type)\n if (messageType?.actionsExpireAfterHours && message.sentAt) {\n const expiry = new Date(message.sentAt.getTime() + messageType.actionsExpireAfterHours * 60 * 60 * 1000)\n if (expiry < new Date()) {\n throw new Error('Actions have expired')\n }\n }\n }\n\n // Capture the un-taken state for the undo log before reserving the action.\n const previousState = snapshotActionState(message)\n let claimedTerminal = false\n const releaseTerminalClaim = async () => {\n if (!claimedTerminal) return\n claimedTerminal = false\n await em.nativeUpdate(\n Message,\n {\n id: message.id,\n tenantId: input.tenantId,\n actionTaken: action.id,\n actionTakenByUserId: input.userId,\n },\n { actionTaken: null, actionTakenByUserId: null, actionTakenAt: null },\n )\n }\n\n if (shouldRecordActionTaken) {\n // Atomically reserve the terminal action BEFORE running the target\n // command so concurrent requests cannot both execute it. The losing\n // request matches 0 rows and surfaces the existing 409 response.\n const claimedRows = await em.nativeUpdate(\n Message,\n { id: message.id, tenantId: input.tenantId, actionTaken: null, deletedAt: null },\n {\n actionTaken: action.id,\n actionTakenByUserId: input.userId,\n actionTakenAt: new Date(),\n },\n )\n if (claimedRows === 0) {\n const current = await findOneWithDecryption(\n em,\n Message,\n { id: message.id, tenantId: input.tenantId, deletedAt: null },\n undefined,\n { tenantId: input.tenantId, organizationId: input.organizationId },\n )\n throw Object.assign(new Error('Action already taken'), {\n actionTaken: current?.actionTaken ?? message.actionTaken ?? action.id,\n })\n }\n claimedTerminal = true\n }\n\n const commandBus = ctx.container.resolve('commandBus') as {\n execute: (\n commandId: string,\n options: { input: unknown; ctx: unknown; metadata?: unknown }\n ) => Promise<{ result: unknown; logEntry?: unknown }>\n }\n\n let result: Record<string, unknown> = {}\n let operationLogEntry: unknown | null = null\n\n if (action.commandId) {\n try {\n const actionInput = resolveActionCommandInput(\n action,\n message,\n {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n },\n input.payload ?? {},\n )\n\n if (actionInput.id == null) {\n const fallbackId = action.objectRef?.entityId ?? message.sourceEntityId ?? null\n if (typeof fallbackId === 'string' && fallbackId.trim().length > 0) {\n actionInput.id = fallbackId\n }\n }\n\n const commandResult = await commandBus.execute(action.commandId, {\n input: actionInput,\n ctx: {\n container: ctx.container,\n auth: {\n sub: input.userId,\n tenantId: input.tenantId,\n orgId: input.organizationId,\n },\n organizationScope: null,\n selectedOrganizationId: input.organizationId,\n organizationIds: input.organizationId ? [input.organizationId] : null,\n },\n metadata: {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n resourceKind: 'messages',\n },\n })\n\n result = (commandResult.result as Record<string, unknown>) ?? {}\n operationLogEntry = commandResult.logEntry ?? null\n } catch (err) {\n // The target command never completed \u2014 release the reservation so the\n // action stays retryable, matching the pre-claim failure semantics.\n await releaseTerminalClaim()\n if (isCrudHttpError(err) || getCommandInterceptorHttpRejection(err)) {\n throw err\n }\n logger.error('executeActionCommand sub-command failed', { err })\n getTelemetryRuntime()?.reportError(err, { module: 'messages', code: 'messages.action_failed' })\n throw new Error('Action failed')\n }\n } else if (action.href) {\n const safeRedirect = resolveActionHref(action, message, {\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n })\n if (!safeRedirect) {\n await releaseTerminalClaim()\n throw new Error('Action has an unsafe redirect target')\n }\n result = { redirect: safeRedirect }\n } else {\n await releaseTerminalClaim()\n throw new Error('Action has no executable target')\n }\n\n if (shouldRecordActionTaken) {\n const terminalResult = await commandBus.execute('messages.actions.record_terminal', {\n input: {\n messageId: message.id,\n actionId: action.id,\n result,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n userId: input.userId,\n previousState,\n },\n ctx: {\n container: ctx.container,\n auth: ctx.auth ?? null,\n organizationScope: null,\n selectedOrganizationId: input.organizationId,\n organizationIds: input.organizationId ? [input.organizationId] : null,\n },\n })\n if (!operationLogEntry) {\n operationLogEntry = terminalResult.logEntry ?? null\n }\n await emitMessagesEvent(\n 'messages.message.action_taken',\n {\n messageId: message.id,\n actionId: action.id,\n userId: input.userId,\n recipientUserId: input.userId,\n result,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n },\n { persistent: true }\n )\n }\n\n return {\n ok: true,\n actionId: action.id,\n result,\n operationLogEntry,\n }\n },\n}\n\nregisterCommand(recordTerminalActionCommand)\nregisterCommand(executeActionCommand)\n"],
5
+ "mappings": "AACA,SAAS,SAAS;AAClB,SAAS,uBAA4C;AACrD,SAAS,0BAA4C;AACrD,SAAS,oCAAoC;AAC7C,SAAS,uBAAuB;AAChC,SAAS,0CAA0C;AACnD,SAAS,2BAA2B;AACpC,SAAS,6BAA6B;AACtC,SAAS,SAAS,eAAe,wBAAwB;AACzD,SAAS,yBAAyB;AAClC,SAAS,6CAA6C;AACtD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,sBAAsB;AAC/B,SAAS,gCAAwD;AACjE,SAAS,oBAAoB;AAE7B,MAAM,SAAS,aAAa,UAAU,EAAE,MAAM,EAAE,WAAW,UAAU,CAAC;AAEtE,MAAM,4BAA4B,EAAE,OAAO;AAAA,EACzC,aAAa,EAAE,OAAO,EAAE,SAAS;AAAA,EACjC,qBAAqB,EAAE,OAAO,EAAE,SAAS;AAAA,EACzC,eAAe,EAAE,OAAO,EAAE,SAAS;AAAA,EACnC,cAAc,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAC3D,CAAC;AAED,MAAM,6BAA6B,EAAE,OAAO;AAAA,EAC1C,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,QAAQ,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAAA,EACxC,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,EAC1B,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,QAAQ,EAAE,OAAO,EAAE,KAAK;AAAA;AAAA;AAAA;AAAA,EAIxB,eAAe,0BAA0B,SAAS;AACpD,CAAC;AAID,MAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,WAAW,EAAE,OAAO,EAAE,KAAK;AAAA,EAC3B,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,SAAS,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAAE,SAAS;AAAA,EACpD,UAAU,EAAE,OAAO,EAAE,KAAK;AAAA,EAC1B,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS;AAAA,EAC3C,QAAQ,EAAE,OAAO,EAAE,KAAK;AAC1B,CAAC;AAWD,SAAS,MAAM,OAA+C;AAC5D,SAAO,QAAQ,MAAM,YAAY,IAAI;AACvC;AAEA,SAAS,OAAO,OAA+C;AAC7D,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,IAAI,KAAK,KAAK;AACvB;AAEA,eAAe,oBAAoB,IAAmB,OAAkC;AACtF,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI,MAAM;AAAA,MACV,UAAU,MAAM;AAAA,MAChB,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,EACnE;AACA,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,mBAAmB;AACjD,2BAAyB,OAA4B,OAAO;AAE5D,QAAM,YAAY,MAAM,GAAG,QAAQ,kBAAkB;AAAA,IACnD,WAAW,MAAM;AAAA,IACjB,iBAAiB,MAAM;AAAA,IACvB,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,eAAe;AAC/C,SAAO;AACT;AAEA,eAAe,qBAAqB,IAAmB,OAA2B;AAChF,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,MACE,IAAI,MAAM;AAAA,MACV,UAAU,MAAM;AAAA,MAChB,WAAW;AAAA,IACb;AAAA,IACA;AAAA,IACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,EACnE;AACA,MAAI,CAAC,QAAS,OAAM,IAAI,MAAM,mBAAmB;AACjD,2BAAyB,OAA4B,OAAO;AAC5D,QAAM,YAAY,MAAM,GAAG,QAAQ,kBAAkB;AAAA,IACnD,WAAW,MAAM;AAAA,IACjB,iBAAiB,MAAM;AAAA,IACvB,WAAW;AAAA,EACb,CAAC;AACD,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,eAAe;AAC/C,SAAO;AACT;AAEA,SAAS,oBAAoB,SAAuC;AAClE,SAAO;AAAA,IACL,aAAa,QAAQ,eAAe;AAAA,IACpC,qBAAqB,QAAQ,uBAAuB;AAAA,IACpD,eAAe,MAAM,QAAQ,aAAa;AAAA,IAC1C,cAAc,QAAQ,gBAAgB;AAAA,EACxC;AACF;AAEA,MAAM,8BAAqE;AAAA,EACzE,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,QAAQ,2BAA2B,MAAM,QAAQ;AACvD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,UAAU,MAAM,oBAAoB,IAAI,KAAK;AACnD,WAAO,EAAE,QAAQ,oBAAoB,OAAO,EAAE;AAAA,EAChD;AAAA,EACA,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,QAAQ,2BAA2B,MAAM,QAAQ;AACvD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,UAAU,MAAM,oBAAoB,IAAI,KAAK;AACnD,UAAM,kBACJ,QAAQ,gBAAgB,MAAM,YAAY,QAAQ,wBAAwB,MAAM;AAClF,QAAI,CAAC,iBAAiB;AACpB,UAAI,QAAQ,aAAa;AACvB,cAAM,IAAI,MAAM,sBAAsB;AAAA,MACxC;AAIA,YAAM,cAAc,MAAM,GAAG;AAAA,QAC3B;AAAA,QACA,EAAE,IAAI,MAAM,WAAW,UAAU,MAAM,UAAU,aAAa,MAAM,WAAW,KAAK;AAAA,QACpF;AAAA,UACE,aAAa,MAAM;AAAA,UACnB,qBAAqB,MAAM;AAAA,UAC3B,eAAe,oBAAI,KAAK;AAAA,QAC1B;AAAA,MACF;AACA,UAAI,gBAAgB,GAAG;AACrB,cAAM,IAAI,MAAM,sBAAsB;AAAA,MACxC;AACA,cAAQ,cAAc,MAAM;AAC5B,cAAQ,sBAAsB,MAAM;AAAA,IACtC;AACA,QAAI,CAAC,QAAQ,eAAe;AAC1B,cAAQ,gBAAgB,oBAAI,KAAK;AAAA,IACnC;AAGA,YAAQ,eAAe,MAAM;AAC7B,UAAM,GAAG,MAAM;AACf,WAAO,EAAE,IAAI,KAAK;AAAA,EACpB;AAAA,EACA,MAAM,aAAa,UAAU,SAAS,KAAK;AACzC,UAAM,QAAQ,2BAA2B,MAAM,QAAQ;AACvD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,UAAU,MAAM,oBAAoB,IAAI,KAAK;AACnD,WAAO,oBAAoB,OAAO;AAAA,EACpC;AAAA,EACA,UAAU,OAAO,EAAE,OAAO,UAAU,MAAM;AACxC,UAAM,SAAS,2BAA2B,MAAM,KAAK;AAIrD,UAAM,SACH,OAAO,iBACP,UAAU,UACX;AACF,WAAO;AAAA,MACL,aAAa;AAAA,MACb,cAAc;AAAA,MACd,YAAY,OAAO;AAAA,MACnB,UAAU,OAAO;AAAA,MACjB,gBAAgB,OAAO;AAAA,MACvB,SAAS;AAAA,QACP,MAAM;AAAA,UACJ;AAAA,UACA,OAAQ,UAAU,SAA6C;AAAA,QACjE;AAAA,MACF;AAAA,MACA,gBAAgB;AAAA,MAChB,eAAe,UAAU,SAAS;AAAA,IACpC;AAAA,EACF;AAAA,EACA,MAAM,OAAO,EAAE,UAAU,IAAI,MAAM;AACjC,UAAM,OAAO,mBAAqD,QAAQ;AAC1E,UAAM,SAAS,MAAM;AACrB,QAAI,CAAC,OAAQ;AACb,UAAM,YAAY,UAAU;AAC5B,QAAI,CAAC,UAAW;AAChB,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,UAAU,MAAM,sBAAsB,IAAI,SAAS,EAAE,IAAI,UAAU,CAAC;AAC1E,QAAI,CAAC,QAAS;AACd,YAAQ,cAAc,OAAO;AAC7B,YAAQ,sBAAsB,OAAO;AACrC,YAAQ,gBAAgB,OAAO,OAAO,aAAa;AACnD,YAAQ,eAAe,OAAO;AAC9B,UAAM,GAAG,MAAM;AAAA,EACjB;AACF;AAEA,MAAM,uBAGF;AAAA,EACF,IAAI;AAAA,EACJ,MAAM,QAAQ,UAAU,KAAK;AAC3B,UAAM,QAAQ,oBAAoB,MAAM,QAAQ;AAChD,UAAM,KAAM,IAAI,UAAU,QAAQ,IAAI,EAAoB,KAAK;AAC/D,UAAM,UAAU,MAAM,qBAAqB,IAAI,KAAK;AAMpD,iCAA6B;AAAA,MAC3B,cAAc;AAAA,MACd,YAAY,QAAQ;AAAA,MACpB,SAAS,QAAQ;AAAA,MACjB,SAAS,IAAI,WAAW;AAAA,IAC1B,CAAC;AAED,UAAM,UAAU,MAAM,GAAG,KAAK,eAAe,EAAE,WAAW,QAAQ,GAAG,CAAC;AACtE,UAAM,SAAS,8BAA8B,SAAS,SAAS,MAAM,QAAQ;AAE7E,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,kBAAkB;AAAA,IACpC;AAKA,QAAI,OAAO,aAAa,CAAC,uBAAuB,OAAO,SAAS,GAAG;AACjE,YAAM,IAAI,MAAM,+BAA+B;AAAA,IACjD;AAEA,QAAI,QAAQ,aAAa;AACvB,YAAM,OAAO,OAAO,IAAI,MAAM,sBAAsB,GAAG,EAAE,aAAa,QAAQ,YAAY,CAAC;AAAA,IAC7F;AAEA,UAAM,0BAA0B,wBAAwB,MAAM;AAE9D,UAAM,aAAa,yBAAyB,OAAO;AACnD,QAAI,YAAY,WAAW;AACzB,UAAI,IAAI,KAAK,WAAW,SAAS,IAAI,oBAAI,KAAK,GAAG;AAC/C,cAAM,IAAI,MAAM,sBAAsB;AAAA,MACxC;AAAA,IACF,OAAO;AACL,YAAM,cAAc,eAAe,QAAQ,IAAI;AAC/C,UAAI,aAAa,2BAA2B,QAAQ,QAAQ;AAC1D,cAAM,SAAS,IAAI,KAAK,QAAQ,OAAO,QAAQ,IAAI,YAAY,0BAA0B,KAAK,KAAK,GAAI;AACvG,YAAI,SAAS,oBAAI,KAAK,GAAG;AACvB,gBAAM,IAAI,MAAM,sBAAsB;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAGA,UAAM,gBAAgB,oBAAoB,OAAO;AACjD,QAAI,kBAAkB;AACtB,UAAM,uBAAuB,YAAY;AACvC,UAAI,CAAC,gBAAiB;AACtB,wBAAkB;AAClB,YAAM,GAAG;AAAA,QACP;AAAA,QACA;AAAA,UACE,IAAI,QAAQ;AAAA,UACZ,UAAU,MAAM;AAAA,UAChB,aAAa,OAAO;AAAA,UACpB,qBAAqB,MAAM;AAAA,QAC7B;AAAA,QACA,EAAE,aAAa,MAAM,qBAAqB,MAAM,eAAe,KAAK;AAAA,MACtE;AAAA,IACF;AAEA,QAAI,yBAAyB;AAI3B,YAAM,cAAc,MAAM,GAAG;AAAA,QAC3B;AAAA,QACA,EAAE,IAAI,QAAQ,IAAI,UAAU,MAAM,UAAU,aAAa,MAAM,WAAW,KAAK;AAAA,QAC/E;AAAA,UACE,aAAa,OAAO;AAAA,UACpB,qBAAqB,MAAM;AAAA,UAC3B,eAAe,oBAAI,KAAK;AAAA,QAC1B;AAAA,MACF;AACA,UAAI,gBAAgB,GAAG;AACrB,cAAM,UAAU,MAAM;AAAA,UACpB;AAAA,UACA;AAAA,UACA,EAAE,IAAI,QAAQ,IAAI,UAAU,MAAM,UAAU,WAAW,KAAK;AAAA,UAC5D;AAAA,UACA,EAAE,UAAU,MAAM,UAAU,gBAAgB,MAAM,eAAe;AAAA,QACnE;AACA,cAAM,OAAO,OAAO,IAAI,MAAM,sBAAsB,GAAG;AAAA,UACrD,aAAa,SAAS,eAAe,QAAQ,eAAe,OAAO;AAAA,QACrE,CAAC;AAAA,MACH;AACA,wBAAkB;AAAA,IACpB;AAEA,UAAM,aAAa,IAAI,UAAU,QAAQ,YAAY;AAOrD,QAAI,SAAkC,CAAC;AACvC,QAAI,oBAAoC;AAExC,QAAI,OAAO,WAAW;AACpB,UAAI;AACF,cAAM,cAAc;AAAA,UAClB;AAAA,UACA;AAAA,UACA;AAAA,YACE,UAAU,MAAM;AAAA,YAChB,gBAAgB,MAAM;AAAA,YACtB,QAAQ,MAAM;AAAA,UAChB;AAAA,UACA,MAAM,WAAW,CAAC;AAAA,QACpB;AAEA,YAAI,YAAY,MAAM,MAAM;AAC1B,gBAAM,aAAa,OAAO,WAAW,YAAY,QAAQ,kBAAkB;AAC3E,cAAI,OAAO,eAAe,YAAY,WAAW,KAAK,EAAE,SAAS,GAAG;AAClE,wBAAY,KAAK;AAAA,UACnB;AAAA,QACF;AAEA,cAAM,gBAAgB,MAAM,WAAW,QAAQ,OAAO,WAAW;AAAA,UAC/D,OAAO;AAAA,UACP,KAAK;AAAA,YACH,WAAW,IAAI;AAAA,YACf,MAAM;AAAA,cACJ,KAAK,MAAM;AAAA,cACX,UAAU,MAAM;AAAA,cAChB,OAAO,MAAM;AAAA,YACf;AAAA,YACA,mBAAmB;AAAA,YACnB,wBAAwB,MAAM;AAAA,YAC9B,iBAAiB,MAAM,iBAAiB,CAAC,MAAM,cAAc,IAAI;AAAA,UACnE;AAAA,UACA,UAAU;AAAA,YACR,UAAU,MAAM;AAAA,YAChB,gBAAgB,MAAM;AAAA,YACtB,cAAc;AAAA,UAChB;AAAA,QACF,CAAC;AAED,iBAAU,cAAc,UAAsC,CAAC;AAC/D,4BAAoB,cAAc,YAAY;AAAA,MAChD,SAAS,KAAK;AAGZ,cAAM,qBAAqB;AAC3B,YAAI,gBAAgB,GAAG,KAAK,mCAAmC,GAAG,GAAG;AACnE,gBAAM;AAAA,QACR;AACA,eAAO,MAAM,2CAA2C,EAAE,IAAI,CAAC;AAC/D,4BAAoB,GAAG,YAAY,KAAK,EAAE,QAAQ,YAAY,MAAM,yBAAyB,CAAC;AAC9F,cAAM,IAAI,MAAM,eAAe;AAAA,MACjC;AAAA,IACF,WAAW,OAAO,MAAM;AACtB,YAAM,eAAe,kBAAkB,QAAQ,SAAS;AAAA,QACtD,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,MAChB,CAAC;AACD,UAAI,CAAC,cAAc;AACjB,cAAM,qBAAqB;AAC3B,cAAM,IAAI,MAAM,sCAAsC;AAAA,MACxD;AACA,eAAS,EAAE,UAAU,aAAa;AAAA,IACpC,OAAO;AACL,YAAM,qBAAqB;AAC3B,YAAM,IAAI,MAAM,iCAAiC;AAAA,IACnD;AAEA,QAAI,yBAAyB;AAC3B,YAAM,iBAAiB,MAAM,WAAW,QAAQ,oCAAoC;AAAA,QAClF,OAAO;AAAA,UACL,WAAW,QAAQ;AAAA,UACnB,UAAU,OAAO;AAAA,UACjB;AAAA,UACA,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM;AAAA,UACtB,QAAQ,MAAM;AAAA,UACd;AAAA,QACF;AAAA,QACA,KAAK;AAAA,UACH,WAAW,IAAI;AAAA,UACf,MAAM,IAAI,QAAQ;AAAA,UAClB,mBAAmB;AAAA,UACnB,wBAAwB,MAAM;AAAA,UAC9B,iBAAiB,MAAM,iBAAiB,CAAC,MAAM,cAAc,IAAI;AAAA,QACnE;AAAA,MACF,CAAC;AACD,UAAI,CAAC,mBAAmB;AACtB,4BAAoB,eAAe,YAAY;AAAA,MACjD;AACA,YAAM;AAAA,QACJ;AAAA,QACA;AAAA,UACE,WAAW,QAAQ;AAAA,UACnB,UAAU,OAAO;AAAA,UACjB,QAAQ,MAAM;AAAA,UACd,iBAAiB,MAAM;AAAA,UACvB;AAAA,UACA,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM;AAAA,QACxB;AAAA,QACA,EAAE,YAAY,KAAK;AAAA,MACrB;AAAA,IACF;AAEA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,UAAU,OAAO;AAAA,MACjB;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;AAEA,gBAAgB,2BAA2B;AAC3C,gBAAgB,oBAAoB;",
6
6
  "names": []
7
7
  }
@@ -209,6 +209,10 @@ GatewaySessionInitialization = __decorateClass([
209
209
  Index({
210
210
  name: "gateway_session_initializations_prune_idx",
211
211
  expression: 'create index "gateway_session_initializations_prune_idx" on "gateway_session_initializations" ("tenant_id", "organization_id", "updated_at") where "gateway_transaction_id" is not null'
212
+ }),
213
+ Index({
214
+ name: "gateway_session_initializations_retention_idx",
215
+ properties: ["tenantId", "organizationId", "updatedAt", "id"]
212
216
  })
213
217
  ], GatewaySessionInitialization);
214
218
  OptionalProps;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/payment_gateways/data/entities.ts"],
4
- "sourcesContent": ["import { OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: 'gateway_transactions' })\n@Index({ properties: ['paymentId', 'organizationId', 'tenantId'] })\n@Index({ properties: ['providerKey', 'providerSessionId', 'organizationId'] })\n@Index({ properties: ['organizationId', 'tenantId', 'unifiedStatus'] })\nexport class GatewayTransaction {\n [OptionalProps]?: 'unifiedStatus' | 'gatewayStatus' | 'providerSessionId' | 'gatewayPaymentId' | 'gatewayRefundId' | 'redirectUrl' | 'clientSecret' | 'capturedAmount' | 'gatewayMetadata' | 'webhookLog' | 'lastWebhookAt' | 'lastPolledAt' | 'expiresAt' | 'createdAt' | 'updatedAt' | 'deletedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'payment_id', type: 'uuid' })\n paymentId!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'provider_session_id', type: 'text', nullable: true })\n providerSessionId?: string | null\n\n @Property({ name: 'gateway_payment_id', type: 'text', nullable: true })\n gatewayPaymentId?: string | null\n\n @Property({ name: 'gateway_refund_id', type: 'text', nullable: true })\n gatewayRefundId?: string | null\n\n @Property({ name: 'unified_status', type: 'text' })\n unifiedStatus: string = 'pending'\n\n @Property({ name: 'gateway_status', type: 'text', nullable: true })\n gatewayStatus?: string | null\n\n @Property({ name: 'redirect_url', type: 'text', nullable: true })\n redirectUrl?: string | null\n\n @Property({ name: 'client_secret', type: 'text', nullable: true })\n clientSecret?: string | null\n\n @Property({ name: 'amount', type: 'numeric', precision: 18, scale: 4 })\n amount!: string\n\n @Property({ name: 'captured_amount', type: 'numeric', precision: 18, scale: 4, default: '0' })\n capturedAmount: string = '0'\n\n @Property({ name: 'currency_code', type: 'text' })\n currencyCode!: string\n\n @Property({ name: 'gateway_metadata', type: 'jsonb', nullable: true })\n gatewayMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'webhook_log', type: 'jsonb', nullable: true })\n webhookLog?: Array<{ eventType: string; receivedAt: string; idempotencyKey: string; unifiedStatus: string; processed: boolean }> | null\n\n @Property({ name: 'last_webhook_at', type: Date, nullable: true })\n lastWebhookAt?: Date | null\n\n @Property({ name: 'last_polled_at', type: Date, nullable: true })\n lastPolledAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n expiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n@Entity({ tableName: 'gateway_payment_operations' })\n@Unique({\n name: 'gateway_payment_operations_scope_operation_unique',\n properties: ['operationId', 'organizationId', 'tenantId'],\n})\n@Index({ properties: ['transactionId', 'operationType', 'organizationId', 'tenantId'] })\n@Index({ properties: ['status', 'leaseExpiresAt'] })\nexport class GatewayPaymentOperation {\n [OptionalProps]?: 'status' | 'attemptCount' | 'result' | 'reservedAmount' | 'leaseExpiresAt' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_id', type: 'text' })\n operationId!: string\n\n @Property({ name: 'transaction_id', type: 'uuid' })\n transactionId!: string\n\n @Property({ name: 'operation_type', type: 'text' })\n operationType!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'request_hash', type: 'text' })\n requestHash!: string\n\n @Property({ name: 'provider_idempotency_key', type: 'text' })\n providerIdempotencyKey!: string\n\n @Property({ name: 'status', type: 'text' })\n status: string = 'in_progress'\n\n @Property({ name: 'attempt_token', type: 'text' })\n attemptToken!: string\n\n @Property({ name: 'attempt_count', type: 'integer' })\n attemptCount: number = 1\n\n @Property({ name: 'result', type: 'jsonb', nullable: true })\n result?: Record<string, unknown> | null\n\n @Property({ name: 'reserved_amount', type: 'numeric', precision: 18, scale: 4, nullable: true })\n reservedAmount?: string | null\n\n @Property({ name: 'lease_expires_at', type: Date, nullable: true })\n leaseExpiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_session_initializations' })\n@Unique({\n name: 'gateway_session_initializations_scope_operation_unique',\n properties: ['operationKey', 'providerKey', 'organizationId', 'tenantId'],\n})\n@Index({\n name: 'gateway_session_initializations_prune_idx',\n expression:\n 'create index \"gateway_session_initializations_prune_idx\" on \"gateway_session_initializations\" (\"tenant_id\", \"organization_id\", \"updated_at\") where \"gateway_transaction_id\" is not null',\n})\nexport class GatewaySessionInitialization {\n [OptionalProps]?: 'claimToken' | 'claimedAt' | 'gatewayTransactionId' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_key', type: 'text' })\n operationKey!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'claim_token', type: 'uuid', nullable: true })\n claimToken?: string | null\n\n @Property({ name: 'claimed_at', type: Date, nullable: true })\n claimedAt?: Date | null\n\n @Property({ name: 'gateway_transaction_id', type: 'uuid', nullable: true })\n gatewayTransactionId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_webhook_events' })\n@Unique({\n name: 'gateway_webhook_events_idempotency_unique',\n properties: ['idempotencyKey', 'providerKey', 'organizationId', 'tenantId'],\n})\nexport class WebhookProcessedEvent {\n [OptionalProps]?: 'processedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'idempotency_key', type: 'text' })\n idempotencyKey!: string\n\n @Property({ name: 'event_type', type: 'text' })\n eventType!: string\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'processed_at', type: Date, onCreate: () => new Date() })\n processedAt: Date = new Date()\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: 'gateway_transactions' })\n@Index({ properties: ['paymentId', 'organizationId', 'tenantId'] })\n@Index({ properties: ['providerKey', 'providerSessionId', 'organizationId'] })\n@Index({ properties: ['organizationId', 'tenantId', 'unifiedStatus'] })\nexport class GatewayTransaction {\n [OptionalProps]?: 'unifiedStatus' | 'gatewayStatus' | 'providerSessionId' | 'gatewayPaymentId' | 'gatewayRefundId' | 'redirectUrl' | 'clientSecret' | 'capturedAmount' | 'gatewayMetadata' | 'webhookLog' | 'lastWebhookAt' | 'lastPolledAt' | 'expiresAt' | 'createdAt' | 'updatedAt' | 'deletedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'payment_id', type: 'uuid' })\n paymentId!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'provider_session_id', type: 'text', nullable: true })\n providerSessionId?: string | null\n\n @Property({ name: 'gateway_payment_id', type: 'text', nullable: true })\n gatewayPaymentId?: string | null\n\n @Property({ name: 'gateway_refund_id', type: 'text', nullable: true })\n gatewayRefundId?: string | null\n\n @Property({ name: 'unified_status', type: 'text' })\n unifiedStatus: string = 'pending'\n\n @Property({ name: 'gateway_status', type: 'text', nullable: true })\n gatewayStatus?: string | null\n\n @Property({ name: 'redirect_url', type: 'text', nullable: true })\n redirectUrl?: string | null\n\n @Property({ name: 'client_secret', type: 'text', nullable: true })\n clientSecret?: string | null\n\n @Property({ name: 'amount', type: 'numeric', precision: 18, scale: 4 })\n amount!: string\n\n @Property({ name: 'captured_amount', type: 'numeric', precision: 18, scale: 4, default: '0' })\n capturedAmount: string = '0'\n\n @Property({ name: 'currency_code', type: 'text' })\n currencyCode!: string\n\n @Property({ name: 'gateway_metadata', type: 'jsonb', nullable: true })\n gatewayMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'webhook_log', type: 'jsonb', nullable: true })\n webhookLog?: Array<{ eventType: string; receivedAt: string; idempotencyKey: string; unifiedStatus: string; processed: boolean }> | null\n\n @Property({ name: 'last_webhook_at', type: Date, nullable: true })\n lastWebhookAt?: Date | null\n\n @Property({ name: 'last_polled_at', type: Date, nullable: true })\n lastPolledAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n expiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n@Entity({ tableName: 'gateway_payment_operations' })\n@Unique({\n name: 'gateway_payment_operations_scope_operation_unique',\n properties: ['operationId', 'organizationId', 'tenantId'],\n})\n@Index({ properties: ['transactionId', 'operationType', 'organizationId', 'tenantId'] })\n@Index({ properties: ['status', 'leaseExpiresAt'] })\nexport class GatewayPaymentOperation {\n [OptionalProps]?: 'status' | 'attemptCount' | 'result' | 'reservedAmount' | 'leaseExpiresAt' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_id', type: 'text' })\n operationId!: string\n\n @Property({ name: 'transaction_id', type: 'uuid' })\n transactionId!: string\n\n @Property({ name: 'operation_type', type: 'text' })\n operationType!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'request_hash', type: 'text' })\n requestHash!: string\n\n @Property({ name: 'provider_idempotency_key', type: 'text' })\n providerIdempotencyKey!: string\n\n @Property({ name: 'status', type: 'text' })\n status: string = 'in_progress'\n\n @Property({ name: 'attempt_token', type: 'text' })\n attemptToken!: string\n\n @Property({ name: 'attempt_count', type: 'integer' })\n attemptCount: number = 1\n\n @Property({ name: 'result', type: 'jsonb', nullable: true })\n result?: Record<string, unknown> | null\n\n @Property({ name: 'reserved_amount', type: 'numeric', precision: 18, scale: 4, nullable: true })\n reservedAmount?: string | null\n\n @Property({ name: 'lease_expires_at', type: Date, nullable: true })\n leaseExpiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_session_initializations' })\n@Unique({\n name: 'gateway_session_initializations_scope_operation_unique',\n properties: ['operationKey', 'providerKey', 'organizationId', 'tenantId'],\n})\n@Index({\n name: 'gateway_session_initializations_prune_idx',\n expression:\n 'create index \"gateway_session_initializations_prune_idx\" on \"gateway_session_initializations\" (\"tenant_id\", \"organization_id\", \"updated_at\") where \"gateway_transaction_id\" is not null',\n})\n@Index({\n name: 'gateway_session_initializations_retention_idx',\n properties: ['tenantId', 'organizationId', 'updatedAt', 'id'],\n})\nexport class GatewaySessionInitialization {\n [OptionalProps]?: 'claimToken' | 'claimedAt' | 'gatewayTransactionId' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_key', type: 'text' })\n operationKey!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'claim_token', type: 'uuid', nullable: true })\n claimToken?: string | null\n\n @Property({ name: 'claimed_at', type: Date, nullable: true })\n claimedAt?: Date | null\n\n @Property({ name: 'gateway_transaction_id', type: 'uuid', nullable: true })\n gatewayTransactionId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_webhook_events' })\n@Unique({\n name: 'gateway_webhook_events_idempotency_unique',\n properties: ['idempotencyKey', 'providerKey', 'organizationId', 'tenantId'],\n})\nexport class WebhookProcessedEvent {\n [OptionalProps]?: 'processedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'idempotency_key', type: 'text' })\n idempotencyKey!: string\n\n @Property({ name: 'event_type', type: 'text' })\n eventType!: string\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'processed_at', type: Date, onCreate: () => new Date() })\n processedAt: Date = new Date()\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -97,27 +97,50 @@ async function refreshPaymentSessionInitialization(em, ownership, scope, claimed
97
97
  return refreshedRows > 0;
98
98
  }
99
99
  async function pruneCompletedPaymentSessionInitializations(em, scope, options = {}) {
100
+ return prunePaymentSessionInitializations(em, scope, options, false);
101
+ }
102
+ async function pruneExpiredPaymentSessionInitializations(em, scope, options = {}) {
103
+ return prunePaymentSessionInitializations(em, scope, options, true);
104
+ }
105
+ async function prunePaymentSessionInitializations(em, scope, options, includeIncomplete) {
100
106
  const now = options.now ?? /* @__PURE__ */ new Date();
101
107
  const batchSize = options.batchSize ?? PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE;
102
108
  if (!Number.isInteger(batchSize) || batchSize < 1) {
103
109
  throw new Error("[internal] Payment-session initialization prune batch size must be a positive integer");
104
110
  }
105
111
  const cutoff = new Date(now.getTime() - PAYMENT_SESSION_REPLAY_WINDOW_MS);
112
+ const eligibleState = includeIncomplete ? `(gateway_transaction_id is not null or (
113
+ gateway_transaction_id is null and (
114
+ (claim_token is null and claimed_at is null)
115
+ or (claim_token is not null and claimed_at < ?)
116
+ )
117
+ ))` : "gateway_transaction_id is not null";
106
118
  const connection = em.getConnection();
107
119
  const result = await connection.execute(
108
120
  `
109
- delete from gateway_session_initializations
110
- where id in (
121
+ with expired_claims as materialized (
111
122
  select id from gateway_session_initializations
112
123
  where tenant_id = ?
113
124
  and organization_id = ?
114
- and gateway_transaction_id is not null
115
125
  and updated_at < ?
116
- order by updated_at asc
126
+ and ${eligibleState}
127
+ order by updated_at asc, id asc
117
128
  limit ?
129
+ for update skip locked
118
130
  )
131
+ delete from gateway_session_initializations
132
+ where tenant_id = ? and organization_id = ?
133
+ and id in (select id from expired_claims)
119
134
  `,
120
- [scope.tenantId, scope.organizationId, cutoff, batchSize],
135
+ [
136
+ scope.tenantId,
137
+ scope.organizationId,
138
+ cutoff,
139
+ ...includeIncomplete ? [cutoff] : [],
140
+ batchSize,
141
+ scope.tenantId,
142
+ scope.organizationId
143
+ ],
121
144
  "run"
122
145
  );
123
146
  return result?.affectedRows ?? result?.rowCount ?? 0;
@@ -130,6 +153,7 @@ export {
130
153
  claimPaymentSessionInitialization,
131
154
  findPaymentSessionInitialization,
132
155
  pruneCompletedPaymentSessionInitializations,
156
+ pruneExpiredPaymentSessionInitializations,
133
157
  reclaimPaymentSessionInitialization,
134
158
  refreshPaymentSessionInitialization,
135
159
  releasePaymentSessionInitialization
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/payment_gateways/lib/session-idempotency.ts"],
4
- "sourcesContent": ["import { createHash, randomUUID } from 'node:crypto'\nimport { UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { GatewaySessionInitialization } from '../data/entities'\n\ntype Scope = { organizationId: string; tenantId: string }\n\nexport const PAYMENT_SESSION_REPLAY_WINDOW_MS = 24 * 60 * 60 * 1000\nexport const PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE =\n 'payment-gateway-session-initialization-prune'\nexport const PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE = 1_000\n\nexport type OwnedPaymentSessionInitialization = {\n id: string\n claimToken: string\n}\n\nexport function buildPaymentSessionOperationKey(input: {\n idempotencyKey: string\n paymentId: string\n providerKey: string\n scope: Scope\n}): string {\n const digest = createHash('sha256')\n .update(JSON.stringify([\n input.scope.tenantId,\n input.scope.organizationId,\n input.providerKey,\n input.paymentId,\n input.idempotencyKey,\n ]))\n .digest('hex')\n return `om-payment-session:${digest}`\n}\n\nexport async function findPaymentSessionInitialization(\n em: EntityManager,\n operationKey: string,\n providerKey: string,\n scope: Scope,\n): Promise<GatewaySessionInitialization | null> {\n return findOneWithDecryption(\n em.fork(),\n GatewaySessionInitialization,\n {\n operationKey,\n providerKey,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n}\n\nexport async function claimPaymentSessionInitialization(\n em: EntityManager,\n operationKey: string,\n providerKey: string,\n scope: Scope,\n claimedAt: Date,\n): Promise<OwnedPaymentSessionInitialization | null> {\n const claimEm = em.fork()\n const claimToken = randomUUID()\n const record = claimEm.create(GatewaySessionInitialization, {\n operationKey,\n providerKey,\n claimToken,\n claimedAt,\n gatewayTransactionId: null,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n try {\n await claimEm.persist(record).flush()\n return { id: record.id, claimToken }\n } catch (error: unknown) {\n if (error instanceof UniqueConstraintViolationException) return null\n throw error\n }\n}\n\nexport async function reclaimPaymentSessionInitialization(\n em: EntityManager,\n claim: GatewaySessionInitialization,\n scope: Scope,\n claimedAt: Date,\n staleBefore: Date,\n): Promise<OwnedPaymentSessionInitialization | null> {\n const claimToken = randomUUID()\n const claimedRows = await em.fork().nativeUpdate(\n GatewaySessionInitialization,\n {\n id: claim.id,\n operationKey: claim.operationKey,\n providerKey: claim.providerKey,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n gatewayTransactionId: null,\n $or: [\n { claimToken: null },\n { claimedAt: { $lt: staleBefore } },\n ],\n },\n { claimToken, claimedAt, updatedAt: claimedAt },\n )\n return claimedRows > 0 ? { id: claim.id, claimToken } : null\n}\n\nexport async function releasePaymentSessionInitialization(\n em: EntityManager,\n ownership: OwnedPaymentSessionInitialization,\n scope: Scope,\n): Promise<void> {\n await em.fork().nativeUpdate(\n GatewaySessionInitialization,\n {\n id: ownership.id,\n claimToken: ownership.claimToken,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n gatewayTransactionId: null,\n },\n { claimToken: null, claimedAt: null, updatedAt: new Date() },\n )\n}\n\nexport async function refreshPaymentSessionInitialization(\n em: EntityManager,\n ownership: OwnedPaymentSessionInitialization,\n scope: Scope,\n claimedAt: Date,\n): Promise<boolean> {\n const refreshedRows = await em.fork().nativeUpdate(\n GatewaySessionInitialization,\n {\n id: ownership.id,\n claimToken: ownership.claimToken,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n gatewayTransactionId: null,\n },\n { claimedAt, updatedAt: claimedAt },\n )\n return refreshedRows > 0\n}\n\nexport async function pruneCompletedPaymentSessionInitializations(\n em: EntityManager,\n scope: Scope,\n options: { now?: Date; batchSize?: number } = {},\n): Promise<number> {\n const now = options.now ?? new Date()\n const batchSize = options.batchSize ?? PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE\n if (!Number.isInteger(batchSize) || batchSize < 1) {\n throw new Error('[internal] Payment-session initialization prune batch size must be a positive integer')\n }\n const cutoff = new Date(now.getTime() - PAYMENT_SESSION_REPLAY_WINDOW_MS)\n const connection = em.getConnection()\n const result = await connection.execute(\n `\n delete from gateway_session_initializations\n where id in (\n select id from gateway_session_initializations\n where tenant_id = ?\n and organization_id = ?\n and gateway_transaction_id is not null\n and updated_at < ?\n order by updated_at asc\n limit ?\n )\n `,\n [scope.tenantId, scope.organizationId, cutoff, batchSize],\n 'run',\n ) as { affectedRows?: number; rowCount?: number } | undefined\n return result?.affectedRows ?? result?.rowCount ?? 0\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createHash, randomUUID } from 'node:crypto'\nimport { UniqueConstraintViolationException } from '@mikro-orm/core'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { GatewaySessionInitialization } from '../data/entities'\n\ntype Scope = { organizationId: string; tenantId: string }\n\nexport const PAYMENT_SESSION_REPLAY_WINDOW_MS = 24 * 60 * 60 * 1000\nexport const PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE =\n 'payment-gateway-session-initialization-prune'\nexport const PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE = 1_000\n\nexport type OwnedPaymentSessionInitialization = {\n id: string\n claimToken: string\n}\n\nexport function buildPaymentSessionOperationKey(input: {\n idempotencyKey: string\n paymentId: string\n providerKey: string\n scope: Scope\n}): string {\n const digest = createHash('sha256')\n .update(JSON.stringify([\n input.scope.tenantId,\n input.scope.organizationId,\n input.providerKey,\n input.paymentId,\n input.idempotencyKey,\n ]))\n .digest('hex')\n return `om-payment-session:${digest}`\n}\n\nexport async function findPaymentSessionInitialization(\n em: EntityManager,\n operationKey: string,\n providerKey: string,\n scope: Scope,\n): Promise<GatewaySessionInitialization | null> {\n return findOneWithDecryption(\n em.fork(),\n GatewaySessionInitialization,\n {\n operationKey,\n providerKey,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n undefined,\n scope,\n )\n}\n\nexport async function claimPaymentSessionInitialization(\n em: EntityManager,\n operationKey: string,\n providerKey: string,\n scope: Scope,\n claimedAt: Date,\n): Promise<OwnedPaymentSessionInitialization | null> {\n const claimEm = em.fork()\n const claimToken = randomUUID()\n const record = claimEm.create(GatewaySessionInitialization, {\n operationKey,\n providerKey,\n claimToken,\n claimedAt,\n gatewayTransactionId: null,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n })\n try {\n await claimEm.persist(record).flush()\n return { id: record.id, claimToken }\n } catch (error: unknown) {\n if (error instanceof UniqueConstraintViolationException) return null\n throw error\n }\n}\n\nexport async function reclaimPaymentSessionInitialization(\n em: EntityManager,\n claim: GatewaySessionInitialization,\n scope: Scope,\n claimedAt: Date,\n staleBefore: Date,\n): Promise<OwnedPaymentSessionInitialization | null> {\n const claimToken = randomUUID()\n const claimedRows = await em.fork().nativeUpdate(\n GatewaySessionInitialization,\n {\n id: claim.id,\n operationKey: claim.operationKey,\n providerKey: claim.providerKey,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n gatewayTransactionId: null,\n $or: [\n { claimToken: null },\n { claimedAt: { $lt: staleBefore } },\n ],\n },\n { claimToken, claimedAt, updatedAt: claimedAt },\n )\n return claimedRows > 0 ? { id: claim.id, claimToken } : null\n}\n\nexport async function releasePaymentSessionInitialization(\n em: EntityManager,\n ownership: OwnedPaymentSessionInitialization,\n scope: Scope,\n): Promise<void> {\n await em.fork().nativeUpdate(\n GatewaySessionInitialization,\n {\n id: ownership.id,\n claimToken: ownership.claimToken,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n gatewayTransactionId: null,\n },\n { claimToken: null, claimedAt: null, updatedAt: new Date() },\n )\n}\n\nexport async function refreshPaymentSessionInitialization(\n em: EntityManager,\n ownership: OwnedPaymentSessionInitialization,\n scope: Scope,\n claimedAt: Date,\n): Promise<boolean> {\n const refreshedRows = await em.fork().nativeUpdate(\n GatewaySessionInitialization,\n {\n id: ownership.id,\n claimToken: ownership.claimToken,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n gatewayTransactionId: null,\n },\n { claimedAt, updatedAt: claimedAt },\n )\n return refreshedRows > 0\n}\n\nexport async function pruneCompletedPaymentSessionInitializations(\n em: EntityManager,\n scope: Scope,\n options: { now?: Date; batchSize?: number } = {},\n): Promise<number> {\n return prunePaymentSessionInitializations(em, scope, options, false)\n}\n\nexport async function pruneExpiredPaymentSessionInitializations(\n em: EntityManager,\n scope: Scope,\n options: { now?: Date; batchSize?: number } = {},\n): Promise<number> {\n return prunePaymentSessionInitializations(em, scope, options, true)\n}\n\nasync function prunePaymentSessionInitializations(\n em: EntityManager,\n scope: Scope,\n options: { now?: Date; batchSize?: number },\n includeIncomplete: boolean,\n): Promise<number> {\n const now = options.now ?? new Date()\n const batchSize = options.batchSize ?? PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE\n if (!Number.isInteger(batchSize) || batchSize < 1) {\n throw new Error('[internal] Payment-session initialization prune batch size must be a positive integer')\n }\n const cutoff = new Date(now.getTime() - PAYMENT_SESSION_REPLAY_WINDOW_MS)\n const eligibleState = includeIncomplete\n ? `(gateway_transaction_id is not null or (\n gateway_transaction_id is null and (\n (claim_token is null and claimed_at is null)\n or (claim_token is not null and claimed_at < ?)\n )\n ))`\n : 'gateway_transaction_id is not null'\n const connection = em.getConnection()\n const result = await connection.execute(\n `\n with expired_claims as materialized (\n select id from gateway_session_initializations\n where tenant_id = ?\n and organization_id = ?\n and updated_at < ?\n and ${eligibleState}\n order by updated_at asc, id asc\n limit ?\n for update skip locked\n )\n delete from gateway_session_initializations\n where tenant_id = ? and organization_id = ?\n and id in (select id from expired_claims)\n `,\n [\n scope.tenantId,\n scope.organizationId,\n cutoff,\n ...(includeIncomplete ? [cutoff] : []),\n batchSize,\n scope.tenantId,\n scope.organizationId,\n ],\n 'run',\n ) as { affectedRows?: number; rowCount?: number } | undefined\n return result?.affectedRows ?? result?.rowCount ?? 0\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,19 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+ class Migration20261018180000_payment_gateways extends Migration {
3
+ isTransactional() {
4
+ return false;
5
+ }
6
+ up() {
7
+ this.addSql('drop index concurrently if exists "gateway_session_initializations_retention_idx";');
8
+ this.addSql(
9
+ 'create index concurrently "gateway_session_initializations_retention_idx" on "gateway_session_initializations" ("tenant_id", "organization_id", "updated_at", "id");'
10
+ );
11
+ }
12
+ down() {
13
+ this.addSql('drop index concurrently if exists "gateway_session_initializations_retention_idx";');
14
+ }
15
+ }
16
+ export {
17
+ Migration20261018180000_payment_gateways
18
+ };
19
+ //# sourceMappingURL=Migration20261018180000_payment_gateways.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/payment_gateways/migrations/Migration20261018180000_payment_gateways.ts"],
4
+ "sourcesContent": ["import { Migration } from '@mikro-orm/migrations'\n\nexport class Migration20261018180000_payment_gateways extends Migration {\n override isTransactional(): boolean {\n return false\n }\n\n override up(): void | Promise<void> {\n this.addSql('drop index concurrently if exists \"gateway_session_initializations_retention_idx\";')\n this.addSql(\n 'create index concurrently \"gateway_session_initializations_retention_idx\" on \"gateway_session_initializations\" (\"tenant_id\", \"organization_id\", \"updated_at\", \"id\");',\n )\n }\n\n override down(): void | Promise<void> {\n this.addSql('drop index concurrently if exists \"gateway_session_initializations_retention_idx\";')\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,iBAAiB;AAEnB,MAAM,iDAAiD,UAAU;AAAA,EAC7D,kBAA2B;AAClC,WAAO;AAAA,EACT;AAAA,EAES,KAA2B;AAClC,SAAK,OAAO,oFAAoF;AAChG,SAAK;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAES,OAA6B;AACpC,SAAK,OAAO,oFAAoF;AAAA,EAClG;AACF;",
6
+ "names": []
7
+ }
@@ -3,7 +3,7 @@ import { createLogger } from "@open-mercato/shared/lib/logger";
3
3
  import {
4
4
  PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,
5
5
  PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE,
6
- pruneCompletedPaymentSessionInitializations
6
+ pruneExpiredPaymentSessionInitializations
7
7
  } from "../lib/session-idempotency.js";
8
8
  const logger = createLogger("payment_gateways");
9
9
  let continuationQueue = null;
@@ -27,13 +27,13 @@ async function handle(job, ctx) {
27
27
  return;
28
28
  }
29
29
  const em = ctx.resolve("em").fork();
30
- const deleted = await pruneCompletedPaymentSessionInitializations(
30
+ const deleted = await pruneExpiredPaymentSessionInitializations(
31
31
  em,
32
32
  { tenantId, organizationId },
33
33
  { batchSize: PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE }
34
34
  );
35
35
  if (deleted > 0) {
36
- logger.info("Pruned completed payment-session initialization claims", {
36
+ logger.info("Pruned expired payment-session initialization claims", {
37
37
  deleted,
38
38
  tenantId,
39
39
  organizationId
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/payment_gateways/workers/session-initialization-prune.ts"],
4
- "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createModuleQueue, type JobContext, type Queue, type QueuedJob, type WorkerMeta } from '@open-mercato/queue'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,\n PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE,\n pruneCompletedPaymentSessionInitializations,\n} from '../lib/session-idempotency'\n\ntype PrunePayload = {\n tenantId?: string\n organizationId?: string\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nconst logger = createLogger('payment_gateways')\nlet continuationQueue: Queue<PrunePayload> | null = null\n\nfunction getContinuationQueue(): Queue<PrunePayload> {\n continuationQueue ??= createModuleQueue<PrunePayload>(\n PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,\n { concurrency: 1 },\n )\n return continuationQueue\n}\n\nexport const metadata: WorkerMeta = {\n queue: PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,\n id: 'payment_gateways:session-initialization-prune',\n concurrency: 1,\n}\n\nexport default async function handle(\n job: QueuedJob<PrunePayload>,\n ctx: HandlerContext,\n): Promise<void> {\n const tenantId = job.payload?.tenantId\n const organizationId = job.payload?.organizationId\n if (!tenantId || !organizationId) {\n logger.warn('Skipping payment-session initialization prune without tenant scope')\n return\n }\n\n const em = ctx.resolve<EntityManager>('em').fork()\n const deleted = await pruneCompletedPaymentSessionInitializations(\n em,\n { tenantId, organizationId },\n { batchSize: PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE },\n )\n if (deleted > 0) {\n logger.info('Pruned completed payment-session initialization claims', {\n deleted,\n tenantId,\n organizationId,\n })\n }\n if (deleted >= PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE) {\n await getContinuationQueue().enqueue({ tenantId, organizationId }, { delayMs: 1_000 })\n }\n}\n"],
5
- "mappings": "AACA,SAAS,yBAAuF;AAChG,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWP,MAAM,SAAS,aAAa,kBAAkB;AAC9C,IAAI,oBAAgD;AAEpD,SAAS,uBAA4C;AACnD,wBAAsB;AAAA,IACpB;AAAA,IACA,EAAE,aAAa,EAAE;AAAA,EACnB;AACA,SAAO;AACT;AAEO,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAEA,eAAO,OACL,KACA,KACe;AACf,QAAM,WAAW,IAAI,SAAS;AAC9B,QAAM,iBAAiB,IAAI,SAAS;AACpC,MAAI,CAAC,YAAY,CAAC,gBAAgB;AAChC,WAAO,KAAK,oEAAoE;AAChF;AAAA,EACF;AAEA,QAAM,KAAK,IAAI,QAAuB,IAAI,EAAE,KAAK;AACjD,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA,EAAE,UAAU,eAAe;AAAA,IAC3B,EAAE,WAAW,gDAAgD;AAAA,EAC/D;AACA,MAAI,UAAU,GAAG;AACf,WAAO,KAAK,0DAA0D;AAAA,MACpE;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AACA,MAAI,WAAW,iDAAiD;AAC9D,UAAM,qBAAqB,EAAE,QAAQ,EAAE,UAAU,eAAe,GAAG,EAAE,SAAS,IAAM,CAAC;AAAA,EACvF;AACF;",
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { createModuleQueue, type JobContext, type Queue, type QueuedJob, type WorkerMeta } from '@open-mercato/queue'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport {\n PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,\n PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE,\n pruneExpiredPaymentSessionInitializations,\n} from '../lib/session-idempotency'\n\ntype PrunePayload = {\n tenantId?: string\n organizationId?: string\n}\n\ntype HandlerContext = JobContext & {\n resolve: <T = unknown>(name: string) => T\n}\n\nconst logger = createLogger('payment_gateways')\nlet continuationQueue: Queue<PrunePayload> | null = null\n\nfunction getContinuationQueue(): Queue<PrunePayload> {\n continuationQueue ??= createModuleQueue<PrunePayload>(\n PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,\n { concurrency: 1 },\n )\n return continuationQueue\n}\n\nexport const metadata: WorkerMeta = {\n queue: PAYMENT_SESSION_INITIALIZATION_PRUNE_QUEUE,\n id: 'payment_gateways:session-initialization-prune',\n concurrency: 1,\n}\n\nexport default async function handle(\n job: QueuedJob<PrunePayload>,\n ctx: HandlerContext,\n): Promise<void> {\n const tenantId = job.payload?.tenantId\n const organizationId = job.payload?.organizationId\n if (!tenantId || !organizationId) {\n logger.warn('Skipping payment-session initialization prune without tenant scope')\n return\n }\n\n const em = ctx.resolve<EntityManager>('em').fork()\n const deleted = await pruneExpiredPaymentSessionInitializations(\n em,\n { tenantId, organizationId },\n { batchSize: PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE },\n )\n if (deleted > 0) {\n logger.info('Pruned expired payment-session initialization claims', {\n deleted,\n tenantId,\n organizationId,\n })\n }\n if (deleted >= PAYMENT_SESSION_INITIALIZATION_PRUNE_BATCH_SIZE) {\n await getContinuationQueue().enqueue({ tenantId, organizationId }, { delayMs: 1_000 })\n }\n}\n"],
5
+ "mappings": "AACA,SAAS,yBAAuF;AAChG,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAWP,MAAM,SAAS,aAAa,kBAAkB;AAC9C,IAAI,oBAAgD;AAEpD,SAAS,uBAA4C;AACnD,wBAAsB;AAAA,IACpB;AAAA,IACA,EAAE,aAAa,EAAE;AAAA,EACnB;AACA,SAAO;AACT;AAEO,MAAM,WAAuB;AAAA,EAClC,OAAO;AAAA,EACP,IAAI;AAAA,EACJ,aAAa;AACf;AAEA,eAAO,OACL,KACA,KACe;AACf,QAAM,WAAW,IAAI,SAAS;AAC9B,QAAM,iBAAiB,IAAI,SAAS;AACpC,MAAI,CAAC,YAAY,CAAC,gBAAgB;AAChC,WAAO,KAAK,oEAAoE;AAChF;AAAA,EACF;AAEA,QAAM,KAAK,IAAI,QAAuB,IAAI,EAAE,KAAK;AACjD,QAAM,UAAU,MAAM;AAAA,IACpB;AAAA,IACA,EAAE,UAAU,eAAe;AAAA,IAC3B,EAAE,WAAW,gDAAgD;AAAA,EAC/D;AACA,MAAI,UAAU,GAAG;AACf,WAAO,KAAK,wDAAwD;AAAA,MAClE;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAAA,EACH;AACA,MAAI,WAAW,iDAAiD;AAC9D,UAAM,qBAAqB,EAAE,QAAQ,EAAE,UAAU,eAAe,GAAG,EAAE,SAAS,IAAM,CAAC;AAAA,EACvF;AACF;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useCallback, useState } from "react";
4
+ import Link from "next/link";
5
+ import { useSearchParams } from "next/navigation";
6
+ import { useT } from "@open-mercato/shared/lib/i18n/context";
7
+ import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
8
+ import { Button } from "@open-mercato/ui/primitives/button";
9
+ import { Alert, AlertDescription } from "@open-mercato/ui/primitives/alert";
10
+ import { Spinner } from "@open-mercato/ui/primitives/spinner";
11
+ import { replaceWithPageReload } from "@open-mercato/core/modules/portal/lib/navigation";
12
+ function PortalMagicLinkPage({ params }) {
13
+ const t = useT();
14
+ const orgSlug = params.orgSlug;
15
+ const searchParams = useSearchParams();
16
+ const token = searchParams.get("token") ?? "";
17
+ const [submitting, setSubmitting] = useState(false);
18
+ const [error, setError] = useState(
19
+ token ? null : t("portal.magicLink.error.missingToken", "Sign-in link is invalid or incomplete.")
20
+ );
21
+ const handleSignIn = useCallback(async () => {
22
+ if (!token) return;
23
+ setSubmitting(true);
24
+ setError(null);
25
+ try {
26
+ const result = await apiCall("/api/customer_accounts/magic-link/verify", {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/json" },
29
+ credentials: "include",
30
+ body: JSON.stringify({ token })
31
+ });
32
+ if (result.ok && result.result?.ok) {
33
+ replaceWithPageReload(`/${orgSlug}/portal/dashboard`);
34
+ return;
35
+ }
36
+ setError(t("portal.magicLink.error.invalidToken", "This sign-in link is invalid or has expired. Request a new one."));
37
+ } catch {
38
+ setError(t("portal.magicLink.error.generic", "Sign-in failed. Please try again."));
39
+ } finally {
40
+ setSubmitting(false);
41
+ }
42
+ }, [token, orgSlug, t]);
43
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto flex w-full max-w-sm flex-col gap-4 py-12", children: [
44
+ /* @__PURE__ */ jsxs("div", { className: "text-center", children: [
45
+ /* @__PURE__ */ jsx("h1", { className: "text-2xl font-bold tracking-tight", children: t("portal.magicLink.title", "Sign in with a link") }),
46
+ /* @__PURE__ */ jsx("p", { className: "mt-1.5 text-sm text-muted-foreground", children: t("portal.magicLink.description", "Continue to sign in to the portal with the link from your email.") })
47
+ ] }),
48
+ error ? /* @__PURE__ */ jsx(Alert, { status: "error", children: /* @__PURE__ */ jsx(AlertDescription, { children: error }) }) : null,
49
+ token ? /* @__PURE__ */ jsxs(Button, { type: "button", onClick: () => void handleSignIn(), disabled: submitting, className: "w-full rounded-lg", children: [
50
+ submitting ? /* @__PURE__ */ jsx(Spinner, {}) : null,
51
+ t("portal.magicLink.submit", "Sign in")
52
+ ] }) : null,
53
+ /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", className: "rounded-lg", children: /* @__PURE__ */ jsx(Link, { href: `/${orgSlug}/portal/login`, children: t("portal.magicLink.backToLogin", "Back to sign in") }) })
54
+ ] });
55
+ }
56
+ export {
57
+ PortalMagicLinkPage as default
58
+ };
59
+ //# sourceMappingURL=page.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/portal/frontend/%5BorgSlug%5D/portal/magic-link/page.tsx"],
4
+ "sourcesContent": ["\"use client\"\nimport { useCallback, useState } from 'react'\nimport Link from 'next/link'\nimport { useSearchParams } from 'next/navigation'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { apiCall } from '@open-mercato/ui/backend/utils/apiCall'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Alert, AlertDescription } from '@open-mercato/ui/primitives/alert'\nimport { Spinner } from '@open-mercato/ui/primitives/spinner'\nimport { replaceWithPageReload } from '@open-mercato/core/modules/portal/lib/navigation'\n\ntype Props = { params: { orgSlug: string } }\n\nexport default function PortalMagicLinkPage({ params }: Props) {\n const t = useT()\n const orgSlug = params.orgSlug\n const searchParams = useSearchParams()\n const token = searchParams.get('token') ?? ''\n const [submitting, setSubmitting] = useState(false)\n const [error, setError] = useState<string | null>(\n token ? null : t('portal.magicLink.error.missingToken', 'Sign-in link is invalid or incomplete.'),\n )\n\n const handleSignIn = useCallback(async () => {\n if (!token) return\n setSubmitting(true)\n setError(null)\n try {\n const result = await apiCall<{ ok: boolean; error?: string }>('/api/customer_accounts/magic-link/verify', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n credentials: 'include',\n body: JSON.stringify({ token }),\n })\n if (result.ok && result.result?.ok) {\n replaceWithPageReload(`/${orgSlug}/portal/dashboard`)\n return\n }\n setError(t('portal.magicLink.error.invalidToken', 'This sign-in link is invalid or has expired. Request a new one.'))\n } catch {\n setError(t('portal.magicLink.error.generic', 'Sign-in failed. Please try again.'))\n } finally {\n setSubmitting(false)\n }\n }, [token, orgSlug, t])\n\n return (\n <div className=\"mx-auto flex w-full max-w-sm flex-col gap-4 py-12\">\n <div className=\"text-center\">\n <h1 className=\"text-2xl font-bold tracking-tight\">{t('portal.magicLink.title', 'Sign in with a link')}</h1>\n <p className=\"mt-1.5 text-sm text-muted-foreground\">\n {t('portal.magicLink.description', 'Continue to sign in to the portal with the link from your email.')}\n </p>\n </div>\n {error ? (\n <Alert status=\"error\">\n <AlertDescription>{error}</AlertDescription>\n </Alert>\n ) : null}\n {token ? (\n <Button type=\"button\" onClick={() => void handleSignIn()} disabled={submitting} className=\"w-full rounded-lg\">\n {submitting ? <Spinner /> : null}\n {t('portal.magicLink.submit', 'Sign in')}\n </Button>\n ) : null}\n <Button asChild variant=\"outline\" className=\"rounded-lg\">\n <Link href={`/${orgSlug}/portal/login`}>{t('portal.magicLink.backToLogin', 'Back to sign in')}</Link>\n </Button>\n </div>\n )\n}\n"],
5
+ "mappings": ";AAgDM,SACE,KADF;AA/CN,SAAS,aAAa,gBAAgB;AACtC,OAAO,UAAU;AACjB,SAAS,uBAAuB;AAChC,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,cAAc;AACvB,SAAS,OAAO,wBAAwB;AACxC,SAAS,eAAe;AACxB,SAAS,6BAA6B;AAIvB,SAAR,oBAAqC,EAAE,OAAO,GAAU;AAC7D,QAAM,IAAI,KAAK;AACf,QAAM,UAAU,OAAO;AACvB,QAAM,eAAe,gBAAgB;AACrC,QAAM,QAAQ,aAAa,IAAI,OAAO,KAAK;AAC3C,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI;AAAA,IACxB,QAAQ,OAAO,EAAE,uCAAuC,wCAAwC;AAAA,EAClG;AAEA,QAAM,eAAe,YAAY,YAAY;AAC3C,QAAI,CAAC,MAAO;AACZ,kBAAc,IAAI;AAClB,aAAS,IAAI;AACb,QAAI;AACF,YAAM,SAAS,MAAM,QAAyC,4CAA4C;AAAA,QACxG,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,aAAa;AAAA,QACb,MAAM,KAAK,UAAU,EAAE,MAAM,CAAC;AAAA,MAChC,CAAC;AACD,UAAI,OAAO,MAAM,OAAO,QAAQ,IAAI;AAClC,8BAAsB,IAAI,OAAO,mBAAmB;AACpD;AAAA,MACF;AACA,eAAS,EAAE,uCAAuC,iEAAiE,CAAC;AAAA,IACtH,QAAQ;AACN,eAAS,EAAE,kCAAkC,mCAAmC,CAAC;AAAA,IACnF,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,OAAO,SAAS,CAAC,CAAC;AAEtB,SACE,qBAAC,SAAI,WAAU,qDACb;AAAA,yBAAC,SAAI,WAAU,eACb;AAAA,0BAAC,QAAG,WAAU,qCAAqC,YAAE,0BAA0B,qBAAqB,GAAE;AAAA,MACtG,oBAAC,OAAE,WAAU,wCACV,YAAE,gCAAgC,kEAAkE,GACvG;AAAA,OACF;AAAA,IACC,QACC,oBAAC,SAAM,QAAO,SACZ,8BAAC,oBAAkB,iBAAM,GAC3B,IACE;AAAA,IACH,QACC,qBAAC,UAAO,MAAK,UAAS,SAAS,MAAM,KAAK,aAAa,GAAG,UAAU,YAAY,WAAU,qBACvF;AAAA,mBAAa,oBAAC,WAAQ,IAAK;AAAA,MAC3B,EAAE,2BAA2B,SAAS;AAAA,OACzC,IACE;AAAA,IACJ,oBAAC,UAAO,SAAO,MAAC,SAAQ,WAAU,WAAU,cAC1C,8BAAC,QAAK,MAAM,IAAI,OAAO,iBAAkB,YAAE,gCAAgC,iBAAiB,GAAE,GAChG;AAAA,KACF;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,11 @@
1
+ const metadata = {
2
+ titleKey: "portal.title",
3
+ title: "Sign In",
4
+ navHidden: true
5
+ };
6
+ var page_meta_default = metadata;
7
+ export {
8
+ page_meta_default as default,
9
+ metadata
10
+ };
11
+ //# sourceMappingURL=page.meta.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../../../src/modules/portal/frontend/%5BorgSlug%5D/portal/magic-link/page.meta.ts"],
4
+ "sourcesContent": ["import type { PageMetadata } from '@open-mercato/shared/modules/registry'\n\nexport const metadata: PageMetadata = {\n titleKey: 'portal.title',\n title: 'Sign In',\n navHidden: true,\n}\n\nexport default metadata\n"],
5
+ "mappings": "AAEO,MAAM,WAAyB;AAAA,EACpC,UAAU;AAAA,EACV,OAAO;AAAA,EACP,WAAW;AACb;AAEA,IAAO,oBAAQ;",
6
+ "names": []
7
+ }