ussd

package
v1.6.2 Latest Latest
Warning

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

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

README

pkg/mobile/ussd

The interactive USSD application — the menu-driven flow a mobile subscriber walks through to register, request a loan, pick a payout method, repay, and manage their PIN. The concepts (the session model, the menu engine, the port interfaces) live in doc.go and are visible via go doc ./pkg/mobile/ussd.

Layer diagram

flowchart TD
    GW["Telecom gateway<br/>Africa's Talking"] -->|HTTP form post| SVC["USSDService.HandleRequest"]
    SVC -->|resolves provider by name| Prov["USSDProvider<br/>transport contract"]
    Prov --> Handler["USSDHandler.HandleRequest"]

    subgraph handler_deps[Handler dependencies]
        SM["SessionManager<br/>Redis, 5 min TTL"]
        MR["MenuRegistry<br/>menu graph + presets"]
        LOC["InMemoryLocalizer<br/>en / sw / fr"]
        NM["NetworkMapper<br/>MCC+MNC to MoMo network"]
    end
    Handler --> handler_deps

    Handler -->|depends on narrow ports| UserPort["ussd.UserService"]
    Handler --> LoanPort["ussd.LoanService"]
    Handler --> RatePort["ussd.RateService"]
    Handler --> PinPort["ussd.PINService"]

    UserPort --> UserAdapter["adapters.UserServiceAdapter"]
    LoanPort --> LoanAdapter["credit module's<br/>LoanServiceAdapter"]
    RatePort --> RateAdapter["credit module's<br/>RateServiceAdapter"]
    PinPort --> PinSvc["pkg/pin.Service"]

    LoanAdapter --> Reg["offramp.Registry"]
    Reg --> MG["adapters.MoneyGramOffRampAdapter"]
    Reg --> YC["adapters.YellowCardOffRampAdapter"]
    MG --> TTransfer["adapters.StellarTreasuryTransfer"]
    YC --> TTransfer

    UserAdapter --> Domain["user / account / stellar /<br/>payment services"]
    LoanAdapter --> Domain
    RateAdapter --> Domain
    TTransfer --> Domain

Two things to read off the diagram:

  • The handler never imports a concrete service. It programs against four port interfaces (UserService, LoanService, RateService, PINService) plus a contracts.AccountNotifier. Every arrow out of the handler crosses an interface boundary.
  • The off-ramp adapters are not USSD ports. MoneyGramOffRampAdapter and YellowCardOffRampAdapter implement offramp.Provider (the payment contract), not ussd.LoanService. They sit inside pkg/mobile/ussd/adapters/ because they're USSD-channel glue, but they're consumed by the credit module's LoanServiceAdapter through the offramp.Registry, not by the USSD handler directly.

Request lifecycle

  1. Transport in. The telecom gateway POSTs to the HTTP endpoint. The route resolves a USSDProvider by name from USSDService and calls ParseRequest, which normalizes the gateway's form fields into a USSDRequest (session ID, phone, service code, network code, current input). For Africa's Talking the text field accumulates all inputs as input1*input2*input3; the provider extracts only the last segment.
  2. Session resolve. USSDHandler.HandleRequest calls SessionManager.GetOrCreateSession against Redis. A new session starts at the language menu; an existing session resumes at session.CurrentMenu. Sessions expire after 5 minutes of inactivity.
  3. First dial vs. continuation. Empty input means a fresh dial — handleInitialRequest checks UserService.GetUserWithAccounts and routes to registration (unknown number) or the main menu (registered user). On a retry with the same session ID the menu is always reset to main to avoid stale state.
  4. Menu dispatch. handleMenuInput looks up session.CurrentMenu in the MenuRegistry and invokes that menu's MenuHandler with a MenuContext (session + input + manager). The handler returns a MenuResponse — MenuTypeContinue ("CON") keeps the session open, MenuTypeEnd ("END") releases it.
  5. Side effects. Handlers call the port interfaces (LoanService, PINService, etc.) for any real work. PII-bearing menus (registration, PIN entry, national ID) are listed in sensitiveMenus and logged with redaction; phone numbers are always redacted in logs.
  6. Transport out. The handler's string response is wrapped in a USSDResponse and handed back to the provider's FormatResponse, which returns the gateway-specific shape.

Package layout

pkg/mobile/ussd/
├── doc.go                  # concepts — the go doc surface
├── README.md               # this file — architecture & navigation
├── handler.go              # USSDHandler: request routing, menu dispatch, all screen handlers
├── service.go              # USSDService: provider registry + request dispatch
├── session.go              # SessionManager: Redis-backed session lifecycle
├── types.go                # Session, Menu, MenuRegistry, port interfaces, request/response DTOs
├── menu.go                 # MenuBuilder + MenuRegistry
├── menu_presets.go         # StandardLoanMenuPreset: the full menu graph (register, loan, repay, PIN)
├── localization.go         # InMemoryLocalizer: language-keyed translations with fallback
├── network_mapper.go       # NetworkMapper: MCC+MNC → MoMo network + ISO country
├── adapters/               # USSD-to-domain glue (see adapters/doc.go)
│   ├── doc.go
│   ├── user_service_adapter.go      # ussd.UserService ← user + account + stellar
│   ├── offramp_moneygram.go         # offramp.Provider ← moneygram.Client
│   ├── offramp_yellowcard.go        # offramp.Provider ← yellowcard.YellowcardAdapter
│   └── treasury_transfer.go         # offramp.TreasuryTransfer ← stellar.Service
└── providers/              # transport adapters (USSDProvider implementations)
    └── africastalking/
        └── adapter.go      # Africa's Talking USSD gateway

