Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
30d5037
docs(spec): payment refund design (2026-05-30)
knutties May 30, 2026
1316f71
docs(plan): implementation plan for payment refund
knutties May 30, 2026
0be07c3
feat(db): tighten reverses_transaction_id uniqueness to type='transfer'
knutties May 30, 2026
6f43689
feat(error): add refund AppError variants
knutties May 30, 2026
32349f1
feat(domain): type_label renders 'Refund' for payment rows linked to …
knutties May 30, 2026
1cb0ceb
feat(repo): find_refunds_of + sum_refunds_of
knutties May 30, 2026
b3aabc3
feat(ledger): code 210 create_payment_refund + create_payment_refund_…
knutties May 30, 2026
29bd425
feat(service): pb_payment_service::refund_payment
knutties May 30, 2026
361e4ec
refactor(service): hoist original_amount/remaining computation; docum…
knutties May 30, 2026
61e2a53
feat(api): refund DTOs + From<RefundResult>
knutties May 30, 2026
7bdd44e
feat(api): POST /pb-accounts/{id}/payments/{id}/refund
knutties May 30, 2026
d62fd17
refactor(api): drop unrequested legacy /accounts refund alias
knutties May 30, 2026
9b3194a
feat(smithy): RefundPBAccountPayment operation
knutties May 30, 2026
e343b93
feat(smithy): regen client SDK + openapi for RefundPBAccountPayment
knutties May 30, 2026
1915308
test(e2e): PbaWorld refund fields + refund step bindings
knutties May 30, 2026
846cba1
test(e2e): payment_refund.feature scenarios
knutties May 30, 2026
f65446f
test(e2e): tighten payment_refund scenarios 6/8/11 with reason/remain…
knutties May 30, 2026
b423f28
feat(admin): refund_payment_form + process_refund_payment handlers
knutties May 30, 2026
deb36b8
feat(admin): payment refund form template
knutties May 30, 2026
6b81823
feat(admin): refund button + history block + refund-of affordance on …
knutties May 30, 2026
e7a3b85
refactor(admin): cleanup refund handler nits
knutties May 30, 2026
ecedc10
test(ui-e2e): UiWorld refund fields + UI step bindings
knutties May 30, 2026
18d94db
test(ui-e2e): payment_refund_admin.feature scenarios
knutties May 30, 2026
acf0d19
test(ui-e2e): bypass HTML5 max on refund form; tighten history-entry …
knutties May 30, 2026
ab7e98e
docs: payment refund
knutties May 30, 2026
1aa1e0e
style: cargo fmt pba-service
knutties May 30, 2026
c352f02
test(e2e): reorder payment_refund scenarios so account_id ends as the…
knutties May 31, 2026
c4dd357
fix(service): refund primary row id equals refund correlation_id
knutties May 31, 2026
0002a9b
test(ui-e2e): cache last_payment_id in goto_last_payment_detail
knutties May 31, 2026
c033b9a
style: cargo fmt goto_last_payment_detail
knutties May 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ PostgreSQL (account metadata) + TigerBeetle (ledger / balances)
| `POST` | `/accounts/{id}/payments` | Pay a merchant (MCC-validated, others-first splitting) |
| `POST` | `/accounts/{id}/withdrawals` | Withdraw from self-pool only |
| `POST` | `/normal-accounts/{id}/transfers/{id}/reverse` | Reverse a posted transfer (admin) |
| `POST` | `/pb-accounts/{id}/payments/{id}/refund` | Refund a settled PB payment in whole or in part; multiple partials allowed (admin) |

