As the name of the project suggests, this is a simple implementation of MPESA. If you are East-African you already know what MPESA is, but if you don't, I will have a description herein to help you understand inner workings that you may not have been aware of.
Click here to learn about What MPESA is
Whatever follows, is the design and architecture of our simple MPESA application.
This application is built as a Monolith
application following Domain Driven Design principles. The application uses a
postgres database for storage.
Domain Driven Design is at the heart of our simple mpesa application.
DDD principles have concepts called contexts. The application uses the following bounded contexts:
- Admin
- Agent
- Merchant
- Subscriber
- Transaction
- Account
- Statement
- Tariff
- Auth
- Customer
The application needs some form of administration by a super user charge with the responsibility of running and maintaining
the application to ensure reliability and stability. This user is an admin
and is given their own bounded context. Some
responsibilities/actions of this user are:
- Can login to system or register.
- Can assign float to a Super Agent.
- Can configure tariff
- Can suspend/change status of a customer account
- Can view/edit/delete customer accounts
As the application grows and scales the administrator context would have more responsibilities.
-
The application would need more than one administrator and more so more than one category of administrators. In the case of MPESA, some examples of administrators with their roles include:
i. Customer Care - is a part admin who would assist customers with information about the system and troubleshoot problems.
ii. Finance - is a part admin whose responsibilities would be financial and accounting aspect in the system.
iii. IT - an admin whose responsible for the infrastructure that the system runs on.
iv.
We have acquired or developed a wallet and money transfer service for a Telco and we have been given the go ahead by the Central Bank to deploy the application and get some customers to use our system. There are however some initial steps the business has to perform to start onboarding new customers. The system should be able to have some level of autonomy when it comes to the flow of money. That is where agents come in.
The initial obstacle in the pilot was gaining the agent’s trust and encouraging them to process cash withdrawals and agent training.
Source wikipedia
Our first initial steps before we can roll out
- Acquire and entity licensed to hold public money. A bank.
- Create a super agent(s) whose task would be depositing money to our bank account.
- Once a super agent deposits to our account, we assign them with an equivalent amount of float they can sell to other agents.
- When we onboard an ordinary agent, they will have a balance of zero, and they will approach the super agent to get float.
Agents are important customers to the system. They can also have various categories depending on the business use case. For our example we have 2 types of agents:
- Super Agent
- Ordinary Agent
MPESA has 2 types of merchants.
- A merchant to provides utility services to their customers
- A merchant that sells goods and services to customers
Both merchants have unique ways of how customers pay for their services/goods. However both merchants have an account number.
- Pay bill number as an account number for a merchant - Customer provides the
pay bill number
,a customer account number
and theamount
. - Till number as an account number for a merchant - Customer provides the
till number
andamount
.
Pay bill number
is usually given to utility companies that need to identify from whom the payment is coming from by the
customer account number
.
Till number
is usually given to small scale traders that want to accept payment via MPESA from their customers.
For our example we stick to one general merchant that accepts payment.
A subscriber does not have much going on. They can authenticate and perform a transaction.
Contains all business logic in regard to transactions happening in the system. It enforces the transaction rules and business policy.
Business policies:
- A transaction cannot happen between identical customers i.e a customer cannot transact with themselves
- A deposit cannot be done by none other customer than an agent
- A customer cannot perform a withdrawal with no other customer than an agent
- A super agent is however only allowed to do deposits for other agents only
- Customers are not allowed to deposit, withdraw or transfer money below the minimum amount allowed
- Apply transaction fee as per the tariff configured
The main responsibility of this context is managing customer accounts/wallets. Responsibilities:
- Updating account balances, credit/debit accounts
- Updating system ledger after changing account balances
The main responsibility of this context is managing the system ledger. If we scale the system, we can view this ledger
as the statements/transactions event store. Borrowing from event sourcing
design, our statement context is a record
of every event with customer transactions.
This context has a responsibility of configuring and maintaining the tariff used in various transactions.
The system has 4 different types of users, admin
, agent
, subscriber
and merchant
. The auth context is responsible
for authenticating and authorizing these users into the system.
This context is mainly an aggregator of the agent
, merchant
and subscriber
contexts. It exposes common functionality
for, which can be used in other core contexts.
To begin with, the application uses postgres as the backend database.
The application uses postgres as the database server. So here are instructions on how to setup postgresql on your machine using docker.
Get the official postgres docker image.
$ docker pull postgres
Then create a container from the image with the following variables
$ docker create \
--name mpesa-db \
-e POSTGRES_USER=mpesa \
-e POSTGRES_PASSWORD=mpesa \
-p 5432:5432 \
postgres
Run the following command to start the container
$ docker start mpesa-db
Running the application is as simple as running any other go application but first we need to copy and create our configuration.
$ git clone https://github.com/SirWaithaka/simple-mpesa.git
$ cd simple-mpesa
$ cp config.yml.example config.yml
This configuration file looks something like this
database:
host: "127.0.0.1"
port: "5432"
user: "mpesa"
password: "mpesa"
dbname: "mpesa"
app_secret_key: "eQig7GS4cHO2su"
You can change the config variables depending on your database setup, here I choose to follow the default setup shown at database installation step.
$ mkdir bin
$ go build -o bin/mpesa-server cmd/mpesa-server.go
$ ./bin/mpesa-server
It will install all dependencies required and produce a binary for your platform.
Make sure you have docker installed and working properly.
$ docker build -t simple-mpesa:latest .
$ docker container create --network=host --name mpesa-server --restart unless-stopped simple-mpesa
$ docker container start mpesa-server
The server will start at port 6700
.
Enjoy.
A description of the api.
All the routes exposed in the application are all defined in this function
func apiRouteGroup(api fiber.Router, domain *registry.Domain, config app.Config) {
api.Post("/login/:user_type", user_handlers.Authenticate(domain, config))
api.Post("/user/:user_type", user_handlers.Register(domain))
// create group at /api/admin
admin := api.Group("/admin", middleware.AuthByBearerToken(config.Secret))
admin.Post("/assign-float", user_handlers.AssignFloat(domain.Admin))
admin.Post("/update-charge", user_handlers.UpdateCharge(domain.Tariff))
admin.Get("/get-tariff", user_handlers.GetTariff(domain.Tariff))
admin.Put("/super-agent-status", user_handlers.UpdateSuperAgentStatus(domain.Agent))
// create group at /api/account
account := api.Group("/account", middleware.AuthByBearerToken(config.Secret))
account.Get("/balance", account_handlers.BalanceEnquiry(domain.Account))
account.Get("/statement", account_handlers.MiniStatement(domain.Statement))
// create group at /api/transaction
transaction := api.Group("/transaction", middleware.AuthByBearerToken(config.Secret))
transaction.Post("/deposit", transaction_handlers.Deposit(domain.Transactor))
transaction.Post("/transfer", transaction_handlers.Transfer(domain.Transactor))
transaction.Post("/withdraw", transaction_handlers.Withdraw(domain.Transactor))
}
The routes are mounted on the prefix /api
so your requests should point to
POST /api/login/<user_type> <-- user_type can be either of agent, admininistrator, merchant, subscriber
POST /api/user/<user_type> # for registration <-- user_type can be either of agent, admininistrator, merchant, subscriber
POST /api/admin/assign-float
POST /api/admin/update-charge
GET /api/admin/get-tariff
PUT /api/admin/super-agent-status
GET /api/account/balance
POST /api/account/statement
POST /api/transaction/deposit
POST /api/transaction/transfer
POST /api/transaction/withdraw
The api can be used to register 4 types of users: admin
, agent
, merchant
and subscriber
An admin can be registered to the api with the following POST
parameters
firstName
, lastName
, email
, password
Curl request example
curl --request POST \
--url http://localhost:6700/api/user/administrator \
--header 'content-type: application/x-www-form-urlencoded' \
--data firstName=Admin \
--data lastName=Waithaka \
--data [email protected] \
--data password=mnbvcxz
Response example
{
"status": "success",
"message": "user created",
"data": {
"userID": "ac8f944b-b0aa-4029-9caf-dfe67007bc84",
"userType": "administrator"
}
}
At minimum, you need to create 2 agents, one of which will become a super agent
. An agent can be registered to the api
with the following POST
parameters
firstName
, lastName
, email
, phoneNumber
, password
Curl request example
curl --request POST \
--url http://localhost:6700/api/user/agent \
--header 'content-type: application/x-www-form-urlencoded' \
--data firstName=Agent \
--data lastName=Waithaka \
--data [email protected] \
--data phoneNumber=254700000000 \
--data password=mnbvcxz
Response example
{
"status": "success",
"message": "user created",
"data": {
"userID": "cca7d227-74ae-4d47-aae8-a0ab952aac28",
"userType": "agent"
}
}
A merchant can be registered to the api with the following POST
parameters
firstName
, lastName
, email
, phoneNumber
, password
Curl request example
curl --request POST \
--url http://localhost:6700/api/user/merchant \
--header 'content-type: application/x-www-form-urlencoded' \
--data firstName=Merchant \
--data lastName=Waithaka \
--data [email protected] \
--data phoneNumber=254700000000 \
--data password=mnbvcxz
Response example
{
"status": "success",
"message": "user created",
"data": {
"userID": "c3a81710-ef66-47d9-adc9-f365a324ed5c",
"userType": "merchant"
}
}
A subscriber can be registered to the api with the following POST
parameters
firstName
, lastName
, email
, phoneNumber
, password
Curl request example
curl --request POST \
--url http://localhost:6700/api/user/subscriber \
--header 'content-type: application/x-www-form-urlencoded' \
--data firstName=Subscriber \
--data lastName=Waithaka \
--data [email protected] \
--data phoneNumber=254700000000 \
--data password=mnbvcxz
Response example
{
"status": "success",
"message": "user created",
"data": {
"userID": "cf9d8f28-357e-4ac7-9b5f-eaa8609e6c2f",
"userType": "subscriber"
}
}
You can use the following POST
parameters for login with any of the 4 users
email
, password
Curl request example for subscriber login
curl --request POST \
--url http://localhost:6700/api/login/subscriber \
--header 'content-type: application/x-www-form-urlencoded' \
--data [email protected] \
--data password=mnbvcxz
Response example
{
"userId": "cf9d8f28-357e-4ac7-9b5f-eaa8609e6c2f",
"userType": "subscriber",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6ImNmOWQ4ZjI4LTM1N2UtNGFjNy05YjVmLWVhYTg2MDllNmMyZiIsInVzZXJUeXBlIjoic3Vic2NyaWJlciJ9LCJleHAiOjE2MDU0MTYwNjAsImlhdCI6MTYwNTM5NDQ2MH0.lAJ4WpF2Mnfg52iuTOoPV8nvbHV3JrMQOC-5xXrQ5EE"
}
NOTE: The remaining endpoints require the token acquired above for authentication
There are some initial setups that need to be done before you can begin doing transactions.
Before you can start transacting, you need to login as an administrator and create a super agent by changing the status of an existing agent. When registering an agent, you ought to have created at minimum 2 agents. It is now that we need make one of those agents a super agent.
The following endpoint is used to update the super agent status
of an agent.
PUT /api/admin/super-agent-status
requires the following post parameters: email
Curl request example
curl --request PUT \
--url http://localhost:6700/api/admin/super-agent-status \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6Ijc2YmM0YWEzLTAyNWQtNGQ1YS1hNWZiLWY1NDk1NTdmNjM0YSIsInVzZXJUeXBlIjoiYWRtaW5pc3RyYXRvciJ9LCJleHAiOjE2MDU0NTE4MDUsImlhdCI6MTYwNTQzMDIwNX0.8lTWl9hGr9GTST7WpEpzKdm_gqhMkf4qUellLx4o5bw' \
--header 'content-type: application/x-www-form-urlencoded' \
--data [email protected]
Response example
{
"status": "success",
"message": "Super Agent Status updated"
}
Logged in as an administrator, you need to assign float to your super-agent
using the following endpoint
POST /api/admin/assign-float
Curl request example
curl --request POST \
--url http://localhost:6700/api/admin/assign-float \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6ImE3OGVjNjNhLTA0ZWItNDAzNC1iZmVkLTBhNmMwMjU3ZTJlNCIsInVzZXJUeXBlIjoiYWRtaW5pc3RyYXRvciJ9LCJleHAiOjE2MDUzMjExNzUsImlhdCI6MTYwNTI5OTU3NX0.4fGlMJQB-eylKwOAwa4d16nVQQt3uYgwPbUjYt7j9zA' \
--header 'content-type: application/x-www-form-urlencoded' \
--data [email protected] \
--data amount=100000
Response example
{
"status": "success",
"message": "Float has been assigned.",
"data": {
"balance": 100000
}
}
The super-agent
is limited to depositing to agents only. You will need to transfer the acquired float to other agents
you have registered.
The default tariff in the system is set to zero amount for all chargeable transactions. You could begin testing transactions using the default tariff and later choose to configure your own tariff. Choose your poison :-).
You can configure a tariff by updating the available charges. The system doesn't allow you to add any other charge band.
GET /api/admin/get-tariff
- use this endpoint to get the available configured transaction charges
Response example
{
"status": "success",
"message": "Tariff retrieved",
"data": [
{
"id": "acf3e6bf-c9de-45b4-a8b6-bf97f92b783a",
"txnOperation": "WITHDRAW",
"srcUserType": "subscriber",
"destUserType": "agent",
"fee": 0
},
{
"id": "0e5a4aaa-135a-4464-96c9-d021f769bdb7",
"txnOperation": "WITHDRAW",
"srcUserType": "merchant",
"destUserType": "agent",
"fee": 0
},
{
"id": "243e7ecc-c2dd-41bb-9953-1278050bfb64",
"txnOperation": "WITHDRAW",
"srcUserType": "agent",
"destUserType": "agent",
"fee": 0
},
{
"id": "f8835176-316c-49de-b001-687e2c4a338d",
"txnOperation": "TRANSFER",
"srcUserType": "agent",
"destUserType": "agent",
"fee": 0
},
{
"id": "4edeb6d0-37cd-4c67-997a-0b3fa93b722d",
"txnOperation": "TRANSFER",
"srcUserType": "subscriber",
"destUserType": "subscriber",
"fee": 0
},
{
"id": "94c0ae8b-a131-41b9-b5af-5235b8926fa4",
"txnOperation": "TRANSFER",
"srcUserType": "merchant",
"destUserType": "subscriber",
"fee": 0
},
{
"id": "450e4baa-58c3-41b3-abe5-a55555492e0c",
"txnOperation": "TRANSFER",
"srcUserType": "subscriber",
"destUserType": "merchant",
"fee": 0
},
{
"id": "3623a89f-c496-41c8-b6c9-73429cc4ef9d",
"txnOperation": "TRANSFER",
"srcUserType": "agent",
"destUserType": "merchant",
"fee": 0
}
]
}
POST /api/admin/update-charge
- use this endpoint to update a charge using its id
.
You need the following POST
parameters
amount
, chargeId
- The amount should be in cents
.
Curl request example
curl --request POST \
--url http://localhost:6700/api/admin/update-charge \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6Ijc2YmM0YWEzLTAyNWQtNGQ1YS1hNWZiLWY1NDk1NTdmNjM0YSIsInVzZXJUeXBlIjoiYWRtaW5pc3RyYXRvciJ9LCJleHAiOjE2MDU0NTE4MDUsImlhdCI6MTYwNTQzMDIwNX0.8lTWl9hGr9GTST7WpEpzKdm_gqhMkf4qUellLx4o5bw' \
--header 'content-type: application/x-www-form-urlencoded' \
--data amount=1050 \
--data chargeId=acf3e6bf-c9de-45b4-a8b6-bf97f92b783a
Response example
{
"status": "success",
"message": "charge configured"
}
While configuring a charge requires you to provide the amount in cents
, performing transactions requires the amount to
be in whole units i.e. shillings
Transacting also requires you to provide an accountNo
, use the email
of the customer as the accountNo
customerType
can be either of agent
, merchant
or subscriber
A deposit is only done by an agent
. You need an agent
token to perform this transaction.
You need the following POST
parameters
amount
, accountNo
and customerType
Curl request example
curl --request POST \
--url http://localhost:6700/api/transaction/deposit \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6ImNjYTdkMjI3LTc0YWUtNGQ0Ny1hYWU4LWEwYWI5NTJhYWMyOCIsInVzZXJUeXBlIjoiYWdlbnQifSwiZXhwIjoxNjA1MzIxODEyLCJpYXQiOjE2MDUzMDAyMTJ9.jFLfjScuvHaOV68n11sRticy2ntzQRhwbNq5E4sPmQI' \
--header 'content-type: application/x-www-form-urlencoded' \
--data amount=400 \
--data [email protected] \
--data customerType=subscriber
Response example
{
"status": "success",
"message": "Success",
"data": {
"message": "Transaction under processing. You will receive a message shortly."
}
}
You need the following POST
parameters
amount
, agentNumber
Use agent email for agentNumber
Curl request example
curl --request POST \
--url http://localhost:6700/api/transaction/withdraw \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6ImNmOWQ4ZjI4LTM1N2UtNGFjNy05YjVmLWVhYTg2MDllNmMyZiIsInVzZXJUeXBlIjoic3Vic2NyaWJlciJ9LCJleHAiOjE2MDU0MTYwNjAsImlhdCI6MTYwNTM5NDQ2MH0.lAJ4WpF2Mnfg52iuTOoPV8nvbHV3JrMQOC-5xXrQ5EE' \
--header 'content-type: application/x-www-form-urlencoded' \
--data amount=40 \
--data [email protected]
Response example
{
"status": "success",
"message": "Success",
"data": {
"message": "Transaction under processing. You will receive a message shortly."
}
}
You need the following POST
parameters
amount
, accountNo
and customerType
Curl request example
curl --request POST \
--url http://localhost:6700/api/transaction/transfer \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6ImNmOWQ4ZjI4LTM1N2UtNGFjNy05YjVmLWVhYTg2MDllNmMyZiIsInVzZXJUeXBlIjoic3Vic2NyaWJlciJ9LCJleHAiOjE2MDUzMjE5MDMsImlhdCI6MTYwNTMwMDMwM30.vLiHdNTr4onTVqUZbLbdpwgbH98VYzHJJU-JKtFOHVg' \
--header 'content-type: application/x-www-form-urlencoded' \
--data amount=30 \
--data [email protected] \
--data customerType=merchant
Response example
{
"status": "success",
"message": "Success",
"data": {
"message": "Transaction under processing. You will receive a message shortly."
}
}
This is just a GET
request, no params
Curl request example
curl --request GET \
--url http://localhost:6700/api/account/balance \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6ImNmOWQ4ZjI4LTM1N2UtNGFjNy05YjVmLWVhYTg2MDllNmMyZiIsInVzZXJUeXBlIjoic3Vic2NyaWJlciJ9LCJleHAiOjE2MDUzNjY3NTMsImlhdCI6MTYwNTM0NTE1M30.-Piib6bXzYqb0S8nLo76SBTyGmWi7UPUMExptIcqBZI'
Response example
{
"status": "success",
"message": "Your current balance is 690",
"data": {
"userID": "cf9d8f28-357e-4ac7-9b5f-eaa8609e6c2f",
"balance": 690
}
}
This is just a GET
request, no params
Curl request example
curl --request GET \
--url http://localhost:6700/api/account/statement \
--header 'authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7InVzZXJJZCI6Ijk4YmNmMmY1LWRiY2ItNDk1NS04NTU0LTc0OWYxMTVhZjU5OCIsImVtYWlsIjoiIn0sImV4cCI6MTYwNDA2OTE0MywiaWF0IjoxNjA0MDQ3NTQzfQ.IYyclrC66aweehs_A4Sigmc83a27udmPofM2yOeut9Q'
Response example
{
"status": "success",
"message": "mini statement retrieved for the past 5 transactions",
"data": {
"message": "mini statement retrieved for the past 5 transactions",
"userID": "cf9d8f28-357e-4ac7-9b5f-eaa8609e6c2f",
"transactions": [
{
"transactionId": "97c3ff6d-72d5-479d-8838-85a5c32985a2",
"transactionType": "DEPOSIT",
"createdAt": "2020-11-14T01:59:28.613007+03:00",
"creditedAmount": 400,
"debitedAmount": 0,
"userId": "cf9d8f28-357e-4ac7-9b5f-eaa8609e6c2f",
"accountId": "63978e26-9c0d-40eb-a24b-d1ae51e21942"
},
{
"transactionId": "4be4a008-b18e-4d4b-95d3-58b660d5b931",
"transactionType": "TRANSFER",
"createdAt": "2020-11-14T01:59:05.949066+03:00",
"creditedAmount": 0,
"debitedAmount": 30,
"userId": "cf9d8f28-357e-4ac7-9b5f-eaa8609e6c2f",
"accountId": "63978e26-9c0d-40eb-a24b-d1ae51e21942"
},
{
"transactionId": "45da6c6a-03d8-4d58-849a-fd80bbfabbb4",
"transactionType": "TRANSFER",
"createdAt": "2020-11-14T01:57:04.622507+03:00",
"creditedAmount": 0,
"debitedAmount": 40,
"userId": "cf9d8f28-357e-4ac7-9b5f-eaa8609e6c2f",
"accountId": "63978e26-9c0d-40eb-a24b-d1ae51e21942"
}
]
}
}
Tests have not been written for the application but i have very important talks i would share here that i cant recommend enough about how to go about testing the application.
An approach to testing this application would be something in the following lines.
- Test the code in the interactor files
- Test the code in the repository files
The above files carry the bulk of the behaviour of the whole application, they are the business logic of the application. The rest of the files are just implementation details that could change rapidly and the tests written for them would certainly fail after change.
e.g. the http handler functions in the application use gofiber, writing unit tests for them is good but not desired, because gofiber can be replaced with mux easily and that would break your tests.