stripe

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: 15 Imported by: 0

Documentation

Overview

Package stripe is a Stripe payment provider for Ledger.

Ledger computes plans, prices and invoices; Stripe only collects. This provider keeps no plans, prices or subscriptions on the Stripe side. It holds one Stripe customer per tenant and the cards saved to it, captured through Checkout in setup mode, and copies each finished Ledger invoice into a Stripe invoice that charges the tenant's default card.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnsupported is returned by the Import methods: Ledger owns plans,
	// features, subscriptions and invoices, so there is nothing to pull back
	// from Stripe.
	ErrUnsupported = errors.New("stripe: operation not supported")

	// ErrNoCustomer is returned when a tenant has no Stripe customer yet.
	// Call EnsureCustomer first.
	ErrNoCustomer = errors.New("stripe: tenant has no stripe customer")

	// ErrMissingTenant is returned when a call is made without a tenant id.
	ErrMissingTenant = errors.New("stripe: tenant id is required")

	// ErrNotPaid is returned by RefundInvoice when the Stripe invoice has no
	// paid payment to refund.
	ErrNotPaid = errors.New("stripe: invoice has no paid payment")
)

Functions

func NewPlugin

NewPlugin wraps p so it can be passed to ledger.WithPlugin. It panics if p is nil: build p with New and check its error first.

Types

type Config

type Config struct {
	// SecretKey is the Stripe secret API key. It is never logged or put in
	// an error.
	SecretKey string
	// WebhookSecret is the signing secret for Stripe webhook events.
	WebhookSecret string
	// Customers maps Ledger tenants to Stripe customer ids.
	Customers CustomerStore
	// Currency is the ISO currency code sent on setup-mode Checkout
	// sessions. Empty means "usd"; it is lowercased.
	Currency string
	// contains filtered or unexported fields
}

Config configures a Provider.

type CustomerStore

type CustomerStore interface {
	// CustomerID returns the tenant's Stripe customer id, or "" when it has
	// none.
	CustomerID(ctx context.Context, tenantID string) (string, error)
	// SetCustomerID records the tenant's Stripe customer id.
	SetCustomerID(ctx context.Context, tenantID, customerID string) error
}

CustomerStore maps Ledger tenants to Stripe customer ids. The host app persists the mapping.

type Provider

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

Provider is a provider.Provider backed by Stripe.

func New

func New(cfg Config) (*Provider, error)

New builds a Provider. SecretKey, WebhookSecret and Customers are required.

func (*Provider) CreateSetupSession

func (p *Provider) CreateSetupSession(ctx context.Context, tenantID, successURL, cancelURL string) (string, error)

CreateSetupSession opens a Checkout session in setup mode so the tenant can save a card, and returns the URL to send them to. The tenant must already have a customer (see EnsureCustomer); otherwise it returns ErrNoCustomer.

func (*Provider) EnsureCustomer

func (p *Provider) EnsureCustomer(ctx context.Context, tenantID, email, name string) (string, error)

EnsureCustomer returns the tenant's Stripe customer id, creating the customer on first call. Creation sends the idempotency key "customer:<tenantID>". Stripe only honours that key for about 24 hours and only for identical parameters: a retry inside the window with the same email and name gets the same customer back, a retry with a different email or name fails with Stripe's idempotency_error, and a retry after the window creates a second customer.

func (*Provider) HandleWebhook

func (p *Provider) HandleWebhook(ctx context.Context, payload []byte) (*provider.WebhookResult, error)

HandleWebhook maps a Stripe event onto Ledger. It assumes Verify has already passed for payload and does not check the signature again.

invoice.paid and invoice.payment_failed report the Ledger invoice named in the Stripe invoice's metadata. setup_intent.succeeded and a setup-mode checkout.session.completed make the saved card the customer's default for invoices and report the tenant. Any other event, or one that does not carry Ledger metadata, comes back with Handled false and no error.

Handling the same event twice gives the same result, and an event for a card older than the customer's current default leaves that default alone.

func (*Provider) ImportFeature

func (p *Provider) ImportFeature(context.Context, string) (*feature.Feature, error)

ImportFeature returns ErrUnsupported.

func (*Provider) ImportInvoice

func (p *Provider) ImportInvoice(context.Context, string) (*invoice.Invoice, error)

ImportInvoice returns ErrUnsupported.

func (*Provider) ImportPlan

