Cursor Task — packages/sd-api/ FastAPI Backend
Section titled “Cursor Task — packages/sd-api/ FastAPI Backend”Date: 2026-03-22
Assigned to: Cursor (claude-sonnet-4-6 recommended)
Working directory: \\wsl$\Ubuntu-24.04\home\ta\projects\monorepo\packages\sd-api\
Context
Section titled “Context”This is the $MART DEBT App (SD App) — a SvelteKit PWA for leveraged investing analysis.
Phase 1 is complete: a static snapshot renderer reading pre-calculated JSON. Phase 2 task: build the live Python API backend so the SvelteKit UI can call it with user-entered inputs and get live results.
The math engine (packages/sd-math/) is already fully implemented and tested:
interest-onlystrategy: 37 tests passing against LevPro golden fixturesterm-loanstrategy: 34 tests passing against LevPro golden fixtures
Your job: build the FastAPI wrapper around sd-math. No new math. Pure transport layer.
Hard Rules
Section titled “Hard Rules”- No math in the API. All calculations are delegated to
sd-math. Callsd_math.calculate()orsd_math.strategies.term_loan.calculate(). Never implement calculation logic in the API package. - Field names match sd-math exactly. Do not rename fields. The API Pydantic models mirror the sd-math Python dataclass models. If sd-math has
marginal_rate, the API hasmarginal_rate. - TDD. Write failing tests first. Tests must use httpx
TestClient. Tests pass before you signal complete. - Do not modify
packages/sd-math/. It is a locked artifact. Read it; don’t change it. - Run locally only. No deployment work. Just
uvicorn main:app --reloadrunning and tests passing.
Ordered Reading List
Section titled “Ordered Reading List”Read these in order before writing a single line of code:
- This file (you’re reading it)
D:\FSS\KB\Business\_WorkingOn\Projects\SD-App\api-design.md— API structure, request/response schema, predefined profiles, endpoint definitionspackages/sd-math/src/sd_math/models/inputs.py—LoanProfile,TermLoanProfile,TaxProfile,ReturnProfile,RegionProfile,UserProfilepackages/sd-math/src/sd_math/models/outputs.py—AnnualRow,TermLoanAnnualRow,SummaryScenario,ProjectionSummary,ProjectionResultpackages/sd-math/src/sd_math/__init__.py—calculate()function signaturepackages/sd-math/src/sd_math/strategies/term_loan.py—calculate()signature (takesTermLoanProfile, notLoanProfile)packages/sd-math/tests/fixtures/Interest-only/interest-only-ca.json— golden fixture values for test assertionspackages/sd-math/tests/fixtures/Term-loan/term-loan-ca.json— term loan golden fixture valuespackages/sd-math/tests/conftest.py— predefined test profiles (TAX_35, TAX_50, COMMON_LOAN, COMMON_TERM_LOAN, ON_REGION)
Deliverable
Section titled “Deliverable”A working FastAPI package at packages/sd-api/ with:
packages/sd-api/├── pyproject.toml├── main.py # FastAPI app, CORS, router registration├── profiles.py # Predefined profile registry (middle-income, high-income)├── routers/│ └── projections.py # POST endpoints for interest-only and term-loan├── schemas/│ ├── inputs.py # Pydantic request models│ └── outputs.py # Pydantic response models└── tests/ └── test_api.py # Integration tests via httpx TestClientpyproject.toml
Section titled “pyproject.toml”[build-system]requires = ["hatchling"]build-backend = "hatchling.build"
[project]name = "sd-api"version = "0.1.0"requires-python = ">=3.11"dependencies = [ "fastapi>=0.110", "uvicorn[standard]>=0.29", "sd-math",]
[project.optional-dependencies]dev = ["pytest>=8.0", "httpx>=0.27"]
[tool.pytest.ini_options]testpaths = ["tests"]sd-math is a workspace package — install it as an editable dependency:
cd packages/sd-apipython3 -m venv .venvsource .venv/bin/activatepip install -e ".[dev]"pip install -e "../sd-math"API Spec Summary
Section titled “API Spec Summary”See api-design.md for full details. Key points:
Two POST endpoints
Section titled “Two POST endpoints”POST /api/v1/ca/projections/interest-onlyPOST /api/v1/ca/projections/term-loanTwo request patterns
Section titled “Two request patterns”Pattern 1 — predefined profile:
{ "profile_id": "middle-income" }Pattern 2 — custom inputs:
{ "profile_id": "custom", "loan_profile": { "loan_amount": 150000, "interest_rate": 0.08, "holding_period": 10, "lev_to_non_lev_ratio": 0.0 }, "tax_profile": { "marginal_rate": 0.43, "prov_tax_rate": 0.1815, "fed_tax_rate": 0.2485, "cg_inclusion_rate": 0.50, "div_tax_rate": 0.218, "region_profile": { "region": "ON", "country": "ca", "prov_deductibility_limited": false, "fed_deductibility_limited": false, "prov_ratio_of_interest_deductible": 1.0, "fed_ratio_of_interest_deductible": 1.0, "prov_cap_gains_are_income": false, "fed_cap_gains_are_income": false, "prov_defer_unused_deductions": false, "fed_defer_unused_deductions": false } }, "return_profile": { "expected_return": 0.07, "dcg_pct": 0.70, "taxable_cg_pct": 0.25, "div_pct": 0.05, "interest_pct": 0.00, "distrib_reinvest_ratio": 1.0 }, "returns": [0.00, 0.03, 0.07, 0.10]}For term-loan custom requests, use term_loan_profile (with loan_payment, loan_interest_rate, n_loan_periods) instead of loan_profile.
Response — 4-section structure
Section titled “Response — 4-section structure”{ "meta_data": { "strategy": "interest-only", "country": "ca", "version": "1.0" }, "inputs": { "profile_id": "middle-income", "loan_profile": { "..." }, "tax_profile": { "..." }, "return_profile": { "..." }, "returns": [0.0, 0.03, 0.07, 0.1] }, "analysis_summary": { "better_than_return": 0.0666, "at_annual_investment": 5850, "bt_annual_interest": 9000, "scenarios": [ { "expected_return": 0.00, "no_lev_bt_balance": 58500, "net_lev_bt_bal": -14321, "net_increase": -72821, "pct_increase": -1.245, "is_better_than": false }, { "expected_return": 0.0666, "no_lev_bt_balance": 83638, "net_lev_bt_bal": 83638, "net_increase": 0, "pct_increase": 0.0, "is_better_than": true } ] }, "analysis_details": { "expected_return": 0.07, "annual_rows": [ { "year": 1, "no_lev_bt_balance": 6257, "no_lev_at_distrib": 306, "no_lev_acb": 6257, "lev_bt_balance": 9650, "lev_at_distrib": 472, "net_lev_bt_bal": 472, "net_lev_acb": 472, "net_increase": -5785, "pct_increase": -0.9246 } ] }}Term loan annual_rows add: loan_balance, interest_part_of_pmt, loan_reduction, at_cashflow.
The is_better_than flag on SummaryScenario is computed by the API: check if abs(scenario.expected_return - better_than_return) < 1e-8.
Default returns if not specified: [0.00, 0.03, 0.07, 0.10]. sd-math inserts the Better-Than scenario automatically.
Predefined Profiles (profiles.py)
Section titled “Predefined Profiles (profiles.py)”Build a profile registry. These exact values are used in the golden fixtures — assertions will verify against them.
# Ontario region — no QC deductibility limitsON_REGION = RegionProfile( region="ON", country="ca", prov_deductibility_limited=False, fed_deductibility_limited=False, prov_ratio_of_interest_deductible=1.0, fed_ratio_of_interest_deductible=1.0, prov_cap_gains_are_income=False, fed_cap_gains_are_income=False, prov_defer_unused_deductions=False, fed_defer_unused_deductions=False,)
COMMON_RETURN = ReturnProfile( expected_return=0.07, dcg_pct=0.70, taxable_cg_pct=0.25, div_pct=0.05, interest_pct=0.00, distrib_reinvest_ratio=1.0,)
INTEREST_ONLY_PROFILES = { "middle-income": UserProfile( id="middle-income", label="Middle Income (35% marginal rate)", loan_profile=LoanProfile(loan_amount=100_000.0, interest_rate=0.09, holding_period=10, lev_to_non_lev_ratio=0.0), tax_profile=TaxProfile(marginal_rate=0.35, prov_tax_rate=0.1315, fed_tax_rate=0.2185, cg_inclusion_rate=0.50, div_tax_rate=0.159, region_profile=ON_REGION), return_profile=COMMON_RETURN, ), "high-income": UserProfile( id="high-income", label="High Income (50% marginal rate)", loan_profile=LoanProfile(loan_amount=100_000.0, interest_rate=0.09, holding_period=10, lev_to_non_lev_ratio=0.0), tax_profile=TaxProfile(marginal_rate=0.50, prov_tax_rate=0.2315, fed_tax_rate=0.2685, cg_inclusion_rate=0.50, div_tax_rate=0.313, region_profile=ON_REGION), return_profile=COMMON_RETURN, ),}
TERM_LOAN_PROFILES = { "middle-income": { ... same tax + return, but TermLoanProfile(loan_amount=37543, loan_payment=5850, loan_interest_rate=0.09, n_loan_periods=10) }, "high-income": { ... },}Test Strategy
Section titled “Test Strategy”Write tests in tests/test_api.py using httpx TestClient. Tests must:
- Smoke test — hit both endpoints with
{"profile_id": "middle-income"}; verify HTTP 200 + 4-section response structure - Summary values — assert
better_than_return ≈ 0.0666(middle-income, interest-only,pytest.approx(abs=0.001)) - Year 10 net balance — assert
analysis_details.annual_rows[-1].net_lev_bt_bal ≈ 83638(middle-income, 7% return,pytest.approx(abs=2)) - Term loan year 10 — assert
analysis_details.annual_rows[-1].net_lev_bt_bal ≈ 29079(middle-income, 7% return,pytest.approx(abs=2)) - Custom inputs — hit
/interest-onlywith a customloan_profile(different loan_amount); verify response is structurally valid - Invalid profile —
{"profile_id": "nonexistent"}; verify HTTP 422 or 404
These assertion values come from the sd-math golden fixtures — you can read them directly from:
packages/sd-math/tests/fixtures/Interest-only/interest-only-ca.json(fixtureio-ca-35pct-7pct-annual, year 10)packages/sd-math/tests/fixtures/Term-loan/term-loan-ca.json(fixturetl-ca-35pct-7pct-annual, year 10)
Implementation Order
Section titled “Implementation Order”pyproject.toml+ venv setupschemas/inputs.py— Pydantic models mirroring sd-math inputsschemas/outputs.py— Pydantic models mirroring sd-math outputsprofiles.py— predefined profile registrymain.py— FastAPI app skeletonrouters/projections.py— endpoint stubs returning 501tests/test_api.py— failing tests (red)- Implement interest-only endpoint (green)
- Implement term-loan endpoint (green)
- All tests pass → signal complete
Out of Scope
Section titled “Out of Scope”- Deployment to any platform
- Authentication or rate limiting
- RRSP strategy (not yet in sd-math)
- US country support
- Any changes to
packages/sd-math/ - SvelteKit UI changes (separate Claude task)
Completion Signal
Section titled “Completion Signal”Signal complete when:
pytestshows all tests passing (minimum 6 tests)uvicorn main:appstarts without error- Both endpoints respond to
{"profile_id": "middle-income"}with correct values
Paste the pytest summary here when done.