Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ gha-creds-*.json
## vscode settings
/.vscode
*.db.init.lock

# Hypothesis example database
.hypothesis/
1 change: 1 addition & 0 deletions changelog.d/final-simulation-report-results.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`PATCH /{country_id}/simulation` and `PATCH /{country_id}/report` no longer change a complete result: an update that repeats it returns the record unchanged, and one that would change it answers 409. A simulation result from an older country model can still be replaced by the installed model's, and an update to an outdated report ID now completes its current equivalent, the record reads serve for that ID, instead of the outdated row. Both routes reject a non-integer `id` and a completion whose output is null.
1 change: 1 addition & 0 deletions changelog.d/remove-legacy-user-routes.removed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Removed the v1 user-profile routes (`/{country_id}/user-profile`) and saved-policy routes (`/{country_id}/user-policy`), which read and changed records by a caller-supplied user or record ID without a credential; signed-in users keep their saved work through the `/v2` routes, which take the owner from the verified credential.
10 changes: 7 additions & 3 deletions docs/canonical-spm.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,13 @@ the full calculation envelope inside `output`, including its
`spm_provenance`. A JSON-encoded output string is also accepted. This endpoint
validates the one compact SPM receipt strictly before storage. Completed output
that includes the former sibling `spm_config`, an old receipt or a rich receipt
is rejected rather than translated. Null update fields are ignored. The legacy `api_version` input
is ignored; writes record the installed country model version. Top-level `spm`,
even null, returns HTTP 400 `SPM_SETTINGS_UNSUPPORTED` on POST and PATCH.
is rejected rather than translated. Null update fields, and output that decodes
to null, are ignored, and `id` must be an integer. The legacy `api_version` input
is ignored; an applied write records the installed country model version. A
complete result from the installed country model, or a newer one, is final: an
update that repeats it writes nothing, and one that would change it returns HTTP
409. Top-level `spm`, even null, returns HTTP 400 `SPM_SETTINGS_UNSUPPORTED` on
POST and PATCH.

POST, PATCH and `GET /us/simulation/{id}` return the simulation record inside
`result`. Non-string JSON `output` and `simulation_spec_json` are returned as
Expand Down
15 changes: 2 additions & 13 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 | 15 |
| request count | 86 |
| db entity count | 6 |
| sim flow count | 3 |

Expand Down Expand Up @@ -50,17 +50,6 @@ Generated from `policyengine_api/migration_registry.py` and `tests/contract/regi
| `GET` | `/v2/policies/{policy_id}?country_id=us` | 200 | `policy` | none | `status`, `message`, `result.item.id`, `result.item.country_id`, `result.item.tax_benefit_model_id`, `result.item.parameter_values`, `result.item.created_at`, `result.item.updated_at` |
| `GET` | `/v2/policies?country_id=us` | 200 | `policy` | none | `status`, `message`, `result.items`, `result.offset`, `result.limit`, `result.has_more` |

### `saved_policy_v1_compatibility`

- Current contract: `api_v1_compatible`
- Future owner: PR 10: Policy Migration

| Method | Path | Status | Route group | Required headers | Stable response fields |
| --- | --- | ---: | --- | --- | --- |
| `POST` | `/us/user-policy` | 201 | `policy` | none | `status`, `message`, `result.id`, `result.reform_id`, `result.reform_label`, `result.baseline_id`, `result.user_id` |
| `GET` | `/us/user-policy/{user_id}` | 200 | `policy` | none | `status`, `message`, `result` |
| `PUT` | `/us/user-policy` | 200 | `policy` | none | `status`, `message`, `result.id` |

### `user_policy_associations_v2`

- Current contract: `typed_v2_resources`
Expand Down
4 changes: 1 addition & 3 deletions docs/engineering/skills/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ uv run pytest \
tests/unit/services/test_policy_mirroring.py \
tests/unit/services/test_user_policy_mirroring.py \
tests/unit/routes/test_policy_dual_write_routes.py \
tests/unit/routes/test_user_policy_dual_write_routes.py \
tests/unit/test_migration_flags.py \
tests/unit/test_readiness.py \
tests/contract -q
Expand Down Expand Up @@ -221,8 +220,7 @@ uv run pytest \
tests/contract/test_v1_route_contracts.py \
tests/unit/services/test_policy_service.py \
tests/unit/services/test_user_policy_service.py \
tests/unit/routes/test_policy_dual_write_routes.py \
tests/unit/routes/test_user_policy_dual_write_routes.py -q
tests/unit/routes/test_policy_dual_write_routes.py -q
```

