Production Change Request

Assessing the Impact on the Existing System

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

In the previous lesson, Product introduced a new production requirement:

Authorized administrators must be able to issue a full refund for an eligible paid Order through the existing Payment Provider, and the successful refund must become visible durable system state.

We deliberately did not begin by writing:

REFUNDED

or creating:

refunds

tables.

Now we need to understand what the requirement actually changes.

Our goal is:

Assess the refund requirement against the existing production system and classify each boundary as something that must change, may change, or should remain unchanged.

This is one of the most important differences between:

greenfield implementation

and:

production system evolution

Start From the Existing System

Before changing anything, recall what is already true.

Order lifecycle

UNPAID
   ├── payment success ──→ PAID
   └── cancellation ─────→ CANCELLED

Current enum:

public enum OrderStatus {
    UNPAID,
    PAID,
    CANCELLED
}

Historically:

PAID

and:

CANCELLED

were terminal because the initial scope had no refund requirement.

That was correct for v1.

It is no longer automatically correct for the new requirement.


Payment Is Already Separate From Order Lifecycle

This existing design decision matters enormously.

We deliberately separated:

Order lifecycle

from:

Payment attempt lifecycle

A Payment attempt may experience:

success

confirmed failure

timeout

unknown outcome

without adding:

PAYMENT_TIMEOUT

PAYMENT_FAILED

PAYMENT_PENDING

to OrderStatus.

That principle should guide refunds too.

We should not automatically add:

REFUND_PENDING

REFUND_FAILED

REFUND_TIMEOUT

to OrderStatus.

Those sound like external-operation states rather than Order business states.


Impact Classification

We will classify each major area as:

MUST CHANGE
MAY CHANGE

or:

SHOULD NOT CHANGE

This prevents us from touching unrelated parts of the system merely because the feature is significant.


1. Order Domain — MUST BE REVISITED

The Order Domain cannot remain completely untouched.

Why?

Today:

PAID

means:

payment successfully completed

no further OrderStatus transition supported

The new business requirement says:

a paid Order may later have
its successful payment refunded

That invalidates the old assumption:

PAID is always terminal

So the Order lifecycle must at least be reconsidered.


Does OrderStatus Definitely Need REFUNDED?

Not yet.

Two broad models are possible.

Option A — Refund changes OrderStatus

Conceptually:

UNPAID
   ├──→ PAID ──→ REFUNDED
   └──→ CANCELLED

This makes:

REFUNDED

a clear business lifecycle state.

Potential benefit:

Customer reads Order
→ immediately sees REFUNDED

Potential cost:

existing API enum changes

database CHECK constraint changes

existing clients must understand new value

financial processing state risks leaking
into Order lifecycle

Option B — Order Remains PAID, Refund Is Separate Financial State

Conceptually:

Order.status = PAID

Payment / Refund state
= REFUNDED

This preserves:

Order lifecycle

as separate from:

financial operation history

Potential benefit:

clean separation between
commercial Order state
and financial processing state

Potential cost:

Customer-facing Order representation
needs another way to show refund outcome

Neither option should be chosen casually.

This is now a real design decision.


What Is Certain About the Domain?

Even before choosing representation, we know the business rule:

UNPAID
→ cannot be refunded
CANCELLED
→ cannot be refunded
successfully PAID
→ may be eligible for refund

and:

successfully fully refunded payment
→ cannot be fully refunded again

Some domain/application boundary must protect those rules.


Should Order Own Refund Eligibility?

Probably some part of refund eligibility belongs to Order business state.

For example:

is this Order PAID?

is clearly an Order lifecycle question.

But:

has provider refund already succeeded?

is there an unresolved refund attempt?

belongs to financial workflow state.

So eligibility will likely combine:

Order state
+
Refund/Payment workflow state

at the application layer.

This mirrors existing Payment design.


2. Existing markPaid() — SHOULD NOT CHANGE

The new requirement does not change what successful payment means.

Existing rule remains:

UNPAID
→ markPaid()
→ PAID

We should not weaken it into:

UNPAID
→ PAID

REFUNDED
→ PAID

or allow:

PAID
→ PAID

arbitrarily.

Refund support should add a new responsibility rather than corrupt existing payment semantics.


3. Cancellation — SHOULD NOT BECOME REFUND

Current cancellation:

UNPAID
→ CANCELLED

