giftcard

package
v1.48.2 Latest Latest
Warning

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

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package giftcard is the gift-card domain: a prepaid, code-addressable balance an org issues and a customer redeems against orders.

Money-correctness design (READ THIS before touching redeem):

  • A GiftCard carries an immutable InitialBalanceCents. It NEVER carries a mutable "remaining" counter that two concurrent redeems could race on.
  • Every debit is a GiftCardRedemption ledger line (append-only fact).
  • The spendable balance is a PROJECTION: InitialBalance − Σ(active redemptions). Because balance is derived, there is no lost-update: two concurrent redeems each write their own ledger line and the projection reflects both. (Rich Hickey: the redemptions are the values; balance is a view.)
  • Redemption is idempotent by construction: the ledger line's storage id is deterministic in the caller-supplied idempotency key (giftcardredemption.DeterministicID). A duplicate submit resolves to the same row via the storage ON CONFLICT(id, kind, namespace) upsert — one debit, never two — WITHOUT relying on datastore.RunInTransaction, which is a no-op on this backend (datastore/datastore.go) and provides no isolation.

Over-redeem is prevented at apply time by checking the projected balance (Σ ledger) against the requested amount before writing the new line; the deterministic id makes the check-then-write safe against retries, and the projection makes distinct concurrent redeems additive rather than lossy.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrNotRedeemable      = errors.New("gift card is not redeemable (disabled or expired)")
	ErrInsufficientFunds  = errors.New("gift card has insufficient balance")
	ErrNonPositiveAmount  = errors.New("redeem amount must be positive")
	ErrMissingIdempotency = errors.New("idempotency key is required to redeem")
	ErrCurrencyMismatch   = errors.New("redeem currency does not match gift card currency")
)

Money errors. Callers map these to 4xx; none of them move money.

Functions

func BalanceCents

func BalanceCents(db *datastore.Datastore, g *GiftCard) (currency.Cents, error)

BalanceCents is the spendable balance: InitialBalance − Σ(active redemptions). This is a projection over facts; there is no mutable balance field to race.

func DrawnCents

func DrawnCents(db *datastore.Datastore, giftCardId string) (currency.Cents, error)

DrawnCents returns the total non-voided amount already redeemed from a card. It is the Σ over the append-only redemption ledger — the authoritative debit total. Balance is derived from it, never stored, so concurrent distinct redemptions are additive (never lost).

func Query

func Redeem

func Redeem(db *datastore.Datastore, g *GiftCard, amount currency.Cents, curr currency.Type, orderId, idempotencyKey string) (*giftcardredemption.GiftCardRedemption, currency.Cents, error)

Redeem draws amount from the gift card and returns the resulting redemption line plus the balance remaining AFTER this redemption.

Idempotency (money-critical): the redemption row id is deterministic in idempotencyKey (giftcardredemption.DeterministicID). A retry with the same key resolves to the same stored row via the storage ON CONFLICT upsert — the amount is debited exactly once, never twice, WITHOUT relying on datastore.RunInTransaction (a no-op that gives no isolation on this backend).

On a duplicate key we return the ALREADY-STORED line (not the caller's possibly-different amount) so a replayed request can never silently change a past debit.

db MUST be namespaced to the gift card's org (the caller passes org.Namespaced(c.Context())); every read/write below is thereby tenant-scoped.

func Void

func Void(db *datastore.Datastore, g *GiftCard, redemptionId string) (currency.Cents, error)

Void reverses a redemption by APPENDING a compensating reversal line (a negative-amount ledger entry) rather than mutating the original — the ledger is append-only, so a debit is never rewritten; it is cancelled by an equal and opposite fact. This restores the drawn amount to the projected balance.

Idempotent: the reversal's id is deterministic in the original redemption id ("void:"+id), so voiding the same redemption twice resolves to the same reversal row via ON CONFLICT — the balance is restored exactly once. Returns the balance after the void.

Types

type GiftCard

type GiftCard struct {
	mixin.Model[GiftCard]

	// Code is the human-facing gift-card code (e.g. "GIFT-4F2A-9C1D").
	// Unique within an org namespace by convention (issue rejects collisions).
	// Stored uppercased for case-insensitive lookup.
	Code string `json:"code"`

	InitialBalanceCents currency.Cents `json:"initialBalanceCents"`
	Currency            currency.Type  `json:"currency" orm:"default:usd"`

	// Region this card is valid in (optional; empty = any region in the org).
	RegionId string `json:"regionId,omitempty"`

	// Optional order this card was purchased on (audit).
	OrderId string `json:"orderId,omitempty"`

	// Disabled cards cannot be redeemed even if balance remains.
	Disabled bool `json:"disabled"`

	// EndsAt, when set and in the past, makes the card unredeemable (expiry).
	EndsAt *time.Time `json:"endsAt,omitempty"`

	Metadata  Map    `json:"metadata,omitempty" datastore:"-"`
	Metadata_ string `json:"-" datastore:",noindex"`
}

GiftCard is a prepaid balance addressable by Code, scoped to the issuing org's namespace. InitialBalanceCents is immutable after issue; the spendable balance is computed from the redemption ledger (see BalanceCents in the giftcard API), so a gift card row is a fact, not a mutable wallet.

func New

func New(db *datastore.Datastore) *GiftCard

func (*GiftCard) Load

func (g *GiftCard) Load(ps []datastore.Property) (err error)

func (*GiftCard) Redeemable

func (g *GiftCard) Redeemable() bool

Redeemable reports whether the card may be debited right now, ignoring balance (balance is checked separately against the projected ledger sum).

func (*GiftCard) Save

func (g *GiftCard) Save() ([]datastore.Property, error)

Jump to

Keyboard shortcuts

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