subscription

package
v1.10.0 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: MIT Imports: 38 Imported by: 0

README

subscription

Billing: plans, subscriptions, invoices, coupons, usage and feature entitlements.

None of the money logic lives here. It delegates to the ledger engine (github.com/xraph/ledger) and spends its own effort on the part that touches auth: hooking lifecycle events so a new organisation gets a trial without anyone clicking anything, and answering the entitlement question your feature flags need.

You choose whether a tenant is an organisation or a user, which decides who the invoice belongs to.

Use it when

  • You are charging for the product and want subscription state next to identity, not in a separate system
  • Feature gating depends on plan, so your code needs to ask "is this tenant entitled to X"
  • New signups should land on a trial automatically

Skip it when

  • You are not charging yet. This is a lot of surface area to carry for revenue you do not have
  • Your billing already lives in Stripe and you are happy querying it there
  • You have not wired the ledger engine. The plugin has nothing to delegate to without it

Wiring

authsome.WithPlugin(subscription.New(subscription.Config{
    DefaultPlanSlug:    "starter",
    AutoSubscribeOnOrg: true,
    TrialDays:          14,
})),

Config

Field Type Default What it does
PathPrefix string /v1/billing Where billing routes are mounted
DefaultPlanSlug string empty Plan auto-assigned to a new tenant
AutoSubscribeOnOrg bool false Subscribe on org creation, in organisation mode
AutoSubscribeOnUser bool false Subscribe on signup, in user mode
TrialDays int 0 Trial length for auto-subscribed tenants. 0 means no trial. Subscriptions created through the API take the plan's own trial_days
Checkout CheckoutProvider nil Takes payment before a tenant moves onto a paid plan. See Taking payment
AllowSelfService bool false Deprecated and unused. Self-service is the subscription.self_service_upgrade setting
DisableRoutes bool false Mount no HTTP routes. Set it when you serve your own billing API on top of the plugin's ledger
DisableAutoSubscribe bool false Turn off the org-create and signup hooks, whatever the settings say. Set it when you create subscriptions yourself

Settings

Key Default
subscription.default_plan empty
subscription.tenant_mode organization
subscription.auto_subscribe_org true
subscription.auto_subscribe_user false
subscription.trial_days 14
subscription.self_service_upgrade true
subscription.billing_roles ["owner"]
subscription.grace_period_days 3

billing_roles can be set per app and per organization, so an organization can let its admins manage billing without changing it for everyone else.

Self-service

These are the routes your app's own UI calls. They work on the caller's tenant, so you never pass a subscription ID, and you don't have to pass a tenant either: in organization mode it's the session's active organization, in user mode it's the user.

Method Path Purpose
GET /subscription Your subscription (with its plan), or null, plus can_manage
POST /subscription Subscribe. Body: plan (slug or ID), optional seats
PATCH /subscription Change plan, seats, or both
POST /subscription/cancel Cancel at the period end, or now with {"when":"now"}
POST /subscription/resume Take back a cancellation scheduled for the period end
GET /entitlements Every feature on your plan, checked in one call
POST /portal A billing portal URL, when your checkout provider has one

Anyone in the organization can read. To write you need self-service on (subscription.self_service_upgrade) and a role listed in subscription.billing_roles, which is just owner out of the box. GET /subscription tells you which with can_manage, so you can hide the upgrade button from people who'd get a 403.

Writes answer with the updated subscription, so you don't need to fetch it again:

{
  "subscription": {
    "id": "sub_...",
    "status": "active",
    "plan": { "slug": "team", "name": "Team", "amount_minor": 4900, "features": [] },
    "current_period_end": "2026-12-10T09:30:00Z",
    "cancel_at": null,
    "seats": { "feature_key": "seats", "used": 3, "limit": 10 }
  }
}

A cancellation at the period end leaves status at active and sets cancel_at. That's your cue to show "ends on" and a button that calls /subscription/resume. A subscription that's past_due or paused still counts as the tenant's subscription: GET shows it, and subscribing again answers 409.

Pausing is still an operator action, through the ID routes below. A tenant can't unpause itself or change the plan of a paused subscription, because a pause is often how you stop service for an unpaid invoice. Agents and other delegated sessions can read but not write.

Taking payment

Without a checkout provider, subscribing grants the plan straight away. That's fine for free plans and trials. To charge, give the plugin a CheckoutProvider. Every subscribe and every plan or seat change asks it first: return a URL to send the customer to your payment page, or an empty result when there's nothing to pay and the change should just happen.

func (s stripeCheckout) Checkout(ctx context.Context, req subscription.CheckoutRequest) (subscription.CheckoutResult, error) {
    // req.Plan is where they're going, req.CurrentPlan where they are.
    // Return CheckoutResult{} to apply now, or CheckoutResult{URL: ...} to charge first.
}

authsome.WithPlugin(subscription.New(subscription.Config{
    Checkout: stripeCheckout{},
}))

When you return a URL the API answers {"checkout_url": "..."} and changes nothing. Once your payment provider's webhook says they paid, call plugin.CompleteCheckout(ctx, req). It's safe to call twice for one payment and records the same audit entry and event as the API.

Cancel and resume never go through the provider. Listen for subscription.canceled, subscription.plan_changed and subscription.resumed and update your provider to match, or a customer who cancels in your app keeps getting charged.

If your provider also implements PortalProvider, POST /portal returns a link to its hosted page for payment details and invoices. Without one that route answers 404.

A provider error is logged and the client gets a 502, so nothing from your payment provider's error text reaches the browser.

Two subscribes for one tenant at the same moment are serialized within a process, not across replicas, because ledger has no per-tenant uniqueness yet.

Endpoints

Mounted under PathPrefix, /v1/billing by default. The self-service routes are above; these are the rest, mostly for operators.

Method Path Purpose
GET /plans List plans you can buy: active base plans, cheapest first. Operators can pass status=all, and anyone can pass include_addons=true
POST /plans Create a plan
GET /plans/:planId Read a plan
POST /plans/:planId/activate Activate a plan
POST /plans/:planId/archive Archive a plan
GET /subscriptions List subscriptions
POST /subscriptions Create a subscription
GET /subscriptions/active Read the active subscription
POST /subscriptions/:subId/change-plan Move to another plan
POST /subscriptions/:subId/pause Pause
POST /subscriptions/:subId/resume Resume
POST /subscriptions/:subId/cancel Cancel
GET /invoices List invoices
GET /invoices/:invoiceId Read an invoice
POST /invoices/:invoiceId/pay Mark paid
POST /invoices/:invoiceId/void Void
GET /entitlements/:featureKey Check a feature entitlement
GET /usage Usage summary
GET /coupons List coupons
POST /coupons Create a coupon
DELETE /coupons/:couponId Delete a coupon

Subscription, invoice, entitlement and usage reads on these routes are scoped to a tenant: pass its tenant_id. (The self-service routes above fill it in from the session instead.) You can read a tenant you belong to (organisation mode) or are (user mode). Anyone else gets a 404, whether the tenant exists or not, unless they hold billing:manage, which reads every tenant. Plans stay app-wide.

The ID routes answer a transition the ledger refuses (changing or resuming a canceled subscription, say) with a 409. You need billing:manage for GET /coupons and for the dashboard billing views as well, because both list data from the whole app. One caveat: on an engine with no RBAC, the coupon list and the write routes only ask for a session, since there is no permission to check. The tenant reads go the other way and treat a missing RBAC as not holding billing:manage.

billing:manage doesn't cross apps. When your request is scoped to an app (an API key or an app-bound session), every billing route stays inside it. An app_id naming another app gets a 404, and so does a subscription, invoice, coupon or plan from another app addressed by ID. Leave app_id off and the route uses your request's app.

To run your own billing API, set DisableRoutes and call plugin.Service() from your handlers. Pass your ledger to authsome.WithLedgerEngine and the plugin picks it up when the engine starts, so you can skip RebindLedgerOnPlugins. If you build the ledger after the engine and hand it over with SetLedgerEngine, you still need to call it.

Lifecycle hooks

Hook What happens
AfterOrgCreate Auto-subscribes the new organisation, in organisation mode
AfterSignUp Auto-subscribes the new user, in user mode
AfterMemberAdd Records seat usage against the ledger, as authsome.orgs.members

organization supplies the tenant in organisation mode. Entitlement checks pair well with whatever feature flagging you already run.

Documentation

Overview

contract.go: Wires the subscription plugin into the forge-dashboard contract surface via plugin.ContractContributor.

Package subscription provides a billing/subscription management plugin for authsome.

It delegates all billing logic to the ledger engine (github.com/xraph/ledger) and hooks into authsome lifecycle events (org creation, user signup, member changes) to automate subscription workflows. The plugin provides:

  • Dynamic settings for configurable tenant mode, trial periods, and auto-subscription
  • REST API routes for plan, subscription, invoice, and entitlement management
  • A rich dashboard UI for administrators to manage billing
  • User and organization detail section contributions

Index

Constants

This section is empty.

Variables

View Source
var (
	// SettingDefaultPlan is the plan slug to auto-assign to new tenants.
	SettingDefaultPlan = settings.Define("subscription.default_plan", "",
		settings.WithDisplayName("Default Plan"),
		settings.WithDescription("Plan slug to auto-assign to new tenants"),
		settings.WithCategory("Subscription"),
		settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp),
		settings.WithInputType(formconfig.FieldText),
		settings.WithPlaceholder("e.g. free, starter"),
		settings.WithHelpText("Leave empty to disable auto-subscription"),
		settings.WithOrder(10),
	)

	// SettingTenantMode controls whether subscriptions are org-level or user-level.
	SettingTenantMode = settings.Define("subscription.tenant_mode", "organization",
		settings.WithDisplayName("Tenant Mode"),
		settings.WithDescription("Whether subscriptions are scoped to organizations or individual users"),
		settings.WithCategory("Subscription"),
		settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp),
		settings.WithInputType(formconfig.FieldSelect),
		settings.WithOptions(
			formconfig.SelectOption{Label: "Organization", Value: "organization"},
			formconfig.SelectOption{Label: "User", Value: "user"},
		),
		settings.WithHelpText("Determines whether billing is org-level or user-level"),
		settings.WithOrder(15),
	)

	// SettingAutoSubscribeOrg controls auto-subscription for new organizations.
	SettingAutoSubscribeOrg = settings.Define("subscription.auto_subscribe_org", true,
		settings.WithDisplayName("Auto-Subscribe Organizations"),
		settings.WithDescription("Automatically create a subscription when an organization is created"),
		settings.WithCategory("Subscription"),
		settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp),
		settings.WithVisibleWhen("subscription.tenant_mode", "organization", "eq"),
		settings.WithOrder(20),
	)

	// SettingAutoSubscribeUser controls auto-subscription for new users.
	SettingAutoSubscribeUser = settings.Define("subscription.auto_subscribe_user", false,
		settings.WithDisplayName("Auto-Subscribe Users"),
		settings.WithDescription("Automatically create a subscription when a user signs up"),
		settings.WithCategory("Subscription"),
		settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp),
		settings.WithVisibleWhen("subscription.tenant_mode", "user", "eq"),
		settings.WithOrder(30),
	)

	// SettingTrialDays controls the default trial period for new subscriptions.
	SettingTrialDays = settings.Define("subscription.trial_days", 14,
		settings.WithDisplayName("Trial Period (days)"),
		settings.WithDescription("Number of trial days for new subscriptions (0 = no trial)"),
		settings.WithCategory("Subscription"),
		settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp),
		settings.WithEnforceable(),
		settings.WithInputType(formconfig.FieldNumber),
		settings.WithUIValidation(formconfig.Validation{Min: intPtr(0), Max: intPtr(365)}),
		settings.WithHelpText("Set to 0 to disable trials. Default: 14"),
		settings.WithOrder(40),
	)

	// SettingSelfServiceUpgrade controls whether tenants can change their own plan.
	SettingSelfServiceUpgrade = settings.Define("subscription.self_service_upgrade", true,
		settings.WithDisplayName("Allow Self-Service Plan Changes"),
		settings.WithDescription("Allow users/orgs to upgrade or downgrade their own plan"),
		settings.WithCategory("Subscription"),
		settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp),
		settings.WithOrder(50),
	)

	// SettingBillingRoles lists the organization member roles that may change
	// their own organization's plan through the self-service routes. It is
	// the org-level billing permission: app roles in Warden aren't scoped to
	// an organization yet, so the grant lives here, where an organization can
	// override it for itself.
	SettingBillingRoles = settings.Define("subscription.billing_roles", []string{"owner"},
		settings.WithDisplayName("Billing Roles"),
		settings.WithDescription("Organization roles that can change the organization's plan"),
		settings.WithCategory("Subscription"),
		settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp, settings.ScopeOrg),
		settings.WithInputType(formconfig.FieldTextarea),
		settings.WithPlaceholder("owner\nadmin"),
		settings.WithHelpText("One role per line. Members with any other role can see the plan but not change it. Default: owner"),
		settings.WithOrder(55),
	)

	// SettingGracePeriodDays controls the grace period after billing failure.
	SettingGracePeriodDays = settings.Define("subscription.grace_period_days", 3,
		settings.WithDisplayName("Grace Period (days)"),
		settings.WithDescription("Days after billing failure before restricting access"),
		settings.WithCategory("Subscription"),
		settings.WithScopes(settings.ScopeGlobal, settings.ScopeApp),
		settings.WithInputType(formconfig.FieldNumber),
		settings.WithUIValidation(formconfig.Validation{Min: intPtr(0), Max: intPtr(30)}),
		settings.WithHelpText("Set to 0 for immediate restriction. Default: 3"),
		settings.WithOrder(60),
	)
)
View Source
var ErrCancelDue = errors.New("subscription: the cancellation is already due")

ErrCancelDue is returned by KeepSubscription when the scheduled cancellation's time has already come.

View Source
var ErrNoScheduledCancel = errors.New("subscription: no cancellation is scheduled")

ErrNoScheduledCancel is returned by KeepSubscription when the subscription has no cancellation waiting at the end of its period.

Functions

This section is empty.

Types

type CancelCurrentRequest added in v1.10.0

type CancelCurrentRequest struct {
	When     string `json:"when,omitempty"`
	TenantID string `json:"tenant_id,omitempty"`
}

CancelCurrentRequest cancels at the period end (the default) or now.

type CancelSubscriptionRequest added in v1.9.0

type CancelSubscriptionRequest struct {
	SubID       string `path:"subId"`
	Immediately bool   `json:"immediately"`
}

type ChangePlanRequest added in v1.9.0

type ChangePlanRequest struct {
	SubID     string `path:"subId"`
	NewPlanID string `json:"new_plan_id"`
}

type CheckEntitlementRequest added in v1.9.0

type CheckEntitlementRequest struct {
	FeatureKey string `path:"featureKey"`
	AppID      string `query:"app_id,omitempty"`
	TenantID   string `query:"tenant_id"`
}

type CheckoutProvider added in v1.10.0

type CheckoutProvider interface {
	Checkout(ctx context.Context, req CheckoutRequest) (CheckoutResult, error)
}

CheckoutProvider decides whether a self-service plan change has to be paid for before it happens.

When one is configured, every subscribe and every plan or seat change goes through Checkout first. Answer with a URL to send the customer to your payment page; nothing changes until your webhook calls Plugin.CompleteCheckout. Answer with an empty URL when there's nothing to pay (a free plan, a downgrade, fewer seats) and the change is applied straight away. Cancellations and resumes never go through the provider: keep your payment provider in step by listening for the subscription.canceled, subscription.plan_changed and subscription.resumed events.

type CheckoutRequest

type CheckoutRequest struct {
	TenantID string
	AppID    string
	// Plan is the plan being moved to.
	Plan *plan.Plan
	// Current is the tenant's subscription, nil for a new one, and
	// CurrentPlan its plan.
	Current     *lsub.Subscription
	CurrentPlan *plan.Plan
	// Quantity holds seat counts by feature key, nil to keep the current ones.
	Quantity map[string]int64
	// ReturnURL and CancelURL are where the client asked to land after
	// paying or backing out. Either may be empty.
	ReturnURL string
	CancelURL string
	// UserID is who asked for the change.
	UserID id.UserID
}

CheckoutRequest describes one plan change.

type CheckoutResult added in v1.10.0

type CheckoutResult struct {
	// URL is where to send the customer to pay. Empty applies the change now.
	URL string
}

CheckoutResult is a CheckoutProvider's answer.

type Config

