Skip to content

Rationalize the entitlement refresh cycle #681

@allanlasser

Description

@allanlasser

Problem

After working on #666, the codebase now has two parallel implementations of the same concept:

  • Subscription.update_on is referenced by a branch in the restore_organization task that advances it's update_on value monthly and broadcasts cache invalidations to clients.
  • EntitlementGrant.update_on is referenced by a second branch in restore_organization that does the same exact thing.

Both Subscription and EntitlementGrant are sources that confer entitlements on organizations. Both refresh monthly. The cadence is hard-coded in both places, and the serializer has to tie-break across them ("pick the soonest update_on date among all entitlements").

EntitlementGrant was designed to stand as a sibling to Subscription, but suffers from the same faulty assumption: that refresh cadence belongs to the source. It doesn't. Resources (monthly requests, AI credits, etc.) and their refresh cadence are properties of an entitlement.

A plan and a grant are sources; the entitlement is the contract.

Proposal

This is a rough sketch — details may change during shaping and implementation.

  1. Move cadence to Entitlement. Add refresh_interval = DurationField() (default 1 month) and an optional refresh_anchor for billing-cycle alignment.
  2. Unify the "grant" abstraction. Every source that confers an entitlement (subscriptions, explicit/rule-based grants, or other future sources like coupons) become a row in a join model that remembers the update_on date. This model joins organizations with an entitlement and the source (subscription, grant) that confers the entitlement.
  3. Collapse restore_organization to one loop over join rows whose update_on has passed, advancing by the entitlement's refresh_interval. Instead of storing update_on on each organization then finding each entitlement to update, we store the information on the relationship between the org and its entitlement. This is more efficient and allows for more flexibility in how frequently entitlements refresh.
  4. Move per-org resource counters (like Organization.max_users and similar) onto entitlements, so they can be updated and adjusted as orgs change plans.
  5. Subscription remains as a record and manager for billing-specific logic — Stripe sub_id, cancel_at_period_end, etc. — but no longer locks in the refresh cadence.

What this gets us

  • We can create and offer entitlements that update outside of a monthly cadence. For example, we could offer AI Credits that refresh daily or hourly, instead of monthly.
  • Resources from multiple sources compose naturally (a plan giving 100 docs + a grant giving +50 docs) instead of one shadowing the other.
  • The serializer's "soonest update_on" tie-break goes away — the date lives on the source row, not on the source model.
  • One refresh task, one broadcast point and simpler reasoning about cadence with many fewer database requests per task run.

Costs + risks

  • Existing Subscription.update_on values need to migrate to the new join table.
  • Existing use of org.subscription.update_on or subscription.cancel-style logic needs review.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions