@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/gateway_payment_operation/index.js +2 -0
- package/dist/generated/entities/gateway_payment_operation/index.js.map +2 -2
- package/dist/generated/entities/gateway_transaction/index.js +2 -0
- package/dist/generated/entities/gateway_transaction/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/payment_gateways/api/capture/route.js +1 -1
- package/dist/modules/payment_gateways/api/capture/route.js.map +2 -2
- package/dist/modules/payment_gateways/data/entities.js +7 -0
- package/dist/modules/payment_gateways/data/entities.js.map +2 -2
- package/dist/modules/payment_gateways/lib/capture-ledger.js +183 -0
- package/dist/modules/payment_gateways/lib/capture-ledger.js.map +7 -0
- package/dist/modules/payment_gateways/lib/gateway-service.js +70 -8
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/payment-operation-idempotency.js +1 -0
- package/dist/modules/payment_gateways/lib/payment-operation-idempotency.js.map +2 -2
- package/dist/modules/payment_gateways/migrations/Migration20260725101500_payment_gateways.js +16 -0
- package/dist/modules/payment_gateways/migrations/Migration20260725101500_payment_gateways.js.map +7 -0
- package/dist/modules/query_index/lib/indexer.js +12 -2
- package/dist/modules/query_index/lib/indexer.js.map +2 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js +5 -2
- package/dist/modules/workflows/backend/definitions/visual-editor/page.js.map +2 -2
- package/dist/modules/workflows/components/ActivitiesEditor.js +67 -11
- package/dist/modules/workflows/components/ActivitiesEditor.js.map +2 -2
- package/dist/modules/workflows/components/TransitionsEditor.js +205 -163
- package/dist/modules/workflows/components/TransitionsEditor.js.map +2 -2
- package/dist/modules/workflows/data/validators.js +34 -1
- package/dist/modules/workflows/data/validators.js.map +3 -3
- package/dist/modules/workflows/lib/activity-executor.js +18 -2
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/format-validation-error.js +46 -1
- package/dist/modules/workflows/lib/format-validation-error.js.map +2 -2
- package/generated/entities/gateway_payment_operation/index.ts +1 -0
- package/generated/entities/gateway_transaction/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/payment_gateways/api/capture/route.ts +1 -1
- package/src/modules/payment_gateways/data/entities.ts +8 -2
- package/src/modules/payment_gateways/lib/capture-ledger.ts +260 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +83 -10
- package/src/modules/payment_gateways/lib/payment-operation-idempotency.ts +1 -0
- package/src/modules/payment_gateways/migrations/.snapshot-open-mercato.json +34 -0
- package/src/modules/payment_gateways/migrations/Migration20260725101500_payment_gateways.ts +21 -0
- package/src/modules/query_index/lib/indexer.ts +25 -2
- package/src/modules/workflows/backend/definitions/visual-editor/page.tsx +12 -3
- package/src/modules/workflows/components/ActivitiesEditor.tsx +96 -12
- package/src/modules/workflows/components/TransitionsEditor.tsx +75 -17
- package/src/modules/workflows/data/validators.ts +52 -1
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
- package/src/modules/workflows/lib/activity-executor.ts +38 -3
- 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": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AACvC,SAAS,oBAAoB;AAC7B,SAAS,uBAAuB;AAChC,SAAS,qBAAqB;AAE9B,SAAS,0BAA0B;AACnC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,iCAAiC;AAEhC,MAAM,WAAW;AAAA,EACtB,MAAM;AAAA,EACN,MAAM,EAAE,aAAa,MAAM,iBAAiB,CAAC,0BAA0B,EAAE;AAC3E;AAEA,eAAsB,KAAK,KAAc;AACvC,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,YAAY,CAAC,KAAK,OAAO;AAClC,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,QAAM,UAAU,MAAM,aAAsB,GAAG;AAC/C,QAAM,SAAS,cAAc,UAAU,OAAO;AAC9C,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,aAAa,KAAK,EAAE,OAAO,mBAAmB,SAAS,OAAO,MAAM,QAAQ,EAAE,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACzG;AAEA,QAAM,YAAY,MAAM,uBAAuB;AAC/C,QAAM,cAAc,MAAM;AAAA,IACxB;AAAA,IACA;AAAA,MACE,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,QAAQ,KAAK,OAAO;AAAA,MACpB,cAAc;AAAA,MACd,YAAY,OAAO,KAAK;AAAA,MACxB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,MACpB,iBAAiB,OAAO;AAAA,IAC1B;AAAA,IACA,oBAAoB,IAAI;AAAA,EAC1B;AACA,MAAI,CAAC,YAAY,IAAI;AACnB,WAAO,aAAa;AAAA,MAClB,YAAY,aAAa,EAAE,OAAO,6BAA6B;AAAA,MAC/D,EAAE,QAAQ,YAAY,eAAe,IAAI;AAAA,IAC3C;AAAA,EACF;AAEA,QAAM,UAAU,UAAU,QAAQ,uBAAuB;AAEzD,MAAI;AACF,UAAM,SAAS,MAAM,QAAQ;AAAA,MAC3B,OAAO,KAAK;AAAA,MACZ,OAAO,KAAK;AAAA,MACZ,EAAE,gBAAgB,KAAK,OAAiB,UAAU,KAAK,SAAS;AAAA,MAChE,OAAO,KAAK;AAAA,IACd;AACA,UAAM,2CAA2C,YAAY,uBAAuB;AAAA,MAClF,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,MACrB,QAAQ,KAAK,OAAO;AAAA,MACpB,cAAc;AAAA,MACd,YAAY,OAAO,KAAK;AAAA,MACxB,WAAW;AAAA,MACX,eAAe,IAAI;AAAA,MACnB,gBAAgB,IAAI;AAAA,IACtB,CAAC;AACD,WAAO,aAAa,KAAK,MAAM;AAAA,EACjC,SAAS,KAAc;AACrB,QAAI,gBAAgB,GAAG,GAAG;AACxB,aAAO,aAAa,KAAK,IAAI,MAAM,EAAE,QAAQ,IAAI,OAAO,CAAC;AAAA,IAC3D;AACA,UAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,WAAO,aAAa,KAAK,EAAE,OAAO,QAAQ,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EAC9D;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,MAAM,CAAC,kBAAkB;AAAA,EACzB,SAAS;AAAA,EACT,SAAS;AAAA,IACP,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,MAAM,CAAC,kBAAkB;AAAA,MACzB,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,
|
|
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": ";;;;;;;;;;AAAA,SAAS,qBAAqB;AAC9B,SAAS,QAAQ,OAAO,YAAY,UAAU,cAAc;AAOzD;AADI,IAAM,qBAAN,MAAyB;AAAA,EAAzB;AAsBL,yBAAwB;
|
|
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
|
-
|
|
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) =>
|
|
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;
|