billing

package
v0.51.1 Latest Latest
Warning

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

Go to latest
Published: Nov 22, 2024 License: Apache-2.0 Imports: 20 Imported by: 0

README

Billing Guide

note: If you are here to know how to bypass trial and restrictive quotas, directly jump to Avoiding trial and restrictive quotas.

For self serve billing, we use two external systems Orb and Stripe. Orb is used for managing the subscription and Stripe is used for payment processing.

Orb

Orb is used to manage the subscription lifecycle, issue invoices as per the subscribed plan and auto-collect payment using Stripe for the issued invoices. It also supports usage based billing so currently we also report project duckdb size to Orb, and it can be used for billing.

Subscription

A subscription is created when a user subscribes to a plan. It contains information about the plan, start date, end date, billing cycle etc.

Plan

A plan is an entity that defines the pricing and quotas to be used like number of projects etc. We mainly have 3 plans - Starter, Teams and Enterprise. Starter is the default plan which has 30 days of free trial and limited quotas. Teams is paid plan that user can upgrade to after adding payment details in Stripe through our billing portal. Enterprise are custom plans manually assigned.

Webhooks

Only invoice related events are handled to make sure payments are collected without any issues.

Stripe

Stripe is used for payment processing. Customer add payment method and valid billable address in Stripe through our billing portal. Orb uses this information to auto-collect payment for the issued invoices.

Payment Method

A payment method is a card that is added by the user in Stripe. It is used to pay for the invoices issued by Orb.

Billable Address

A billable address is the address of the user used for tax purposes by Orb to issue invoices.

Webhooks

Payment method and customer update related events are handled to make sure valid payment method and billable address are present.

Implementation

Each org is linked to an Orb customer and Stripe customer using billing_customer_id and payment_customer_id fields in database.

On creation of an org, a customer is created in Orb with org_id as the external customer id and a customer is created in Stripe, customer id returned from Stripe is updated in Orb customer as payment provider id.

Billing Issues

Each org can have various billing issues like never-subscribed, on-trial, trial ended, payment method missing, no billable address, payment failed, subscription cancelled etc. Each issue has relevant metadata and much of the billing logic is based on these issues.

River Jobs

River is used to run persistent background jobs for initializing billing for orgs, processing webhook events and running trial related checks and sending emails. When running through devtool river UI is available at http://localhost:7070 and can be used to see the status of the jobs.

Configuration

Following are the billing related configuration:

RILL_ADMIN_ORB_API_KEY=
RILL_ADMIN_STRIPE_API_KEY=
RILL_ADMIN_STRIPE_WEBHOOK_SECRET=
RILL_ADMIN_ORB_WEBHOOK_SECRET=

Both Orb and Stripe provides test-mode for development. Depending on the ...API_KEY admin service will connect to the test or prod env.

Cloud .env has the test-mode values for first three and devtool starts stripe cli to forward Stripe events to the admin service. RILL_ADMIN_ORB_WEBHOOK_SECRET will be commented out as there is no automated way to set up utility to listed Orb events. Orb events are anyways used for handling rare scenarios.

Manual Orb Webhook setup (optional)

If required, one can use ngrok cli to start a forwarder like this ngrok http --domain=<ngrok-provided-ip> 8080. Visit https://app.withorb.com/webhooks to add an endpoint with URL https://<ngrok-provided-ip>/billing/webhook and configure the signing secret as RILL_ADMIN_ORB_WEBHOOK_SECRET.

Usage

On new org creation there is no subscription created but few billing issues will be raised which can be viewed using rill billing list-issues.

Trial plan

On first project deployment, org will automatically moved to trial plan having 30 days and after grace period of 9 more days, all projects will be hibernated until upgraded to a paid plan. Trial plan will have limited quotas for projects, slots etc.

Num Trial quota

By default, each user across lifetime is restricted to 2 trials. This is not checked for superusers and if needed can be increased for any user using rill sudo quota set --user <email> --trial-orgs <num>

Avoiding trial and restrictive quotas

Immediately after org creation or at any time in the future, superusers can use rill sudo org set-internal-plan <org-name> to set the org to internal plan which will have unlimited quotas and no trial.