and:

Inventory restored

The refund requirement does not redefine cancellation.

Therefore this remains invalid:

PAID
→ cancel()

We should not make:

CancelOrderUseCase

perform financial refunds.

That would merge:

unpaid Order cancellation

with:

paid financial reversal

which are different workflows.


Cancellation API Should Remain Stable

Existing:

POST /api/v1/orders/{orderId}/cancel

continues to mean:

cancel eligible UNPAID Order

It should not suddenly mean:

cancel or maybe refund depending on status

That would make API behavior ambiguous and harder to reason about.

Refund deserves its own explicit application operation.


4. Inventory — SHOULD NOT CHANGE

The new requirement explicitly says:

refund
does not automatically restore Inventory

Therefore refund workflow must not call:

InventoryRepository.restore(...)

Existing Inventory rules remain unchanged.

This is important because a financial refund is not equivalent to:

physical product returned to stock

Why This Boundary Protects Future Changes

If Product later asks for:

returns

that requirement might introduce:

return received
    ↓
inspect item
    ↓
restock eligible quantity

That would be a separate workflow.

Keeping refund independent now prevents us from encoding false assumptions.


5. Product — SHOULD NOT CHANGE

Refund eligibility must not depend on:

current Product active state

or:

current Product price

Suppose:

Order O1 paid at priceCents = 5000

and Product is later:

inactive

or price changes to:

7000

O1's refund must still be based on the historical paid Order/payment.

Therefore:

ProductRepository

should not become a required collaborator of the refund workflow.

That would be a warning sign.


Historical Pricing Becomes Valuable Again

OrderItem.unitPriceCents already preserves:

purchase-time price

That means the Order total remains historically stable.

Refund logic must not reconstruct the amount from current Product data.

This earlier design decision makes the new production change easier.


6. Money Representation — SHOULD NOT CHANGE

We already use:

long cents

with fields such as:

unitPriceCents

totalCents

The new requirement introduces no:

partial refunds

multi-currency

fractional refund allocation

So we do not need a new generic Money framework.

A full refund amount can remain expressed in integer minor units.


7. Payment Capability — MUST CHANGE

This is the most obvious impact.

The current external boundary conceptually supports:

pay(...)

The system now needs:

refund(...)

or another provider-neutral way to express the new external financial capability.

We must not call the provider directly from:

RefundOrderUseCase

with provider-specific HTTP details.

The existing principle remains:

UseCase
    ↓
external Service boundary
    ↓
provider implementation

Do We Extend PaymentService?

This is an open design question.

For example:

public interface PaymentService {

    PaymentResult pay(...);

    RefundResult refund(...);
}

could be reasonable because both operations target the same external payment capability.

But we should assess whether:

payment execution

and:

refund execution

share a coherent provider-neutral boundary.

We should not create:

RefundService

automatically just because the method name differs.

Remember our convention:

Service

represents external capability boundaries.

The abstraction should reflect real external responsibility.


8. Payment Provider Adapter — MUST CHANGE

Whatever boundary we choose, the concrete provider integration needs refund support.

That includes:

HTTP request

provider authentication

timeout behavior

response mapping

error translation

But exact implementation is blocked on provider capability research.

We currently do not know enough to write trustworthy provider DTOs.


Provider-Specific Questions Are Blocking

Before implementation we need to know:

Does the provider support full refund?

Which Payment/provider reference is required?

Does the provider require the original payment ID?

Does it support idempotency?

Can a refund be queried?

What is a confirmed rejection?

What happens when request times out?

Does provider return a Refund ID?

These are not academic questions.

They determine consistency design.


9. PaymentAttempt — MUST BE REVIEWED, NOT REUSED BLINDLY

The current system already has durable Payment-attempt concepts because payment can produce:

success

failure

unknown outcome

and needs:

duplicate execution protection

provider references

reconciliation

Refund has similar failure characteristics.

That does not automatically mean:

PaymentAttempt

should be reused for refunds.


Why Reusing PaymentAttempt May Be Wrong

A Payment attempt means conceptually:

try to charge an unpaid Order

A Refund attempt means:

try to return money
for a previous successful payment

They have different:

eligibility rules

external operation

direction of money movement

lifecycle meaning

provider references

Forcing both into one generic record may make the model vague.


Why a Separate RefundAttempt May Be Reasonable

