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 ¶
- Variables
- func FormError(c *kit.Ctx, err error) error
- func HTTPError(c *kit.Ctx, err error) error
- func Team(id int64) string
- func User(id int64) string
- func VerifySignature(payload []byte, header, secret string, now time.Time) error
- type Billing
- func (b *Billing) Check(ctx context.Context, owner, limit string, used int) error
- func (b *Billing) Checkout(c *kit.Ctx, o CheckoutOptions) (string, error)
- func (b *Billing) Current(ctx context.Context, owner string) (Plan, *Subscription, error)
- func (b *Billing) Has(ctx context.Context, owner, feature string) (bool, error)
- func (b *Billing) Limit(ctx context.Context, owner, limit string) (int, bool, error)
- func (b *Billing) Migrate(ctx context.Context) error
- func (*Billing) Name() string
- func (b *Billing) Plan(id string) (Plan, bool)
- func (b *Billing) Plans() []Plan
- func (b *Billing) Portal(c *kit.Ctx, owner, returnURL string) (string, error)
- func (b *Billing) SetSeats(ctx context.Context, owner string, seats int) error
- func (b *Billing) Setup(s *kit.Setup) error
- func (b *Billing) Subscription(ctx context.Context, owner string) (*Subscription, error)
- func (b *Billing) SyncSubscription(ctx context.Context, id string) error
- type CheckoutOptions
- type Interval
- type LimitError
- type Options
- type Plan
- type Status
- type StripeError
- type Subscription
Constants ¶
This section is empty.
Variables ¶
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.
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.
var ErrSignature = errors.New("billing: the webhook's signature is invalid")
ErrSignature is a webhook whose signature doesn't check out.
Functions ¶
func FormError ¶
FormError turns a *LimitError into a form error, in the user's language, for an action to return. Other errors stay as they are.
Types ¶
type Billing ¶
type Billing struct {
// contains filtered or unexported fields
}
Billing is the billing plugin. Create it with New.
func (*Billing) Check ¶
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 ¶
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 ¶
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) Portal ¶
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 ¶
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) Subscription ¶
Subscription returns the owner's subscription, active or not, or nil if they never had one.
func (*Billing) SyncSubscription ¶
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 LimitError ¶
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.
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.