Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
14a6624
Derive every v2 country list from one list in code
anth-volk Oct 21, 2026
2729c37
Record each served country in a countries table that country columns …
anth-volk Oct 21, 2026
0d30696
Serve the countries table at GET /v2/countries
anth-volk Oct 22, 2026
f9a19c1
Refuse to start when the countries table and the country list differ
anth-volk Oct 22, 2026
6b8cbd2
Stop startup only when the countries table answers with a disagreement
anth-volk Oct 22, 2026
975c80f
Keep the startup check's log tests independent of earlier Alembic tests
anth-volk Oct 22, 2026
326ada0
Call the Simulation Entrypoint's economy report routes through its ge…
anth-volk Oct 27, 2026
c6bbec9
Submit economy reports at POST /v2/reports, refusing household-only c…
anth-volk Oct 27, 2026
c289269
Read economy reports by ID and by v1 number, starting a report's firs…
anth-volk Oct 27, 2026
1335b3e
Re-run an economy report and list its runs with their versions
anth-volk Oct 27, 2026
4bafe84
List chosen economy reports by ID, without their results
anth-volk Oct 27, 2026
0acb683
Save v2 report IDs in saved reports after checking them with the simu…
anth-volk Oct 27, 2026
5009a58
Save v2 simulation IDs in saved simulations after checking them with …
anth-volk Oct 27, 2026
fcc9cf8
Check the household-only refusal against migrated Postgres as the run…
anth-volk Oct 27, 2026
9ddbc46
Answer 503 when the gateway token for the simulation service cannot b…
anth-volk Oct 27, 2026
078150d
Share the simulation service's contract outside the economy report pa…
anth-volk Oct 27, 2026
be4230a
Refuse a household-only country's economy report even when the simula…
anth-volk Oct 27, 2026
d7ed56e
Share the base URL, request identifier and gateway auth setup between…
anth-volk Oct 27, 2026
f23b755
Require a saved report's v2 and v1 references to name the same report
anth-volk Oct 27, 2026
94ad81e
Read the simulation service's declared error answers through its gene…
anth-volk Oct 27, 2026
1515f74
Check that simultaneous first reads of a report start one run
anth-volk Oct 27, 2026
8790650
Check a report's country without reading its result, so an unreadable…
anth-volk Oct 28, 2026
0862080
Require a saved simulation's v2 and v1 references to name the same si…
anth-volk Oct 28, 2026
c19a91d
Lock a saved report or simulation while it is repointed
anth-volk Oct 28, 2026
57e5240
Declare each report route's own error answers and log where failures …
anth-volk Oct 28, 2026
e9cfc7e
Say which report routes refuse a report that cannot run, and log thei…
anth-volk Oct 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/v2-integration-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ jobs:
run: uv run coverage run --branch -m pytest -q tests/integration/test_v2_catalog_installed.py
env:
RUN_V2_CATALOG_COMPATIBILITY: "1"
- name: Test v2 metadata publication and resource routes
run: uv run coverage run -a --branch -m pytest -q tests/integration/test_v2_catalog_publication.py tests/integration/test_v2_metadata_routes.py
- name: Test v2 metadata publication, resource routes, the countries list and economy reports
run: uv run coverage run -a --branch -m pytest -q tests/integration/test_v2_catalog_publication.py tests/integration/test_v2_metadata_routes.py tests/integration/test_v2_country_routes.py tests/integration/test_v2_economy_reports.py
- name: Test v2 policy persistence and immediate v1 mirroring
run: uv run coverage run -a --branch -m pytest -q tests/integration/test_v2_policy_persistence.py tests/integration/test_v1_policy_dual_write.py tests/integration/test_mysql_policy_dual_write.py tests/integration/test_v2_user_policy_mirroring.py tests/integration/test_v1_user_policy_dual_write.py
- name: Test signed-in identity provisioning and user associations
run: uv run coverage run -a --branch -m pytest -q tests/integration/test_v2_user_identity_persistence.py
run: uv run coverage run -a --branch -m pytest -q tests/integration/test_v2_user_identity_persistence.py tests/integration/test_v2_saved_reference_updates.py
- name: Test comparison-run persistence and privilege reconciliation as the managed roles
run: uv run coverage run -a --branch -m pytest -q tests/integration/test_v2_stage12_comparison_persistence.py tests/integration/test_v2_privilege_reconciliation.py
- name: Test v2 household persistence and immediate v1 copying
Expand Down
1 change: 1 addition & 0 deletions changelog.d/v2-countries-route.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `GET /v2/countries`, which lists every row of the countries table: each served country's ID and whether it supports household and society-wide calculations.
1 change: 1 addition & 0 deletions changelog.d/v2-countries-startup-check.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The Public API refuses to start when the v2 `countries` table and its country list in code name different countries, or when the table or the privilege to read it is missing. The check runs on Cloud Run and wherever a v2 runtime database is configured; when it cannot query the table, it logs why and startup continues.
1 change: 1 addition & 0 deletions changelog.d/v2-countries-table.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added a v2 `countries` table that records, for each country the Public API serves, whether it supports household and society-wide calculations. Its revision lists the US and the UK, both with both. Users' primary country and the country of every v2 household, policy and user association now reference this table instead of a fixed US-and-UK check.
1 change: 1 addition & 0 deletions changelog.d/v2-economy-reports.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `/v2/reports` for economy reports, which the simulation service calculates and stores. `POST /v2/reports` creates a report and its first run on the country's current catalog version. `GET /v2/reports/{report_id}` and `GET /v2/reports/by-legacy-number/{number}` read one, `GET /v2/reports?report_id=...` lists up to 25 chosen reports without their results, and reading a report that has never run, such as one migrated from v1, starts its first run. `POST /v2/reports/{report_id}/runs` runs a report again on the current catalog version, reusing existing simulation runs unless `force` is set, and `GET /v2/reports/{report_id}/runs` lists its runs with their versions. A country whose `countries` row does not allow society-wide calculations gets a 422 naming it when it submits or re-runs a report, before anything is sent to the simulation service. A report whose stored result the simulation service cannot read back is a 502. Nothing calls these routes yet.
1 change: 1 addition & 0 deletions changelog.d/v2-saved-records-v2-ids.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`/v2/user-reports` accepts a v2 `report_id` and `/v2/user-simulations` a v2 `simulation_id`, alone or with the v1 ID, when a record is saved or repointed; the simulation service is asked first whether the report or simulation exists in the country, and an unknown one is a 422. A saved report or simulation holding both IDs must name the v2 record migrated from that v1 record, or the request is a 400. A shared report (`/v2/shared-reports/{id}`) now shows its `report_id`.
41 changes: 32 additions & 9 deletions docs/engineering/migration-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Generated from `policyengine_api/migration_registry.py` and `tests/contract/regi
| Metric | Count |
| --- | ---: |
| route group count | 9 |
| workflow count | 16 |
| request count | 89 |
| workflow count | 18 |
| request count | 96 |
| db entity count | 6 |
| sim flow count | 3 |

Expand All @@ -18,12 +18,12 @@ Generated from `policyengine_api/migration_registry.py` and `tests/contract/regi
| --- | --- | --- | --- |
| `health` | `health`, `simulation-gateway-check`, `liveness-check`, `readiness-check` | `none` | `none` |
| `specification` | `specification` | `none` | `none` |
| `metadata` | `metadata`, `datasets`, `economy-options`, `parameter-values`, `parameters`, `regions`, `tax-benefit-model-versions`, `tax-benefit-models`, `variables` | `metadata` | `none` |
| `metadata` | `metadata`, `countries`, `datasets`, `economy-options`, `parameter-values`, `parameters`, `regions`, `tax-benefit-model-versions`, `tax-benefit-models`, `variables` | `metadata` | `none` |
| `policy` | `policy`, `policies`, `user-policy`, `user-policies` | `policy` | `none` |
| `household` | `household`, `households`, `user-household`, `user-households`, `calculate`, `calculate-full` | `household` | `household` |
| `economy` | `economy` | `simulation` | `economy` |
| `simulation` | `simulation`, `simulations`, `user-simulations` | `simulation` | `economy` |
| `report` | `report`, `user-reports`, `shared-reports` | `report` | `report` |
| `report` | `report`, `reports`, `user-reports`, `shared-reports` | `report` | `report` |
| `user_profile` | `user-profile`, `me` | `user` | `none` |