type Config struct {
	// PathPrefix is the HTTP path prefix for billing routes.
	// Defaults to "/v1/billing".
	PathPrefix string

	// DefaultPlanSlug is the plan slug to auto-assign to new tenants.
	// If empty, no auto-subscription occurs unless overridden by settings.
	DefaultPlanSlug string

	// AutoSubscribeOnOrg creates a subscription when an organization is created.
	// Only applies when tenant mode is "organization".
	AutoSubscribeOnOrg bool

	// AutoSubscribeOnUser creates a subscription when a user signs up.
	// Only applies when tenant mode is "user".
	AutoSubscribeOnUser bool

	// TrialDays is the default trial period for new subscriptions.
	// 0 means no trial. Can be overridden by settings.
	TrialDays int

	// AllowSelfService is unused.
	//
	// Deprecated: self-service is governed by the
	// subscription.self_service_upgrade setting (on by default), and who in an
	// organization may use it by subscription.billing_roles. This field never
	// had an effect and will be removed.
	AllowSelfService bool

	// DisableRoutes mounts no HTTP routes. Use it when the host serves its
	// own billing API on top of the plugin's ledger.
	DisableRoutes bool

	// Checkout takes payment before a tenant moves onto a paid plan through
	// the self-service routes. nil grants the plan directly. A provider that
	// also implements PortalProvider backs the billing portal route.
	Checkout CheckoutProvider

	// DisableAutoSubscribe turns off both the org-create and user-signup
	// auto-subscribe hooks, whatever the settings say. Use it when the host
	// creates subscriptions itself.
	DisableAutoSubscribe bool
}

Config configures the subscription plugin.

type CouponResponse added in v1.9.0

type CouponResponse struct {
	ID             string  `json:"id"`
	Code           string  `json:"code"`
	Name           string  `json:"name"`
	Type           string  `json:"type"`
	Amount         string  `json:"amount,omitempty"`
	Percentage     int     `json:"percentage,omitempty"`
	Currency       string  `json:"currency"`
	MaxRedemptions int     `json:"max_redemptions"`
	TimesRedeemed  int     `json:"times_redeemed"`
	ValidFrom      *string `json:"valid_from,omitempty"`
	ValidUntil     *string `json:"valid_until,omitempty"`
	AppID          string  `json:"app_id"`
}

type CreateCouponRequest added in v1.9.0

type CreateCouponRequest struct {
	Code           string  `json:"code"`
	Name           string  `json:"name"`
	Type           string  `json:"type"`
	Amount         int64   `json:"amount"`
	Percentage     int     `json:"percentage"`
	Currency       string  `json:"currency"`
	MaxRedemptions int     `json:"max_redemptions"`
	ValidFrom      *string `json:"valid_from,omitempty"`
	ValidUntil     *string `json:"valid_until,omitempty"`
	AppID          string  `json:"app_id"`
}

type CreatePlanRequest added in v1.9.0

type CreatePlanRequest struct {
	Name        string            `json:"name"`
	Slug        string            `json:"slug"`
	Description string            `json:"description"`
	Currency    string            `json:"currency"`
	TrialDays   int               `json:"trial_days"`
	BaseAmount  int64             `json:"base_amount"`
	Period      string            `json:"period"`
	AppID       string            `json:"app_id"`
	IsAddon     bool              `json:"is_addon"`
	Features    []FeatureInput    `json:"features,omitempty"`
	Metadata    map[string]string `json:"metadata,omitempty"`
}

type CreateSubscriptionRequest added in v1.9.0

type CreateSubscriptionRequest struct {
	TenantID string `json:"tenant_id"`
	PlanID   string `json:"plan_id"`
	AppID    string `json:"app_id"`
}

type CurrentSubscriptionRequest added in v1.10.0

type CurrentSubscriptionRequest struct {
	TenantID string `query:"tenant_id,omitempty"`
}

CurrentSubscriptionRequest reads the caller's tenant's subscription.

type CurrentSubscriptionResponse added in v1.10.0

type CurrentSubscriptionResponse struct {
	Subscription *SubscriptionView `json:"subscription"`
	CanManage    bool              `json:"can_manage"`
}

CurrentSubscriptionResponse carries the subscription, null when the tenant has none, and whether the caller may change it.

type DeleteCouponRequest added in v1.9.0

type DeleteCouponRequest struct {
	CouponID string `path:"couponId"`
}

type EntitlementResponse added in v1.9.0

type EntitlementResponse struct {
	Allowed   bool   `json:"allowed"`
	Feature   string `json:"feature"`
	Used      int64  `json:"used"`
	Limit     int64  `json:"limit"`
	Remaining int64  `json:"remaining"`
	Reason    string `json:"reason,omitempty"`
}

type EntitlementsResponse added in v1.10.0

type EntitlementsResponse struct {
	Entitlements map[string]EntitlementResponse `json:"entitlements"`
}

EntitlementsResponse maps each feature on the active plan to its check.

type FeatureInput added in v1.9.0

type FeatureInput struct {
	Key       string `json:"key"`
	Name      string `json:"name"`
	Type      string `json:"type"`
	Limit     int64  `json:"limit"`
	Period    string `json:"period"`
	SoftLimit bool   `json:"soft_limit"`
}

type GetActiveSubRequest added in v1.9.0

type GetActiveSubRequest struct {
	AppID    string `query:"app_id"`
	TenantID string `query:"tenant_id"`
}

type GetInvoiceRequest added in v1.9.0

type GetInvoiceRequest struct {
	InvoiceID string `path:"invoiceId"`
}

type GetPlanRequest added in v1.9.0

type GetPlanRequest struct {
	PlanID string `path:"planId"`
}

type InvoiceIDRequest added in v1.9.0

type InvoiceIDRequest struct {
	InvoiceID string `path:"invoiceId"`
}

type InvoiceResponse added in v1.9.0

type InvoiceResponse struct {
	ID             string `json:"id"`
	TenantID       string `json:"tenant_id"`
	SubscriptionID string `json:"subscription_id"`
	Status         string `json:"status"`
	Currency       string `json:"currency"`
	Total          string `json:"total"`
	PeriodStart    string `json:"period_start"`
	PeriodEnd      string `json:"period_end"`
}