Finally regenerate and validate migration contracts:
Expand Down
51 changes: 2 additions & 49 deletions docs/generated/migration_contracts.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"metadata": {
"db_entity_count": 6,
"request_count": 89,
"request_count": 86,
"route_group_count": 9,
"sim_flow_count": 3,
"workflow_count": 16
"workflow_count": 15
},
"route_groups": [
{
Expand Down Expand Up @@ -203,53 +203,6 @@
}
]
},
{
"current_contract": "api_v1_compatible",
"future_owner_pr": "PR 10: Policy Migration",
"name": "saved_policy_v1_compatibility",
"requests": [
{
"expected_status": 201,
"headers": [],
"method": "POST",
"path": "/us/user-policy",
"route_group": "policy",
"stable_response_fields": [
"status",
"message",
"result.id",
"result.reform_id",
"result.reform_label",
"result.baseline_id",
"result.user_id"
]
},
{
"expected_status": 200,
"headers": [],
"method": "GET",
"path": "/us/user-policy/{user_id}",
"route_group": "policy",
"stable_response_fields": [
"status",
"message",
"result"
]
},
{
"expected_status": 200,
"headers": [],
"method": "PUT",
"path": "/us/user-policy",
"route_group": "policy",
"stable_response_fields": [
"status",
"message",
"result.id"
]
}
]
},
{
"current_contract": "typed_v2_resources",
"future_owner_pr": "PR 12A: User Login and v1-Anchored User Associations",
Expand Down
13 changes: 13 additions & 0 deletions docs/migration/stage-10-v2-policies.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,16 @@ The normal staging promotion still restores the previously serving Cloud Run
revision when promotion or the stable-URL health check fails. It does not
create persistent policy fixtures or change the configured write selection as
part of each release.

## Removal of the v1 saved-policy routes

`POST /{country_id}/user-policy`, `GET /{country_id}/user-policy/{user_id}`
and `PUT /{country_id}/user-policy` are removed. They read and changed saved
policies by a caller-supplied `user_id` or record `id` with no credential, and
only legacy.policyengine.org called them; Cloud Run request logs for the 30
days to 2026-10-19 showed no saved-policy requests. No new v1 saved policies
are created, so none are copied to v2. The saved-policy copying and v1
saved-policy service modules stay until dual-write is deleted, and existing
Cloud SQL `user_policies` rows are kept. Signed-in users keep their saved
policies through `/v2/user-policies`, which takes the owner from the verified
credential.
6 changes: 0 additions & 6 deletions policyengine_api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ def log_timing(message: str) -> None:
from policyengine_api.routes.metadata_routes import metadata_bp

log_timing("Metadata routes import completed")
from policyengine_api.routes.user_profile_routes import user_profile_bp

log_timing("User profile routes import completed")
from policyengine_api.routes.reform_impact_routes import reform_impact_bp
from policyengine_api.routes.report_output_routes import report_output_bp
from policyengine_api.routes.simulation_routes import simulation_bp
Expand Down Expand Up @@ -129,9 +126,6 @@ def log_timing(message: str) -> None:
app.register_blueprint(economy_bp)
log_timing("Economy routes registered")

app.register_blueprint(user_profile_bp)
log_timing("User profile routes registered")

app.register_blueprint(reform_impact_bp)
log_timing("Simulations endpoint registered")

Expand Down
36 changes: 32 additions & 4 deletions policyengine_api/openapi_spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -616,13 +616,17 @@ paths:
description: >-
Update the record selected by body id within the path country. Supply at
least one non-null status, output or error_message; complete status requires
non-null output. Null update fields are ignored, and omitted fields retain
their values. The installed country model version and current run state
are updated with the record. Preserve the full household calculation
non-null output, and id must be an integer. Null update fields, and output
that decodes to null, are ignored; omitted fields retain their values. An
applied update records the installed country model version and the current
run state. Preserve the full household calculation
envelope, including spm_provenance, inside output. Its SPM selection is
derived from that receipt and belongs to the linked household; this
operation does not select a measurement. Top-level spm is
rejected, including null, with HTTP 400 SPM_SETTINGS_UNSUPPORTED.
A complete result from the installed country model, or a newer one, is
final: an update that repeats it returns the record unchanged, and one that
would change it is rejected with HTTP 409.
parameters:
- name: country_id
in: path
Expand All @@ -640,11 +644,14 @@ paths:
description: Updated simulation record.
content: *simulation_record_content
400:
description: Invalid update, no fields to update, or independent SPM selection.
description: Invalid update or id, no fields to update, or independent SPM selection.
content: *simulation_error_content
404:
description: The simulation does not exist in the selected country.
content: *simulation_error_content
409:
description: The simulation is complete, and the update would change its result.
content: *simulation_error_content
/{country_id}/simulation/{simulation_id}:
get:
summary: Get a stored simulation record
Expand Down Expand Up @@ -760,6 +767,11 @@ paths:
Update a report output status, result, or error message. Completed US
economy outputs require compact v2 comparison provenance. Each receipt
contains the complete resolved configuration; a sibling spm_config is rejected.
The id must be an integer. An id stored under an outdated cache version
updates its current equivalent, the record reads serve for it, and the
response keeps the requested id. A complete result is final: an update
that repeats it returns the record unchanged, and one that would change
it is rejected with HTTP 409.
parameters:
- name: country_id
in: path
Expand Down Expand Up @@ -809,6 +821,22 @@ paths:
finished_at:
type: string
nullable: true
"409":
description: The report is complete, and the update would change its result.
content:
application/json:
schema:
type: object
required: [status, message, result]
properties:
status:
type: string
enum: [error]
message:
type: string
result:
type: object
nullable: true
/{country_id}/report/{report_id}:
get:
summary: Get a report output
Expand Down
Loading
Loading