A separate concept could own:

RefundAttemptId

OrderId

original successful payment reference

provider refund reference

refund amount

attempt state

timestamps

But those fields are not yet finalized.

We need provider semantics first.

The current impact conclusion is simply:

refund needs durable external-operation state

not:

create this exact schema now

10. External Side-Effect Strategy — MUST REUSE EXISTING PRINCIPLES

The existing Payment design taught us:

never hold a DB transaction open
while waiting for remote provider

That rule still applies.

Do not design:

BEGIN TRANSACTION

lock Order

call provider refund
(wait 5 seconds)

update DB

COMMIT

This would hold database locks across network latency and failure.


Refund Needs Multi-Phase Coordination

At a high level, the workflow will likely need:

local decision / durable attempt
        ↓
COMMIT
        ↓
provider refund call
        ↓
local completion / reconciliation

This mirrors the consistency problem already solved conceptually for Payment.

Exact fields and states come later.


11. Transaction Boundaries — MUST CHANGE FOR NEW WORKFLOW

Refund cannot be:

one PostgreSQL transaction

because provider execution is external.

Therefore we need at least:

short local transaction before provider call

and:

short local transaction after provider result

with durable state connecting them.

This is different from:

Create Order idempotency

which was entirely local and could be one transaction.


Compare Create Order vs Refund

Create Order:

PostgreSQL only

so:

claim
+
Inventory
+
Order

can commit atomically.

Refund:

PostgreSQL
+
External Payment Provider

so:

local DB

cannot atomically commit with:

provider side effect

Different failure model.

Different design.


12. Unknown Outcome Handling — MUST BE ADDED

The new workflow must explicitly handle:

refund call timed out

as:

outcome unknown

not automatically:

refund failed

Why?

Because the provider may already have returned money.

Blindly retrying could create:

double refund

depending on provider semantics.


Unknown Outcome Blocks Certain Actions

Once a refund attempt is unresolved, what should another administrator request do?

We should not allow:

start a completely new refund

without first resolving whether the previous one succeeded.

Therefore likely invariant:

At most one unresolved refund operation may exist for an Order/payment at a time.

This mirrors our Payment coordination principle.

Exact persistence enforcement comes later.


13. Refund Idempotency — MUST BE DESIGNED

We learned from the Create Order incident that side-effecting retry safety needs:

logical operation identity

Refund needs the same careful reasoning.

But we should not blindly reuse:

Idempotency-Key

from Create Order without considering provider semantics.

There are at least two layers:

our application refund operation identity

and:

provider idempotency / provider refund identity

These must align safely.


14. Order Persistence — MAY CHANGE

If we decide:

Order.status gains REFUNDED

then persistence definitely changes.

Current database constraint conceptually allows:

UNPAID
PAID
CANCELLED

A new OrderStatus would require a Flyway migration updating the CHECK constraint.

But if refund state remains entirely separate:

orders.status

may remain unchanged.

So Order table impact depends on the lifecycle design.


Existing Schema Makes Evolution Manageable

We used:

TEXT + CHECK

rather than PostgreSQL native enum.

That means adding a new lifecycle value, if chosen, can be performed through a normal migration.

Earlier simplicity improves changeability now.


15. New Persistence — MUST CHANGE SOMEWHERE

Regardless of OrderStatus decision, we need durable refund workflow information.

Why?

Because we must survive:

application restart

provider timeout

provider success + local failure

duplicate admin request

Pure in-memory state cannot do that.

Therefore some new persistence is unavoidable.


But Do Not Decide the Table Yet

We should not immediately create:

refunds (...)

without deciding:

what is a Refund?

what is a RefundAttempt?

can there be multiple attempts?

what is the provider identity?

what states exist?

what must be reconciled?

Impact analysis tells us persistence changes.

Design determines the schema.


16. Historical Data — MUST BE SUPPORTED

This requirement applies to:

existing PAID Orders

not only new Orders created after deployment.

That means migration/design must not depend on new creation-time refund metadata that historical Orders do not possess.

We must verify that existing successful Payment state contains enough information to identify the provider transaction that can be refunded.


Historical Payment Reference Is Critical

A refund typically needs to refer back to:

the successful provider payment

If existing paid Orders do not persist the required provider reference, the change becomes more difficult.

That is one of the first things we must inspect in the next design/investigation step.

Possible outcomes:

