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

credits

package
v0.1.1 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: Apache-2.0 Imports: 25 Imported by: 0

Documentation

Overview

Package credits keeps prepaid credit for accounts: a ledger, purchases through a payment provider, and the charges the app makes for its work.

One credit is one EUR cent. Every mutation locks the account row and appends to the ledger; corrections are new entries. Purchases and reversals are idempotent per payment, and the app's charges per account and reference. Apps that reserve credit for work in progress report those holds through Config.Holds, and use WithAccount to change their own rows under the same lock.

templ: version: v0.3.1070

Index

Constants

View Source
const (
	KindTopUp      = "top_up"     // a purchase
	KindUsage      = "usage"      // the app's charge for work
	KindRefund     = "refund"     // a refunded purchase
	KindChargeback = "chargeback" // a disputed purchase
	KindAdjustment = "adjustment" // an operator's or the app's correction
	KindExpiry     = "expiry"     // credit that expired unused
	KindForfeit    = "forfeit"    // credit left when the account was deleted
)

The ledger's entry kinds.

View Source
const MaxChargeCents = 99_999_999

MaxChargeCents is the largest charge one purchase can have, fee included.

View Source
const MaxHistoryPage = 100_000

MaxHistoryPage is the last page History serves, so a huge page number cannot make the query skip millions of rows.

Variables

View Source
var (
	// ErrInsufficient means a debit exceeds the available credit.
	ErrInsufficient = errors.New("credits: insufficient credit")
	// ErrConflict means a reference was already used for a different amount or kind.
	ErrConflict = errors.New("credits: the reference was already used differently")
	// ErrNotFound means the account does not exist.
	ErrNotFound = errors.New("credits: account not found")
	// ErrInvalid means an amount, reference or description is out of range.
	ErrInvalid = errors.New("credits: invalid amount, reference or description")
)
View Source
var (
	// ErrAmount means a purchase amount is outside the policy's limits.
	ErrAmount = errors.New("credits: the purchase amount is outside the allowed range")
	// ErrPurchasesOff means the module has no payment provider.
	ErrPurchasesOff = errors.New("credits: buying credit is not available")
)
View Source
var Migrations = app.Migrations{Namespace: "credits", Files: sub(migrationFiles, "migrations")}

Migrations create the credit tables. List them after accounts.Migrations.

Functions

func FormatEuros

func FormatEuros(cents int64) string

FormatEuros renders cents as an amount such as "€12.50" or "−€3.00".

func Label

func Label(e Entry) string

Label names a ledger entry for people: its description, or what its kind means.

func ParseEuros

func ParseEuros(s string) (int64, bool)

ParseEuros reads an amount such as "12", "12.5" or "€12,50" as cents.

Types

type Config

type Config struct {
	// Payments takes purchases. Nil turns purchases off: the balance and history stay,
	// the pages say that buying credit is not available, and Webhook answers 404. A
	// process that only spends credit, such as an API gateway, holds no payment
	// configuration this way.
	Payments payments.Provider
	// Policy is required with Payments.
	Policy Policy
	// Holds reports how much of an account's credit the app has reserved for work in
	// progress. It runs inside the transaction that holds the account lock, and must
	// only read. Nil means the app keeps no holds.
	Holds func(ctx context.Context, tx *sql.Tx, accountID string) (int64, error)
}

Config configures the module.

type Credits

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

Credits is the credits module.

func New

func New(a *app.App, accts *accounts.Accounts, cfg Config) (*Credits, error)

New returns the credits module. It forfeits an account's remaining credit when the account is deleted, and registers the credits operator commands.

func (*Credits) Adjust

func (cr *Credits) Adjust(ctx context.Context, accountID string, cents int64, reference, description string) error

Adjust records a correction or grant, positive or negative, idempotent per reference. Unlike Debit it may take the balance below zero.

func (*Credits) Debit

func (cr *Credits) Debit(ctx context.Context, accountID string, cents int64, reference, description string) error

Debit takes credit for work. See Tx.Debit.

func (*Credits) History

func (cr *Credits) History(ctx context.Context, accountID string, page, perPage int) ([]Entry, bool, error)

History returns one page of an account's entries, newest first, and whether more follow. Pages start at 1.

func (*Credits) Mount

func (cr *Credits) Mount(r gin.IRouter, layout ui.Layout)

Mount adds the credits page to r, a group at the page's path that uses accounts.RequireSignIn, such as /account/credits:

  • GET "" shows the balance, a purchase form and the transactions.
  • POST "" starts a purchase and redirects to the provider's checkout.
  • GET "/quote" prices an amount for the purchase form.
  • GET "/summary" renders one page of transactions ("page", from 1).
  • GET "/balance" renders the available balance.
  • GET "/payment-status" reports a purchase started from this browser after the buyer returns, or answers 204 when there is none.

The pages allow the provider's checkout origin as a form target, because starting a purchase redirects there. Without a payment provider they show the balance and transactions, and say that buying credit is not available.

func (*Credits) PaymentStatus

func (cr *Credits) PaymentStatus(ctx context.Context, accountID, intentID string) (string, error)

PaymentStatus returns the provider status of an account's purchase: created, pending, paid, refunded, charged_back, failed, canceled or expired. A purchase of another account, or none, returns ErrNotFound.

func (*Credits) Purchase

func (cr *Credits) Purchase(ctx context.Context, account accounts.Account, creditCents int64, successURL string) (Purchase, error)

Purchase prices creditCents, records the purchase, and creates a checkout at the payment provider. The webhook later credits what the purchase records.

func (*Credits) Purchasing

func (cr *Credits) Purchasing() bool

Purchasing reports whether buying credit is available.

func (*Credits) Quote

func (cr *Credits) Quote(creditCents int64) (Quote, error)

Quote prices creditCents of credit, or returns ErrAmount outside the policy's limits.

func (*Credits) Summary

func (cr *Credits) Summary(ctx context.Context, accountID string) (Summary, error)

Summary reads the balance and holds in one consistent snapshot, without locking.

func (*Credits) Webhook

func (cr *Credits) Webhook(c *gin.Context)

Webhook receives payment provider webhooks. Mount it on a public route with neither CSRF nor sign-in, such as POST /webhooks/payments. docs/credits.md lists its outcomes. Without a payment provider it answers 404.

It logs event and intent IDs and outcomes only, never bodies or buyer details.

func (*Credits) WithAccount

func (cr *Credits) WithAccount(ctx context.Context, accountID string, fn func(tx *Tx) error) error

WithAccount runs fn in a transaction that holds the account's lock. Framework debits and the app's own changes through Tx.SQL commit together, or not at all. Lock the account before any app rows, and do no remote calls inside fn.

type Entry

type Entry struct {
	ID          string
	Kind        string
	AmountCents int64
	// Reference is the app's reference for usage and adjustments, or empty.
	Reference   string
	Description string
	CreatedAt   time.Time
}

Entry is one ledger entry.

type Policy

type Policy struct {
	// MinPurchaseCents and MaxPurchaseCents bound the credit one purchase buys. The
	// charge, fee included, cannot exceed MaxChargeCents either.
	MinPurchaseCents int64
	MaxPurchaseCents int64
	// FeeBasisPoints and FeeFixedCents add a fee on top of the credit, grossed up so it
	// also covers the percentage charged on itself. Zero means no fee.
	FeeBasisPoints int64
	FeeFixedCents  int64
}

Policy holds the business rules for purchases. They are the app's to choose.

func (Policy) Fee

func (p Policy) Fee(creditCents int64) int64

Fee returns the fee for buying creditCents. It is grossed up so that a provider charging FeeBasisPoints and FeeFixedCents on the total is covered by the fee.

type Purchase

type Purchase struct {
	Quote
	IntentID    string
	CheckoutURL string
}

Purchase is a started purchase: the buyer pays at CheckoutURL.

type Quote

type Quote struct {
	CreditCents int64
	FeeCents    int64
	ChargeCents int64
}

Quote is what buying an amount of credit costs.

type Summary

type Summary struct {
	BalanceCents   int64
	HeldCents      int64
	AvailableCents int64
}

Summary is an account's credit at one moment.

type Tx

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

Tx is an account's credit inside a transaction that holds the account's lock.

func (*Tx) Available

func (t *Tx) Available() (int64, error)

Available returns the balance minus the app's holds. It is never more than the balance and never below zero unless the balance is.

func (*Tx) Balance

func (t *Tx) Balance() (int64, error)

Balance returns the account's balance, holds included.

func (*Tx) Capture

func (t *Tx) Capture(cents int64, reference, description string) error

Capture charges for work a hold reserved, once the app has settled the hold in the same transaction. Unlike Debit it does not check the available credit: the hold did that when the work started, and finished work is charged even if a refund lowered the balance since. Repeating a capture with the same reference and amount succeeds without charging twice.

func (*Tx) Debit

func (t *Tx) Debit(cents int64, reference, description string) error

Debit takes credit for work. It fails with ErrInsufficient when it exceeds the available credit. Repeating a debit with the same reference and amount succeeds without taking credit twice.

func (*Tx) Expire

func (t *Tx) Expire(cents int64) error

Expire removes credit that expired, as one expiry entry. The credits/expiry module calls it after checking, under the account lock, that the credit is due; apps take credit with Debit.

func (*Tx) SQL

func (t *Tx) SQL() *sql.Tx

SQL returns the transaction, for the app's own tables. Do not commit or roll it back.

Directories

Path Synopsis
Package expiry expires credit that has gone unused, after warning its owner.
Package expiry expires credit that has gone unused, after warning its owner.

Jump to

Keyboard shortcuts

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