Documentation
¶
Overview ¶
Package controllers is the HTTP handler layer, built on Fiber. Each controller is a thin adapter for one group of routes: it parses and validates the incoming request, calls a service, and maps the result or error onto an HTTP response. Business logic stays in the services below; controllers only translate between HTTP and those services.
Every controller is constructed with its dependencies injected (NewAuthController, NewWebhookController, and so on), so handlers hold interfaces rather than reaching for globals. The doc comments carry Swagger annotations, which drive the generated API reference.
The controllers ¶
AuthController serves the challenge-response endpoints: it issues a Stellar challenge and, on a valid signed response, returns a JWT (see the auth package).
WebhookController receives inbound payment-provider webhooks. It verifies the provider's HMAC signature against the configured secret before handing the event to the webhook layer, and rejects anything unsigned or mismatched.
USSDController and SMSCallbackController handle the mobile-gateway callbacks. The USSD handler takes the provider from the URL path, forwards the session to the USSD service, and returns the CON/END string the gateway expects; the SMS handler ingests delivery-report callbacks and passes them to the SMS layer.
Index ¶
- type AirtelCallbackController
- type AuthController
- type ChallengeResponse
- type DarajaCallbackController
- func (ctrl *DarajaCallbackController) AsyncResult(c *fiber.Ctx) error
- func (ctrl *DarajaCallbackController) AsyncTimeout(c *fiber.Ctx) error
- func (ctrl *DarajaCallbackController) C2BConfirmation(c *fiber.Ctx) error
- func (ctrl *DarajaCallbackController) C2BValidation(c *fiber.Ctx) error
- func (ctrl *DarajaCallbackController) EnableBalanceTracking(balances repository.MpesaBalanceRepository, ...)
- func (ctrl *DarajaCallbackController) Register(app fiber.Router)
- func (ctrl *DarajaCallbackController) STKCallback(c *fiber.Ctx) error
- type DarajaHakikishaController
- type LoanReferenceResolver
- type SMSCallbackController
- type USSDController
- type VerifyRequest
- type VerifyResponse
- type WebhookController
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type AirtelCallbackController ¶ added in v1.6.0
type AirtelCallbackController struct {
// contains filtered or unexported fields
}
AirtelCallbackController handles inbound Airtel Money notifications.
It differs from its Daraja sibling in two ways, both of which come from the rail rather than from taste.
Airtel can sign a callback. When callback authentication is enabled, an HmacSHA256 arrives alongside the body, and a body carrying a hash that does not verify under our key is rejected outright. That is a stronger position than Daraja allows — but a verified hash still only proves Airtel sent it, not that the payment settled, so nothing here credits anything.
A transaction can report twice. Airtel documents its callback as carrying intermediate or final status with nothing in the payload to tell them apart, so every callback is recorded — including a TF, which its Daraja counterpart deliberately drops — and the repository folds the second notification into the first row rather than treating it as a duplicate.
Loan attribution does not happen here. The callback carries no reference, only the transaction id we minted, so the row is staged unattributed and the credit-side poller — which knows which loan that id belongs to — is what ties the two together.
func NewAirtelCallbackController ¶ added in v1.6.0
func NewAirtelCallbackController(repo repository.AirtelTransactionRepository, cfg config.AirtelConfig, serverEnv string) *AirtelCallbackController
NewAirtelCallbackController wires the controller.
func (*AirtelCallbackController) CollectionCallback ¶ added in v1.6.0
func (ctrl *AirtelCallbackController) CollectionCallback(c *fiber.Ctx) error
CollectionCallback records an Airtel collection notification. @Description Record an Airtel Money collection notification as an observation for the poller to confirm @Summary Record an Airtel collection callback @Tags Airtel @Accept json @Produce json @Param slug path string true "Callback slug" @Param body body airtel.Callback true "Collection status notification" @Success 200 {string} string "Recorded" @Failure 400 {object} fiber.Error "Undecodable callback" @Failure 403 {object} fiber.Error "Source not permitted, or the hash did not verify" @Failure 500 {object} fiber.Error "Failed to record the observation" @Router /api/v1/callbacks/airtel/{slug}/collection [post]
func (*AirtelCallbackController) Register ¶ added in v1.6.0
func (ctrl *AirtelCallbackController) Register(app fiber.Router)
Register mounts the callback route. The slug is the unguessable path segment; the route hangs under it so the path cannot be enumerated.
type AuthController ¶
type AuthController struct {
// contains filtered or unexported fields
}
AuthController handles authentication endpoints including challenge generation and verification.
func NewAuthController ¶
func NewAuthController(challengeService auth.ChallengeService, jwtService *auth.JWTService, adminPublicKey string, validationService *validation.ValidatorService) *AuthController
NewAuthController creates a new AuthController with the provided services.
func (*AuthController) GetChallenge ¶
func (ctrl *AuthController) GetChallenge(c *fiber.Ctx) error
GetChallenge generates a new authentication challenge. @Description Generate a Stellar transaction challenge for authentication @Summary Get Authentication Challenge @Tags Authentication @Accept json @Produce json @Success 200 {object} ChallengeResponse "Challenge generated successfully" @Failure 500 {object} middleware.Response "Failed to generate challenge" @Router /api/v1/auth/challenge [get]
func (*AuthController) VerifyChallenge ¶
func (ctrl *AuthController) VerifyChallenge(c *fiber.Ctx) error
VerifyChallenge verifies a signed challenge and returns a JWT token. @Description Verify a signed Stellar transaction challenge and receive a JWT token @Summary Verify Challenge @Tags Authentication @Accept json @Produce json @Param body body VerifyRequest true "Signed challenge verification" @Success 200 {object} VerifyResponse "Challenge verified successfully" @Failure 400 {object} middleware.Response "Invalid request body or transaction" @Failure 401 {object} middleware.Response "Challenge verification failed" @Failure 404 {object} middleware.Response "Challenge not found" @Failure 500 {object} middleware.Response "Failed to generate token" @Router /api/v1/auth/verify [post]
type ChallengeResponse ¶
type ChallengeResponse struct {
// ChallengeID identifies this challenge; echo it back in VerifyRequest.
ChallengeID string `json:"challenge_id" example:"01h2xcejqtf2nbrexx3vqjhazz"`
// Transaction is the base64 Stellar transaction envelope (XDR) to sign, unsubmitted.
Transaction string `json:"transaction" example:"AAAAAgAAAAC..."`
// ExpiresAt is the Unix timestamp (seconds) after which this challenge is no longer valid.
ExpiresAt int64 `json:"expires_at" example:"1735689600"`
}
ChallengeResponse represents the authentication challenge response.
type DarajaCallbackController ¶ added in v1.4.1
type DarajaCallbackController struct {
// contains filtered or unexported fields
}
DarajaCallbackController handles the inbound M-Pesa notifications.
Daraja signs nothing. A callback is never evidence — it is an observation, recorded in the staging table, and credited only after the poller confirms it independently. The controller's whole job is to land the bytes honestly, in the right shape, fast.
func NewDarajaCallbackController ¶ added in v1.4.1
func NewDarajaCallbackController(repo repository.MpesaTransactionRepository, cfg config.MpesaConfig, serverEnv string, resolve LoanReferenceResolver) *DarajaCallbackController
NewDarajaCallbackController wires the controller.
func (*DarajaCallbackController) AsyncResult ¶ added in v1.4.1
func (ctrl *DarajaCallbackController) AsyncResult(c *fiber.Ctx) error
AsyncResult receives the result of a Transaction Status, Account Balance or Reversal query. The result URL and the timeout URL are distinct routes — they cannot be told apart by their payload. @Description Record the result of an asynchronous Daraja query @Summary Record an async query result @Tags Daraja @Accept json @Produce json @Param slug path string true "Callback slug" @Param kind path string true "Query family" Enums(status, balance, reversal) @Param body body mpesa.ResultEnvelope true "Result delivery" @Success 200 {string} string "Recorded" @Failure 400 {object} fiber.Error "Undecodable result" @Failure 403 {object} fiber.Error "Source not permitted" @Failure 500 {object} fiber.Error "Failed to record the observation" @Router /api/v1/callbacks/daraja/{slug}/{kind}/result [post]
func (*DarajaCallbackController) AsyncTimeout ¶ added in v1.4.1
func (ctrl *DarajaCallbackController) AsyncTimeout(c *fiber.Ctx) error
AsyncTimeout receives the queue-timeout delivery. A timeout is never a failure — it moves the record to unknown, and the poller resolves it with TransactionStatus. @Description Record a queue-timeout delivery as unknown for the poller to resolve @Summary Record an async queue timeout @Tags Daraja @Accept json @Produce json @Param slug path string true "Callback slug" @Param kind path string true "Query family" Enums(status, balance, reversal) @Param body body mpesa.ResultEnvelope true "Timeout delivery" @Success 200 {string} string "Recorded" @Failure 400 {object} fiber.Error "Undecodable result" @Failure 403 {object} fiber.Error "Source not permitted" @Failure 500 {object} fiber.Error "Failed to record the observation" @Router /api/v1/callbacks/daraja/{slug}/{kind}/timeout [post]
func (*DarajaCallbackController) C2BConfirmation ¶ added in v1.4.1
func (ctrl *DarajaCallbackController) C2BConfirmation(c *fiber.Ctx) error
C2BConfirmation receives the confirmation callback after a payment settled. @Description Record a settled C2B payment as an observation for the poller to confirm @Summary Record a C2B confirmation @Tags Daraja @Accept json @Produce json @Param slug path string true "Callback slug" @Param body body mpesa.C2BNotificationWire true "Confirmation notification" @Success 200 {string} string "Recorded" @Failure 400 {object} fiber.Error "Undecodable confirmation" @Failure 403 {object} fiber.Error "Source not permitted" @Failure 500 {object} fiber.Error "Failed to record the observation" @Router /api/v1/callbacks/daraja/{slug}/c2b/confirmation [post]
func (*DarajaCallbackController) C2BValidation ¶ added in v1.4.1
func (ctrl *DarajaCallbackController) C2BValidation(c *fiber.Ctx) error
C2BValidation receives the validation callback. Responds inside the budget with the accept/reject decision. @Description Decide whether to accept an incoming C2B payment. Any answer other than ResultCode 0 rejects it. @Summary Validate a C2B payment @Tags Daraja @Accept json @Produce json @Param slug path string true "Callback slug" @Param body body mpesa.C2BNotificationWire true "Validation notification" @Success 200 {object} mpesa.ValidationResponse "Accept/reject decision" @Failure 400 {object} fiber.Error "Undecodable notification" @Failure 403 {object} fiber.Error "Source not permitted" @Router /api/v1/callbacks/daraja/{slug}/c2b/validation [post]
func (*DarajaCallbackController) EnableBalanceTracking ¶ added in v1.4.1
func (ctrl *DarajaCallbackController) EnableBalanceTracking(balances repository.MpesaBalanceRepository, collectionFloorKES, disbursementFloorKES int64, logger *slog.Logger)
EnableBalanceTracking wires persistence and floor alerts for the Account Balance async result. Optional and additive — call it after construction when a BalancePoller is running; without it the constructor's behaviour is unchanged.
func (*DarajaCallbackController) Register ¶ added in v1.4.1
func (ctrl *DarajaCallbackController) Register(app fiber.Router)
Register mounts the callback routes on the given fiber group. The slug is the unguessable path segment; routes hang under it so the path cannot be enumerated. Note the path carries no blocked word — Daraja rejects URLs containing mpesa, safaricom, exe, exec, cmd, sql or query, which the client asserts at registration time.
func (*DarajaCallbackController) STKCallback ¶ added in v1.4.1
func (ctrl *DarajaCallbackController) STKCallback(c *fiber.Ctx) error
STKCallback receives the M-Pesa Express result. @Description M-Pesa Express payment callback @Summary Record an STK push result @Tags Daraja @Accept json @Produce json @Param slug path string true "Callback slug" @Param body body mpesa.ExpressCallbackEnvelope true "Express result delivery" @Success 200 {string} string "Recorded or dropped" @Failure 400 {object} fiber.Error "Undecodable callback" @Failure 403 {object} fiber.Error "Source not permitted" @Failure 500 {object} fiber.Error "Failed to record the observation" @Router /api/v1/callbacks/daraja/{slug}/stk/result [post]
type DarajaHakikishaController ¶ added in v1.4.1
type DarajaHakikishaController struct {
// contains filtered or unexported fields
}
DarajaHakikishaController is the OAuth issuer and resolver C2B Hakikisha calls against. Every other Daraja-facing route has Safaricom pushing us a notification we authenticate by IP and an unguessable path; this one inverts it — Safaricom authenticates to us with client_credentials, the way we authenticate to every other Daraja API. See pkg/payment/mpesa/hakikisha.go.
Deliberately not built on pkg/auth.JWTService: that issuer is hard-coupled to the admin claim shape, and the two token families must share nothing — a token minted for one must never verify against the other.
func NewDarajaHakikishaController ¶ added in v1.4.1
func NewDarajaHakikishaController(repo repository.MpesaTransactionRepository, cfg config.MpesaConfig, serverEnv string) *DarajaHakikishaController
NewDarajaHakikishaController wires the controller.
func (*DarajaHakikishaController) Register ¶ added in v1.4.1
func (ctrl *DarajaHakikishaController) Register(app fiber.Router)
Register mounts the Hakikisha routes under the same slug every other Daraja-facing route uses.
func (*DarajaHakikishaController) Resolve ¶ added in v1.4.1
func (ctrl *DarajaHakikishaController) Resolve(c *fiber.Ctx) error
Resolve answers the account-name lookup, bearer-checked against the token Token issued. @Description Resolve an account number to a display name for the payer's confirmation screen @Summary Hakikisha account resolve @Tags Daraja @Accept json @Produce json @Param slug path string true "Callback slug" @Param body body mpesa.HakikishaRequest true "Resolve request" @Success 200 {object} mpesa.HakikishaResponse "Found or not-found answer" @Failure 401 {object} map[string]string "errorCode, errorMessage" @Failure 403 {object} fiber.Error "Source not permitted" @Failure 422 {object} fiber.Error "Undecodable request" @Router /api/v1/callbacks/daraja/{slug}/hakikisha/resolve [post]
func (*DarajaHakikishaController) Token ¶ added in v1.4.1
func (ctrl *DarajaHakikishaController) Token(c *fiber.Ctx) error
Token issues a short-lived bearer token to a client_credentials caller authenticated with HTTP Basic. @Description Issue a bearer token for the Hakikisha resolve endpoint @Summary Hakikisha OAuth token @Tags Daraja @Produce json @Param slug path string true "Callback slug" @Param grant_type query string true "Must be client_credentials" @Success 200 {object} map[string]any "access_token, expires_in" @Failure 401 {object} map[string]string "errorCode, errorMessage" @Failure 403 {object} fiber.Error "Source not permitted" @Router /api/v1/callbacks/daraja/{slug}/hakikisha/oauth/token [post]
type LoanReferenceResolver ¶ added in v1.4.1
LoanReferenceResolver resolves a loan reference to a loan ID, or "" when the reference does not resolve. It is injected because the loan lookup lives in the credit module.
type SMSCallbackController ¶
type SMSCallbackController struct {
// contains filtered or unexported fields
}
SMSCallbackController handles SMS callback endpoints.
func NewSMSCallbackController ¶
func NewSMSCallbackController(handler *sms.DeliveryReportHandler) *SMSCallbackController
NewSMSCallbackController creates a new SMSCallbackController.
func (*SMSCallbackController) HandleDeliveryReport ¶
func (ctrl *SMSCallbackController) HandleDeliveryReport(c *fiber.Ctx) error
HandleDeliveryReport processes incoming SMS delivery report callbacks from Africa's Talking. AT sends form-encoded POST requests with delivery status updates. @Description Handle incoming SMS delivery report callbacks from Africa's Talking @Summary Process SMS Delivery Report @Tags Mobile @Accept application/x-www-form-urlencoded @Produce plain @Param id formData string true "AT message ID" @Param status formData string true "Delivery status" @Param phoneNumber formData string false "Recipient phone number" @Param networkCode formData string false "Network code" @Param failureReason formData string false "Failure reason (if applicable)" @Success 200 {string} string "ok" @Failure 400 {object} fiber.Error "Invalid callback payload" @Router /api/v1/mobile/sms/{provider}/delivery [post]
type USSDController ¶
type USSDController struct {
// contains filtered or unexported fields
}
USSDController handles USSD callback endpoints.
func NewUSSDController ¶
func NewUSSDController(ussdService *ussd.USSDService) *USSDController
NewUSSDController creates a new USSDController with the provided USSD service.
func (*USSDController) HandleCallback ¶
func (ctrl *USSDController) HandleCallback(c *fiber.Ctx) error
HandleCallback handles incoming USSD callback requests from any registered provider. @Description Handle USSD callback requests from the USSD gateway. The provider is specified in the URL path. @Summary USSD Callback Handler @Tags Mobile @Accept application/x-www-form-urlencoded @Produce plain @Param provider path string true "USSD provider name (e.g. africastalking)" @Param sessionId formData string true "Session ID" @Param phoneNumber formData string true "User's phone number" @Param text formData string false "User input text" @Param serviceCode formData string true "USSD service code" @Param networkCode formData string false "Network code" @Success 200 {string} string "USSD response (CON/END)" @Failure 500 {string} string "END An error occurred. Please try again." @Router /api/v1/mobile/ussd/{provider} [post]
type VerifyRequest ¶
type VerifyRequest struct {
// ChallengeID is the ID returned by GET /auth/challenge.
ChallengeID string `json:"challenge_id" validate:"required,base64url" example:"01h2xcejqtf2nbrexx3vqjhazz"`
// SignedTransaction is the challenge transaction XDR, signed by the account's Stellar keypair.
SignedTransaction string `json:"signed_transaction" validate:"required,stellar_xdr" example:"AAAAAgAAAAC..."`
}
VerifyRequest represents the challenge verification request body.
type VerifyResponse ¶
type VerifyResponse struct {
// Token is the issued JWT, sent as a Bearer token on subsequent requests.
Token string `json:"token" example:"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."`
// ExpiresAt is the Unix timestamp (seconds) at which Token expires.
ExpiresAt int64 `json:"expires_at" example:"1735689600"`
}
VerifyResponse represents the challenge verification response.
type WebhookController ¶
type WebhookController struct {
// contains filtered or unexported fields
}
WebhookController handles webhook endpoints for payment providers.
func NewWebhookController ¶
func NewWebhookController(eventHandler webhook.WebhookEventHandler, apiKey, secretKey string) *WebhookController
NewWebhookController creates a new WebhookController with the provided dependencies. apiKey and secretKey are the YellowCard API credentials.
func (*WebhookController) HandleYellowCardWebhook ¶
func (ctrl *WebhookController) HandleYellowCardWebhook(c *fiber.Ctx) error
HandleYellowCardWebhook processes incoming webhooks from YellowCard. @Description Handle incoming webhooks from YellowCard payment service @Summary Process YellowCard Payment Webhook @Tags Webhooks @Accept json @Produce json @Param X-YC-Signature header string true "HMAC signature for verification" @Param body body yellowcard.WebhookEvent true "Webhook event data" @Success 200 {string} string "ok" @Failure 400 {object} fiber.Error "Invalid webhook payload" @Failure 401 {object} fiber.Error "Signature verification failed" @Failure 500 {object} fiber.Error "Failed to process webhook" @Router /api/v1/webhooks/yellowcard [post]