Skip to content

Let a saved policy be repointed to a v2 policy (PATCH /v2/user-policies policy_id) - #3968

Closed
anth-volk wants to merge 2 commits into
masterfrom
apiv2/1e-user-policy-repoint
Closed

anth-volk wants to merge 2 commits into
masterfrom
apiv2/1e-user-policy-repoint

Conversation

@anth-volk

@anth-volk anth-volk commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #3965

Summary

  • PATCH /v2/user-policies/{id} accepts policy_id. Supplied alone, it points the saved policy at that v2 policy and clears legacy_policy_id, which no longer describes the saved content. Supplied with legacy_policy_id, both are stored, and policy_id must be the v2 copy of that v1 policy when the Stage 10 mirror made one (the service cannot compare them otherwise, as on creation).
  • The policy must exist in the row's country: unknown policies return 404, policies from another country return 400, and a policy_id that differs from the v1 policy's v2 copy returns 400, as on creation. An explicit null is refused (422).
  • Rows maintained by the v1 saved-policy mirror still return 409.

Why: once the app's v3 workspace saves reforms as /v2/policies policies (PolicyEngine/policyengine-app-v2#1347), editing a reform's parameters creates a new v2 policy. Without this change the row would have to be deleted and recreated, which changes the saved reform's ID that report records reference (user_policy_id).

This is part of plan group 1E (v3 metadata and saved reforms). It is the bottom of a three-PR stack; the two above it fill the v2 metadata gaps (the plan's item 1E-1).

API contract change

Additive: the PATCH request schema gains the optional policy_id property (visible in /v2/openapi.json). Clearing legacy_policy_id happens only when a caller sends policy_id without it, which no existing caller does. Nothing calls this yet.

What stays on the v1 routes

Everything: no v1 route or v1 data changes. Saved policies created through legacy_policy_id behave exactly as before.

Testing

  • uv run pytest tests/unit/v2/test_user_policy_service.py tests/unit/v2/test_user_policy_routes.py tests/unit/v2/test_user_route_auth.py tests/contract -q: 138 passed. New tests: repointing to a v2 policy clears the v1 reference and calls no v1 check; repointing with both references keeps both; unknown, other-country and conflicting policies change nothing; the request model accepts policy_id and refuses null; the route forwards policy_id; the OpenAPI schema lists it.
  • Against a disposable local PostgreSQL: tests/integration/test_alembic_v2_lifecycle.py tests/integration/test_v2_user_identity_persistence.py: 21 passed.
  • The Stage 12A association notes (docs/migration/stage-12a-user-login.md) describe the new PATCH policy_id behavior.
  • uv run --frozen --extra dev mypy: no issues. ruff format and ruff check on changed files: clean. python scripts/run_quality_guards.py: passed. python scripts/export_migration_contracts.py: no changes.

Merge order and deployment

Possible conflicts

The open ruff refactor PRs (#3920, #3924, #3932–#3934) may touch the user-policy service modules; whichever merges second rebases.

🤖 Generated with Claude Code

PATCH /v2/user-policies/{id} now accepts policy_id. Supplied alone, it
points the saved policy at that v2 policy and clears legacy_policy_id,
which no longer describes the saved content. Supplied with
legacy_policy_id, the two must name the same policy. The policy must
exist in the row's country. Without this, the app could not change the
parameters of a reform saved on v2 without deleting the row and losing
its ID, which reports link to.

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

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.23810% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 84.12%. Comparing base (c39ae36) to head (ad5c259).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
...es/v2/user_policies/database_connectors/updates.py 83.33% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #3968       +/-   ##
===========================================
+ Coverage   46.17%   84.12%   +37.94%     
===========================================
  Files         239      255       +16     
  Lines       13987    15378     +1391     
  Branches     2272     2558      +286     
===========================================
+ Hits         6458    12936     +6478     
+ Misses       6958     1683     -5275     
- Partials      571      759      +188     

☔ 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.

The route description, the request model and service docstrings, and the
changelog fragment said that policy_id and legacy_policy_id "must name the
same policy". The service can check that only when the v1 policy has a v2
copy from the Stage 10 mirror; otherwise it stores both references, exactly
as creation does. Say that, and add the new PATCH policy_id behavior to the
Stage 12A association notes, which still described only legacy_policy_id.

Documentation-only change: no behavior changes, so no test can fail first.

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.

Let a saved policy be repointed to a v2 policy

1 participant