If creating org through cli you can just use rill org create <org-name> && rill sudo org set-internal-plan <org-name>.

Continuing with Trial plan and upgrade

To continue with trial plan, user can upgrade to Teams plan after entering payment details and address in billing portal. In test-mode, use any of the various test cards listed here

After fixing the issues, user can upgrade to Teams plan using cli as well rill billing subscription edit --plan Teams.

Subscription cancel and renewal

User can cancel the subscription using rill billing subscription cancel and renew it using rill billing subscription renew --plan <plan-name>.

Org deletion

On org deletion by rill org delete all the billing related data will be cleared and org will be removed from Orb and Stripe.

rill billing subscription list
rill billing subscription edit --plan <plan-name>
rill billing subscription cancel
rill billing subscription renew --plan <plan-name>

renew can only be used if subscription is cancelled. edit can only be used to change plan and not renewal.

Advanced use cases

Generally following are not allowed

  • plan downgrade (meaning moving to a plan having less quotas than org's current quotas)
  • moving to a paid plan without entering payment details and address or when there is an overdue payment
  • moving to an Orb plan which does not have public: true set in metadata. Only Starter and Teams are public plans

but can be overridden using rill billing subscription edit --plan <plan-name> --force when editing plan or rill billing subscription renew --plan <plan-name> --force when renewing.

Extending Trial

If needed, trial can be extended using rill sudo billing extend-trial --org <org> --days <num-days>.

Deleting billing issues

If needed, billing issues can be deleted using rill sudo billing delete-issue --org <org>.

Local dev and testing

rill devtool start cloud starts the admin service with Orb and Stripe integration (except for Orb webhooks, see above for manual setup). All billing related background jobs like checking for trial end, hibernation on subscription cancellation, etc. rely on end or grace end times set in respective billing issues.

These times can be artificially advanced using rill devtool subscription advance-time to test out various billing scenarios.

The command will use the default org and yesterday as due date. Pass --org or use rill org switch to change the org. Pass --time to advance due dates to a specific time.

  1. On a fresh trial plan running the above will advance time to trial's end date. To test "Ending soon" pass a time that is in the future but within 7 days.
  2. Once the trial has ended, running the command again will advance time to end the grace period.
  3. When on a cancelled team plan, running the command will advance time to end the plan end date (this is the billing cycle end date). Note that there will not be a grace period.
  4. When a invoice payment fails, running the command will end the grace period.

Documentation

Index

Constants

View Source
const (
	SupportEmail    = "support@rilldata.com"
	DefaultTimeZone = "UTC"
)

Variables

View Source
var ErrCustomerIDRequired = errors.New("customer id is required")
View Source
var ErrNotFound = errors.New("not found")

Functions

func Email added in v0.49.0

func Email(organization *database.Organization) string

Types

type Biller

type Biller interface {
	Name() string
	GetDefaultPlan(ctx context.Context) (*Plan, error)
	GetPlans(ctx context.Context) ([]*Plan, error)
	// GetPublicPlans for listing purposes
	GetPublicPlans(ctx context.Context) ([]*Plan, error)
	// GetPlan returns the plan with the given biller plan ID.
	GetPlan(ctx context.Context, id string) (*Plan, error)
	// GetPlanByName returns the plan with the given Rill plan name.
	GetPlanByName(ctx context.Context, name string) (*Plan, error)

	// CreateCustomer creates a customer for the given organization in the billing system and returns the external customer ID.
	CreateCustomer(ctx context.Context, organization *database.Organization, provider PaymentProvider) (*Customer, error)
	FindCustomer(ctx context.Context, customerID string) (*Customer, error)
	UpdateCustomerPaymentID(ctx context.Context, customerID string, provider PaymentProvider, paymentProviderID string) error
	UpdateCustomerEmail(ctx context.Context, customerID, email string) error
	DeleteCustomer(ctx context.Context, customerID string) error

	// CreateSubscription creates a subscription for the given organization. Subscription starts immediately.
	CreateSubscription(ctx context.Context, customerID string, plan *Plan) (*Subscription, error)
	// GetActiveSubscription returns the active subscription for the given organization
	GetActiveSubscription(ctx context.Context, customerID string) (*Subscription, error)
	// CancelSubscriptionsForCustomer cancels all the subscriptions for the given organization and returns the end date of the subscription
	CancelSubscriptionsForCustomer(ctx context.Context, customerID string, cancelOption SubscriptionCancellationOption) (time.Time, error)

	// ChangeSubscriptionPlan changes the plan of the given subscription immediately and returns the updated subscription
	ChangeSubscriptionPlan(ctx context.Context, subscriptionID string, plan *Plan) (*Subscription, error)
	// UnscheduleCancellation cancels the scheduled cancellation for the given subscription and returns the updated subscription
	UnscheduleCancellation(ctx context.Context, subscriptionID string) (*Subscription, error)

	GetInvoice(ctx context.Context, invoiceID string) (*Invoice, error)
	IsInvoiceValid(ctx context.Context, invoice *Invoice) bool
	IsInvoicePaid(ctx context.Context, invoice *Invoice) bool

	MarkCustomerTaxExempt(ctx context.Context, customerID string) error
	UnmarkCustomerTaxExempt(ctx context.Context, customerID string) error

	ReportUsage(ctx context.Context, usage []*Usage) error

	GetReportingGranularity() UsageReportingGranularity
	GetReportingWorkerCron() string

	// WebhookHandlerFunc returns a http.HandlerFunc that can be used to handle incoming webhooks from the payment provider. Return nil if you don't want to register any webhook handlers. jobs is used to enqueue jobs for processing the webhook events.
	WebhookHandlerFunc(ctx context.Context, jobs jobs.Client) httputil.Handler
}

func NewNoop

func NewNoop() Biller

func NewOrb

func NewOrb(logger *zap.Logger, orbKey, webhookSecret, taxProvider string) Biller

type Customer added in v0.48.0

type Customer struct {
	ID                string
	Email             string
	Name              string
	PaymentProviderID string
	PortalURL         string
}

type Invoice added in v0.50.0

type Invoice struct {
	ID             string
	Status         string
	CustomerID     string
	Amount         string
	Currency       string
	DueDate        time.Time
	CreatedAt      time.Time
	SubscriptionID string
	Metadata       map[string]interface{}
}

type Orb

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

func (*Orb) CancelSubscriptionsForCustomer

func (o *Orb) CancelSubscriptionsForCustomer(ctx context.Context, customerID string, cancelOption SubscriptionCancellationOption) (time.Time, error)

func (*Orb) ChangeSubscriptionPlan

func (o *Orb) ChangeSubscriptionPlan(ctx context.Context, subscriptionID string, plan *Plan) (*Subscription, error)

func (*Orb) CreateCustomer

func (o *Orb) CreateCustomer(ctx context.Context, organization *database.Organization, provider PaymentProvider) (*Customer, error)

func (*Orb) CreateSubscription

func (o *Orb) CreateSubscription(ctx context.Context, customerID string, plan *Plan) (*Subscription, error)

func (*Orb) DeleteCustomer added in v0.51.0

func (o *Orb) DeleteCustomer(ctx context.Context, customerID string) error

func (*Orb) FindCustomer added in v0.48.0

func (o *Orb) FindCustomer(ctx context.Context, customerID string) (*Customer, error)

func (*Orb) GetActiveSubscription added in v0.50.0

func (o *Orb) GetActiveSubscription(ctx context.Context, customerID string) (*Subscription, error)

func (*Orb) GetDefaultPlan

func (o *Orb) GetDefaultPlan(ctx context.Context) (*Plan, error)

func (*Orb) GetInvoice added in v0.50.0

func (o *Orb) GetInvoice(ctx context.Context, invoiceID string) (*Invoice, error)

func (*Orb) GetPlan

func (o *Orb) GetPlan(ctx context.Context, id string) (*Plan, error)

func (*Orb) GetPlanByName

func (o *Orb) GetPlanByName(ctx context.Context, name string) (*Plan, error)

func (*Orb) GetPlans

func (o *Orb) GetPlans(ctx context.Context) ([]*Plan, error)

func (*Orb) GetPublicPlans

func (o *Orb) GetPublicPlans(ctx context.Context) ([]*Plan, error)

func (*Orb) GetReportingGranularity

func (o *Orb) GetReportingGranularity() UsageReportingGranularity

func (*Orb) GetReportingWorkerCron

func (o *Orb) GetReportingWorkerCron() string

func (*Orb) IsInvoicePaid added in v0.50.0

func (o *Orb) IsInvoicePaid(ctx context.Context, invoice *Invoice) bool

func (*Orb) IsInvoiceValid added in v0.50.0

func (o *Orb) IsInvoiceValid(ctx context.Context, invoice *Invoice) bool

func (*Orb) MarkCustomerTaxExempt added in v0.51.0

func (o *Orb) MarkCustomerTaxExempt(ctx context.Context, customerID string) error

func (*Orb) Name

func (o *Orb) Name() string

func (*Orb) ReportUsage

func (o *Orb) ReportUsage(ctx context.Context, usage []*Usage) error

func (*Orb) UnmarkCustomerTaxExempt added in v0.51.0

func (o *Orb) UnmarkCustomerTaxExempt(ctx context.Context, customerID string) error

func (*Orb) UnscheduleCancellation added in v0.50.0

func (o *Orb) UnscheduleCancellation(ctx context.Context, subscriptionID string) (*Subscription, error)

func (*Orb) UpdateCustomerEmail added in v0.49.0

func (o *Orb) UpdateCustomerEmail(ctx context.Context, customerID, email string) error

func (*Orb) UpdateCustomerPaymentID added in v0.48.0

func (o *Orb) UpdateCustomerPaymentID(ctx context.Context, customerID string, provider PaymentProvider, paymentProviderID string) error

func (*Orb) WebhookHandlerFunc added in v0.50.0

func (o *Orb) WebhookHandlerFunc(ctx context.Context, jc jobs.Client) httputil.Handler

type PaymentProvider added in v0.48.0

type PaymentProvider string
const (
	PaymentProviderStripe PaymentProvider = "stripe"
)

type Plan

type Plan struct {
	ID              string // ID of the plan in the external billing system
	Name            string // Unique name of the plan in Rill, can be empty if biller does not support it
	PlanType        PlanType
	DisplayName     string
	Description     string
	TrialPeriodDays int
	Default         bool
	Public          bool
	Quotas          Quotas
	Metadata        map[string]string
}

type PlanType added in v0.51.1

type PlanType int
const (
	TrailPlanType PlanType = iota
	TeamPlanType
	ManagedPlanType
	EnterprisePlanType
)

type Quotas

type Quotas struct {
	StorageLimitBytesPerDeployment *int64

	// Existing quotas
	NumProjects           *int
	NumDeployments        *int
	NumSlotsTotal         *int
	NumSlotsPerDeployment *int
	NumOutstandingInvites *int
}

type Subscription

type Subscription struct {
	ID                           string
	Customer                     *Customer
	Plan                         *Plan
	StartDate                    time.Time
	EndDate                      time.Time
	CurrentBillingCycleStartDate time.Time
	CurrentBillingCycleEndDate   time.Time
	TrialEndDate                 time.Time
	Metadata                     map[string]string
}

type SubscriptionCancellationOption

type SubscriptionCancellationOption int
const (
	SubscriptionCancellationOptionEndOfSubscriptionTerm SubscriptionCancellationOption = iota
	SubscriptionCancellationOptionImmediate
)

type Usage

type Usage struct {
	CustomerID     string
	MetricName     string
	Value          float64
	ReportingGrain UsageReportingGranularity
	StartTime      time.Time // Start time of the usage period
	EndTime        time.Time // End time of the usage period
	Metadata       map[string]interface{}
}

type UsageReportingGranularity

type UsageReportingGranularity string
const (
	UsageReportingGranularityNone UsageReportingGranularity = ""
	UsageReportingGranularityHour UsageReportingGranularity = "hour"
)

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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