Documentation
¶
Overview ¶
Package repository is the data-access layer. It holds the GORM-backed repositories for the core models — users, accounts, and transactions — behind interfaces that expose persistence operations and nothing else. Business rules live in the service packages above; this layer only reads and writes rows.
Repositories bundles the three repositories, and NewRepositories wires them from a single *gorm.DB. Each constructor (and NewRepositories itself) rejects a nil database with errors.ErrNilDB. Every repository owns its own sentinel errors — ErrUserNotFound, ErrAccountNotFound, ErrTransactionNotFound, and the matching ErrFailedTo* values — and translates raw GORM errors into them, so callers match on package errors and never import GORM's.
Reads that return lists take limit and offset for pagination; the service layer is responsible for turning page numbers into those.
Soft deletes ¶
Deletion is reversible and implemented by hand through a deleted_at column, not GORM's automatic soft delete. Delete stamps deleted_at with the current time, Restore clears it (an Unscoped update, since the row is otherwise hidden), and every read filters on deleted_at IS NULL. A deleted row therefore stays in the table but disappears from normal queries until it is restored.
Transactions ¶
Write methods come in a plain form and a WithTx form. The WithTx variant accepts a *gorm.DB supplied by the caller, so a write can join a larger unit of work — for example creating a user and their first account atomically. The account index handed to a new account is derived as one past the highest existing index across all accounts, with index 0 reserved for the admin.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrAccountNotFound = errors.New("account not found") ErrFailedToCreateAccount = errors.New("failed to create account") ErrFailedToGetAccount = errors.New("failed to get account") ErrFailedToGetAccountsByUser = errors.New("failed to get accounts by user") ErrFailedToGetNextIndex = errors.New("failed to get next account index") ErrFailedToUpdateAccount = errors.New("failed to update account") ErrFailedToDeleteAccount = errors.New("failed to delete account") ErrFailedToRestoreAccount = errors.New("failed to restore account") )
Common errors for AccountRepository
var ( ErrMpesaConflict = errors.New("mpesa transaction already recorded") ErrFailedToRecord = errors.New("failed to record mpesa observation") ErrMpesaNotFound = errors.New("mpesa transaction not found") ErrFailedToPoll = errors.New("failed to fetch due mpesa observations") )
Errors for MpesaTransactionRepository.
var ( ErrTransactionNotFound = errors.New("transaction not found") ErrFailedToCreateTransaction = errors.New("failed to create transaction") ErrFailedToCreateBatchTransactions = errors.New("failed to create batch transactions") ErrFailedToGetTransaction = errors.New("failed to get transaction") ErrFailedToGetTransactionByHash = errors.New("failed to get transaction by stellar hash") ErrFailedToGetTransactionsByLoanID = errors.New("failed to get transactions by loan ID") ErrFailedToGetTransactionsByUserID = errors.New("failed to get transactions by user ID") ErrFailedToGetTransactionsByStatus = errors.New("failed to get transactions by status") ErrFailedToUpdateTransaction = errors.New("failed to update transaction") ErrFailedToListTransactions = errors.New("failed to list transactions") ErrFailedToCountTransactions = errors.New("failed to count transactions") )
Common errors for TransactionRepository
var ( ErrUserNotFound = errors.New("user not found") ErrFailedToCreateUser = errors.New("failed to create user") ErrFailedToGetUser = errors.New("failed to get user") ErrFailedToGetUsers = errors.New("failed to get users") ErrFailedToGetUsersByKYC = errors.New("failed to get users by KYC status") ErrFailedToGetUsersByRole = errors.New("failed to get users by role") ErrFailedToCountUsers = errors.New("failed to count users") ErrFailedToCountAdmins = errors.New("failed to count admins") ErrFailedToUpdateUser = errors.New("failed to update user") ErrFailedToDeleteUser = errors.New("failed to delete user") ErrFailedToRestoreUser = errors.New("failed to restore user") )
Common errors for UserRepository
var ErrMpesaBalanceQueryNotFound = errors.New("mpesa balance query not found")
ErrMpesaBalanceQueryNotFound means no pending query matches the OriginatorConversationID a balance result arrived with.
var ErrMpesaValidationNotFound = errors.New("mpesa number validation not found")
ErrMpesaValidationNotFound means no cached verdict exists for that hash.
Functions ¶
func IsTransactionUpdatableColumn ¶ added in v1.0.0
IsTransactionUpdatableColumn reports whether col may be written via UpdateFields. Exposed for test-guarding the service-layer partial-update mapping against drift.
Types ¶
type AccountRepository ¶
type AccountRepository interface {
// Create operations
Create(ctx context.Context, account *models.Account) error
CreateWithTx(ctx context.Context, tx *gorm.DB, account *models.Account) error
// Read operations
GetByID(ctx context.Context, id string) (*models.Account, error)
GetByUserID(ctx context.Context, userID string) (*models.Account, error)
GetByPublicKey(ctx context.Context, publicKey string) (*models.Account, error)
GetNextAccountIndex(ctx context.Context, userID string) (int, error)
GetNextAccountIndexWithTx(ctx context.Context, tx *gorm.DB) (int, error)
// EnsureAccountIndexFloor advances account_index_seq so the next handed-out
// index is >= floor. No-op when the sequence is already at or past floor.
EnsureAccountIndexFloor(ctx context.Context, floor int64) error
// Update operations
Update(ctx context.Context, account *models.Account) error
UpdateChainStatus(ctx context.Context, id string, chainStatus string) error
Restore(ctx context.Context, id string) error
// Delete operations
Delete(ctx context.Context, id string) error
}
AccountRepository defines the interface for account data access
func NewAccountRepository ¶
func NewAccountRepository(db *gorm.DB) (AccountRepository, error)
NewAccountRepository creates a new AccountRepository
type MpesaBalanceRepository ¶ added in v1.4.1
type MpesaBalanceRepository interface {
// RecordQuery notes that a balance query for shortcode was sent, keyed on
// the ack's OriginatorConversationID.
RecordQuery(ctx context.Context, originatorConversationID string, shortcode uint) error
// ResolveQuery returns the shortcode a landing result's
// OriginatorConversationID was queried for, or ErrMpesaBalanceQueryNotFound.
ResolveQuery(ctx context.Context, originatorConversationID string) (uint, error)
// RecordBalance persists one parsed account balance snapshot.
RecordBalance(ctx context.Context, shortcode uint, accountName, currency string, availableKES int64, observedAt time.Time) error
}
MpesaBalanceRepository correlates Account Balance requests with their asynchronous results and persists the parsed figures.
func NewMpesaBalanceRepository ¶ added in v1.4.1
func NewMpesaBalanceRepository(db *gorm.DB) (MpesaBalanceRepository, error)
NewMpesaBalanceRepository builds the repository.
type MpesaNumberValidationRepository ¶ added in v1.4.1
type MpesaNumberValidationRepository interface {
// Get returns the cached verdict for a hash, or ErrMpesaValidationNotFound.
Get(ctx context.Context, identityHash string) (*models.MpesaNumberValidation, error)
// Upsert records a fresh verdict, replacing whatever was cached for the
// same identity hash.
Upsert(ctx context.Context, v *models.MpesaNumberValidation) error
}
MpesaNumberValidationRepository caches Mobile Number Validation verdicts so a payer's identity is checked at the paid rate at most once per cache entry rather than once per payment.
func NewMpesaNumberValidationRepository ¶ added in v1.4.1
func NewMpesaNumberValidationRepository(db *gorm.DB) (MpesaNumberValidationRepository, error)
NewMpesaNumberValidationRepository builds the repository.
type MpesaPullCursorRepository ¶ added in v1.4.1
type MpesaPullCursorRepository interface {
// Get returns the cursor's current position — the exclusive start of the
// next window to sweep.
Get(ctx context.Context) (time.Time, error)
// Advance moves the cursor forward to at.
Advance(ctx context.Context, at time.Time) error
}
MpesaPullCursorRepository tracks how far the Pull reconciliation sweep has walked. One row (id=1); Get and Advance both operate on it.
func NewMpesaPullCursorRepository ¶ added in v1.4.1
func NewMpesaPullCursorRepository(db *gorm.DB) (MpesaPullCursorRepository, error)
NewMpesaPullCursorRepository builds the repository.
type MpesaTransactionRepository ¶ added in v1.4.1
type MpesaTransactionRepository interface {
// Record inserts a new observation. A duplicate TransID returns
// ErrMpesaConflict, which is the idempotency check the controller relies
// on.
Record(ctx context.Context, tx *models.MpesaTransaction) error
// GetByTransID fetches one observation by receipt, e.g. to confirm a
// callback against.
GetByTransID(ctx context.Context, transID string) (*models.MpesaTransaction, error)
// GetByCheckoutID fetches one observation by its STK checkout request ID,
// e.g. to tie a query-confirmed payment back to its callback row.
GetByCheckoutID(ctx context.Context, checkoutID string) (*models.MpesaTransaction, error)
// DuePoll returns unconfirmed observations whose NextPollAt has come due,
// newest last so a stuck poll does not starve newer ones.
DuePoll(ctx context.Context, limit int) ([]*models.MpesaTransaction, error)
// Confirm marks an observation verified and stamps how, which is what
// moves it out of the poller's due set.
Confirm(ctx context.Context, transID string, via models.MpesaTransactionConfirmVia, loanID string) error
// UpdatePoll writes the next poll moment — the cadence lives in a column,
// not in in-memory timers, per the mgpoller reasoning.
UpdatePoll(ctx context.Context, transID string, at time.Time) error
// StopPoll clears the poll moment, taking the observation out of the due
// set for good: the terminal-failure path, where re-asking Daraja can never
// change the answer.
StopPoll(ctx context.Context, transID string) error
// GetLoanIDByReference resolves a loan reference to a loan ID on the shared
// platform DB. Raw SQL rather than the credit module's repository because
// importing the lending module would invert the layering — the same
// reasoning mgpoller documents for its loan status constants. Returns ""
// when the reference resolves to nothing.
GetLoanIDByReference(ctx context.Context, reference string) (string, error)
// SetReversalState writes the reversal lifecycle state.
SetReversalState(ctx context.Context, transID string, state models.MpesaTransactionReversal) error
// UpdateFields sets the mutable fields a Pull reconciler fills in —
// unmasked MSISDN, loan attribution, confirmed status.
UpdateFields(ctx context.Context, tx *models.MpesaTransaction) error
// UpsertFromPull records tx if Pull is the first thing to see it — the
// callback was lost — or updates the mutable fields on the existing row
// when it was already recorded from a callback. This is the entry point
// the Pull reconciler uses instead of choosing between Record and
// UpdateFields itself.
UpsertFromPull(ctx context.Context, tx *models.MpesaTransaction) error
}
MpesaTransactionRepository persists inbound M-Pesa observations.
It exists to make confirm-before-credit enforceable, so Create is careful to give only the "recorded" state and Confirm marks the verified one.
func NewMpesaTransactionRepository ¶ added in v1.4.1
func NewMpesaTransactionRepository(db *gorm.DB) (MpesaTransactionRepository, error)
NewMpesaTransactionRepository creates a new MpesaTransactionRepository.
type Repositories ¶
type Repositories struct {
// Core repositories
User UserRepository
Account AccountRepository
Transaction TransactionRepository
Mpesa MpesaTransactionRepository
MpesaValidation MpesaNumberValidationRepository
MpesaPullCursor MpesaPullCursorRepository
MpesaBalance MpesaBalanceRepository
}
func NewRepositories ¶
func NewRepositories(db *gorm.DB) (*Repositories, error)
type TransactionRepository ¶
type TransactionRepository interface {
// Create operations
Create(ctx context.Context, tx *models.Transaction) error
BatchCreate(ctx context.Context, txs []*models.Transaction) error
// Read operations
GetByID(ctx context.Context, id string) (*models.Transaction, error)
GetByStellarHash(ctx context.Context, txHash string) (*models.Transaction, error)
ListByExternalID(ctx context.Context, externalID string) ([]*models.Transaction, error)
GetByLoanIDAndType(ctx context.Context, loanID, txType string) (*models.Transaction, error)
GetByLoanID(ctx context.Context, loanID string, limit, offset int) ([]*models.Transaction, error)
GetByUserID(ctx context.Context, userID string, limit, offset int) ([]*models.Transaction, error)
GetByStatus(ctx context.Context, status string, limit, offset int) ([]*models.Transaction, error)
// List returns transactions newest first, filtered by status and/or type
// when either is given. Empty filters return everything.
List(ctx context.Context, status, txType string, limit, offset int) ([]*models.Transaction, error)
// Count returns the number of transactions matching the same filters List applies.
Count(ctx context.Context, status, txType string) (int64, error)
// Update operations
Update(ctx context.Context, tx *models.Transaction) error
// UpdateFields writes only the supplied columns, validated against the same
// allow-list Update uses. Preferred for partial changes to avoid rewriting
// the text/jsonb columns and all indexes on every touch.
UpdateFields(ctx context.Context, id string, fields map[string]any) error
}
TransactionRepository defines the interface for transaction data access
func NewTransactionRepository ¶
func NewTransactionRepository(db *gorm.DB) (TransactionRepository, error)
NewTransactionRepository creates a new instance of TransactionRepository
type UserRepository ¶
type UserRepository interface {
// Create operations
Create(ctx context.Context, user *models.User) error
CreateWithTx(ctx context.Context, tx *gorm.DB, user *models.User) error
// Read operations
GetByID(ctx context.Context, id string) (*models.User, error)
GetByMobileNumber(ctx context.Context, mobileNumber string) (*models.User, error)
GetByNationalID(ctx context.Context, nationalID string) (*models.User, error)
GetByKYCStatus(ctx context.Context, kycStatus string, limit, offset int) ([]*models.User, error)
GetByRole(ctx context.Context, role string, limit, offset int) ([]*models.User, error)
List(ctx context.Context, limit, offset int) ([]*models.User, error)
// ListFiltered returns users newest first, filtered by status and/or KYC
// status when either is given. Empty filters return every non-deleted user.
ListFiltered(ctx context.Context, status, kycStatus string, limit, offset int) ([]*models.User, error)
// Count operations
Count(ctx context.Context) (int64, error)
// CountFiltered returns the number of users matching ListFiltered's filters.
CountFiltered(ctx context.Context, status, kycStatus string) (int64, error)
CountByKYCStatus(ctx context.Context, kycStatus string) (int64, error)
CountByRole(ctx context.Context, role string) (int64, error)
CountAdmins(ctx context.Context) (int, error)
// Update operations
Update(ctx context.Context, user *models.User) error
// UpdateMobileNumber rebinds a user to a new MSISDN. Deliberately separate
// from Update (which does not touch mobile_number) because this changes the
// account's identity anchor — it is only used by new-SIM account recovery,
// after the caller has verified ownership. The unique index on
// mobile_number is the final guard against binding a number twice.
UpdateMobileNumber(ctx context.Context, userID, mobileNumber string) error
Restore(ctx context.Context, id string) error
// Delete operations
Delete(ctx context.Context, id string) error
}
UserRepository defines the interface for user data access
func NewUserRepository ¶
func NewUserRepository(db *gorm.DB) (UserRepository, error)
NewUserRepository creates a new instance of UserRepository