Skip to content

Serve household reports through /v2/reports - #4293

Draft
anth-volk wants to merge 28 commits into
masterfrom
v2plan/c1-16-household-reports
Draft

anth-volk wants to merge 28 commits into
masterfrom
v2plan/c1-16-household-reports

Conversation

@anth-volk

@anth-volk anth-volk commented Nov 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #4291

Summary

/v2/reports serves household reports: reports over two household simulations of one stored household. There is no separate household-report route. This replaces #4132, which was built on the simulation service's /v2/household-reports routes that the redesign removed.

Create. POST /v2/reports takes a body told apart by kind. Economy bodies are unchanged. A household body is:

{"country_id": "uk", "kind": "household", "label": "…", "description": null,
 "household_id": "<uuid>",
 "baseline": {"policy_id": null}, "reform": {"policy_id": "<uuid>"}}
  • Both simulations calculate the household's default_year, as /v2/household-calculations and the simulation service's household reports do, so the body names no year.
  • reform null or omitted makes a report of the baseline alone. Each empty object is current law.
  • Each simulation always calculates every yearly, monthly and eternal variable the household lacks, as /calculate-full does, because the household report page needs every value. There is no add_missing_variables option, unlike Serve household reports at /v2/reports #4132 and /v2/household-calculations: a body naming it is refused with 422, and the simulation service refuses it too. reports.inputs is {} for a household report, so reads answer options: {}.
  • The body names no region, dataset, dynamic, cliff option, SPM settings or PolicyEngine.py version.

