Plan duration is a hardcoded 30-day constant the plan model cannot express
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
SubscriptionModal.tsx:102—'Never expires — free plan'is rendered wheneveractiveUntilis 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.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.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
ResetRowto a real reset time or drop it.
Related
- unicitynetwork/aggregator-subscription#83 — item 8, the server-side half
- #509 — the billing surface, which would have to state a plan's term
- #510 — the downgrade that silently burns the remainder of the 30-day period
Source: unicity-sphere/sphere