existing reference is sufficient

or:

historical provider data must be recovered

or:

some historical Orders cannot be safely automated
without additional operational handling

We must not pretend we know before checking.


17. API Command Surface — MUST CHANGE

Administrators need a way to initiate the workflow.

Therefore a new API operation is required.

We have not finalized the path, but it should likely fit existing business-action conventions.

Current examples:

POST /api/v1/orders/{orderId}/pay
POST /api/v1/orders/{orderId}/cancel

Refund should be similarly explicit.

Exact endpoint design belongs to the next design lesson.


18. Customer Read API — MUST CHANGE SOMEHOW

Product says:

Customer can see that payment was refunded.

Therefore existing Order reads need to expose refund information somehow.

This affects:

GET /api/v1/orders

and/or:

GET /api/v1/orders/{orderId}

depending on response design.


But We Must Protect Compatibility

Existing clients already understand:

UNPAID

PAID

CANCELLED

Adding:

REFUNDED

to the same enum may be a breaking change for clients using exhaustive enum handling.

An additive optional field may be safer in some environments.

We need to evaluate the actual client contract.


Example Compatibility Question

Existing client:

switch status:
    UNPAID
    PAID
    CANCELLED

If server suddenly returns:

REFUNDED

what happens?

Possible client behavior:

crash

deserialize failure

unknown status fallback

safe default

We must not assume.

Production API evolution includes consumer behavior.


19. API Version — MAY CHANGE, BUT NOT AUTOMATICALLY

A new requirement does not automatically require:

/api/v2

If we can evolve the v1 contract compatibly, keeping:

/api/v1

may be correct.

If the only reasonable representation is breaking for existing consumers, versioning may become justified.

We need evidence before deciding.


20. Problem Details — MUST BE EXTENDED

Refund introduces new business conflict categories.

Likely examples include:

Order not refundable

Refund already completed

Refund currently unresolved/in progress

But exact public codes are not finalized yet.

We should not invent all names in impact analysis.

The important conclusion is:

central RFC 7807 mapping
must cover new refund workflow failures

Existing Error Principles Stay

Still:

400
→ malformed transport/request
401
→ unauthenticated
403
→ insufficient authority
404
→ missing resource according to security contract
409
→ valid request conflicts with current business state
500
→ unexpected system failure

Refund should fit this model.


21. Security — MUST CHANGE

A new privileged endpoint needs authorization.

Requirement:

administrator only

Therefore security configuration/tests must explicitly protect the refund operation.

We should not rely only on:

frontend hides button

or:

Handler manually checks a string role

outside established security patterns.


Customer Ownership Still Does Not Authorize Refund

A Customer may own:

Order O1

That allows them to:

view their Order

It does not grant:

refund authority

Ownership and administrative permission remain distinct concepts.


22. Customer Identity Model — SHOULD NOT CHANGE

We still do not need:

Customer entity

CustomerRepository

Refund concerns an existing Order and administrator authority.

No local Customer aggregate is necessary.

The existing external CustomerId remains enough for ownership/read behavior.


23. Observability — MUST CHANGE

Refund is a financial workflow.

We need operational visibility into:

refund requested

provider call initiated

refund confirmed successful

refund confirmed rejected

refund outcome unknown

refund reconciled

Structured logging must make these states understandable.


Correlation Still Applies

Each HTTP request still gets:

requestId

Refund operation may also need its own durable:

RefundAttemptId

or equivalent.

These identities serve different purposes, just like:

requestId

and:

PaymentAttemptId

do today.


Metrics — MAY CHANGE

Useful bounded metrics may eventually include:

refund attempts

refund success

refund confirmed failure

refund unknown outcome

But do not add:

OrderId

CustomerId

RefundAttemptId

as metric labels.

Detailed identity belongs in logs.


24. Health Checks — SHOULD NOT CHANGE BY DEFAULT

The Payment Provider is an external runtime dependency.

Should readiness fail whenever provider refund capability is unavailable?

Probably not automatically.

Existing health semantics should remain focused on whether the application can safely serve normal traffic.

A temporary provider issue may affect:

refund

without making the entire application incapable of:

browse

order history

product management

Do not expand readiness without a deliberate operational requirement.


25. CI — SHOULD NOT FUNDAMENTALLY CHANGE

The existing CI pipeline already runs:

build

tests

