Skip to content

backfill subscriptions onto plan prices - #782

Closed
mitchelljkotler wants to merge 6 commits into
pricing-modelsfrom
pricing-models-backfill
Closed

backfill subscriptions onto plan prices#782
mitchelljkotler wants to merge 6 commits into
pricing-modelsfrom
pricing-models-backfill

Conversation

@mitchelljkotler

@mitchelljkotler mitchelljkotler commented Aug 26, 2026

Copy link
Copy Markdown
Member

This adds a command to backfill subscriptions into the new data model

Claude notes:

One command, no schema, no Stripe calls. The checks worth doing are the refusals — no --actor, unknown --actor, and running it before consolidate_stripe_products — because the command's whole safety story is that it stops rather than guessing.

Then spot-check the distinction it exists to make: a paying subscriber lands on the standard price, a comped organization lands on the comped price with granted_by filled in.

@allanlasser
allanlasser temporarily deployed to squarelet-pi-pricing-mo-7hpy4u August 26, 2026 15:43 Inactive
@mitchelljkotler
mitchelljkotler force-pushed the pricing-models-backfill branch from 6b6b8fe to a1b18c9 Compare August 27, 2026 13:03
@mitchelljkotler
mitchelljkotler temporarily deployed to squarelet-pi-pricing-mo-7hpy4u August 27, 2026 13:04 Inactive
@mitchelljkotler
mitchelljkotler force-pushed the pricing-models-backfill branch from a1b18c9 to 6ee5503 Compare August 27, 2026 20:09
@mitchelljkotler
mitchelljkotler temporarily deployed to squarelet-pi-pricing-mo-7hpy4u August 27, 2026 20:10 Inactive
@mitchelljkotler
mitchelljkotler temporarily deployed to squarelet-pi-pricing-mo-7hpy4u August 27, 2026 21:02 Inactive
@mitchelljkotler
mitchelljkotler temporarily deployed to squarelet-pi-pricing-mo-wawwsj September 1, 2026 13:57 Inactive
mitchelljkotler and others added 5 commits September 1, 2026 11:56
285 lines that repoint `plan` on every subscription in production, with
no tests.  The Stripe half needs none, but the half that decides what
happens to each row is a pure function of local data and worth pinning:

- is_billing separating standard from comped, which is what keeps years
  of admin-granted free access off a paid price
- _preflight refusing to write when a (slug, is_billing) pair is
  unmapped, and writing nothing at all when it does refuse -- the
  deliberate absence of a log-and-skip path is the command's main safety
  property and nothing checked it held
- DEFERRED_SLUGS genuinely left alone
- per-user subscribers still billing excluded, and comped ones NOT

That last one is the subtle case.  The exclusion is per-user AND billing,
so a comped organization -- per-user plan, no Stripe subscription -- must
still be migrated.  Its correctness rests on subscription_id being
nullable and on both halves of a two-condition exclude; the naive
`exclude(plan__price_per_user__gt=0)` strands every comped organization
with a null plan_price, which then fails step 3c much later and much less
legibly.  Verified the test fails against that mutation.

Targets are derived from LEGACY_PLAN_MAP rather than listed, so the
fixture cannot drift from the map it is standing in for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
_preflight promises to refuse the whole run unless every case is
accounted for, but its collision scan only compared pending rows against
each other.  A per-user subscriber still billing is excluded from this
step and yet still occupies (organization, plan), so an organization
holding one of those plus a legacy comp that maps onto the same canonical
plan got past the check and aborted mid-write on a bare IntegrityError --
precisely the outcome the check exists to prevent.

The scan now also looks at rows outside `pending`.  Extracted to its own
method: _preflight was running three unrelated checks and had outgrown
being read in one go.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The targets fixture built a plan at each canonical slug without checking
whether the migration had already seeded one.  Where it had, AutoSlugField
quietly made `professional-2`, the command could not find its target
prices, and every test that runs it failed on the pre-flight check --
including the two collision tests, which never reached the assertion they
were making.

Whether the seeded plans are present depends on whether a transactional
test has flushed the database first, so this passed locally and failed on
a clean one.  The fixture now reuses a seeded plan when there is one.

Verified in all three states: the file alone against a freshly migrated
database, the full suite against that same database once flushed, and the
full suite against a fresh one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same treatment as the migration and consolidation command: no pointers to
a document the reviewer does not have, and no step numbers that mean
nothing outside it.

Two of these had also gone stale.  The command still described per-user
subscribers as being split "at renewal", and reported them as "awaiting
their renewal-time split" -- the design that was replaced by a single
all-at-once decomposition.  An operator reading that summary would have
been told to expect something that is no longer going to happen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mitchelljkotler
mitchelljkotler temporarily deployed to squarelet-pi-pricing-mo-wawwsj September 1, 2026 15:57 Inactive
No organization holds it any more, so there is nothing left to have a
pricing conversation about.  With no subscriptions on the slug it never
reaches `pending`, so removing it from DEFERRED_SLUGS changes nothing the
command does today -- and if one did turn up, the pre-flight would refuse
the whole run rather than guess at a target, which is the outcome we want.

custom-crp and sunlight-premium-annual stay deferred; both still need a
decision.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mitchelljkotler
mitchelljkotler temporarily deployed to squarelet-pi-pricing-mo-wawwsj September 1, 2026 18:11 Inactive
@mitchelljkotler
mitchelljkotler marked this pull request as ready for review September 1, 2026 18:12
@mitchelljkotler
mitchelljkotler marked this pull request as draft September 2, 2026 16:14
@mitchelljkotler

Copy link
Copy Markdown
Member Author

Do not review yet, this needs to be re-ordered

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.

2 participants