Repository navigation
feat(payments): fail-closed gateway selection with PayPal adapter and failover - #802
Conversation
…ilover orchestration Implements the issue #413 processing layer on top of the #780 provider management surface: - IPaymentGatewaySelector: config-driven, fail-closed routing ordered by priority with opt-in currency/country/payment-method/amount filters; no eligible gateway is an explicit GatewaySelectionFailedException, never a silent default. - PayPalPaymentGateway: second IPaymentGateway adapter for the charge and refund paths (PayPal Orders v2 create+capture, captures refund), fail-closed sandbox-default configuration, OAuth2 token handling with PayPal-Request-Id idempotency, and explicit failure classification (transport/429/5xx retriable, 4xx terminal, unconfirmed capture outcome-unknown). Webhook ingress stays in Commerce.Billing. - FailoverPaymentGateway composite as the single resolved IPaymentGateway: same-gateway retry (clamped by Payments:Retry), next-priority failover skipping attempted gateways, max-3-attempt budget, per-gateway circuit breaker with half-open probe, all-gateways-down alert through the existing system notification pattern, and provider-pinned refunds, inquiries and cancellations. - GatewayMetricsService: per-gateway 24h/7d/30d success-rate and latency aggregates from payment records; circuit snapshots complement the #780 provider health surface. - Payment path re-attribution: payments record the gateway that actually processed them (RecordProcessingProvider) before provider mapping bind. - Tests: selection matrix, failover sequences (retriable vs terminal vs outcome-unknown, skip-attempted, max-3, clamps), circuit-breaker transitions, fail-closed no-match, PayPal adapter matrix, simulated charge/refund round-trip, and window metrics. References #413
…gateway-failover-20261011 # Conflicts: # apps/api/Source/Modules/GameGuild.Commerce.Payments/GameGuild.Commerce.Payments.csproj
|
Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: gg-closeout-bot.
|
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 37 medium |
| ErrorProne | 1 high |
| CodeStyle | 3 minor |
🟢 Metrics 384 complexity
Metric Results Complexity 384
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
6be7c2c to
1bdae87
Compare
|
Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: closeout-bot.
|
1bdae87 to
aa1af3e
Compare
…tures array - Route context payment-method inference now accepts both underscore and hyphen separators, so PayPal billing-agreement tokens (ba-XXX) route with the paypal method type instead of staying untyped. - PayPal capture extraction descends purchase_units[0].payments.captures[0] so a completed capture maps to success with its verified provider mapping. Both defects were surfaced by the new GatewayRouting unit tests in CI.
…ration host Gateway selection is fail-closed (issue #413): with no PaymentGateways configuration the Testing environment resolves zero eligible gateways and the charge endpoint answers 500 by design. The integration host now explicitly enables the simulated Stripe gateway it charges through.
|
External-check note per the #740/#796 protocol: Codacy Static Code Analysis fails on this PR with no actionable diff output. The same external bot fails identically on already-merged PRs #790 and #794 (and passes elsewhere without any configuration change on our side), so this is a chronic external-service condition, not a defect introduced by this diff. The in-repo static analysis equivalents all pass: |
…gistrations, failover-test ctor wiring, client regen Refs #415
…ailover-20261011 feat(payments): fail-closed gateway selection with PayPal adapter and failover
…gistrations, failover-test ctor wiring, client regen Refs #415
- Keep develop's multi-gateway registration; refund execution stays pinned to the owner gateway (ProviderId/Currency) inside RefundProcessingService - Dispute accept/challenge requests gain ProviderId and route through FailoverPaymentGateway pinned to the owner gateway; PayPal adapter fails closed with an explicit unsupported error
References #413
Implements the multi-gateway processing layer on top of the #780 provider-management foundation and #782/#403 retry configuration, following the verifier's safe-default spec (issue #413 verification comment).
What this PR adds
1.
IPaymentGatewaySelector— config-driven, fail-closed routingapps/api/Source/Modules/GameGuild.Commerce.Payments/Abstractions/IPaymentGatewaySelector.cs+Services/ConfigPaymentGatewaySelector.csIsEnabled(PaymentGateways:*), and — when aPayments:GatewayRoutingentry exists — the entry enabled and its opt-in filters (currency / country / payment-method / min-max amount, all inclusive, empty = unrestricted) matching the routing context derived from the charge request.Payments:GatewayRouting:Gateways:*:Priority; default well-known stripe=100, paypal=200; stable order for ties).GatewaySelectionFailedExceptionlisting every gateway and its rejection reason — never a silent fallback to a default gateway. Startup validation rejects duplicate/inconsistent routing entries.2. Second processing adapter — PayPal
Services/PayPal/PayPalPaymentGateway.cs(+IPayPalApiClient/PayPalApiClientHTTP client with OAuth2 client-credentials token caching andPayPal-Request-Ididempotency).IPaymentGatewaycontract; customer/payment-method/subscription management fail closed with explicit*_not_supportederrors (those flows live in the Commerce.Billing PayPal integration).PaymentGateways:PayPal): disabled by default, environment defaults tosandbox, enabling real calls without credentials fails startup validation and every call re-checks. Simulation mode mirrors the Stripe simulated-response conventions.RetriableGatewayFailure; HTTP 4xx answers areTerminalDecline; a success response without a confirmed capture isOutcomeUnknown(no transaction reference, left to reconciliation — matching the Stripe outcome-unknown convention).3. Failover orchestration in the payment path
Services/FailoverPaymentGateway.csis now the singleIPaymentGatewayresolved by the payment path (PaymentsModuleDI), soProcessPaymentCommandHandler,RetryPaymentCommandHandler,OrderPaymentService(order charges), and the refund/cancel handlers all gain the behaviour without depending on a specific adapter.Payments:GatewayFailover:SameGatewayRetryLimit, clamped by feat(payments): configurable retry/dunning with recovery metrics and refund auditing #782'sPayments:Retry:MaxRetries), then failover to the next-priority gateway skipping already-attempted gateways, with a hard cap of 3 attempts total (Payments:GatewayFailover:MaxAttemptsTotal, the issue Multi-Gateway Payment Processing #413 default).CircuitBreaking/InMemoryGatewayCircuitBreaker, process-wide singleton): consecutive-retriable-failure threshold opens the circuit, open duration then grants exactly one half-open probe (probe lease expires so a cancelled probe cannot wedge the gateway); conclusive answers (including declines) count as healthy for the breaker.all_gateways_unavailableerror code and an alert is raised through the existing system-alert notification pattern (NotificationGatewayAvailabilityAlerter, mirroringGameGuild.Monitoring.SLA.AlertManager:NotificationType.System, recipientGuid.Empty, high priority, throttled to one alert per 5 minutes).GatewayRefundRequest.ProviderId/providerIdparameters); an unknown owner provider fails closed.Payment.RecordProcessingProviderre-attributes the payment before the verified provider mapping is bound (a bound mapping keeps its provider authoritative).4. Per-gateway health + metrics
Services/GatewayMetricsService: per-gateway 24h/7d/30d success-rate and average processing-latency aggregates computed from payment records (ProcessedAt - CreatedAtover succeeded payments), plus unresolved counts; providers without activity in a window are omitted rather than zero-inflated.IGatewayCircuitBreaker.GetAllSnapshots) complement the feat(billing): external provider management API and migration dry-run #780 external-providers health surface (GET /api/v{v}/billing/external-providers) — no new HTTP endpoints were added, so the OpenAPI surface is unchanged.5. Tests (
GameGuild.Commerce.Payments.UnitTests/GatewayRouting/)GatewaySelectionTests): priority ordering (default + configured), currency/country/payment-method/amount filters, adapter-disabled and routing-entry-disabled exclusion (fail-closed conjunction), route-context derivation, fail-closedSelectPrimaryon empty registry / all-filtered, routing options validator.GatewayFailoverTests, via a programmableFakePaymentGateway): success attribution, same-gateway retry then next-priority failover (skip attempted), max-3-attempt budget, terminal decline stops, outcome-unknown stops, retry-budget clamp (MaxRetries=0skips same-gateway retry), transport exceptions retriable, all-circuits-open → typed failure + alert, open-circuit skip, currency-filtered failover, refund pinning + fail-closed unknown owner, inquiry pinning.GatewayCircuitBreakerTests): Closed→Open at threshold, open rejecting until the open duration elapses, single half-open probe, probe success closes, probe failure reopens, per-gateway isolation (deterministicTimeProvider).PayPalPaymentGatewayTests): fail-closed defaults, sandbox-default base URL, disabled fail-closed, simulated charge/refund round-trip, real-mode capture mapping, transport/5xx retriable vs 4xx terminal classification, unconfirmed-capture outcome-unknown, not-supported surfaces, amount formatting (zero-decimal currencies), options validator.GatewayMetricsServiceTests): per-window/per-provider grouping, success-rate math, latency mean, empty windows, unresolved counts.PaymentPathFailoverAttributionTests): handler re-attribution on failover, primary provider kept when the primary answers,RecordProcessingProviderbound/unbound semantics; module DI test extended to assert the new registrations incl. the failover composite as the singleIPaymentGateway.Behaviour notes / safety
PaymentGateways:Stripe:IsEnabled(defaultfalse) on the charge path. Environments that charge through the API must explicitly enable their gateway — asappsettings.Development.jsonand the OpenAPI CI capture already do (PaymentGateways__Stripe__IsEnabled=true); the Testing-environment subscription integration host was updated accordingly. Environments without the section keep an explicitly failing charge endpoint instead of a silently working one.Payments:Retry(Refund & Retry Workflows #403/feat(payments): configurable retry/dunning with recovery metrics and refund auditing #782); the new synchronous failover loop is governed byPayments:GatewayFailover(defaults: 3 total attempts, 1 same-gateway retry, 5-failure circuit threshold, 60s open duration).Deferred (tracked for follow-up, not in this PR)
GatewayConfigurationpersistence beyond feat(billing): external provider management API and migration dry-run #780's provider state surface (credentials remain in configuration).External checks: Codacy / GitGuardian / cla-signed are external bots; results will be documented per the #740 protocol if they flag anything.