models

package
v1.4.1 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: AGPL-3.0 Imports: 6 Imported by: 0

Documentation

Overview

Package models holds the GORM persistence models — the database schema expressed as Go structs. Each type maps to one table and carries the JSON and gorm struct tags that define its API shape and its columns. The repository layer reads and writes these; the service layers wrap them in their own DTOs.

The entities are User, Account, and Transaction, plus SecurityQuestion for the PIN-recovery flow. An Account is a Stellar child account belonging to a User; a Transaction optionally references a User and an Account. PIN material lives on the User (its hash, attempt count, and lockout time), and security-question answers are stored hashed.

Conventions

Every model follows the same rules. The primary key is a UUIDv7 assigned in a BeforeCreate hook, so IDs are unique and time-ordered without a database sequence. A TableName method pins the table name. Secret fields — PIN hash and security-answer hash — are tagged json:"-" so they never serialize into a response. User and Account carry a nullable, indexed DeletedAt that the repository stamps and clears by hand for reversible soft deletes; transactions are never deleted.

Canonical enums

The string constants defined here are the single source of truth for the values the service state machines validate against: KYC statuses, the shared user and account lifecycle statuses, and the transaction statuses, categories, and types. Referencing these constants rather than bare strings keeps the services and the stored rows in agreement.

Index

Constants

View Source
const (
	// Transaction Status
	TxStatusPending   = "pending"
	TxStatusSubmitted = "submitted"
	TxStatusSuccess   = "success"
	TxStatusFailed    = "failed"
	TxStatusCancelled = "cancelled"

	// Transaction Types — Loan Disbursement
	TxTypeVaultBorrow  = "vault_borrow"  // USDC borrowed from Stellar vault to user account
	TxTypeOffRamp      = "off_ramp"      // Off-ramp initiated (crypto-to-fiat via YellowCard)
	TxTypeFiatFailover = "fiat_failover" // Fiat failover after direct settlement refund
	TxTypeVaultRepay   = "vault_repay"   // USDC repaid from treasury back to Stellar vault
	TxTypeRefund       = "refund"        // USDC returned by an anchor after a cancelled off-ramp

	// TxTypeAnchorTransfer is the on-chain USDC leg of an anchor withdrawal:
	// treasury to the anchor's withdraw account. Distinct from TxTypeOffRamp,
	// which records the off-chain fiat leg the anchor pays out afterwards — a
	// cash-pickup disbursement produces both.
	TxTypeAnchorTransfer = "anchor_transfer"

	// TxTypeLoanRepayment is the off-chain leg: cash the borrower hands over
	// at a MoneyGram agent, or pays in over a mobile-money paybill. It is what
	// the borrower did, not what settled — the USDC leg is separate and can
	// lag it or fail.
	TxTypeLoanRepayment = "loan_repayment"

	// TxTypeAnchorDeposit is the on-chain USDC leg of an anchor deposit:
	// the anchor to the treasury. The mirror of TxTypeAnchorTransfer.
	TxTypeAnchorDeposit = "anchor_deposit"
)
View Source
const (
	TxCategoryOnChain  = "on_chain"
	TxCategoryOffChain = "off_chain"
)

Transaction categories. Derived from TxType rather than stored: every type is settled either on the Stellar ledger or off it, never both, so a column would only add a way for the two to disagree.

View Source
const (
	// ChainStatusPending is set at registration: the sponsored-creation
	// transaction has been dispatched but not confirmed.
	ChainStatusPending = "pending"

	// ChainStatusConfirmed means the account was observed on the network.
	ChainStatusConfirmed = "confirmed"

	// ChainStatusFailed means creation retries were exhausted. Lending is
	// blocked for these until EnsureOnChainAccount heals them.
	ChainStatusFailed = "failed"

	// ChainStatusUnknown marks rows that predate this column.
	ChainStatusUnknown = "unknown"
)

Account.ChainStatus values. The account row is committed before the Stellar account is submitted, so this is what distinguishes a row whose keypair exists on-chain from one whose creation never landed.

View Source
const (
	// User KYC Status
	KYCStatusPending  = "pending"
	KYCStatusVerified = "verified"
	KYCStatusRejected = "rejected"
	KYCStatusExpired  = "expired"

	// User/Account Status
	StatusActive    = "active"
	StatusSuspended = "suspended"
	StatusBlocked   = "blocked"
	StatusFrozen    = "frozen"
	StatusClosed    = "closed"
)

Constants for status enums

View Source
const PredefinedQuestionCount = 5

PredefinedQuestionCount is the total number of predefined security questions available for selection. Question IDs range from 1 to PredefinedQuestionCount.

Variables

This section is empty.

Functions

func TxCategoryFor added in v1.0.0

func TxCategoryFor(txType string) string

TxCategoryFor reports where a transaction type settles.

