From 50dfe906409b8160f81af207f5e95c8a16ee5802 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Tue, 20 Oct 2026 18:08:01 +0100 Subject: [PATCH 1/4] fix(gateway): reopen circuit after failed half-open probe --- backend/src/integrations/gateway-adapter.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/backend/src/integrations/gateway-adapter.ts b/backend/src/integrations/gateway-adapter.ts index db2b0197..a78f93e4 100644 --- a/backend/src/integrations/gateway-adapter.ts +++ b/backend/src/integrations/gateway-adapter.ts @@ -150,8 +150,12 @@ export abstract class BaseGatewayAdapter implements IGatewayAdapter { const processingTime = Date.now() - startTime; this.recordFailure(processingTime); - // Check if should open circuit - if (this.health.consecutiveFailures >= this.config.circuitBreakerThreshold) { + // A failed probe in half-open state must reopen the circuit immediately; + // otherwise a single probe failure can leave the gateway half-open. + if ( + this.health.consecutiveFailures >= this.config.circuitBreakerThreshold || + this.circuitState === CircuitState.HalfOpen + ) { this.openCircuit(); } From 5b2b8f1f66a22e3c33adf807d228c1228b5ab4e8 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Tue, 20 Oct 2026 18:08:21 +0100 Subject: [PATCH 2/4] test(gateway): cover failed payments and half-open recovery --- .../src/integrations/gateway-adapter.test.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 backend/src/integrations/gateway-adapter.test.ts diff --git a/backend/src/integrations/gateway-adapter.test.ts b/backend/src/integrations/gateway-adapter.test.ts new file mode 100644 index 00000000..cbe11f0e --- /dev/null +++ b/backend/src/integrations/gateway-adapter.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; +import { + BaseGatewayAdapter, + type GatewayConfig, + type PaymentRequest, + type PaymentResponse, +} from './gateway-adapter'; + +const config: GatewayConfig = { + gatewayType: 'stripe', + enabled: true, + priority: 1, + maxRetries: 0, + timeoutMs: 1_000, + healthCheckIntervalMs: 60_000, + circuitBreakerThreshold: 3, + credentials: {}, +}; + +const request: PaymentRequest = { + amount: '12.50', + currency: 'USD', + recipient: 'acct_test', + idempotencyKey: 'test-payment-1', +}; + +class TestGateway extends BaseGatewayAdapter { + nextResponse: PaymentResponse = { + success: true, + transactionId: 'txn_test', + status: 'completed', + }; + shouldThrow = false; + + getType() { + return this.config.gatewayType; + } + + protected async executePayment(): Promise { + if (this.shouldThrow) throw new Error('provider unavailable'); + return this.nextResponse; + } + + protected async performHealthCheck(): Promise {} + + forceHalfOpen() { + // Simulate the state after the circuit-breaker cooldown has elapsed. + this.circuitState = 'half_open' as typeof this.circuitState; + } +} + +describe('BaseGatewayAdapter circuit-breaker payment outcomes', () => { + it('records a resolved but failed payment as a failure', async () => { + const gateway = new TestGateway(config); + gateway.nextResponse = { + success: false, + transactionId: 'txn_failed', + status: 'failed', + errorCode: 'DECLINED', + }; + + const response = await gateway.processPayment(request); + + expect(response.status).toBe('failed'); + expect(gateway.getHealthStatus().consecutiveFailures).toBe(1); + expect(gateway.getHealthStatus().errorRate).toBe(1); + expect(gateway.getHealthStatus().successRate).toBe(0); + }); + + it('reopens the circuit immediately when a half-open probe throws', async () => { + const gateway = new TestGateway(config); + gateway.forceHalfOpen(); + gateway.shouldThrow = true; + + await expect(gateway.processPayment(request)).rejects.toThrow('provider unavailable'); + await expect(gateway.processPayment(request)).rejects.toThrow('circuit is open'); + }); +}); From d99905eae61c781ebd769244b4165d86e1c7503e Mon Sep 17 00:00:00 2001 From: Jimmy Date: Tue, 20 Oct 2026 18:08:24 +0100 Subject: [PATCH 3/4] docs(gateway): explain failure metrics and safe failover --- docs/GATEWAY_FAILURE_SEMANTICS.md | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/GATEWAY_FAILURE_SEMANTICS.md diff --git a/docs/GATEWAY_FAILURE_SEMANTICS.md b/docs/GATEWAY_FAILURE_SEMANTICS.md new file mode 100644 index 00000000..649c1827 --- /dev/null +++ b/docs/GATEWAY_FAILURE_SEMANTICS.md @@ -0,0 +1,37 @@ +# Gateway failure and circuit-breaker semantics + +This note documents the behavior expected from the shared gateway adapter. + +## Payment outcomes are not transport outcomes + +A provider call can resolve at the HTTP/SDK layer while reporting a declined or +failed payment. The adapter must record a failure whenever `success` is false +or `status` is `failed`; it must not increase the success rate merely because +the provider call resolved. The original response is returned so callers retain +the provider's transaction identifier and error details. + +## Circuit-breaker recovery + +After the failure threshold is reached, the gateway circuit opens. Once the +cooldown expires, one request may act as a half-open probe. A successful probe +closes the circuit and resets consecutive failures. A failed response or thrown +error during that probe reopens the circuit immediately; it must not remain +half-open until another threshold is reached. + +## Safe fallback and idempotency + +Do not automatically replay a payment against another provider solely because +the response is a definitive decline/failure: the provider may already have +processed the request. Failover after ambiguous transport errors requires a +shared idempotency key or reconciliation that can establish the original +outcome. Keep the same payment identifier and idempotency key across safe +retries. + +## Verification + +From `backend/`, run: + +```bash +npm test -- src/integrations/gateway-adapter.test.ts +npm run build +``` From 1a51775aa9851e100e0a4c39530e9fcef0bae21c Mon Sep 17 00:00:00 2001 From: Jimmy Date: Tue, 20 Oct 2026 18:08:33 +0100 Subject: [PATCH 4/4] docs: link gateway failure semantics from delivery index --- docs/ITERATION_11.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/ITERATION_11.md b/docs/ITERATION_11.md index 26ce9fcf..3116cc52 100644 --- a/docs/ITERATION_11.md +++ b/docs/ITERATION_11.md @@ -16,6 +16,11 @@ services and tests so future changes remain reviewable. When adding a provider, cover quote creation, timeout behavior, provider failure mapping, and idempotent retry behavior in the provider test suite. +## Gateway failure semantics + +- Failure metrics, half-open circuit recovery, and safe failover guidance: + [`GATEWAY_FAILURE_SEMANTICS.md`](GATEWAY_FAILURE_SEMANTICS.md) + ## Payment correctness - Payment reconciliation and mismatch handling: