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
- 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) 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
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.
const TrialLength = 14 * 24 * time.Hour
TrialLength is the Pro trial every new account gets: 14 days, no card.
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 trial and founding rows (false: neither is running). 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.
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
// 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) 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.