type ListCouponsRequest added in v1.9.0

type ListCouponsRequest struct {
	AppID string `query:"app_id"`
}

type ListCouponsResponse added in v1.9.0

type ListCouponsResponse struct {
	Coupons []CouponResponse `json:"coupons"`
	Total   int              `json:"total"`
}

type ListEntitlementsRequest added in v1.10.0

type ListEntitlementsRequest struct {
	TenantID string `query:"tenant_id,omitempty"`
}

ListEntitlementsRequest reads every entitlement of the caller's tenant.

type ListInvoicesRequest added in v1.9.0

type ListInvoicesRequest struct {
	AppID    string `query:"app_id"`
	TenantID string `query:"tenant_id"`
}

type ListInvoicesResponse added in v1.9.0

type ListInvoicesResponse struct {
	Invoices []InvoiceResponse `json:"invoices"`
	Total    int               `json:"total"`
}

type ListPlansRequest added in v1.9.0

type ListPlansRequest struct {
	AppID string `query:"app_id"`
	// Status filters by plan status. Empty means active. "all", "archived"
	// and "draft" are for billing managers.
	Status string `query:"status,omitempty"`
	// IncludeAddons lists add-on plans as well as base plans.
	IncludeAddons bool `query:"include_addons,omitempty"`
}

type ListPlansResponse added in v1.9.0

type ListPlansResponse struct {
	Plans []PlanResponse `json:"plans"`
	Total int            `json:"total"`
}

type ListSubscriptionsRequest added in v1.9.0

type ListSubscriptionsRequest struct {
	AppID    string `query:"app_id"`
	TenantID string `query:"tenant_id"`
	Status   string `query:"status,omitempty"`
}

type ListSubscriptionsResponse added in v1.9.0

type ListSubscriptionsResponse struct {
	Subscriptions []SubscriptionResponse `json:"subscriptions"`
	Total         int                    `json:"total"`
}

type MarkInvoicePaidRequest added in v1.9.0

type MarkInvoicePaidRequest struct {
	InvoiceID  string `path:"invoiceId"`
	PaymentRef string `json:"payment_ref"`
}

type PlanFeatureResponse added in v1.10.0

type PlanFeatureResponse struct {
	Key       string `json:"key"`
	Name      string `json:"name"`
	Type      string `json:"type"`
	Limit     *int64 `json:"limit"`
	Period    string `json:"period,omitempty"`
	SoftLimit bool   `json:"soft_limit"`
}

PlanFeatureResponse is one feature a plan includes. A nil Limit means unlimited.

type PlanIDRequest added in v1.9.0

type PlanIDRequest struct {
	PlanID string `path:"planId"`
}

type PlanResponse added in v1.9.0

type PlanResponse struct {
	ID            string            `json:"id"`
	Name          string            `json:"name"`
	Slug          string            `json:"slug"`
	Description   string            `json:"description"`
	Currency      string            `json:"currency"`
	Status        string            `json:"status"`
	TrialDays     int               `json:"trial_days"`
	BaseAmount    string            `json:"base_amount,omitempty"`
	BillingPeriod string            `json:"billing_period,omitempty"`
	FeaturesCount int               `json:"features_count"`
	IsAddon       bool              `json:"is_addon"`
	AppID         string            `json:"app_id"`
	Metadata      map[string]string `json:"metadata,omitempty"`
	// AmountMinor is the base price in minor units (cents for usd), for
	// arithmetic and locale formatting. BaseAmount is the same price
	// formatted in major units.
	AmountMinor *int64                `json:"amount_minor,omitempty"`
	Features    []PlanFeatureResponse `json:"features"`
}

type Plugin

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

Plugin is the subscription management plugin for authsome.

func New added in v1.9.0

func New(cfg ...Config) *Plugin

New creates a new subscription plugin with the given configuration.

func (*Plugin) CompleteCheckout added in v1.10.0

func (p *Plugin) CompleteCheckout(ctx context.Context, req CheckoutRequest) (*lsub.Subscription, error)

CompleteCheckout applies a plan change once its payment has landed. Call it from your payment provider's webhook with the CheckoutRequest you got in Checkout (keep what you need from it in the checkout session's metadata).

It is safe to call more than once for the same payment: when the tenant is already on that plan with those seats, it changes nothing and returns the subscription. It moves the tenant's current subscription when there is one, even if a different one was current at checkout, and records the same audit entry and event the self-service routes do.

func (*Plugin) DeclareSettings added in v1.9.0

func (p *Plugin) DeclareSettings(m *settings.Manager) error

DeclareSettings implements plugin.SettingsProvider.

func (*Plugin) Name added in v1.9.0

func (p *Plugin) Name() string

Name returns the plugin name.

func (*Plugin) OnAfterMemberAdd added in v1.9.0

func (p *Plugin) OnAfterMemberAdd(ctx context.Context, _ *organization.Member) error

OnAfterMemberAdd records seat usage when a member is added to an organization.

func (*Plugin) OnAfterOrgCreate added in v1.9.0

func (p *Plugin) OnAfterOrgCreate(ctx context.Context, o *organization.Organization) error

OnAfterOrgCreate auto-creates a subscription for new organizations when tenant mode is "organization" and auto-subscribe is enabled.

func (*Plugin) OnAfterOrgDelete added in v1.9.0

func (p *Plugin) OnAfterOrgDelete(ctx context.Context, orgID id.OrgID) error

OnAfterOrgDelete cancels every subscription whose tenant is the deleted org so the ledger doesn't keep billing a tenant that no longer exists. Cancellation is best-effort: failures are logged and the hook always returns nil so the org-delete pipeline keeps draining other plugins.

func (*Plugin) OnAfterSignUp added in v1.9.0

