@open-mercato/core 0.6.7-develop.6696.1.4236709f7e → 0.6.7-develop.6706.1.b3a4c759bb
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/modules/auth/frontend/login.js +4 -1
- package/dist/modules/auth/frontend/login.js.map +2 -2
- package/dist/modules/customers/api/companies/[id]/route.js +3 -4
- package/dist/modules/customers/api/companies/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js +3 -2
- package/dist/modules/customers/api/people/[id]/companies/enriched/route.js.map +2 -2
- package/dist/modules/customers/components/detail/ActiveDealCard.js +2 -1
- package/dist/modules/customers/components/detail/ActiveDealCard.js.map +2 -2
- package/dist/modules/customers/components/detail/CompanyKpiBar.js +6 -5
- package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
- package/dist/modules/customers/components/detail/dashboard/helpers.js +4 -3
- package/dist/modules/customers/components/detail/dashboard/helpers.js.map +2 -2
- package/dist/modules/customers/lib/dealStatus.js +38 -0
- package/dist/modules/customers/lib/dealStatus.js.map +7 -0
- package/dist/modules/payment_gateways/api/sessions/route.js +5 -0
- package/dist/modules/payment_gateways/api/sessions/route.js.map +2 -2
- package/dist/modules/payment_gateways/di.js +17 -1
- package/dist/modules/payment_gateways/di.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +8 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js +58 -0
- package/dist/modules/payment_gateways/lib/order-amount-reconciliation.js.map +7 -0
- package/dist/modules/sales/di.js +7 -0
- package/dist/modules/sales/di.js.map +2 -2
- package/dist/modules/sales/services/paymentOrderTotalResolver.js +52 -0
- package/dist/modules/sales/services/paymentOrderTotalResolver.js.map +7 -0
- package/package.json +7 -7
- package/src/modules/auth/frontend/login.tsx +4 -1
- package/src/modules/customers/api/companies/[id]/route.ts +3 -4
- package/src/modules/customers/api/people/[id]/companies/enriched/route.ts +3 -2
- package/src/modules/customers/components/detail/ActiveDealCard.tsx +2 -1
- package/src/modules/customers/components/detail/CompanyKpiBar.tsx +6 -5
- package/src/modules/customers/components/detail/dashboard/helpers.ts +4 -3
- package/src/modules/customers/lib/dealStatus.ts +48 -0
- package/src/modules/payment_gateways/api/sessions/route.ts +5 -0
- package/src/modules/payment_gateways/di.ts +28 -2
- package/src/modules/payment_gateways/i18n/de.json +3 -0
- package/src/modules/payment_gateways/i18n/en.json +3 -0
- package/src/modules/payment_gateways/i18n/es.json +3 -0
- package/src/modules/payment_gateways/i18n/pl.json +3 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +14 -0
- package/src/modules/payment_gateways/lib/order-amount-reconciliation.ts +93 -0
- package/src/modules/sales/di.ts +9 -0
- package/src/modules/sales/services/paymentOrderTotalResolver.ts +67 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/payment_gateways/lib/gateway-service.ts"],
|
|
4
|
-
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n getGatewayAdapter,\n type CreateSessionInput,\n type CreateSessionResult,\n type CaptureResult,\n type RefundResult,\n type CancelResult,\n type GatewayAdapter,\n type GatewayPaymentStatus,\n type PaymentGatewayPresentationRequest,\n type UnifiedPaymentStatus,\n} from '@open-mercato/shared/modules/payment_gateways/types'\nimport type { CredentialsService } from '../../integrations/lib/credentials-service'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { IntegrationLogService } from '../../integrations/lib/log-service'\nimport { conflict } from '@open-mercato/shared/lib/crud/errors'\nimport { GatewayPaymentOperation, GatewaySessionInitialization, GatewayTransaction } from '../data/entities'\nimport { canApplyManualAction, isValidTransition, type ManualGatewayAction } from './status-machine'\nimport { emitPaymentGatewayEvent } from '../events'\nimport { readGatewayMetadata, readWebhookLog } from './transaction-fields'\nimport {\n alignCapturedAmountWithStatus,\n assertCaptureWithinRemaining,\n formatAmountUnits,\n parseAmountUnits,\n releaseCaptureAmount,\n reserveCaptureAmount,\n settleCapturedAmount,\n} from './capture-ledger'\nimport {\n completePaymentOperation,\n failPaymentOperation,\n preparePaymentOperation,\n} from './payment-operation-idempotency'\nimport {\n buildPaymentSessionOperationKey,\n claimPaymentSessionInitialization,\n findPaymentSessionInitialization,\n reclaimPaymentSessionInitialization,\n refreshPaymentSessionInitialization,\n releasePaymentSessionInitialization,\n} from './session-idempotency'\n\nconst PAYMENT_SESSION_CLAIM_STALE_MS = 30_000\nconst PAYMENT_SESSION_WAIT_INTERVAL_MS = 25\n\nfunction assertManualActionAllowed(action: ManualGatewayAction, transaction: GatewayTransaction): void {\n const current = transaction.unifiedStatus as UnifiedPaymentStatus\n if (!canApplyManualAction(action, current)) {\n throw conflict(`Cannot ${action} a payment in status \"${current}\"`)\n }\n}\n\nfunction applyAdapterResultStatus(\n action: ManualGatewayAction,\n transaction: GatewayTransaction,\n resultStatus: UnifiedPaymentStatus,\n): boolean {\n const current = transaction.unifiedStatus as UnifiedPaymentStatus\n if (resultStatus === current) return false\n if (isValidTransition(resultStatus, current)) return false\n if (!isValidTransition(current, resultStatus)) {\n throw conflict(\n `Gateway returned status \"${resultStatus}\" which is not a valid transition from \"${current}\" for ${action}`,\n )\n }\n transaction.unifiedStatus = resultStatus\n return true\n}\n\nexport interface PaymentGatewayServiceDeps {\n em: EntityManager\n integrationCredentialsService: CredentialsService\n integrationStateService?: IntegrationStateService\n integrationLogService?: IntegrationLogService\n sessionClaimOptions?: {\n staleAfterMs?: number\n heartbeatIntervalMs?: number\n pollIntervalMs?: number\n }\n}\n\nexport interface CreatePaymentSessionInput {\n providerKey: string\n paymentId: string\n idempotencyKey?: string\n orderId?: string\n amount: number\n currencyCode: string\n captureMethod?: 'automatic' | 'manual'\n paymentTypes?: string[]\n description?: string\n successUrl?: string\n cancelUrl?: string\n metadata?: Record<string, unknown>\n presentation?: PaymentGatewayPresentationRequest\n organizationId: string\n tenantId: string\n}\n\nexport function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {\n const { em, integrationCredentialsService, integrationLogService } = deps\n const claimStaleAfterMs = Math.max(1, deps.sessionClaimOptions?.staleAfterMs ?? PAYMENT_SESSION_CLAIM_STALE_MS)\n const claimHeartbeatIntervalMs = Math.max(\n 1,\n deps.sessionClaimOptions?.heartbeatIntervalMs ?? Math.floor(claimStaleAfterMs / 3),\n )\n const claimPollIntervalMs = Math.max(1, deps.sessionClaimOptions?.pollIntervalMs ?? PAYMENT_SESSION_WAIT_INTERVAL_MS)\n\n async function findTransactionOrThrow(\n transactionId: string,\n scope: { organizationId: string; tenantId: string },\n targetEm: EntityManager = em,\n ): Promise<GatewayTransaction> {\n const transaction = await findOneWithDecryption(\n targetEm,\n GatewayTransaction,\n {\n id: transactionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!transaction) {\n throw new Error('Transaction not found')\n }\n return transaction\n }\n\n function readProviderSessionId(transaction: GatewayTransaction): string {\n if (typeof transaction.providerSessionId === 'string' && transaction.providerSessionId.trim().length > 0) {\n return transaction.providerSessionId\n }\n throw new Error('Transaction is missing provider session id')\n }\n\n async function emitStatusEvent(status: UnifiedPaymentStatus, payload: Record<string, unknown>) {\n type PaymentGatewayEventId = Parameters<typeof emitPaymentGatewayEvent>[0]\n const eventMap: Partial<Record<UnifiedPaymentStatus, PaymentGatewayEventId>> = {\n authorized: 'payment_gateways.payment.authorized',\n captured: 'payment_gateways.payment.captured',\n failed: 'payment_gateways.payment.failed',\n refunded: 'payment_gateways.payment.refunded',\n cancelled: 'payment_gateways.payment.cancelled',\n }\n const eventId = eventMap[status]\n if (!eventId) return\n await emitPaymentGatewayEvent(eventId, payload)\n }\n\n async function writeTransactionLog(\n providerKey: string,\n scope: { organizationId: string; tenantId: string },\n transactionId: string,\n level: 'info' | 'warn' | 'error',\n message: string,\n payload?: Record<string, unknown> | null,\n code?: string | null,\n ) {\n if (!integrationLogService) return\n await integrationLogService.write({\n integrationId: `gateway_${providerKey}`,\n scopeEntityType: 'payment_transaction',\n scopeEntityId: transactionId,\n level,\n message,\n code,\n payload: payload ?? null,\n }, scope)\n }\n\n async function resolveAdapterAndCredentials(providerKey: string, scope: { organizationId: string; tenantId: string }) {\n const integrationId = `gateway_${providerKey}`\n const selectedVersion = deps.integrationStateService\n ? await deps.integrationStateService.resolveApiVersion(integrationId, scope)\n : undefined\n const adapter = getGatewayAdapter(providerKey, selectedVersion)\n if (!adapter) {\n throw new Error(\n selectedVersion\n ? `No gateway adapter registered for provider: ${providerKey} (version: ${selectedVersion})`\n : `No gateway adapter registered for provider: ${providerKey}`,\n )\n }\n const credentials = await integrationCredentialsService.resolve(integrationId, scope) ?? {}\n\n return { adapter, credentials }\n }\n\n type ManualOperationResult = CaptureResult | RefundResult | CancelResult\n\n async function executeManualOperation<TResult extends ManualOperationResult>(input: {\n action: ManualGatewayAction\n transactionId: string\n operationId?: string\n payload: Record<string, unknown>\n scope: { organizationId: string; tenantId: string }\n assertInitialAllowed?: (transaction: GatewayTransaction) => void\n beforeInvoke?: (context: { transaction: GatewayTransaction; operation: GatewayPaymentOperation }) => Promise<void>\n releaseOnFailure?: (context: {\n transaction: GatewayTransaction\n operation: GatewayPaymentOperation\n providerInvoked: boolean\n }) => Promise<void>\n invoke: (context: {\n adapter: GatewayAdapter\n credentials: Record<string, unknown>\n transaction: GatewayTransaction\n idempotencyKey: string\n }) => Promise<TResult>\n applyResult: (transaction: GatewayTransaction, result: TResult) => void\n afterCommit: (transaction: GatewayTransaction, result: TResult) => Promise<void>\n }): Promise<TResult> {\n const transaction = await findTransactionOrThrow(input.transactionId, input.scope)\n const prepared = await preparePaymentOperation({\n em,\n transactionId: transaction.id,\n providerKey: transaction.providerKey,\n action: input.action,\n operationId: input.operationId,\n payload: input.payload,\n scope: input.scope,\n assertInitialAllowed: () => {\n assertManualActionAllowed(input.action, transaction)\n input.assertInitialAllowed?.(transaction)\n },\n })\n if (prepared.kind === 'completed') {\n if (prepared.result.status !== transaction.unifiedStatus) {\n throw conflict(\n `Cannot replay ${input.action} because the payment status has changed since the operation completed`,\n )\n }\n return prepared.result as unknown as TResult\n }\n\n const operation = prepared.claim.record\n // Everything after this flips to true is post-settlement: the provider moved the money and the\n // completion transaction committed it. A later event or logging failure must not undo either,\n // so the failure path below only runs while the operation can still be rolled back.\n let settled = false\n // Flips once the provider call returned, so the failure path can tell \"the provider never\n // moved the money\" from \"the provider moved it and only our bookkeeping failed\" \u2014 the two\n // cases need opposite rollback decisions.\n let providerInvoked = false\n try {\n await input.beforeInvoke?.({ transaction, operation })\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n const result = await input.invoke({\n adapter,\n credentials,\n transaction,\n idempotencyKey: prepared.claim.providerIdempotencyKey,\n })\n providerInvoked = true\n const statusChanged = await completePaymentOperation(\n em,\n prepared.claim,\n result as unknown as Record<string, unknown>,\n async (tx) => {\n const current = await findTransactionOrThrow(input.transactionId, input.scope, tx)\n const changed = applyAdapterResultStatus(input.action, current, result.status)\n input.applyResult(current, result)\n return changed\n },\n )\n settled = true\n if (statusChanged) {\n await emitStatusEvent(result.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await input.afterCommit(transaction, result)\n return result\n } catch (error: unknown) {\n if (!settled) {\n if (input.releaseOnFailure) {\n await input.releaseOnFailure({ transaction, operation, providerInvoked }).catch(() => undefined)\n }\n await Promise.allSettled([failPaymentOperation(em, prepared.claim)])\n }\n throw error\n }\n }\n\n function createGatewayTransaction(\n manager: EntityManager,\n input: CreatePaymentSessionInput,\n session: CreateSessionResult,\n id?: string,\n ): GatewayTransaction {\n const data = {\n paymentId: input.paymentId,\n providerKey: input.providerKey,\n providerSessionId: session.sessionId,\n unifiedStatus: session.status,\n redirectUrl: session.redirectUrl\n ?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null),\n clientSecret: session.clientSecret ?? null,\n amount: String(input.amount),\n // An automatic-capture session comes back already captured, so the ledger has to start\n // at the full amount \u2014 otherwise a later manual capture would look like the first one.\n capturedAmount: session.status === 'captured' ? String(input.amount) : '0',\n currencyCode: input.currencyCode,\n gatewayMetadata: {\n ...(session.providerData ?? {}),\n ...(session.clientSession ? { clientSession: session.clientSession } : {}),\n },\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n deletedAt: null,\n }\n return id\n ? manager.create(GatewayTransaction, { id, ...data })\n : manager.create(GatewayTransaction, data)\n }\n\n function restoreSession(transaction: GatewayTransaction): CreateSessionResult {\n const metadata = readGatewayMetadata(transaction.gatewayMetadata)\n const { clientSession, ...providerData } = metadata\n return {\n sessionId: readProviderSessionId(transaction),\n status: transaction.unifiedStatus as UnifiedPaymentStatus,\n ...(transaction.clientSecret ? { clientSecret: transaction.clientSecret } : {}),\n ...(transaction.redirectUrl ? { redirectUrl: transaction.redirectUrl } : {}),\n ...(Object.keys(providerData).length > 0 ? { providerData } : {}),\n ...(clientSession && typeof clientSession === 'object'\n ? { clientSession: clientSession as CreateSessionResult['clientSession'] }\n : {}),\n }\n }\n\n async function recordCreatedSession(\n transaction: GatewayTransaction,\n input: CreatePaymentSessionInput,\n scope: { organizationId: string; tenantId: string },\n ): Promise<void> {\n await emitPaymentGatewayEvent('payment_gateways.session.created', {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n status: transaction.unifiedStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n await writeTransactionLog(\n transaction.providerKey,\n scope,\n transaction.id,\n 'info',\n 'Payment session created',\n {\n paymentId: transaction.paymentId,\n providerSessionId: transaction.providerSessionId,\n status: transaction.unifiedStatus,\n amount: input.amount,\n currencyCode: input.currencyCode,\n },\n )\n }\n\n function startClaimHeartbeat(\n ownership: { id: string; claimToken: string },\n scope: { organizationId: string; tenantId: string },\n ): () => Promise<void> {\n let stopped = false\n let timer: ReturnType<typeof setTimeout> | null = null\n let pendingRefresh = Promise.resolve()\n\n const schedule = () => {\n timer = setTimeout(() => {\n pendingRefresh = refreshPaymentSessionInitialization(em, ownership, scope, new Date())\n .then(\n (refreshed) => {\n if (refreshed && !stopped) schedule()\n },\n () => {\n if (!stopped) schedule()\n },\n )\n }, claimHeartbeatIntervalMs)\n }\n schedule()\n\n return async () => {\n stopped = true\n if (timer) clearTimeout(timer)\n await pendingRefresh\n }\n }\n\n return {\n async createPaymentSession(input: CreatePaymentSessionInput): Promise<{ transaction: GatewayTransaction; session: CreateSessionResult }> {\n const scope = { organizationId: input.organizationId, tenantId: input.tenantId }\n const { adapter, credentials } = await resolveAdapterAndCredentials(input.providerKey, scope)\n\n const sessionInput: CreateSessionInput = {\n paymentId: input.paymentId,\n orderId: input.orderId,\n idempotencyKey: input.idempotencyKey,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n amount: input.amount,\n currencyCode: input.currencyCode,\n captureMethod: input.captureMethod,\n paymentTypes: input.paymentTypes,\n description: input.description,\n successUrl: input.successUrl,\n cancelUrl: input.cancelUrl,\n metadata: input.metadata,\n presentation: input.presentation,\n credentials,\n }\n\n if (!input.idempotencyKey) {\n const session = await adapter.createSession(sessionInput)\n const transaction = createGatewayTransaction(em, input, session)\n await em.persist(transaction).flush()\n await recordCreatedSession(transaction, input, scope)\n return { transaction, session }\n }\n\n const operationKey = buildPaymentSessionOperationKey({\n idempotencyKey: input.idempotencyKey,\n paymentId: input.paymentId,\n providerKey: input.providerKey,\n scope,\n })\n while (true) {\n const existing = await findPaymentSessionInitialization(em, operationKey, input.providerKey, scope)\n if (existing?.gatewayTransactionId) {\n const transaction = await findOneWithDecryption(\n em.fork(),\n GatewayTransaction,\n {\n id: existing.gatewayTransactionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!transaction) {\n throw new Error('Completed payment session is missing its gateway transaction')\n }\n return { transaction, session: restoreSession(transaction) }\n }\n\n const claimedAt = new Date()\n const staleBefore = new Date(claimedAt.getTime() - claimStaleAfterMs)\n const ownership = existing\n ? await reclaimPaymentSessionInitialization(em, existing, scope, claimedAt, staleBefore)\n : await claimPaymentSessionInitialization(em, operationKey, input.providerKey, scope, claimedAt)\n\n if (!ownership) {\n await new Promise((resolve) => setTimeout(resolve, claimPollIntervalMs))\n continue\n }\n\n let session: CreateSessionResult\n const stopHeartbeat = startClaimHeartbeat(ownership, scope)\n try {\n session = await adapter.createSession({ ...sessionInput, idempotencyKey: operationKey })\n } catch (error) {\n await releasePaymentSessionInitialization(em, ownership, scope).catch(() => undefined)\n throw error\n } finally {\n await stopHeartbeat()\n }\n\n const transactionId = randomUUID()\n const transaction = await em.fork().transactional(async (transactionEm) => {\n const finalizedRows = await transactionEm.nativeUpdate(\n GatewaySessionInitialization,\n {\n id: ownership.id,\n claimToken: ownership.claimToken,\n gatewayTransactionId: null,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n {\n gatewayTransactionId: transactionId,\n claimToken: null,\n claimedAt: null,\n updatedAt: new Date(),\n },\n )\n if (finalizedRows === 0) return null\n const created = createGatewayTransaction(transactionEm, input, session, transactionId)\n await transactionEm.persist(created).flush()\n return created\n })\n\n if (!transaction) {\n continue\n }\n await recordCreatedSession(transaction, input, scope)\n return { transaction, session }\n }\n },\n\n async capturePayment(\n transactionId: string,\n amount: number | undefined,\n scope: { organizationId: string; tenantId: string },\n operationId?: string,\n ): Promise<CaptureResult> {\n let reservedUnits: bigint | null = null\n let providerAmount = amount\n return executeManualOperation({\n action: 'capture',\n transactionId,\n operationId,\n payload: { amount: amount ?? null },\n scope,\n assertInitialAllowed: (transaction) => { assertCaptureWithinRemaining(transaction, amount) },\n beforeInvoke: async ({ transaction, operation }) => {\n const alreadyCapturedUnits = parseAmountUnits(transaction.capturedAmount)\n reservedUnits = await reserveCaptureAmount(em, { transaction, operation, amount, scope })\n // \"Capture the rest\" has to name the remaining amount once part of the authorization is\n // already captured, otherwise the provider would capture the full amount a second time.\n if (amount === undefined && alreadyCapturedUnits > 0n) {\n providerAmount = Number(formatAmountUnits(reservedUnits))\n }\n },\n // The slice goes back only while the provider has not been called yet. Once the capture\n // call returned, the money may already have moved and only the completion transaction\n // failed, so releasing would reopen the authorization for a fresh operation id and allow an\n // over-capture. Holding the reservation is the conservative outcome: a retry of this same\n // operation id reuses both the reservation and the provider idempotency key, so the provider\n // itself collapses the duplicate.\n releaseOnFailure: async ({ transaction, operation, providerInvoked }) => {\n if (reservedUnits === null) return\n if (providerInvoked) {\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'warn',\n 'Capture reservation stays outstanding because the provider call already returned',\n { operationId: operation.operationId, reservedAmount: formatAmountUnits(reservedUnits) },\n )\n return\n }\n const released = await releaseCaptureAmount(em, { transactionId, operation, reservedUnits, scope })\n if (released) return\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'warn',\n 'Capture reservation stays outstanding after a failed capture',\n { operationId: operation.operationId, reservedAmount: formatAmountUnits(reservedUnits) },\n )\n },\n invoke: ({ adapter, credentials, transaction, idempotencyKey }) => adapter.capture({\n sessionId: readProviderSessionId(transaction),\n amount: providerAmount,\n credentials,\n idempotencyKey,\n }),\n applyResult: (transaction, result) => {\n transaction.gatewayMetadata = {\n ...readGatewayMetadata(transaction.gatewayMetadata),\n captureResult: result.providerData,\n }\n if (reservedUnits !== null) settleCapturedAmount(transaction, reservedUnits, result.capturedAmount)\n },\n afterCommit: (transaction, result) => writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment captured',\n { amount: amount ?? null, status: result.status, capturedAmount: result.capturedAmount },\n ),\n })\n },\n\n async refundPayment(\n transactionId: string,\n amount: number | undefined,\n reason: string | undefined,\n scope: { organizationId: string; tenantId: string },\n operationId?: string,\n ): Promise<RefundResult> {\n return executeManualOperation({\n action: 'refund',\n transactionId,\n operationId,\n payload: { amount: amount ?? null, reason: reason ?? null },\n scope,\n invoke: ({ adapter, credentials, transaction, idempotencyKey }) => adapter.refund({\n sessionId: readProviderSessionId(transaction),\n amount,\n reason,\n credentials,\n idempotencyKey,\n }),\n applyResult: (transaction, result) => {\n transaction.gatewayRefundId = result.refundId\n transaction.gatewayMetadata = {\n ...readGatewayMetadata(transaction.gatewayMetadata),\n refundResult: result.providerData,\n }\n },\n afterCommit: (transaction, result) => writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment refunded',\n { amount: amount ?? null, reason: reason ?? null, status: result.status, refundId: result.refundId },\n ),\n })\n },\n\n async cancelPayment(\n transactionId: string,\n reason: string | undefined,\n scope: { organizationId: string; tenantId: string },\n operationId?: string,\n ): Promise<CancelResult> {\n return executeManualOperation({\n action: 'cancel',\n transactionId,\n operationId,\n payload: { reason: reason ?? null },\n scope,\n invoke: ({ adapter, credentials, transaction, idempotencyKey }) => adapter.cancel({\n sessionId: readProviderSessionId(transaction),\n reason,\n credentials,\n idempotencyKey,\n }),\n applyResult: () => {},\n afterCommit: (transaction, result) => writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment cancelled',\n { reason: reason ?? null, status: result.status },\n ),\n })\n },\n\n async getPaymentStatus(transactionId: string, scope: { organizationId: string; tenantId: string }): Promise<GatewayPaymentStatus> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const status = await adapter.getStatus({\n sessionId: readProviderSessionId(transaction),\n credentials,\n })\n\n if (status.status !== transaction.unifiedStatus && isValidTransition(transaction.unifiedStatus as UnifiedPaymentStatus, status.status)) {\n const previousStatus = transaction.unifiedStatus\n transaction.unifiedStatus = status.status\n alignCapturedAmountWithStatus(transaction, status.status)\n transaction.gatewayStatus = status.status\n transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), statusResult: status.providerData ?? null }\n transaction.lastPolledAt = new Date()\n await em.flush()\n await emitStatusEvent(status.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n previousStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment status updated by poller',\n {\n previousStatus,\n nextStatus: status.status,\n },\n )\n }\n\n return status\n },\n\n async syncTransactionStatus(transactionId: string, update: {\n unifiedStatus: UnifiedPaymentStatus\n providerStatus?: string\n providerData?: Record<string, unknown>\n webhookEvent?: {\n eventType: string\n idempotencyKey: string\n processed: boolean\n receivedAt?: string\n }\n }, scope: { organizationId: string; tenantId: string }): Promise<void> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n const currentStatus = transaction.unifiedStatus as UnifiedPaymentStatus\n const canTransition = isValidTransition(currentStatus, update.unifiedStatus)\n const shouldApplyStatus = canTransition && update.unifiedStatus !== currentStatus\n const previousStatus = transaction.unifiedStatus\n if (shouldApplyStatus) {\n transaction.unifiedStatus = update.unifiedStatus\n alignCapturedAmountWithStatus(transaction, update.unifiedStatus)\n }\n if (update.providerStatus) {\n transaction.gatewayStatus = update.providerStatus\n }\n if (update.providerData) {\n transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), ...update.providerData }\n }\n if (update.webhookEvent) {\n const webhookLog = readWebhookLog(transaction.webhookLog)\n webhookLog.push({\n eventType: update.webhookEvent.eventType,\n receivedAt: update.webhookEvent.receivedAt ?? new Date().toISOString(),\n idempotencyKey: update.webhookEvent.idempotencyKey,\n unifiedStatus: update.unifiedStatus,\n processed: update.webhookEvent.processed,\n })\n transaction.webhookLog = webhookLog\n }\n transaction.lastWebhookAt = new Date()\n await em.flush()\n if (shouldApplyStatus) {\n await emitStatusEvent(update.unifiedStatus, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n previousStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n shouldApplyStatus ? 'info' : 'warn',\n shouldApplyStatus ? 'Payment status synchronized from webhook' : 'Webhook received with no status transition',\n {\n previousStatus,\n nextStatus: update.unifiedStatus,\n providerStatus: update.providerStatus ?? null,\n eventType: update.webhookEvent?.eventType ?? null,\n idempotencyKey: update.webhookEvent?.idempotencyKey ?? null,\n },\n )\n },\n\n async findTransaction(id: string, scope: { organizationId: string; tenantId: string }): Promise<GatewayTransaction | null> {\n return findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async findTransactionBySessionId(\n providerSessionId: string,\n scope: { organizationId: string; tenantId: string },\n providerKey?: string,\n ): Promise<GatewayTransaction | null> {\n return findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n providerSessionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n ...(providerKey ? { providerKey } : {}),\n },\n undefined,\n scope,\n )\n },\n\n async listTransactionsForStatusPolling(scope?: {\n organizationId?: string\n tenantId?: string\n providerKey?: string\n limit?: number\n }): Promise<GatewayTransaction[]> {\n const where: Record<string, unknown> = {\n unifiedStatus: { $in: ['pending', 'authorized', 'partially_captured'] },\n deletedAt: null,\n }\n if (scope?.organizationId) where.organizationId = scope.organizationId\n if (scope?.tenantId) where.tenantId = scope.tenantId\n if (scope?.providerKey) where.providerKey = scope.providerKey\n\n return findWithDecryption(\n em,\n GatewayTransaction,\n where,\n {\n orderBy: { updatedAt: 'asc' },\n limit: scope?.limit ?? 100,\n },\n scope,\n )\n },\n }\n}\n\nexport type PaymentGatewayService = ReturnType<typeof createPaymentGatewayService>\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { randomUUID } from 'node:crypto'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport {\n getGatewayAdapter,\n type CreateSessionInput,\n type CreateSessionResult,\n type CaptureResult,\n type RefundResult,\n type CancelResult,\n type GatewayAdapter,\n type GatewayPaymentStatus,\n type PaymentGatewayPresentationRequest,\n type PaymentOrderTotalResolver,\n type UnifiedPaymentStatus,\n} from '@open-mercato/shared/modules/payment_gateways/types'\nimport type { CredentialsService } from '../../integrations/lib/credentials-service'\nimport type { IntegrationStateService } from '../../integrations/lib/state-service'\nimport type { IntegrationLogService } from '../../integrations/lib/log-service'\nimport { conflict } from '@open-mercato/shared/lib/crud/errors'\nimport { GatewayPaymentOperation, GatewaySessionInitialization, GatewayTransaction } from '../data/entities'\nimport { canApplyManualAction, isValidTransition, type ManualGatewayAction } from './status-machine'\nimport { emitPaymentGatewayEvent } from '../events'\nimport { readGatewayMetadata, readWebhookLog } from './transaction-fields'\nimport { reconcileSessionAmountWithOrder } from './order-amount-reconciliation'\nimport {\n alignCapturedAmountWithStatus,\n assertCaptureWithinRemaining,\n formatAmountUnits,\n parseAmountUnits,\n releaseCaptureAmount,\n reserveCaptureAmount,\n settleCapturedAmount,\n} from './capture-ledger'\nimport {\n completePaymentOperation,\n failPaymentOperation,\n preparePaymentOperation,\n} from './payment-operation-idempotency'\nimport {\n buildPaymentSessionOperationKey,\n claimPaymentSessionInitialization,\n findPaymentSessionInitialization,\n reclaimPaymentSessionInitialization,\n refreshPaymentSessionInitialization,\n releasePaymentSessionInitialization,\n} from './session-idempotency'\n\nconst PAYMENT_SESSION_CLAIM_STALE_MS = 30_000\nconst PAYMENT_SESSION_WAIT_INTERVAL_MS = 25\n\nfunction assertManualActionAllowed(action: ManualGatewayAction, transaction: GatewayTransaction): void {\n const current = transaction.unifiedStatus as UnifiedPaymentStatus\n if (!canApplyManualAction(action, current)) {\n throw conflict(`Cannot ${action} a payment in status \"${current}\"`)\n }\n}\n\nfunction applyAdapterResultStatus(\n action: ManualGatewayAction,\n transaction: GatewayTransaction,\n resultStatus: UnifiedPaymentStatus,\n): boolean {\n const current = transaction.unifiedStatus as UnifiedPaymentStatus\n if (resultStatus === current) return false\n if (isValidTransition(resultStatus, current)) return false\n if (!isValidTransition(current, resultStatus)) {\n throw conflict(\n `Gateway returned status \"${resultStatus}\" which is not a valid transition from \"${current}\" for ${action}`,\n )\n }\n transaction.unifiedStatus = resultStatus\n return true\n}\n\nexport interface PaymentGatewayServiceDeps {\n em: EntityManager\n integrationCredentialsService: CredentialsService\n integrationStateService?: IntegrationStateService\n integrationLogService?: IntegrationLogService\n /**\n * Optional seam supplied by the module that owns orders (`sales` by default).\n * When absent, session amounts cannot be reconciled and are accepted as-is.\n */\n paymentOrderTotalResolver?: PaymentOrderTotalResolver | null\n sessionClaimOptions?: {\n staleAfterMs?: number\n heartbeatIntervalMs?: number\n pollIntervalMs?: number\n }\n}\n\nexport interface CreatePaymentSessionInput {\n providerKey: string\n paymentId: string\n idempotencyKey?: string\n orderId?: string\n amount: number\n currencyCode: string\n captureMethod?: 'automatic' | 'manual'\n paymentTypes?: string[]\n description?: string\n successUrl?: string\n cancelUrl?: string\n metadata?: Record<string, unknown>\n presentation?: PaymentGatewayPresentationRequest\n organizationId: string\n tenantId: string\n}\n\nexport function createPaymentGatewayService(deps: PaymentGatewayServiceDeps) {\n const { em, integrationCredentialsService, integrationLogService } = deps\n const claimStaleAfterMs = Math.max(1, deps.sessionClaimOptions?.staleAfterMs ?? PAYMENT_SESSION_CLAIM_STALE_MS)\n const claimHeartbeatIntervalMs = Math.max(\n 1,\n deps.sessionClaimOptions?.heartbeatIntervalMs ?? Math.floor(claimStaleAfterMs / 3),\n )\n const claimPollIntervalMs = Math.max(1, deps.sessionClaimOptions?.pollIntervalMs ?? PAYMENT_SESSION_WAIT_INTERVAL_MS)\n\n async function findTransactionOrThrow(\n transactionId: string,\n scope: { organizationId: string; tenantId: string },\n targetEm: EntityManager = em,\n ): Promise<GatewayTransaction> {\n const transaction = await findOneWithDecryption(\n targetEm,\n GatewayTransaction,\n {\n id: transactionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!transaction) {\n throw new Error('Transaction not found')\n }\n return transaction\n }\n\n function readProviderSessionId(transaction: GatewayTransaction): string {\n if (typeof transaction.providerSessionId === 'string' && transaction.providerSessionId.trim().length > 0) {\n return transaction.providerSessionId\n }\n throw new Error('Transaction is missing provider session id')\n }\n\n async function emitStatusEvent(status: UnifiedPaymentStatus, payload: Record<string, unknown>) {\n type PaymentGatewayEventId = Parameters<typeof emitPaymentGatewayEvent>[0]\n const eventMap: Partial<Record<UnifiedPaymentStatus, PaymentGatewayEventId>> = {\n authorized: 'payment_gateways.payment.authorized',\n captured: 'payment_gateways.payment.captured',\n failed: 'payment_gateways.payment.failed',\n refunded: 'payment_gateways.payment.refunded',\n cancelled: 'payment_gateways.payment.cancelled',\n }\n const eventId = eventMap[status]\n if (!eventId) return\n await emitPaymentGatewayEvent(eventId, payload)\n }\n\n async function writeTransactionLog(\n providerKey: string,\n scope: { organizationId: string; tenantId: string },\n transactionId: string,\n level: 'info' | 'warn' | 'error',\n message: string,\n payload?: Record<string, unknown> | null,\n code?: string | null,\n ) {\n if (!integrationLogService) return\n await integrationLogService.write({\n integrationId: `gateway_${providerKey}`,\n scopeEntityType: 'payment_transaction',\n scopeEntityId: transactionId,\n level,\n message,\n code,\n payload: payload ?? null,\n }, scope)\n }\n\n async function resolveAdapterAndCredentials(providerKey: string, scope: { organizationId: string; tenantId: string }) {\n const integrationId = `gateway_${providerKey}`\n const selectedVersion = deps.integrationStateService\n ? await deps.integrationStateService.resolveApiVersion(integrationId, scope)\n : undefined\n const adapter = getGatewayAdapter(providerKey, selectedVersion)\n if (!adapter) {\n throw new Error(\n selectedVersion\n ? `No gateway adapter registered for provider: ${providerKey} (version: ${selectedVersion})`\n : `No gateway adapter registered for provider: ${providerKey}`,\n )\n }\n const credentials = await integrationCredentialsService.resolve(integrationId, scope) ?? {}\n\n return { adapter, credentials }\n }\n\n type ManualOperationResult = CaptureResult | RefundResult | CancelResult\n\n async function executeManualOperation<TResult extends ManualOperationResult>(input: {\n action: ManualGatewayAction\n transactionId: string\n operationId?: string\n payload: Record<string, unknown>\n scope: { organizationId: string; tenantId: string }\n assertInitialAllowed?: (transaction: GatewayTransaction) => void\n beforeInvoke?: (context: { transaction: GatewayTransaction; operation: GatewayPaymentOperation }) => Promise<void>\n releaseOnFailure?: (context: {\n transaction: GatewayTransaction\n operation: GatewayPaymentOperation\n providerInvoked: boolean\n }) => Promise<void>\n invoke: (context: {\n adapter: GatewayAdapter\n credentials: Record<string, unknown>\n transaction: GatewayTransaction\n idempotencyKey: string\n }) => Promise<TResult>\n applyResult: (transaction: GatewayTransaction, result: TResult) => void\n afterCommit: (transaction: GatewayTransaction, result: TResult) => Promise<void>\n }): Promise<TResult> {\n const transaction = await findTransactionOrThrow(input.transactionId, input.scope)\n const prepared = await preparePaymentOperation({\n em,\n transactionId: transaction.id,\n providerKey: transaction.providerKey,\n action: input.action,\n operationId: input.operationId,\n payload: input.payload,\n scope: input.scope,\n assertInitialAllowed: () => {\n assertManualActionAllowed(input.action, transaction)\n input.assertInitialAllowed?.(transaction)\n },\n })\n if (prepared.kind === 'completed') {\n if (prepared.result.status !== transaction.unifiedStatus) {\n throw conflict(\n `Cannot replay ${input.action} because the payment status has changed since the operation completed`,\n )\n }\n return prepared.result as unknown as TResult\n }\n\n const operation = prepared.claim.record\n // Everything after this flips to true is post-settlement: the provider moved the money and the\n // completion transaction committed it. A later event or logging failure must not undo either,\n // so the failure path below only runs while the operation can still be rolled back.\n let settled = false\n // Flips once the provider call returned, so the failure path can tell \"the provider never\n // moved the money\" from \"the provider moved it and only our bookkeeping failed\" \u2014 the two\n // cases need opposite rollback decisions.\n let providerInvoked = false\n try {\n await input.beforeInvoke?.({ transaction, operation })\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n const result = await input.invoke({\n adapter,\n credentials,\n transaction,\n idempotencyKey: prepared.claim.providerIdempotencyKey,\n })\n providerInvoked = true\n const statusChanged = await completePaymentOperation(\n em,\n prepared.claim,\n result as unknown as Record<string, unknown>,\n async (tx) => {\n const current = await findTransactionOrThrow(input.transactionId, input.scope, tx)\n const changed = applyAdapterResultStatus(input.action, current, result.status)\n input.applyResult(current, result)\n return changed\n },\n )\n settled = true\n if (statusChanged) {\n await emitStatusEvent(result.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await input.afterCommit(transaction, result)\n return result\n } catch (error: unknown) {\n if (!settled) {\n if (input.releaseOnFailure) {\n await input.releaseOnFailure({ transaction, operation, providerInvoked }).catch(() => undefined)\n }\n await Promise.allSettled([failPaymentOperation(em, prepared.claim)])\n }\n throw error\n }\n }\n\n function createGatewayTransaction(\n manager: EntityManager,\n input: CreatePaymentSessionInput,\n session: CreateSessionResult,\n id?: string,\n ): GatewayTransaction {\n const data = {\n paymentId: input.paymentId,\n providerKey: input.providerKey,\n providerSessionId: session.sessionId,\n unifiedStatus: session.status,\n redirectUrl: session.redirectUrl\n ?? (session.clientSession?.type === 'redirect' ? session.clientSession.redirectUrl : null),\n clientSecret: session.clientSecret ?? null,\n amount: String(input.amount),\n // An automatic-capture session comes back already captured, so the ledger has to start\n // at the full amount \u2014 otherwise a later manual capture would look like the first one.\n capturedAmount: session.status === 'captured' ? String(input.amount) : '0',\n currencyCode: input.currencyCode,\n gatewayMetadata: {\n ...(session.providerData ?? {}),\n ...(session.clientSession ? { clientSession: session.clientSession } : {}),\n },\n organizationId: input.organizationId,\n tenantId: input.tenantId,\n deletedAt: null,\n }\n return id\n ? manager.create(GatewayTransaction, { id, ...data })\n : manager.create(GatewayTransaction, data)\n }\n\n function restoreSession(transaction: GatewayTransaction): CreateSessionResult {\n const metadata = readGatewayMetadata(transaction.gatewayMetadata)\n const { clientSession, ...providerData } = metadata\n return {\n sessionId: readProviderSessionId(transaction),\n status: transaction.unifiedStatus as UnifiedPaymentStatus,\n ...(transaction.clientSecret ? { clientSecret: transaction.clientSecret } : {}),\n ...(transaction.redirectUrl ? { redirectUrl: transaction.redirectUrl } : {}),\n ...(Object.keys(providerData).length > 0 ? { providerData } : {}),\n ...(clientSession && typeof clientSession === 'object'\n ? { clientSession: clientSession as CreateSessionResult['clientSession'] }\n : {}),\n }\n }\n\n async function recordCreatedSession(\n transaction: GatewayTransaction,\n input: CreatePaymentSessionInput,\n scope: { organizationId: string; tenantId: string },\n ): Promise<void> {\n await emitPaymentGatewayEvent('payment_gateways.session.created', {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n status: transaction.unifiedStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n await writeTransactionLog(\n transaction.providerKey,\n scope,\n transaction.id,\n 'info',\n 'Payment session created',\n {\n paymentId: transaction.paymentId,\n providerSessionId: transaction.providerSessionId,\n status: transaction.unifiedStatus,\n amount: input.amount,\n currencyCode: input.currencyCode,\n },\n )\n }\n\n function startClaimHeartbeat(\n ownership: { id: string; claimToken: string },\n scope: { organizationId: string; tenantId: string },\n ): () => Promise<void> {\n let stopped = false\n let timer: ReturnType<typeof setTimeout> | null = null\n let pendingRefresh = Promise.resolve()\n\n const schedule = () => {\n timer = setTimeout(() => {\n pendingRefresh = refreshPaymentSessionInitialization(em, ownership, scope, new Date())\n .then(\n (refreshed) => {\n if (refreshed && !stopped) schedule()\n },\n () => {\n if (!stopped) schedule()\n },\n )\n }, claimHeartbeatIntervalMs)\n }\n schedule()\n\n return async () => {\n stopped = true\n if (timer) clearTimeout(timer)\n await pendingRefresh\n }\n }\n\n return {\n async createPaymentSession(input: CreatePaymentSessionInput): Promise<{ transaction: GatewayTransaction; session: CreateSessionResult }> {\n const scope = { organizationId: input.organizationId, tenantId: input.tenantId }\n await reconcileSessionAmountWithOrder({\n orderId: input.orderId,\n amount: input.amount,\n currencyCode: input.currencyCode,\n scope,\n resolver: deps.paymentOrderTotalResolver,\n })\n const { adapter, credentials } = await resolveAdapterAndCredentials(input.providerKey, scope)\n\n const sessionInput: CreateSessionInput = {\n paymentId: input.paymentId,\n orderId: input.orderId,\n idempotencyKey: input.idempotencyKey,\n tenantId: input.tenantId,\n organizationId: input.organizationId,\n amount: input.amount,\n currencyCode: input.currencyCode,\n captureMethod: input.captureMethod,\n paymentTypes: input.paymentTypes,\n description: input.description,\n successUrl: input.successUrl,\n cancelUrl: input.cancelUrl,\n metadata: input.metadata,\n presentation: input.presentation,\n credentials,\n }\n\n if (!input.idempotencyKey) {\n const session = await adapter.createSession(sessionInput)\n const transaction = createGatewayTransaction(em, input, session)\n await em.persist(transaction).flush()\n await recordCreatedSession(transaction, input, scope)\n return { transaction, session }\n }\n\n const operationKey = buildPaymentSessionOperationKey({\n idempotencyKey: input.idempotencyKey,\n paymentId: input.paymentId,\n providerKey: input.providerKey,\n scope,\n })\n while (true) {\n const existing = await findPaymentSessionInitialization(em, operationKey, input.providerKey, scope)\n if (existing?.gatewayTransactionId) {\n const transaction = await findOneWithDecryption(\n em.fork(),\n GatewayTransaction,\n {\n id: existing.gatewayTransactionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n if (!transaction) {\n throw new Error('Completed payment session is missing its gateway transaction')\n }\n return { transaction, session: restoreSession(transaction) }\n }\n\n const claimedAt = new Date()\n const staleBefore = new Date(claimedAt.getTime() - claimStaleAfterMs)\n const ownership = existing\n ? await reclaimPaymentSessionInitialization(em, existing, scope, claimedAt, staleBefore)\n : await claimPaymentSessionInitialization(em, operationKey, input.providerKey, scope, claimedAt)\n\n if (!ownership) {\n await new Promise((resolve) => setTimeout(resolve, claimPollIntervalMs))\n continue\n }\n\n let session: CreateSessionResult\n const stopHeartbeat = startClaimHeartbeat(ownership, scope)\n try {\n session = await adapter.createSession({ ...sessionInput, idempotencyKey: operationKey })\n } catch (error) {\n await releasePaymentSessionInitialization(em, ownership, scope).catch(() => undefined)\n throw error\n } finally {\n await stopHeartbeat()\n }\n\n const transactionId = randomUUID()\n const transaction = await em.fork().transactional(async (transactionEm) => {\n const finalizedRows = await transactionEm.nativeUpdate(\n GatewaySessionInitialization,\n {\n id: ownership.id,\n claimToken: ownership.claimToken,\n gatewayTransactionId: null,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n },\n {\n gatewayTransactionId: transactionId,\n claimToken: null,\n claimedAt: null,\n updatedAt: new Date(),\n },\n )\n if (finalizedRows === 0) return null\n const created = createGatewayTransaction(transactionEm, input, session, transactionId)\n await transactionEm.persist(created).flush()\n return created\n })\n\n if (!transaction) {\n continue\n }\n await recordCreatedSession(transaction, input, scope)\n return { transaction, session }\n }\n },\n\n async capturePayment(\n transactionId: string,\n amount: number | undefined,\n scope: { organizationId: string; tenantId: string },\n operationId?: string,\n ): Promise<CaptureResult> {\n let reservedUnits: bigint | null = null\n let providerAmount = amount\n return executeManualOperation({\n action: 'capture',\n transactionId,\n operationId,\n payload: { amount: amount ?? null },\n scope,\n assertInitialAllowed: (transaction) => { assertCaptureWithinRemaining(transaction, amount) },\n beforeInvoke: async ({ transaction, operation }) => {\n const alreadyCapturedUnits = parseAmountUnits(transaction.capturedAmount)\n reservedUnits = await reserveCaptureAmount(em, { transaction, operation, amount, scope })\n // \"Capture the rest\" has to name the remaining amount once part of the authorization is\n // already captured, otherwise the provider would capture the full amount a second time.\n if (amount === undefined && alreadyCapturedUnits > 0n) {\n providerAmount = Number(formatAmountUnits(reservedUnits))\n }\n },\n // The slice goes back only while the provider has not been called yet. Once the capture\n // call returned, the money may already have moved and only the completion transaction\n // failed, so releasing would reopen the authorization for a fresh operation id and allow an\n // over-capture. Holding the reservation is the conservative outcome: a retry of this same\n // operation id reuses both the reservation and the provider idempotency key, so the provider\n // itself collapses the duplicate.\n releaseOnFailure: async ({ transaction, operation, providerInvoked }) => {\n if (reservedUnits === null) return\n if (providerInvoked) {\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'warn',\n 'Capture reservation stays outstanding because the provider call already returned',\n { operationId: operation.operationId, reservedAmount: formatAmountUnits(reservedUnits) },\n )\n return\n }\n const released = await releaseCaptureAmount(em, { transactionId, operation, reservedUnits, scope })\n if (released) return\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'warn',\n 'Capture reservation stays outstanding after a failed capture',\n { operationId: operation.operationId, reservedAmount: formatAmountUnits(reservedUnits) },\n )\n },\n invoke: ({ adapter, credentials, transaction, idempotencyKey }) => adapter.capture({\n sessionId: readProviderSessionId(transaction),\n amount: providerAmount,\n credentials,\n idempotencyKey,\n }),\n applyResult: (transaction, result) => {\n transaction.gatewayMetadata = {\n ...readGatewayMetadata(transaction.gatewayMetadata),\n captureResult: result.providerData,\n }\n if (reservedUnits !== null) settleCapturedAmount(transaction, reservedUnits, result.capturedAmount)\n },\n afterCommit: (transaction, result) => writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment captured',\n { amount: amount ?? null, status: result.status, capturedAmount: result.capturedAmount },\n ),\n })\n },\n\n async refundPayment(\n transactionId: string,\n amount: number | undefined,\n reason: string | undefined,\n scope: { organizationId: string; tenantId: string },\n operationId?: string,\n ): Promise<RefundResult> {\n return executeManualOperation({\n action: 'refund',\n transactionId,\n operationId,\n payload: { amount: amount ?? null, reason: reason ?? null },\n scope,\n invoke: ({ adapter, credentials, transaction, idempotencyKey }) => adapter.refund({\n sessionId: readProviderSessionId(transaction),\n amount,\n reason,\n credentials,\n idempotencyKey,\n }),\n applyResult: (transaction, result) => {\n transaction.gatewayRefundId = result.refundId\n transaction.gatewayMetadata = {\n ...readGatewayMetadata(transaction.gatewayMetadata),\n refundResult: result.providerData,\n }\n },\n afterCommit: (transaction, result) => writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment refunded',\n { amount: amount ?? null, reason: reason ?? null, status: result.status, refundId: result.refundId },\n ),\n })\n },\n\n async cancelPayment(\n transactionId: string,\n reason: string | undefined,\n scope: { organizationId: string; tenantId: string },\n operationId?: string,\n ): Promise<CancelResult> {\n return executeManualOperation({\n action: 'cancel',\n transactionId,\n operationId,\n payload: { reason: reason ?? null },\n scope,\n invoke: ({ adapter, credentials, transaction, idempotencyKey }) => adapter.cancel({\n sessionId: readProviderSessionId(transaction),\n reason,\n credentials,\n idempotencyKey,\n }),\n applyResult: () => {},\n afterCommit: (transaction, result) => writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment cancelled',\n { reason: reason ?? null, status: result.status },\n ),\n })\n },\n\n async getPaymentStatus(transactionId: string, scope: { organizationId: string; tenantId: string }): Promise<GatewayPaymentStatus> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n const { adapter, credentials } = await resolveAdapterAndCredentials(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n )\n\n const status = await adapter.getStatus({\n sessionId: readProviderSessionId(transaction),\n credentials,\n })\n\n if (status.status !== transaction.unifiedStatus && isValidTransition(transaction.unifiedStatus as UnifiedPaymentStatus, status.status)) {\n const previousStatus = transaction.unifiedStatus\n transaction.unifiedStatus = status.status\n alignCapturedAmountWithStatus(transaction, status.status)\n transaction.gatewayStatus = status.status\n transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), statusResult: status.providerData ?? null }\n transaction.lastPolledAt = new Date()\n await em.flush()\n await emitStatusEvent(status.status, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n previousStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n 'info',\n 'Payment status updated by poller',\n {\n previousStatus,\n nextStatus: status.status,\n },\n )\n }\n\n return status\n },\n\n async syncTransactionStatus(transactionId: string, update: {\n unifiedStatus: UnifiedPaymentStatus\n providerStatus?: string\n providerData?: Record<string, unknown>\n webhookEvent?: {\n eventType: string\n idempotencyKey: string\n processed: boolean\n receivedAt?: string\n }\n }, scope: { organizationId: string; tenantId: string }): Promise<void> {\n const transaction = await findTransactionOrThrow(transactionId, scope)\n const currentStatus = transaction.unifiedStatus as UnifiedPaymentStatus\n const canTransition = isValidTransition(currentStatus, update.unifiedStatus)\n const shouldApplyStatus = canTransition && update.unifiedStatus !== currentStatus\n const previousStatus = transaction.unifiedStatus\n if (shouldApplyStatus) {\n transaction.unifiedStatus = update.unifiedStatus\n alignCapturedAmountWithStatus(transaction, update.unifiedStatus)\n }\n if (update.providerStatus) {\n transaction.gatewayStatus = update.providerStatus\n }\n if (update.providerData) {\n transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), ...update.providerData }\n }\n if (update.webhookEvent) {\n const webhookLog = readWebhookLog(transaction.webhookLog)\n webhookLog.push({\n eventType: update.webhookEvent.eventType,\n receivedAt: update.webhookEvent.receivedAt ?? new Date().toISOString(),\n idempotencyKey: update.webhookEvent.idempotencyKey,\n unifiedStatus: update.unifiedStatus,\n processed: update.webhookEvent.processed,\n })\n transaction.webhookLog = webhookLog\n }\n transaction.lastWebhookAt = new Date()\n await em.flush()\n if (shouldApplyStatus) {\n await emitStatusEvent(update.unifiedStatus, {\n transactionId: transaction.id,\n paymentId: transaction.paymentId,\n providerKey: transaction.providerKey,\n previousStatus,\n organizationId: transaction.organizationId,\n tenantId: transaction.tenantId,\n })\n }\n await writeTransactionLog(\n transaction.providerKey,\n { organizationId: transaction.organizationId, tenantId: transaction.tenantId },\n transaction.id,\n shouldApplyStatus ? 'info' : 'warn',\n shouldApplyStatus ? 'Payment status synchronized from webhook' : 'Webhook received with no status transition',\n {\n previousStatus,\n nextStatus: update.unifiedStatus,\n providerStatus: update.providerStatus ?? null,\n eventType: update.webhookEvent?.eventType ?? null,\n idempotencyKey: update.webhookEvent?.idempotencyKey ?? null,\n },\n )\n },\n\n async findTransaction(id: string, scope: { organizationId: string; tenantId: string }): Promise<GatewayTransaction | null> {\n return findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n id,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n },\n undefined,\n scope,\n )\n },\n\n async findTransactionBySessionId(\n providerSessionId: string,\n scope: { organizationId: string; tenantId: string },\n providerKey?: string,\n ): Promise<GatewayTransaction | null> {\n return findOneWithDecryption(\n em,\n GatewayTransaction,\n {\n providerSessionId,\n organizationId: scope.organizationId,\n tenantId: scope.tenantId,\n deletedAt: null,\n ...(providerKey ? { providerKey } : {}),\n },\n undefined,\n scope,\n )\n },\n\n async listTransactionsForStatusPolling(scope?: {\n organizationId?: string\n tenantId?: string\n providerKey?: string\n limit?: number\n }): Promise<GatewayTransaction[]> {\n const where: Record<string, unknown> = {\n unifiedStatus: { $in: ['pending', 'authorized', 'partially_captured'] },\n deletedAt: null,\n }\n if (scope?.organizationId) where.organizationId = scope.organizationId\n if (scope?.tenantId) where.tenantId = scope.tenantId\n if (scope?.providerKey) where.providerKey = scope.providerKey\n\n return findWithDecryption(\n em,\n GatewayTransaction,\n where,\n {\n orderBy: { updatedAt: 'asc' },\n limit: scope?.limit ?? 100,\n },\n scope,\n )\n },\n }\n}\n\nexport type PaymentGatewayService = ReturnType<typeof createPaymentGatewayService>\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,kBAAkB;AAE3B,SAAS,uBAAuB,0BAA0B;AAC1D;AAAA,EACE;AAAA,OAWK;AAIP,SAAS,gBAAgB;AACzB,SAAkC,8BAA8B,0BAA0B;AAC1F,SAAS,sBAAsB,yBAAmD;AAClF,SAAS,+BAA+B;AACxC,SAAS,qBAAqB,sBAAsB;AACpD,SAAS,uCAAuC;AAChD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,iCAAiC;AACvC,MAAM,mCAAmC;AAEzC,SAAS,0BAA0B,QAA6B,aAAuC;AACrG,QAAM,UAAU,YAAY;AAC5B,MAAI,CAAC,qBAAqB,QAAQ,OAAO,GAAG;AAC1C,UAAM,SAAS,UAAU,MAAM,yBAAyB,OAAO,GAAG;AAAA,EACpE;AACF;AAEA,SAAS,yBACP,QACA,aACA,cACS;AACT,QAAM,UAAU,YAAY;AAC5B,MAAI,iBAAiB,QAAS,QAAO;AACrC,MAAI,kBAAkB,cAAc,OAAO,EAAG,QAAO;AACrD,MAAI,CAAC,kBAAkB,SAAS,YAAY,GAAG;AAC7C,UAAM;AAAA,MACJ,4BAA4B,YAAY,2CAA2C,OAAO,SAAS,MAAM;AAAA,IAC3G;AAAA,EACF;AACA,cAAY,gBAAgB;AAC5B,SAAO;AACT;AAqCO,SAAS,4BAA4B,MAAiC;AAC3E,QAAM,EAAE,IAAI,+BAA+B,sBAAsB,IAAI;AACrE,QAAM,oBAAoB,KAAK,IAAI,GAAG,KAAK,qBAAqB,gBAAgB,8BAA8B;AAC9G,QAAM,2BAA2B,KAAK;AAAA,IACpC;AAAA,IACA,KAAK,qBAAqB,uBAAuB,KAAK,MAAM,oBAAoB,CAAC;AAAA,EACnF;AACA,QAAM,sBAAsB,KAAK,IAAI,GAAG,KAAK,qBAAqB,kBAAkB,gCAAgC;AAEpH,iBAAe,uBACb,eACA,OACA,WAA0B,IACG;AAC7B,UAAM,cAAc,MAAM;AAAA,MACxB;AAAA,MACA;AAAA,MACA;AAAA,QACE,IAAI;AAAA,QACJ,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,QAChB,WAAW;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,QAAI,CAAC,aAAa;AAChB,YAAM,IAAI,MAAM,uBAAuB;AAAA,IACzC;AACA,WAAO;AAAA,EACT;AAEA,WAAS,sBAAsB,aAAyC;AACtE,QAAI,OAAO,YAAY,sBAAsB,YAAY,YAAY,kBAAkB,KAAK,EAAE,SAAS,GAAG;AACxG,aAAO,YAAY;AAAA,IACrB;AACA,UAAM,IAAI,MAAM,4CAA4C;AAAA,EAC9D;AAEA,iBAAe,gBAAgB,QAA8B,SAAkC;AAE7F,UAAM,WAAyE;AAAA,MAC7E,YAAY;AAAA,MACZ,UAAU;AAAA,MACV,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,WAAW;AAAA,IACb;AACA,UAAM,UAAU,SAAS,MAAM;AAC/B,QAAI,CAAC,QAAS;AACd,UAAM,wBAAwB,SAAS,OAAO;AAAA,EAChD;AAEA,iBAAe,oBACb,aACA,OACA,eACA,OACA,SACA,SACA,MACA;AACA,QAAI,CAAC,sBAAuB;AAC5B,UAAM,sBAAsB,MAAM;AAAA,MAChC,eAAe,WAAW,WAAW;AAAA,MACrC,iBAAiB;AAAA,MACjB,eAAe;AAAA,MACf;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,WAAW;AAAA,IACtB,GAAG,KAAK;AAAA,EACV;AAEA,iBAAe,6BAA6B,aAAqB,OAAqD;AACpH,UAAM,gBAAgB,WAAW,WAAW;AAC5C,UAAM,kBAAkB,KAAK,0BACzB,MAAM,KAAK,wBAAwB,kBAAkB,eAAe,KAAK,IACzE;AACJ,UAAM,UAAU,kBAAkB,aAAa,eAAe;AAC9D,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI;AAAA,QACR,kBACI,+CAA+C,WAAW,cAAc,eAAe,MACvF,+CAA+C,WAAW;AAAA,MAChE;AAAA,IACF;AACA,UAAM,cAAc,MAAM,8BAA8B,QAAQ,eAAe,KAAK,KAAK,CAAC;AAE1F,WAAO,EAAE,SAAS,YAAY;AAAA,EAChC;AAIA,iBAAe,uBAA8D,OAqBxD;AACnB,UAAM,cAAc,MAAM,uBAAuB,MAAM,eAAe,MAAM,KAAK;AACjF,UAAM,WAAW,MAAM,wBAAwB;AAAA,MAC7C;AAAA,MACA,eAAe,YAAY;AAAA,MAC3B,aAAa,YAAY;AAAA,MACzB,QAAQ,MAAM;AAAA,MACd,aAAa,MAAM;AAAA,MACnB,SAAS,MAAM;AAAA,MACf,OAAO,MAAM;AAAA,MACb,sBAAsB,MAAM;AAC1B,kCAA0B,MAAM,QAAQ,WAAW;AACnD,cAAM,uBAAuB,WAAW;AAAA,MAC1C;AAAA,IACF,CAAC;AACD,QAAI,SAAS,SAAS,aAAa;AACjC,UAAI,SAAS,OAAO,WAAW,YAAY,eAAe;AACxD,cAAM;AAAA,UACJ,iBAAiB,MAAM,MAAM;AAAA,QAC/B;AAAA,MACF;AACA,aAAO,SAAS;AAAA,IAClB;AAEA,UAAM,YAAY,SAAS,MAAM;AAIjC,QAAI,UAAU;AAId,QAAI,kBAAkB;AACtB,QAAI;AACF,YAAM,MAAM,eAAe,EAAE,aAAa,UAAU,CAAC;AACrD,YAAM,EAAE,SAAS,YAAY,IAAI,MAAM;AAAA,QACrC,YAAY;AAAA,QACZ,EAAE,gBAAgB,YAAY,gBAAgB,UAAU,YAAY,SAAS;AAAA,MAC/E;AACA,YAAM,SAAS,MAAM,MAAM,OAAO;AAAA,QAChC;AAAA,QACA;AAAA,QACA;AAAA,QACA,gBAAgB,SAAS,MAAM;AAAA,MACjC,CAAC;AACD,wBAAkB;AAClB,YAAM,gBAAgB,MAAM;AAAA,QAC1B;AAAA,QACA,SAAS;AAAA,QACT;AAAA,QACA,OAAO,OAAO;AACZ,gBAAM,UAAU,MAAM,uBAAuB,MAAM,eAAe,MAAM,OAAO,EAAE;AACjF,gBAAM,UAAU,yBAAyB,MAAM,QAAQ,SAAS,OAAO,MAAM;AAC7E,gBAAM,YAAY,SAAS,MAAM;AACjC,iBAAO;AAAA,QACT;AAAA,MACF;AACA,gBAAU;AACV,UAAI,eAAe;AACjB,cAAM,gBAAgB,OAAO,QAAQ;AAAA,UACnC,eAAe,YAAY;AAAA,UAC3B,WAAW,YAAY;AAAA,UACvB,aAAa,YAAY;AAAA,UACzB,gBAAgB,YAAY;AAAA,UAC5B,UAAU,YAAY;AAAA,QACxB,CAAC;AAAA,MACH;AACA,YAAM,MAAM,YAAY,aAAa,MAAM;AAC3C,aAAO;AAAA,IACT,SAAS,OAAgB;AACvB,UAAI,CAAC,SAAS;AACZ,YAAI,MAAM,kBAAkB;AAC1B,gBAAM,MAAM,iBAAiB,EAAE,aAAa,WAAW,gBAAgB,CAAC,EAAE,MAAM,MAAM,MAAS;AAAA,QACjG;AACA,cAAM,QAAQ,WAAW,CAAC,qBAAqB,IAAI,SAAS,KAAK,CAAC,CAAC;AAAA,MACrE;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAEA,WAAS,yBACP,SACA,OACA,SACA,IACoB;AACpB,UAAM,OAAO;AAAA,MACX,WAAW,MAAM;AAAA,MACjB,aAAa,MAAM;AAAA,MACnB,mBAAmB,QAAQ;AAAA,MAC3B,eAAe,QAAQ;AAAA,MACvB,aAAa,QAAQ,gBACf,QAAQ,eAAe,SAAS,aAAa,QAAQ,cAAc,cAAc;AAAA,MACvF,cAAc,QAAQ,gBAAgB;AAAA,MACtC,QAAQ,OAAO,MAAM,MAAM;AAAA;AAAA;AAAA,MAG3B,gBAAgB,QAAQ,WAAW,aAAa,OAAO,MAAM,MAAM,IAAI;AAAA,MACvE,cAAc,MAAM;AAAA,MACpB,iBAAiB;AAAA,QACf,GAAI,QAAQ,gBAAgB,CAAC;AAAA,QAC7B,GAAI,QAAQ,gBAAgB,EAAE,eAAe,QAAQ,cAAc,IAAI,CAAC;AAAA,MAC1E;AAAA,MACA,gBAAgB,MAAM;AAAA,MACtB,UAAU,MAAM;AAAA,MAChB,WAAW;AAAA,IACb;AACA,WAAO,KACH,QAAQ,OAAO,oBAAoB,EAAE,IAAI,GAAG,KAAK,CAAC,IAClD,QAAQ,OAAO,oBAAoB,IAAI;AAAA,EAC7C;AAEA,WAAS,eAAe,aAAsD;AAC5E,UAAM,WAAW,oBAAoB,YAAY,eAAe;AAChE,UAAM,EAAE,eAAe,GAAG,aAAa,IAAI;AAC3C,WAAO;AAAA,MACL,WAAW,sBAAsB,WAAW;AAAA,MAC5C,QAAQ,YAAY;AAAA,MACpB,GAAI,YAAY,eAAe,EAAE,cAAc,YAAY,aAAa,IAAI,CAAC;AAAA,MAC7E,GAAI,YAAY,cAAc,EAAE,aAAa,YAAY,YAAY,IAAI,CAAC;AAAA,MAC1E,GAAI,OAAO,KAAK,YAAY,EAAE,SAAS,IAAI,EAAE,aAAa,IAAI,CAAC;AAAA,MAC/D,GAAI,iBAAiB,OAAO,kBAAkB,WAC1C,EAAE,cAAqE,IACvE,CAAC;AAAA,IACP;AAAA,EACF;AAEA,iBAAe,qBACb,aACA,OACA,OACe;AACf,UAAM,wBAAwB,oCAAoC;AAAA,MAChE,eAAe,YAAY;AAAA,MAC3B,WAAW,YAAY;AAAA,MACvB,aAAa,YAAY;AAAA,MACzB,QAAQ,YAAY;AAAA,MACpB,gBAAgB,YAAY;AAAA,MAC5B,UAAU,YAAY;AAAA,IACxB,CAAC;AACD,UAAM;AAAA,MACJ,YAAY;AAAA,MACZ;AAAA,MACA,YAAY;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,QACE,WAAW,YAAY;AAAA,QACvB,mBAAmB,YAAY;AAAA,QAC/B,QAAQ,YAAY;AAAA,QACpB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,MACtB;AAAA,IACF;AAAA,EACF;AAEA,WAAS,oBACP,WACA,OACqB;AACrB,QAAI,UAAU;AACd,QAAI,QAA8C;AAClD,QAAI,iBAAiB,QAAQ,QAAQ;AAErC,UAAM,WAAW,MAAM;AACrB,cAAQ,WAAW,MAAM;AACvB,yBAAiB,oCAAoC,IAAI,WAAW,OAAO,oBAAI,KAAK,CAAC,EAClF;AAAA,UACC,CAAC,cAAc;AACb,gBAAI,aAAa,CAAC,QAAS,UAAS;AAAA,UACtC;AAAA,UACA,MAAM;AACJ,gBAAI,CAAC,QAAS,UAAS;AAAA,UACzB;AAAA,QACF;AAAA,MACJ,GAAG,wBAAwB;AAAA,IAC7B;AACA,aAAS;AAET,WAAO,YAAY;AACjB,gBAAU;AACV,UAAI,MAAO,cAAa,KAAK;AAC7B,YAAM;AAAA,IACR;AAAA,EACF;AAEA,SAAO;AAAA,IACL,MAAM,qBAAqB,OAA8G;AACvI,YAAM,QAAQ,EAAE,gBAAgB,MAAM,gBAAgB,UAAU,MAAM,SAAS;AAC/E,YAAM,gCAAgC;AAAA,QACpC,SAAS,MAAM;AAAA,QACf,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB;AAAA,QACA,UAAU,KAAK;AAAA,MACjB,CAAC;AACD,YAAM,EAAE,SAAS,YAAY,IAAI,MAAM,6BAA6B,MAAM,aAAa,KAAK;AAE5F,YAAM,eAAmC;AAAA,QACvC,WAAW,MAAM;AAAA,QACjB,SAAS,MAAM;AAAA,QACf,gBAAgB,MAAM;AAAA,QACtB,UAAU,MAAM;AAAA,QAChB,gBAAgB,MAAM;AAAA,QACtB,QAAQ,MAAM;AAAA,QACd,cAAc,MAAM;AAAA,QACpB,eAAe,MAAM;AAAA,QACrB,cAAc,MAAM;AAAA,QACpB,aAAa,MAAM;AAAA,QACnB,YAAY,MAAM;AAAA,QAClB,WAAW,MAAM;AAAA,QACjB,UAAU,MAAM;AAAA,QAChB,cAAc,MAAM;AAAA,QACpB;AAAA,MACF;AAEA,UAAI,CAAC,MAAM,gBAAgB;AACzB,cAAM,UAAU,MAAM,QAAQ,cAAc,YAAY;AACxD,cAAM,cAAc,yBAAyB,IAAI,OAAO,OAAO;AAC/D,cAAM,GAAG,QAAQ,WAAW,EAAE,MAAM;AACpC,cAAM,qBAAqB,aAAa,OAAO,KAAK;AACpD,eAAO,EAAE,aAAa,QAAQ;AAAA,MAChC;AAEA,YAAM,eAAe,gCAAgC;AAAA,QACnD,gBAAgB,MAAM;AAAA,QACtB,WAAW,MAAM;AAAA,QACjB,aAAa,MAAM;AAAA,QACnB;AAAA,MACF,CAAC;AACD,aAAO,MAAM;AACX,cAAM,WAAW,MAAM,iCAAiC,IAAI,cAAc,MAAM,aAAa,KAAK;AAClG,YAAI,UAAU,sBAAsB;AAClC,gBAAMA,eAAc,MAAM;AAAA,YACxB,GAAG,KAAK;AAAA,YACR;AAAA,YACA;AAAA,cACE,IAAI,SAAS;AAAA,cACb,gBAAgB,MAAM;AAAA,cACtB,UAAU,MAAM;AAAA,cAChB,WAAW;AAAA,YACb;AAAA,YACA;AAAA,YACA;AAAA,UACF;AACA,cAAI,CAACA,cAAa;AAChB,kBAAM,IAAI,MAAM,8DAA8D;AAAA,UAChF;AACA,iBAAO,EAAE,aAAAA,cAAa,SAAS,eAAeA,YAAW,EAAE;AAAA,QAC7D;AAEA,cAAM,YAAY,oBAAI,KAAK;AAC3B,cAAM,cAAc,IAAI,KAAK,UAAU,QAAQ,IAAI,iBAAiB;AACpE,cAAM,YAAY,WACd,MAAM,oCAAoC,IAAI,UAAU,OAAO,WAAW,WAAW,IACrF,MAAM,kCAAkC,IAAI,cAAc,MAAM,aAAa,OAAO,SAAS;AAEjG,YAAI,CAAC,WAAW;AACd,gBAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,mBAAmB,CAAC;AACvE;AAAA,QACF;AAEA,YAAI;AACJ,cAAM,gBAAgB,oBAAoB,WAAW,KAAK;AAC1D,YAAI;AACF,oBAAU,MAAM,QAAQ,cAAc,EAAE,GAAG,cAAc,gBAAgB,aAAa,CAAC;AAAA,QACzF,SAAS,OAAO;AACd,gBAAM,oCAAoC,IAAI,WAAW,KAAK,EAAE,MAAM,MAAM,MAAS;AACrF,gBAAM;AAAA,QACR,UAAE;AACA,gBAAM,cAAc;AAAA,QACtB;AAEA,cAAM,gBAAgB,WAAW;AACjC,cAAM,cAAc,MAAM,GAAG,KAAK,EAAE,cAAc,OAAO,kBAAkB;AACzE,gBAAM,gBAAgB,MAAM,cAAc;AAAA,YACxC;AAAA,YACA;AAAA,cACE,IAAI,UAAU;AAAA,cACd,YAAY,UAAU;AAAA,cACtB,sBAAsB;AAAA,cACtB,gBAAgB,MAAM;AAAA,cACtB,UAAU,MAAM;AAAA,YAClB;AAAA,YACA;AAAA,cACE,sBAAsB;AAAA,cACtB,YAAY;AAAA,cACZ,WAAW;AAAA,cACX,WAAW,oBAAI,KAAK;AAAA,YACtB;AAAA,UACF;AACA,cAAI,kBAAkB,EAAG,QAAO;AAChC,gBAAM,UAAU,yBAAyB,eAAe,OAAO,SAAS,aAAa;AACrF,gBAAM,cAAc,QAAQ,OAAO,EAAE,MAAM;AAC3C,iBAAO;AAAA,QACT,CAAC;AAED,YAAI,CAAC,aAAa;AAChB;AAAA,QACF;AACA,cAAM,qBAAqB,aAAa,OAAO,KAAK;AACpD,eAAO,EAAE,aAAa,QAAQ;AAAA,MAChC;AAAA,IACF;AAAA,IAEA,MAAM,eACJ,eACA,QACA,OACA,aACwB;AACxB,UAAI,gBAA+B;AACnC,UAAI,iBAAiB;AACrB,aAAO,uBAAuB;AAAA,QAC5B,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,SAAS,EAAE,QAAQ,UAAU,KAAK;AAAA,QAClC;AAAA,QACA,sBAAsB,CAAC,gBAAgB;AAAE,uCAA6B,aAAa,MAAM;AAAA,QAAE;AAAA,QAC3F,cAAc,OAAO,EAAE,aAAa,UAAU,MAAM;AAClD,gBAAM,uBAAuB,iBAAiB,YAAY,cAAc;AACxE,0BAAgB,MAAM,qBAAqB,IAAI,EAAE,aAAa,WAAW,QAAQ,MAAM,CAAC;AAGxF,cAAI,WAAW,UAAa,uBAAuB,IAAI;AACrD,6BAAiB,OAAO,kBAAkB,aAAa,CAAC;AAAA,UAC1D;AAAA,QACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAOA,kBAAkB,OAAO,EAAE,aAAa,WAAW,gBAAgB,MAAM;AACvE,cAAI,kBAAkB,KAAM;AAC5B,cAAI,iBAAiB;AACnB,kBAAM;AAAA,cACJ,YAAY;AAAA,cACZ,EAAE,gBAAgB,YAAY,gBAAgB,UAAU,YAAY,SAAS;AAAA,cAC7E,YAAY;AAAA,cACZ;AAAA,cACA;AAAA,cACA,EAAE,aAAa,UAAU,aAAa,gBAAgB,kBAAkB,aAAa,EAAE;AAAA,YACzF;AACA;AAAA,UACF;AACA,gBAAM,WAAW,MAAM,qBAAqB,IAAI,EAAE,eAAe,WAAW,eAAe,MAAM,CAAC;AAClG,cAAI,SAAU;AACd,gBAAM;AAAA,YACJ,YAAY;AAAA,YACZ,EAAE,gBAAgB,YAAY,gBAAgB,UAAU,YAAY,SAAS;AAAA,YAC7E,YAAY;AAAA,YACZ;AAAA,YACA;AAAA,YACA,EAAE,aAAa,UAAU,aAAa,gBAAgB,kBAAkB,aAAa,EAAE;AAAA,UACzF;AAAA,QACF;AAAA,QACA,QAAQ,CAAC,EAAE,SAAS,aAAa,aAAa,eAAe,MAAM,QAAQ,QAAQ;AAAA,UACjF,WAAW,sBAAsB,WAAW;AAAA,UAC5C,QAAQ;AAAA,UACR;AAAA,UACA;AAAA,QACF,CAAC;AAAA,QACD,aAAa,CAAC,aAAa,WAAW;AACpC,sBAAY,kBAAkB;AAAA,YAC5B,GAAG,oBAAoB,YAAY,eAAe;AAAA,YAClD,eAAe,OAAO;AAAA,UACxB;AACA,cAAI,kBAAkB,KAAM,sBAAqB,aAAa,eAAe,OAAO,cAAc;AAAA,QACpG;AAAA,QACA,aAAa,CAAC,aAAa,WAAW;AAAA,UACpC,YAAY;AAAA,UACZ,EAAE,gBAAgB,YAAY,gBAAgB,UAAU,YAAY,SAAS;AAAA,UAC7E,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,UACA,EAAE,QAAQ,UAAU,MAAM,QAAQ,OAAO,QAAQ,gBAAgB,OAAO,eAAe;AAAA,QACzF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,cACJ,eACA,QACA,QACA,OACA,aACuB;AACvB,aAAO,uBAAuB;AAAA,QAC5B,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,SAAS,EAAE,QAAQ,UAAU,MAAM,QAAQ,UAAU,KAAK;AAAA,QAC1D;AAAA,QACA,QAAQ,CAAC,EAAE,SAAS,aAAa,aAAa,eAAe,MAAM,QAAQ,OAAO;AAAA,UAChF,WAAW,sBAAsB,WAAW;AAAA,UAC5C;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAAA,QACD,aAAa,CAAC,aAAa,WAAW;AACpC,sBAAY,kBAAkB,OAAO;AACrC,sBAAY,kBAAkB;AAAA,YAC5B,GAAG,oBAAoB,YAAY,eAAe;AAAA,YAClD,cAAc,OAAO;AAAA,UACvB;AAAA,QACF;AAAA,QACA,aAAa,CAAC,aAAa,WAAW;AAAA,UACpC,YAAY;AAAA,UACZ,EAAE,gBAAgB,YAAY,gBAAgB,UAAU,YAAY,SAAS;AAAA,UAC7E,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,UACA,EAAE,QAAQ,UAAU,MAAM,QAAQ,UAAU,MAAM,QAAQ,OAAO,QAAQ,UAAU,OAAO,SAAS;AAAA,QACrG;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,cACJ,eACA,QACA,OACA,aACuB;AACvB,aAAO,uBAAuB;AAAA,QAC5B,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,SAAS,EAAE,QAAQ,UAAU,KAAK;AAAA,QAClC;AAAA,QACA,QAAQ,CAAC,EAAE,SAAS,aAAa,aAAa,eAAe,MAAM,QAAQ,OAAO;AAAA,UAChF,WAAW,sBAAsB,WAAW;AAAA,UAC5C;AAAA,UACA;AAAA,UACA;AAAA,QACF,CAAC;AAAA,QACD,aAAa,MAAM;AAAA,QAAC;AAAA,QACpB,aAAa,CAAC,aAAa,WAAW;AAAA,UACpC,YAAY;AAAA,UACZ,EAAE,gBAAgB,YAAY,gBAAgB,UAAU,YAAY,SAAS;AAAA,UAC7E,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,UACA,EAAE,QAAQ,UAAU,MAAM,QAAQ,OAAO,OAAO;AAAA,QAClD;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IAEA,MAAM,iBAAiB,eAAuB,OAAoF;AAChI,YAAM,cAAc,MAAM,uBAAuB,eAAe,KAAK;AACrE,YAAM,EAAE,SAAS,YAAY,IAAI,MAAM;AAAA,QACrC,YAAY;AAAA,QACZ,EAAE,gBAAgB,YAAY,gBAAgB,UAAU,YAAY,SAAS;AAAA,MAC/E;AAEA,YAAM,SAAS,MAAM,QAAQ,UAAU;AAAA,QACrC,WAAW,sBAAsB,WAAW;AAAA,QAC5C;AAAA,MACF,CAAC;AAED,UAAI,OAAO,WAAW,YAAY,iBAAiB,kBAAkB,YAAY,eAAuC,OAAO,MAAM,GAAG;AACtI,cAAM,iBAAiB,YAAY;AACnC,oBAAY,gBAAgB,OAAO;AACnC,sCAA8B,aAAa,OAAO,MAAM;AACxD,oBAAY,gBAAgB,OAAO;AACnC,oBAAY,kBAAkB,EAAE,GAAG,oBAAoB,YAAY,eAAe,GAAG,cAAc,OAAO,gBAAgB,KAAK;AAC/H,oBAAY,eAAe,oBAAI,KAAK;AACpC,cAAM,GAAG,MAAM;AACf,cAAM,gBAAgB,OAAO,QAAQ;AAAA,UACnC,eAAe,YAAY;AAAA,UAC3B,WAAW,YAAY;AAAA,UACvB,aAAa,YAAY;AAAA,UACzB;AAAA,UACA,gBAAgB,YAAY;AAAA,UAC5B,UAAU,YAAY;AAAA,QACxB,CAAC;AACD,cAAM;AAAA,UACJ,YAAY;AAAA,UACZ,EAAE,gBAAgB,YAAY,gBAAgB,UAAU,YAAY,SAAS;AAAA,UAC7E,YAAY;AAAA,UACZ;AAAA,UACA;AAAA,UACA;AAAA,YACE;AAAA,YACA,YAAY,OAAO;AAAA,UACrB;AAAA,QACF;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,sBAAsB,eAAuB,QAUhD,OAAoE;AACrE,YAAM,cAAc,MAAM,uBAAuB,eAAe,KAAK;AACrE,YAAM,gBAAgB,YAAY;AAClC,YAAM,gBAAgB,kBAAkB,eAAe,OAAO,aAAa;AAC3E,YAAM,oBAAoB,iBAAiB,OAAO,kBAAkB;AACpE,YAAM,iBAAiB,YAAY;AACnC,UAAI,mBAAmB;AACrB,oBAAY,gBAAgB,OAAO;AACnC,sCAA8B,aAAa,OAAO,aAAa;AAAA,MACjE;AACA,UAAI,OAAO,gBAAgB;AACzB,oBAAY,gBAAgB,OAAO;AAAA,MACrC;AACA,UAAI,OAAO,cAAc;AACvB,oBAAY,kBAAkB,EAAE,GAAG,oBAAoB,YAAY,eAAe,GAAG,GAAG,OAAO,aAAa;AAAA,MAC9G;AACA,UAAI,OAAO,cAAc;AACvB,cAAM,aAAa,eAAe,YAAY,UAAU;AACxD,mBAAW,KAAK;AAAA,UACd,WAAW,OAAO,aAAa;AAAA,UAC/B,YAAY,OAAO,aAAa,eAAc,oBAAI,KAAK,GAAE,YAAY;AAAA,UACrE,gBAAgB,OAAO,aAAa;AAAA,UACpC,eAAe,OAAO;AAAA,UACtB,WAAW,OAAO,aAAa;AAAA,QACjC,CAAC;AACD,oBAAY,aAAa;AAAA,MAC3B;AACA,kBAAY,gBAAgB,oBAAI,KAAK;AACrC,YAAM,GAAG,MAAM;AACf,UAAI,mBAAmB;AACrB,cAAM,gBAAgB,OAAO,eAAe;AAAA,UAC1C,eAAe,YAAY;AAAA,UAC3B,WAAW,YAAY;AAAA,UACvB,aAAa,YAAY;AAAA,UACzB;AAAA,UACA,gBAAgB,YAAY;AAAA,UAC5B,UAAU,YAAY;AAAA,QACxB,CAAC;AAAA,MACH;AACA,YAAM;AAAA,QACJ,YAAY;AAAA,QACZ,EAAE,gBAAgB,YAAY,gBAAgB,UAAU,YAAY,SAAS;AAAA,QAC7E,YAAY;AAAA,QACZ,oBAAoB,SAAS;AAAA,QAC7B,oBAAoB,6CAA6C;AAAA,QACjE;AAAA,UACE;AAAA,UACA,YAAY,OAAO;AAAA,UACnB,gBAAgB,OAAO,kBAAkB;AAAA,UACzC,WAAW,OAAO,cAAc,aAAa;AAAA,UAC7C,gBAAgB,OAAO,cAAc,kBAAkB;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,gBAAgB,IAAY,OAAyF;AACzH,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,UACE;AAAA,UACA,gBAAgB,MAAM;AAAA,UACtB,UAAU,MAAM;AAAA,UAChB,WAAW;AAAA,QACb;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,2BACJ,mBACA,OACA,aACoC;AACpC,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,UACE;AAAA,UACA,gBAAgB,MAAM;AAAA,UACtB,UAAU,MAAM;AAAA,UAChB,WAAW;AAAA,UACX,GAAI,cAAc,EAAE,YAAY,IAAI,CAAC;AAAA,QACvC;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,IAEA,MAAM,iCAAiC,OAKL;AAChC,YAAM,QAAiC;AAAA,QACrC,eAAe,EAAE,KAAK,CAAC,WAAW,cAAc,oBAAoB,EAAE;AAAA,QACtE,WAAW;AAAA,MACb;AACA,UAAI,OAAO,eAAgB,OAAM,iBAAiB,MAAM;AACxD,UAAI,OAAO,SAAU,OAAM,WAAW,MAAM;AAC5C,UAAI,OAAO,YAAa,OAAM,cAAc,MAAM;AAElD,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,UACE,SAAS,EAAE,WAAW,MAAM;AAAA,UAC5B,OAAO,OAAO,SAAS;AAAA,QACzB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
6
|
"names": ["transaction"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { conflict } from "@open-mercato/shared/lib/crud/errors";
|
|
2
|
+
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
3
|
+
import { createFallbackTranslator } from "@open-mercato/shared/lib/i18n/translate";
|
|
4
|
+
const AMOUNT_TOLERANCE = 1e-4;
|
|
5
|
+
function normalizeCurrencyCode(currencyCode) {
|
|
6
|
+
return currencyCode.trim().toUpperCase();
|
|
7
|
+
}
|
|
8
|
+
async function resolveConflictTranslator() {
|
|
9
|
+
try {
|
|
10
|
+
const { translate } = await resolveTranslations();
|
|
11
|
+
return translate;
|
|
12
|
+
} catch {
|
|
13
|
+
return createFallbackTranslator({});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function isPaymentOrderTotalResolver(candidate) {
|
|
17
|
+
return !!candidate && typeof candidate === "object" && typeof candidate.resolveOrderTotal === "function";
|
|
18
|
+
}
|
|
19
|
+
function assertSessionAmountMatchesOrderTotal(requested, orderTotal, translate) {
|
|
20
|
+
if (normalizeCurrencyCode(requested.currencyCode) !== normalizeCurrencyCode(orderTotal.currencyCode)) {
|
|
21
|
+
throw conflict(translate(
|
|
22
|
+
"payment_gateways.errors.sessionCurrencyMismatch",
|
|
23
|
+
"Payment session currency {currencyCode} does not match the currency of order {orderId}",
|
|
24
|
+
{ currencyCode: normalizeCurrencyCode(requested.currencyCode), orderId: requested.orderId }
|
|
25
|
+
));
|
|
26
|
+
}
|
|
27
|
+
if (Math.abs(requested.amount - orderTotal.amountDue) > AMOUNT_TOLERANCE) {
|
|
28
|
+
throw conflict(translate(
|
|
29
|
+
"payment_gateways.errors.sessionAmountMismatch",
|
|
30
|
+
"Payment session amount {amount} does not match the amount due for order {orderId}",
|
|
31
|
+
{ amount: requested.amount, orderId: requested.orderId }
|
|
32
|
+
));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function reconcileSessionAmountWithOrder(input) {
|
|
36
|
+
const { orderId, resolver } = input;
|
|
37
|
+
if (!orderId || !resolver) return;
|
|
38
|
+
const orderTotal = await resolver.resolveOrderTotal(orderId, input.scope);
|
|
39
|
+
const translate = await resolveConflictTranslator();
|
|
40
|
+
if (!orderTotal) {
|
|
41
|
+
throw conflict(translate(
|
|
42
|
+
"payment_gateways.errors.sessionOrderNotFound",
|
|
43
|
+
"Order {orderId} was not found in the current scope",
|
|
44
|
+
{ orderId }
|
|
45
|
+
));
|
|
46
|
+
}
|
|
47
|
+
assertSessionAmountMatchesOrderTotal(
|
|
48
|
+
{ orderId, amount: input.amount, currencyCode: input.currencyCode },
|
|
49
|
+
orderTotal,
|
|
50
|
+
translate
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
export {
|
|
54
|
+
assertSessionAmountMatchesOrderTotal,
|
|
55
|
+
isPaymentOrderTotalResolver,
|
|
56
|
+
reconcileSessionAmountWithOrder
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=order-amount-reconciliation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/payment_gateways/lib/order-amount-reconciliation.ts"],
|
|
4
|
+
"sourcesContent": ["import { conflict } from '@open-mercato/shared/lib/crud/errors'\nimport { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'\nimport { createFallbackTranslator, type TranslateWithFallbackFn } from '@open-mercato/shared/lib/i18n/translate'\nimport type {\n PaymentGatewayScope,\n PaymentOrderTotal,\n PaymentOrderTotalResolver,\n} from '@open-mercato/shared/modules/payment_gateways/types'\n\n/** Amounts are stored as numeric(18,4); anything below that is rounding noise, not a mismatch. */\nconst AMOUNT_TOLERANCE = 0.0001\n\nfunction normalizeCurrencyCode(currencyCode: string): string {\n return currencyCode.trim().toUpperCase()\n}\n\n/**\n * Conflict copy is served from the module catalog on a request path. Contexts\n * without a registered module dictionary (CLI commands, workers, unit tests)\n * fall back to the English template shipped with each call, so a missing\n * dictionary degrades the wording of a rejection but never its outcome.\n */\nasync function resolveConflictTranslator(): Promise<TranslateWithFallbackFn> {\n try {\n const { translate } = await resolveTranslations()\n return translate\n } catch {\n return createFallbackTranslator({})\n }\n}\n\nexport function isPaymentOrderTotalResolver(candidate: unknown): candidate is PaymentOrderTotalResolver {\n return !!candidate\n && typeof candidate === 'object'\n && typeof (candidate as PaymentOrderTotalResolver).resolveOrderTotal === 'function'\n}\n\nexport function assertSessionAmountMatchesOrderTotal(\n requested: { orderId: string; amount: number; currencyCode: string },\n orderTotal: PaymentOrderTotal,\n translate: TranslateWithFallbackFn,\n): void {\n if (normalizeCurrencyCode(requested.currencyCode) !== normalizeCurrencyCode(orderTotal.currencyCode)) {\n throw conflict(translate(\n 'payment_gateways.errors.sessionCurrencyMismatch',\n 'Payment session currency {currencyCode} does not match the currency of order {orderId}',\n { currencyCode: normalizeCurrencyCode(requested.currencyCode), orderId: requested.orderId },\n ))\n }\n if (Math.abs(requested.amount - orderTotal.amountDue) > AMOUNT_TOLERANCE) {\n throw conflict(translate(\n 'payment_gateways.errors.sessionAmountMismatch',\n 'Payment session amount {amount} does not match the amount due for order {orderId}',\n { amount: requested.amount, orderId: requested.orderId },\n ))\n }\n}\n\n/**\n * Reconciles a caller-supplied session amount against the authoritative amount\n * due for the referenced order. Reconciliation is skipped when the request\n * references no order, or when no module registered a resolver (for example an\n * installation without the `sales` module) \u2014 there is nothing authoritative to\n * compare against in either case. A referenced order that does not resolve\n * inside the caller's tenant/organization scope is rejected exactly like an\n * unknown one, so the response never reveals whether it exists elsewhere.\n */\nexport async function reconcileSessionAmountWithOrder(input: {\n orderId?: string\n amount: number\n currencyCode: string\n scope: PaymentGatewayScope\n resolver?: PaymentOrderTotalResolver | null\n}): Promise<void> {\n const { orderId, resolver } = input\n if (!orderId || !resolver) return\n\n const orderTotal = await resolver.resolveOrderTotal(orderId, input.scope)\n const translate = await resolveConflictTranslator()\n if (!orderTotal) {\n throw conflict(translate(\n 'payment_gateways.errors.sessionOrderNotFound',\n 'Order {orderId} was not found in the current scope',\n { orderId },\n ))\n }\n\n assertSessionAmountMatchesOrderTotal(\n { orderId, amount: input.amount, currencyCode: input.currencyCode },\n orderTotal,\n translate,\n )\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,gBAAgB;AACzB,SAAS,2BAA2B;AACpC,SAAS,gCAA8D;AAQvE,MAAM,mBAAmB;AAEzB,SAAS,sBAAsB,cAA8B;AAC3D,SAAO,aAAa,KAAK,EAAE,YAAY;AACzC;AAQA,eAAe,4BAA8D;AAC3E,MAAI;AACF,UAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB;AAChD,WAAO;AAAA,EACT,QAAQ;AACN,WAAO,yBAAyB,CAAC,CAAC;AAAA,EACpC;AACF;AAEO,SAAS,4BAA4B,WAA4D;AACtG,SAAO,CAAC,CAAC,aACJ,OAAO,cAAc,YACrB,OAAQ,UAAwC,sBAAsB;AAC7E;AAEO,SAAS,qCACd,WACA,YACA,WACM;AACN,MAAI,sBAAsB,UAAU,YAAY,MAAM,sBAAsB,WAAW,YAAY,GAAG;AACpG,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,MACA,EAAE,cAAc,sBAAsB,UAAU,YAAY,GAAG,SAAS,UAAU,QAAQ;AAAA,IAC5F,CAAC;AAAA,EACH;AACA,MAAI,KAAK,IAAI,UAAU,SAAS,WAAW,SAAS,IAAI,kBAAkB;AACxE,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,MACA,EAAE,QAAQ,UAAU,QAAQ,SAAS,UAAU,QAAQ;AAAA,IACzD,CAAC;AAAA,EACH;AACF;AAWA,eAAsB,gCAAgC,OAMpC;AAChB,QAAM,EAAE,SAAS,SAAS,IAAI;AAC9B,MAAI,CAAC,WAAW,CAAC,SAAU;AAE3B,QAAM,aAAa,MAAM,SAAS,kBAAkB,SAAS,MAAM,KAAK;AACxE,QAAM,YAAY,MAAM,0BAA0B;AAClD,MAAI,CAAC,YAAY;AACf,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,MACA,EAAE,QAAQ;AAAA,IACZ,CAAC;AAAA,EACH;AAEA;AAAA,IACE,EAAE,SAAS,QAAQ,MAAM,QAAQ,cAAc,MAAM,aAAa;AAAA,IAClE;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/modules/sales/di.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DefaultSalesCalculationService } from "./services/salesCalculationServi
|
|
|
4
4
|
import { DefaultTaxCalculationService } from "./services/taxCalculationService.js";
|
|
5
5
|
import { SalesDocumentNumberGenerator } from "./services/salesDocumentNumberGenerator.js";
|
|
6
6
|
import { DefaultSalesOrderService } from "./services/salesOrderService.js";
|
|
7
|
+
import { createSalesPaymentOrderTotalResolver } from "./services/paymentOrderTotalResolver.js";
|
|
7
8
|
import {
|
|
8
9
|
SalesOrder,
|
|
9
10
|
SalesOrderLine,
|
|
@@ -98,6 +99,12 @@ function register(container) {
|
|
|
98
99
|
salesOrderService: asFunction(({ em }) => {
|
|
99
100
|
return new DefaultSalesOrderService(em);
|
|
100
101
|
}).singleton().proxy(),
|
|
102
|
+
// Authoritative amount-due lookup consumed by `payment_gateways` when it
|
|
103
|
+
// reconciles a caller-supplied payment-session amount (#4488). Registering
|
|
104
|
+
// it here keeps the gateway module free of any dependency on sales.
|
|
105
|
+
paymentOrderTotalResolver: asFunction(({ em }) => {
|
|
106
|
+
return createSalesPaymentOrderTotalResolver({ em });
|
|
107
|
+
}).scoped().proxy(),
|
|
101
108
|
SalesOrder: asValue(SalesOrder),
|
|
102
109
|
SalesOrderLine: asValue(SalesOrderLine),
|
|
103
110
|
SalesOrderAdjustment: asValue(SalesOrderAdjustment),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/modules/sales/di.ts"],
|
|
4
|
-
"sourcesContent": ["import { asFunction, asValue } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EventBus } from '@open-mercato/events'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OptimisticLockCurrentReader } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { registerOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport { DefaultSalesCalculationService } from './services/salesCalculationService'\nimport { DefaultTaxCalculationService } from './services/taxCalculationService'\nimport { SalesDocumentNumberGenerator } from './services/salesDocumentNumberGenerator'\nimport { DefaultSalesOrderService } from './services/salesOrderService'\nimport {\n SalesOrder,\n SalesOrderLine,\n SalesOrderAdjustment,\n SalesQuote,\n SalesQuoteLine,\n SalesQuoteAdjustment,\n SalesChannel,\n SalesShipment,\n SalesShipmentItem,\n SalesInvoice,\n SalesInvoiceLine,\n SalesCreditMemo,\n SalesCreditMemoLine,\n SalesPayment,\n SalesPaymentAllocation,\n SalesReturn,\n SalesReturnLine,\n SalesNote,\n SalesDocumentAddress,\n SalesDocumentTag,\n SalesDocumentTagAssignment,\n SalesShippingMethod,\n SalesDeliveryWindow,\n SalesPaymentMethod,\n SalesTaxRate,\n} from './data/entities'\n\ntype AppCradle = AppContainer['cradle'] & {\n em: EntityManager\n eventBus?: EventBus | null\n}\n\nconst RESOURCE_KIND_ORDER = 'sales.order'\nconst RESOURCE_KIND_PAYMENT = 'sales.payment'\nconst RESOURCE_KIND_SHIPMENT = 'sales.shipment'\n\nasync function readOrderUpdatedAtById(\n em: EntityManager,\n orderId: string | null | undefined,\n tenantId: string,\n organizationId: string | null | undefined,\n): Promise<string | null> {\n if (!orderId) return null\n const row = await em.findOne(\n SalesOrder,\n {\n id: orderId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n deletedAt: null,\n },\n { fields: ['updatedAt'] as const },\n )\n return row?.updatedAt instanceof Date ? row.updatedAt.toISOString() : null\n}\n\nconst readSalesOrderUpdatedAt: OptimisticLockCurrentReader = async (\n em: EntityManager,\n { resourceId, tenantId, organizationId },\n) => readOrderUpdatedAtById(em, resourceId, tenantId, organizationId)\n\n// Payments and shipments are guarded against the PARENT ORDER's aggregate\n// version (their mutations recalc order totals / fulfilled quantities, so the\n// order is the consistency boundary \u2014 same model as lines/adjustments/returns).\n// The CRUD-layer guard therefore must compare against the order, not the\n// payment/shipment row: resolve the parent order from the sub-resource id and\n// return its `updated_at`. This keeps the single optimistic-lock header\n// consistent across the makeCrudRoute guard and the command-level\n// `enforceSalesDocumentOptimisticLock` guard (both read the order version).\nconst readPaymentParentOrderUpdatedAt: OptimisticLockCurrentReader = async (\n em: EntityManager,\n { resourceId, tenantId, organizationId },\n) => {\n const payment = await em.findOne(\n SalesPayment,\n {\n id: resourceId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n deletedAt: null,\n },\n { fields: ['order'] as const },\n )\n const orderId = (payment as { order?: { id?: string } | string | null } | null)?.order\n const resolvedOrderId = typeof orderId === 'string' ? orderId : orderId?.id ?? null\n return readOrderUpdatedAtById(em, resolvedOrderId, tenantId, organizationId)\n}\n\nconst readShipmentParentOrderUpdatedAt: OptimisticLockCurrentReader = async (\n em: EntityManager,\n { resourceId, tenantId, organizationId },\n) => {\n const shipment = await em.findOne(\n SalesShipment,\n {\n id: resourceId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n deletedAt: null,\n },\n { fields: ['order'] as const },\n )\n const orderId = (shipment as { order?: { id?: string } | string | null } | null)?.order\n const resolvedOrderId = typeof orderId === 'string' ? orderId : orderId?.id ?? null\n return readOrderUpdatedAtById(em, resolvedOrderId, tenantId, organizationId)\n}\n\n// Hand-wired sales readers registered at module-load time. `sales.order` mirrors\n// the auto-registered generic reader (kept as the polymorphic-table override\n// reference). `sales.payment`/`sales.shipment` redirect the CRUD-layer guard to\n// the parent order so payments/shipments guard the aggregate (Phase 3 Gap A/B)\n// instead of their own row. The guard's mode check short-circuits when\n// `OM_OPTIMISTIC_LOCK=off`.\nregisterOptimisticLockReaders({\n [RESOURCE_KIND_ORDER]: readSalesOrderUpdatedAt,\n [RESOURCE_KIND_PAYMENT]: readPaymentParentOrderUpdatedAt,\n [RESOURCE_KIND_SHIPMENT]: readShipmentParentOrderUpdatedAt,\n})\n\nexport function register(container: AppContainer) {\n container.register({\n salesCalculationService: asFunction(({ eventBus }: AppCradle) => {\n return new DefaultSalesCalculationService(eventBus ?? null)\n })\n .singleton()\n .proxy(),\n taxCalculationService: asFunction(({ em, eventBus }: AppCradle) => {\n return new DefaultTaxCalculationService(em, eventBus ?? null)\n })\n .singleton()\n .proxy(),\n salesDocumentNumberGenerator: asFunction(({ em }: AppCradle) => {\n return new SalesDocumentNumberGenerator(em)\n })\n .singleton()\n .proxy(),\n salesOrderService: asFunction(({ em }: AppCradle) => {\n return new DefaultSalesOrderService(em)\n })\n .singleton()\n .proxy(),\n SalesOrder: asValue(SalesOrder),\n SalesOrderLine: asValue(SalesOrderLine),\n SalesOrderAdjustment: asValue(SalesOrderAdjustment),\n SalesQuote: asValue(SalesQuote),\n SalesQuoteLine: asValue(SalesQuoteLine),\n SalesQuoteAdjustment: asValue(SalesQuoteAdjustment),\n SalesChannel: asValue(SalesChannel),\n SalesShipment: asValue(SalesShipment),\n SalesShipmentItem: asValue(SalesShipmentItem),\n SalesInvoice: asValue(SalesInvoice),\n SalesInvoiceLine: asValue(SalesInvoiceLine),\n SalesCreditMemo: asValue(SalesCreditMemo),\n SalesCreditMemoLine: asValue(SalesCreditMemoLine),\n SalesPayment: asValue(SalesPayment),\n SalesPaymentAllocation: asValue(SalesPaymentAllocation),\n SalesReturn: asValue(SalesReturn),\n SalesReturnLine: asValue(SalesReturnLine),\n SalesNote: asValue(SalesNote),\n SalesDocumentAddress: asValue(SalesDocumentAddress),\n SalesDocumentTag: asValue(SalesDocumentTag),\n SalesDocumentTagAssignment: asValue(SalesDocumentTagAssignment),\n SalesShippingMethod: asValue(SalesShippingMethod),\n SalesDeliveryWindow: asValue(SalesDeliveryWindow),\n SalesPaymentMethod: asValue(SalesPaymentMethod),\n SalesTaxRate: asValue(SalesTaxRate),\n })\n\n // `crudMutationGuardService` is registered platform-wide in the shared\n // DI bootstrap (`packages/shared/src/lib/di/container.ts`). The\n // hand-wired sales.order reader above already lives in the global store,\n // so this module no longer needs its own DI binding.\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import { asFunction, asValue } from 'awilix'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { EventBus } from '@open-mercato/events'\nimport type { AppContainer } from '@open-mercato/shared/lib/di/container'\nimport type { OptimisticLockCurrentReader } from '@open-mercato/shared/lib/crud/optimistic-lock'\nimport { registerOptimisticLockReaders } from '@open-mercato/shared/lib/crud/optimistic-lock-store'\nimport { DefaultSalesCalculationService } from './services/salesCalculationService'\nimport { DefaultTaxCalculationService } from './services/taxCalculationService'\nimport { SalesDocumentNumberGenerator } from './services/salesDocumentNumberGenerator'\nimport { DefaultSalesOrderService } from './services/salesOrderService'\nimport { createSalesPaymentOrderTotalResolver } from './services/paymentOrderTotalResolver'\nimport {\n SalesOrder,\n SalesOrderLine,\n SalesOrderAdjustment,\n SalesQuote,\n SalesQuoteLine,\n SalesQuoteAdjustment,\n SalesChannel,\n SalesShipment,\n SalesShipmentItem,\n SalesInvoice,\n SalesInvoiceLine,\n SalesCreditMemo,\n SalesCreditMemoLine,\n SalesPayment,\n SalesPaymentAllocation,\n SalesReturn,\n SalesReturnLine,\n SalesNote,\n SalesDocumentAddress,\n SalesDocumentTag,\n SalesDocumentTagAssignment,\n SalesShippingMethod,\n SalesDeliveryWindow,\n SalesPaymentMethod,\n SalesTaxRate,\n} from './data/entities'\n\ntype AppCradle = AppContainer['cradle'] & {\n em: EntityManager\n eventBus?: EventBus | null\n}\n\nconst RESOURCE_KIND_ORDER = 'sales.order'\nconst RESOURCE_KIND_PAYMENT = 'sales.payment'\nconst RESOURCE_KIND_SHIPMENT = 'sales.shipment'\n\nasync function readOrderUpdatedAtById(\n em: EntityManager,\n orderId: string | null | undefined,\n tenantId: string,\n organizationId: string | null | undefined,\n): Promise<string | null> {\n if (!orderId) return null\n const row = await em.findOne(\n SalesOrder,\n {\n id: orderId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n deletedAt: null,\n },\n { fields: ['updatedAt'] as const },\n )\n return row?.updatedAt instanceof Date ? row.updatedAt.toISOString() : null\n}\n\nconst readSalesOrderUpdatedAt: OptimisticLockCurrentReader = async (\n em: EntityManager,\n { resourceId, tenantId, organizationId },\n) => readOrderUpdatedAtById(em, resourceId, tenantId, organizationId)\n\n// Payments and shipments are guarded against the PARENT ORDER's aggregate\n// version (their mutations recalc order totals / fulfilled quantities, so the\n// order is the consistency boundary \u2014 same model as lines/adjustments/returns).\n// The CRUD-layer guard therefore must compare against the order, not the\n// payment/shipment row: resolve the parent order from the sub-resource id and\n// return its `updated_at`. This keeps the single optimistic-lock header\n// consistent across the makeCrudRoute guard and the command-level\n// `enforceSalesDocumentOptimisticLock` guard (both read the order version).\nconst readPaymentParentOrderUpdatedAt: OptimisticLockCurrentReader = async (\n em: EntityManager,\n { resourceId, tenantId, organizationId },\n) => {\n const payment = await em.findOne(\n SalesPayment,\n {\n id: resourceId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n deletedAt: null,\n },\n { fields: ['order'] as const },\n )\n const orderId = (payment as { order?: { id?: string } | string | null } | null)?.order\n const resolvedOrderId = typeof orderId === 'string' ? orderId : orderId?.id ?? null\n return readOrderUpdatedAtById(em, resolvedOrderId, tenantId, organizationId)\n}\n\nconst readShipmentParentOrderUpdatedAt: OptimisticLockCurrentReader = async (\n em: EntityManager,\n { resourceId, tenantId, organizationId },\n) => {\n const shipment = await em.findOne(\n SalesShipment,\n {\n id: resourceId,\n tenantId,\n ...(organizationId ? { organizationId } : {}),\n deletedAt: null,\n },\n { fields: ['order'] as const },\n )\n const orderId = (shipment as { order?: { id?: string } | string | null } | null)?.order\n const resolvedOrderId = typeof orderId === 'string' ? orderId : orderId?.id ?? null\n return readOrderUpdatedAtById(em, resolvedOrderId, tenantId, organizationId)\n}\n\n// Hand-wired sales readers registered at module-load time. `sales.order` mirrors\n// the auto-registered generic reader (kept as the polymorphic-table override\n// reference). `sales.payment`/`sales.shipment` redirect the CRUD-layer guard to\n// the parent order so payments/shipments guard the aggregate (Phase 3 Gap A/B)\n// instead of their own row. The guard's mode check short-circuits when\n// `OM_OPTIMISTIC_LOCK=off`.\nregisterOptimisticLockReaders({\n [RESOURCE_KIND_ORDER]: readSalesOrderUpdatedAt,\n [RESOURCE_KIND_PAYMENT]: readPaymentParentOrderUpdatedAt,\n [RESOURCE_KIND_SHIPMENT]: readShipmentParentOrderUpdatedAt,\n})\n\nexport function register(container: AppContainer) {\n container.register({\n salesCalculationService: asFunction(({ eventBus }: AppCradle) => {\n return new DefaultSalesCalculationService(eventBus ?? null)\n })\n .singleton()\n .proxy(),\n taxCalculationService: asFunction(({ em, eventBus }: AppCradle) => {\n return new DefaultTaxCalculationService(em, eventBus ?? null)\n })\n .singleton()\n .proxy(),\n salesDocumentNumberGenerator: asFunction(({ em }: AppCradle) => {\n return new SalesDocumentNumberGenerator(em)\n })\n .singleton()\n .proxy(),\n salesOrderService: asFunction(({ em }: AppCradle) => {\n return new DefaultSalesOrderService(em)\n })\n .singleton()\n .proxy(),\n // Authoritative amount-due lookup consumed by `payment_gateways` when it\n // reconciles a caller-supplied payment-session amount (#4488). Registering\n // it here keeps the gateway module free of any dependency on sales.\n paymentOrderTotalResolver: asFunction(({ em }: AppCradle) => {\n return createSalesPaymentOrderTotalResolver({ em })\n })\n .scoped()\n .proxy(),\n SalesOrder: asValue(SalesOrder),\n SalesOrderLine: asValue(SalesOrderLine),\n SalesOrderAdjustment: asValue(SalesOrderAdjustment),\n SalesQuote: asValue(SalesQuote),\n SalesQuoteLine: asValue(SalesQuoteLine),\n SalesQuoteAdjustment: asValue(SalesQuoteAdjustment),\n SalesChannel: asValue(SalesChannel),\n SalesShipment: asValue(SalesShipment),\n SalesShipmentItem: asValue(SalesShipmentItem),\n SalesInvoice: asValue(SalesInvoice),\n SalesInvoiceLine: asValue(SalesInvoiceLine),\n SalesCreditMemo: asValue(SalesCreditMemo),\n SalesCreditMemoLine: asValue(SalesCreditMemoLine),\n SalesPayment: asValue(SalesPayment),\n SalesPaymentAllocation: asValue(SalesPaymentAllocation),\n SalesReturn: asValue(SalesReturn),\n SalesReturnLine: asValue(SalesReturnLine),\n SalesNote: asValue(SalesNote),\n SalesDocumentAddress: asValue(SalesDocumentAddress),\n SalesDocumentTag: asValue(SalesDocumentTag),\n SalesDocumentTagAssignment: asValue(SalesDocumentTagAssignment),\n SalesShippingMethod: asValue(SalesShippingMethod),\n SalesDeliveryWindow: asValue(SalesDeliveryWindow),\n SalesPaymentMethod: asValue(SalesPaymentMethod),\n SalesTaxRate: asValue(SalesTaxRate),\n })\n\n // `crudMutationGuardService` is registered platform-wide in the shared\n // DI bootstrap (`packages/shared/src/lib/di/container.ts`). The\n // hand-wired sales.order reader above already lives in the global store,\n // so this module no longer needs its own DI binding.\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
|
|
2
|
+
import { SalesOrder } from "../data/entities.js";
|
|
3
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4
|
+
function toAmount(value) {
|
|
5
|
+
const parsed = typeof value === "number" ? value : Number(value ?? 0);
|
|
6
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
7
|
+
}
|
|
8
|
+
function resolveOrderAmountDue(order) {
|
|
9
|
+
const outstanding = toAmount(order.outstandingAmount);
|
|
10
|
+
const paid = toAmount(order.paidTotalAmount);
|
|
11
|
+
const refunded = toAmount(order.refundedTotalAmount);
|
|
12
|
+
if (outstanding > 0 || paid > 0 || refunded > 0) return outstanding;
|
|
13
|
+
return toAmount(order.grandTotalGrossAmount);
|
|
14
|
+
}
|
|
15
|
+
function createSalesPaymentOrderTotalResolver(deps) {
|
|
16
|
+
return {
|
|
17
|
+
async resolveOrderTotal(orderId, scope) {
|
|
18
|
+
if (!UUID_PATTERN.test(orderId) || !scope.tenantId || !scope.organizationId) return null;
|
|
19
|
+
const order = await findOneWithDecryption(
|
|
20
|
+
deps.em,
|
|
21
|
+
SalesOrder,
|
|
22
|
+
{
|
|
23
|
+
id: orderId,
|
|
24
|
+
tenantId: scope.tenantId,
|
|
25
|
+
organizationId: scope.organizationId,
|
|
26
|
+
deletedAt: null
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
fields: [
|
|
30
|
+
"currencyCode",
|
|
31
|
+
"outstandingAmount",
|
|
32
|
+
"paidTotalAmount",
|
|
33
|
+
"refundedTotalAmount",
|
|
34
|
+
"grandTotalGrossAmount"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
scope
|
|
38
|
+
);
|
|
39
|
+
if (!order) return null;
|
|
40
|
+
return {
|
|
41
|
+
orderId,
|
|
42
|
+
currencyCode: order.currencyCode,
|
|
43
|
+
amountDue: resolveOrderAmountDue(order)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export {
|
|
49
|
+
createSalesPaymentOrderTotalResolver,
|
|
50
|
+
resolveOrderAmountDue
|
|
51
|
+
};
|
|
52
|
+
//# sourceMappingURL=paymentOrderTotalResolver.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/sales/services/paymentOrderTotalResolver.ts"],
|
|
4
|
+
"sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type {\n PaymentGatewayScope,\n PaymentOrderTotal,\n PaymentOrderTotalResolver,\n} from '@open-mercato/shared/modules/payment_gateways/types'\nimport { SalesOrder } from '../data/entities'\n\nconst UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\nfunction toAmount(value: string | number | null | undefined): number {\n const parsed = typeof value === 'number' ? value : Number(value ?? 0)\n return Number.isFinite(parsed) ? parsed : 0\n}\n\n/**\n * The amount a gateway session may legitimately charge for an order: what is\n * still due. `outstanding_amount` is recomputed from payments and refunds, so\n * it is the authoritative value once either exists. Orders that were never\n * paid fall back to the grand total, which also covers rows written before\n * outstanding totals were tracked.\n */\nexport function resolveOrderAmountDue(order: Pick<\n SalesOrder,\n 'outstandingAmount' | 'paidTotalAmount' | 'refundedTotalAmount' | 'grandTotalGrossAmount'\n>): number {\n const outstanding = toAmount(order.outstandingAmount)\n const paid = toAmount(order.paidTotalAmount)\n const refunded = toAmount(order.refundedTotalAmount)\n if (outstanding > 0 || paid > 0 || refunded > 0) return outstanding\n return toAmount(order.grandTotalGrossAmount)\n}\n\nexport function createSalesPaymentOrderTotalResolver(deps: { em: EntityManager }): PaymentOrderTotalResolver {\n return {\n async resolveOrderTotal(orderId: string, scope: PaymentGatewayScope): Promise<PaymentOrderTotal | null> {\n if (!UUID_PATTERN.test(orderId) || !scope.tenantId || !scope.organizationId) return null\n const order = await findOneWithDecryption(\n deps.em,\n SalesOrder,\n {\n id: orderId,\n tenantId: scope.tenantId,\n organizationId: scope.organizationId,\n deletedAt: null,\n },\n {\n fields: [\n 'currencyCode',\n 'outstandingAmount',\n 'paidTotalAmount',\n 'refundedTotalAmount',\n 'grandTotalGrossAmount',\n ] as const,\n },\n scope,\n )\n if (!order) return null\n return {\n orderId,\n currencyCode: order.currencyCode,\n amountDue: resolveOrderAmountDue(order),\n }\n },\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,6BAA6B;AAMtC,SAAS,kBAAkB;AAE3B,MAAM,eAAe;AAErB,SAAS,SAAS,OAAmD;AACnE,QAAM,SAAS,OAAO,UAAU,WAAW,QAAQ,OAAO,SAAS,CAAC;AACpE,SAAO,OAAO,SAAS,MAAM,IAAI,SAAS;AAC5C;AASO,SAAS,sBAAsB,OAG3B;AACT,QAAM,cAAc,SAAS,MAAM,iBAAiB;AACpD,QAAM,OAAO,SAAS,MAAM,eAAe;AAC3C,QAAM,WAAW,SAAS,MAAM,mBAAmB;AACnD,MAAI,cAAc,KAAK,OAAO,KAAK,WAAW,EAAG,QAAO;AACxD,SAAO,SAAS,MAAM,qBAAqB;AAC7C;AAEO,SAAS,qCAAqC,MAAwD;AAC3G,SAAO;AAAA,IACL,MAAM,kBAAkB,SAAiB,OAA+D;AACtG,UAAI,CAAC,aAAa,KAAK,OAAO,KAAK,CAAC,MAAM,YAAY,CAAC,MAAM,eAAgB,QAAO;AACpF,YAAM,QAAQ,MAAM;AAAA,QAClB,KAAK;AAAA,QACL;AAAA,QACA;AAAA,UACE,IAAI;AAAA,UACJ,UAAU,MAAM;AAAA,UAChB,gBAAgB,MAAM;AAAA,UACtB,WAAW;AAAA,QACb;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,YACN;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,QACA;AAAA,MACF;AACA,UAAI,CAAC,MAAO,QAAO;AACnB,aAAO;AAAA,QACL;AAAA,QACA,cAAc,MAAM;AAAA,QACpB,WAAW,sBAAsB,KAAK;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.6.7-develop.
|
|
3
|
+
"version": "0.6.7-develop.6706.1.b3a4c759bb",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -254,16 +254,16 @@
|
|
|
254
254
|
"zod": "^4.4.3"
|
|
255
255
|
},
|
|
256
256
|
"peerDependencies": {
|
|
257
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
258
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
259
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
257
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6706.1.b3a4c759bb",
|
|
258
|
+
"@open-mercato/shared": "0.6.7-develop.6706.1.b3a4c759bb",
|
|
259
|
+
"@open-mercato/ui": "0.6.7-develop.6706.1.b3a4c759bb",
|
|
260
260
|
"react": "^19.0.0",
|
|
261
261
|
"react-dom": "^19.0.0"
|
|
262
262
|
},
|
|
263
263
|
"devDependencies": {
|
|
264
|
-
"@open-mercato/ai-assistant": "0.6.7-develop.
|
|
265
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
266
|
-
"@open-mercato/ui": "0.6.7-develop.
|
|
264
|
+
"@open-mercato/ai-assistant": "0.6.7-develop.6706.1.b3a4c759bb",
|
|
265
|
+
"@open-mercato/shared": "0.6.7-develop.6706.1.b3a4c759bb",
|
|
266
|
+
"@open-mercato/ui": "0.6.7-develop.6706.1.b3a4c759bb",
|
|
267
267
|
"@testing-library/dom": "^10.4.1",
|
|
268
268
|
"@testing-library/jest-dom": "^6.9.1",
|
|
269
269
|
"@testing-library/react": "^16.3.1",
|
|
@@ -135,7 +135,10 @@ export default function LoginPage() {
|
|
|
135
135
|
try {
|
|
136
136
|
const res = await apiCall<{ userId?: string }>('/api/auth/feature-check', {
|
|
137
137
|
method: 'POST',
|
|
138
|
-
headers: {
|
|
138
|
+
headers: {
|
|
139
|
+
'content-type': 'application/json',
|
|
140
|
+
'x-om-unauthorized-redirect': '0',
|
|
141
|
+
},
|
|
139
142
|
body: JSON.stringify({ features: [] }),
|
|
140
143
|
cache: 'no-store',
|
|
141
144
|
})
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
mergeCompanyCustomFieldValues,
|
|
30
30
|
} from '../../../lib/customFieldRouting'
|
|
31
31
|
import { isOpenInteractionStatus, TERMINAL_INTERACTION_STATUS_LIST } from '../../../lib/interactionStatus'
|
|
32
|
+
import { isOpenDealStatus, isWonDealStatus } from '../../../lib/dealStatus'
|
|
32
33
|
import {
|
|
33
34
|
CUSTOMER_INTERACTION_ACTIVITY_ADAPTER_SOURCE,
|
|
34
35
|
EXAMPLE_TODO_SOURCE,
|
|
@@ -1015,10 +1016,8 @@ export async function GET(_req: Request, ctx: { params?: { id?: string } }) {
|
|
|
1015
1016
|
.map((value) => (value instanceof Date ? value.toISOString() : typeof value === 'string' ? value : ''))
|
|
1016
1017
|
.filter((value) => value.length > 0),
|
|
1017
1018
|
)
|
|
1018
|
-
const activeDeals = dealLinksForMetrics.filter(
|
|
1019
|
-
|
|
1020
|
-
)
|
|
1021
|
-
const wonDeals = dealLinksForMetrics.filter((deal) => deal.status === 'won')
|
|
1019
|
+
const activeDeals = dealLinksForMetrics.filter((deal) => isOpenDealStatus(deal.status))
|
|
1020
|
+
const wonDeals = dealLinksForMetrics.filter((deal) => isWonDealStatus(deal.status))
|
|
1022
1021
|
const activeDealsValue = activeDeals.reduce((sum, deal) => sum + (parseDealAmount(deal.valueAmount) ?? 0), 0)
|
|
1023
1022
|
const ltvValue = wonDeals.length
|
|
1024
1023
|
? wonDeals.reduce((sum, deal) => sum + (parseDealAmount(deal.valueAmount) ?? 0), 0)
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
filterActivePersonCompanyLinks,
|
|
26
26
|
withActiveCustomerPersonCompanyLinkFilter,
|
|
27
27
|
} from '../../../../../lib/personCompanyLinkTable'
|
|
28
|
+
import { isOpenDealStatus, isWonDealStatus } from '../../../../../lib/dealStatus'
|
|
28
29
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
29
30
|
|
|
30
31
|
const logger = createLogger('customers')
|
|
@@ -280,13 +281,13 @@ export async function GET(req: Request, ctx: { params?: { id?: string } }) {
|
|
|
280
281
|
|
|
281
282
|
const activeDeals = companyDealLinks
|
|
282
283
|
.map((dcl) => dcl.deal as CustomerDeal)
|
|
283
|
-
.filter((deal) => deal.status
|
|
284
|
+
.filter((deal) => isOpenDealStatus(deal.status) && !deal.deletedAt)
|
|
284
285
|
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime())
|
|
285
286
|
const activeDeal = activeDeals.length > 0 ? activeDeals[0] : null
|
|
286
287
|
|
|
287
288
|
const wonDeals = companyDealLinks
|
|
288
289
|
.map((dcl) => dcl.deal as CustomerDeal)
|
|
289
|
-
.filter((deal) => deal.status
|
|
290
|
+
.filter((deal) => isWonDealStatus(deal.status) && !deal.deletedAt)
|
|
290
291
|
let clv: { amount: number; currency: string } | null = null
|
|
291
292
|
if (wonDeals.length > 0) {
|
|
292
293
|
const currencies = new Map<string, number>()
|
|
@@ -7,6 +7,7 @@ import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
|
7
7
|
import { Badge } from '@open-mercato/ui/primitives/badge'
|
|
8
8
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
9
9
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
10
|
+
import { isOpenDealStatus } from '../../lib/dealStatus'
|
|
10
11
|
import type { DealSummary } from '../formConfig'
|
|
11
12
|
import { formatCurrency } from './utils'
|
|
12
13
|
|
|
@@ -25,7 +26,7 @@ export function ActiveDealCard({ deals, onHide }: ActiveDealCardProps) {
|
|
|
25
26
|
const [pipelineStages, setPipelineStages] = React.useState<PipelineStageOption[]>([])
|
|
26
27
|
|
|
27
28
|
const activeDeals = React.useMemo(
|
|
28
|
-
() => deals.filter((d) => d.status
|
|
29
|
+
() => deals.filter((d) => isOpenDealStatus(d.status)),
|
|
29
30
|
[deals],
|
|
30
31
|
)
|
|
31
32
|
|