subscription

package
v1.9.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 3, 2026 License: MIT Imports: 4 Imported by: 2

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func StretchColumns

func StretchColumns(st *Stretch) (start, end, originalEnd, floor *time.Time)

StretchColumns splits a stretch into the four nullable columns the stores keep it in, in UTC. A nil stretch is four nils.

Types

type DueField

type DueField string

DueField names the date a lifecycle step reads from a subscription.

const (
	// DueCancel reads cancel_at, a scheduled cancellation.
	DueCancel DueField = "cancel_at"
	// DueTrialEnd reads trial_end.
	DueTrialEnd DueField = "trial_end"
	// DuePeriodEnd reads current_period_end.
	DuePeriodEnd DueField = "current_period_end"
)

type DueOpts

type DueOpts struct {
	Field    DueField
	Before   time.Time
	Statuses []Status
	AppID    string
	Limit    int
	Offset   int
}

DueOpts selects the subscriptions whose Field is at or before Before and whose status is one of Statuses (any status when it is empty), earliest date first, ties broken by id. An empty AppID means every app: the lifecycle clock runs across apps. The database stores index each date behind the status, (status, cancel_at) and so on, so a caller that names its Statuses reads the index and not every row that ever carried the date: an immediate cancel stamps cancel_at too, so that date is set on every canceled row.

type ListOpts

type ListOpts struct {
	Status Status
	Limit  int
	Offset int
}

type Period

type Period struct {
	Start time.Time `json:"start"`
	End   time.Time `json:"end"`
}

Period is one billing period, [Start, End).

type Renewal

type Renewal struct {
	Subscription *Subscription `json:"subscription"`
	Ended        []Period      `json:"ended"`
}

Renewal is what OnSubscriptionRenewed receives when the lifecycle clock moves a subscription on: the subscription in its new period, and every period that ended in the move, oldest first, catch-up periods included.

type Resume

type Resume struct {
	// PausedAt is the paused_at the engine read, nil when there was none.
	PausedAt *time.Time
	// Status is what the subscription resumes to: trialing when its trial
	// was still running when it was paused, active otherwise.
	Status Status
	// PeriodEnd is the current period's end moved on by the length of the
	// pause, or nil to leave the period alone. A cancel scheduled for the
	// old period end (cancel_at equal to current_period_end) moves with it.
	PeriodEnd *time.Time
	// TrialEnd is the trial end moved on by the length of the pause, or nil
	// to leave trial_end as it is.
	TrialEnd *time.Time
	// Stretch is the stretch record to store, or nil to leave it as it is.
	Stretch *Stretch
}

Resume is what a resume writes, in one conditional store write that lands only while the subscription is still paused and still holds the PausedAt the engine read (nil: none). Everything here is worked out by the engine from that read, so a second pause and resume in between must not let it land. The period start never changes.

type Status

type Status string
const (
	StatusActive   Status = "active"
	StatusTrialing Status = "trialing"
	StatusPastDue  Status = "past_due"
	StatusCanceled Status = "canceled"
	StatusExpired  Status = "expired"
	StatusPaused   Status = "paused"
)

type Store

type Store interface {
	Create(ctx context.Context, s *Subscription) error
	Get(ctx context.Context, subID id.SubscriptionID) (*Subscription, error)
	GetActive(ctx context.Context, tenantID string, appID string) (*Subscription, error)
	List(ctx context.Context, tenantID string, appID string, opts ListOpts) ([]*Subscription, error)
	Update(ctx context.Context, s *Subscription) error
	Cancel(ctx context.Context, subID id.SubscriptionID, cancelAt time.Time) error
}

type Stretch

type Stretch struct {
	Start       time.Time
	End         time.Time
	OriginalEnd time.Time
	Floor       *time.Time
}

Stretch is the most recent billing period a resume stretched: it began at Start, was due to end at OriginalEnd, and ends at End once every pause in it is added on. The periods after it follow the clock's anchor from End (whatever nextPeriod gives); the ones before it ran on the cadence that led to OriginalEnd. Floor is where that earlier cadence began when an older stretch came before it (the older stretch's End): periods before Floor cannot be proved, since only one stretch is remembered. A nil Floor means the earlier cadence runs back to the subscription's creation.

func StretchFromColumns

func StretchFromColumns(start, end, originalEnd, floor *time.Time) *Stretch

StretchFromColumns is StretchColumns the other way: nil unless start, end and originalEnd are all set.

type Subscription

type Subscription struct {
	types.Entity
	ID                 id.SubscriptionID `json:"id"`
	TenantID           string            `json:"tenant_id"`
	PlanID             id.PlanID         `json:"plan_id"`
	Status             Status            `json:"status"`
	CurrentPeriodStart time.Time         `json:"current_period_start"`
	CurrentPeriodEnd   time.Time         `json:"current_period_end"`
	TrialStart         *time.Time        `json:"trial_start,omitempty"`
	TrialEnd           *time.Time        `json:"trial_end,omitempty"`
	CanceledAt         *time.Time        `json:"canceled_at,omitempty"`
	CancelAt           *time.Time        `json:"cancel_at,omitempty"`
	EndedAt            *time.Time        `json:"ended_at,omitempty"`
	// PausedAt is when the subscription was paused. It is set while the
	// status is paused and cleared by a resume, which moves the period end
	// and a running trial's end on by the length of the pause. A
	// subscription paused before this field existed has none.
	PausedAt *time.Time `json:"paused_at,omitempty"`
	// Stretch remembers the most recent period a resume stretched, so
	// ledger.ForPeriod can still prove the periods around it. Ledger keeps
	// it for itself; it is not part of the JSON.
	Stretch      *Stretch          `json:"-"`
	AppID        string            `json:"app_id"`
	ProviderID   string            `json:"provider_id,omitempty"`
	ProviderName string            `json:"provider_name,omitempty"`
	Metadata     map[string]string `json:"metadata,omitempty"`

	// Quantity holds the current count for each quantity-priced plan
	// feature, keyed by feature key. Seats are the usual case.
	//
	// A seat count is a level and not a flow, so it is stored here and set
	// when the subscription is created or changed, rather than derived
	// from usage events. Aggregating a stream would answer "how many seats
	// were added this month", which is a different question.
	Quantity map[string]int64 `json:"quantity,omitempty"`
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL