billingnotify

package
v1.15.2 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

Documentation

Overview

Package billingnotify delivers account-trash transitions (trash, restore) to the external billing service as durable River jobs.

A trash asks billing to cancel the subscription at period end; a restore asks it to revert that. Both used to be best-effort HTTP calls after commit, so a single billing 5xx on restore left a restored customer's subscription scheduled to cancel with no retry. The job is enqueued in the same transaction as the trash or restore, retried with River's backoff, and is convergent and serialized per account: the worker takes a transaction-scoped advisory lock on the account, re-reads its state and posts while still holding the lock, and skips a notice the account has since moved past (a trash notice for an account that was restored, a restore notice for one trashed again, either for an account already purged — purge has its own cancel call). Because the read and the post happen under one per-account lock, and every transition enqueues its own notice in its own transaction, the last post for an account always reflects its latest committed state — even when River runs a trash and a restore notice concurrently or out of order.

The job is deliberately NOT River-unique by account: River's uniqueness must include the running state, so a restore notice inserted while a trash notice is running would be dropped as a duplicate — the running trash post would land last and leave a live customer set to cancel. The advisory lock gives the one-at-a-time property without that loss.

Index

Constants

View Source
const (
	ModeTrash   = "trash"
	ModeRestore = "restore"
)

Modes.

View Source
const MaxAttempts = 20

MaxAttempts bounds retries (River's exponential backoff spreads 20 attempts over roughly two weeks — longer than any billing outage we should survive silently).

Variables

View Source
var ErrNotFound = errors.New("billingnotify: billing service has no account-state endpoint (404)")

ErrNotFound is what a Poster returns when the billing service does not know the endpoint (an older service answering 404). It is permanent: the job completes with a log line instead of retrying for two weeks.

Functions

func LockKey

func LockKey(userID string) string

LockKey is the per-account advisory lock key notices serialize on.

Types

type Args

type Args struct {
	UserID string `json:"user_id"`
	Mode   string `json:"mode"`
}

Args is the job payload.

func (Args) Kind

func (Args) Kind() string

Kind implements river.JobArgs.

type Jobs

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

Jobs is the registrar + enqueuer. Poster is late-bound (the agent API that owns the billing URLs is built after the River client starts).

func New

func New(pool *pgxpool.Pool) *Jobs

New builds the registrar over the database the account state lives in.

func (*Jobs) EnqueueTx

func (j *Jobs) EnqueueTx(ctx context.Context, tx pgx.Tx, userID, mode string) error

EnqueueTx inserts the notice in the caller's transaction (the trash or restore transaction), so the notice exists exactly when the transition commits.

func (*Jobs) RegisterJobs

func (j *Jobs) RegisterJobs(w *river.Workers) []*river.PeriodicJob

RegisterJobs implements jobs.Registrar.

func (*Jobs) SetEnqueuer

func (j *Jobs) SetEnqueuer(e jobs.Enqueuer)

SetEnqueuer injects the shared River client.

func (*Jobs) SetPoster

func (j *Jobs) SetPoster(p Poster)

SetPoster late-binds the HTTP sender.

type Poster

type Poster func(ctx context.Context, userID, mode string) error

Poster sends one notice. Returns ErrNotFound for a 404.

type Worker

type Worker struct {
	river.WorkerDefaults[Args]
	// contains filtered or unexported fields
}

Worker posts one notice.

func NewWorkerForTest

func NewWorkerForTest(j *Jobs) *Worker

NewWorkerForTest returns the worker RegisterJobs registers.

func (*Worker) Timeout

func (w *Worker) Timeout(*river.Job[Args]) time.Duration

Timeout bounds one attempt.

func (*Worker) Work

func (w *Worker) Work(ctx context.Context, job *river.Job[Args]) error

Work posts the notice unless the account has moved past it. The state read and the post happen under one per-account advisory lock (held by this transaction for at most the job timeout), so notices for one account are strictly serialized.

Jump to

Keyboard shortcuts

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