PostgreSQL integration tests

container verification

Refund tests should join the same pipeline.

No new CI platform is required.


26. Test Strategy — MUST EXPAND

Refund introduces new tests across several layers.

Domain/application

UNPAID cannot refund

CANCELLED cannot refund

eligible PAID Order can begin refund workflow

already refunded cannot refund again

depending on final lifecycle representation.


Provider Boundary Tests

Need controlled HTTP/provider stub scenarios for:

success

confirmed rejection

timeout

connection failure

malformed response

unknown outcome

duplicate/idempotency behavior

No real production Payment Provider in automated tests.


Persistence/Transaction Tests

Need real PostgreSQL tests for:

durable refund attempt

one unresolved attempt invariant

local completion

rollback

concurrent admin requests

depending on final schema.


Concurrency Tests

At minimum:

two administrators
attempt full refund
for same Order

must not produce:

two independent provider refund executions

This will require real concurrency reasoning, not just Domain checks.


Recovery Tests

We will need to model:

provider refund succeeds

local state application fails

and prove:

system does not issue a second refund blindly

This is likely the highest-risk scenario.


27. Deployment/Migration — MUST BE PLANNED

Because this is production, we must ask:

Can migration deploy before code?

Can old code run against new schema?

Can new code read old rows?

Does API rollout need frontend coordination?

Can feature be enabled safely?

These questions did not matter as much in early greenfield lessons.

They matter now.


Prefer Additive Migration

Where practical:

add table

add nullable/additive field

add new constraint carefully

is safer than:

drop old structure

rewrite all historical data

require immediate synchronized deployment

But exact migration design waits for final modeling.


28. Documentation — MUST CHANGE

We will eventually need to update:

OpenAPI

refund API behavior

provider integration design

Order lifecycle documentation

possibly RFC/ADR

depending on final design significance.

A new financial workflow is substantial enough that architecture/design documentation should remain traceable.


29. Backlog — MUST EVOLVE

The original backlog stopped at:

BACKEND-128

Then production added:

BUG-201

Now Product introduces another set of work.

We should not make one enormous ticket:

Add refunds

The impact map suggests separate bounded work.

But ticket creation belongs after enough design is complete.


Must Change / May Change / Must Not Change

We can now summarize.

MUST CHANGE

Payment provider integration capability

refund application workflow

durable financial-operation persistence

administrator API surface

administrator authorization rules

customer/support read visibility

error mapping

testing

observability

migration/deployment planning

technical documentation

MUST BE REVISITED

Order lifecycle

OrderStatus representation

existing PaymentAttempt model

API compatibility

historical provider payment references

PaymentService boundary

These areas definitely need analysis, but the final implementation is not decided.


SHOULD NOT CHANGE

Product Domain

current Product price behavior

Inventory quantity behavior

unpaid cancellation semantics

Create Order idempotency semantics

Customer ownership model

Customer identity boundary

money representation in cents

Handler → UseCase → Repository architecture

PostgreSQL as local source of truth

Payment provider isolation

This is just as important as knowing what must change.


Preliminary Dependency Map

Conceptually:

Admin HTTP request
        ↓
Security
        ↓
Refund Order UseCase
        ↓
Order
        ↓
Refund workflow persistence
        ↓
Payment external boundary
        ↓
Payment Provider

Then after provider result:

Payment Provider result
        ↓
Refund workflow
        ↓
durable refund state
        ↓
Order/customer read model

The exact classes and transactions are not yet designed.


What We Learned From Existing Boundaries

Several earlier design decisions are now paying off.

Historical Order pricing

Because OrderItem stores:

unitPriceCents

we do not depend on current Product price.

Separate Payment boundary

Because provider HTTP is isolated, we can extend financial capability without putting provider code into Order.

Payment state separate from OrderStatus

Because we did not add payment failures/timeouts to OrderStatus, we have room to model refund processing without automatically turning Order lifecycle into external-provider telemetry.

Modular monolith

We can add the workflow inside the existing application without creating another service.

PostgreSQL

We already have a durable coordination store for refund attempt state.


Production Evolution Tests Architecture Quality

Earlier we said:

optimize for changeability

not:

design every future feature

Refunds are now testing that decision.

We did not build refund functionality prematurely.

But our boundaries should make it possible to add now without rewriting:

Product

Inventory

Order creation

security model

entire application architecture