What lives where, and why

If you're changing… …edit this file
A screen's text, options, or flow menu_presets.go (the menu graph) or handler.go (the screen handler)
The shape of Session / Menu / a port interface types.go
How sessions are stored or expire session.go
A translation string or a new language localization.go (+ the menu's WithTitle/WithOption maps)
A telco → MoMo network mapping network_mapper.go
How a USSD gateway's HTTP is parsed/formatted providers/<gateway>/adapter.go
How the USSD flow calls user/account/stellar adapters/user_service_adapter.go
How an off-ramp provider is invoked from USSD adapters/offramp_<provider>.go (but the contract is pkg/payment/offramp)
Treasury USDC movement for settlement adapters/treasury_transfer.go
  • doc.go — the concept surface (go doc ./pkg/mobile/ussd).
  • adapters/doc.go — the ports-and-glue overview.
  • pkg/payment/README.md — the off-ramp contract the adapters implement, and the registry-based routing.
  • pkg/pin/doc.go — the PIN service that satisfies ussd.PINService.

Documentation

Overview

Package ussd is the interactive USSD application: the menu-driven flow a mobile subscriber walks through to register, request a loan, pick a payout method, repay, and manage their PIN.

The package is layered. USSDService is the outermost entry point — it holds the registered USSDProvider transports (one per telecom gateway) and dispatches an incoming request to USSDHandler. The handler owns the application logic: it resolves the Session and routes each input with an explicit switch on the session's current menu.

The MenuRegistry is a rendering store, not a router: it supplies a screen's localized title and options, and nothing more.

There is no session-level login. A caller is identified by the MSISDN the gateway reports, and authorization happens at the screens that move money, where the handler calls PINService.VerifyPIN — loan confirmation and the repayment gate. A new screen that moves money needs its own VerifyPIN call; nothing gates it implicitly.

Session and menus

SessionManager persists session state in Redis with a short TTL (default 5 minutes). A Session carries the current menu, the menu history stack, the chosen language, and a free-form Data bag for cross-screen state (loan amount, payout method, national ID, etc.). Menus are registered once at startup. Handlers return the gateway's wire prefix directly: "CON " keeps the session open, "END " releases it.

MenuRegistry holds the menu graph. Menus are built with MenuBuilder and grouped into presets — StandardLoanMenuPreset wires the full registration, loan, repayment, PIN, and security-question flow. Each Menu carries language-keyed Titles and Options so the same graph renders in any supported language.

Localization

InMemoryLocalizer stores translations keyed by language and message key, with fallback to the default language when a translation is missing.

Network mapping

NetworkMapper resolves a telco's MCC+MNC code (e.g. "63902") to the corresponding mobile-money network (M-Pesa, Airtel Money, etc.) and ISO country. It is used during registration to pre-fill the user's network and country from the carrier-reported code.

Ports

The handler depends on four narrow interfaces defined in this package — UserService, LoanService, RateService, PINService — plus a contracts.AccountNotifier for side-effect SMS. Each port is satisfied by an adapter in pkg/mobile/ussd/adapters, which translates between the USSD request/response shapes and the underlying user, account, loan, off-ramp, and Stellar services. The handler never imports those services directly.

Providers

USSDProvider is the transport contract: ParseRequest turns a gateway's HTTP form post into a normalized USSDRequest, FormatResponse turns a USSDResponse back into the gateway's expected shape, and ValidateRequest gates entry. Concrete transports live under providers/ (today: providers/africastalking).

For the architecture diagram, request lifecycle, and file/subpackage map, see README.md.

Index

Constants

View Source
const (
	MinMoneyGramDepositStroops int64 = int64(moneygram.MinDepositUSD * 1e7)
	MaxMoneyGramDepositStroops int64 = int64(moneygram.MaxDepositUSD * 1e7)
)

MoneyGram's production on-ramp bounds, in stroops.

Variables

View Source
var ErrSessionNotFound = errors.New("session not found")

ErrSessionNotFound marks a session that is genuinely absent from the cache, as distinct from a cache that cannot be read. Both end the borrower's turn, but only one of them is an outage.

View Source
var NetworkMappings = map[string]NetworkMapping{

	"63902": {MobileNetworkCode: "63902", MomoNetworkCode: "MPESA", MomoNetworkName: "M-Pesa", TelcoName: "Safaricom Kenya", Country: "KE"},
	"63903": {MobileNetworkCode: "63903", MomoNetworkCode: "AIRTEL", MomoNetworkName: "Airtel Money", TelcoName: "Airtel Kenya", Country: "KE"},
	"63907": {MobileNetworkCode: "63907", MomoNetworkCode: "TKASH", MomoNetworkName: "T-Kash", TelcoName: "Orange Kenya", Country: "KE"},
	"63999": {MobileNetworkCode: "63999", MomoNetworkCode: "EQUITEL", MomoNetworkName: "Equitel", TelcoName: "Equitel Kenya", Country: "KE"},

	"64101": {MobileNetworkCode: "64101", MomoNetworkCode: "AIRTEL_UG", MomoNetworkName: "Airtel Money", TelcoName: "Airtel Uganda", Country: "UG"},
	"64110": {MobileNetworkCode: "64110", MomoNetworkCode: "MTN_UG", MomoNetworkName: "MTN Mobile Money", TelcoName: "MTN Uganda", Country: "UG"},
	"64114": {MobileNetworkCode: "64114", MomoNetworkCode: "AFRICELL_UG", MomoNetworkName: "Africell Money", TelcoName: "Africell Uganda", Country: "UG"},

	"64002": {MobileNetworkCode: "64002", MomoNetworkCode: "TIGO_TZ", MomoNetworkName: "Tigo Pesa", TelcoName: "Tigo Tanzania", Country: "TZ"},
	"64004": {MobileNetworkCode: "64004", MomoNetworkCode: "VODACOM_TZ", MomoNetworkName: "M-Pesa", TelcoName: "Vodacom Tanzania", Country: "TZ"},
	"64005": {MobileNetworkCode: "64005", MomoNetworkCode: "AIRTEL_TZ", MomoNetworkName: "Airtel Money", TelcoName: "Airtel Tanzania", Country: "TZ"},

	"63510": {MobileNetworkCode: "63510", MomoNetworkCode: "MTN_RW", MomoNetworkName: "MTN Mobile Money", TelcoName: "MTN Rwanda", Country: "RW"},
	"63513": {MobileNetworkCode: "63513", MomoNetworkCode: "TIGO_RW", MomoNetworkName: "Tigo Cash", TelcoName: "Tigo Rwanda", Country: "RW"},
	"63514": {MobileNetworkCode: "63514", MomoNetworkCode: "AIRTEL_RW", MomoNetworkName: "Airtel Money", TelcoName: "Airtel Rwanda", Country: "RW"},

	"62120": {MobileNetworkCode: "62120", MomoNetworkCode: "AIRTEL_NG", MomoNetworkName: "Airtel Money", TelcoName: "Airtel Nigeria", Country: "NG"},
	"62130": {MobileNetworkCode: "62130", MomoNetworkCode: "MTN_NG", MomoNetworkName: "MTN MoMo", TelcoName: "MTN Nigeria", Country: "NG"},
	"62150": {MobileNetworkCode: "62150", MomoNetworkCode: "GLO_NG", MomoNetworkName: "Glo Mobile Money", TelcoName: "Glo Nigeria", Country: "NG"},
	"62160": {MobileNetworkCode: "62160", MomoNetworkCode: "ETISALAT_NG", MomoNetworkName: "9Mobile", TelcoName: "Etisalat Nigeria", Country: "NG"},

	"62001": {MobileNetworkCode: "62001", MomoNetworkCode: "MTN_GH", MomoNetworkName: "MTN Mobile Money", TelcoName: "MTN Ghana", Country: "GH"},
	"62002": {MobileNetworkCode: "62002", MomoNetworkCode: "VODAFONE_GH", MomoNetworkName: "Vodafone Cash", TelcoName: "Vodafone Ghana", Country: "GH"},
	"62006": {MobileNetworkCode: "62006", MomoNetworkCode: "AIRTELTIGO_GH", MomoNetworkName: "AirtelTigo Money", TelcoName: "AirtelTigo Ghana", Country: "GH"},

	"63601": {MobileNetworkCode: "63601", MomoNetworkCode: "ETHIO", MomoNetworkName: "Telebirr", TelcoName: "EthioTelecom", Country: "ET"},

	"64501": {MobileNetworkCode: "64501", MomoNetworkCode: "AIRTEL_ZM", MomoNetworkName: "Airtel Money", TelcoName: "Airtel Zambia", Country: "ZM"},
	"64502": {MobileNetworkCode: "64502", MomoNetworkCode: "MTN_ZM", MomoNetworkName: "MTN Mobile Money", TelcoName: "MTN Zambia", Country: "ZM"},

	"65001": {MobileNetworkCode: "65001", MomoNetworkCode: "TNM_MW", MomoNetworkName: "TNM Mpamba", TelcoName: "TNM Malawi", Country: "MW"},
	"65010": {MobileNetworkCode: "65010", MomoNetworkCode: "AIRTEL_MW", MomoNetworkName: "Airtel Money", TelcoName: "Airtel Malawi", Country: "MW"},

	"65501": {MobileNetworkCode: "65501", MomoNetworkCode: "VODACOM_ZA", MomoNetworkName: "Vodacom", TelcoName: "Vodacom South Africa", Country: "ZA"},
	"65502": {MobileNetworkCode: "65502", MomoNetworkCode: "TELKOM_ZA", MomoNetworkName: "Telkom Pay", TelcoName: "Telkom South Africa", Country: "ZA"},
	"65507": {MobileNetworkCode: "65507", MomoNetworkCode: "CELLC_ZA", MomoNetworkName: "CellC", TelcoName: "CellC South Africa", Country: "ZA"},
	"65510": {MobileNetworkCode: "65510", MomoNetworkCode: "MTN_ZA", MomoNetworkName: "MTN Mobile Money", TelcoName: "MTN South Africa", Country: "ZA"},

	"99999": {MobileNetworkCode: "99999", MomoNetworkCode: "SANDBOX", MomoNetworkName: "Sandbox Network", TelcoName: "Athena", Country: "KE"},
}

NetworkMappings maps telco's mobile network code (MCC + MNC) to mobile money networks

Functions

func Format

func Format(language, key string, args ...any) string

Format provides string formatting with localization

func GetLocalizedMessage

func GetLocalizedMessage(language, key string) string

GetLocalizedMessage is a helper function for backward compatibility

Types

type BioUpdate added in v1.0.0

type BioUpdate struct {
	BirthDate  string // YYYY-MM-DD
	Address    string
	City       string
	PostalCode string
}

BioUpdate carries the optional SEP-9 fields set from the account menu.

type CarrierRepaymentPrompter added in v1.6.0

type CarrierRepaymentPrompter interface {
	// PromptRepaymentVia pushes a prompt for the full payoff on the named
	// cash-in provider. The provider id is the registry's own ("mpesa",
	// "airtel"); an unregistered one is an error, never a silent fallback.
	PromptRepaymentVia(ctx context.Context, loanID, phoneNumber, providerID string) error
}

CarrierRepaymentPrompter is the capability of pushing a prompt on a named rail. The borrower chooses their network from the repay menu rather than having it inferred from their MSISDN: prefix tables go stale silently as the regulator reallocates ranges, and a prompt pushed at the wrong network is a support call.

type CustomMenuPreset

type CustomMenuPreset struct {
	// contains filtered or unexported fields
}

CustomMenuPreset allows for the definition of arbitrary menu flows.

func NewCustomMenuPreset

func NewCustomMenuPreset(name string) *CustomMenuPreset

NewCustomMenuPreset creates a new instance of CustomMenuPreset.

func (*CustomMenuPreset) AddMenu

func (p *CustomMenuPreset) AddMenu(menu *Menu) *CustomMenuPreset

AddMenu adds a menu to the custom menu preset.

func (*CustomMenuPreset) GetName

func (p *CustomMenuPreset) GetName() string

GetName returns the name of the custom menu preset.

func (*CustomMenuPreset) Initialize

func (p *CustomMenuPreset) Initialize(registry *MenuRegistry)

NewCustomMenuPreset creates a new instance of CustomMenuPreset.

type HandlerDeps added in v1.1.2

type HandlerDeps struct {
	SessionManager  *SessionManager
	MenuRegistry    *MenuRegistry
	UserService     UserService
	LoanService     LoanService
	RateService     RateService
	PINService      PINService
	AccountNotifier contracts.AccountNotifier
	LoanNotifier    contracts.LoanNotifier

	// RepayPaybill is the mobile-money paybill number shown on the repay
	// screens — the configured M-Pesa collection shortcode. Blank hides the
	// mobile-money option entirely.
	RepayPaybill string

	// MpesaPrompter enables the STK prompt rail when the loan service
	// satisfies RepaymentPrompter.
	MpesaPrompter bool

	// AirtelPrompter enables the Airtel Money prompt rail when the loan
	// service satisfies CarrierRepaymentPrompter. Separate from
	// MpesaPrompter because the rails are enabled by separate integrations
	// and either can be live without the other.
	AirtelPrompter bool
}

HandlerDeps are the collaborators and settings a USSDHandler needs. SessionManager and MenuRegistry are required; the services may be nil, in which case the flows that need them degrade to an error screen.

type InMemoryLocalizer

type InMemoryLocalizer struct {
	// contains filtered or unexported fields
}

InMemoryLocalizer implements Localizer using an in-memory map for fast translation lookups.

func NewInMemoryLocalizer

func NewInMemoryLocalizer(defaultLang string) *InMemoryLocalizer

NewInMemoryLocalizer creates a new in-memory localizer

func (*InMemoryLocalizer) AddTranslation

func (l *InMemoryLocalizer) AddTranslation(key, language, message string)

AddTranslation adds a translation

func (*InMemoryLocalizer) Get

func (l *InMemoryLocalizer) Get(language, key string) string

Get retrieves a localized message

func (*InMemoryLocalizer) GetWithDefault

func (l *InMemoryLocalizer) GetWithDefault(language, key, defaultMsg string) string

GetWithDefault retrieves a localized message with a default fallback

func (*InMemoryLocalizer) HasKey

func (l *InMemoryLocalizer) HasKey(key string) bool

HasKey checks if a key exists

func (*InMemoryLocalizer) LoadStandardTranslations

func (l *InMemoryLocalizer) LoadStandardTranslations()

LoadStandardTranslations loads standard translations

type LoanApproval

type LoanApproval struct {
	Approved     bool
	Reason       string
	InterestRate float64
}

LoanApproval contains the result of a loan eligibility check, including approval status and terms.

type LoanProductConfig

type LoanProductConfig struct {
	ProductID         string // UUID of the loan product record.
	MinAmountCents    int64  // Minimum loan in fiat cents (e.g. 50000 = KES 500).
	MaxAmountCents    int64  // Maximum auto-approved loan in fiat cents (e.g. 300000 = KES 3,000).
	Currency          string // ISO 4217 currency code (e.g. "KES").
	DurationDays      int    // Fixed loan term in days (e.g. 30).
	RepaymentSchedule string // Repayment cadence (e.g. "lump_sum").
	InterestRateBps   int32  // Fallback annual rate in basis points; vault APR takes precedence.
	OriginationFeeBps int32  // One-off fee in bps (1 bps = 0.01 %). Zero when product has none.
}

LoanProductConfig provides loan product parameters for the USSD flow. It is loaded once at startup from the highest-priority active loan product.

type LoanRequest

type LoanRequest struct {
	UserID          string
	AccountID       string // Database UUID of the user's account record.
	StellarAddress  string // Stellar public key for vault borrow recipient.
	ProductID       string // Loan product ID from LoanProductConfig.
	PhoneNumber     string
	RecipientName   string
	NationalID      string
	CountryCode     string
	NetworkCode     string
	NetworkName     string
	PrincipalAmount int64 // USDC amount in stroops (converted from local currency).
	PrincipalAsset  string
	DurationDays    int
	RepaymentSched  string
	LocalAmount     int64   // Local currency amount in cents (e.g. KES cents).
	LocalCurrency   string  // ISO 4217 currency code (e.g. "KES").
	ConversionRate  float64 // YellowCard buy rate at quote time (e.g. 153.50).

	// Off-ramp routing — selects the provider (mobile money vs cash pickup).
	// Empty defaults to mobile money for back-compat with menus that don't
	// expose the cash-pickup branch yet.
	PayoutMethod string

	// Cash-pickup-only KYC fields (MoneyGram). Ignored by mobile-money flows.
	FirstName          string
	LastName           string
	BirthDate          string // ISO-8601 (YYYY-MM-DD)
	Address            string
	PostalCode         string
	City               string
	AddressCountryCode string
	ChildAccountIndex  uint32 // per-user Stellar derivation index for SEP-10 memo
}

LoanRequest represents a loan request from the USSD flow.

type LoanService

type LoanService interface {
	// GetUserLoans returns all loans for the given user, formatted for USSD display.
	GetUserLoans(ctx context.Context, userID string) ([]any, error)

	// RequestLoan orchestrates the full loan disbursement cycle.
	RequestLoan(ctx context.Context, req *LoanRequest) (any, error)

	// CheckLoanEligibility checks whether the user qualifies for the requested amount.
	CheckLoanEligibility(ctx context.Context, userID string, amount int64, duration int) (*LoanApproval, error)

	// GetProductConfig returns the active loan product parameters used by the
	// USSD flow (limits, duration, schedule). Returns nil when no product is configured.
	GetProductConfig() *LoanProductConfig

	// GetRepaymentQuote returns the amount currently owed on a loan, in both
	// USDC stroops and local currency cents. The USDC figure is derived from
	// the vault's current borrow_index relative to the index at origination;
	// the local figure applies the latest FX. Returns an error (hard-fail —
	// no stale fallback) when the vault or FX is unavailable.
	GetRepaymentQuote(ctx context.Context, loanID string) (*RepaymentQuote, error)

	// InitiateRepayment opens a MoneyGram cash deposit for the loan.
	//
	// It returns as soon as the request is accepted, not when the deposit
	// exists. Quoting, SEP-10 authentication, SEP-24 initiation, link
	// shortening and the SMS together took over fifteen seconds against the
	// sandbox — past the point Africa's Talking abandons a USSD session, so
	// waiting for them leaves the borrower's screen dead before it renders.
	//
	// Everything the borrower needs arrives by SMS: the interactive link on
	// success, a failure notice otherwise. Nothing on the USSD screen depends
	// on the outcome, which is what makes returning early honest rather than a
	// shortcut. An error here means the request was refused outright.
	InitiateRepayment(ctx context.Context, loanID, phoneNumber string) error
}

LoanService defines the interface for loan-related operations. Implementations live in the lending module; the USSD handler depends only on this consumer-defined interface.

type Localizer

type Localizer interface {
	// Get retrieves a localized message.
	Get(language, key string) string

	// GetWithDefault retrieves a localized message with a default fallback.
	GetWithDefault(language, key, defaultMsg string) string

	// HasKey checks if a key exists.
	HasKey(key string) bool

	// AddTranslation adds a translation.
	AddTranslation(key, language, message string)
}

Localizer defines the contract for retrieving translated strings based on language preferences.

type Menu struct {
	ID         string
	Title      map[string]string // Language to Title
	Options    []MenuOption
	ParentMenu string
}

Menu is a single screen: a localized title and the options rendered beneath it. The registry is a rendering store, not a router — USSDHandler routes on the session's current menu with an explicit switch.

func (m *Menu) GetOption(key string) (*MenuOption, error)

GetOption retrieves an option by key

func (m *Menu) Render(language string) string

Render renders a menu for display

type MenuBuilder struct {
	// contains filtered or unexported fields
}

MenuBuilder provides a fluent interface for programmatically constructing Menu instances.

func NewMenuBuilder

func NewMenuBuilder(id string) *MenuBuilder

NewMenuBuilder creates a new menu builder

func (mb *MenuBuilder) Build() *Menu

Build returns the built menu

func (mb *MenuBuilder) WithOption(key string, labels map[string]string, targetMenu string) *MenuBuilder

WithOption adds an option to the menu

func (mb *MenuBuilder) WithParentMenu(parentMenu string) *MenuBuilder

WithParentMenu sets the parent menu

func (mb *MenuBuilder) WithTitle(language, title string) *MenuBuilder

WithTitle sets the menu title for a language

type MenuOption struct {
	Key        string
	Label      map[string]string // Language to Label
	TargetMenu string
}

MenuOption represents a user-selectable choice within a Menu.

type MenuPreset interface {
	// Initialize loads menus into the registry.
	Initialize(registry *MenuRegistry)

	// GetName returns the preset name.
	GetName() string
}

MenuPreset defines a reusable collection of menus for a specific workflow or use case.

type MenuRegistry struct {
	// contains filtered or unexported fields
}

MenuRegistry stores and provides access to all configured menus in the USSD application.

func NewMenuRegistry

func NewMenuRegistry() *MenuRegistry

NewMenuRegistry creates a new menu registry

func (mr *MenuRegistry) Clear()

Clear removes all menus from the registry

func (mr *MenuRegistry) Get(menuID string) (*Menu, error)

Get retrieves a menu by ID

func (mr *MenuRegistry) GetAll() map[string]*Menu

GetAll returns all registered menus

func (mr *MenuRegistry) Register(menu *Menu)

Register registers a menu

func (mr *MenuRegistry) Unregister(menuID string)

Unregister removes a menu from the registry

type NetworkMapper

type NetworkMapper struct{}

NetworkMapper maps telcos mobile network codes (MCC + MNC) to MoMo network info

func NewNetworkMapper

func NewNetworkMapper() *NetworkMapper

NewNetworkMapper creates a new network mapper

func (*NetworkMapper) GetCountryFromMobileNetworkCode

func (m *NetworkMapper) GetCountryFromMobileNetworkCode(MobileNetworkCode string) string

GetCountryFromMobileNetworkCode returns the country code for a mobile network code

func (*NetworkMapper) MapMobileNetworkCode

func (m *NetworkMapper) MapMobileNetworkCode(MobileNetworkCode string) (*NetworkMapping, error)

MapMobileNetworkCode maps a telco's mobile network code (MCC + MNC) to MoMo network info

type NetworkMapping

type NetworkMapping struct {
	MobileNetworkCode string // Telco's mobile network code (MCC + MNC) (e.g., "63902")
	MomoNetworkCode   string // Momo network code (e.g., "MPESA")
	MomoNetworkName   string // Momo Display name (e.g., "M-Pesa")
	TelcoName         string // Telco name (e.g., "Safaricom Kenya")
	Country           string // ISO country code (e.g., "KE")
}

NetworkMapping maps telco's mobile network code (MCC + MNC) to MoMo network info

type PINService

type PINService interface {
	// SetPIN creates a new PIN for a user (during registration).
	SetPIN(ctx context.Context, userID, pin string) error

	// VerifyPIN checks the supplied PIN against the stored hash.
	// Returns (true, nil) on success, (false, nil) on wrong PIN.
	VerifyPIN(ctx context.Context, userID, pin string) (bool, error)

	// ChangePIN verifies the old PIN and sets a new one.
	ChangePIN(ctx context.Context, userID, oldPin, newPin string) error

	// ResetPIN sets a new PIN after identity verification (security questions).
	ResetPIN(ctx context.Context, userID, newPin string) error

	// IsLocked reports whether the account is locked and when the lockout expires.
	IsLocked(ctx context.Context, userID string) (bool, time.Time, error)

	// HasPIN reports whether the user has a PIN set.
	HasPIN(ctx context.Context, userID string) (bool, error)

	// SetSecurityQuestions stores hashed answers for the given questions.
	SetSecurityQuestions(ctx context.Context, userID string, questions []pin.QuestionAnswer) error

	// VerifySecurityAnswers checks the supplied answers against stored hashes.
	VerifySecurityAnswers(ctx context.Context, userID string, answers []pin.QuestionAnswer) (bool, error)

	// GetUserQuestionIDs returns the predefined question IDs the user has configured.
	GetUserQuestionIDs(ctx context.Context, userID string) ([]int, error)

	// GetRemainingAttempts returns the number of PIN attempts left before lockout.
	GetRemainingAttempts(ctx context.Context, userID string) (int, error)
}

PINService defines the interface for PIN management operations used by the USSD handler. It is satisfied by pin.Service.

type RateService

type RateService interface {
	// GetExchangeRate returns the current sell rate for the specified currency.
	GetExchangeRate(ctx context.Context, currency string) (sellRate float64, err error)
}

RateService provides exchange rate lookups for local currency conversion.

type RegisterUserRequest

type RegisterUserRequest struct {
	MobileNumber      string
	MobileCountryCode string
	NetworkCode       string
	FullName          string
	NationalID        string
	PreferredLanguage string

	// PinHash is the pre-hashed PIN (from pin.HashPIN), written atomically with
	// the user so no PIN-less account is ever persisted. Empty only when the
	// deployment has no PIN service configured.
	PinHash  string
	PinSetAt *time.Time

	// Optional SEP-9 bio (MoneyGram cash-pickup prefill). Empty when the user
	// skips the bio step. BirthDate is YYYY-MM-DD.
	BirthDate  string
	Address    string
	City       string
	PostalCode string
}

RegisterUserRequest contains the necessary information to register a new user via USSD.

type RepaymentPrompter added in v1.4.1

type RepaymentPrompter interface {
	// PromptRepayment pushes a prompt at the borrower's handset for the full
	// payoff. It returns once the prompt is accepted for delivery, not when
	// it is paid; an error means the push was refused outright.
	PromptRepayment(ctx context.Context, loanID, phoneNumber string) error
}

RepaymentPrompter is the capability of pushing a payment prompt at the borrower's handset — M-Pesa Express today. A LoanService may satisfy it; the repay rail offers the prompt only when the wired service does and the builder enables it.

type RepaymentQuote added in v1.0.0

type RepaymentQuote struct {
	LoanID             string
	AmountUSDCStroops  int64   // What the borrower owes in USDC, stroops.
	AmountLocalCents   int64   // Same, converted at the FX rate below.
	LocalCurrency      string  // ISO 4217 (e.g. "KES").
	BorrowIndexAtQuote int64   // WAD scale (1e18).
	FXRate             float64 // local-per-USD at quote time.
	QuoteSource        string  // "mg_primary" | "yc_fallback" | "stale_cache".
	AsOf               time.Time
}

RepaymentQuote is the live amount owed on a loan, computed from the vault borrow_index + current FX. Stored repayment quotes are advisory only — this is the figure to show on USSD screens, in SMS reminders, and to validate borrower payments against (with ±2 % tolerance).

type Session

type Session struct {
	SessionID     string         `json:"session_id"`
	PhoneNumber   string         `json:"phone_number"`
	ServiceCode   string         `json:"service_code"`
	NetworkCode   string         `json:"network_code"`
	UserID        string         `json:"user_id,omitempty"`
	CurrentMenu   string         `json:"current_menu"`
	PreviousMenus []string       `json:"previous_menus"`
	Language      string         `json:"language"`
	Data          map[string]any `json:"data"`
	CreatedAt     time.Time      `json:"created_at"`
	UpdatedAt     time.Time      `json:"updated_at"`
}

Session represents a single interactive USSD session with a mobile user.

type SessionManager

type SessionManager struct {
	// contains filtered or unexported fields
}

SessionManager handles the storage, retrieval, and lifecycle of USSD sessions.

func NewSessionManager

func NewSessionManager(cache *redis.Client, sessionDuration time.Duration) *SessionManager

func (*SessionManager) CreateSession

func (sm *SessionManager) CreateSession(ctx context.Context, sessionID, phoneNumber, serviceCode, networkCode string) (*Session, error)

CreateSession creates a new USSD session

func (*SessionManager) DeleteSession

func (sm *SessionManager) DeleteSession(ctx context.Context, sessionID string) error

DeleteSession deletes a session

func (*SessionManager) ExtendSession

func (sm *SessionManager) ExtendSession(ctx context.Context, sessionID string) error

ExtendSession extends the session expiration

func (*SessionManager) GetData

func (sm *SessionManager) GetData(ctx context.Context, sessionID, key string) (any, error)

GetData retrieves a data value from the session

func (*SessionManager) GetOrCreateSession

func (sm *SessionManager) GetOrCreateSession(ctx context.Context, sessionID, phoneNumber, serviceCode, networkCode string) (*Session, error)

GetOrCreateSession gets an existing session or creates a new one

func (*SessionManager) GetSession

func (sm *SessionManager) GetSession(ctx context.Context, sessionID string) (*Session, error)

GetSession retrieves a session by session ID

func (*SessionManager) GoBack

func (sm *SessionManager) GoBack(ctx context.Context, sessionID string) error

GoBack navigates to previous menu

func (*SessionManager) NavigateToMenu

func (sm *SessionManager) NavigateToMenu(ctx context.Context, sessionID, menu string) error

NavigateToMenu navigates to a new menu

func (*SessionManager) SaveSession

func (sm *SessionManager) SaveSession(ctx context.Context, session *Session) error

SaveSession saves a session to cache

func (*SessionManager) SetData

func (sm *SessionManager) SetData(ctx context.Context, sessionID, key string, value any) error

SetData sets a data value in the session

func (*SessionManager) SetLanguage

func (sm *SessionManager) SetLanguage(ctx context.Context, sessionID, language string) error

SetLanguage sets the session language

func (*SessionManager) SetUserID

func (sm *SessionManager) SetUserID(ctx context.Context, sessionID, userID string) error

SetUserID associates a user ID with the session

func (*SessionManager) UpdateSession

func (sm *SessionManager) UpdateSession(ctx context.Context, sessionID string, updates func(*Session)) error

UpdateSession updates session data

type SimplifiedMenuPreset

type SimplifiedMenuPreset struct{}

SimplifiedMenuPreset provides a streamlined set of menus with fewer steps.

func NewSimplifiedMenuPreset

func NewSimplifiedMenuPreset() *SimplifiedMenuPreset

NewSimplifiedMenuPreset creates a new instance of SimplifiedMenuPreset.

func (*SimplifiedMenuPreset) GetName

func (p *SimplifiedMenuPreset) GetName() string

GetName returns the name of the simplified menu preset.

func (*SimplifiedMenuPreset) Initialize

func (p *SimplifiedMenuPreset) Initialize(registry *MenuRegistry)

Initialize registers the simplified menu preset.

type StandardLoanMenuPreset

type StandardLoanMenuPreset struct{}

StandardLoanMenuPreset provides the standard set of menus for the default loan application flow.

func NewStandardLoanMenuPreset

func NewStandardLoanMenuPreset() *StandardLoanMenuPreset

NewStandardLoanMenuPreset creates a new instance of StandardLoanMenuPreset.

func (*StandardLoanMenuPreset) GetName

func (p *StandardLoanMenuPreset) GetName() string

GetName returns the name of the menu preset.

func (*StandardLoanMenuPreset) Initialize

func (p *StandardLoanMenuPreset) Initialize(registry *MenuRegistry)

Initialize registers all menus for the standard loan flow including the restructured main menu (4 items), PIN management menus, and security question setup menus.

type TranslationBuilder

type TranslationBuilder struct {
	// contains filtered or unexported fields
}

TranslationBuilder provides a fluent interface for populating an InMemoryLocalizer.

func NewTranslationBuilder

func NewTranslationBuilder(localizer *InMemoryLocalizer) *TranslationBuilder

NewTranslationBuilder creates a new translation builder

func (*TranslationBuilder) Add

func (tb *TranslationBuilder) Add(key string, translations map[string]string) *TranslationBuilder

Add adds a translation with multiple languages

func (*TranslationBuilder) Build

Build returns the localizer

type USSDHandler

type USSDHandler struct {
	// contains filtered or unexported fields
}

USSDHandler orchestrates the processing of incoming USSD requests, managing sessions and routing to appropriate menus.

func NewUSSDHandler

func NewUSSDHandler(deps HandlerDeps) *USSDHandler

NewUSSDHandler builds the handler.

func (*USSDHandler) HandleRequest

func (h *USSDHandler) HandleRequest(ctx context.Context, sessionID, phoneNumber, serviceCode, networkCode, input string) (string, error)

HandleRequest handles a USSD request

type USSDProvider

type USSDProvider interface {
	// ParseRequest parses the provider-specific HTTP request into a standardized USSDRequest.
	ParseRequest(ctx context.Context, data map[string]string) (*USSDRequest, error)

	// FormatResponse formats the USSDResponse into the provider-specific response format.
	FormatResponse(ctx context.Context, response *USSDResponse) (any, error)

	// GetProviderName returns the name of the USSD provider.
	GetProviderName() string

	// ValidateRequest validates the incoming request from the provider.
	ValidateRequest(ctx context.Context, data map[string]string) error
}

USSDProvider defines the contract for integrating with different telecommunication USSD gateways.

type USSDRequest

type USSDRequest struct {
	SessionID    string
	PhoneNumber  string
	Input        string
	ServiceCode  string
	NetworkCode  string
	ProviderData map[string]string // For provider-specific data
}

USSDRequest encapsulates the normalized data received from a USSD gateway provider.

type USSDResponse

type USSDResponse struct {
	Type    string // "CON" for continue, "END" for terminate
	Message string
}

USSDResponse contains the formatted data to be returned to the USSD gateway provider.

type USSDService

type USSDService struct {
	// contains filtered or unexported fields
}

USSDService manages multiple USSD providers and routes incoming requests to the appropriate handler.

func NewUSSDService

func NewUSSDService(handler *USSDHandler) *USSDService

NewUSSDService creates a new USSD service.

func (*USSDService) DeleteAllProviders

func (s *USSDService) DeleteAllProviders() error

DeleteAllProviders removes all USSD providers from the service.

func (*USSDService) DeleteProvider

func (s *USSDService) DeleteProvider(name string) error

DeleteProvider removes a USSD provider from the service.

func (*USSDService) GetProvider

func (s *USSDService) GetProvider(name string) (USSDProvider, error)

GetProvider retrieves a USSD provider by name.

func (*USSDService) GetProviders

func (s *USSDService) GetProviders() map[string]USSDProvider

GetProviders retrieves all USSD providers.

func (*USSDService) HandleRequest

func (s *USSDService) HandleRequest(ctx context.Context, providerName string, data map[string]string) (any, error)

HandleRequest processes a USSD request using the specified provider.

func (*USSDService) RegisterProvider

func (s *USSDService) RegisterProvider(name string, provider USSDProvider)

RegisterProvider registers a USSD provider with the service.

type UserService

type UserService interface {
	// GetUserWithAccounts retrieves a user and their associated accounts.
	GetUserWithAccounts(ctx context.Context, userIDOrPhone string) (any, []any, error)
	// RegisterUser creates a new user account based on the provided registration request.
	RegisterUser(ctx context.Context, req *RegisterUserRequest) (any, []any, error)
	// NationalIDExists reports whether a user is already registered with the
	// given national ID, so registration can reject a duplicate up front rather
	// than at the final atomic insert (the DB unique constraint is the real
	// guard; this is the fast, friendly path).
	NationalIDExists(ctx context.Context, nationalID string) (bool, error)

	// UpdateBio sets the optional SEP-9 bio fields on an existing user (added
	// post-registration via the account menu for faster cash pickup). Empty
	// fields are left unchanged; birthDate is YYYY-MM-DD.
	UpdateBio(ctx context.Context, userID string, bio BioUpdate) error

	// GetUserIDByNationalID resolves the account behind a national ID. Used by
	// new-SIM recovery to find the existing account when registration hits the
	// duplicate-ID case. Returns "" when no user holds that ID.
	GetUserIDByNationalID(ctx context.Context, nationalID string) (string, error)

	// RebindMobileNumber moves an account to the dialing SIM. Only called after
	// the caller has verified ownership via security questions — it changes the
	// account's identity anchor.
	RebindMobileNumber(ctx context.Context, userID, mobileNumber string) error
}

UserService defines the contract for user identity and account management operations required by the USSD flow.

Directories

Path Synopsis
Package adapters holds the USSD-to-domain glue: each file implements a narrow port interface declared elsewhere against the concrete user, account, off-ramp, and Stellar services.
Package adapters holds the USSD-to-domain glue: each file implements a narrow port interface declared elsewhere against the concrete user, account, off-ramp, and Stellar services.
providers
africastalking
Package africastalking implements the USSD provider adapter for AfricasTalking.
Package africastalking implements the USSD provider adapter for AfricasTalking.

Jump to

Keyboard shortcuts

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