06 · Card holds

Track active authorization holds and available balance

Compute available balance as posted funds minus active card holds, with capture, void and expiry.

FastAPI · SQLitemedium

What the hardened grader checks

Solutions

Reference

reference

Stores holds with timestamps and filters out expired ones whenever a balance is read.

Naive: accepted (3/3)Hardened: accepted (12/12)

Alternative

alt_valid

Retires expired records at transaction entry and dispatches mutations centrally; reference filters active holds by timestamp.

Naive: accepted (3/3)Hardened: accepted (12/12)

wrong_available_ignores_holds

Authorizes against posted funds, ignoring existing holds.

Naive: rejected (2/3)Hardened: rejected (7/12)

wrong_expired_holds_count

Counts expired active records against availability.

Naive: accepted (3/3)Hardened: rejected (11/12)

wrong_partial_capture_keeps_remainder

Leaves the remainder held after a partial capture.

Naive: rejected (2/3)Hardened: rejected (9/12)

Held-out solutions

Tests × solutions

Each row groups a test’s parameterized cases; any failing case marks the row as Fail. * marks a wrong solution’s declared must_fail test.

Hardened · 12 recorded cases · 9 test rows
TestStarterReferenceAlternativeavailable ignores holdsexpired holds countpartial capture keeps remainder
Existing holds reduce availableFailPassPassFail *PassPass
Partial capture releases remainderFailPassPassPassPassFail *
Expiry exactly seven daysFailPassPassFailFail *Pass
Void overcapture and persistenceFailPassPassFailPassPass
Overdraft boundaryFailPassPassFailPassPass
Capture amount validation before closed statusFailPassPassPassPassFail
Authorization validation and unknown idsFailPassPassPassPassPass
Naive clock rejectedPassPassPassPassPassPass
Random operation modelFailPassPassFailPassFail
Naive grader
Naive · 3 recorded cases · 3 test rows
TestStarterReferenceAlternativeavailable ignores holdsexpired holds countpartial capture keeps remainder
Full captureFailPassPassPassPassPass
Ticket worked exampleFailPassPassFailPassFail
Invalid amount rejectedPassPassPassPassPassPass

The ticket

Ticket: available balance must include active holds

holds.main.create_app(database_path,clock)->FastAPI uses SQLite. The declared holds.clock.Clock Protocol has now()->datetime, always aware (otherwise ValueError). The factory seeds two synthetic accounts on first use, without resetting existing state: acct_standard: posted10000, overdraft0; acct_flex: posted10000, overdraft5000. All money is integer CAD minor units (2 dp). All amounts must be positive ints, never bool.

  • GET /accounts/{id}/balance ->200 {posted_minor,available_minor}. available = posted minus sum of active, unexpired holds.
  • POST /accounts/{id}/authorizations {amount_minor} ->201 {auth_id} iff amount <= available + that account's overdraft limit; otherwise402 insufficient_funds with no effect.
  • A hold expires exactly 7×24 hours after creation, and stops counting at that instant.
  • POST /authorizations/{id}/capture {amount_minor}: amount <= held amount; overcapture ->422 capture_exceeds_hold with no effect. Successful capture ->200 {auth_id,status:"captured"}, deduct amount from posted balance, close the hold and release ALL unused remainder.
  • POST /authorizations/{id}/void ->200 {auth_id,status:"voided"}, close hold, no posted change.
  • Capturing/voiding an expired or already closed hold ->409 authorization_closed, no effect.
  • Invalid body/amount ->422 invalid_amount; unknown account/auth ->404 not_found. Errors are {"error":{"code":"...","message":"..."}}; message text is free. For capture, validate body/amount first, then existence, then closed/expired status, then overcapture. Thus a positive overcapture on a closed/expired hold returns 409 authorization_closed; an invalid amount on that hold returns 422 invalid_amount. Void has no JSON body.
  • Auth ids are nonempty unique strings; available may be negative within overdraft. State survives creating another app on the same file. Operations are atomic and per account.

Worked example (acct_standard, CAD minor units): authorize 3000 -> 201 and the balance is {posted_minor: 10000, available_minor: 7000}. Capture 1200 -> 200 and the balance is {posted_minor: 8800, available_minor: 8800}: the 1200 is deducted from posted and the unused 1800 of the hold is released. A further authorization of 8801 is declined with 402 insufficient_funds; 8800 is approved.

Tests use the HTTP interface and the injected clock only. No real accounts, system clock delays or network payments. Run python -m pytest tests -q from starter.

Source