Recurring subs Phase 4 — admin UI + buy-page rendering + Pro-tier gate
Phase 4 surfaces the recurring-subscription schema (migration 0011) and
renewal-worker (Phase 2, commit 7007bf8) through every layer operators
and buyers actually see:
API
- Policy struct + repo gain is_recurring, renewal_period_days,
grace_period_days, trial_days. RecurringConfig / RecurringUpdate
helper structs keep create_policy / update_policy signatures
manageable.
- CreatePolicyReq + UpdatePolicyReq accept all four fields. Validation
rejects internally inconsistent combos (recurring=true with period=0,
trial > renewal period, period >5y, grace >90d).
- New tier::enforce_recurring_feature gate. Pro/Patron only — Creator
and Unlicensed get a 402 with upgrade_url. The gate fires on both
create-policy and the false→true transition in update-policy.
- list_public_policies now surfaces is_recurring, renewal_period_days,
trial_days so SDKs and the buy page can render cadence.
Admin UI (web/index.html)
- Create-policy form gets a "Recurring subscription (Pro)" section:
is_recurring checkbox + cadence preset (monthly/quarterly/etc/custom)
+ grace period + trial days. Live enable/disable: the inputs gray
out unless the box is ticked, and the custom-days input grays out
unless "Custom" is selected.
- Edit-policy modal mirrors the same section, pre-populated from the
policy's current values.
- Policies-list table shows a gold "every Nd" badge alongside the
trial badge so operators can see at a glance which policies renew.
Buy page (/buy/<slug>)
- Tier cards on a recurring policy render a "Renews monthly/annually/
every N days" meta line + a "/mo" / "/yr" / "/Nd" suffix on the
price unit, so the headline reads "$25 / mo" not just "$25".
- First-cycle trial banner shows when trial_days > 0.
- TIERS JSON map exposes is_recurring + renewal_period_days +
trial_days so the JS price-update path keeps the cadence suffix
in sync when the buyer clicks between tiers.
Tests (+4, total now 53)
- recurring_policy_blocked_on_creator_tier — 402 + upgrade_url
- pro_tier_creates_monthly_recurring_policy — full create + verify
via both admin GET and public list endpoint
- recurring_requires_positive_period — validator rejects period=0
- edit_policy_to_recurring_respects_tier_gate — Creator 402 on flip,
Pro 200 on same flip, name-only PATCH on already-recurring policy
doesn't re-fire the gate after downgrade
Drive-by: wrap the state-machine ASCII diagram in subscriptions.rs in
a ```text fence so cargo's doc-test runner stops trying to compile box
characters as Rust tokens.
This commit is contained in:
@@ -166,10 +166,32 @@ pub struct Policy {
|
||||
/// Defaults to true; migration 0007 adds this column.
|
||||
#[serde(default = "default_true")]
|
||||
pub public: bool,
|
||||
/// Recurring subscription cadence (migration 0011). When `is_recurring`
|
||||
/// is true, the renewal worker will create a fresh invoice every
|
||||
/// `renewal_period_days` and the buy page renders the price as
|
||||
/// "every N days" / "monthly".
|
||||
#[serde(default)]
|
||||
pub is_recurring: bool,
|
||||
/// Days between renewal cycles. Ignored when `is_recurring = false`.
|
||||
/// Common values: 30 (monthly), 365 (annual).
|
||||
#[serde(default)]
|
||||
pub renewal_period_days: i64,
|
||||
/// Days the subscription stays in `past_due` before transitioning to
|
||||
/// `lapsed`. Migration default is 7.
|
||||
#[serde(default = "default_grace_period_days")]
|
||||
pub grace_period_days: i64,
|
||||
/// Free-trial length at first cycle. 0 = no trial. The first invoice
|
||||
/// is still issued (for $0 / 1-sat) so the buyer email + license
|
||||
/// flow is consistent; renewal worker charges the real price after
|
||||
/// `trial_days`.
|
||||
#[serde(default)]
|
||||
pub trial_days: i64,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
fn default_grace_period_days() -> i64 { 7 }
|
||||
|
||||
fn default_true() -> bool { true }
|
||||
|
||||
/// A machine activated under a license. One row per active install.
|
||||
|
||||
Reference in New Issue
Block a user