func (p *Provider) ImportPlan(context.Context, string) (*plan.Plan, error)

ImportPlan returns ErrUnsupported.

func (*Provider) ImportSubscription

func (p *Provider) ImportSubscription(context.Context, string) (*subscription.Subscription, error)

ImportSubscription returns ErrUnsupported.

func (*Provider) ListPaymentMethods

func (p *Provider) ListPaymentMethods(ctx context.Context, tenantID string) ([]provider.PaymentMethod, error)

ListPaymentMethods returns the tenant's saved cards, live from Stripe. The card set as the customer's invoice default is marked IsDefault. A tenant with no customer has no cards.

func (*Provider) Name

func (p *Provider) Name() string

Name returns "stripe".

func (*Provider) RefundInvoice

func (p *Provider) RefundInvoice(ctx context.Context, stripeInvoiceID string, amount *types.Money, refundKey string) (bool, error)

RefundInvoice refunds the payment that paid a Stripe invoice. A nil amount refunds whatever is left of the payment; otherwise amount, in minor units of the payment's currency, is refunded. full reports whether the payment is now refunded in total. An invoice paid by more than one payment is refused, since it is not clear which one to refund.

refundKey identifies one refund intent and is required. The caller makes a new key for each refund it means to issue and reuses that key when it retries, so a retry after a lost response does not refund twice. The refund is sent with the idempotency key "refund:<stripe invoice>:<key>". Stripe caps idempotency keys at 255 characters, so refundKey may be at most 200, which leaves room for the prefix.

func (*Provider) SyncFeature

func (p *Provider) SyncFeature(context.Context, *feature.Feature) (string, error)

SyncFeature does nothing: Ledger owns features and Stripe never sees them.

func (*Provider) SyncInvoice

func (p *Provider) SyncInvoice(ctx context.Context, inv *invoice.Invoice) (string, error)

SyncInvoice copies a Ledger invoice to Stripe and finalizes it, so Stripe charges the tenant's default card. It returns the Stripe invoice id.

It creates a draft Stripe invoice with auto-advance off, then one invoice item per Ledger line, each naming the draft: in this API version a new invoice does not pull in the customer's pending items. Credit and discount lines go across as negative items. Only the finalize call turns auto-advance on, so Stripe never collects on a draft that is missing lines. If adding a line fails, the draft is deleted before the error is returned.

Only a pending or past-due invoice is sent; any other status is an error and Stripe is not called. An invoice that already names a Stripe invoice (ProviderName "stripe" and a ProviderID) returns that id, again without calling Stripe.

The lines must sum to the invoice total, or nothing is sent. A zero-total invoice is not sent and returns ("", nil); Ledger's SyncInvoiceToProvider marks such an invoice paid without calling SyncInvoice at all. A negative total is an error.

Before creating anything it looks through the customer's 20 most recent Stripe invoices for one carrying this Ledger invoice id. An open, paid or uncollectible one was already sent, and its id is returned with nothing created, so a retry after a lost response does not bill twice. A draft one is a leftover from a failed attempt and is deleted. Void ones are ignored.

The draft is created without an idempotency key of ours (stripe-go still keys its own network retries), so a retry after a failure starts a fresh draft instead of replaying a deleted one. The items and the finalize call are keyed on the draft: "invoice:<id>:<draft id>:<line id>" and "invoice:<id>:<draft id>:finalize". The lookup only reaches 20 invoices back, so the caller should still keep the returned id and not sync an invoice that already has one.

func (*Provider) SyncPlan

func (p *Provider) SyncPlan(context.Context, *plan.Plan) (string, error)

SyncPlan does nothing: Ledger owns plans and Stripe never sees them.

func (*Provider) SyncSubscription

func (p *Provider) SyncSubscription(ctx context.Context, s *subscription.Subscription) (string, error)

SyncSubscription makes sure the subscription's tenant has a Stripe customer and returns the customer id. Stripe never holds the subscription itself.

func (*Provider) Verify

func (p *Provider) Verify(payload []byte, signatureHeader string) error

Verify checks a webhook payload against its Stripe-Signature header using the configured webhook secret. It rejects a missing or wrong signature, a signature older than five minutes, and an event from another release train. stripe-go compares only the train, the part after the dot, so any "*.endive" version passes while a "*.dahlia" one or an undotted pre-train version is refused. Call it before HandleWebhook.

Jump to

Keyboard shortcuts

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