Files
zeekayandHanzo Dev e5897cf101 every new account was created without a balance, and nothing said so
RegisterAccountHooks opens a balance when an account is created, setting
available and held to 0. The available field was Required, and a required
number field refuses its zero value — "available: cannot be blank" — so the save
failed on EVERY account. The hook logs the error and returns nil, which is the
right shape for a side effect that must not fail account creation, and is also
why this was silent: the account existed, the balance did not, and the first
thing to look one up got "no rows in result set".

Zero is not a missing balance. It is the balance every account opens with, and
requiring the field made that state unrepresentable. held was never marked
required, which is what the pair should always have looked like.

Found by binding the hooks in a test and asking whether the balance was there.
It was not.

Also covered on the same path, because it is the one that decides whether money
moves: a transaction in a currency the bank cannot price is refused on create,
and one in USD, EUR or BTC still passes. That closes the loop on the earlier
CanPrice fix — it was proved at the function, and is now proved through the hook
a request actually travels.

hooks 12.5% -> 18.3%.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-25 12:57:09 -07:00

67 lines
1.8 KiB
Go

package collections
import "github.com/hanzoai/base/core"
const BalanceCollectionName = "balances"
// EnsureBalanceCollection creates the balances collection if it does not exist.
// Each record tracks a single currency balance for an account.
// Accounts can have multiple balance records (one per currency).
func EnsureBalanceCollection(app core.App) error {
_, err := app.FindCollectionByNameOrId(BalanceCollectionName)
if err == nil {
return nil
}
c := core.NewBaseCollection(BalanceCollectionName, BalanceCollectionName)
// Superuser only (nil rules) in every mode — balances are read through the
// authenticated /v1/bank routes, never as anonymous collection reads.
c.Fields.Add(
&core.RelationField{
Name: "account",
CollectionId: AccountCollectionName,
Required: true,
MaxSelect: 1,
},
// ISO 4217 currency code.
&core.TextField{
Name: "currency",
Required: true,
Min: 3,
Max: 3,
},
// Available balance in minor units (cents).
//
// NOT Required: a required number field refuses its zero value ("available:
// cannot be blank"), and zero is the balance every account opens with. The
// hook that creates a balance on account creation set it to 0 and was
// refused every time — it logs and returns nil, so each new customer was
// created with no balance row at all and nothing said so. `held` was never
// marked required, which is what the pair should always have looked like.
&core.NumberField{
Name: "available",
},
// Held (pending) balance in minor units.
&core.NumberField{
Name: "held",
},
&core.AutodateField{
Name: "created",
OnCreate: true,
},
&core.AutodateField{
Name: "updated",
OnCreate: true,
OnUpdate: true,
},
)
return app.Save(c)
}