Help us shape the future of Go! Take the 2026 Go Developer Survey

billingmail

package
v0.40.22 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package billingmail sends Rivolt Pro's transactional billing emails for Stripe subscriptions: the purchase confirmation (the renewal terms and how to cancel, which Stripe's own receipt does not carry), the payment-failed notice during the 7-day grace (when it ends, how to update the card), and the source of the yearly-renewal reminder (internal/launchnotice).

They are service emails: they go out with engagement emails off, outside the engagement budget, to an account's verified address. Each is sent once per event: a row in account_notices per (account, kind) is claimed, sent and marked (the same machinery as the launch notice), the kind carrying what the email is about (the subscription id, the failed period), so a replay, a second replica or a restart cannot send it twice and a failed send is retried by the next run. The job reads the state the Stripe webhook wrote (entitlements and billing_stripe_subs), so a webhook that succeeded but whose email failed is caught up; nothing here calls Stripe.

Links go to the fixed Plan card (launchnotice.PlanURL), which opens the billing portal after sign-in: never a pre-made portal session URL, which would work as a bearer link to the customer's billing page if the email were forwarded.

Index

Constants

Kinds, for the metric (rivolt_dated_notices_total{kind,outcome}) and the notice rows.

View Source
const RecentFor = 7 * 24 * time.Hour

RecentFor is how far back a subscription event still triggers a purchase confirmation: a subscription last touched longer ago than this is older than the job (or a long outage) and is not announced as new.

Variables

This section is empty.

Functions

func Amount

func Amount(amount int64, currency string) string

Amount is "$34.99".

func FailureKind

func FailureKind(periodEnd time.Time) string

func FailureSubject

func FailureSubject(FailureParams) string

FailureSubject is the subject of the payment-failed notice.

func PriceLabel

func PriceLabel(amount int64, currency, interval string) string

PriceLabel is "$34.99 a year".

func PurchaseKind

func PurchaseKind(subscription string) string

PurchaseKind and FailureKind are the notice kinds: one confirmation per subscription, one failed-payment notice per failed period.

A notice kind is at most 40 characters of [a-z0-9_] (account_notices' CHECK), and a real Stripe id is ~28 characters of MIXED case ("sub_1MowQVLkdIwHu7ix..."), so the subscription is named by the first 16 hex of its sha256: deterministic, lower case, 33 characters.

func PurchaseSubject

func PurchaseSubject(p PurchaseParams) string

PurchaseSubject is the subject of the purchase confirmation. While the free period runs it names the first charge, so the two confirmations (this one and the receipt of a later charge) can be told apart in an inbox.

func RenderFailure

func RenderFailure(p FailureParams) string

RenderFailure is the payment-failed notice: when the grace ends and how to update the card.

func RenderPurchase

func RenderPurchase(p PurchaseParams) string

RenderPurchase is the purchase confirmation: the renewal terms, how to cancel and the refund rule (Stripe's receipt has none of them). Wording by growth. While the subscription is trialing the free period is the founding year or a grant, so the line does not name which.

Types

type Failure

type Failure struct {
	UserID    uuid.UUID
	Email     string
	PeriodEnd time.Time // the period that ended unpaid; grace runs from here
}

Failure is a renewal that failed and is in its grace.

type FailureParams

type FailureParams struct {
	GraceEnds string `json:"grace_ends"` // YYYY-MM-DD
}

type Job

type Job struct {
	DB      *sql.DB // the system pool
	Mailer  Mailer
	Now     func() time.Time
	Logger  *slog.Logger
	Skip    map[uuid.UUID]bool // the demo and bypass accounts
	Observe func(kind, outcome string)
}

Job sends the purchase confirmations and payment-failed notices.

func (*Job) RunOnce

func (j *Job) RunOnce(ctx context.Context) (Stats, error)

RunOnce sends what is due. Each send is claimed first; a failed send releases its claim for the next run.

type Mailer

type Mailer interface {
	Send(ctx context.Context, m email.Message) error
}

Mailer is what the job sends through.

type Purchase

type Purchase struct {
	UserID       uuid.UUID
	Email        string
	Subscription string
	PeriodEnd    time.Time // the next renewal, or the first charge while trialing
	Interval     string    // month or year
	Amount       int64     // minor units
	Currency     string
	Trialing     bool
}

Purchase is a subscription to confirm.

type PurchaseParams

type PurchaseParams struct {
	Interval string `json:"interval"`
	Amount   string `json:"amount"` // "$34.99"
	Price    string `json:"price"`  // "$34.99 a year"
	Date     string `json:"date"`   // YYYY-MM-DD: the renewal, or the first charge
	Trialing bool   `json:"trialing"`
}

PurchaseParams and FailureParams are what the emails state (stored with the notice row, so a retry sends the same text).

type RenewalSource

type RenewalSource struct{ DB *sql.DB }

RenewalSource lists the annual Stripe subscriptions about to renew, for launchnotice.RenewalJob (the yearly-renewal reminder, at least 30 days ahead). A subscription set to cancel does not renew and is not listed.

func (RenewalSource) AnnualRenewals

func (s RenewalSource) AnnualRenewals(ctx context.Context, from, to time.Time) ([]launchnotice.Renewal, error)

AnnualRenewals implements launchnotice.RenewalSource.

type Stats

type Stats struct{ Considered, Sent, Failed, Skipped int }

Stats is one run's outcome.

Jump to

Keyboard shortcuts

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