Unknown types report off_chain: a type this function has not been taught about has no ledger presence to claim.

Types

type Account

type Account struct {
	ID           string     `json:"id" gorm:"type:uuid;primaryKey"`
	UserID       string     `json:"user_id" gorm:"type:uuid;not null;index"`
	PublicKey    string     `json:"public_key" gorm:"type:varchar(56);uniqueIndex;not null"`
	AccountIndex int        `json:"account_index" gorm:"not null"`
	Status       string     `json:"status" gorm:"type:varchar(20);not null;default:'active'"`
	ChainStatus  string     `json:"chain_status" gorm:"type:varchar(20);not null;default:'pending'"`
	CreatedAt    time.Time  `json:"created_at" gorm:"autoCreateTime;not null"`
	UpdatedAt    time.Time  `json:"updated_at" gorm:"autoUpdateTime;not null"`
	DeletedAt    *time.Time `json:"deleted_at,omitempty" gorm:"index"`

	User User `gorm:"foreignKey:UserID"`
}

Account represents a Stellar child account

func (*Account) BeforeCreate

func (account *Account) BeforeCreate(tx *gorm.DB) error

BeforeCreate sets the ID before creating a new account

func (Account) TableName

func (Account) TableName() string

TableName specifies the table name for Account model

type Date added in v1.0.0

type Date struct{ time.Time }

func (Date) MarshalJSON added in v1.0.0

func (d Date) MarshalJSON() ([]byte, error)

func (*Date) Scan added in v1.0.0

func (d *Date) Scan(value any) error

Scan reads a DATE value from the database into d.

func (Date) Value added in v1.0.0

func (d Date) Value() (driver.Value, error)

Value renders the date as a DATE literal for the database driver. Without this, GORM serializes the embedded time.Time via String(), which Postgres rejects for a DATE column.

type MpesaNumberValidation added in v1.4.1

type MpesaNumberValidation struct {
	ID string `json:"id" gorm:"type:uuid;primaryKey"`

	// IdentityHash is sha256(msisdn|idType|idNumber), hex-encoded.
	IdentityHash string `json:"identity_hash" gorm:"column:identity_hash;type:varchar(64);uniqueIndex;not null"`

	Matched      bool   `json:"matched" gorm:"not null"`
	ResponseCode string `json:"response_code" gorm:"column:response_code;type:varchar(10);not null"`

	CheckedAt time.Time `json:"checked_at" gorm:"column:checked_at;not null"`
}

MpesaNumberValidation caches the verdict of a Mobile Number Validation call, keyed on a hash of the (msisdn, idType, idNumber) tuple so the cache itself never stores the PII it exists to avoid re-checking.

func (*MpesaNumberValidation) BeforeCreate added in v1.4.1

func (v *MpesaNumberValidation) BeforeCreate(g *gorm.DB) error

BeforeCreate sets the ID before creating a new MpesaNumberValidation.

func (MpesaNumberValidation) TableName added in v1.4.1

func (MpesaNumberValidation) TableName() string

TableName specifies the table name for MpesaNumberValidation.

type MpesaTransaction added in v1.4.1

type MpesaTransaction struct {
	ID string `json:"id" gorm:"type:uuid;primaryKey"`

	// TransID is the M-Pesa receipt. Unique-indexed. The idempotency key.
	TransID string `json:"trans_id" gorm:"column:trans_id;type:varchar(20);uniqueIndex;not null"`

	Source       MpesaTransactionSource      `json:"source" gorm:"type:varchar(20);not null;index"`
	Confirmed    bool                        `json:"confirmed" gorm:"not null;default:false"`
	ConfirmedVia *MpesaTransactionConfirmVia `json:"confirmed_via,omitempty" gorm:"type:varchar(20)"`

	BillRefNumber string  `json:"bill_ref_number" gorm:"type:varchar(20);index"`
	LoanID        *string `json:"loan_id,omitempty" gorm:"type:uuid;index"`

	AmountKes int64 `json:"amount_kes" gorm:"column:amount_kes;not null"` // minor units

	// MsidnMasked comes from a C2B callback; MsidnFull comes from the Pull
	// reconciler. Full is nullable PII; never log it.
	MsidnMasked string  `json:"msisdn_masked,omitempty" gorm:"column:msisdn_masked"`
	MsidnFull   *string `json:"msisdn_full,omitempty" gorm:"column:msisdn_full"`

	// PayerName comes from the C2B callback or a reversal result. Nullable PII.
	PayerName *string `json:"payer_name,omitempty" gorm:"type:varchar(100)"`

	TransTime time.Time `json:"trans_time" gorm:"not null"`

	// ThirdPartyTransID is our own correlation handle, echoed validation →
	// confirmation.
	ThirdPartyTransID *string `json:"third_party_trans_id,omitempty"`

	CheckoutRequestID *string `json:"checkout_request_id,omitempty" gorm:"index"` // STK
	MerchantRequestID *string `json:"merchant_request_id,omitempty"`              // STK
	SequenceID        *string `json:"sequence_id,omitempty" gorm:"type:varchar(200);index"`

	// RawPayload is the bytes as received. It is not optional: when a
	// reconciliation disagrees with a callback, the bytes settle it.
	RawPayload datatypes.JSON `json:"raw_payload" gorm:"type:jsonb;not null"`

	Reversal   MpesaTransactionReversal `json:"reversal_state" gorm:"column:reversal_state;type:varchar(20);not null;default:'none'"`
	NextPollAt *time.Time               `json:"next_poll_at,omitempty" gorm:"index"`

	CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;not null"`
	UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;not null"`
}

