@open-mercato/core 0.6.7-develop.6677.1.beabb7ca12 → 0.6.7-develop.6685.1.77c0a5591b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/gateway_payment_operation/index.js +2 -0
  3. package/dist/generated/entities/gateway_payment_operation/index.js.map +2 -2
  4. package/dist/generated/entities/gateway_transaction/index.js +2 -0
  5. package/dist/generated/entities/gateway_transaction/index.js.map +2 -2
  6. package/dist/generated/entity-fields-registry.js +2 -0
  7. package/dist/generated/entity-fields-registry.js.map +2 -2
  8. package/dist/modules/payment_gateways/api/capture/route.js +1 -1
  9. package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
  10. package/dist/modules/payment_gateways/data/entities.js +7 -0
  11. package/dist/modules/payment_gateways/data/entities.js.map +2 -2
  12. package/dist/modules/payment_gateways/lib/capture-ledger.js +183 -0
  13. package/dist/modules/payment_gateways/lib/capture-ledger.js.map +7 -0
  14. package/dist/modules/payment_gateways/lib/gateway-service.js +70 -8
  15. package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
  16. package/dist/modules/payment_gateways/lib/payment-operation-idempotency.js +1 -0
  17. package/dist/modules/payment_gateways/lib/payment-operation-idempotency.js.map +2 -2
  18. package/dist/modules/payment_gateways/migrations/Migration20260725101500_payment_gateways.js +16 -0
  19. package/dist/modules/payment_gateways/migrations/Migration20260725101500_payment_gateways.js.map +7 -0
  20. package/dist/modules/query_index/lib/indexer.js +12 -2
  21. package/dist/modules/query_index/lib/indexer.js.map +2 -2
  22. package/dist/modules/workflows/backend/definitions/visual-editor/page.js +5 -2
  23. package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
  24. package/dist/modules/workflows/components/ActivitiesEditor.js +67 -11
  25. package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
  26. package/dist/modules/workflows/components/TransitionsEditor.js +205 -163
  27. package/dist/modules/workflows/components/TransitionsEditor.js.map +2 -2
  28. package/dist/modules/workflows/data/validators.js +34 -1
  29. package/dist/modules/workflows/data/validators.js.map +3 -3
  30. package/dist/modules/workflows/lib/activity-executor.js +18 -2
  31. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  32. package/dist/modules/workflows/lib/format-validation-error.js +46 -1
  33. package/dist/modules/workflows/lib/format-validation-error.js.map +2 -2
  34. package/generated/entities/gateway_payment_operation/index.ts +1 -0
  35. package/generated/entities/gateway_transaction/index.ts +1 -0
  36. package/generated/entity-fields-registry.ts +2 -0
  37. package/package.json +7 -7
  38. package/src/modules/payment_gateways/api/capture/route.ts +1 -1
  39. package/src/modules/payment_gateways/data/entities.ts +8 -2
  40. package/src/modules/payment_gateways/lib/capture-ledger.ts +260 -0
  41. package/src/modules/payment_gateways/lib/gateway-service.ts +83 -10
  42. package/src/modules/payment_gateways/lib/payment-operation-idempotency.ts +1 -0
  43. package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +34 -0
  44. package/src/modules/payment_gateways/migrations/Migration20260725101500_payment_gateways.ts +21 -0
  45. package/src/modules/query_index/lib/indexer.ts +25 -2
  46. package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +12 -3
  47. package/src/modules/workflows/components/ActivitiesEditor.tsx +96 -12
  48. package/src/modules/workflows/components/TransitionsEditor.tsx +75 -17
  49. package/src/modules/workflows/data/validators.ts +52 -1
  50. package/src/modules/workflows/i18n/de.json +2 -0
  51. package/src/modules/workflows/i18n/en.json +2 -0
  52. package/src/modules/workflows/i18n/es.json +2 -0
  53. package/src/modules/workflows/i18n/pl.json +2 -0
  54. package/src/modules/workflows/lib/activity-executor.ts +38 -3
  55. package/src/modules/workflows/lib/format-validation-error.ts +60 -0
