A commercial fuel-card platform whose business logic lives in Oracle PL/SQL, with a .NET 9 data layer that does nothing but call it.
Modelled on the shape of a real fleet-payments programme — card issuing and acceptance, real-time authorization at the pump, nightly settlement and interchange, and volume-tier rebates back to the customer.
sql/ is 1,370 lines across four packages, and it is not boilerplate:
| Routine | Lines | Cyclomatic | What makes it hard |
|---|---|---|---|
pkg_authorization.authorize |
212 | 28 | 11 parameters, 9 tables, a custom exception used as control flow, a MERGE on the velocity counter, and two different partial-approval paths (daily limit, then credit) that can each downgrade an approval |
pkg_settlement.sweep_transactions |
61 | 5 | cursor loop that CONTINUEs past bad rows into an exception queue rather than failing the batch |
pkg_rebate_billing.tiered_rebate |
34 | 6 | marginal tier walk — each slice of volume earns its own rate, not the top rate on everything |
pkg_settlement.interchange_for |
37 | 5 | MCC-specific rate, falling back to network-wide, falling back to a floor |
pkg_rebate_billing.convert_amount |
44 | 6 | FX at the day's fixing, falling back to the last fixing on or before it |
pkg_settlement.reprice_batch |
27 | 1 | EXECUTE IMMEDIATE — dynamic SQL the analyzer flags |
Across the corpus: 23 migratable routines, 13 that write, 5 returning ref cursors, 10 stamping rows with the database clock, and every risk flag the analyzer knows — cursors, dynamic-sql, exception-handling, merge.
The marginal tier walk is the one worth watching in a demo. It is the rule most often got wrong when this logic is reimplemented by hand, because the naive reading — look up the customer's tier, apply that rate to everything — is both simpler and wrong, and it overpays.
Every method in src/FleetCard.Data/ is a marshalling shim. This is the whole of AuthorizeAsync, the entry point to 212 lines of decisioning:
p.Add("p_card_id", cardId);
// … nine more …
p.Add("p_reason", dbType: DbType.String, direction: ParameterDirection.Output, size: 60);
await _db.ExecuteAsync(new CommandDefinition(
"pkg_authorization.authorize", p,
commandType: CommandType.StoredProcedure, cancellationToken: ct));No card control, no velocity rule, no partial-approval logic. 23 live CommandType.StoredProcedure call sites, and not one line of business logic on the .NET side.
sql/01_schema.sql 17 tables, sequences, indexes
sql/02_pkg_authorization.sql the real-time decision path
sql/03_pkg_settlement.sql nightly batch, interchange, reprice
sql/04_pkg_rebate_billing.sql marginal tier rebates, FX, invoicing
src/FleetCard.Data/ 3 repositories — 23 delegating methods
src/FleetCard.App/ console host that wires them together
- Open in CogniDev Workbench (Open from GitHub → cognidevwb/fleet-card-plsql).
- Run Understand.
- Run Move stored procedures to .NET.
One task per routine, ported into the repository that already calls it, compiled after each one. The run finishes with a coverage report reconciling all 23 against the delivered C# — every parameter accounted for, every table the procedure wrote still written, the return shape unchanged, and no call site still reaching for the procedure.
dotnet build -c Release # no database required