MpesaTransaction is the observation log for inbound M-Pesa notifications. Every callback lands here before anything else happens, so the confirm-before-credit discipline is enforceable.

A row is never evidence of a payment. Daraja signs nothing, so a well-formed callback proves only that something posted to a URL. It becomes a payment only after ConfirmedVia names an independent check.

func (*MpesaTransaction) BeforeCreate added in v1.4.1

func (tx *MpesaTransaction) BeforeCreate(g *gorm.DB) error

BeforeCreate sets the ID before creating a new MpesaTransaction

func (MpesaTransaction) TableName added in v1.4.1

func (MpesaTransaction) TableName() string

TableName specifies the table name for MpesaTransaction model

type MpesaTransactionConfirmVia added in v1.4.1

type MpesaTransactionConfirmVia string

MpesaTransactionConfirmVia is how an observation was independently verified.

const (
	MpesaConfirmViaSTKQuery          MpesaTransactionConfirmVia = "stk_query"
	MpesaConfirmViaPull              MpesaTransactionConfirmVia = "pull"
	MpesaConfirmViaTransactionStatus MpesaTransactionConfirmVia = "transaction_status"
)

The confirmation paths.

type MpesaTransactionReversal added in v1.4.1

type MpesaTransactionReversal string

MpesaTransactionReversal is the state of a reversal for this transaction.

const (
	MpesaReversalNone     MpesaTransactionReversal = "none"
	MpesaReversalProposed MpesaTransactionReversal = "proposed"
	MpesaReversalApproved MpesaTransactionReversal = "approved"
	MpesaReversalSent     MpesaTransactionReversal = "sent"
	MpesaReversalComplete MpesaTransactionReversal = "complete"
	MpesaReversalFailed   MpesaTransactionReversal = "failed"
)

The reversal states.

type MpesaTransactionSource added in v1.4.1

type MpesaTransactionSource string

MpesaTransactionSource is where an inbound notification came from. It decides which confirmation path can verify it.

const (
	MpesaSourceSTKCallback     MpesaTransactionSource = "stk_callback"
	MpesaSourceC2BConfirmation MpesaTransactionSource = "c2b_confirmation"
	MpesaSourcePull            MpesaTransactionSource = "pull"
	MpesaSourceStatusResult    MpesaTransactionSource = "status_result"
	MpesaSourceSTKQuery        MpesaTransactionSource = "stk_query"
)

The inbound sources.

type SecurityQuestion

type SecurityQuestion struct {
	ID         string    `json:"id" gorm:"type:uuid;primaryKey"`
	UserID     string    `json:"user_id" gorm:"type:uuid;not null;index"`
	QuestionID int       `json:"question_id" gorm:"not null"`
	AnswerHash string    `json:"-" gorm:"type:varchar(72);not null"`
	CreatedAt  time.Time `json:"created_at" gorm:"autoCreateTime;not null"`
	UpdatedAt  time.Time `json:"updated_at" gorm:"autoUpdateTime;not null"`

	User User `gorm:"foreignKey:UserID"`
}

SecurityQuestion stores a hashed answer to a predefined security question for a given user. Each user may have multiple security questions, identified by QuestionID (an integer index into a predefined question list). The combination of (UserID, QuestionID) is unique.

func (*SecurityQuestion) BeforeCreate

func (sq *SecurityQuestion) BeforeCreate(tx *gorm.DB) error

BeforeCreate sets a UUIDv7 primary key before inserting a new row.

func (SecurityQuestion) TableName

func (SecurityQuestion) TableName() string

TableName specifies the table name for the SecurityQuestion model.

type Transaction

