Repository navigation
Cap confirmation retries, persist reconciliation history, and alert on credit-usage spikes - #1912
Merged
yusuftomilola merged 2 commits intoSep 25, 2026
Conversation
…ion history, alert on usage spikes Caps scheduled confirmation polling per payment. A payment that reaches PAYMENT_CONFIRMATION_MAX_ATTEMPTS or PAYMENT_CONFIRMATION_MAX_PROVIDER_ERROR_STREAK is escalated to MANUAL_REVIEW with a reason naming the cap and is excluded from later passes without another provider call. The provider-error streak cap is the one deliberate exception to the existing rule that a provider outage must not mass-flag payments, because a long bounded streak on a single payment is a different signal than one bad run; an alert is logged when either cap trips. Persists every reconciliation pass in a new reconciliation_runs table (with migration, entity, and registration) so drift can be answered historically, recording the summary counters, duration, and outcome, with failures recorded rather than masking the original error. Exposes the history at GET /payments/admin/reconciliation-runs with a bounded limit. Adds threshold-based credit-usage spike detection after a usage event is charged. It alerts only with enough history, a meaningful absolute total, and a multiple of the rolling baseline; the alert is logged, counted on /metrics, and emailed best-effort, and can never roll back or fail the charge. Isolates the wallet balance read: it now runs inside a transaction that takes the same pessimistic row lock the funding path takes, and the ledger aggregate runs through that same transaction manager. Documents the confirmed READ COMMITTED isolation guarantee and the writers it does not cover. Closes DistinctCodes#1801 Closes DistinctCodes#1800 Closes DistinctCodes#1799 Closes DistinctCodes#1798
|
@zainabbaba31-source is attempting to deploy a commit to the naijabuz's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@zainabbaba31-source Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
…n-retries-alerts-isolation # Conflicts: # backend/src/payments/payments.module.ts # backend/src/wallets/wallets.service.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes four issues assigned to this account, covering payments, credits and wallets.
Cap the confirmation retries (#1801)
payment-confirmation.service.tsis single-shot per invocation, so the real unbounded retry surface is the reconciliation loop that re-polls everyAWAITING_CONFIRMATIONpayment every five minutes. This adds two independent hard caps per payment:PAYMENT_CONFIRMATION_MAX_ATTEMPTS(default 20) — total scheduled confirmation polls.PAYMENT_CONFIRMATION_MAX_PROVIDER_ERROR_STREAK(default 10) — consecutive provider-unreachable polls.Reaching either cap escalates the payment to
MANUAL_REVIEWwith a reason naming the cap and the counts, logs anALERT:line, and excludes it from later passes without another provider call — including a row that was already past a cap when the process started, so a pre-cap deployment cannot keep hammering a degraded provider. The provider-error-streak cap is a deliberate, documented exception to the existing rule that a provider outage must not mass-flag payments: one bad run is evidence of a possible broad outage, whereas a long bounded streak on a single payment is evidence that this payment needs a human. The escalation counters are surfaced ingetMetrics().Persist reconciliation runs (#1800)
Each pass is now recorded in a new
reconciliation_runstable (entity, migration1790003000000, registered inPaymentsModule) with the summary counters, start/finish/duration, outcome, and adetailscontext. A run that throws is markedfailedwith the error rather than the original failure being masked, and a failed history write is logged and swallowed so it can never crash the cron. Exposed atGET /payments/admin/reconciliation-runs, newest first, with the limit clamped to 1–500 (default 50).Credit-usage spike alert (#1799)
After a usage event is durably recorded and charged, the service compares the tenant's current rolling window total against the average of comparable prior windows. It alerts only when all three signals agree: enough history (
CREDITS_USAGE_SPIKE_MIN_SAMPLES, default 3), a meaningful absolute total (CREDITS_USAGE_SPIKE_MIN_AMOUNT, default 10000 minor units, so tiny tenants do not alert on rounding), and at leastCREDITS_USAGE_SPIKE_MULTIPLIER(default 5.0) times the baseline. On a spike it logs anALERT:line, countsmanagehub_credit_usage_spikes_totalon/metrics, and emailsSUPPORT_EMAILusing the existing nodemailer/Handlebars pattern. Detection runs strictly after both durable writes and every failure path is swallowed, so an alert problem can never roll back a charge or make a caller retry.Wallet balance isolation (#1798)
The balance read previously ran a bare
SUMaggregate with no transaction and no lock. It now runs inside a transaction that first takes the samepessimistic_writerow lockfundCustodialWallettakes, with the aggregate executed through that same transaction manager, so the read serialises against the app's own writers. The Javadoc states the confirmed isolation level (PostgreSQL's default READ COMMITTED, per-connection overridable), exactly what the lock does guarantee, and what it does not cover — writers outside this service that append towallet_ledger_entrieswithout taking the same lock. Verified thatfundCustodialWalletis currently the only ledger writer; there is no settlement/credit wallet-debit path today. A test asserts the read is transaction- and lock-scoped.Configuration
New optional variables, documented in
backend/.env.example:PAYMENT_CONFIRMATION_MAX_ATTEMPTS,PAYMENT_CONFIRMATION_MAX_PROVIDER_ERROR_STREAK,CREDITS_USAGE_SPIKE_WINDOW_MINUTES,CREDITS_USAGE_SPIKE_MULTIPLIER,CREDITS_USAGE_SPIKE_MIN_AMOUNT,CREDITS_USAGE_SPIKE_MIN_SAMPLES.Testing
Per the task constraints, no install, build, lint, or test run was performed — the maintainer runs those manually. No new dependencies were added, so
package-lock.jsonis unaffected. Specs were extended inreconciliation.service.spec.ts,metered-usage.service.spec.tsandwallets.service.spec.ts, plus a newdto/reconciliation-run-response.dto.spec.ts.Reviewer note
MetricsServiceis provided only inAppModuleand is neither exported nor global, whileSettlementServiceandReconciliationServiceinject it from their own modules. That looks like a pre-existing dependency-injection problem onmainthat this PR inherits (rather than introduces) — worth confirming when the backend is next started.Closing issues
Closes #1801
Closes #1800
Closes #1799
Closes #1798