repository

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: 10 Imported by: 0

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

View Source
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

View Source
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.

View Source
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

View Source
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

View Source
var ErrMpesaBalanceQueryNotFound = errors.New("mpesa balance query not found")

ErrMpesaBalanceQueryNotFound means no pending query matches the OriginatorConversationID a balance result arrived with.

View Source
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

func IsTransactionUpdatableColumn(col string) bool

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

Jump to

Keyboard shortcuts

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