Loan & Investment Marketplace · Phase 02

Turn decisions into commitments

Recorded decisions, versioned offers and funding reservations

Approve or decline with a reason, issue an offer, accept its exact version and reserve simulated funding. Keep acceptance, commitment and payment distinct.

By Afzal AhmedDetailed guide · 5 min reference readingRead in chapters

The question we’ll work through

What must happen between approval and a funding commitment?

An accepted versioned offer and a capacity-checked reservation, with no implied payment.

Before you begin

Complete Phase 1 and submit a synthetic application. Start both the website preview and local .NET API.

Jump to the guided exercise ↓

This guide is part of a phased educational application. Behaviour is labelled as planned, demonstrated or verified. Examples use synthetic data and simulated money; a real-money launch would require separate commercial, legal, security and operational decisions.

Open the interactive application →

Continue one case across three perspectives

Phase 1 ended with an assigned application. This phase adds an assessment decision, an offer, borrower acceptance and a funding reservation. The same case history connects the steps. There is still no disbursement, repayment or real money.

Use the interactive application with the local API running. The setup commands are in Phase 1.

Make a decision with a reason

As the assigned operations reviewer, enter a reason of 10–500 characters and approve or decline. The API checks the role, the reviewer’s ownership, the expected revision, the current status and synthetic evidence. A borrower cannot approve a case by calling the endpoint directly. Another reviewer cannot overwrite its owner’s decision.

Approval moves the application to Approved. It does not generate a payment or reserve capital. Decline ends the current application journey while preserving the reason. A second contradictory decision is rejected rather than silently replacing the first.

Issue terms that can be identified

After approval, issue offer v1 with a reason. Its terms snapshot the principal, twelve-month term, zero demo interest, zero fees and an expiry seven days from issue. Total repayable equals the principal under these teaching assumptions; a repayment schedule has not yet been implemented.

The reviewer may supersede an unaccepted offer. Issuing v2 marks v1 Superseded while keeping it inspectable. Accepted offers cannot be rewritten through this operation. Changing an accepted agreement would require a separately designed amendment process.

Offer version and application revision answer different questions. The offer version identifies exact terms. The application revision identifies the whole case state observed by the caller. Acceptance supplies both, so a stale view or an outdated offer is rejected.

Accept the exact offer

Switch back to the borrower. Review the displayed terms and select the confirmation checkbox. Acceptance records the offered version and timestamp. The service rejects an expired or superseded offer. Operations must issue a new version after expiry; the interface does not silently extend the old terms.

The borrower’s action changes the case to Accepted. This is a demonstration of explicit consent to synthetic terms, not an electronic-signature service or a legally verified lending agreement.

Reveal the eligible opportunity

Switch to Cedar Demo Funding. Accepted cases appear as opportunities identified by case number and principal. Borrower documents, names, purposes and internal review notes are excluded from the partner response. Accepted terms are visible because they define the commitment being considered.

Cedar starts with £50,000 of synthetic capacity. Each opportunity is funded by one partner in full. The interface separates available capacity from reserved commitments. Deployed and received amounts remain £0 because this phase has not executed a payment.

Reserve without overspending

Reserve an accepted case. The API checks that capacity remains sufficient, the current revision matches, the case is still Accepted and nothing is already reserved. It changes the status to Funding Reserved and records the partner and amount.

The capacity calculation and reservation occur inside the same single-process mutation lock. Two concurrent requests against different cases cannot both spend the same final capacity. A repeated reservation against one case cannot reserve its principal again. The integration tests exercise both situations.

The lifecycle is Approved → Offer Issued → Accepted → Funding Reserved. These are distinct events. The final state means a commitment exists; it does not mean funds were sent, received or reconciled. Reservation cancellation, expiry and release rules remain future work, so the demo has no misleading “refund” shortcut.

Understand the implementation boundary

The ASP.NET Core service enforces the rules and provides durable local JSON state. Angular presents the three perspectives, reports API errors and allows refreshing after a stale update. The activity history records the actor, time, action and explanation. It is application history, not an immutable financial audit ledger.

The service binds to loopback and restricts browser origins to the documented local previews. Demo session selection is intentionally open to anyone on the same machine. Do not expose the API or enter personal information. A deployable service needs real authentication, an authoritative database, operational controls and a more complete financial model.

Run the failure scenarios

powershell
dotnet build marketplace/Marketplace.Api
node scripts/test-marketplace.mjs

The HTTP suite checks successful decisions and reservations, but also invalid amounts, missing evidence, unassigned reviewers, borrower isolation, stale commands, superseded and expired offers, duplicate creation, duplicate reservations and competing requests for limited capacity. It restarts the API to verify that successful state was saved, and runs against a temporary store rather than your review data.

For a manual concurrency exercise, open the same case in two browser tabs under Jordan’s identity. Act in one tab, then act using the older state in the other. The server rejects the stale command. Refresh before choosing the next action; do not blindly retry a transition whose outcome might already be saved.

What remains after Phase 2

The next work should establish real identity and SQL persistence before expanding the payment lifecycle. Payment preparation, independent authorization, uncertain outcomes, reconciliation, repayment schedules, arrears and closure require their own rules and tests. They are not implied by the funding reservation demonstrated here.

Practise, then reflect

Your turn to make the decision

Issue offer v1, supersede it with v2 and try accepting v1 through the API. Then accept v2 and reserve its principal from the funding workspace.

Compare your reasoning with mine

The API rejects the old offer version. Accepting the current offer records its version and time. Reserving reduces available capacity while deployed and received totals remain zero.

Take it one step further

Why should a reservation stay distinct from an outgoing payment, especially if a payment response is lost?

A useful companion

Clean Architecture and pragmatic CQRS →

Bring the question to your own application

If you would like to work through a similar design or implementation decision together, we can use it as the starting point for a mentoring session.

Explore practical mentoring →