## App V2 Workflow Contracts
Expand Down Expand Up @@ -128,12 +128,12 @@ Generated from `policyengine_api/migration_registry.py` and `tests/contract/regi
| `POST` | `/v2/user-simulations?country_id=us` | 201 | `simulation` | `Authorization` | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.user_id`, `result.item.simulation_id`, `result.item.legacy_simulation_id`, `result.item.name`, `result.item.description`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/user-simulations/{association_id}?country_id=us` | 200 | `simulation` | `Authorization` | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.user_id`, `result.item.simulation_id`, `result.item.legacy_simulation_id`, `result.item.name`, `result.item.description`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/user-simulations?country_id=us` | 200 | `simulation` | `Authorization` | `status`, `message`, `result.items`, `result.offset`, `result.limit`, `result.has_more` |
| `PATCH` | `/v2/user-simulations/{association_id}?country_id=us` | 200 | `simulation` | `Authorization` | `status`, `message`, `result.item.id`, `result.item.legacy_simulation_id`, `result.item.name`, `result.item.description`, `result.item.updated_at` |
| `PATCH` | `/v2/user-simulations/{association_id}?country_id=us` | 200 | `simulation` | `Authorization` | `status`, `message`, `result.item.id`, `result.item.simulation_id`, `result.item.legacy_simulation_id`, `result.item.name`, `result.item.description`, `result.item.updated_at` |
| `DELETE` | `/v2/user-simulations/{association_id}?country_id=us` | 204 | `simulation` | `Authorization` | |
| `POST` | `/v2/user-simulations?country_id=us` | 201 | `simulation` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.user_id`, `result.item.simulation_id`, `result.item.legacy_simulation_id`, `result.item.name`, `result.item.description`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/user-simulations/{association_id}?country_id=us` | 200 | `simulation` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.user_id`, `result.item.simulation_id`, `result.item.legacy_simulation_id`, `result.item.name`, `result.item.description`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/user-simulations?country_id=us` | 200 | `simulation` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.items`, `result.offset`, `result.limit`, `result.has_more` |
| `PATCH` | `/v2/user-simulations/{association_id}?country_id=us` | 200 | `simulation` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.item.id`, `result.item.legacy_simulation_id`, `result.item.name`, `result.item.description`, `result.item.updated_at` |
| `PATCH` | `/v2/user-simulations/{association_id}?country_id=us` | 200 | `simulation` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.item.id`, `result.item.simulation_id`, `result.item.legacy_simulation_id`, `result.item.name`, `result.item.description`, `result.item.updated_at` |
| `DELETE` | `/v2/user-simulations/{association_id}?country_id=us` | 204 | `simulation` | `PolicyEngine-Unauthenticated-User` | |

