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
- func Amount(amount int64, currency string) string
- func FailureKind(periodEnd time.Time) string
- func FailureSubject(FailureParams) string
- func PriceLabel(amount int64, currency, interval string) string
- func PurchaseKind(subscription string) string
- func PurchaseSubject(p PurchaseParams) string
- func RenderFailure(p FailureParams) string
- func RenderPurchase(p PurchaseParams) string
- type Failure
- type FailureParams
- type Job
- type Mailer
- type Purchase
- type PurchaseParams
- type RenewalSource
- type Stats
Constants ¶
const ( KindPurchase = launchnotice.KindBillingPurchase KindPaymentFailed = launchnotice.KindBillingPaymentFailed )
Kinds, for the metric (rivolt_dated_notices_total{kind,outcome}) and the notice rows.
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 FailureKind ¶
func FailureSubject ¶
func FailureSubject(FailureParams) string
FailureSubject is the subject of the payment-failed notice.
func PriceLabel ¶
PriceLabel is "$34.99 a year".
func PurchaseKind ¶
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.
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 ¶
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.