@@ -83,7 +83,7 @@ const openApi = {
83
83
  tags: [paymentGatewaysTag],
84
84
  responses: [
85
85
  { status: 200, description: "Payment captured" },
86
- { status: 409, description: "Invalid payment status transition" },
86
+ { status: 409, description: "Invalid payment status transition, cumulative capture ceiling exceeded, or conflicting capture operation" },
87
87
  { status: 422, description: "Invalid payload" },
88
88
  { status: 502, description: "Gateway provider error" }
89
89
  ]
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/payment_gateways/api/capture/route.ts"],
4
- "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { captureSchema } from '../../data/validators'\nimport type { PaymentGatewayService } from '../../lib/gateway-service'\nimport { paymentGatewaysTag } from '../openapi'\nimport {\n resolveUserFeatures,\n runPaymentGatewayMutationGuardAfterSuccess,\n runPaymentGatewayMutationGuards,\n} from '../guards'\n\nconst gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'\n\nexport const metadata = {\n path: '/payment_gateways/capture',\n POST: { requireAuth: true, requireFeatures: ['payment_gateways.capture'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const payload = await readJsonSafe<unknown>(req)\n const parsed = captureSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const guardResult = await runPaymentGatewayMutationGuards(\n container,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: gatewayTransactionResourceKind,\n resourceId: parsed.data.transactionId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as Record<string, unknown>,\n },\n resolveUserFeatures(auth),\n )\n if (!guardResult.ok) {\n return NextResponse.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const service = container.resolve('paymentGatewayService') as PaymentGatewayService\n\n try {\n const result = await service.capturePayment(\n parsed.data.transactionId,\n parsed.data.amount,\n { organizationId: auth.orgId as string, tenantId: auth.tenantId },\n parsed.data.operationId,\n )\n await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: gatewayTransactionResourceKind,\n resourceId: parsed.data.transactionId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return NextResponse.json(result)\n } catch (err: unknown) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const message = err instanceof Error ? err.message : 'Capture failed'\n return NextResponse.json({ error: message }, { status: 502 })\n }\n}\n\nexport const openApi = {\n tags: [paymentGatewaysTag],\n summary: 'Capture an authorized payment',\n methods: {\n POST: {\n summary: 'Capture payment',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 200, description: 'Payment captured' },\n { status: 409, description: 'Invalid payment status transition' },\n { status: 422, description: 'Invalid payload' },\n { status: 502, description: 'Gateway provider error' },\n ],\n },\n },\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { NextResponse } from 'next/server'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe'\nimport { isCrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { captureSchema } from '../../data/validators'\nimport type { PaymentGatewayService } from '../../lib/gateway-service'\nimport { paymentGatewaysTag } from '../openapi'\nimport {\n resolveUserFeatures,\n runPaymentGatewayMutationGuardAfterSuccess,\n runPaymentGatewayMutationGuards,\n} from '../guards'\n\nconst gatewayTransactionResourceKind = 'payment_gateways.gateway_transaction'\n\nexport const metadata = {\n path: '/payment_gateways/capture',\n POST: { requireAuth: true, requireFeatures: ['payment_gateways.capture'] },\n}\n\nexport async function POST(req: Request) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.tenantId || !auth.orgId) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const payload = await readJsonSafe<unknown>(req)\n const parsed = captureSchema.safeParse(payload)\n if (!parsed.success) {\n return NextResponse.json({ error: 'Invalid payload', details: parsed.error.flatten() }, { status: 422 })\n }\n\n const container = await createRequestContainer()\n const guardResult = await runPaymentGatewayMutationGuards(\n container,\n {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: gatewayTransactionResourceKind,\n resourceId: parsed.data.transactionId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n mutationPayload: parsed.data as Record<string, unknown>,\n },\n resolveUserFeatures(auth),\n )\n if (!guardResult.ok) {\n return NextResponse.json(\n guardResult.errorBody ?? { error: 'Operation blocked by guard' },\n { status: guardResult.errorStatus ?? 422 },\n )\n }\n\n const service = container.resolve('paymentGatewayService') as PaymentGatewayService\n\n try {\n const result = await service.capturePayment(\n parsed.data.transactionId,\n parsed.data.amount,\n { organizationId: auth.orgId as string, tenantId: auth.tenantId },\n parsed.data.operationId,\n )\n await runPaymentGatewayMutationGuardAfterSuccess(guardResult.afterSuccessCallbacks, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n userId: auth.sub ?? '',\n resourceKind: gatewayTransactionResourceKind,\n resourceId: parsed.data.transactionId,\n operation: 'update',\n requestMethod: req.method,\n requestHeaders: req.headers,\n })\n return NextResponse.json(result)\n } catch (err: unknown) {\n if (isCrudHttpError(err)) {\n return NextResponse.json(err.body, { status: err.status })\n }\n const message = err instanceof Error ? err.message : 'Capture failed'\n return NextResponse.json({ error: message }, { status: 502 })\n }\n}\n\nexport const openApi = {\n tags: [paymentGatewaysTag],\n summary: 'Capture an authorized payment',\n methods: {\n POST: {\n summary: 'Capture payment',\n tags: [paymentGatewaysTag],\n responses: [\n { status: 200, description: 'Payment captured' },\n { status: 409, description: 'Invalid payment status transition, cumulative capture ceiling exceeded, or conflicting capture operation' },\n { status: 422, description: 'Invalid payload' },\n { status: 502, description: 'Gateway provider error' },\n ],\n },\n },\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -14,6 +14,7 @@ OptionalProps;
14
14
  let GatewayTransaction = class {
15
15
  constructor() {
16
16
  this.unifiedStatus = "pending";
17
+ this.capturedAmount = "0";
17
18
  this.createdAt = /* @__PURE__ */ new Date();
18
19
  this.updatedAt = /* @__PURE__ */ new Date();
19
20
  }
@@ -51,6 +52,9 @@ __decorateClass([
51
52
  __decorateClass([
52
53
  Property({ name: "amount", type: "numeric", precision: 18, scale: 4 })
53
54
  ], GatewayTransaction.prototype, "amount", 2);
55
+ __decorateClass([
56
+ Property({ name: "captured_amount", type: "numeric", precision: 18, scale: 4, default: "0" })
57
+ ], GatewayTransaction.prototype, "capturedAmount", 2);
54
58
  __decorateClass([
55
59
  Property({ name: "currency_code", type: "text" })
56
60
  ], GatewayTransaction.prototype, "currencyCode", 2);
@@ -132,6 +136,9 @@ __decorateClass([
132
136
  __decorateClass([
133
137
  Property({ name: "result", type: "jsonb", nullable: true })
134
138
  ], GatewayPaymentOperation.prototype, "result", 2);
139
+ __decorateClass([
140
+ Property({ name: "reserved_amount", type: "numeric", precision: 18, scale: 4, nullable: true })
141
+ ], GatewayPaymentOperation.prototype, "reservedAmount", 2);
135
142
  __decorateClass([
136
143
  Property({ name: "lease_expires_at", type: Date, nullable: true })
137
144
  ], GatewayPaymentOperation.prototype, "leaseExpiresAt", 2);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/payment_gateways/data/entities.ts"],
4
- "sourcesContent": ["import { OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: 'gateway_transactions' })\n@Index({ properties: ['paymentId', 'organizationId', 'tenantId'] })\n@Index({ properties: ['providerKey', 'providerSessionId', 'organizationId'] })\n@Index({ properties: ['organizationId', 'tenantId', 'unifiedStatus'] })\nexport class GatewayTransaction {\n [OptionalProps]?: 'unifiedStatus' | 'gatewayStatus' | 'providerSessionId' | 'gatewayPaymentId' | 'gatewayRefundId' | 'redirectUrl' | 'clientSecret' | 'gatewayMetadata' | 'webhookLog' | 'lastWebhookAt' | 'lastPolledAt' | 'expiresAt' | 'createdAt' | 'updatedAt' | 'deletedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'payment_id', type: 'uuid' })\n paymentId!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'provider_session_id', type: 'text', nullable: true })\n providerSessionId?: string | null\n\n @Property({ name: 'gateway_payment_id', type: 'text', nullable: true })\n gatewayPaymentId?: string | null\n\n @Property({ name: 'gateway_refund_id', type: 'text', nullable: true })\n gatewayRefundId?: string | null\n\n @Property({ name: 'unified_status', type: 'text' })\n unifiedStatus: string = 'pending'\n\n @Property({ name: 'gateway_status', type: 'text', nullable: true })\n gatewayStatus?: string | null\n\n @Property({ name: 'redirect_url', type: 'text', nullable: true })\n redirectUrl?: string | null\n\n @Property({ name: 'client_secret', type: 'text', nullable: true })\n clientSecret?: string | null\n\n @Property({ name: 'amount', type: 'numeric', precision: 18, scale: 4 })\n amount!: string\n\n @Property({ name: 'currency_code', type: 'text' })\n currencyCode!: string\n\n @Property({ name: 'gateway_metadata', type: 'jsonb', nullable: true })\n gatewayMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'webhook_log', type: 'jsonb', nullable: true })\n webhookLog?: Array<{ eventType: string; receivedAt: string; idempotencyKey: string; unifiedStatus: string; processed: boolean }> | null\n\n @Property({ name: 'last_webhook_at', type: Date, nullable: true })\n lastWebhookAt?: Date | null\n\n @Property({ name: 'last_polled_at', type: Date, nullable: true })\n lastPolledAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n expiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n@Entity({ tableName: 'gateway_payment_operations' })\n@Unique({\n name: 'gateway_payment_operations_scope_operation_unique',\n properties: ['operationId', 'organizationId', 'tenantId'],\n})\n@Index({ properties: ['transactionId', 'operationType', 'organizationId', 'tenantId'] })\n@Index({ properties: ['status', 'leaseExpiresAt'] })\nexport class GatewayPaymentOperation {\n [OptionalProps]?: 'status' | 'attemptCount' | 'result' | 'leaseExpiresAt' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_id', type: 'text' })\n operationId!: string\n\n @Property({ name: 'transaction_id', type: 'uuid' })\n transactionId!: string\n\n @Property({ name: 'operation_type', type: 'text' })\n operationType!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'request_hash', type: 'text' })\n requestHash!: string\n\n @Property({ name: 'provider_idempotency_key', type: 'text' })\n providerIdempotencyKey!: string\n\n @Property({ name: 'status', type: 'text' })\n status: string = 'in_progress'\n\n @Property({ name: 'attempt_token', type: 'text' })\n attemptToken!: string\n\n @Property({ name: 'attempt_count', type: 'integer' })\n attemptCount: number = 1\n\n @Property({ name: 'result', type: 'jsonb', nullable: true })\n result?: Record<string, unknown> | null\n\n @Property({ name: 'lease_expires_at', type: Date, nullable: true })\n leaseExpiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_session_initializations' })\n@Unique({\n name: 'gateway_session_initializations_scope_operation_unique',\n properties: ['operationKey', 'providerKey', 'organizationId', 'tenantId'],\n})\nexport class GatewaySessionInitialization {\n [OptionalProps]?: 'claimToken' | 'claimedAt' | 'gatewayTransactionId' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_key', type: 'text' })\n operationKey!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'claim_token', type: 'uuid', nullable: true })\n claimToken?: string | null\n\n @Property({ name: 'claimed_at', type: Date, nullable: true })\n claimedAt?: Date | null\n\n @Property({ name: 'gateway_transaction_id', type: 'uuid', nullable: true })\n gatewayTransactionId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_webhook_events' })\n@Unique({\n name: 'gateway_webhook_events_idempotency_unique',\n properties: ['idempotencyKey', 'providerKey', 'organizationId', 'tenantId'],\n})\nexport class WebhookProcessedEvent {\n [OptionalProps]?: 'processedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'idempotency_key', type: 'text' })\n idempotencyKey!: string\n\n @Property({ name: 'event_type', type: 'text' })\n eventType!: string\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'processed_at', type: Date, onCreate: () => new Date() })\n processedAt: Date = new Date()\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { OptionalProps } from '@mikro-orm/core'\nimport { Entity, Index, PrimaryKey, Property, Unique } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: 'gateway_transactions' })\n@Index({ properties: ['paymentId', 'organizationId', 'tenantId'] })\n@Index({ properties: ['providerKey', 'providerSessionId', 'organizationId'] })\n@Index({ properties: ['organizationId', 'tenantId', 'unifiedStatus'] })\nexport class GatewayTransaction {\n [OptionalProps]?: 'unifiedStatus' | 'gatewayStatus' | 'providerSessionId' | 'gatewayPaymentId' | 'gatewayRefundId' | 'redirectUrl' | 'clientSecret' | 'capturedAmount' | 'gatewayMetadata' | 'webhookLog' | 'lastWebhookAt' | 'lastPolledAt' | 'expiresAt' | 'createdAt' | 'updatedAt' | 'deletedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'payment_id', type: 'uuid' })\n paymentId!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'provider_session_id', type: 'text', nullable: true })\n providerSessionId?: string | null\n\n @Property({ name: 'gateway_payment_id', type: 'text', nullable: true })\n gatewayPaymentId?: string | null\n\n @Property({ name: 'gateway_refund_id', type: 'text', nullable: true })\n gatewayRefundId?: string | null\n\n @Property({ name: 'unified_status', type: 'text' })\n unifiedStatus: string = 'pending'\n\n @Property({ name: 'gateway_status', type: 'text', nullable: true })\n gatewayStatus?: string | null\n\n @Property({ name: 'redirect_url', type: 'text', nullable: true })\n redirectUrl?: string | null\n\n @Property({ name: 'client_secret', type: 'text', nullable: true })\n clientSecret?: string | null\n\n @Property({ name: 'amount', type: 'numeric', precision: 18, scale: 4 })\n amount!: string\n\n @Property({ name: 'captured_amount', type: 'numeric', precision: 18, scale: 4, default: '0' })\n capturedAmount: string = '0'\n\n @Property({ name: 'currency_code', type: 'text' })\n currencyCode!: string\n\n @Property({ name: 'gateway_metadata', type: 'jsonb', nullable: true })\n gatewayMetadata?: Record<string, unknown> | null\n\n @Property({ name: 'webhook_log', type: 'jsonb', nullable: true })\n webhookLog?: Array<{ eventType: string; receivedAt: string; idempotencyKey: string; unifiedStatus: string; processed: boolean }> | null\n\n @Property({ name: 'last_webhook_at', type: Date, nullable: true })\n lastWebhookAt?: Date | null\n\n @Property({ name: 'last_polled_at', type: Date, nullable: true })\n lastPolledAt?: Date | null\n\n @Property({ name: 'expires_at', type: Date, nullable: true })\n expiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n\n @Property({ name: 'deleted_at', type: Date, nullable: true })\n deletedAt?: Date | null\n}\n\n@Entity({ tableName: 'gateway_payment_operations' })\n@Unique({\n name: 'gateway_payment_operations_scope_operation_unique',\n properties: ['operationId', 'organizationId', 'tenantId'],\n})\n@Index({ properties: ['transactionId', 'operationType', 'organizationId', 'tenantId'] })\n@Index({ properties: ['status', 'leaseExpiresAt'] })\nexport class GatewayPaymentOperation {\n [OptionalProps]?: 'status' | 'attemptCount' | 'result' | 'reservedAmount' | 'leaseExpiresAt' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_id', type: 'text' })\n operationId!: string\n\n @Property({ name: 'transaction_id', type: 'uuid' })\n transactionId!: string\n\n @Property({ name: 'operation_type', type: 'text' })\n operationType!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'request_hash', type: 'text' })\n requestHash!: string\n\n @Property({ name: 'provider_idempotency_key', type: 'text' })\n providerIdempotencyKey!: string\n\n @Property({ name: 'status', type: 'text' })\n status: string = 'in_progress'\n\n @Property({ name: 'attempt_token', type: 'text' })\n attemptToken!: string\n\n @Property({ name: 'attempt_count', type: 'integer' })\n attemptCount: number = 1\n\n @Property({ name: 'result', type: 'jsonb', nullable: true })\n result?: Record<string, unknown> | null\n\n @Property({ name: 'reserved_amount', type: 'numeric', precision: 18, scale: 4, nullable: true })\n reservedAmount?: string | null\n\n @Property({ name: 'lease_expires_at', type: Date, nullable: true })\n leaseExpiresAt?: Date | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_session_initializations' })\n@Unique({\n name: 'gateway_session_initializations_scope_operation_unique',\n properties: ['operationKey', 'providerKey', 'organizationId', 'tenantId'],\n})\nexport class GatewaySessionInitialization {\n [OptionalProps]?: 'claimToken' | 'claimedAt' | 'gatewayTransactionId' | 'createdAt' | 'updatedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'operation_key', type: 'text' })\n operationKey!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'claim_token', type: 'uuid', nullable: true })\n claimToken?: string | null\n\n @Property({ name: 'claimed_at', type: Date, nullable: true })\n claimedAt?: Date | null\n\n @Property({ name: 'gateway_transaction_id', type: 'uuid', nullable: true })\n gatewayTransactionId?: string | null\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })\n createdAt: Date = new Date()\n\n @Property({ name: 'updated_at', type: Date, onCreate: () => new Date(), onUpdate: () => new Date() })\n updatedAt: Date = new Date()\n}\n\n@Entity({ tableName: 'gateway_webhook_events' })\n@Unique({\n name: 'gateway_webhook_events_idempotency_unique',\n properties: ['idempotencyKey', 'providerKey', 'organizationId', 'tenantId'],\n})\nexport class WebhookProcessedEvent {\n [OptionalProps]?: 'processedAt'\n\n @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })\n id!: string\n\n @Property({ name: 'provider_key', type: 'text' })\n providerKey!: string\n\n @Property({ name: 'idempotency_key', type: 'text' })\n idempotencyKey!: string\n\n @Property({ name: 'event_type', type: 'text' })\n eventType!: string\n\n @Property({ name: 'organization_id', type: 'uuid' })\n organizationId!: string\n\n @Property({ name: 'tenant_id', type: 'uuid' })\n tenantId!: string\n\n @Property({ name: 'processed_at', type: Date, onCreate: () => new Date() })\n processedAt: Date = new Date()\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,183 @@
1
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
2
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { GatewayPaymentOperation, GatewayTransaction } from "../data/entities.js";
4
+ const AMOUNT_SCALE = 4;
5
+ const AMOUNT_UNITS_PER_WHOLE = 10n ** BigInt(AMOUNT_SCALE);
6
+ const DECIMAL_PATTERN = /^(-?)(\d*)(?:\.(\d*))?$/;
7
+ function parseAmountUnits(value) {
8
+ if (value === null || value === void 0) return 0n;
9
+ const text = typeof value === "number" ? value.toFixed(AMOUNT_SCALE) : value.trim();
10
+ const match = DECIMAL_PATTERN.exec(text);
11
+ const whole = match?.[2] ?? "";
12
+ const fraction = match?.[3] ?? "";
13
+ if (!match || whole === "" && fraction === "") {
14
+ throw new Error(`[internal] Unsupported gateway amount value: ${String(value)}`);
15
+ }
16
+ const keptFraction = fraction.slice(0, AMOUNT_SCALE).padEnd(AMOUNT_SCALE, "0");
17
+ const nextDigit = fraction.charAt(AMOUNT_SCALE);
18
+ let units = BigInt(whole === "" ? "0" : whole) * AMOUNT_UNITS_PER_WHOLE + BigInt(keptFraction);
19
+ if (nextDigit !== "" && Number(nextDigit) >= 5) units += 1n;
20
+ return match[1] === "-" ? -units : units;
21
+ }
22
+ function formatAmountUnits(units) {
23
+ const negative = units < 0n;
24
+ const absolute = negative ? -units : units;
25
+ const fraction = (absolute % AMOUNT_UNITS_PER_WHOLE).toString().padStart(AMOUNT_SCALE, "0");
26
+ return `${negative ? "-" : ""}${absolute / AMOUNT_UNITS_PER_WHOLE}.${fraction}`;
27
+ }
28
+ function formatAmountLabel(units) {
29
+ return formatAmountUnits(units).replace(/\.?0+$/, "");
30
+ }
31
+ function captureConflict(code, message) {
32
+ return new CrudHttpError(409, { error: message, code });
33
+ }
34
+ function resolveCaptureAmounts(transaction, amount) {
35
+ const authorizedUnits = parseAmountUnits(transaction.amount);
36
+ const capturedUnits = parseAmountUnits(transaction.capturedAmount);
37
+ const remainingUnits = authorizedUnits - capturedUnits;
38
+ return {
39
+ authorizedUnits,
40
+ capturedUnits,
41
+ remainingUnits,
42
+ requestedUnits: amount === void 0 ? remainingUnits : parseAmountUnits(amount)
43
+ };
44
+ }
45
+ function assertCaptureWithinRemaining(transaction, amount) {
46
+ const amounts = resolveCaptureAmounts(transaction, amount);
47
+ if (amounts.remainingUnits <= 0n) {
48
+ throw captureConflict(
49
+ "payment_capture_ceiling_exceeded",
50
+ `Transaction is already fully captured (${formatAmountLabel(amounts.capturedUnits)} of ${formatAmountLabel(amounts.authorizedUnits)})`
51
+ );
52
+ }
53
+ if (amounts.requestedUnits <= 0n) {
54
+ throw captureConflict("payment_capture_amount_invalid", "Capture amount must be greater than zero");
55
+ }
56
+ if (amounts.requestedUnits > amounts.remainingUnits) {
57
+ throw captureConflict(
58
+ "payment_capture_ceiling_exceeded",
59
+ `Capture amount ${formatAmountLabel(amounts.requestedUnits)} exceeds the ${formatAmountLabel(amounts.remainingUnits)} still capturable on authorized amount ${formatAmountLabel(amounts.authorizedUnits)} (already captured ${formatAmountLabel(amounts.capturedUnits)})`
60
+ );
61
+ }
62
+ return amounts;
63
+ }
64
+ async function reserveCaptureAmount(em, input) {
65
+ if (input.operation.reservedAmount !== null && input.operation.reservedAmount !== void 0) {
66
+ return parseAmountUnits(input.operation.reservedAmount);
67
+ }
68
+ const amounts = assertCaptureWithinRemaining(input.transaction, input.amount);
69
+ const previousCapturedAmount = input.transaction.capturedAmount;
70
+ const reservedAmount = formatAmountUnits(amounts.requestedUnits);
71
+ const nextCapturedAmount = formatAmountUnits(amounts.capturedUnits + amounts.requestedUnits);
72
+ await em.transactional(async (tx) => {
73
+ const reserved = await tx.nativeUpdate(
74
+ GatewayTransaction,
75
+ {
76
+ id: input.transaction.id,
77
+ organizationId: input.scope.organizationId,
78
+ tenantId: input.scope.tenantId,
79
+ deletedAt: null,
80
+ capturedAmount: previousCapturedAmount
81
+ },
82
+ { capturedAmount: nextCapturedAmount, updatedAt: /* @__PURE__ */ new Date() }
83
+ );
84
+ if (reserved !== 1) {
85
+ throw captureConflict(
86
+ "payment_capture_reservation_conflict",
87
+ "This transaction changed while the capture amount was being reserved; re-read it and retry with the remaining amount"
88
+ );
89
+ }
90
+ const stamped = await tx.nativeUpdate(
91
+ GatewayPaymentOperation,
92
+ {
93
+ id: input.operation.id,
94
+ organizationId: input.scope.organizationId,
95
+ tenantId: input.scope.tenantId,
96
+ reservedAmount: null
97
+ },
98
+ { reservedAmount, updatedAt: /* @__PURE__ */ new Date() }
99
+ );
100
+ if (stamped !== 1) {
101
+ throw captureConflict(
102
+ "payment_capture_reservation_conflict",
103
+ "Capture reservation could not be recorded for this operation"
104
+ );
105
+ }
106
+ });
107
+ Object.assign(input.operation, { reservedAmount });
108
+ return amounts.requestedUnits;
109
+ }
110
+ function settleCapturedAmount(transaction, reservedUnits, capturedAmount) {
111
+ const actualUnits = typeof capturedAmount === "number" && Number.isFinite(capturedAmount) && capturedAmount >= 0 ? parseAmountUnits(capturedAmount) : reservedUnits;
112
+ if (actualUnits === reservedUnits) return;
113
+ const settledUnits = parseAmountUnits(transaction.capturedAmount) - reservedUnits + actualUnits;
114
+ transaction.capturedAmount = formatAmountUnits(settledUnits < 0n ? 0n : settledUnits);
115
+ }
116
+ function alignCapturedAmountWithStatus(transaction, status) {
117
+ if (status !== "captured") return;
118
+ const authorizedUnits = parseAmountUnits(transaction.amount);
119
+ if (parseAmountUnits(transaction.capturedAmount) >= authorizedUnits) return;
120
+ transaction.capturedAmount = formatAmountUnits(authorizedUnits);
121
+ }
122
+ async function releaseCaptureAmount(em, input) {
123
+ const reservedAmount = formatAmountUnits(input.reservedUnits);
124
+ try {
125
+ return await em.transactional(async (tx) => {
126
+ const current = await findOneWithDecryption(
127
+ tx,
128
+ GatewayTransaction,
129
+ {
130
+ id: input.transactionId,
131
+ organizationId: input.scope.organizationId,
132
+ tenantId: input.scope.tenantId
133
+ },
134
+ void 0,
135
+ input.scope
136
+ );
137
+ if (!current) return false;
138
+ const releasedUnits = parseAmountUnits(current.capturedAmount) - input.reservedUnits;
139
+ const released = await tx.nativeUpdate(
140
+ GatewayTransaction,
141
+ {
142
+ id: input.transactionId,
143
+ organizationId: input.scope.organizationId,
144
+ tenantId: input.scope.tenantId,
145
+ capturedAmount: current.capturedAmount
146
+ },
147
+ { capturedAmount: formatAmountUnits(releasedUnits < 0n ? 0n : releasedUnits), updatedAt: /* @__PURE__ */ new Date() }
148
+ );
149
+ if (released !== 1) return false;
150
+ const cleared = await tx.nativeUpdate(
151
+ GatewayPaymentOperation,
152
+ {
153
+ id: input.operation.id,
154
+ organizationId: input.scope.organizationId,
155
+ tenantId: input.scope.tenantId,
156
+ reservedAmount
157
+ },
158
+ { reservedAmount: null, updatedAt: /* @__PURE__ */ new Date() }
159
+ );
160
+ if (cleared !== 1) {
161
+ throw captureConflict(
162
+ "payment_capture_reservation_conflict",
163
+ "Capture reservation could not be released for this operation"
164
+ );
165
+ }
166
+ Object.assign(input.operation, { reservedAmount: null });
167
+ return true;
168
+ });
169
+ } catch {
170
+ return false;
171
+ }
172
+ }
173
+ export {
174
+ alignCapturedAmountWithStatus,
175
+ assertCaptureWithinRemaining,
176
+ formatAmountUnits,
177
+ parseAmountUnits,
178
+ releaseCaptureAmount,
179
+ reserveCaptureAmount,
180
+ resolveCaptureAmounts,
181
+ settleCapturedAmount
182
+ };
183
+ //# sourceMappingURL=capture-ledger.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/payment_gateways/lib/capture-ledger.ts"],
4
+ "sourcesContent": ["import type { EntityManager } from '@mikro-orm/postgresql'\nimport { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'\nimport { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport type { UnifiedPaymentStatus } from '@open-mercato/shared/modules/payment_gateways/types'\nimport { GatewayPaymentOperation, GatewayTransaction } from '../data/entities'\n\ntype Scope = { organizationId: string; tenantId: string }\n\nconst AMOUNT_SCALE = 4\nconst AMOUNT_UNITS_PER_WHOLE = 10n ** BigInt(AMOUNT_SCALE)\nconst DECIMAL_PATTERN = /^(-?)(\\d*)(?:\\.(\\d*))?$/\n\n/**\n * Money in this module lives in `numeric(18,4)` columns and is read back as strings. Every\n * comparison and every sum runs on integer minor units (1 unit = 10^-4) so partial captures\n * never accumulate floating-point drift. Extra precision beyond four decimals is rounded\n * half-up, matching what Postgres would store in the column anyway.\n */\nexport function parseAmountUnits(value: string | number | null | undefined): bigint {\n if (value === null || value === undefined) return 0n\n const text = typeof value === 'number' ? value.toFixed(AMOUNT_SCALE) : value.trim()\n const match = DECIMAL_PATTERN.exec(text)\n const whole = match?.[2] ?? ''\n const fraction = match?.[3] ?? ''\n if (!match || (whole === '' && fraction === '')) {\n throw new Error(`[internal] Unsupported gateway amount value: ${String(value)}`)\n }\n const keptFraction = fraction.slice(0, AMOUNT_SCALE).padEnd(AMOUNT_SCALE, '0')\n const nextDigit = fraction.charAt(AMOUNT_SCALE)\n let units = BigInt(whole === '' ? '0' : whole) * AMOUNT_UNITS_PER_WHOLE + BigInt(keptFraction)\n if (nextDigit !== '' && Number(nextDigit) >= 5) units += 1n\n return match[1] === '-' ? -units : units\n}\n\nexport function formatAmountUnits(units: bigint): string {\n const negative = units < 0n\n const absolute = negative ? -units : units\n const fraction = (absolute % AMOUNT_UNITS_PER_WHOLE).toString().padStart(AMOUNT_SCALE, '0')\n return `${negative ? '-' : ''}${absolute / AMOUNT_UNITS_PER_WHOLE}.${fraction}`\n}\n\nfunction formatAmountLabel(units: bigint): string {\n return formatAmountUnits(units).replace(/\\.?0+$/, '')\n}\n\nfunction captureConflict(code: string, message: string): CrudHttpError {\n return new CrudHttpError(409, { error: message, code })\n}\n\nexport type CaptureAmounts = {\n authorizedUnits: bigint\n capturedUnits: bigint\n remainingUnits: bigint\n requestedUnits: bigint\n}\n\n/**\n * Resolves what a capture request means for the transaction's running total. An omitted\n * amount captures everything that is still authorized, not the original full amount.\n */\nexport function resolveCaptureAmounts(transaction: GatewayTransaction, amount: number | undefined): CaptureAmounts {\n const authorizedUnits = parseAmountUnits(transaction.amount)\n const capturedUnits = parseAmountUnits(transaction.capturedAmount)\n const remainingUnits = authorizedUnits - capturedUnits\n return {\n authorizedUnits,\n capturedUnits,\n remainingUnits,\n requestedUnits: amount === undefined ? remainingUnits : parseAmountUnits(amount),\n }\n}\n\n/**\n * Rejects a capture whose amount would push the captured-to-date total past the authorized\n * amount. This is the cumulative ceiling: each individual request may look small, but the\n * sum of all captures against one authorization can never exceed it (#4487).\n */\nexport function assertCaptureWithinRemaining(\n transaction: GatewayTransaction,\n amount: number | undefined,\n): CaptureAmounts {\n const amounts = resolveCaptureAmounts(transaction, amount)\n if (amounts.remainingUnits <= 0n) {\n throw captureConflict(\n 'payment_capture_ceiling_exceeded',\n `Transaction is already fully captured (${formatAmountLabel(amounts.capturedUnits)} of ${formatAmountLabel(amounts.authorizedUnits)})`,\n )\n }\n if (amounts.requestedUnits <= 0n) {\n throw captureConflict('payment_capture_amount_invalid', 'Capture amount must be greater than zero')\n }\n if (amounts.requestedUnits > amounts.remainingUnits) {\n throw captureConflict(\n 'payment_capture_ceiling_exceeded',\n `Capture amount ${formatAmountLabel(amounts.requestedUnits)} exceeds the ${formatAmountLabel(amounts.remainingUnits)} still capturable on authorized amount ${formatAmountLabel(amounts.authorizedUnits)} (already captured ${formatAmountLabel(amounts.capturedUnits)})`,\n )\n }\n return amounts\n}\n\n/**\n * Claims the requested slice of the authorized amount before the provider is called, so two\n * concurrent captures can never both charge. The increment is a compare-and-swap on the\n * previous captured-to-date value: whoever loses it never reaches the adapter. The reserved\n * amount is stamped on the operation row in the same transaction, so a retry of the same\n * operation id reuses its reservation instead of reserving twice.\n */\nexport async function reserveCaptureAmount(em: EntityManager, input: {\n transaction: GatewayTransaction\n operation: GatewayPaymentOperation\n amount: number | undefined\n scope: Scope\n}): Promise<bigint> {\n if (input.operation.reservedAmount !== null && input.operation.reservedAmount !== undefined) {\n return parseAmountUnits(input.operation.reservedAmount)\n }\n const amounts = assertCaptureWithinRemaining(input.transaction, input.amount)\n const previousCapturedAmount = input.transaction.capturedAmount\n const reservedAmount = formatAmountUnits(amounts.requestedUnits)\n const nextCapturedAmount = formatAmountUnits(amounts.capturedUnits + amounts.requestedUnits)\n\n await em.transactional(async (tx) => {\n const reserved = await tx.nativeUpdate(\n GatewayTransaction,\n {\n id: input.transaction.id,\n organizationId: input.scope.organizationId,\n tenantId: input.scope.tenantId,\n deletedAt: null,\n capturedAmount: previousCapturedAmount,\n },\n { capturedAmount: nextCapturedAmount, updatedAt: new Date() },\n )\n if (reserved !== 1) {\n throw captureConflict(\n 'payment_capture_reservation_conflict',\n 'This transaction changed while the capture amount was being reserved; re-read it and retry with the remaining amount',\n )\n }\n const stamped = await tx.nativeUpdate(\n GatewayPaymentOperation,\n {\n id: input.operation.id,\n organizationId: input.scope.organizationId,\n tenantId: input.scope.tenantId,\n reservedAmount: null,\n },\n { reservedAmount, updatedAt: new Date() },\n )\n if (stamped !== 1) {\n throw captureConflict(\n 'payment_capture_reservation_conflict',\n 'Capture reservation could not be recorded for this operation',\n )\n }\n })\n\n Object.assign(input.operation, { reservedAmount })\n return amounts.requestedUnits\n}\n\n/**\n * Reconciles the reservation with what the provider actually captured. Runs on the transaction\n * instance loaded inside the completion transaction, so the adjustment commits together with\n * the operation result.\n */\nexport function settleCapturedAmount(\n transaction: GatewayTransaction,\n reservedUnits: bigint,\n capturedAmount: number | undefined,\n): void {\n const actualUnits = typeof capturedAmount === 'number' && Number.isFinite(capturedAmount) && capturedAmount >= 0\n ? parseAmountUnits(capturedAmount)\n : reservedUnits\n if (actualUnits === reservedUnits) return\n const settledUnits = parseAmountUnits(transaction.capturedAmount) - reservedUnits + actualUnits\n transaction.capturedAmount = formatAmountUnits(settledUnits < 0n ? 0n : settledUnits)\n}\n\n/**\n * Keeps the ledger honest for captures that happened outside the capture endpoint. A webhook or a\n * status poll reports money that already moved but carries no captured amount, so only `captured`\n * \u2014 the provider saying the whole authorization was taken \u2014 can be translated into a number: the\n * full amount. The ledger is raised, never lowered, so an already recorded capture survives.\n * `partially_captured` carries no recoverable amount, so it is left alone and the remainder stays\n * capturable.\n */\nexport function alignCapturedAmountWithStatus(\n transaction: GatewayTransaction,\n status: UnifiedPaymentStatus,\n): void {\n if (status !== 'captured') return\n const authorizedUnits = parseAmountUnits(transaction.amount)\n if (parseAmountUnits(transaction.capturedAmount) >= authorizedUnits) return\n transaction.capturedAmount = formatAmountUnits(authorizedUnits)\n}\n\n/**\n * Gives the reserved slice back after a failed provider call. Both writes share one\n * transaction, so a partial release is impossible: either the amount becomes capturable again\n * and the operation stops holding it, or the reservation stays outstanding. Returning `false`\n * means the reservation is still held \u2014 the conservative outcome when it is unknown whether\n * the provider took the money.\n */\nexport async function releaseCaptureAmount(em: EntityManager, input: {\n transactionId: string\n operation: GatewayPaymentOperation\n reservedUnits: bigint\n scope: Scope\n}): Promise<boolean> {\n const reservedAmount = formatAmountUnits(input.reservedUnits)\n try {\n return await em.transactional(async (tx) => {\n const current = await findOneWithDecryption(\n tx,\n GatewayTransaction,\n {\n id: input.transactionId,\n organizationId: input.scope.organizationId,\n tenantId: input.scope.tenantId,\n },\n undefined,\n input.scope,\n )\n if (!current) return false\n const releasedUnits = parseAmountUnits(current.capturedAmount) - input.reservedUnits\n const released = await tx.nativeUpdate(\n GatewayTransaction,\n {\n id: input.transactionId,\n organizationId: input.scope.organizationId,\n tenantId: input.scope.tenantId,\n capturedAmount: current.capturedAmount,\n },\n { capturedAmount: formatAmountUnits(releasedUnits < 0n ? 0n : releasedUnits), updatedAt: new Date() },\n )\n if (released !== 1) return false\n const cleared = await tx.nativeUpdate(\n GatewayPaymentOperation,\n {\n id: input.operation.id,\n organizationId: input.scope.organizationId,\n tenantId: input.scope.tenantId,\n reservedAmount,\n },\n { reservedAmount: null, updatedAt: new Date() },\n )\n if (cleared !== 1) {\n throw captureConflict(\n 'payment_capture_reservation_conflict',\n 'Capture reservation could not be released for this operation',\n )\n }\n Object.assign(input.operation, { reservedAmount: null })\n return true\n })\n } catch {\n return false\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -8,6 +8,15 @@ import { GatewaySessionInitialization, GatewayTransaction } from "../data/entiti
8
8
  import { canApplyManualAction, isValidTransition } from "./status-machine.js";
9
9
  import { emitPaymentGatewayEvent } from "../events.js";
10
10
  import { readGatewayMetadata, readWebhookLog } from "./transaction-fields.js";
11
+ import {
12
+ alignCapturedAmountWithStatus,
13
+ assertCaptureWithinRemaining,
14
+ formatAmountUnits,
15
+ parseAmountUnits,
16
+ releaseCaptureAmount,
17
+ reserveCaptureAmount,
18
+ settleCapturedAmount
19
+ } from "./capture-ledger.js";
11
20
  import {
12
21
  completePaymentOperation,
13
22
  failPaymentOperation,
@@ -29,11 +38,6 @@ function assertManualActionAllowed(action, transaction) {
29
38
  throw conflict(`Cannot ${action} a payment in status "${current}"`);
30
39
  }
31
40
  }
32
- function assertCaptureAmountAllowed(amount, transaction) {
33
- if (amount !== void 0 && amount > Number(transaction.amount)) {
34
- throw conflict(`Capture amount ${amount} exceeds authorized transaction amount ${transaction.amount}`);
35
- }
36
- }
37
41
  function applyAdapterResultStatus(action, transaction, resultStatus) {
38
42
  const current = transaction.unifiedStatus;
39
43
  if (resultStatus === current) return false;
@@ -137,7 +141,11 @@ function createPaymentGatewayService(deps) {
137
141
  }
138
142
  return prepared.result;
139
143
  }
144
+ const operation = prepared.claim.record;
145
+ let settled = false;
146
+ let providerInvoked = false;
140
147
  try {
148
+ await input.beforeInvoke?.({ transaction, operation });
141
149
  const { adapter, credentials } = await resolveAdapterAndCredentials(
142
150
  transaction.providerKey,
143
151
  { organizationId: transaction.organizationId, tenantId: transaction.tenantId }
@@ -148,6 +156,7 @@ function createPaymentGatewayService(deps) {
148
156
  transaction,
149
157
  idempotencyKey: prepared.claim.providerIdempotencyKey
150
158
  });
159
+ providerInvoked = true;
151
160
  const statusChanged = await completePaymentOperation(
152
161
  em,
153
162
  prepared.claim,
@@ -159,6 +168,7 @@ function createPaymentGatewayService(deps) {
159
168
  return changed;
160
169
  }
161
170
  );
171
+ settled = true;
162
172
  if (statusChanged) {
163
173
  await emitStatusEvent(result.status, {
164
174
  transactionId: transaction.id,
@@ -171,7 +181,12 @@ function createPaymentGatewayService(deps) {
171
181
  await input.afterCommit(transaction, result);
172
182
  return result;
173
183
  } catch (error) {
174
- await Promise.allSettled([failPaymentOperation(em, prepared.claim)]);
184
+ if (!settled) {
185
+ if (input.releaseOnFailure) {
186
+ await input.releaseOnFailure({ transaction, operation, providerInvoked }).catch(() => void 0);
187
+ }
188
+ await Promise.allSettled([failPaymentOperation(em, prepared.claim)]);
189
+ }
175
190
  throw error;
176
191
  }
177
192
  }
@@ -184,6 +199,9 @@ function createPaymentGatewayService(deps) {
184
199
  redirectUrl: session.redirectUrl ?? (session.clientSession?.type === "redirect" ? session.clientSession.redirectUrl : null),
185
200
  clientSecret: session.clientSecret ?? null,
186
201
  amount: String(input.amount),
202
+ // An automatic-capture session comes back already captured, so the ledger has to start
203
+ // at the full amount — otherwise a later manual capture would look like the first one.
204
+ capturedAmount: session.status === "captured" ? String(input.amount) : "0",
187
205
  currencyCode: input.currencyCode,
188
206
  gatewayMetadata: {
189
207
  ...session.providerData ?? {},
@@ -356,16 +374,57 @@ function createPaymentGatewayService(deps) {
356
374
  }
357
375
  },
358
376
  async capturePayment(transactionId, amount, scope, operationId) {
377
+ let reservedUnits = null;
378
+ let providerAmount = amount;
359
379
  return executeManualOperation({
360
380
  action: "capture",
361
381
  transactionId,
362
382
  operationId,
363
383
  payload: { amount: amount ?? null },
364
384
  scope,
365
- assertInitialAllowed: (transaction) => assertCaptureAmountAllowed(amount, transaction),
385
+ assertInitialAllowed: (transaction) => {
386
+ assertCaptureWithinRemaining(transaction, amount);
387
+ },
388
+ beforeInvoke: async ({ transaction, operation }) => {
389
+ const alreadyCapturedUnits = parseAmountUnits(transaction.capturedAmount);
390
+ reservedUnits = await reserveCaptureAmount(em, { transaction, operation, amount, scope });
391
+ if (amount === void 0 && alreadyCapturedUnits > 0n) {
392
+ providerAmount = Number(formatAmountUnits(reservedUnits));
393
+ }
394
+ },
395
+ // The slice goes back only while the provider has not been called yet. Once the capture
396
+ // call returned, the money may already have moved and only the completion transaction
397
+ // failed, so releasing would reopen the authorization for a fresh operation id and allow an
398
+ // over-capture. Holding the reservation is the conservative outcome: a retry of this same
399
+ // operation id reuses both the reservation and the provider idempotency key, so the provider
400
+ // itself collapses the duplicate.
401
+ releaseOnFailure: async ({ transaction, operation, providerInvoked }) => {
402
+ if (reservedUnits === null) return;
403
+ if (providerInvoked) {
404
+ await writeTransactionLog(
405
+ transaction.providerKey,
406
+ { organizationId: transaction.organizationId, tenantId: transaction.tenantId },
407
+ transaction.id,
408
+ "warn",
409
+ "Capture reservation stays outstanding because the provider call already returned",
410
+ { operationId: operation.operationId, reservedAmount: formatAmountUnits(reservedUnits) }
411
+ );
412
+ return;
413
+ }
414
+ const released = await releaseCaptureAmount(em, { transactionId, operation, reservedUnits, scope });
415
+ if (released) return;
416
+ await writeTransactionLog(
417
+ transaction.providerKey,
418
+ { organizationId: transaction.organizationId, tenantId: transaction.tenantId },
419
+ transaction.id,
420
+ "warn",
421
+ "Capture reservation stays outstanding after a failed capture",
422
+ { operationId: operation.operationId, reservedAmount: formatAmountUnits(reservedUnits) }
423
+ );
424
+ },
366
425
  invoke: ({ adapter, credentials, transaction, idempotencyKey }) => adapter.capture({
367
426
  sessionId: readProviderSessionId(transaction),
368
- amount,
427
+ amount: providerAmount,
369
428
  credentials,
370
429
  idempotencyKey
371
430
  }),
@@ -374,6 +433,7 @@ function createPaymentGatewayService(deps) {
374
433
  ...readGatewayMetadata(transaction.gatewayMetadata),
375
434
  captureResult: result.providerData
376
435
  };
436
+ if (reservedUnits !== null) settleCapturedAmount(transaction, reservedUnits, result.capturedAmount);
377
437
  },
378
438
  afterCommit: (transaction, result) => writeTransactionLog(
379
439
  transaction.providerKey,
@@ -454,6 +514,7 @@ function createPaymentGatewayService(deps) {
454
514
  if (status.status !== transaction.unifiedStatus && isValidTransition(transaction.unifiedStatus, status.status)) {
455
515
  const previousStatus = transaction.unifiedStatus;
456
516
  transaction.unifiedStatus = status.status;
517
+ alignCapturedAmountWithStatus(transaction, status.status);
457
518
  transaction.gatewayStatus = status.status;
458
519
  transaction.gatewayMetadata = { ...readGatewayMetadata(transaction.gatewayMetadata), statusResult: status.providerData ?? null };
459
520
  transaction.lastPolledAt = /* @__PURE__ */ new Date();
@@ -488,6 +549,7 @@ function createPaymentGatewayService(deps) {
488
549
  const previousStatus = transaction.unifiedStatus;
489
550
  if (shouldApplyStatus) {
490
551
  transaction.unifiedStatus = update.unifiedStatus;
552
+ alignCapturedAmountWithStatus(transaction, update.unifiedStatus);
491
553
  }
492
554
  if (update.providerStatus) {
493
555
  transaction.gatewayStatus = update.providerStatus;