### `user_report_associations_v2`
Expand All @@ -146,14 +146,14 @@ Generated from `policyengine_api/migration_registry.py` and `tests/contract/regi
| `POST` | `/v2/user-reports?country_id=us` | 201 | `report` | `Authorization` | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.user_id`, `result.item.report_id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.description`, `result.item.user_policy_id`, `result.item.baseline_user_policy_id`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.last_run_at`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/user-reports/{association_id}?country_id=us` | 200 | `report` | `Authorization` | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.user_id`, `result.item.report_id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.description`, `result.item.user_policy_id`, `result.item.baseline_user_policy_id`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.last_run_at`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/user-reports?country_id=us` | 200 | `report` | `Authorization` | `status`, `message`, `result.items`, `result.offset`, `result.limit`, `result.has_more` |
| `PATCH` | `/v2/user-reports/{association_id}?country_id=us` | 200 | `report` | `Authorization` | `status`, `message`, `result.item.id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.description`, `result.item.user_policy_id`, `result.item.baseline_user_policy_id`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.last_run_at`, `result.item.updated_at` |
| `PATCH` | `/v2/user-reports/{association_id}?country_id=us` | 200 | `report` | `Authorization` | `status`, `message`, `result.item.id`, `result.item.report_id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.description`, `result.item.user_policy_id`, `result.item.baseline_user_policy_id`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.last_run_at`, `result.item.updated_at` |
| `DELETE` | `/v2/user-reports/{association_id}?country_id=us` | 204 | `report` | `Authorization` | |
| `POST` | `/v2/user-reports?country_id=us` | 201 | `report` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.user_id`, `result.item.report_id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.description`, `result.item.user_policy_id`, `result.item.baseline_user_policy_id`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.last_run_at`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/user-reports/{association_id}?country_id=us` | 200 | `report` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.user_id`, `result.item.report_id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.description`, `result.item.user_policy_id`, `result.item.baseline_user_policy_id`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.last_run_at`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/user-reports?country_id=us` | 200 | `report` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.items`, `result.offset`, `result.limit`, `result.has_more` |
| `PATCH` | `/v2/user-reports/{association_id}?country_id=us` | 200 | `report` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.item.id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.description`, `result.item.user_policy_id`, `result.item.baseline_user_policy_id`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.last_run_at`, `result.item.updated_at` |
| `PATCH` | `/v2/user-reports/{association_id}?country_id=us` | 200 | `report` | `PolicyEngine-Unauthenticated-User` | `status`, `message`, `result.item.id`, `result.item.report_id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.description`, `result.item.user_policy_id`, `result.item.baseline_user_policy_id`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.last_run_at`, `result.item.updated_at` |
| `DELETE` | `/v2/user-reports/{association_id}?country_id=us` | 204 | `report` | `PolicyEngine-Unauthenticated-User` | |
| `GET` | `/v2/shared-reports/{association_id}?country_id=us` | 200 | `report` | none | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.baseline_reform`, `result.item.last_run_at`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/shared-reports/{association_id}?country_id=us` | 200 | `report` | none | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.report_id`, `result.item.legacy_report_id`, `result.item.name`, `result.item.source_note`, `result.item.provisions`, `result.item.validation`, `result.item.budget_window_years`, `result.item.baseline_reform`, `result.item.last_run_at`, `result.item.created_at`, `result.item.updated_at` |

