Expense Report

Level: Advanced 60–90 min

Concepts: Business LogicValidationStateEdge Cases

Solutions: C# | TypeScript | Python


Build an expense report system that enforces company spending policies and approval workflows.

Requirements

An expense report contains:

  • employeeName — who submitted it
  • expenses — a list of individual expense items
  • status — Draft, Pending, Approved, Rejected

Each expense item has:

  • description — what the expense is for
  • amount — cost in dollars (positive number)
  • category — one of: Meals, Travel, Accommodation, Equipment, Other

Spending Limits

Each category has a per-item limit:

CategoryPer-Item Limit
Meals$50
Travel$500
Accommodation$200
Equipment$1,000
Other$100

An expense item that exceeds its category limit is flagged as over-limit.

Report Limits

  • Total report amount must not exceed $5,000
  • A report with any over-limit items requires manager approval
  • A report over $2,500 total requires manager approval regardless of individual items

Approval Workflow

  1. A report starts as Draft
  2. Submitting a report moves it to Pending
  3. A pending report can be Approved or Rejected
  4. Only pending reports can be approved or rejected
  5. Approved and rejected reports cannot be modified
  6. A draft report can have expenses added or removed
  7. Submitting an empty report is not allowed

Output

An expense report should produce a summary:

Expense Report: Alice Johnson
Status: Pending

  Meals: Team lunch            $45.00
  Travel: Flight to NYC       $350.00
  Meals: Client dinner         $62.00 [OVER LIMIT]
  Equipment: Laptop          $1,200.00 [OVER LIMIT]

Total: $1,657.00
Requires Approval: Yes (over-limit items)

Test Cases

ScenarioExpensesTotalApproval Required?
All under limitsMeals $30, Travel $200$230No
One over-limit itemMeals $60$60Yes (over limit)
Total over $2,500Travel $500 x 6$3,000Yes (total)
Empty report submit(none)$0Error: cannot submit
Over $5,000Equipment $1,000 x 6$6,000Error: exceeds maximum

Additional cases:

  • Cannot add expenses to an approved report
  • Cannot approve a draft report (must be pending)
  • Rejecting a report requires a reason
  • A report can be resubmitted after rejection (moves back to Draft first)

Bonus

  • Add date tracking — expenses older than 90 days are flagged as stale
  • Add receipt requirements — items over $25 must have a receipt attached (boolean flag)
  • Add department budgets — each department has a monthly spending cap

Hint

Start with the simplest case: a report with one expense, no limits exceeded. Build the approval state machine separately from the spending policy rules. Compose them together only after both work independently.

Reference Walkthrough

Full C#, TypeScript, and Python implementations live at tddbuddy-reference-katas/expense-report with twenty-two scenarios across all three languages, fluent ReportBuilder and ExpenseItemBuilder, six domain-specific exception types, and a SpendingPolicy lookup for per-category limits.

This kata ships in middle gear — one commit per language with the full domain design. See the repo’s Gears section for why that’s a deliberate teaching choice.


  • Examples Pin Intent. Properties Pin the Invariants.
    Example-based tests anchor scenarios in the team's vocabulary. Property-based tests anchor invariants across the input space. The agent needs both axes, and most suites only carry one.
  • The Test Pyramid Was an Economic Argument
    The test pyramid was not a quality law. It was a cost structure: unit tests were cheap, integration tests were expensive, so you wrote many of the first and few of the second. Agents collapsed the cost of writing tests at every level, and the cheapest test that still tells the truth is the one that pins a seam the agent cannot fake.
  • Agents Amplify Whatever Vocabulary They Find
    Agents do not bring vocabulary to your codebase. They mirror what's already there. Strong vocabulary gets agent contributions that read like the team. Weak vocabulary gets faster mediocrity. The work you did for craft reasons just became leverage.