Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added mpesa express query #108

Merged
merged 8 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 57 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# __Daraja Multiplatform__
# **Daraja Multiplatform**

<p align="left">
<img src="https://img.shields.io/badge/-ANDROID-3AA335?logo=kotlin&logoColor=white&style=for-the-badge">
Expand All @@ -12,6 +12,7 @@
[Kotlin multiplatform](https://kotlinlang.org/docs/multiplatform.html) wrapper for Mpesa API
dubbed [_Daraja API_](https://developer.safaricom.co.ke/) (Daraja means bridge in Swahili) that
supports integration with your Android(Kotlin/Java), iOS(Swift) and JVM applications.

> M-PESA is a mobile money transfer service in Kenya that allows users to store and transfer money
> through their mobile phones.

Expand Down Expand Up @@ -43,7 +44,7 @@ To get started, you’ll need to create an account on the Daraja API portal to u
API. [How to get started with Daraja API](https://developer.safaricom.co.ke/Documentation).

After successfully creating an account on the Daraja API portal and creating a new Daraja app,
you’ll need to add your ___consumer key___, ___consumer secret___ and ___pass key___ obtained from
you’ll need to add your **_consumer key_**, **_consumer secret_** and **_pass key_** obtained from
the Daraja API portal to your project.

## Features
Expand All @@ -52,6 +53,7 @@ The SDK offers the following functionalities from the Daraja API:

- [x] Authorization - Gives you a time bound access token to call allowed APIs.
- [x] M-Pesa Express - Merchant initiated online payments.
- [x] M-Pesa Express Query - Check the status of a Lipa Na M-Pesa Online Payment(M-Pesa Express).
- [x] Dynamic QR - Generates a dynamic M-PESA QR code.
- [x] Customer To Business (C2B)
- [ ] Business To Customer (B2C) - Transact between an M-Pesa short code to a phone number
Expand All @@ -67,7 +69,7 @@ The SDK offers the following functionalities from the Daraja API:

## Usage

# Android - Kotlin <img src="assets/kotlin_logo.png" width="34" />
# Android - Kotlin <img src="assets/kotlin_logo.png" width="34" />

### Setting Up

Expand All @@ -91,7 +93,7 @@ The SDK offers the following functionalities from the Daraja API:
dependencies {
implementation 'io.github.victorkabata:daraja-multiplatform:0.9.3'
}
```
```

</details>

Expand Down Expand Up @@ -141,8 +143,8 @@ accessTokenResult

- To initiate M-Pesa Express(Lipa na M-Pesa Online) STK request, invoke the `mpesaExpress` function:

```Kotlin
val darajaPaymentResponse: DarajaResult<DarajaPaymentResponse> = daraja.mpesaExpress(
```kotlin
val darajaPaymentResponse: DarajaResult<MpesaExpressResponse> = daraja.mpesaExpress(
businessShortCode = "174379",
amount = 1,
phoneNumber = "07xxxxxxxx",
Expand All @@ -151,12 +153,29 @@ val darajaPaymentResponse: DarajaResult<DarajaPaymentResponse> = daraja.mpesaExp
accountReference = "CompanyName"
)

darajaPaymentResponse
.onSuccess { paymentResponse ->
// Successfully requested M-Pesa STK request
}.onFailure { error ->
// Failed to request M-Pesa STK
}
darajaPaymentResponse.onSuccess { paymentResponse ->
// Successfully requested M-Pesa STK request
}.onFailure { error ->
// Failed to request M-Pesa STK
}
```

### Query M-Pesa Express STK

- To check the status of M-Pesa Express(Lipa na M-Pesa Online) STK request, invoke the `mpesaExpressQuery` function:

```kotlin
val darajaMpesaExpressQuery:DarajaResult<QueryMpesaExpressResponse> = daraja.mpesaExpressQuery(
businessShortCode = "174379",
timeStamp = "20160216165627",
checkOutRequestID = "ws_CO_260520211133524545"
)

darajaMpesaExpressQuery.onSuccess{ mpesaExpressQuery->
// Successfully request M-Pesa STK request status
}.onFailure{ error->
// Failed to request M-Pesa STK status
}
```

### Generate Dynamic QR Code
Expand Down Expand Up @@ -258,13 +277,13 @@ accountBalanceResponse.onSuccess {
}
```

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

### Setting Up

- To add ___DarajaMultiplatform___ package to your Xcode Project, open your Xcode project, navigate
to the File tab within the macOS bar and click __Select Packages__ then __Add Package Dependency
__. Enter the package name ie. DarajaMultiplatform or the URL package GitHub URL:
- To add **_DarajaMultiplatform_** package to your Xcode Project, open your Xcode project, navigate
to the File tab within the macOS bar and click **Select Packages** then **Add Package Dependency
**. Enter the package name ie. DarajaMultiplatform or the URL package GitHub URL:

```curl
https://github.com/VictorKabata/DarajaSwiftPackage.git
Expand Down Expand Up @@ -326,6 +345,25 @@ var darajaResponse = daraja.mpesaExpress(

```

### Query M-Pesa Express STK

- To check the status of M-Pesa Express(Lipa na M-Pesa Online) STK request, invoke the `mpesaExpressQuery` function:

```swift
var mpesaExpressQuery = daraja.mpesaExpressQuery(
businessShortCode: "174379",
timestamp: "20160216165627",
checkoutRequestID: "ws_CO_260520211133524545"
)

mpesaExpressQuery.onSuccess(action:{ data in
// Successfully requested M-Pesa STK request status
})
.onFailure(action: {error in
// Failed to request M-Pesa STK status
})
```

### Generate Dynamic QR Code

- Generate a dynamic m-pesa qr code
Expand All @@ -340,10 +378,10 @@ var darajaQrCode= daraja.generateDynamicQr(
size = 400
)

darajaQrCode.onSuccess(action:{data in
darajaQrCode.onSuccess(action:{data in
// Successfully generated a QR code in base64 string format
})
.onFailure(action: {error in
.onFailure(action: {error in
// Failed to generate a QR code
})
```
Expand Down Expand Up @@ -383,4 +421,4 @@ accountBalanceResponse.onSuccess(action: { data in
}).onFailure(action: { error in
// Failed to request account balance
})
```
```
4 changes: 2 additions & 2 deletions app-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ plugins {

android {
namespace = "com.vickikbt.app_android"
compileSdk = 33
compileSdk = 34

defaultConfig {
applicationId = "com.vickikbt.app_android"

minSdk = 21
targetSdk = 33
targetSdk = 34
versionCode = 1
versionName = "1.0"

Expand Down
2 changes: 1 addition & 1 deletion daraja/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ kover {
reports {
verify {
rule {
minBound(30)
minBound(20)
}
}

Expand Down
28 changes: 28 additions & 0 deletions daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ import com.vickbt.darajakmp.network.models.DynamicQrRequest
import com.vickbt.darajakmp.network.models.DynamicQrResponse
import com.vickbt.darajakmp.network.models.MpesaExpressRequest
import com.vickbt.darajakmp.network.models.MpesaExpressResponse
import com.vickbt.darajakmp.network.models.QueryMpesaExpressRequest
import com.vickbt.darajakmp.network.models.QueryMpesaExpressResponse
import com.vickbt.darajakmp.utils.C2BResponseType
import com.vickbt.darajakmp.utils.DarajaEnvironment
import com.vickbt.darajakmp.utils.DarajaIdentifierType
Expand Down Expand Up @@ -182,6 +184,32 @@ class Daraja(
darajaApiService.initiateMpesaExpress(mpesaExpressRequest = mpesaExpressRequest)
}

/**
* @param [businessShortCode] - This is the organization's shortcode (Paybill or Buy Goods) used to identify an organization and receive the transaction.
* @param [timestamp] - This is the timestamp of the transaction, normally in the format of Year+Month+Date+Hour+Minute+Second(YYYYMMDDHHmmss)
* @param [checkoutRequestID] - This is a global unique identifier of the processed checkout transaction request.
* */
fun mpesaExpressQuery(
businessShortCode: String,
timestamp: String,
checkoutRequestID: String
): DarajaResult<QueryMpesaExpressResponse> = runBlocking(Dispatchers.IO) {
val darajaPassword = getDarajaPassword(
shortCode = businessShortCode,
passkey = passKey ?: "",
timestamp = timestamp
)

val queryMpesaExpressRequest = QueryMpesaExpressRequest(
businessShortCode = businessShortCode,
password = darajaPassword,
timestamp = timestamp,
checkoutRequestID = checkoutRequestID
)

darajaApiService.queryMpesaExpress(queryMpesaExpressRequest = queryMpesaExpressRequest)
}

/**Generate a dynamic qr code to initiate payment
*
* @param [merchantName] Name of the company/M-Pesa merchant name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import com.vickbt.darajakmp.network.models.DynamicQrRequest
import com.vickbt.darajakmp.network.models.DynamicQrResponse
import com.vickbt.darajakmp.network.models.MpesaExpressRequest
import com.vickbt.darajakmp.network.models.MpesaExpressResponse
import com.vickbt.darajakmp.network.models.QueryMpesaExpressRequest
import com.vickbt.darajakmp.network.models.QueryMpesaExpressResponse
import com.vickbt.darajakmp.utils.DarajaEndpoints
import com.vickbt.darajakmp.utils.DarajaResult
import com.vickbt.darajakmp.utils.getOrThrow
Expand Down Expand Up @@ -87,6 +89,18 @@ internal class DarajaApiService(
}.body()
}

internal suspend fun queryMpesaExpress(queryMpesaExpressRequest: QueryMpesaExpressRequest): DarajaResult<QueryMpesaExpressResponse> =
darajaSafeApiCall {
val accessToken = inMemoryCache.get(1) {
fetchAccessToken().getOrThrow()
}

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

internal suspend fun generateDynamicQr(dynamicQrRequest: DynamicQrRequest): DarajaResult<DynamicQrResponse> =
darajaSafeApiCall {
val accessToken = inMemoryCache.get(1) {
Expand Down
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
internal data class QueryMpesaExpressRequest(
@SerialName("BusinessShortCode")
val businessShortCode: String,

@SerialName("Password")
val password: String,

@SerialName("Timestamp")
val timestamp: String,

@SerialName("CheckoutRequestID")
val checkoutRequestID: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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

/**
* @param [responseCode] - This is a numeric status code that indicates the status of the transaction submission. 0 means successful submission and any other code means an error occurred.
* @param [responseDescription] - Response description is an acknowledgment message from the API that gives the status of the request submission usually maps to a specific ResponseCode value. It can be a "Success" submission message or an error description.Response description is an acknowledgment message from the API that gives the status of the request submission usually maps to a specific ResponseCode value. It can be a "Success" submission message or an error description.
* @param [merchantRequestID] - This is a global unique Identifier for any submitted payment request.
* @param [checkoutRequestID] - This is a global unique identifier of the processed checkout transaction request.
* @param [resultCode] - This is a numeric status code that indicates the status of the transaction processing. 0 means successful processing and any other code means an error occurred or the transaction failed.
* @param [resultDescription] - Response description is an acknowledgment message from the API that gives the status of the request submission usually maps to a specific ResponseCode value. It can be a "Success" submission message or an error description.
* */
@Serializable
data class QueryMpesaExpressResponse(
@SerialName("ResponseCode")
val responseCode: String,

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

@SerialName("MerchantRequestID")
val merchantRequestID: String,

@SerialName("CheckoutRequestID")
val checkoutRequestID: String,

@SerialName("ResultCode")
val resultCode: String,

@SerialName("ResultDesc")
val resultDescription: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ internal object DarajaEndpoints {

const val REQUEST_ACCESS_TOKEN = "oauth/v1/generate?grant_type=client_credentials"
const val INITIATE_MPESA_EXPRESS = "mpesa/stkpush/v1/processrequest"
const val QUERY_MPESA_EXPRESS = "mpesa/stkpushquery/v1/query"
const val QUERY_MPESA_TRANSACTION = "mpesa/stkpushquery/v1/query"
const val C2B_REGISTRATION_URL = "mpesa/c2b/v1/registerurl"
const val INITIATE_C2B = "mpesa/c2b/v1/simulate"
Expand Down
Loading