Before asking the simulation service, the Public API checks, in this order, with /v2/household-calculations' checks and errors:

  1. The country's countries row has household_enabled. Otherwise it answers 422. A household-only country passes, and still gets 422 for economy reports.
  2. The household is stored in the country. Otherwise it answers 404.
  3. The household has a default year. Otherwise it answers 422.
  4. Each named policy is stored in the country (404). None of them may set a parameter the running catalog version lacks (422 missing_parameters, the baseline's policy first), as economy reports are checked (Refuse /v2/reports and /v2/simulations submissions whose policies set missing parameters #4286).

A database or catalog that cannot be read during these checks answers 503. Then SimulationSubmissionService.submit_household_report sends the simulation service's POST /reports:

{"country": "uk", "report_type": "household", "label": "…", "description": null,
 "household_id": "<uuid>",
 "baseline": {"policy_id": null}, "reform": {"policy_id": "<uuid>"}}

Its answers are read exactly as an economy report's are: the same 201/202, 422, 400, 500, 503 and lost-answer handling, and the same 502 with report_id when the created report cannot be read back. The simulation service's household 400 codes (unknown_household, unknown_policy, unknown_catalog_version, no_household_function, missing_reference) describe its state or rows the Public API already checked, so they answer 503. Its 422s (missing_parameters, household_not_calculable) answer 422 with their reasons.

Read and list. A report whose row has household_id set is a household report.

  • GET /v2/reports/{report_id} answers its definition, with kind household and household_id, and its newest run, whatever its status and versions. Once that run has succeeded, result holds each simulation run's calculated household, read from simulation_runs.household_result as the household function's version 1 result document (the document household_jobs.result holds). Each side has the same shape as a /v2/household-calculations result: household, warnings, spm_config and spm_provenance. result.reform is null for a report of the baseline alone.
  • A succeeded run whose simulation run is missing, or whose document is not a version 1 result document with a calculated household, answers 500 ("The report's stored result cannot be read").
  • Once the newest run has failed, failure gives each side's calculation error from its failure document: code, message, input_errors and spm_error, as a /v2/household-calculations error. A side is null when it succeeded or has no readable failure document, as for a run Modal refused. The run's error_message still says why it failed.
  • GET /v2/reports?report_id=… lists household reports beside economy reports, in the order named and without results or failures.

User-visible contract changes

  • POST /v2/reports accepts kind household. Its schema is a union told apart by kind, and its 404 and 422 descriptions cover the household checks.
  • GET /v2/reports/{report_id} and GET /v2/reports answer household reports instead of 422 (or leaving them out of lists). Their item schemas are unions told apart by kind, now of all three kinds. No kind is refused any longer, so the read's 422 for a kind it could not read yet is gone.
  • The migration contracts record result.item.kind on the report read. The regenerated documents are included.
  • The other report and simulation routes are unchanged. Saved reports (/v2/user-reports) already accept any report's report_id.

Merge order

It was built on #4286 and #4281, which have both merged; master is merged in (no rebase), and the base is master.

Environment variables and secrets

No new variables or secrets. The new code reads:

The workflow change adds tests/integration/test_v2_household_reports.py to an existing step of v2-integration-check.yml. Its database URL and settings are the job's literal disposable values in env: (V2_MIGRATION_DATABASE_URL, V2_ALEMBIC_DISPOSABLE_TEST, DEPLOYMENT_ENVIRONMENT). No secret is involved.

Not verified:

  • that each environment's SIMULATION_ENTRYPOINT_URL serves a revision with household reports in /reports (C1 · 15);
  • that the Auth0 client the gateway token comes from may call that route.

Inline suppressions

None added.

Testing

Each change was built test-first, and each failing test was committed before its fix.

  • Unit:
    • the household submission checks and their order, against stand-ins, including a household with no default year;
    • the simulation service request body and a sample of its answers, through an in-memory transport;
    • the household report reads, failures, lists and creates, against SQLite's copy of the v2 tables;
    • the create, read and list routes over a stand-in service, and the OpenAPI schema.
  • Integration, on migrated PostgreSQL 17 as the runtime role:
    • a household-only UK creates, reads and lists household reports, including one of the baseline alone;
    • a household the country does not store, or one with no default year, is refused before anything is sent;
    • economy reports are still refused there.
  • Changed existing tests:
    • every SimulationSubmissionService construction passes households;
    • test_v2_report_reads's unreadable-kind test now uses a budget window;
    • test_alembic_workflows checks that CI runs the new integration file.
  • Local runs: only the tests this PR adds or changes, by node ID. alembic -c alembic-v2.ini check on a fresh PostgreSQL 17 database reports no operations. lint-imports, python -m scripts.rules validate, python scripts/run_quality_guards.py, ruff, ruff format and mypy pass. Nothing under policyengine_api/data/v2/ changed.

Review

Four read-only review rounds, none of which found a bug, then a fifth on the changes after merging #4290 and dropping add_missing_variables. Every quality finding was fixed, each code change test-first:

  1. The household checks now reuse /v2/household-calculations' country check, error and household source. Region and dataset moved off the shared read to the economy read. The report items share one base model. The changelog's "two simulations" is corrected. Tests cover the create dispatch.
  2. Each side's failure has a schema of its own. The household 400 codes and a failed or unreadable report on PostgreSQL are now tested. Every report kind has a name, and the stored documents are typed as JSON.
  3. The service type for a side's failure now always names its code. Each side's simulation run has its own status in the PostgreSQL tests.
  4. A comment naming the code that reads the stored documents is corrected, and the SQLite tests store each side's recorded status.
  5. A stale test parameter ID, left when the unreadable-kind case was removed, is gone (it stopped every test shard from collecting); an unused test argument is dropped.

🤖 Generated with Claude Code

anth-volk and others added 20 commits November 13, 2026 00:18
… (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ports

The submission service checks that the country's row allows household
calculations, that the household and each policy are stored in the
country, and that the policies' parameters all exist in the running
catalog, then sends report_type household with the household, year,
policies and add_missing_variables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A household report is told from an economy report by its household_id.
Its result, once its newest run has succeeded, is each simulation run's
calculated household from simulation_runs.household_result, read as the
household function's version 1 result document; a document that cannot
be read answers 500. Lists name household reports beside economy reports.
The create body is a union told apart by kind, and the household checks'
refusals answer 422 (no household calculations) and 404 (household not
stored). Budget windows are still not read here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d that CI runs them (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ling)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The request names no year, as the simulation service's household
reports and household calculations take none; a household with no
default year is refused with 422 before the policies are read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed run's simulation runs may hold the household function's failure
document; the read gives each side's code, message, input errors and SPM
error, as a household calculation's read does, and none for a side
without a readable one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on household reads, the create dispatch (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Household reports use the household calculations' country check, error
and household source; a household read has no region or dataset, which
move to the economy read; the report items share one base of the fields
every kind has, and the reads one helper for the newest run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 400 codes, failures on PostgreSQL (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each side's failure has its own schema, whose code is always named and
whose text is about reports; the stored documents are typed as JSON;
every report kind has a name for the unsupported-kind message; and the
create route's 422 text reads as one sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd named, each side's run status (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each side's failure is a report type of its own whose code is always
named, read straight from the failure document; the integration tests
give each side's simulation run its own status, and a test checks that
every report kind has a name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e each side's recorded status

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.50000% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.31%. Comparing base (2eae02a) to head (bd3dcf8).
⚠️ Report is 33 commits behind head on master.

Files with missing lines Patch % Lines
..._api/services/v2/stored_reports/transformations.py 76.47% 2 Missing and 2 partials ⚠️
.../v2/stored_reports/household_results/validators.py 83.33% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4293      +/-   ##
==========================================
+ Coverage   93.22%   93.31%   +0.08%     
==========================================
  Files         357      364       +7     
  Lines       20526    21040     +514     
  Branches     3338     3406      +68     
==========================================
+ Hits        19136    19634     +498     
- Misses        818      828      +10     
- Partials      572      578       +6     
Flag Coverage Δ
integration 56.77% <91.00%> (+0.91%) ⬆️
unit 90.91% <94.50%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…bases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every report kind is now read, listed and created by /v2/reports: the
read types, item models, request union and submission client hold all
three, budget windows declare their own region and dataset, the window
tests build the submission service with a household source, and the
tests of a kind that cannot be read yet are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every kind of report is now read, listed and created here, so the 422
for an unreadable kind, its error and the kind filter on lists are gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk and others added 4 commits November 13, 2026 03:16
… (failing)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user decided household reports always add every variable the
household lacks, since their caller needs every value; the simulation
service now refuses the field. The request body and the body sent to
its /reports no longer have it, and reads find nothing about it in
options. Household calculations keep their option.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Serve household reports through /v2/reports

1 participant