mirror of
https://github.com/luxfi/bank.git
synced 2026-10-30 17:02:07 +00:00
Each Ensure creates its collection once and returns early ever after, so the owner-scoped rules of v1.5.0 only ever applied to a fresh database. One created by an earlier release kept list/view open to anyone on accounts, balances, transactions, wallets and cards through every upgrade. The rules are now one table, used at creation and re-asserted on every boot. The users collection's anonymous create is closed the same way: identity is Lux ID, so no anonymous caller makes a users record. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_016dDw5Lp9sZFctwvZLn4nVz
236 lines
5.2 KiB
Go
236 lines
5.2 KiB
Go
package collections
|
|
|
|
import "github.com/hanzoai/base/core"
|
|
|
|
const (
|
|
TransactionCollectionName = "transactions"
|
|
FeeCollectionName = "fees"
|
|
SessionCollectionName = "sessions"
|
|
)
|
|
|
|
// EnsureTransactionCollection creates the transactions collection.
|
|
// Each record represents a single payment or conversion event.
|
|
func EnsureTransactionCollection(app core.App) error {
|
|
_, err := app.FindCollectionByNameOrId(TransactionCollectionName)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
c := core.NewBaseCollection(TransactionCollectionName, TransactionCollectionName)
|
|
|
|
readable(c)
|
|
|
|
c.Fields.Add(
|
|
// Source account.
|
|
&core.RelationField{
|
|
Name: "account",
|
|
CollectionId: AccountCollectionName,
|
|
Required: true,
|
|
MaxSelect: 1,
|
|
},
|
|
|
|
// Target beneficiary (nil for conversions / internal moves).
|
|
&core.RelationField{
|
|
Name: "beneficiary",
|
|
CollectionId: BeneficiaryCollectionName,
|
|
MaxSelect: 1,
|
|
},
|
|
|
|
// CurrencyCloud payment/conversion id.
|
|
&core.TextField{
|
|
Name: "ccTransactionId",
|
|
},
|
|
|
|
// Transaction type. `card` is a card purchase — a debit like a payment
|
|
// but on the card rail, so it carries no wire fee and is not routed to
|
|
// forex; the Cards screen lists these specifically. `earn` is a Liquid
|
|
// Protocol vault movement (deposit/borrow/repay/withdraw), carrying no
|
|
// fee and no forex routing.
|
|
&core.SelectField{
|
|
Name: "type",
|
|
Values: []string{"payment", "card", "conversion", "deposit", "withdrawal", "earn", "fee"},
|
|
Required: true,
|
|
MaxSelect: 1,
|
|
},
|
|
|
|
// Direction relative to the account.
|
|
&core.SelectField{
|
|
Name: "direction",
|
|
Values: []string{"credit", "debit"},
|
|
Required: true,
|
|
MaxSelect: 1,
|
|
},
|
|
|
|
// Amount in minor units (cents / satoshis).
|
|
&core.NumberField{
|
|
Name: "amount",
|
|
Required: true,
|
|
},
|
|
|
|
// ISO 4217 currency.
|
|
&core.TextField{
|
|
Name: "currency",
|
|
Required: true,
|
|
Min: 3,
|
|
Max: 3,
|
|
},
|
|
|
|
// Status lifecycle.
|
|
&core.SelectField{
|
|
Name: "status",
|
|
Values: []string{"pending", "processing", "completed", "failed", "cancelled"},
|
|
Required: true,
|
|
MaxSelect: 1,
|
|
},
|
|
|
|
// Payment reference / memo.
|
|
&core.TextField{
|
|
Name: "reference",
|
|
},
|
|
|
|
// Reason for failure or cancellation.
|
|
&core.TextField{
|
|
Name: "reason",
|
|
},
|
|
|
|
// Settlement date (when funds actually move).
|
|
&core.DateField{
|
|
Name: "settlementDate",
|
|
},
|
|
|
|
// Conversion-specific fields.
|
|
&core.NumberField{
|
|
Name: "buyAmount",
|
|
},
|
|
&core.TextField{
|
|
Name: "buyCurrency",
|
|
Min: 3,
|
|
Max: 3,
|
|
},
|
|
&core.NumberField{
|
|
Name: "sellAmount",
|
|
},
|
|
&core.TextField{
|
|
Name: "sellCurrency",
|
|
Min: 3,
|
|
Max: 3,
|
|
},
|
|
|
|
// IFX or CurrencyCloud exchange rate.
|
|
&core.NumberField{
|
|
Name: "exchangeRate",
|
|
},
|
|
|
|
// Arbitrary provider metadata.
|
|
&core.JSONField{
|
|
Name: "metadata",
|
|
MaxSize: 1 << 20,
|
|
},
|
|
|
|
&core.AutodateField{
|
|
Name: "created",
|
|
OnCreate: true,
|
|
},
|
|
&core.AutodateField{
|
|
Name: "updated",
|
|
OnCreate: true,
|
|
OnUpdate: true,
|
|
},
|
|
)
|
|
|
|
return app.Save(c)
|
|
}
|
|
|
|
// FeeTypes are the kinds of charge a fee row may record. It is a closed
|
|
// vocabulary and this is the only place it is written down: a fee whose type is
|
|
// not one of these is refused by the collection, so a caller that invents a name
|
|
// does not record a smaller fee, it records none at all — silently, since the
|
|
// charge is raised in a hook that only logs.
|
|
var FeeTypes = []string{"wire_fee", "conversion_spread", "service_fee", "compliance_surcharge"}
|
|
|
|
// EnsureFeeCollection creates the fees collection. Fees are linked to
|
|
// transactions and track markup, spread, and compliance surcharges.
|
|
func EnsureFeeCollection(app core.App) error {
|
|
_, err := app.FindCollectionByNameOrId(FeeCollectionName)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
c := core.NewBaseCollection(FeeCollectionName, FeeCollectionName)
|
|
|
|
// API rules: all nil — superuser only. Fees are internal records.
|
|
|
|
c.Fields.Add(
|
|
&core.RelationField{
|
|
Name: "transaction",
|
|
CollectionId: TransactionCollectionName,
|
|
Required: true,
|
|
MaxSelect: 1,
|
|
},
|
|
&core.SelectField{
|
|
Name: "type",
|
|
Values: FeeTypes,
|
|
Required: true,
|
|
MaxSelect: 1,
|
|
},
|
|
// Amount in minor units.
|
|
&core.NumberField{
|
|
Name: "amount",
|
|
Required: true,
|
|
},
|
|
&core.TextField{
|
|
Name: "currency",
|
|
Required: true,
|
|
Min: 3,
|
|
Max: 3,
|
|
},
|
|
&core.AutodateField{
|
|
Name: "created",
|
|
OnCreate: true,
|
|
},
|
|
)
|
|
|
|
return app.Save(c)
|
|
}
|
|
|
|
// EnsureSessionCollection creates the sessions collection for tracking
|
|
// user login events and JWT token metadata.
|
|
func EnsureSessionCollection(app core.App) error {
|
|
_, err := app.FindCollectionByNameOrId(SessionCollectionName)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
|
|
c := core.NewBaseCollection(SessionCollectionName, SessionCollectionName)
|
|
|
|
readable(c)
|
|
|
|
c.Fields.Add(
|
|
&core.RelationField{
|
|
Name: "user",
|
|
CollectionId: "_users_auth_",
|
|
Required: true,
|
|
MaxSelect: 1,
|
|
},
|
|
&core.TextField{
|
|
Name: "ipAddress",
|
|
Required: true,
|
|
},
|
|
&core.TextField{
|
|
Name: "userAgent",
|
|
},
|
|
&core.DateField{
|
|
Name: "expiresAt",
|
|
},
|
|
&core.BoolField{
|
|
Name: "revoked",
|
|
},
|
|
&core.AutodateField{
|
|
Name: "created",
|
|
OnCreate: true,
|
|
},
|
|
)
|
|
|
|
return app.Save(c)
|
|
}
|