Skip to content

Commit

Permalink
Merge pull request #97 from VictorKabata/c2b
Browse files Browse the repository at this point in the history
Added C2B feature
  • Loading branch information
VictorKabata authored Sep 21, 2023
2 parents 1195a80 + 0d692af commit df25d23
Show file tree
Hide file tree
Showing 11 changed files with 278 additions and 42 deletions.
69 changes: 52 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@
- [Request Access Token](#request-access-token)
- [Initiate M-Pesa Express STK Request](#initiate-m-pesa-express-stk-request)
- [Query M-Pesa Transaction](#query-m-pesa-transaction)
- [Customer To Business(C2B)](#customer-to-businessc2b)
- [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)
- [Query M-Pesa Transaction](#query-m-pesa-transaction-1)
- [Customer To Business(C2B)](#customer-to-businessc2b)


## Prerequisite

Expand All @@ -40,7 +43,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.
- [ ] Customer To Business (C2B)
- [x] Customer To Business (C2B)
- [ ] 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)
Expand Down Expand Up @@ -101,10 +104,10 @@ val daraja: Daraja = Daraja.Builder()
### Request Access Token

- To request an access token from Daraja API, invoke the `requestAccessToken` function:
- To request an access token from Daraja API, invoke the `authorization` function:

```Kotlin
val accessTokenResult: DarajaResult<DarajaToken> = daraja.requestAuthToken()
val accessTokenResult: DarajaResult<DarajaToken> = daraja.authorization()

accessTokenResult
.onSuccess { accessToken ->
Expand All @@ -117,10 +120,10 @@ accessTokenResult

### Initiate M-Pesa Express STK Request

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

```Kotlin
val darajaPaymentResponse: DarajaResult<DarajaPaymentResponse> = daraja.initiateDarajaStk(
val darajaPaymentResponse: DarajaResult<DarajaPaymentResponse> = daraja.mpesaExpress(
businessShortCode = "174379",
amount = 1,
phoneNumber = "07xxxxxxxx",
Expand All @@ -132,31 +135,63 @@ val darajaPaymentResponse: DarajaResult<DarajaPaymentResponse> = daraja.initiate
darajaPaymentResponse
.onSuccess { paymentResponse ->
// Successfully requested M-Pesa STK request
}
.onFailure { error ->
}.onFailure { error ->
// Failed to request M-Pesa STK
}
```

### Query M-Pesa Transaction

- To check the status of an M-pesa transaction, invoke the `queryMpesaTransaction` function:
- To check the status of an M-pesa transaction, invoke the `transactionStatus` function:

```Kotlin
val darajaTransactionResponse: DarajaResult<DarajaTransactionResponse> = daraja.queryMpesaTransaction(
val darajaTransactionResponse: DarajaResult<DarajaTransactionResponse> = daraja.transactionStatus(
businessShortCode = "174379",
checkoutRequestID = "ws_CO_20122022180112029708374149"
)

darajaTransactionResponse
.onSuccess { transactionResponse ->
// Successfully fetched M-pesa transaction status
}
.onFailure { error ->
}.onFailure { error ->
// Failure fetching M-pesa transaction status
}
```

### Customer To Business(C2B)
- To register the C2B validation and confirmation URL, invoke the `c2bRegistration` function:
```kotlin
val darajaC2BRegistrationResponse:DarajaResult<C2BResponse> = daraja.c2bRegistration(
confirmationURL = "https://mydomain.com/confirmation",
responseType = C2BResponseType.COMPLETED, // C2BResponseType.CANCELLED
businessShortCode = 600981,
validationURL = "https://mydomain.com/validation"
)

darajaC2BRegistrationResponse.onSuccess {
// Successfully registered confirmation and validation URL
}.onFailure{
// Failure registering confirmation and validation URL
}

```
- To initiate a Customer to Business paybill, invoke the `c2b` function:
```kotlin
val c2bResponse: DarajaResult<C2BResponse> = daraja.c2b(
amount = 1,
billReferenceNumber = "600977",
transactionType = DarajaTransactionType.CustomerBuyGoodsOnline, // DarajaTransactionType.CustomerPayBillOnline
phoneNumber = "0708374149",
businessShortCode = "600977" //Optional when using CustomerBuyGoodsOnline
)

c2bResponse.onSuccess {
// Successfully invoked C2B request
}.onFailure{
// Failure invoking C2B request
}
```

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

### Setting Up
Expand Down Expand Up @@ -185,10 +220,10 @@ var daraja=Daraja(
> Network logs are strictly disabled in production mode ie. DarajaEnvironment.productionEnvironment
### Request Access Token
- To request an access token from Daraja API, invoke the `requestAccessToken` function:
- To request an access token from Daraja API, invoke the `authorization` function:

```swift
var accessTokenResult = daraja.requestAccessToken()
var accessTokenResult = daraja.authorization()

accessTokenResult.onSuccess(action: { accessToken in
// Successfully fetched daraja access token
Expand All @@ -201,10 +236,10 @@ accessTokenResult.onSuccess(action: { accessToken in

### Initiate M-Pesa Express STK Request

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

```swift
var darajaResponse = daraja.initiateMpesaExpressPayment(
var darajaResponse = daraja.mpesaExpress(
businessShortCode: "174379",
amount: 1,
phoneNumber: "07xxxxxxxx",
Expand All @@ -225,10 +260,10 @@ var darajaResponse = daraja.initiateMpesaExpressPayment(

### Query M-Pesa Transaction

- To check the status of an M-pesa transaction, invoke the `queryMpesaTransaction` function:
- To check the status of an M-pesa transaction, invoke the `transactionStatus` function:

```swift
var darajaTransactionResponse = daraja.queryMpesaTransaction(
var darajaTransactionResponse = daraja.transactionStatus(
businessShortCode: "174379", checkoutRequestID: "ws_CO_20122022180112029708374149")

darajaTransactionResponse.onSuccess(action: { data in
Expand Down
2 changes: 1 addition & 1 deletion daraja/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ kover {
verify {
rule {
name = "Minimal line coverage rate in percents"
bound { minValue = 60 }
bound { minValue = 50 }
}
}
}
Expand Down
74 changes: 58 additions & 16 deletions daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ package com.vickbt.darajakmp

import com.vickbt.darajakmp.network.DarajaApiService
import com.vickbt.darajakmp.network.DarajaHttpClientFactory
import com.vickbt.darajakmp.network.models.DarajaException
import com.vickbt.darajakmp.network.models.MpesaExpressRequest
import com.vickbt.darajakmp.network.models.MpesaExpressResponse
import com.vickbt.darajakmp.network.models.C2BRegistrationRequest
import com.vickbt.darajakmp.network.models.C2BRequest
import com.vickbt.darajakmp.network.models.C2BResponse
import com.vickbt.darajakmp.network.models.DarajaToken
import com.vickbt.darajakmp.network.models.DarajaTransactionResponse
import com.vickbt.darajakmp.network.models.DarajaTransactionRequest
import com.vickbt.darajakmp.network.models.DarajaTransactionResponse
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.DarajaResult
import com.vickbt.darajakmp.utils.DarajaTransactionType
Expand Down Expand Up @@ -121,7 +124,6 @@ class Daraja constructor(

/**Request access token that is used to authenticate to Daraja APIs
*
* @throws DarajaException
* @return [DarajaToken]
* */
@ObjCName(swiftName = "authorization")
Expand All @@ -142,7 +144,6 @@ class Daraja constructor(
* @param [callbackUrl] This is a valid secure URL that is used to receive notifications from M-Pesa API. It is the endpoint to which the results will be sent by M-Pesa API.
* @param [accountReference] This is an alpha-numeric parameter that is defined by your system as an Identifier of the transaction for CustomerPayBillOnline transaction type.
*
* @throws DarajaException
* @return [MpesaExpressResponse]
* */
@ObjCName(swiftName = "mpesaExpress")
Expand All @@ -158,9 +159,7 @@ class Daraja constructor(
val timestamp = Clock.System.now().getDarajaTimestamp()

val darajaPassword = getDarajaPassword(
shortCode = businessShortCode,
passkey = passKey ?: "",
timestamp = timestamp
shortCode = businessShortCode, passkey = passKey ?: "", timestamp = timestamp
)

val mpesaExpressRequest = MpesaExpressRequest(
Expand All @@ -178,7 +177,7 @@ class Daraja constructor(
)

withContext(ioCoroutineContext) {
return@withContext darajaApiService.initiateMpesaStk(mpesaExpressRequest = mpesaExpressRequest)
return@withContext darajaApiService.initiateMpesaExpress(mpesaExpressRequest = mpesaExpressRequest)
}
}

Expand All @@ -187,19 +186,15 @@ class Daraja constructor(
* @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.
* @param [checkoutRequestID] This is a global unique identifier of the processed checkout transaction request.
*
* @throws DarajaException
* @return [DarajaTransactionResponse]
* */
@ObjCName(swiftName = "transactionStatus")
fun transactionStatus(
businessShortCode: String,
checkoutRequestID: String
businessShortCode: String, checkoutRequestID: String
): DarajaResult<DarajaTransactionResponse> = runBlocking {
val timestamp = Clock.System.now().getDarajaTimestamp()
val darajaPassword = getDarajaPassword(
shortCode = businessShortCode,
passkey = passKey ?: "",
timestamp = timestamp
shortCode = businessShortCode, passkey = passKey ?: "", timestamp = timestamp
)

val darajaTransactionRequest = DarajaTransactionRequest(
Expand All @@ -213,4 +208,51 @@ class Daraja constructor(
return@withContext darajaApiService.queryTransaction(darajaTransactionRequest)
}
}

/**Transact between a phone number registered on M-Pesa to an M-Pesa shortcode
*
* @param [businessShortCode] A unique number is tagged to an M-PESA pay bill/till number of the organization.
* @param [confirmationURL] This is the URL that receives the confirmation request from API upon payment completion.
* @param [validationURL] This is the URL that receives the validation request from the API upon payment submission. The validation URL is only called if the external validation on the registered shortcode is enabled. (By default External Validation is disabled).
* @param [responseType] This parameter specifies what is to happen if for any reason the validation URL is not reachable. Note that, this is the default action value that determines what M-PESA will do in the scenario that your endpoint is unreachable or is unable to respond on time. Only two values are allowed: Completed or Cancelled. Completed means M-PESA will automatically complete your transaction, whereas Cancelled means M-PESA will automatically cancel the transaction, in the event M-PESA is unable to reach your Validation URL.
*
* @return [C2BResponse]
* */
fun c2bRegistration(
businessShortCode: Int,
confirmationURL: String,
validationURL: String? = null,
responseType: C2BResponseType? = C2BResponseType.COMPLETED
): DarajaResult<C2BResponse> = runBlocking {
val c2BRegistrationRequest = C2BRegistrationRequest(
confirmationURL = confirmationURL,
validationURL = validationURL,
responseType = responseType?.name?.lowercase(),
shortCode = businessShortCode
)

withContext(ioCoroutineContext) {
return@withContext darajaApiService.c2bRegistration(c2bRegistrationRequest = c2BRegistrationRequest)
}
}

fun c2b(
amount: Int,
billReferenceNumber: String,
transactionType: DarajaTransactionType,
phoneNumber: String,
businessShortCode: String
): DarajaResult<C2BResponse> = runBlocking {
val c2bRequest = C2BRequest(
amount = amount,
billReferenceNumber = billReferenceNumber,
commandID = transactionType.name,
phoneNumber = phoneNumber.getDarajaPhoneNumber().toLong(),
shortCode = if (transactionType.name == DarajaTransactionType.CustomerPayBillOnline.name) businessShortCode else billReferenceNumber
)

withContext(ioCoroutineContext) {
return@withContext darajaApiService.c2b(c2bRequest = c2bRequest)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,14 @@

package com.vickbt.darajakmp.network

import com.vickbt.darajakmp.network.models.MpesaExpressRequest
import com.vickbt.darajakmp.network.models.MpesaExpressResponse
import com.vickbt.darajakmp.network.models.C2BRegistrationRequest
import com.vickbt.darajakmp.network.models.C2BRequest
import com.vickbt.darajakmp.network.models.C2BResponse
import com.vickbt.darajakmp.network.models.DarajaToken
import com.vickbt.darajakmp.network.models.DarajaTransactionResponse
import com.vickbt.darajakmp.network.models.DarajaTransactionRequest
import com.vickbt.darajakmp.network.models.DarajaTransactionResponse
import com.vickbt.darajakmp.network.models.MpesaExpressRequest
import com.vickbt.darajakmp.network.models.MpesaExpressResponse
import com.vickbt.darajakmp.utils.DarajaEndpoints
import com.vickbt.darajakmp.utils.DarajaResult
import com.vickbt.darajakmp.utils.getOrThrow
Expand Down Expand Up @@ -69,7 +72,7 @@ internal class DarajaApiService constructor(
}

/**Initiate API call using the [httpClient] provided by Ktor to trigger Mpesa Express payment on Daraja API */
internal suspend fun initiateMpesaStk(mpesaExpressRequest: MpesaExpressRequest): DarajaResult<MpesaExpressResponse> =
internal suspend fun initiateMpesaExpress(mpesaExpressRequest: MpesaExpressRequest): DarajaResult<MpesaExpressResponse> =
darajaSafeApiCall {
val accessToken = inMemoryCache.get(1) {
fetchAccessToken().getOrThrow()
Expand All @@ -92,4 +95,30 @@ internal class DarajaApiService constructor(
setBody(darajaTransactionRequest)
}.body()
}

internal suspend fun c2bRegistration(c2bRegistrationRequest: C2BRegistrationRequest): DarajaResult<C2BResponse> =
darajaSafeApiCall {
val accessToken = inMemoryCache.get(1) {
fetchAccessToken().getOrThrow()
}

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

internal suspend fun c2b(c2bRequest: C2BRequest): DarajaResult<C2BResponse> =
darajaSafeApiCall {
val accessToken = inMemoryCache.get(1) {
fetchAccessToken().getOrThrow()
}

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

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2023 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 com.vickbt.darajakmp.utils.C2BResponseType
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.native.ObjCName

@ObjCName(swiftName = "C2BRegistrationRequest")
@Serializable
/***/
internal data class C2BRegistrationRequest(
/**This is the URL that receives the confirmation request from API upon payment completion.*/
@SerialName("ConfirmationURL")
val confirmationURL: String,

/**This is the URL that receives the validation request from the API upon payment submission. The validation URL is only called if the external validation on the registered shortcode is enabled. (By default External Validation is disabled).*/
@SerialName("ValidationURL")
val validationURL: String?,

/**This parameter specifies what is to happen if for any reason the validation URL is not reachable. Note that, this is the default action value that determines what M-PESA will do in the scenario that your endpoint is unreachable or is unable to respond on time. Only two values are allowed: Completed or Cancelled. Completed means M-PESA will automatically complete your transaction, whereas Cancelled means M-PESA will automatically cancel the transaction, in the event M-PESA is unable to reach your Validation URL.*/
@SerialName("ResponseType")
val responseType: String? = C2BResponseType.COMPLETED.name,

/**A unique number is tagged to an M-PESA pay bill/till number of the organization.*/
@SerialName("ShortCode")
val shortCode: Int,
)
Loading

0 comments on commit df25d23

Please sign in to comment.