The contract

Home | Products | Interest

Version

type Version interface {
    Product() string   // "easy-access"
    Version() int      // 1, 2, …
    Name() string
    Family() ProductFamily
    Parameters() []Declaration
    Rules
}

A version lives in a package of its own (easyaccess/v1) and exports one value, Version. It is immutable once adopted; a change to the rules is a new package. The build carries only the versions the bank has: those on sale and those an account runs on. A retired version's postings stay in the bank with a record of the version, the event, the parameters read and the inputs, and are verified by the arithmetic in this package, never by the version's rules.

Events

Rule Fired by the bank when Reads Returns
StartUp(p, day) the process starts, once per adopted version the parameters as resolvable on day an error stops the bank
Open(f) an account opens on the version the open day, the parameters account settings, the first position
PrePosting(f, m) before a movement posts the movement, the position, the parameters nil to allow; a *Refusal to refuse
PostPosting(f, m) after the movement posted the balance as it now stands the day's position
Day(f) the start-of-day pass visits the account yesterday's position, the balance, parameters for both days yesterday closed with its postings, today's position
ParameterChange(f, c) a setting the version reads becomes effective old and new value nothing, or postings
Command(f, name) the console runs one of Commands() the position postings, the position
ChangeOfVersion(f, to) the account moves to another version the position the outgoing version's closing postings
Close(f) the account closes the position the final postings

Every rule is pure: the same facts give the same intents, so the runner may run a rule again after a restart, or after an event that followed the pass, and the last answer is the right one.

Facts and intents

type Facts interface {
    Parameter(key string, day time.Time) (Param, error)
    Account() string
    Day() time.Time
    Position(day time.Time) (luca.Position, bool)
    Balance() luca.Amount
}

type Movement struct {
    Delta       luca.Amount // the change to the balance, signed as the balance is
    Code, Description string
    ValueTime   time.Time
}

type Intents struct {
    Settings  []Setting       // account-scoped parameters to write
    Postings  []Posting       // counterparty -> account, signed as the balance is
    Positions []luca.Position // to project
}

A refusal is gbp.Refuse("reason"); the runner tells it from a fault with gbp.IsRefusal.

Parameters

type Declaration struct {
    Key       string
    Scope     Scope       // market (owner the currency; key RiskFreeRateKey) | bank | version | account
    Kind      Kind        // bps | money | int | cycle | day | text
    Published string      // the value at adoption (a version's own scope; the bank's for a policy)
    Derived   *Derivation // a formula: Source, SpreadBps, Floor, Cap
    Algorithm Algorithm   // code: func(p ParameterReader, day time.Time) (string, error)
}

The runner resolves a parameter for a day (gobank ADR-0006, parameter resolution): a derived parameter from its source and the formula, or by running its algorithm over the reader (the reads it makes are logged as the rule's own); a declared one from the latest setting effective on or before the day at its scope, or from the published value. A setting effective after the day is invisible to it, which is how a future rate change is decided today and takes effect on its day with nothing to do.

A formula is one the bank can describe from its fields; an algorithm is whatever its declarer's code says, described in words in the declaration. The bank's policy derives its rates by algorithm (gobank ADR-0006, ADR-0009): a savings rate that follows the market down at once and up after a lag reads the market on the day and the lag's days earlier and takes the lower. IsDerived is true of either: a derived parameter is read, never set.

gbp.Interest is the interest-bearing account's rules, which a version embeds and overrides where its product differs. gbp.CheckDeclarations is the start-up check every version shares.

Policy

type Policy interface {
    Name() string
    Version() int
    Parameters() []Declaration // at the bank scope, and the market's it reads
    StartUp(p ParameterReader, day time.Time) error
}

A policy is a bank's own rules as versioned code (gobank ADR-0006, ADR-0007): the values it publishes for staff to set (a spread, a lag) and the rates it derives from the market and from them by algorithm. It answers no account event; a version reads the bank's rates by declaring them at the bank scope (easyaccess/v2 reads savings_rate_bps). It lives in the bank's own module, immutable once adopted as a version is. gbp.CheckPolicy is its start-up check.

Testing a version

r := testkit.Open(t, easyaccess.Version, "Liability:Savings:alice", day)
r.Deposit(100000)
r.Advance(32)
testkit.Golden(t, "easy_access_32d", r.Export())

testkit.Runner is the reference runner: it implements Facts over an in-memory go-luca ledger and settings by scope (SetMarket, SetBank, SetVersion), carries out every intent, surfaces refusals from TryDeposit and TryWithdraw, and logs the parameters each rule read in Reads. testkit.CheckVersion asserts a version's identity and declarations; CheckVersionWith writes the bank's settings first for a version whose start-up reads them. GOLDEN_UPDATE=1 rewrites a golden.

testkit.Reader is the parameter resolution alone, over one declarer's declarations and in-memory settings: what a policy tests through.