That is the value of good boundaries.


What We Still Need Before Designing the Workflow

The biggest unanswered question is:

What exactly does the Payment Provider support for refunds?

We need real provider semantics before finalizing:

RefundAttempt state model

idempotency strategy

provider reference fields

reconciliation behavior

retry rules

Without that, we would be inventing a fake financial integration.


Provider Investigation Output

The next provider investigation should produce:

supported refund operation

required original payment reference

full-refund rules

provider idempotency support

provider refund identifier

success response semantics

confirmed failure semantics

timeout/unknown semantics

status lookup/reconciliation capability

Once known, we can design the workflow confidently.


Engineering Principle

The first principle:

Impact analysis asks what the new requirement touches before asking how to implement it. A production change should alter only the responsibilities whose existing assumptions are no longer sufficient.

The second:

Existing boundaries are assets. Historical pricing, isolated provider integration, separate Payment workflow state, and explicit Order lifecycle reduce the blast radius of the refund change.

The third:

External financial side effects must be designed around uncertainty. A provider refund cannot participate in a PostgreSQL transaction, so durable attempt state, retry safety, and reconciliation must be understood before implementation.

And:

Knowing what must not change is part of safe system evolution. Refund support does not justify rewriting Product, Inventory, cancellation, Customer identity, or the entire architecture.


Summary

In this lesson, we assessed the refund requirement against the existing production system.

We established that:

  • Order lifecycle must be revisited because PAID was previously terminal only under the v1 no-refund scope.
  • We have not yet decided whether REFUNDED belongs in OrderStatus.
  • Payment/refund processing states should not automatically become OrderStatus values.
  • Existing markPaid() semantics remain valid.
  • Refund must not be implemented through cancel().
  • CancelOrderUseCase remains an unpaid-order workflow.
  • Refund does not restore Inventory.
  • Product state and current Product price must not participate in refund eligibility or amount.
  • Historical Order pricing remains authoritative.
  • Money representation in integer cents remains sufficient.
  • The Payment external capability must gain refund support.
  • Provider-specific refund HTTP behavior belongs in the provider adapter.
  • Current provider refund semantics are a genuine implementation blocker.
  • Existing PaymentAttempt should be reviewed but not blindly reused.
  • Refund likely needs its own durable external-operation state.
  • The provider call must not occur inside a long database transaction.
  • Refund needs multi-phase local/external/local coordination.
  • Timeout must be treated as potentially unknown outcome.
  • Blind refund retries may cause double financial effects.
  • One unresolved refund operation per Order/payment is a likely invariant to protect.
  • Refund idempotency must be designed together with provider capabilities.
  • orders persistence may or may not change depending on Order lifecycle representation.
  • Some new persistence is definitely required.
  • Historical paid Orders must remain eligible.
  • Existing provider Payment references must be inspected for historical refundability.
  • A new administrator command API is required.
  • Customer-facing Order/read behavior must expose refund outcome somehow.
  • API compatibility must be assessed before introducing a new enum value.
  • /api/v2 should not be introduced automatically.
  • RFC 7807 mapping must expand for refund-specific business failures.
  • Refund initiation requires administrator authority.
  • Customer ownership does not imply refund authority.
  • No new Customer aggregate is required.
  • Structured logging must cover the refund workflow.
  • Metrics may add bounded refund outcomes.
  • Health checks should not automatically make refund-provider availability a global readiness dependency.
  • Existing CI can execute the new tests.
  • Refund requires new domain/application, provider, persistence, concurrency, recovery, authorization, and API tests.
  • Migration and rollout compatibility are now explicit production concerns.
  • The backlog will need to evolve after the design becomes clear.
  • Product, Inventory, cancellation semantics, Create Order idempotency, Customer identity, money representation, and the modular-monolith architecture should remain stable.

Next lesson:

Investigating the Payment Provider Refund Contract

Before we design the final workflow, we need to resolve the real external unknowns:

How is a refund requested?

Which original payment reference is required?

Does the provider support idempotency?

Can the provider return an unknown outcome?

Can refund status be queried later?

What identifier represents the refund?

Can the same successful payment
be refunded more than once?

How do duplicate refund requests behave?

Once those answers are established, we can safely design:

RefundAttempt

provider boundary

transaction boundaries

reconciliation

concurrency

API behavior

without inventing financial semantics.