type Transaction struct {
	ID               string    `json:"id" gorm:"type:uuid;primaryKey"`
	UserID           *string   `json:"user_id,omitempty" gorm:"type:uuid;index"`
	AccountID        *string   `json:"account_id,omitempty" gorm:"type:uuid;index"`
	LoanID           *string   `json:"loan_id,omitempty" gorm:"type:uuid;index"`
	TxType           string    `json:"tx_type" gorm:"type:varchar(50);not null;index"`
	Amount           int64     `json:"amount" gorm:"type:bigint;not null"`
	Asset            string    `json:"asset" gorm:"type:varchar(20);not null;index"`
	StellarTxHash    *string   `json:"stellar_tx_hash,omitempty" gorm:"type:varchar(64);uniqueIndex"`
	StellarLedger    *int64    `json:"stellar_ledger,omitempty" gorm:"type:bigint"`
	ContractID       *string   `json:"contract_id,omitempty" gorm:"type:varchar(56);index"`
	ContractFunction *string   `json:"contract_function,omitempty" gorm:"type:varchar(100)"`
	ExternalID       *string   `json:"external_id,omitempty" gorm:"type:varchar(100);index"`
	ExternalProvider *string   `json:"external_provider,omitempty" gorm:"type:varchar(50);index"`
	ExternalStatus   *string   `json:"external_status,omitempty" gorm:"type:varchar(20)"`
	Description      *string   `json:"description,omitempty" gorm:"type:text"`
	Metadata         *string   `json:"metadata,omitempty" gorm:"type:jsonb"`
	Status           string    `json:"status" gorm:"type:varchar(20);not null;default:'pending';index"`
	CreatedAt        time.Time `json:"created_at" gorm:"autoCreateTime;not null;index"`
	UpdatedAt        time.Time `json:"updated_at" gorm:"autoUpdateTime;not null"`

	User    *User    `gorm:"foreignKey:UserID"`
	Account *Account `gorm:"foreignKey:AccountID"`
}

Transaction represents a blockchain or off-chain transaction

func (*Transaction) BeforeCreate

func (transaction *Transaction) BeforeCreate(tx *gorm.DB) error

BeforeCreate sets the ID before creating a new transaction

func (Transaction) TableName

func (Transaction) TableName() string

TableName specifies the table name for Transaction model

type User

type User struct {
	ID                string     `json:"id" gorm:"type:uuid;primaryKey"`
	MobileNumber      string     `json:"mobile_number" gorm:"type:varchar(20);uniqueIndex;not null"`
	CountryCode       string     `json:"country_code" gorm:"type:varchar(5);not null;default:'KE'"`
	MobileNetworkCode string     `json:"mobile_network_code" gorm:"type:varchar(6);not null;default:'99999'"`
	MomoNetworkCode   string     `json:"momo_network_code" gorm:"type:varchar(20);not null;default:'SANDBOX'"`
	MomoNetworkName   string     `json:"momo_network_name" gorm:"type:varchar(20);not null;default:'Sandbox Network'"`
	TelcoName         string     `json:"telco_name" gorm:"type:varchar(20);not null;default:'Athena'"`
	FullName          *string    `json:"full_name,omitempty" gorm:"type:varchar(255)"`
	BirthDate         *Date      `json:"birth_date,omitempty" gorm:"type:date"`
	Address           *string    `json:"address,omitempty" gorm:"type:varchar(255)"`
	City              *string    `json:"city,omitempty" gorm:"type:varchar(255)"`
	PostalCode        *string    `json:"postal_code,omitempty" gorm:"type:varchar(20)"`
	NationalID        *string    `json:"national_id,omitempty" gorm:"type:varchar(50);uniqueIndex"`
	KYCStatus         string     `json:"kyc_status" gorm:"type:varchar(20);not null;default:'pending'"`
	KYCVerifiedAt     *time.Time `json:"kyc_verified_at,omitempty" gorm:"type:timestamp"`
	PinHash           *string    `json:"-" gorm:"type:varchar(72)"`
	PinAttempts       int        `json:"-" gorm:"not null;default:0"`
	PinLockedUntil    *time.Time `json:"-" gorm:"type:timestamp"`
	PinSetAt          *time.Time `json:"-" gorm:"type:timestamp"`

	PreferredLanguage string     `json:"preferred_language" gorm:"type:varchar(10);not null;default:'en'"`
	Status            string     `json:"status" gorm:"type:varchar(20);not null;default:'active'"`
	Role              string     `json:"role" gorm:"type:varchar(20);not null;default:'user'"`
	CreatedAt         time.Time  `json:"created_at" gorm:"autoCreateTime;not null"`
	UpdatedAt         time.Time  `json:"updated_at" gorm:"autoUpdateTime;not null"`
	DeletedAt         *time.Time `json:"deleted_at,omitempty" gorm:"index"`
}

User represents a system user

func (*User) BeforeCreate

func (user *User) BeforeCreate(tx *gorm.DB) error

BeforeCreate sets the ID before creating a new user

func (User) TableName

func (User) TableName() string

TableName specifies the table name for User model

Jump to

Keyboard shortcuts

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