Skip to content

Instantly share code, notes, and snippets.

@fredflint
Created January 24, 2026 17:53
Show Gist options
  • Select an option

  • Save fredflint/7ba2ab9f669918c3c427b5f0f17f5f8f to your computer and use it in GitHub Desktop.

Select an option

Save fredflint/7ba2ab9f669918c3c427b5f0f17f5f8f to your computer and use it in GitHub Desktop.
Finance Calculator CLI - PRD + Progress (Ralph Native workflow example)

PRD: Financial Calculator CLI

Introduction

Create a command-line financial calculator tool that computes common loan and interest calculations. Supports simple interest, mortgage payments, personal loans, car loans, and balloon payment loans. Designed as a clean, professional portfolio piece demonstrating financial domain knowledge and CLI design patterns.

Goals

  • Provide 5 distinct financial calculators via command-line interface
  • Accept inputs via command-line arguments (no interactive prompts)
  • Return concise, accurate calculations
  • Follow functional programming principles (pure calculation functions)
  • Demonstrate professional code structure suitable for portfolio

User Stories


Phase 1: Foundation


US-001: Create CLI structure and argument parsing [x]

Description: As a developer, I want a clean CLI foundation so I can add calculators incrementally.

Acceptance Criteria:

  • Script named finance_calc.py in root directory
  • Uses argparse with subcommands for each calculator type
  • Subcommands: simple-interest, mortgage, personal-loan, car-loan, balloon
  • Help text shows usage for each calculator
  • Running without args shows available calculators
  • Typecheck passes
  • Script runs: python finance_calc.py --help

US-REVIEW-PHASE1: Foundation Review [x]

Description: Review foundation before building calculators.

Acceptance Criteria:

  • CLI structure runs without errors: python finance_calc.py --help
  • All 5 subcommands listed in help output
  • Typecheck passes: mypy finance_calc.py
  • Apply Linus criteria: Is this the simplest CLI structure possible?
  • No over-engineering (no unnecessary abstractions)

Phase 2: Core Calculators


US-002: Implement simple interest calculator [x]

Description: As a user, I want to calculate simple interest so I can evaluate non-compounding interest scenarios.

Acceptance Criteria:

  • Pure function: calculate_simple_interest(principal, rate, time) -> float
  • Formula: I = P × r × t
  • CLI: python finance_calc.py simple-interest --principal 1000 --rate 5 --years 3
  • Output format: "Simple Interest: $150.00"
  • Rate input as percentage (5 = 5%, not 0.05)
  • Tests cover: basic calculation, zero values, edge cases
  • Typecheck passes

US-003: Implement mortgage calculator [x]

Description: As a user, I want to calculate monthly mortgage payments so I can evaluate home loan affordability.

Acceptance Criteria:

  • Pure function: calculate_mortgage_payment(principal, annual_rate, years) -> float
  • Formula: M = P × [r(1+r)^n] / [(1+r)^n - 1] where r = monthly rate
  • CLI: python finance_calc.py mortgage --principal 200000 --rate 4.5 --years 30
  • Output format: "Monthly Payment: $1,013.37"
  • Tests cover: standard 30-year, 15-year, different rates
  • Typecheck passes

US-004: Implement personal loan calculator [x]

Description: As a user, I want to calculate personal loan payments so I can evaluate unsecured loan costs.

Acceptance Criteria:

  • Pure function: calculate_personal_loan(principal, annual_rate, months) -> float
  • Formula: Same as mortgage but term in months (more common for personal loans)
  • CLI: python finance_calc.py personal-loan --principal 5000 --rate 12 --months 24
  • Output format: "Monthly Payment: $235.37"
  • Tests cover: short-term (12mo), long-term (60mo), high rates
  • Typecheck passes

US-005: Implement car loan calculator [x]

Description: As a user, I want to calculate car loan payments so I can evaluate auto financing options.

Acceptance Criteria:

  • Pure function: calculate_car_loan(principal, annual_rate, years, down_payment) -> dict
  • Returns: monthly payment and total financed amount
  • CLI: python finance_calc.py car-loan --price 25000 --down 5000 --rate 6 --years 5
  • Output format: "Financed: $20,000.00\nMonthly Payment: $386.66"
  • Down payment defaults to 0 if not provided
  • Tests cover: with/without down payment, various terms
  • Typecheck passes

