Skip to content

Persist refunds and prevent cumulative over-refunds#114

Merged
teesofttech merged 2 commits into
masterfrom
persistence/refund-balance-guard
Jul 18, 2026
Merged

Persist refunds and prevent cumulative over-refunds#114
teesofttech merged 2 commits into
masterfrom
persistence/refund-balance-guard

Conversation

@teesofttech

@teesofttech teesofttech commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • implement and register IRefundRepository
  • atomically reserve refundable balance by inserting a pending audit record inside a serializable transaction
  • count pending and confirmed refunds against the captured amount while excluding failed attempts
  • update audit records with normalized provider references, statuses, timestamps, and responses
  • mark the payment refunded only after cumulative provider-confirmed refunds reach the captured amount
  • add the composite reservation index migration and deployment/rollback guidance

Root cause

PaymentService.RefundPaymentAsync compared only the current request with the original transaction amount and never persisted RefundTransaction. Multiple partial refunds could therefore independently pass validation and cumulatively exceed the captured payment.

TDD

Repository and service tests were written first and observed failing before implementation. Coverage includes:

  • cumulative pending and confirmed balance enforcement
  • failed attempts releasing refundable balance while remaining auditable
  • eight concurrent 60-unit reservations against a 100-unit payment
  • confirmed full-refund payment-state update
  • pending asynchronous refund behavior
  • rejected provider response auditing
  • thrown provider failure auditing
  • exhausted-balance rejection before the provider call
  • DI registration and model-index metadata

Concurrency and data model

The repository uses an in-process reservation lock plus a database Serializable transaction. The lock makes same-process behavior deterministic; serializable isolation and the (PaymentTransactionReference, Status) index protect the invariant across service instances.

Audit rows contain normalized refund fields and RefundResponse, not credentials, authorization headers, or raw request payloads.

Migration

AddRefundReservationIndex bounds PaymentTransactionReference to nvarchar(450) for SQL Server indexing and creates IX_Refunds_PaymentTransactionReference_Status.

Before production deployment, run the documented query in docs/refund-persistence.md and resolve references longer than 450 characters. The down migration removes the index, restores nvarchar(max), and retains refund rows.

Validation

  • refund-focused tests: 14 passed
  • concurrency suite: passed five consecutive runs
  • generated SQL inspected for expected ALTER COLUMN and CREATE INDEX only
  • .NET 8 Release build: 0 warnings, 0 errors
  • full test suite: 128 passed, 3 skipped sandbox tests, 0 failed
  • vulnerable NuGet audit: none found
  • changed-file formatting and git diff --check: clean

Closes #62

Summary by CodeRabbit

  • New Features
    • Introduced persistent refund handling with pending/successful/failed outcomes.
    • Added refundable balance reservation with atomic over-refund protection (including concurrent requests).
    • Payment transactions are updated to refunded only once confirmed refunded totals meet the captured amount.
    • Added refund repository support and automatic registration in the service container.
  • Documentation
    • Documented refund persistence behavior, reservation rules, and migration/rollback notes.
  • Tests
    • Added unit tests covering refund outcomes, reservation failure behavior, and concurrent reservation limits.
    • Added repository and DI registration tests, plus coverage for invalid reservation amounts.
  • Bug Fixes
    • Fixed refund processing so gateway exceptions and rejections are recorded correctly and surfaced to callers.

Copilot AI review requested due to automatic review settings July 18, 2026 07:43
@teesofttech teesofttech added area: persistence Database models, repositories, migrations, and consistency priority: P1 High priority; should be addressed next area: core Core SDK behavior, contracts, routing, and error handling labels Jul 18, 2026
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 948be6ca-8a4a-44e3-a4d1-fe0cc5f3c4c8

📥 Commits

Reviewing files that changed from the base of the PR and between 5645ae4 and 0e98881.

📒 Files selected for processing (5)
  • PayBridge.SDK.Test/Unit/PaymentServiceRefundTests.cs
  • PayBridge.SDK.Test/Unit/RefundRepositoryTests.cs
  • PayBridge.SDK/Migrations/20260718074022_AddRefundReservationIndex.cs
  • PayBridge.SDK/Repositories/RefundRepository.cs
  • PayBridge.SDK/Services/PaymentService.cs
🚧 Files skipped from review as they are similar to previous changes (4)
  • PayBridge.SDK.Test/Unit/RefundRepositoryTests.cs
  • PayBridge.SDK/Services/PaymentService.cs
  • PayBridge.SDK/Repositories/RefundRepository.cs
  • PayBridge.SDK.Test/Unit/PaymentServiceRefundTests.cs

📝 Walkthrough

Walkthrough

Refund processing now persists refund attempts, reserves refundable balance transactionally, handles provider outcomes, and updates payment status after confirmed refunds. EF Core indexing, dependency injection, migration guidance, and unit tests are added.

