mirror of
https://github.com/luxfi/bank.git
synced 2026-10-30 17:02:07 +00:00
The running total answered zero for a total it could not compute — a failed query, or a row in a currency with no reference price, both read as an account that has moved nothing today. Nothing spent clears every ceiling, so the control that is meant to cap an account opened it instead. It says so now, and the gate refuses. That is the same sentence the incoming amount is already held to — a limit cannot be enforced in a unit that cannot be converted — applied to what came before it. The vault catalog is held to the pricing tables for the same reason. Nothing about lending depends on a price, since the borrow ceiling is a ratio between two amounts of the same asset, but the position a customer reads is valued in USD and a vault the bank cannot price shows collateral worth nothing. Co-authored-by: Hanzo Dev <[email protected]>
161 lines
5.5 KiB
Go
161 lines
5.5 KiB
Go
package hooks
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/luxfi/bank/collections"
|
|
)
|
|
|
|
// debit puts one outbound transaction through the hooks and reports whether the
|
|
// limit gate admitted it.
|
|
func debit(t *testing.T, app core.App, acct string, usdCents int64) error {
|
|
t.Helper()
|
|
col, err := app.FindCollectionByNameOrId(collections.TransactionCollectionName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := core.NewRecord(col)
|
|
r.Set("account", acct)
|
|
r.Set("type", "payment")
|
|
r.Set("direction", "debit")
|
|
r.Set("amount", usdCents)
|
|
r.Set("currency", "USD")
|
|
r.Set("status", "pending")
|
|
return app.Save(r)
|
|
}
|
|
|
|
// onPlan returns an account holding the named membership ("" for none).
|
|
func onPlan(t *testing.T, app core.App, plan string) string {
|
|
t.Helper()
|
|
id := account(t, app)
|
|
if plan == "" {
|
|
return id
|
|
}
|
|
r, err := app.FindRecordById(collections.AccountCollectionName, id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.Set("plan", plan)
|
|
if err := app.Save(r); err != nil {
|
|
t.Fatalf("putting the account on %s: %v", plan, err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
// Buying the cheapest membership must not take away what an account already
|
|
// had. The tier replaced the entity-type limits outright rather than raising
|
|
// them, so the entry tier cut an individual from $50,000 a day to $10,000 —
|
|
// a customer who paid $29 could move a fifth of what they could move for
|
|
// nothing, and the ladder is on the landing page waiting to be sold.
|
|
func TestAMembershipNeverLowersWhatAnAccountMayMove(t *testing.T) {
|
|
// $20,000: above the entry tier's daily limit, below an individual's.
|
|
const amount int64 = 20_000_00
|
|
|
|
app := limitApp(t)
|
|
RegisterAccountHooks(app)
|
|
if err := debit(t, app, onPlan(t, app, ""), amount); err != nil {
|
|
t.Fatalf("an account with no membership was refused $20,000: %v", err)
|
|
}
|
|
|
|
for _, plan := range []string{"silver", "gold", "black", "sovereign"} {
|
|
app := limitApp(t)
|
|
RegisterAccountHooks(app)
|
|
if err := debit(t, app, onPlan(t, app, plan), amount); err != nil {
|
|
t.Errorf("%s costs money and allows less than holding no membership at all: %v", plan, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The raise still happens — a tier above the baseline lifts it. Sovereign is
|
|
// $1M a day against an individual's $50,000, so an amount between the two
|
|
// separates a working ladder from one that is merely harmless.
|
|
func TestAMembershipAboveTheBaselineRaisesIt(t *testing.T) {
|
|
const amount int64 = 200_000_00 // above individual's $50k, below black's $250k
|
|
|
|
app := limitApp(t)
|
|
RegisterAccountHooks(app)
|
|
if err := debit(t, app, onPlan(t, app, ""), amount); err == nil {
|
|
t.Error("an account with no membership moved $200,000, over the $50,000 individual limit")
|
|
}
|
|
|
|
for _, plan := range []string{"black", "sovereign"} {
|
|
app := limitApp(t)
|
|
RegisterAccountHooks(app)
|
|
if err := debit(t, app, onPlan(t, app, plan), amount); err != nil {
|
|
t.Errorf("%s is meant to carry $200,000 and refused it: %v", plan, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Every rung of the advertised ladder is one an account can actually be put on.
|
|
// The field is a select over the plan ids, so a tier added to the catalog and
|
|
// not to the field is a membership nobody can hold — and the limit gate would
|
|
// read it as no membership at all.
|
|
func TestEveryAdvertisedTierIsOneAnAccountCanHold(t *testing.T) {
|
|
app := limitApp(t)
|
|
for _, p := range collections.Plans {
|
|
if _, ok := collections.PlanByID(p.ID); !ok {
|
|
t.Errorf("%q is advertised but PlanByID does not know it", p.ID)
|
|
}
|
|
if err := debit(t, app, onPlan(t, app, p.ID), 1_00); err != nil {
|
|
t.Errorf("an account cannot be put on the advertised tier %q: %v", p.ID, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A running total that cannot be computed must not read as nothing spent.
|
|
// Nothing spent clears every ceiling, so a history the bank cannot total in one
|
|
// unit would let an account move whatever it liked — the same sentence the
|
|
// incoming amount is already held to, applied to what came before it.
|
|
func TestALimitIsRefusedRatherThanGuessedFromAnUnreadableHistory(t *testing.T) {
|
|
app := limitApp(t)
|
|
acct := account(t, app)
|
|
|
|
// A debit already on the books in a currency with no reference price. The
|
|
// gate refuses to create one now, so this is what a row written before that
|
|
// existed — or around it — looks like.
|
|
col, err := app.FindCollectionByNameOrId(collections.TransactionCollectionName)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := core.NewRecord(col)
|
|
r.Set("account", acct)
|
|
r.Set("type", "payment")
|
|
r.Set("direction", "debit")
|
|
r.Set("amount", 1_000_00)
|
|
r.Set("currency", "ZZZ")
|
|
r.Set("status", "completed")
|
|
if err := app.Save(r); err != nil {
|
|
t.Fatalf("writing the unpriceable row: %v", err)
|
|
}
|
|
|
|
if _, err := getDailySpent(app, acct); err == nil {
|
|
t.Error("a history holding a currency the bank cannot price totalled cleanly — an amount it could not value counted as nothing")
|
|
}
|
|
|
|
// And the gate refuses rather than admitting on a total it does not have.
|
|
RegisterAccountHooks(app)
|
|
if err := debit(t, app, acct, 1_00); err == nil {
|
|
t.Error("a debit was admitted while the account's running total could not be read")
|
|
}
|
|
}
|
|
|
|
// The ordinary case still works: a readable history totals and the gate uses it.
|
|
func TestAReadableHistoryStillTotals(t *testing.T) {
|
|
app := limitApp(t)
|
|
acct := account(t, app)
|
|
RegisterAccountHooks(app)
|
|
|
|
if err := debit(t, app, acct, 1_000_00); err != nil {
|
|
t.Fatalf("an ordinary debit was refused: %v", err)
|
|
}
|
|
spent, err := getDailySpent(app, acct)
|
|
if err != nil {
|
|
t.Fatalf("reading the daily total: %v", err)
|
|
}
|
|
if spent != 1_000_00 {
|
|
t.Errorf("daily spent = %d, want 100000", spent)
|
|
}
|
|
}
|