US-006: Implement balloon payment calculator [x]

Description: As a user, I want to calculate balloon payment loan costs so I can evaluate loans with large final payments.

Acceptance Criteria:

  • Pure function: calculate_balloon_payment(principal, annual_rate, years, balloon_percent) -> dict
  • Returns: monthly payment and final balloon amount
  • Formula: Amortize partial amount, balloon pays remaining principal
  • CLI: python finance_calc.py balloon --principal 50000 --rate 7 --years 5 --balloon 30
  • Output format: "Monthly Payment: $693.04\nBalloon Payment: $15,000.00"
  • Balloon percent is percentage of original principal (30 = 30%)
  • Tests cover: 20%, 30%, 50% balloon scenarios
  • Typecheck passes

US-006a: Fix simple interest output format inconsistency [x]

Description: Simple interest output uses :.2f while all other calculators use :,.2f (with thousands separator).

Acceptance Criteria:

  • Change line 270 from ${interest:.2f} to ${interest:,.2f}
  • Verify: python finance_calc.py simple-interest --principal 10000 --rate 5 --years 3 outputs "$1,500.00" (with comma)
  • All outputs consistently use :,.2f format

US-006b: Extract shared loan payment calculation logic [x]

Description: The loan payment formula is duplicated 4 times (mortgage, personal-loan, car-loan, balloon). Extract to a shared helper function.

Acceptance Criteria:

  • Create pure helper: _calculate_monthly_payment(principal: float, monthly_rate: float, num_payments: int) -> float
  • Helper handles zero rate case (returns principal / num_payments)
  • Refactor all 4 calculator functions to use the helper
  • All existing tests still pass
  • All CLI outputs unchanged (verify with sample inputs)

US-REVIEW-PHASE2: Calculator Functions Review [x]

Description: Review all calculator functions before adding tests and validation.

Acceptance Criteria:

  • All 5 calculators work with sample inputs from acceptance criteria
  • Pure functions only (no side effects, no global state)
  • Formulas match reference section exactly
  • Output formatting consistent (currency with 2 decimals)
  • Apply Linus criteria: Any simpler approach possible?
  • No code duplication between calculators (extract shared logic)

Phase 3: Quality & Polish


US-007: Add comprehensive tests [x]

Description: As a developer, I want automated tests for all calculators to ensure accuracy and prevent regressions.

Acceptance Criteria:

  • Test file: test/test_finance_calc.py
  • Tests for each calculator function (US-002 through US-006)
  • Test edge cases: zero values, very large numbers, maximum terms
  • Test CLI argument parsing and error handling
  • Test output formatting (decimal places, currency symbols)
  • All tests pass: pytest test/test_finance_calc.py -v
  • Test coverage ≥ 90% for calculation functions
  • Typecheck passes

US-008: Add input validation and error handling [x]

Description: As a user, I want clear error messages for invalid inputs so I can correct my command quickly.

Acceptance Criteria:

  • Validate: principal > 0, rate ≥ 0, term > 0
  • Validate: down payment ≤ principal (car loans)
  • Validate: balloon percent between 0-100 (balloon loans)
  • Error messages are clear: "Error: Principal must be greater than 0"
  • Exit with code 1 on validation failure
  • Tests verify all validation rules
  • Typecheck passes

US-REVIEW-FINAL: Final Review [x]

Description: Comprehensive final review before marking project complete.

Acceptance Criteria:

  • All tests pass: pytest test/test_finance_calc.py -v
  • Typecheck clean: mypy finance_calc.py
  • Manual test each calculator with sample inputs
  • Code quality meets portfolio standards
  • Apply Linus criteria to entire codebase
  • No TODO comments or incomplete code
  • README or docstrings explain usage

Non-Goals

  • No interactive prompts (all inputs via CLI args only)
  • No GUI or web interface
  • No amortization schedules (just final calculated amounts)
  • No comparison mode (single calculation per run)
  • No file I/O or saved scenarios
  • No currency conversion or international formats
  • No advanced features (extra payments, rate changes, PMI, taxes)
  • No total interest calculations (just monthly payment amounts)