The API is defined using [Smithy](https://smithy.io/) models under `model/`. A generated Rust client SDK lives at `crates/pba_client/`.

Expand Down
27 changes: 27 additions & 0 deletions WHAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,30 @@ Constraints:
- After reversal, the funds sit in the source normal account. The admin can
separately call `WithdrawFromNormalAccount` to return them to the sponsor's
bank.

## Refunding a PB → merchant payment

Settled PB payments can be refunded by an admin in whole or in part. Each
refund is recorded as a new compensating transaction (1 or 2 rows
mirroring the payment's pool split) plus matching TigerBeetle transfers
in the opposite direction (debit the merchant settlement sentinel, credit
the PB pool). Original payment rows are never mutated; each refund row
links back via `reverses_transaction_id`.

- Multiple partial refunds are allowed per payment; the sum must not
exceed the original payment amount.
- Refund credits self-pool first up to that pool's remaining-unrefunded
amount, then others-pool. This restores the holder's self-pool
flexibility first.
- The PB account must be Active to accept a refund. Refund rows
themselves cannot be refunded.
- Refunds are admin-only via
`POST /pb-accounts/{id}/payments/{id}/refund` and via the Refund
button on the admin transaction-detail page.

The MCC is not re-validated on refund — the original payment already
passed MCC validation, and a refund restores state rather than
authorising new spend. The merchant settlement sentinel has no balance
constraint at TigerBeetle, so refunds never fail with `InsufficientFunds`;
over-amount cases are caught with `RefundAmountInvalid` before TigerBeetle
is touched.
2 changes: 2 additions & 0 deletions crates/pba_client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,8 @@ mod post_normal_account_transfer;

mod post_pb_account_deposit;

mod refund_pb_account_payment;

mod reverse_normal_account_transfer;

mod transfer_to_pb_account;
Expand Down
1 change: 1 addition & 0 deletions crates/pba_client/src/client/customize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@






/// `CustomizableOperation` allows for configuring a single operation invocation before it is sent.
Expand Down
29 changes: 29 additions & 0 deletions crates/pba_client/src/client/refund_pb_account_payment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Code generated by software.amazon.smithy.rust.codegen.smithy-rs. DO NOT EDIT.
impl super::Client {
/// Constructs a fluent builder for the [`RefundPBAccountPayment`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder) operation.
///
/// - The fluent builder is configurable:
/// - [`account_id(impl Into<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::account_id) / [`set_account_id(Option<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::set_account_id):<br>required: **true**<br>(undocumented)<br>
/// - [`payment_id(impl Into<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::payment_id) / [`set_payment_id(Option<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::set_payment_id):<br>required: **true**<br>(undocumented)<br>
/// - [`amount(i64)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::amount) / [`set_amount(Option<i64>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::set_amount):<br>required: **true**<br>Monetary amount in the smallest currency unit (e.g., paise for INR).<br>
/// - [`description(impl Into<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::description) / [`set_description(Option<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::set_description):<br>required: **false**<br>(undocumented)<br>
/// - [`gateway_ref(impl Into<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::gateway_ref) / [`set_gateway_ref(Option<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::set_gateway_ref):<br>required: **false**<br>(undocumented)<br>
/// - [`idempotency_key(impl Into<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::idempotency_key) / [`set_idempotency_key(Option<String>)`](crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::set_idempotency_key):<br>required: **false**<br>(undocumented)<br>
/// - On success, responds with [`RefundPbAccountPaymentOutput`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput) with field(s):
/// - [`refund_id(String)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::refund_id): (undocumented)
/// - [`original_payment_id(String)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::original_payment_id): (undocumented)
/// - [`account_id(String)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::account_id): (undocumented)
/// - [`amount(i64)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::amount): Monetary amount in the smallest currency unit (e.g., paise for INR).
/// - [`amount_to_self(i64)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::amount_to_self): Monetary amount in the smallest currency unit (e.g., paise for INR).
/// - [`amount_to_others(i64)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::amount_to_others): Monetary amount in the smallest currency unit (e.g., paise for INR).
/// - [`original_amount(i64)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::original_amount): Monetary amount in the smallest currency unit (e.g., paise for INR).
/// - [`remaining_refundable(i64)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::remaining_refundable): Monetary amount in the smallest currency unit (e.g., paise for INR).
/// - [`status(String)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::status): (undocumented)
/// - [`correlation_id(String)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::correlation_id): (undocumented)
/// - [`created_at(DateTime)`](crate::operation::refund_pb_account_payment::RefundPbAccountPaymentOutput::created_at): ISO 8601 date-time.
/// - On failure, responds with [`SdkError<RefundPBAccountPaymentError>`](crate::operation::refund_pb_account_payment::RefundPBAccountPaymentError)
pub fn refund_pb_account_payment(&self) -> crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder {
crate::operation::refund_pb_account_payment::builders::RefundPBAccountPaymentFluentBuilder::new(self.handle.clone())
}
}

21 changes: 21 additions & 0 deletions crates/pba_client/src/error_meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,27 @@ impl From<crate::operation::post_pb_account_deposit::PostPBAccountDepositError>
}
}
}
impl<R> From<::aws_smithy_runtime_api::client::result::SdkError<crate::operation::refund_pb_account_payment::RefundPBAccountPaymentError, R>> for Error where R: Send + Sync + std::fmt::Debug + 'static {
fn from(err: ::aws_smithy_runtime_api::client::result::SdkError<crate::operation::refund_pb_account_payment::RefundPBAccountPaymentError, R>) -> Self {
match err {
::aws_smithy_runtime_api::client::result::SdkError::ServiceError(context) => Self::from(context.into_err()),
_ => Error::Unhandled(
crate::error::sealed_unhandled::Unhandled {
meta: ::aws_smithy_types::error::metadata::ProvideErrorMetadata::meta(&err).clone(),
source: err.into(),
}
),
}
}
}
impl From<crate::operation::refund_pb_account_payment::RefundPBAccountPaymentError> for Error {
fn from(err: crate::operation::refund_pb_account_payment::RefundPBAccountPaymentError) -> Self {
match err {
crate::operation::refund_pb_account_payment::RefundPBAccountPaymentError::AccountNotFoundError(inner) => Error::AccountNotFoundError(inner),
crate::operation::refund_pb_account_payment::RefundPBAccountPaymentError::Unhandled(inner) => Error::Unhandled(inner),
}
}
}
impl<R> From<::aws_smithy_runtime_api::client::result::SdkError<crate::operation::reverse_normal_account_transfer::ReverseNormalAccountTransferError, R>> for Error where R: Send + Sync + std::fmt::Debug + 'static {
fn from(err: ::aws_smithy_runtime_api::client::result::SdkError<crate::operation::reverse_normal_account_transfer::ReverseNormalAccountTransferError, R>) -> Self {
match err {
Expand Down
3 changes: 3 additions & 0 deletions crates/pba_client/src/operation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ pub mod post_normal_account_transfer;
/// Types for the `PostPBAccountDeposit` operation.
pub mod post_pb_account_deposit;

/// Types for the `RefundPBAccountPayment` operation.
pub mod refund_pb_account_payment;

/// Types for the `ReverseNormalAccountTransfer` operation.
pub mod reverse_normal_account_transfer;

Expand Down
Loading
Loading