Changes

Refund persistence and reservation

Layer / File(s) Summary
Refund contract and database schema
PayBridge.SDK/Interfaces/IRefundRepository.cs, PayBridge.SDK/Persistence/PayBridgeDbContext.cs, PayBridge.SDK/Migrations/*
Adds reservation and nullable lookup contracts, configures the refund index, and updates migration metadata, provider-specific column types, and rollback behavior.
Refund repository reservation
PayBridge.SDK/Repositories/RefundRepository.cs
Adds refund creation, transactional refundable-balance reservation, retrieval, update operations, and per-payment synchronization.
Payment refund orchestration
PayBridge.SDK/Services/PaymentService.cs, PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
Integrates refund persistence and gateway outcomes into RefundPaymentAsync and registers the repository as scoped.
Refund behavior validation and documentation
PayBridge.SDK.Test/Unit/*RefundTests.cs, PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs, docs/refund-persistence.md
Tests refund outcomes, reservation limits, concurrency, and registration, and documents persistence and migration behavior.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PaymentService
  participant RefundRepository
  participant PaymentGateway
  participant TransactionRepository
  PaymentService->>RefundRepository: Reserve refundable balance
  RefundRepository-->>PaymentService: Reservation result
  PaymentService->>PaymentGateway: Submit refund
  PaymentGateway-->>PaymentService: Refund response or exception
  PaymentService->>RefundRepository: Persist final refund status
  PaymentService->>TransactionRepository: Mark transaction refunded when fully confirmed
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: refund persistence plus cumulative over-refund prevention.
Linked Issues check ✅ Passed The changes implement refund persistence, reservation, concurrency protection, payment-state updates, tests, and migration guidance required by #62.
Out of Scope Changes check ✅ Passed No unrelated changes stand out; the tests, schema migration, and docs all support refund persistence and over-refund prevention.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch persistence/refund-balance-guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces persisted refund audit records and an atomic “refundable balance reservation” mechanism to prevent multiple partial refunds from cumulatively exceeding a payment’s captured amount (Issue #62). It extends the SDK’s refund flow to reserve balance before calling the provider, persist provider outcomes, and only mark a payment as fully refunded once confirmed refunds reach the captured amount.

Changes:

  • Add IRefundRepository + RefundRepository with a serializable reservation transaction that counts Pending and Refunded refunds against the captured amount (excluding Failed).
  • Update PaymentService.RefundPaymentAsync to reserve balance first, persist refund attempts/results, and update the payment status only after cumulative confirmed refunds reach the captured amount.
  • Add EF model/index + migration (IX_Refunds_PaymentTransactionReference_Status), documentation, and unit tests (including concurrent reservation coverage) and DI registration.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
PayBridge.SDK/Services/PaymentService.cs Persists refund attempts/results and enforces cumulative refundable balance via repository reservation.
PayBridge.SDK/Repositories/RefundRepository.cs New repository implementing reservation + persistence and lookup/update operations for refund audit rows.
PayBridge.SDK/Persistence/PayBridgeDbContext.cs Adds composite index model metadata for refund reservations.
PayBridge.SDK/Migrations/PayBridgeDbContextModelSnapshot.cs Updates snapshot for index + bounded PaymentTransactionReference type.
PayBridge.SDK/Migrations/20260718074022_AddRefundReservationIndex.Designer.cs Migration designer for updated model.
PayBridge.SDK/Migrations/20260718074022_AddRefundReservationIndex.cs Migration to alter column to nvarchar(450) and create composite index.
PayBridge.SDK/Interfaces/IRefundRepository.cs Adds TryReserveAsync and makes GetByReferenceAsync nullable.
PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs Registers IRefundRepository in DI.
PayBridge.SDK.Test/Unit/RefundRepositoryTests.cs Verifies index metadata + reservation logic + concurrency behavior.
PayBridge.SDK.Test/Unit/PaymentServiceRefundTests.cs Verifies service persistence/update behavior for refunded/pending/failed paths and reservation rejection.
PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs Asserts DI registration for IRefundRepository.
docs/refund-persistence.md Deployment/rollback guidance + rationale for bounded reference and composite index.
Files not reviewed (1)
  • PayBridge.SDK/Migrations/20260718074022_AddRefundReservationIndex.Designer.cs: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread PayBridge.SDK/Services/PaymentService.cs
Comment thread PayBridge.SDK/Repositories/RefundRepository.cs
Comment thread PayBridge.SDK/Repositories/RefundRepository.cs Outdated
Comment thread PayBridge.SDK/Repositories/RefundRepository.cs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@PayBridge.SDK.Test/Unit/PaymentServiceRefundTests.cs`:
- Around line 31-37: Update the refund outcome tests around the confirmed,
pending, rejected, and exception cases to verify persistence independently of
the mutable reserved object. Assert that the repository’s UpdateAsync method is
called exactly once with the expected transaction for each outcome, including
the scenarios covered by the refund transition tests, rather than relying only
on fixture.Transaction state assertions.

In `@PayBridge.SDK.Test/Unit/RefundRepositoryTests.cs`:
- Around line 69-93: Add a provider-level concurrency test alongside
Concurrent_reservations_cannot_exceed_captured_amount that bypasses the shared
ReservationLock and executes each TryReserveAsync call through independent
database connections and contexts. Run the eight reservation attempts
concurrently, then assert exactly one succeeds and the persisted
pending/refunded total does not exceed 100m, validating cross-process
serializable behavior.

In `@PayBridge.SDK/Migrations/20260718074022_AddRefundReservationIndex.cs`:
- Around line 13-24: The migration AddRefundReservationIndex uses SQL
Server-specific nvarchar types despite being shared across PostgreSQL, MySQL,
and SQLite providers. Replace this with a provider-neutral schema change, or
split the migration into provider-specific versions while preserving the
PaymentTransactionReference alteration, index creation, and rollback behavior
for every configured provider.

In `@PayBridge.SDK/Repositories/RefundRepository.cs`:
- Around line 33-37: Update the refund validation in the repository method
containing the refund reservation logic to reject non-positive refund amounts
before calculating the reservation. Validate refund.Amount > 0 and throw the
existing out-of-range validation exception using the appropriate refund amount
parameter name, preventing negative or zero values from reaching reservedAmount
calculations.
- Around line 39-64: The TryReserveAsync refund reservation flow must execute
its user-managed transaction through the EF Core execution strategy. Wrap the
query, validation, insert, save, and commit logic in
_dbContext.Database.CreateExecutionStrategy() or ExecuteInTransactionAsync,
preserving the existing rollback and boolean outcomes. Add a provider-backed
test covering successful reservation execution with the retrying strategy
enabled.

In `@PayBridge.SDK/Services/PaymentService.cs`:
- Around line 253-264: The current refund finalization logic in PaymentService
only runs for immediate Refunded responses and is not reusable for asynchronous
confirmations. Extract it into an application-level status-update operation that
updates the refund and recalculates the related payment status transactionally,
then invoke that operation from both the immediate refund flow and
webhook/poller handling for later confirmations.
- Around line 224-251: Make the refund flow centered on the stable refund.Id
idempotency key: pass it through RefundPaymentAsync and each gateway so retries
cannot create duplicate provider refunds. When the provider succeeds but
_refundRepository.UpdateAsync fails, retain the provider result for durable
retry rather than returning an unrecoverable pending attempt. Add reconciliation
for pending refunds with uncertain provider outcomes, using the idempotency key
to query or safely retry and then persist the final result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15eccec2-cef9-4ae9-897d-52cdd3964ff7

📥 Commits

Reviewing files that changed from the base of the PR and between ed2244a and 5645ae4.

📒 Files selected for processing (12)
  • PayBridge.SDK.Test/Unit/IServiceCollectionExtensionsTests.cs
  • PayBridge.SDK.Test/Unit/PaymentServiceRefundTests.cs
  • PayBridge.SDK.Test/Unit/RefundRepositoryTests.cs
  • PayBridge.SDK/Externsions/IServiceCollectionExtensions.cs
  • PayBridge.SDK/Interfaces/IRefundRepository.cs
  • PayBridge.SDK/Migrations/20260718074022_AddRefundReservationIndex.Designer.cs
  • PayBridge.SDK/Migrations/20260718074022_AddRefundReservationIndex.cs
  • PayBridge.SDK/Migrations/PayBridgeDbContextModelSnapshot.cs
  • PayBridge.SDK/Persistence/PayBridgeDbContext.cs
  • PayBridge.SDK/Repositories/RefundRepository.cs
  • PayBridge.SDK/Services/PaymentService.cs
  • docs/refund-persistence.md

Comment thread PayBridge.SDK.Test/Unit/PaymentServiceRefundTests.cs
Comment thread PayBridge.SDK.Test/Unit/RefundRepositoryTests.cs
Comment thread PayBridge.SDK/Repositories/RefundRepository.cs
Comment thread PayBridge.SDK/Repositories/RefundRepository.cs Outdated
Comment thread PayBridge.SDK/Services/PaymentService.cs
Comment thread PayBridge.SDK/Services/PaymentService.cs
@teesofttech
teesofttech merged commit 5b1585f into master Jul 18, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Core SDK behavior, contracts, routing, and error handling area: persistence Database models, repositories, migrations, and consistency priority: P1 High priority; should be addressed next

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: persist refunds and prevent cumulative partial refunds from exceeding the payment

2 participants