# Phase 4 — Treat every payment outcome honestly

## The moment the product becomes trustworthy

The first three phases help us create an application, make a decision, agree terms and preserve reliable local state. Phase 4 asks a harder question: what does the marketplace say when it asks another system to move money and the answer is delayed, duplicated or unclear?

This is where a polished screen can hide a dangerous mistake. A green “paid” label may mean that a request was accepted by a provider, that money arrived in an account, or merely that our own code finished sending a message. Those are different facts. The application must use language that matches the evidence.

The examples in this phase remain fictional. No bank, payment provider or real account is connected.

## Meet the three people at the payment boundary

The borrower wants to know whether funds are available and whether a repayment has been received. They should not be shown an invented success simply because a network request finished.

The funding partner wants to know whether reserved capital became deployed exposure and whether a receipt is confirmed. They need figures that distinguish an expectation from a completed event.

Operations needs to investigate exceptions. They need the original instruction, every attempt, the provider reference, the last known response, the person who approved it and the next safe action.

The same payment therefore needs different views, with one authoritative history underneath them.

## The payment lifecycle

We will model a logical payment separately from its delivery attempts. The logical payment answers “what business event are we trying to complete?” An attempt answers “what did we send to an external provider, and what did it tell us?”

| State | Plain-English meaning | What it does not prove |
| --- | --- | --- |
| Prepared | The instruction has been assembled and awaits controlled approval. | That anyone has authorized it. |
| Authorized | An appropriate second person approved the exact instruction. | That the provider accepted it. |
| In flight | The request is being delivered or is awaiting a provider response. | That money arrived. |
| Confirmed | An authoritative response confirms the intended outcome. | That our accounts are reconciled. |
| Failed | The provider has definitively rejected or failed the instruction. | That a replacement is safe without rechecking. |
| Cancelled | The instruction was deliberately stopped under an allowed rule. | That an earlier attempt never existed. |
| Unknown | We cannot safely determine the outcome yet. | That it failed. |

The most important state is often Unknown. It prevents the system from converting uncertainty into a second money movement.

```mermaid
stateDiagram-v2
    [*] --> Prepared
    Prepared --> Authorized: independent approval
    Authorized --> InFlight: submit once
    InFlight --> Confirmed: authoritative success
    InFlight --> Failed: definitive failure
    InFlight --> Unknown: timeout or ambiguous response
    Prepared --> Cancelled: withdrawn before release
    Unknown --> Confirmed: investigation confirms success
    Unknown --> Failed: investigation confirms failure
    Unknown --> InFlight: approved controlled retry
```

## Approval is not submission

The person preparing a payment should not be able to approve the same controlled action simply by changing screens. The approval records the exact amount, destination, agreement version, purpose, currency and relevant conditions.

Before submission, the service checks that the agreement is still valid, the reservation is still available, required verification is current and the approval has not been superseded. These checks belong on the server. A disabled button is helpful to a user, but it is not a security boundary.

In ASP.NET Core, this is a useful application boundary: the controller or endpoint receives intent, the handler loads authoritative state, the domain validates the transition and the persistence layer commits the state and an outbox message together. The provider call happens outside the database transaction.

## Why the provider call cannot share our transaction

SQL Server cannot roll back a bank or payment provider after the provider has accepted a request. Keeping a network call open inside a SQL transaction would also hold locks while waiting for an external system.

The safer sequence is:

1. Prepare and authorize a durable payment instruction.
2. Commit the instruction and an outbox message locally.
3. A worker claims the message and calls the provider.
4. Store the response or the fact that the outcome is unknown.
5. Publish a new internal event for the borrower, partner, ledger and reconciliation views.

If the worker crashes after the provider accepts the request but before our database is updated, the original logical payment remains identifiable. Recovery investigates that identity; it does not create a blind replacement.

## Stable identity and idempotency

Every logical payment needs a stable identifier. Every attempt needs its own attempt number and provider reference. A retry must send the original logical identifier as an idempotency key when the provider supports it.

This protects against a common failure:

1. Our API sends £5,000.
2. The provider moves the money.
3. The network connection breaks before our API receives the response.
4. A user presses “try again”.

Without a stable identity, the second request can move another £5,000. With one, the service can look up the original outcome or route the case to investigation.

Idempotency is not a claim that every external provider behaves perfectly. It is a contract we make explicit, test and monitor.

## Reconciliation is a separate journey

A provider confirmation and an account statement are not the same evidence. We therefore give reconciliation its own dimension:

| Reconciliation state | Meaning |
| --- | --- |
| Unmatched | A confirmed or received event has no matched statement entry yet. |
| Matched | The statement and internal event agree under defined rules. |
| Exception | Amount, date, reference or account does not match. |
| Reviewed | An authorized person has examined the exception. |
| Resolved | The correction or explanation is recorded and linked. |

The application can show “payment confirmed, reconciliation pending.” That sentence is more useful than hiding the difference behind one status badge.

## What each person sees

The borrower sees whether a release or repayment is confirmed, pending investigation or needs an action. They do not need internal provider diagnostics.

The funding partner sees reserved capital, deployed exposure and confirmed receipts as separate figures. A projected return is not a receipt and a receipt is not necessarily available for withdrawal.

Operations sees the complete controlled record: who prepared and approved the instruction, which agreement version was used, each attempt, provider references, response times, reconciliation evidence and the next owner.

These are three projections of one history. They should not be three competing sources of truth.

## Failure scenarios we will test

### The provider rejects the request

The logical payment becomes Failed, the reservation follows the defined release policy and the borrower receives a clear explanation. A replacement requires a new controlled decision if the terms or conditions changed.

### The provider times out

The logical payment becomes Unknown. The reservation remains protected. A worker or operator checks the provider using the original reference. The system does not label it failed and does not automatically submit another payment.

### The publisher sends twice

The consumer sees the same message identifier twice. Its inbox or idempotency record allows one local effect. The second delivery is acknowledged without repeating the effect.

### A statement disagrees

Reconciliation creates an Exception with evidence and an owner. A correction is a linked new event; the original statement and internal record remain visible.

## How this demonstrates senior ASP.NET engineering

This phase is not a payment-provider integration demo. It demonstrates the judgement required before connecting one: explicit state transitions, controlled application boundaries, durable events, safe retries, concurrency awareness, authorization and tests that explore failure rather than only the happy path.

The implementation would use ASP.NET Core endpoints, application handlers, domain policies, EF Core transactions, an outbox worker and provider adapters behind interfaces. Those choices matter because they keep business rules testable and prevent an external dependency from silently defining the product’s truth.

## Evidence for completion

The phase is complete only when the evidence shows:

- a payment cannot be approved by its preparer;
- the exact agreement and amount are retained;
- duplicate commands do not create duplicate logical payments;
- unknown outcomes remain unknown until evidence resolves them;
- retries use a stable identity and fresh authorization;
- duplicate messages do not repeat local effects;
- reconciliation can remain pending after confirmation;
- every exception has an owner, next action and history.

These are implementation and test targets, not claims that a live payment service is already operating.

## Guided practice

Design the response for a £5,000 release that times out after the provider may have accepted it. Write one short message for the borrower, one portfolio line for the funding partner and one operations task. Then list the evidence required before resolving the payment as Confirmed or Failed.

The strongest answer preserves uncertainty, protects the reservation and gives each person a useful next step. That is the product promise expressed through engineering.