### `user_account_v2`

Expand Down Expand Up @@ -190,6 +190,29 @@ Generated from `policyengine_api/migration_registry.py` and `tests/contract/regi
| `GET` | `/us/metadata` | 200 | `metadata` | none | `status`, `result.current_law_id`, `result.economy_options.region`, `result.economy_options.time_period` |
| `GET` | `/uk/metadata` | 200 | `metadata` | none | `status`, `result.current_law_id`, `result.economy_options.region`, `result.economy_options.time_period` |

### `country_selection_v2`

- Current contract: `typed_v2_resources`
- Future owner: Countries table and one country list in code

| Method | Path | Status | Route group | Required headers | Stable response fields |
| --- | --- | ---: | --- | --- | --- |
| `GET` | `/v2/countries` | 200 | `metadata` | none | `status`, `message`, `result.items` |

### `economy_reports_v2`

- Current contract: `typed_v2_resources`
- Future owner: Economy reports on v2

| Method | Path | Status | Route group | Required headers | Stable response fields |
| --- | --- | ---: | --- | --- | --- |
| `POST` | `/v2/reports?country_id=us` | 201 | `report` | none | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.kind`, `result.item.year`, `result.item.baseline_simulation`, `result.item.reform_simulation`, `result.item.status`, `result.item.run`, `result.item.result` |
| `GET` | `/v2/reports?country_id=us&report_id={report_id}` | 200 | `report` | none | `status`, `message`, `result.items` |
| `GET` | `/v2/reports/{report_id}?country_id=us` | 200 | `report` | none | `status`, `message`, `result.item.id`, `result.item.legacy_report_id`, `result.item.status`, `result.item.run`, `result.item.result` |
| `GET` | `/v2/reports/by-legacy-number/{legacy_report_id}?country_id=us` | 200 | `report` | none | `status`, `message`, `result.item.id`, `result.item.legacy_report_id`, `result.item.status` |
| `POST` | `/v2/reports/{report_id}/runs?country_id=us` | 201 | `report` | none | `status`, `message`, `result.item.id`, `result.item.trigger`, `result.item.tax_benefit_model_version_id`, `result.item.policyengine_version`, `result.item.status` |
| `GET` | `/v2/reports/{report_id}/runs?country_id=us` | 200 | `report` | none | `status`, `message`, `result.items` |

### `metadata_resources_v2_preview`

- Current contract: `typed_v2_resources`
Expand Down
30 changes: 30 additions & 0 deletions docs/engineering/skills/alembic-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,3 +294,33 @@ last dependent table is dropped. Its only post-generation correction drops the
nine generated `v2_*` enum types at the end of the baseline downgrade. The v2
Postgres lifecycle test covers empty upgrade, downgrade to base, and re-upgrade
so stale enum types cannot make the generated baseline non-reversible.

### API v2 countries rows

Revision `fb1a3e40d3d6` was autogenerated after adding the `countries` table
and replacing the US-and-UK checks on `users.primary_country` and on the
`country_id` of households, policies and the four user association tables
with foreign keys to it. It contains one hand-written operation: it inserts
the `uk` and `us` rows, both household and society-wide capable, between the
generated table creation and the generated foreign keys, which otherwise fail
on any database whose rows already name either country. The rows are exactly
the values the replaced checks allowed. The generated downgrade removes them
with the table, and its restored checks fail if any record names another
country.

Inserting rows is a data operation, which this skill otherwise forbids. The
maintainers approved this insert as a one-off exception, because the new
foreign keys need the rows in the same revision. Unit tests pin the
operation, and the v2 Postgres lifecycle test covers an upgrade that keeps
existing rows, the downgrade, a downgrade refused by another country's
records, and the re-upgrade.

The table must list exactly the countries in
`policyengine_api/supported_countries.py`: the Public API refuses to start
when they differ (`policyengine_api/startup_country_check.py`). Listing
another country later needs a revision that inserts its row, and the same
human migration decision. The deploy runs that revision before it starts
the new release, so instances of the previous release that start afterwards,
and a rollback to it, refuse to start until the revision is downgraded. The
module docstring of `supported_countries.py` lists the other checks a new
country must widen.
Loading
Loading