billing

package
v1.14.0 Latest Latest
Warning

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

Go to latest
Published: Nov 12, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package billing charges for an app with Stripe: plans defined in Go, Stripe Checkout to subscribe, the customer portal to change or cancel, trials and seats, and limits per plan checked in loads and actions.

Stripe's webhooks keep the app's copy of each subscription current. They are verified, recorded once, and processed as jobs, which fetch the subscription from Stripe: a slow, repeated or out-of-order event is safe.

b, err := billing.New(db, billing.Options{
	SecretKey:     os.Getenv("STRIPE_SECRET_KEY"),
	WebhookSecret: os.Getenv("STRIPE_WEBHOOK_SECRET"),
	Jobs:          queue,
	Plans: []billing.Plan{
		{ID: "free", Name: "Free", Limits: map[string]int{"projects": 3}},
		{ID: "pro", Name: "Pro", Prices: map[billing.Interval]string{billing.Monthly: "price_…"}, TrialDays: 14},
	},
})
err = b.Migrate(ctx)
app, err := ternApp(kit.WithPlugins(b))

Who pays is an owner: a string the app chooses, such as billing.Team(id) for a team or billing.User(id) for a user.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrUnknownPlan = errors.New("billing: unknown plan or interval")
	ErrNoCustomer  = errors.New("billing: this owner has never subscribed")
	ErrNotSet      = errors.New("billing: Options.SecretKey is not set")
)

The errors.

View Source
var ErrLimit = errors.New("billing: the plan's limit is reached")

ErrLimit is what Check returns past a plan's limit; errors.Is matches every *LimitError.

View Source
var ErrSignature = errors.New("billing: the webhook's signature is invalid")

ErrSignature is a webhook whose signature doesn't check out.

Functions

func FormError

func FormError(c *kit.Ctx, err error) error

FormError turns a *LimitError into a form error, in the user's language, for an action to return. Other errors stay as they are.

func HTTPError

func HTTPError(c *kit.Ctx, err error) error

HTTPError turns a *LimitError into a 402 Payment Required, for a load or an endpoint. Other errors stay as they are.

func Team

func Team(id int64) string

Team is the owner key of a team.

func User

func User(id int64) string

User is the owner key of a user.

func VerifySignature

func VerifySignature(payload []byte, header, secret string, now time.Time) error

VerifySignature checks a Stripe-Signature header against the payload and the endpoint's secret, as Stripe's libraries do.

Types

type Billing

type Billing struct {
	// contains filtered or unexported fields
}

Billing is the billing plugin. Create it with New.

func New

func New(db *sql.DB, opts Options) (*Billing, error)

New returns the billing plugin.

func (*Billing) Check

func (b *Billing) Check(ctx context.Context, owner, limit string, used int) error

Check returns a *LimitError if adding one more would go past the owner's plan's limit, given how many they have now; nil if the plan allows it or has no such limit. In an action, before creating:

n, err := projects.Count(ctx, teamID)
if err := b.Check(ctx, billing.Team(teamID), "projects", n); err != nil {
	return Done{}, billing.FormError(c, err)
}

func (*Billing) Checkout

func (b *Billing) Checkout(c *kit.Ctx, o CheckoutOptions) (string, error)

Checkout starts Stripe Checkout to subscribe the owner to a plan, and returns its address to redirect to. The plan's trial applies to owners who haven't subscribed before.

func (*Billing) Current

func (b *Billing) Current(ctx context.Context, owner string) (Plan, *Subscription, error)

Current is the plan that applies to the owner: their subscription's while it's active, else the free plan. The subscription is nil if they never had one.

func (*Billing) Has

func (b *Billing) Has(ctx context.Context, owner, feature string) (bool, error)

Has reports whether the owner's plan includes a feature.

func (*Billing) Limit

func (b *Billing) Limit(ctx context.Context, owner, limit string) (int, bool, error)

Limit is the owner's plan's limit, and whether it has one.

func (*Billing) Migrate

func (b *Billing) Migrate(ctx context.Context) error

Migrate creates the tables, if they don't exist yet.

func (*Billing) Name

func (*Billing) Name() string

Name implements kit.Plugin.

func (*Billing) Plan

func (b *Billing) Plan(id string) (Plan, bool)

Plan returns a plan by its ID.

func (*Billing) Plans

func (b *Billing) Plans() []Plan

Plans returns the plans, in order.

func (*Billing) Portal

func (b *Billing) Portal(c *kit.Ctx, owner, returnURL string) (string, error)

Portal opens Stripe's customer portal for the owner, where they change their plan or payment method, see invoices, or cancel, and returns its address to redirect to. returnURL is where its back link goes.

func (*Billing) SetSeats

func (b *Billing) SetSeats(ctx context.Context, owner string, seats int) error

SetSeats changes the quantity of the owner's per-seat subscription, as members join or leave. Stripe prorates the change. It does nothing for owners without an active per-seat subscription.

func (*Billing) Setup

func (b *Billing) Setup(s *kit.Setup) error

Setup implements kit.Plugin: it serves the webhook endpoint.

func (*Billing) Subscription

func (b *Billing) Subscription(ctx context.Context, owner string) (*Subscription, error)

Subscription returns the owner's subscription, active or not, or nil if they never had one.

func (*Billing) SyncSubscription

func (b *Billing) SyncSubscription(ctx context.Context, id string) error