func (p *Plugin) OnAfterSignUp(ctx context.Context, u *user.User, _ *session.Session) error

OnAfterSignUp auto-creates a subscription for new users when tenant mode is "user" and auto-subscribe is enabled.

func (*Plugin) OnInit added in v1.9.0

func (p *Plugin) OnInit(_ context.Context, engine plugin.Engine) error

OnInit captures bridge and engine references.

func (*Plugin) RegisterContract added in v1.9.0

func (p *Plugin) RegisterContract(
	d *dispatcher.Dispatcher,
	reg contract.Registry,
	wreg contract.WardenRegistry,
	engine plugin.Engine,
) error

func (*Plugin) RegisterRoutes

func (p *Plugin) RegisterRoutes(router forge.Router) error

RegisterRoutes registers billing API routes on a forge.Router.

func (*Plugin) Service added in v1.9.0

func (p *Plugin) Service() *Service

Service returns the subscription service (nil until OnInit completes).

func (*Plugin) SetCheckoutProvider added in v1.10.0

func (p *Plugin) SetCheckoutProvider(cp CheckoutProvider)

SetCheckoutProvider sets the checkout provider after construction. Call it before the engine starts serving; nil turns checkout off.

func (*Plugin) SetLedger added in v1.9.0

func (p *Plugin) SetLedger(l *ledger.Ledger)

SetLedger sets the ledger engine directly, bypassing duck-type discovery.

func (*Plugin) SetLedgerStore added in v1.9.0

func (p *Plugin) SetLedgerStore(st ledgerstore.Store)

SetLedgerStore sets the ledger store directly, bypassing duck-type discovery.

type PortalProvider added in v1.10.0

type PortalProvider interface {
	Portal(ctx context.Context, tenantID, appID, returnURL string) (url string, err error)
}

PortalProvider is optional. A CheckoutProvider that also implements it backs POST /v1/billing/portal: a hosted page where customers update their payment details and see their invoices.

type PortalRequest added in v1.10.0

type PortalRequest struct {
	ReturnURL string `json:"return_url,omitempty"`
	TenantID  string `json:"tenant_id,omitempty"`
}

PortalRequest opens the billing portal for the caller's tenant.

type PortalResponse added in v1.10.0

type PortalResponse struct {
	URL string `json:"url"`
}

PortalResponse carries the portal URL to send the customer to.

type SeatsView added in v1.10.0

type SeatsView struct {
	FeatureKey string `json:"feature_key"`
	Used       int64  `json:"used"`
	Limit      *int64 `json:"limit"`
}

SeatsView reports seat use for a plan with a seat feature. Used counts the organization's members; Limit is the bought quantity, or the plan's limit when none was bought, and nil when unlimited.

type Service added in v1.9.0

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

Service wraps ledger operations with AuthSome context.

func (*Service) ActivatePlan added in v1.9.0

func (s *Service) ActivatePlan(ctx context.Context, planID ledgerid.PlanID) error

ActivatePlan transitions a draft plan to active status.

func (*Service) ArchiveCatalogFeature added in v1.9.0

func (s *Service) ArchiveCatalogFeature(ctx context.Context, featureID ledgerid.FeatureID) error

ArchiveCatalogFeature archives a catalog feature.

func (*Service) ArchivePlan added in v1.9.0

func (s *Service) ArchivePlan(ctx context.Context, planID ledgerid.PlanID) error

ArchivePlan archives a plan by ID.

func (*Service) CalculateMRR added in v1.9.0

func (s *Service) CalculateMRR(ctx context.Context, appID string) string

CalculateMRR estimates monthly recurring revenue across all active subscriptions.

func (*Service) CancelSubscription added in v1.9.0

func (s *Service) CancelSubscription(ctx context.Context, subID ledgerid.SubscriptionID, immediately bool) error

CancelSubscription cancels a subscription, now or at the end of its period.

func (*Service) ChangePlan added in v1.9.0

func (s *Service) ChangePlan(ctx context.Context, subID ledgerid.SubscriptionID, newPlanID ledgerid.PlanID, quantity map[string]int64) (*subscription.Subscription, error)

ChangePlan moves a subscription to another plan through ledger, which refuses a canceled or expired subscription and a plan outside its app. A nil quantity keeps the current seat counts.

func (*Service) CheckEntitlement added in v1.9.0

func (s *Service) CheckEntitlement(ctx context.Context, featureKey string) (*entitlement.Result, error)

CheckEntitlement checks whether a tenant can use a feature.

func (*Service) CountSubscribers added in v1.9.0

func (s *Service) CountSubscribers(ctx context.Context, planID ledgerid.PlanID, appID string) int

CountSubscribers counts active subscriptions for a plan.

func (*Service) CreateCatalogFeature added in v1.9.0

func (s *Service) CreateCatalogFeature(ctx context.Context, f *feature.Feature) error

CreateCatalogFeature creates a new catalog feature.

func (*Service) CreateCoupon added in v1.9.0

func (s *Service) CreateCoupon(ctx context.Context, c *coupon.Coupon) error

CreateCoupon creates a new coupon.

func (*Service) CreatePlan added in v1.9.0

func (s *Service) CreatePlan(ctx context.Context, p *plan.Plan) error

CreatePlan creates a new billing plan.

func (*Service) CurrentSubscription added in v1.10.0

func (s *Service) CurrentSubscription(ctx context.Context, tenantID, appID string) (*subscription.Subscription, error)

CurrentSubscription returns the tenant's newest subscription that hasn't ended (active, trialing, past_due or paused), or nil when it has none.

func (*Service) DeleteCatalogFeature added in v1.9.0

func (s *Service) DeleteCatalogFeature(ctx context.Context, featureID ledgerid.FeatureID) error

DeleteCatalogFeature deletes a catalog feature.