Technical Considerations

  • Python 3.7+ (matches repo standards)
  • Use argparse for CLI (stdlib, no dependencies)
  • All calculation functions are pure (testable, no side effects)
  • Use Decimal for currency calculations (avoid float rounding errors)
  • Follow functional programming principles from CLAUDE.md
  • Single responsibility: each function does one calculation
  • Type hints on all functions
  • Use dataclasses for complex return types if needed

Formulas Reference

Simple Interest:

I = P × r × t
where:
  P = principal
  r = annual rate (as decimal: 5% = 0.05)
  t = time in years

Standard Loan Payment (Mortgage, Personal, Car):

M = P × [r(1+r)^n] / [(1+r)^n - 1]
where:
  M = monthly payment
  P = principal
  r = monthly interest rate (annual_rate / 12)
  n = number of payments (months)

Balloon Payment:

1. Calculate payment on reduced principal (P × (1 - balloon%))
2. Balloon amount = P × balloon%
# Progress Log
## Learnings
(Patterns discovered during implementation)
---
### US-001: Create CLI structure and argument parsing [x]
- TDD approach: wrote 14 tests first (RED), then implemented CLI (GREEN)
- Used argparse with subparsers for clean subcommand structure
- Pattern: `create_parser()` as pure function returning configured parser
- Key decisions:
- Short flags (-p, -r, -y, -m, -d, -b) for convenience
- Each subcommand has its own description and help text
- Running without args shows help (not an error)
- All 14 tests pass, mypy clean
- Files created: finance_calc.py, test/test_finance_calc.py
---
## Review PASSED - US-REVIEW-PHASE1
### Criteria Verified:
1. CLI runs without errors: `python finance_calc.py --help` - PASSED
2. All 5 subcommands listed (simple-interest, mortgage, personal-loan, car-loan, balloon) - PASSED
3. Typecheck passes: `mypy finance_calc.py` - PASSED (no issues found)
### Linus Criteria Analysis:
- **Good taste**: Code is simple and elegant. Single `create_parser()` function, clean separation of concerns.
- **No special cases**: Edge cases handled through argparse design (required args, type validation). The "no subcommand" case handled elegantly with help display.
- **Data structures**: Uses argparse's built-in subparser pattern - standard and appropriate.
- **Complexity**: Nothing to simplify. Code is as minimal as it can be while remaining complete.
### Over-engineering Check:
- No unnecessary base classes or inheritance
- No factory patterns or dependency injection
- No premature abstraction
- Appropriate use of type hints (return types only)
**Verdict**: Foundation is solid. Proceed to Phase 2.
---
### US-002: Implement simple interest calculator [x]
- TDD approach: wrote 11 tests first (RED), all failed on import, then implemented (GREEN)
- Pure function: `calculate_simple_interest(principal, rate, time) -> float`
- Formula: I = P x r x t (rate converted from percentage to decimal internally)
- Key decisions:
- Rate input as percentage (user-friendly: 5 = 5%, not 0.05)
- Internal conversion: `rate_decimal = rate / 100.0`
- Simple CLI wiring: if-block in main() dispatches to pure function
- Test coverage:
- Basic calculation (1000 @ 5% for 3 years = $150)
- Zero values (principal, rate, time)
- Fractional time (0.5 years)
- Large values (1,000,000)
- CLI output formatting (2 decimal places)
- Short flags (-p, -r, -y)
- All 25 tests pass (14 CLI structure + 11 simple interest), mypy clean
---
### US-003: Implement mortgage calculator [x]
- TDD approach: wrote 9 tests first (RED - import error), then implemented (GREEN)
- Pure function: `calculate_mortgage_payment(principal, annual_rate, years) -> float`
- Formula: M = P x [r(1+r)^n] / [(1+r)^n - 1]
- Key decisions:
- Rate input as percentage (4.5 = 4.5%, not 0.045)
- Internal conversion: `r = annual_rate / 12.0 / 100.0`
- Special case for zero interest rate: `principal / n` (simple division)
- Test coverage:
- Standard 30-year ($200k @ 4.5% = $1,013.37)
- 15-year mortgage ($200k @ 4.5% = $1,529.99)
- High rate (8%)
- Low rate (3%)
- Zero rate edge case
- Pure function verification
- CLI output formatting with comma separator (`:,.2f`)
- Short flags (-p, -r, -y)
- All 34 tests pass (14 CLI + 11 simple interest + 9 mortgage), mypy clean
---
### US-004: Implement personal loan calculator [x]
- TDD approach: wrote 9 tests first (RED - import error), then implemented (GREEN)
- Pure function: `calculate_personal_loan(principal, annual_rate, months) -> float`
- Formula: M = P x [r(1+r)^n] / [(1+r)^n - 1] (same as mortgage)
- Key difference from mortgage: takes months directly instead of years
- Key decisions:
- Rate input as percentage (12 = 12%, not 0.12)
- Internal conversion: `r = annual_rate / 12.0 / 100.0`
- Special case for zero interest rate: `principal / n` (simple division)
- Reused same amortization formula pattern as mortgage
- Test coverage:
- Standard 24-month ($5k @ 12% = $235.37)
- Short-term 12-month ($10k @ 10% = $879.16)
- Long-term 60-month ($15k @ 8% = $304.15)
- High rate (24%)
- Zero rate edge case
- Pure function verification
- CLI output formatting with comma separator (`:,.2f`)
- Short flags (-p, -r, -m)
- All 43 tests pass (14 CLI + 11 simple interest + 9 mortgage + 9 personal loan), mypy clean
---
### US-005: Implement car loan calculator [x]
- TDD approach: wrote 10 tests first (RED - import error), then implemented (GREEN)
- Pure function: `calculate_car_loan(principal, annual_rate, years, down_payment) -> dict[str, float]`
- Formula: M = P x [r(1+r)^n] / [(1+r)^n - 1] (same as mortgage)
- Key difference from other calculators: returns dict with 'financed' and 'monthly_payment'
- Key decisions:
- Rate input as percentage (6 = 6%, not 0.06)
- Internal conversion: `r = annual_rate / 12.0 / 100.0`
- Special case for zero interest rate: `financed / n` (simple division)
- Down payment defaults to 0.0 in function signature
- CLI uses --price (not --principal) for user clarity
- Test coverage:
- Basic calculation ($25k, $5k down @ 6% for 5 years = $386.66/month)
- No down payment ($25k @ 6% for 5 years = $483.32/month)
- Short-term 3-year ($20k @ 5% = $599.42/month)
- Large down payment ($30k with $10k down @ 4% for 4 years = $451.58/month)
- Zero rate edge case
- Pure function verification
- Returns dict with correct keys
- CLI output formatting with comma separator (`:,.2f`)
- Short flags (-p, -d, -r, -y)
- Default down payment (omitting --down)
- All 53 tests pass (14 CLI + 11 simple interest + 9 mortgage + 9 personal loan + 10 car loan), mypy clean
---
### US-006: Implement balloon payment calculator [x]
- TDD approach: wrote 9 tests first (RED - import error), then implemented (GREEN)
- Pure function: `calculate_balloon_payment(principal, annual_rate, years, balloon_percent) -> dict[str, float]`
- Formula:
- Balloon amount = P x balloon_percent / 100
- Reduced principal = P - balloon_amount
- Monthly payment = amortize reduced principal using standard formula
- Key decisions:
- Rate input as percentage (7 = 7%, not 0.07)
- Balloon percent as percentage (30 = 30%, not 0.30)
- Returns dict with 'monthly_payment' and 'balloon_payment' keys
- Special case for zero interest rate: reduced_principal / n
- Note: PRD had incorrect expected value ($743.25) - corrected to $693.04 based on formula
- Test coverage:
- 30% balloon ($50k @ 7% for 5 years = $693.04/month + $15k balloon)
- 20% balloon ($100k @ 6% for 5 years = $1,546.62/month + $20k balloon)
- 50% balloon ($40k @ 5% for 3 years = $599.42/month + $20k balloon)
- Zero rate edge case
- Pure function verification
- Returns dict with correct keys
- CLI output formatting with comma separator (`:,.2f`)
- Short flags (-p, -r, -y, -b)
- All 62 tests pass (14 CLI + 11 simple interest + 9 mortgage + 9 personal loan + 10 car loan + 9 balloon), mypy clean
---
## Review ISSUES FOUND - US-REVIEW-PHASE2
### Issues Identified:
**Issue 1: Inconsistent output formatting for simple interest**
- Location: `main()` line 270 in finance_calc.py
- Problem: Simple interest uses `:.2f` while all other calculators use `:,.2f`
- Example: "$1500.00" vs "$1,500.00" (missing thousands separator)
- Fix task: US-006a
**Issue 2: Code duplication in loan payment calculation**
- Location: Lines 46-51, 74-80, 107-113, 147-152 in finance_calc.py
- Problem: The same loan payment formula is repeated 4 times
- Pattern: Zero rate check + rate conversion + factor calculation + payment formula
- Violates DRY principle and makes maintenance harder
- Fix task: US-006b
### Criteria Status:
- [x] All 5 calculators work with sample inputs from acceptance criteria
- [x] Pure functions only (no side effects, no global state)
- [x] Formulas match reference section exactly
- [ ] Output formatting consistent (currency with 2 decimals) - FAILED (Issue 1)
- [x] Apply Linus criteria: Any simpler approach possible?
- [ ] No code duplication between calculators (extract shared logic) - FAILED (Issue 2)
### Inserted Fix Tasks:
- US-006a: Fix simple interest output format inconsistency
- US-006b: Extract shared loan payment calculation logic
**Next Step**: Complete US-006a and US-006b before re-running this review.
---
### US-006a: Fix simple interest output format inconsistency [x]
- Changed line 270 from `${interest:.2f}` to `${interest:,.2f}`
- Verified: `python finance_calc.py simple-interest --principal 10000 --rate 5 --years 3` outputs "$1,500.00" (with comma)
- All 62 tests still pass (existing tests use small values without thousands, still valid)
- mypy passes with no issues
- Consistency achieved: all CLI outputs now use `:,.2f` format
---
### US-006b: Extract shared loan payment calculation logic [x]
- Created pure helper: `_calculate_monthly_payment(principal: float, monthly_rate: float, num_payments: int) -> float`
- Helper is a pure function with no side effects, deterministic output
- Handles zero rate case (returns principal / num_payments)
- Refactored all 4 calculator functions to use the helper:
- `calculate_mortgage_payment` - converts annual_rate to monthly_rate, years to num_payments
- `calculate_personal_loan` - converts annual_rate to monthly_rate, uses months directly
- `calculate_car_loan` - converts annual_rate to monthly_rate, years to num_payments
- `calculate_balloon_payment` - converts annual_rate to monthly_rate, years to num_payments
- Design decision: Helper takes monthly_rate (not annual_rate) to match the formula directly
- This makes the helper more general and matches the formula: M = P x [r(1+r)^n] / [(1+r)^n - 1]
- Conversion from annual rate to monthly rate done at the call site (cleaner separation)
- All 62 existing tests still pass
- mypy --strict passes with no issues
- No behavior change - pure refactor for DRY principle
---
## Review PASSED - US-REVIEW-PHASE2
### Criteria Verified:
1. All 5 calculators work with sample inputs - PASSED
- simple-interest: $150.00
- mortgage: $1,013.37
- personal-loan: $235.37
- car-loan: Financed $20,000.00, Monthly $386.66
- balloon: Monthly $693.04, Balloon $15,000.00
2. Pure functions only - PASSED
- All calculator functions are pure (no side effects, no global state)
- Shared `_calculate_monthly_payment` helper is also pure
3. Formulas match reference section - PASSED
- Simple interest: I = P x r x t
- Loan payment: M = P x [r(1+r)^n] / [(1+r)^n - 1]
- Balloon: Amortizes reduced principal
4. Output formatting consistent - PASSED (fixed in US-006a)
- All outputs use `:,.2f` format with thousands separator
5. Linus criteria - PASSED
- Code is simple and elegant
- Shared helper eliminates duplication
- No unnecessary abstractions
6. No code duplication - PASSED (fixed in US-006b)
- `_calculate_monthly_payment` used by all 4 loan calculators
### Tests and Typecheck:
- 62 tests pass (PYTHONPATH=. pytest -v)
- mypy finance_calc.py: Success (no issues found)
**Verdict**: Phase 2 complete. Proceed to Phase 3 (US-007: Comprehensive tests).
---
### US-007: Add comprehensive tests [x]
- Added 34 new tests (total: 96 tests)
- Test coverage: 97% (previously 38% due to subprocess-based CLI tests not tracking coverage)
- Key test categories added:
1. **_calculate_monthly_payment helper tests** - Direct tests for the shared helper function
2. **Edge cases with very large numbers** - $1B simple interest, $10M mortgage, $5M balloon
3. **Maximum term tests** - 40-year mortgage, 84-month personal loan, 7-year car loan, 10-year balloon
4. **Output formatting tests** - Verifies thousands separator (`:,.2f`) in all outputs
5. **Direct CLI parsing tests** - Tests create_parser() and main() directly (not via subprocess)
6. **CLI argument error tests** - Tests missing required arguments for all subcommands
- Key learnings:
- Subprocess tests don't contribute to coverage (run in separate process)
- Direct function imports with mocked sys.argv give proper coverage
- Formula verification: always compute expected values programmatically to avoid typos
- mypy passes with no issues
- All acceptance criteria verified:
- Test file exists at test/test_finance_calc.py
- Tests cover all calculator functions (US-002 through US-006)
- Edge cases: zero values, very large numbers, maximum terms
- CLI parsing and error handling tested
- Output formatting verified (decimal places, thousands separators)
- Coverage >= 90% (achieved 97%)
- Typecheck passes
---
### US-008: Add input validation and error handling [x]
- TDD approach: wrote 36 validation tests first (RED), then implemented validation (GREEN)
- Created pure helper: `_validate_common_inputs(principal, rate, term) -> str | None`
- Validation at CLI boundary (in main()) keeps pure functions unchanged
- Error messages follow required format: "Error: {specific message}"
- Exit code 1 on validation failure per acceptance criteria
- Key validation rules:
- principal > 0 for all calculators
- rate >= 0 for all calculators (0% is valid)
- term > 0 for all calculators
- down_payment >= 0 and <= price for car loans
- balloon_percent between 0-100 (inclusive) for balloon loans
- Test coverage:
- 32 subprocess-based CLI validation tests (TestInputValidation)
- 4 direct main() validation tests (TestInputValidationDirect)
- Edge cases: negative values, zero values, boundary values (0%, 100% balloon)
- All 132 tests pass (96 existing + 36 new)
- mypy passes with no issues
---
## Review PASSED - US-REVIEW-FINAL - Project Complete
### Final Review Criteria Verified:
1. **All tests pass** - PASSED
- 132 tests pass with `PYTHONPATH=. pytest test/test_finance_calc.py -v`
- Note: PYTHONPATH required due to module import in tests
2. **Typecheck clean** - PASSED
- `mypy finance_calc.py`: "Success: no issues found in 1 source file"
3. **Manual test each calculator** - PASSED
- simple-interest: $150.00 (1000 @ 5% for 3 years)
- mortgage: $1,013.37 (200000 @ 4.5% for 30 years)
- personal-loan: $235.37 (5000 @ 12% for 24 months)
- car-loan: Financed $20,000.00, Monthly $386.66 (25000, 5000 down @ 6% for 5 years)
- balloon: Monthly $693.04, Balloon $15,000.00 (50000 @ 7% for 5 years, 30%)
4. **Code quality meets portfolio standards** - PASSED
- Clean, professional code structure
- Type hints on all functions
- Comprehensive docstrings with formulas
5. **Linus criteria** - PASSED
- Good taste: Code is simple and elegant, uses shared helper for DRY
- No special cases: Zero rate handled elegantly in one place
- Data structures: Appropriate types (float for simple calcs, dict for complex)
- Pure functions: All calculations are pure (no side effects)
6. **No TODO comments** - PASSED
- Grep for TODO/FIXME/HACK/XXX found no matches
7. **Docstrings explain usage** - PASSED
- All functions have comprehensive docstrings with formulas
- CLI help text explains each subcommand and argument
- argparse description text is clear and professional
### Project Summary:
- 347 lines of production code (finance_calc.py)
- 1155 lines of test code (test/test_finance_calc.py)
- 132 total tests with 97% coverage on calculation functions
- 5 financial calculators (simple-interest, mortgage, personal-loan, car-loan, balloon)
- Clean functional programming approach with pure functions
- Professional CLI design suitable for portfolio
**PROJECT COMPLETE**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment