Skip to content

Commit

Permalink
Merge pull request #105 from VictorKabata/feature-account-balance
Browse files Browse the repository at this point in the history
Added account balance feature
  • Loading branch information
VictorKabata authored May 5, 2024
2 parents 68518c9 + 93cfb6d commit 12ded5d
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 48 deletions.
47 changes: 46 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,15 @@ supports integration with your Android(Kotlin/Java), iOS(Swift) and JVM applicat
- [Dynamic QR](#generate-dynamic-qr-code)
- [Query M-Pesa Transaction](#query-m-pesa-transaction)
- [Customer To Business(C2B)](#customer-to-businessc2b)
- [Account Balance](#account-balance)
- [iOS - Swift](#ios---swift)
- [Setting Up](#setting-up-1)
- [Request Access Token](#request-access-token)
- [Initiate M-Pesa Express STK Request](#initiate-m-pesa-express-stk-request-1)
- [Dynamic QR](#generate-dynamic-qr-code)
- [Query M-Pesa Transaction](#query-m-pesa-transaction-1)
- [Customer To Business(C2B)](#customer-to-businessc2b)
- [Account Balance](#account-balance)

## Prerequisite

Expand All @@ -55,7 +57,7 @@ The SDK offers the following functionalities from the Daraja API:
- [ ] Business To Customer (B2C) - Transact between an M-Pesa short code to a phone number
registered on M-Pesa.
- [x] Transaction Status - Check the status of a transaction.
- [ ] Account Balanace - Enquire the balance on an M-Pesa BuyGoods (Till Number)
- [x] Account Balance - Enquire the balance on an M-Pesa BuyGoods (Till Number)
- [ ] Reversal - Reverses an M-Pesa transaction.
- [ ] Tax Remittance - This API enables businesses to remit tax to Kenya Revenue Authority (KRA).
- [ ] Business Pay Bill - Pay bills directly from your business account to a pay bill number, or a
Expand Down Expand Up @@ -234,6 +236,28 @@ c2bResponse.onSuccess {
}
```

### Account Balance

Request the account balance of a short code. This can be used for both B2C, buy goods and pay bill
accounts.

```kotlin
val accountBalanceResponse = daraja.accountBalance(
initiator = "testapi",
initiatorPassword = "Safaricom999!*!",
partyA = 600987,
identifierType = DarajaIdentifierType.TILL_NUMBER,
queueTimeOutURL = "https://mydomain.com/AccountBalance/queue/",
resultURL = "https://mydomain.com/AccountBalance/result/"
)

accountBalanceResponse.onSuccess {
// Successfully request account balance
}.onFailure {
// Failed to request account balance
}
```

# iOS - Swift <img src="assets/swift_logo.png" width="40" />

### Setting Up
Expand Down Expand Up @@ -337,5 +361,26 @@ darajaTransactionResponse.onSuccess(action: { data in
}).onFailure(action: { error in
// Failure fetching M-pesa transaction status
})
```

### Account Balance

Request the account balance of a short code. This can be used for both B2C, buy goods and pay bill
accounts.

```swift
var accountBalanceResponse = daraja.accountBalance(
initiator = "testapi",
initiatorPassword = "Safaricom999!*!",
partyA = 600987,
identifierType = DarajaIdentifierType.TILL_NUMBER,
queueTimeOutURL = "https://mydomain.com/AccountBalance/queue/",
resultURL = "https://mydomain.com/AccountBalance/result/"
)

accountBalanceResponse.onSuccess(action: { data in
// Successfully request account balance
}).onFailure(action: { error in
// Failed to request account balance
})
```
108 changes: 77 additions & 31 deletions daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package com.vickbt.darajakmp

import com.vickbt.darajakmp.network.DarajaApiService
import com.vickbt.darajakmp.network.DarajaHttpClientFactory
import com.vickbt.darajakmp.network.models.AccountBalanceRequest
import com.vickbt.darajakmp.network.models.AccountBalanceResponse
import com.vickbt.darajakmp.network.models.C2BRegistrationRequest
import com.vickbt.darajakmp.network.models.C2BRequest
import com.vickbt.darajakmp.network.models.C2BResponse
Expand All @@ -30,13 +32,15 @@ import com.vickbt.darajakmp.network.models.MpesaExpressRequest
import com.vickbt.darajakmp.network.models.MpesaExpressResponse
import com.vickbt.darajakmp.utils.C2BResponseType
import com.vickbt.darajakmp.utils.DarajaEnvironment
import com.vickbt.darajakmp.utils.DarajaIdentifierType
import com.vickbt.darajakmp.utils.DarajaResult
import com.vickbt.darajakmp.utils.DarajaTransactionCode
import com.vickbt.darajakmp.utils.DarajaTransactionType
import com.vickbt.darajakmp.utils.getDarajaPassword
import com.vickbt.darajakmp.utils.getDarajaPhoneNumber
import com.vickbt.darajakmp.utils.getDarajaTimestamp
import io.ktor.client.HttpClient
import io.ktor.util.encodeBase64
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.runBlocking
Expand Down Expand Up @@ -178,6 +182,46 @@ class Daraja(
darajaApiService.initiateMpesaExpress(mpesaExpressRequest = mpesaExpressRequest)
}

/**Generate a dynamic qr code to initiate payment
*
* @param [merchantName] Name of the company/M-Pesa merchant name
* @param referenceNumber Transaction reference
* @param amount The total amount for the sale/transaction.
* @param transactionCode Transaction Type. The supported types are:
* BG: Pay Merchant (Buy Goods).
*
* WA: Withdraw Cash at Agent Till.
*
* PB: Paybill or Business number.
*
* SM: Send Money(Mobile number)
*
* SB: Sent to Business. Business number CPI in MSISDN format.
* @param cpi Credit Party Identifier. Can be a mobile number, business number, agent till, paybill or business number, or merchant buy goods.
* @param size Size of the QR code image in pixels. QR code image will always be a square image.
*
* @return [DynamicQrResponse]
* */
fun generateDynamicQr(
merchantName: String,
referenceNumber: String,
amount: Int,
transactionCode: DarajaTransactionCode,
cpi: String,
size: Int
): DarajaResult<DynamicQrResponse> = runBlocking(Dispatchers.IO) {
val dynamicQrRequest = DynamicQrRequest(
merchantName = merchantName,
referenceNumber = referenceNumber,
amount = amount,
transactionCode = transactionCode.name,
cpi = cpi,
size = size.toString()
)

darajaApiService.generateDynamicQr(dynamicQrRequest = dynamicQrRequest)
}

/**Request the status of an Mpesa payment transaction
*
* @param [businessShortCode] This is organizations shortcode (Paybill or Buygoods - A 5 to 7 digit account number) used to identify an organization and receive the transaction.
Expand Down Expand Up @@ -250,41 +294,43 @@ class Daraja(
darajaApiService.c2b(c2bRequest = c2bRequest)
}

/**Generate a dynamic qr code to initiate payment
*
* @param [merchantName] Name of the company/M-Pesa merchant name
* @param referenceNumber Transaction reference
* @param amount The total amount for the sale/transaction.
* @param transactionCode Transaction Type. The supported types are:
* BG: Pay Merchant (Buy Goods).
/**Request the account balance of a short code. This can be used for both B2C, buy goods and pay bill accounts.
*
* WA: Withdraw Cash at Agent Till.
* @param [initiator] This is the credential/username used to authenticate the transaction request
* @param [initiatorPassword] This is the credential/password used to authenticate the account balance request
* @param [commandId] A unique command is passed to the M-PESA system. Max length is 64.
* @param [partyA] The shortcode of the organization querying for the account balance.
* @param [identifierType] Type of organization querying for the account balance.
* @param [remarks] Comments that are sent along with the transaction
* @param [queueTimeOutURL] The end-point that receives a timeout message.
* @param [resultURL] It indicates the destination URL which Daraja should send the result message to.
*
* PB: Paybill or Business number.
*
* SM: Send Money(Mobile number)
*
* SB: Sent to Business. Business number CPI in MSISDN format.
* @param cpi Credit Party Identifier. Can be a mobile number, business number, agent till, paybill or business number, or merchant buy goods.
* @param size Size of the QR code image in pixels. QR code image will always be a square image.
* @return [AccountBalanceResponse]
* */
fun generateDynamicQr(
merchantName: String,
referenceNumber: String,
amount: Int,
transactionCode: DarajaTransactionCode,
cpi: String,
size: Int
): DarajaResult<DynamicQrResponse> = runBlocking(Dispatchers.IO) {
val dynamicQrRequest = DynamicQrRequest(
merchantName = merchantName,
referenceNumber = referenceNumber,
amount = amount,
transactionCode = transactionCode.name,
cpi = cpi,
size = size.toString()
fun accountBalance(
initiator: String,
initiatorPassword: String,
commandId: String = "AccountBalance",
partyA: Int,
identifierType: DarajaIdentifierType,
remarks: String = "Account balance request",
queueTimeOutURL: String,
resultURL: String
): DarajaResult<AccountBalanceResponse> = runBlocking(Dispatchers.IO) {
val key = initiator + initiatorPassword
val securityCredential = key.encodeBase64()

val accountBalanceRequest = AccountBalanceRequest(
initiator = initiator,
securityCredential = securityCredential,
commandId = commandId,
partyA = partyA,
identifierType = if (identifierType == DarajaIdentifierType.TILL_NUMBER) 2 else 4,
remarks = remarks,
queueTimeOutURL = queueTimeOutURL,
resultURL = resultURL
)

darajaApiService.generateDynamicQr(dynamicQrRequest = dynamicQrRequest)
darajaApiService.accountBalance(accountBalanceRequest = accountBalanceRequest)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.vickbt.darajakmp.network

import com.vickbt.darajakmp.network.models.AccountBalanceRequest
import com.vickbt.darajakmp.network.models.AccountBalanceResponse
import com.vickbt.darajakmp.network.models.C2BRegistrationRequest
import com.vickbt.darajakmp.network.models.C2BRequest
import com.vickbt.darajakmp.network.models.C2BResponse
Expand Down Expand Up @@ -85,6 +87,18 @@ internal class DarajaApiService(
}.body()
}

internal suspend fun generateDynamicQr(dynamicQrRequest: DynamicQrRequest): DarajaResult<DynamicQrResponse> =
darajaSafeApiCall {
val accessToken = inMemoryCache.get(1) {
fetchAccessToken().getOrThrow()
}

return@darajaSafeApiCall httpClient.post(urlString = DarajaEndpoints.DYNAMIC_QR) {
headers { append(HttpHeaders.Authorization, "Bearer ${accessToken.accessToken}") }
setBody(dynamicQrRequest)
}.body()
}

/**Initiate API call using the [httpClient] provided by Ktor to query the status of an Mpesa Express payment transaction*/
internal suspend fun queryTransaction(darajaTransactionRequest: DarajaTransactionRequest): DarajaResult<DarajaTransactionResponse> =
darajaSafeApiCall {
Expand Down Expand Up @@ -121,15 +135,15 @@ internal class DarajaApiService(
}.body()
}

internal suspend fun generateDynamicQr(dynamicQrRequest: DynamicQrRequest): DarajaResult<DynamicQrResponse> =
internal suspend fun accountBalance(accountBalanceRequest: AccountBalanceRequest): DarajaResult<AccountBalanceResponse> =
darajaSafeApiCall {
val accessToken = inMemoryCache.get(1) {
fetchAccessToken().getOrThrow()
}

return@darajaSafeApiCall httpClient.post(urlString = DarajaEndpoints.DYNAMIC_QR) {
return@darajaSafeApiCall httpClient.post(urlString = DarajaEndpoints.ACCOUNT_BALANCE) {
headers { append(HttpHeaders.Authorization, "Bearer ${accessToken.accessToken}") }
setBody(dynamicQrRequest)
setBody(accountBalanceRequest)
}.body()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@ import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json

/**Initialize Ktor Http Client responsible for handling network operations*/
internal class DarajaHttpClientFactory constructor(private val environment: DarajaEnvironment) {
internal class DarajaHttpClientFactory(private val environment: DarajaEnvironment) {

private val baseURL = if (environment == DarajaEnvironment.SANDBOX_ENVIRONMENT) {
DarajaEndpoints.SANDBOX_BASE_URL
} else {
DarajaEndpoints.PROD_BASE_URL
}
private val baseURL =
if (environment == DarajaEnvironment.SANDBOX_ENVIRONMENT) {
DarajaEndpoints.SANDBOX_BASE_URL
} else {
DarajaEndpoints.PROD_BASE_URL
}

/**Initialize Ktor Http Client responsible for handling network operations*/
internal fun createDarajaHttpClient() = HttpClient {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2024 Daraja Multiplatform
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vickbt.darajakmp.network.models

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
internal data class AccountBalanceRequest(
@SerialName("Initiator")
val initiator: String,

@SerialName("SecurityCredential")
val securityCredential: String,

@SerialName("CommandID")
val commandId: String,

@SerialName("PartyA")
val partyA: Int,

@SerialName("IdentifierType")
val identifierType: Int,

@SerialName("Remarks")
val remarks: String,

@SerialName("QueueTimeOutURL")
val queueTimeOutURL: String,

@SerialName("ResultURL")
val resultURL: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 Daraja Multiplatform
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vickbt.darajakmp.network.models

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class AccountBalanceResponse(
@SerialName("OriginatorConversationID")
val originatorConversationId: String,

@SerialName("ConversationID")
val conversationId: String,

@SerialName("ResponseCode")
val responseCode: String,

@SerialName("ResponseDescription")
val responseDescription: String
)
Loading

0 comments on commit 12ded5d

Please sign in to comment.