Skip to content

Plan duration is a hardcoded 30-day constant the plan model cannot express #512

Description

@KruGoL

Limits and expiry ARE visible today — Settings → Subscription shows the plan
name, a "Renews / expires: " line and two usage bars reading
used / limit for the per-day and per-minute ceilings. What is not visible is
that the 30-day term behind all of it is a constant, not a property of the
plan, and the copy asserts things the data model cannot express.

This matters if plans ever stop being time-based and become limit-only: the
wallet would then describe them wrongly in three places, and the gateway could
not represent them at all.

The term is hardcoded on both sides

Client. planFeatures() emits the bullet '30-day subscription' for every
plan with priceCents > 0. It is a constant in a ternary — PlanInfo carries
planId, name, requestsPerMinute, requestsPerDay, priceCents,
fiatCurrency, and no duration at all. The string reaches the user on the plan
cards (PlanCard.tsx:73) and twice in PlanScreen.tsx (:976, :985).

Server. pricing_plans has no duration column either — after V1/V4/V6/V23
its shape is id, name, requests_per_minute, requests_per_day, price,
price_cents. The term lives in ApiKeyUtils.PAYMENT_VALIDITY_DAYS = 30 and is
applied identically to every plan through getExpiryStartingFrom() at
fulfilment; the legacy flow's proration divides by the same constant. A
per-plan term is not expressible; the only escapes are the admin override
updateActiveUntil(id, instant) and the free tier's NULL active_until.

So a limit-only paid plan is representable today ONLY as active_until = NULL
— which is exactly the shape the UI reads as "free".

Three places that would then lie

  1. SubscriptionModal.tsx:102'Never expires — free plan' is rendered
    whenever activeUntil is null, regardless of the plan. A paid limit-only
    plan would print its real name on the line above and be called free on the
    line below.
  2. SubscriptionModal.tsx:101'Renews / expires: …'. Nothing renews:
    the subscription is not recurring, there is no auto-charge, it simply
    expires. The word is already wrong today.
  3. planFeatures.ts — the '30-day subscription' bullet, on every paid
    card, whatever the plan's real term.

isPlanSelectable() also rests on the assumption: it blocks re-buying the
CURRENT plan while active because "the gateway resets the window to now+30d
with no time carry-over". With no window, that rationale disappears.

Bonus: a dead widget already ships

SubscriptionModal.tsx:125 renders <ResetRow resetAt={null} …> — the value
is hardcoded. msUntil(null) is null, so the row always reads "Daily limit
resets in — continuously"
. The comment says it is "kept for when the gateway
adds a reset time". Today it is a countdown label with nothing to count, next
to a limiter (bucket4j) that refills continuously.

Suggested shape

  • Derive the term from data instead of a constant: an explicit duration on the
    plan (null = no expiry), so the card bullet and the Settings line can both
    be generated rather than asserted. Needs the gateway half — filed as item 8
    of unicitynetwork/aggregator-subscription#83.
  • Until then, at least stop equating "no expiry" with "free": key that copy on
    the plan's price, not on the presence of a date.
  • Either wire ResetRow to a real reset time or drop it.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions