Documentation
¶
Overview ¶
Package entitlements answers "is this account Pro, and why": the rules of Rivolt Pro's plan, in one place.
Rows (table entitlements, one per account and source) say what grants Pro; Compute turns them into a State at a moment in time. Service reads them (cached briefly) and adds the two cases that need no row: billing off (every self-hosted install; everyone is Pro and the table is never read) and the demo account (the showroom is always Pro). Nothing here writes: the rows are written by the billing webhooks, the sign-up trial, the launch-day founding grant and operator grants, each in its own item. Nothing here touches Rivian: the plan is tier-blind to the poller and the shared budget.
Index ¶
- Constants
- Variables
- func ActiveFreeEnd(rows []Row, now time.Time) (time.Time, bool)
- func ActiveTrialEnd(rows []Row, now time.Time) (time.Time, bool)
- func ApplyFounding(ctx context.Context, d *sql.DB, demo uuid.UUID, launch time.Time) (int64, error)
- func FoundingEnd(launch time.Time) time.Time
- func RunFounding(ctx context.Context, pool *sql.DB, out, errOut io.Writer, demoID uuid.UUID, ...) int
- func RunStats(ctx context.Context, db *sql.DB, demo uuid.UUID, every time.Duration, ...)
- type Config
- type FoundingReport
- type Row
- type Rowset
- type Service
- func (s *Service) Billing() bool
- func (s *Service) FreeUntil(ctx context.Context, uid uuid.UUID) (time.Time, bool, error)
- func (s *Service) Invalidate(uid uuid.UUID)
- func (s *Service) ManageSubscription(ctx context.Context, uid uuid.UUID) bool
- func (s *Service) Pro(ctx context.Context, uid uuid.UUID) bool
- func (s *Service) State(ctx context.Context, uid uuid.UUID) (State, error)
- func (s *Service) TrialEnd(ctx context.Context, uid uuid.UUID) (time.Time, bool, error)
- func (s *Service) Window(ctx context.Context, uid uuid.UUID) (histwin.W, bool)
- type State
- type Stats
Constants ¶
const ( SourceApple = "apple" SourceStripe = "stripe" SourceTrial = "trial" SourceFounding = "founding" SourceGrant = "grant" SourceBillingOff = "billing_off" // billing is off on this install: everyone is Pro SourceDemo = "demo" // the demo account is the showroom )
Sources a Pro grant can come from, plus the two that need no row.
const ( StatusActive = "active" StatusPastDue = "past_due" StatusEnded = "ended" )
Statuses of a row.
const DefaultTTL = 15 * time.Second
DefaultTTL bounds how stale a replica's view of an account's rows can be until cross-replica invalidation exists.
const Grace = 7 * 24 * time.Hour
Grace is how long a paid subscription whose renewal failed stays Pro after its period end.
Variables ¶
var ErrFoundingCount = errors.New("entitlements: founding rows do not equal the accounts that existed at launch (demo excluded)")
ErrFoundingCount is the count check failing: founding rows are not accounts-at-launch minus the demo.
Functions ¶
func ActiveFreeEnd ¶
ActiveFreeEnd is the end of the account's free Pro: the latest end among its running founding, grant and trial rows (false: none is running). A new account has none: nothing starts a trial any more, and the 'trial' source is only read, for rows that already exist. A subscription bought while one of them runs must start billing when it ends, not at once (that would bill twice for the same months).
func ActiveTrialEnd ¶
ActiveTrialEnd is the end of a running trial, whatever else the account has: a Stripe subscription bought during the trial starts billing at that moment (Stripe's trial_end), so the checkout asks for it here rather than from the display fields.
func ApplyFounding ¶
ApplyFounding grants the founding year to every account that existed at launch (the demo excluded) in one transaction, then checks that founding rows = those accounts, and rolls back if not. Idempotent: an account that already has a founding row keeps it (its end is not moved), so a re-run grants nothing. It returns the number of rows it wrote.
func FoundingEnd ¶
FoundingEnd is how long the founding grant lasts from launch.
func RunFounding ¶
func RunFounding(ctx context.Context, pool *sql.DB, out, errOut io.Writer, demoID uuid.UUID, launch time.Time, apply bool, expectCount int, expectSHA string) int
RunFounding is the body of `rivolt entitlements-founding` (here, not in cmd/rivolt, so a test drives it against Postgres without pulling in the web bundle). It returns the process exit code.
func RunStats ¶ added in v0.40.17
func RunStats(ctx context.Context, db *sql.DB, demo uuid.UUID, every time.Duration, report func(Stats))
RunStats reads the counts every interval until ctx ends and hands them to report (the first read is at once). A failed read is skipped: the gauges keep their last values and the next tick tries again.
Types ¶
type Config ¶
type Config struct {
// Billing is RIVOLT_BILLING: false (the default, every self-hosted install) makes every account Pro without reading the table.
// Without a database there is nothing to read, so billing counts as off.
Billing bool
// Demo is the demo account's id (always Pro with billing on); uuid.Nil for none.
Demo uuid.UUID
// TTL is how long an account's rows are cached; 0 means DefaultTTL. A grant written on another replica is seen within it.
TTL time.Duration
Log *slog.Logger
// StripeManage is true when Stripe is configured: only then does a billing-off server look at the rows to tell which accounts still
// hold a Stripe subscription (ManageSubscription). Every self-hosted install that sells nothing leaves it false and reads no row.
StripeManage bool
// OnLookupError is told each time the rows could not be read (the metric beside the fail-open warning).
OnLookupError func()
}
Config for New.
type FoundingReport ¶
type FoundingReport struct {
Eligible int // accounts that existed at launch, the demo excluded
Already int // of those, how many already have a founding row
ToGrant int
IDs []uuid.UUID // accounts to grant, sorted
}
FoundingReport is what the grant would do, or did.
func PlanFounding ¶
func PlanFounding(ctx context.Context, q querier, demo uuid.UUID, launch time.Time) (FoundingReport, error)
PlanFounding lists what the founding grant would do for launch. Pure read.
func (FoundingReport) IDsHash ¶
func (r FoundingReport) IDsHash() string
IDsHash is a sha256 over the sorted ids to grant: the dry run prints it and --apply must be given it, so the run that applies is the run that was reviewed.
type Row ¶
type Row struct {
Source string
Status string
PeriodEnd *time.Time // nil: no end
// CancelAtPeriodEnd is a paid subscription set to not renew: still Pro until PeriodEnd (plan_cancels_at).
CancelAtPeriodEnd bool
// UpdatedAt is when the row last changed; it dates an ended row that has no period end (a refund, a revoke).
UpdatedAt time.Time
}
Row is one entitlement row.
type Rowset ¶
type Rowset []Row
Rowset is the cached form: the rows, so a cache hit is recomputed at the current moment (a trial ending is seen on time).
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service reads plans. The zero value is not usable; use New.
func (*Service) Billing ¶
Billing reports whether billing is on; clients hide every paywall control when it is not.
func (*Service) FreeUntil ¶
FreeUntil is the end of the account's running free Pro, a trial or the founding year (false: none). Billing off, the demo account and a failed lookup have none.
func (*Service) Invalidate ¶
Invalidate drops the cached rows of one account on this replica (call it after writing one).
func (*Service) ManageSubscription ¶ added in v0.40.17
ManageSubscription is true when billing is off but the account has a Stripe subscription that is not over: it keeps renewing whatever the flag says, so the client keeps showing the way to manage and cancel it (the Customer Portal, which works with billing off). With billing on the plan state says it all, so this is false then, and false for the demo and on a failed lookup.
func (*Service) Pro ¶
Pro is whether the account is Pro now. It fails OPEN: if the rows cannot be read, the account counts as Pro, because the cost of that is a free account briefly seeing more, while failing closed would hide a paying user's own history. The failure is logged.
func (*Service) State ¶
State is the account's plan now. The error is the lookup's: callers decide what an unknown plan means (Pro below fails open).
func (*Service) TrialEnd ¶
TrialEnd is the end of the account's running trial (false: none). Billing off, the demo account and a failed lookup have none.
type State ¶
type State struct {
Pro bool
// Source is what makes the account Pro: the strongest granting row (a paid one, then founding, grant, trial), billing_off or demo.
// Empty when free.
Source string
// PeriodEnd is when that grant ends, grace included; nil when free or when it has no end.
PeriodEnd *time.Time
// TrialEnds is the trial's end, only while the trial is what grants Pro; nil otherwise.
TrialEnds *time.Time
// CancelsAt is when a paid subscription that is set to not renew stops granting Pro (its period end): "cancelled, paid until ...".
// Only for an active stripe or apple row that is the one granting; nil otherwise.
CancelsAt *time.Time
// GraceEnds is when the grace of a paid subscription whose renewal failed runs out: "payment failed, update your card by ...".
// Only while the row granting Pro is a past-due paid one (stripe or apple); nil otherwise.
GraceEnds *time.Time
// EndedAt and EndedSource describe a LAPSE: for a free account that once had Pro, when the most recent grant stopped and which source
// it was ("Your Pro ended on ..."). Nil/empty for an account that never had Pro, and always for a Pro account.
EndedAt *time.Time
EndedSource string
}
State is the plan of one account at one moment.