func (*Service) DeleteCoupon added in v1.9.0

func (s *Service) DeleteCoupon(ctx context.Context, couponID ledgerid.CouponID) error

DeleteCoupon deletes a coupon.

func (*Service) GenerateInvoice added in v1.9.0

func (s *Service) GenerateInvoice(ctx context.Context, subID ledgerid.SubscriptionID) (*invoice.Invoice, error)

GenerateInvoice generates an invoice for the current billing period.

func (*Service) GetActiveSubscription added in v1.9.0

func (s *Service) GetActiveSubscription(ctx context.Context, tenantID, appID string) (*subscription.Subscription, error)

GetActiveSubscription retrieves the active subscription for a tenant.

func (*Service) GetCatalogFeature added in v1.9.0

func (s *Service) GetCatalogFeature(ctx context.Context, featureID ledgerid.FeatureID) (*feature.Feature, error)

GetCatalogFeature retrieves a catalog feature by ID.

func (*Service) GetCatalogFeatureByKey added in v1.9.0

func (s *Service) GetCatalogFeatureByKey(ctx context.Context, key, appID string) (*feature.Feature, error)

GetCatalogFeatureByKey retrieves a catalog feature by key.

func (*Service) GetCoupon added in v1.9.0

func (s *Service) GetCoupon(ctx context.Context, code, appID string) (*coupon.Coupon, error)

GetCoupon retrieves a coupon by code.

func (*Service) GetCouponByID added in v1.9.1

func (s *Service) GetCouponByID(ctx context.Context, couponID ledgerid.CouponID) (*coupon.Coupon, error)

GetCouponByID retrieves a coupon by ID.

func (*Service) GetInvoice added in v1.9.0

func (s *Service) GetInvoice(ctx context.Context, invID ledgerid.InvoiceID) (*invoice.Invoice, error)

GetInvoice retrieves an invoice by ID.

func (*Service) GetPlan added in v1.9.0

func (s *Service) GetPlan(ctx context.Context, planID ledgerid.PlanID) (*plan.Plan, error)

GetPlan retrieves a plan by ID.

func (*Service) GetPlanBySlug added in v1.10.0

func (s *Service) GetPlanBySlug(ctx context.Context, slug, appID string) (*plan.Plan, error)

GetPlanBySlug retrieves a plan by its slug within an app.

func (*Service) GetSubscription added in v1.9.0

func (s *Service) GetSubscription(ctx context.Context, subID ledgerid.SubscriptionID) (*subscription.Subscription, error)

GetSubscription retrieves a subscription by ID.

func (*Service) GetUsageSummary added in v1.9.0

func (s *Service) GetUsageSummary(ctx context.Context, tenantID, appID string) ([]UsageSummary, error)

GetUsageSummary returns usage breakdown for the active subscription.

func (*Service) KeepSubscription added in v1.10.0

func (s *Service) KeepSubscription(ctx context.Context, subID ledgerid.SubscriptionID) (*subscription.Subscription, error)

KeepSubscription takes back a cancellation scheduled for the end of the period, so the subscription renews as usual. A subscription that has already ended can't be kept.

func (*Service) ListAllInvoices added in v1.9.0

func (s *Service) ListAllInvoices(ctx context.Context, appID string) ([]*invoice.Invoice, error)

ListAllInvoices lists all invoices for an app.

func (*Service) ListCatalogFeatures added in v1.9.0

func (s *Service) ListCatalogFeatures(ctx context.Context, appID string) ([]*feature.Feature, error)

ListCatalogFeatures lists all catalog features for an app.

func (*Service) ListCoupons added in v1.9.0

func (s *Service) ListCoupons(ctx context.Context, appID string) ([]*coupon.Coupon, error)

ListCoupons lists all coupons for an app.

func (*Service) ListInvoices added in v1.9.0

func (s *Service) ListInvoices(ctx context.Context, tenantID, appID string) ([]*invoice.Invoice, error)

ListInvoices lists invoices for a tenant.

func (*Service) ListPlans added in v1.9.0

func (s *Service) ListPlans(ctx context.Context, appID string) ([]*plan.Plan, error)

ListPlans returns all plans for the given app.

func (*Service) ListPlansWithOpts added in v1.10.0

func (s *Service) ListPlansWithOpts(ctx context.Context, appID string, opts plan.ListOpts) ([]*plan.Plan, error)

ListPlansWithOpts returns the app's plans filtered by opts.

func (*Service) ListSubscriptions added in v1.9.0

func (s *Service) ListSubscriptions(ctx context.Context, tenantID, appID string, opts subscription.ListOpts) ([]*subscription.Subscription, error)

ListSubscriptions lists subscriptions for a tenant.

func (*Service) MarkInvoicePaid added in v1.9.0

func (s *Service) MarkInvoicePaid(ctx context.Context, invID ledgerid.InvoiceID, paymentRef string) error

MarkInvoicePaid marks an invoice as paid.

func (*Service) MarkInvoiceVoided added in v1.9.0

func (s *Service) MarkInvoiceVoided(ctx context.Context, invID ledgerid.InvoiceID, reason string) error

MarkInvoiceVoided voids an invoice.

func (*Service) PauseSubscription added in v1.9.0

func (s *Service) PauseSubscription(ctx context.Context, subID ledgerid.SubscriptionID) (*subscription.Subscription, error)

PauseSubscription pauses an active or trialing subscription.

func (*Service) ResumeSubscription added in v1.9.0

func (s *Service) ResumeSubscription(ctx context.Context, subID ledgerid.SubscriptionID) (*subscription.Subscription, error)

ResumeSubscription resumes a paused subscription. Ledger stretches the period and any running trial by the length of the pause, and leaves a subscription that ended while paused alone.

func (*Service) SetLedgerStore added in v1.9.0