SyncSubscription fetches a subscription from Stripe and stores the app's copy: what the webhooks do. Fetching it, instead of trusting the event, makes events that arrive late or out of order harmless.

type CheckoutOptions

type CheckoutOptions struct {
	Owner    string
	Plan     string
	Interval Interval
	// Email fills in the customer's email the first time.
	Email string
	// Seats is the quantity of a per-seat plan; at least 1.
	Seats int
	// SuccessURL and CancelURL are where Stripe sends the customer back:
	// absolute, or paths on the app's own address.
	SuccessURL, CancelURL string
}

CheckoutOptions describe a Checkout for Checkout.

type Interval

type Interval string

Interval is how often a price is charged.

const (
	Monthly Interval = "month"
	Yearly  Interval = "year"
)

The intervals.

type LimitError

type LimitError struct {
	Limit string // "projects"
	Max   int
	Plan  Plan
}

LimitError says which limit was reached, on which plan.

func (*LimitError) Error

func (e *LimitError) Error() string

func (*LimitError) Is

func (e *LimitError) Is(target error) bool

Is makes errors.Is(err, ErrLimit) true.

type Options

type Options struct {
	// SecretKey is Stripe's secret API key, sk_test_… or sk_live_….
	SecretKey string
	// WebhookSecret signs Stripe's webhooks to the app, whsec_…, from the
	// webhook endpoint's settings or `stripe listen`.
	WebhookSecret string
	// Plans are the plans, in the order the pricing page shows them.
	Plans []Plan
	// FreePlan is the plan of owners without an active subscription. The
	// default is the first plan without prices.
	FreePlan string
	// Jobs processes webhooks in the background, with retries. Without
	// it, they're processed in the webhook's request.
	Jobs *jobs.Queue
	// WebhookPath is where Stripe posts events. The default is
	// /_tern/billing/webhook.
	WebhookPath string
	// OnChange is called after an owner's subscription changes, from the
	// webhook's job: to email a receipt, or record it in the audit log.
	OnChange func(ctx context.Context, s Subscription)

	// APIBase is Stripe's API address, for tests. The default is
	// https://api.stripe.com.
	APIBase string
	// HTTPClient sends the API requests. The default has a 30s timeout.
	HTTPClient *http.Client
}

Options configure billing.

type Plan

type Plan struct {
	ID          string `json:"id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	// Prices are the plan's Stripe price IDs, by interval. A plan without
	// prices is free: everyone without a subscription is on it.
	Prices map[Interval]string `json:"-"`
	// Amounts are what the pricing page shows, in the currency's smallest
	// unit (cents), by interval. Stripe charges what its prices say: keep
	// them the same.
	Amounts  map[Interval]int64 `json:"amounts,omitempty"`
	Currency string             `json:"currency,omitempty"` // "usd", "eur"
	// Limits cap what the plan allows: {"projects": 10, "members": 5}. A
	// limit the plan doesn't list is unlimited.
	Limits map[string]int `json:"limits,omitempty"`
	// Features are what the plan includes, for the pricing page and Has.
	Features []string `json:"features,omitempty"`
	// TrialDays is a free trial before the first charge.
	TrialDays int `json:"trialDays,omitempty"`
	// PerSeat charges per member: the subscription's quantity is the seats.
	PerSeat bool `json:"perSeat,omitempty"`
	// Highlight marks the plan to recommend on the pricing page.
	Highlight bool `json:"highlight,omitempty"`

	// Paid and Intervals are for the pricing page, which doesn't see the
	// prices: whether the plan costs money, and the intervals it's paid
	// by. New sets them.
	Paid      bool       `json:"paid"`
	Intervals []Interval `json:"intervals"`
}

Plan is what a customer can subscribe to.

func (Plan) Free

func (p Plan) Free() bool

Free reports whether the plan costs nothing.

type Status

type Status string

Status is a subscription's state, as Stripe names it.

const (
	Active            Status = "active"
	Trialing          Status = "trialing"
	PastDue           Status = "past_due"
	Canceled          Status = "canceled"
	Unpaid            Status = "unpaid"
	Incomplete        Status = "incomplete"
	IncompleteExpired Status = "incomplete_expired"
	Paused            Status = "paused"
)

The statuses.

type StripeError

type StripeError struct {
	Status  int
	Type    string `json:"type"`
	Code    string `json:"code"`
	Message string `json:"message"`
}

StripeError is an error Stripe answered with.

func (*StripeError) Error

func (e *StripeError) Error() string

type Subscription

type Subscription struct {
	Owner    string   `json:"owner"`
	Plan     string   `json:"plan"`
	Status   Status   `json:"status"`
	Interval Interval `json:"interval"`
	Seats    int      `json:"seats"`
	// CurrentPeriodEnd is when it renews, or ends if CancelAtPeriodEnd.
	CurrentPeriodEnd  time.Time  `json:"currentPeriodEnd"`
	CancelAtPeriodEnd bool       `json:"cancelAtPeriodEnd"`
	TrialEnd          *time.Time `json:"trialEnd"`
	CustomerID        string     `json:"-"`
	SubscriptionID    string     `json:"-"`
	UpdatedAt         time.Time  `json:"updatedAt"`
}

Subscription is the app's copy of an owner's Stripe subscription.

func (*Subscription) Active

func (s *Subscription) Active() bool

Active reports whether the subscription's plan applies: it's paid, in its trial, or a payment failed and Stripe is still retrying.

Jump to

Keyboard shortcuts

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