func (s *Service) SetLedgerStore(st ledgerstore.Store)

SetLedgerStore sets the ledger store directly, bypassing duck-type discovery.

func (*Service) Subscribe added in v1.9.0

func (s *Service) Subscribe(ctx context.Context, tenantID string, planID ledgerid.PlanID, appID string, quantity map[string]int64) (*subscription.Subscription, error)

Subscribe creates a new subscription for a tenant on a plan. Ledger picks the trial from the plan's trial_days and the first period from the plan's billing period. quantity sets seat counts for the plan's seat features and may be nil.

func (*Service) UpdateCatalogFeature added in v1.9.0

func (s *Service) UpdateCatalogFeature(ctx context.Context, f *feature.Feature) error

UpdateCatalogFeature updates a catalog feature.

func (*Service) UpdatePlan added in v1.9.0

func (s *Service) UpdatePlan(ctx context.Context, p *plan.Plan) error

UpdatePlan updates an existing plan.

type SubIDRequest added in v1.9.0

type SubIDRequest struct {
	SubID string `path:"subId"`
}

type SubscribeRequest added in v1.10.0

type SubscribeRequest struct {
	Plan      string `json:"plan"`
	Seats     *int64 `json:"seats,omitempty"`
	ReturnURL string `json:"return_url,omitempty"`
	CancelURL string `json:"cancel_url,omitempty"`
	TenantID  string `json:"tenant_id,omitempty"`
}

SubscribeRequest starts a subscription. Plan is a plan slug or ID.

type SubscriptionMutationResponse added in v1.10.0

type SubscriptionMutationResponse struct {
	Subscription *SubscriptionView `json:"subscription,omitempty"`
	CheckoutURL  string            `json:"checkout_url,omitempty"`
}

SubscriptionMutationResponse holds the updated subscription, or a checkout URL when a checkout provider takes payment first. Exactly one is set.

type SubscriptionResponse added in v1.10.0

type SubscriptionResponse struct {
	ID                 string  `json:"id"`
	TenantID           string  `json:"tenant_id"`
	PlanID             string  `json:"plan_id"`
	Status             string  `json:"status"`
	CurrentPeriodStart string  `json:"current_period_start"`
	CurrentPeriodEnd   string  `json:"current_period_end"`
	TrialStart         *string `json:"trial_start,omitempty"`
	TrialEnd           *string `json:"trial_end,omitempty"`
	CanceledAt         *string `json:"canceled_at,omitempty"`
	AppID              string  `json:"app_id"`
	// CancelAt is when a cancellation scheduled for the period end takes
	// effect. CanceledAt stays empty until it does.
	CancelAt *string `json:"cancel_at,omitempty"`
	PausedAt *string `json:"paused_at,omitempty"`
}

type SubscriptionView added in v1.10.0

type SubscriptionView struct {
	ID                 string       `json:"id"`
	TenantID           string       `json:"tenant_id"`
	Status             string       `json:"status"`
	Plan               PlanResponse `json:"plan"`
	CurrentPeriodStart string       `json:"current_period_start"`
	CurrentPeriodEnd   string       `json:"current_period_end"`
	TrialStart         *string      `json:"trial_start"`
	TrialEnd           *string      `json:"trial_end"`
	// CancelAt is when a cancellation scheduled for the period end takes
	// effect; the subscription stays usable until then.
	CancelAt   *string    `json:"cancel_at"`
	CanceledAt *string    `json:"canceled_at"`
	PausedAt   *string    `json:"paused_at"`
	Seats      *SeatsView `json:"seats,omitempty"`
	AppID      string     `json:"app_id"`
}

SubscriptionView is the tenant-facing shape of a subscription: everything a plan card needs in one payload, with the plan embedded and every time in RFC 3339 UTC.

type TenantRequest added in v1.10.0

type TenantRequest struct {
	TenantID string `json:"tenant_id,omitempty"`
}

TenantRequest names nothing but an optional tenant override.

type UpdateSubscriptionRequest added in v1.10.0

type UpdateSubscriptionRequest struct {
	Plan      string `json:"plan,omitempty"`
	Seats     *int64 `json:"seats,omitempty"`
	ReturnURL string `json:"return_url,omitempty"`
	CancelURL string `json:"cancel_url,omitempty"`
	TenantID  string `json:"tenant_id,omitempty"`
}

UpdateSubscriptionRequest changes the plan, the seat count, or both.

type UsageItemResponse added in v1.9.0

type UsageItemResponse struct {
	FeatureKey  string `json:"feature_key"`
	FeatureName string `json:"feature_name"`
	FeatureType string `json:"feature_type"`
	Used        int64  `json:"used"`
	Limit       int64  `json:"limit"`
	Remaining   int64  `json:"remaining"`
	Period      string `json:"period"`
}

type UsageSummary added in v1.9.0

type UsageSummary struct {
	FeatureKey  string
	FeatureName string
	FeatureType string
	Used        int64
	Limit       int64
	Remaining   int64
	Period      string
}

UsageSummary holds feature usage data for display.

type UsageSummaryRequest added in v1.9.0

type UsageSummaryRequest struct {
	AppID    string `query:"app_id"`
	TenantID string `query:"tenant_id"`
}

type UsageSummaryResponse added in v1.9.0

type UsageSummaryResponse struct {
	Usage []UsageItemResponse `json:"usage"`
}

type VoidInvoiceRequest added in v1.9.0

type VoidInvoiceRequest struct {
	InvoiceID string `path:"invoiceId"`
	Reason    string `json:"reason"`
}

Directories

Path Synopsis
Package contract wires the subscription plugin's intent surface into the forge-dashboard contract registry.
Package contract wires the subscription plugin's intent surface into the forge-dashboard contract registry.

Jump to

Keyboard shortcuts

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