diff --git a/.editorconfig b/.editorconfig
index 219dccb8..7a0773af 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,3 +1,3 @@
[*.{kt,kts}]
-disabled_rules = import-ordering, experimental:argument-list-wrapping,experimental:package-name
+disabled_rules = import-ordering, experimental:argument-list-wrapping, package-name
insert_final_newline = true
\ No newline at end of file
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 00000000..f5f04fb9
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,25 @@
+### Description
+
+
+### Issue
+
+
+### Changes Made
+
+
+### Screenshots / GIFs
+
+
+### Checklist
+
+- [] I have run the gradle command: `./gradlew clean build`
+- [] I have added or updated relevant tests.
+- [] I have updated the documentation.
+- [] I have ensured code style consistency by running the gradle command: `./gradlew :daraja:ktlintCheck :daraja:detekt` .
+- [] I have addressed any potential compatibility issues.
+- [] I have considered backward compatibility.
+
+### Additional Notes
+
+
+
diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml
index 24215146..b9bda016 100644
--- a/.github/workflows/maintenance.yml
+++ b/.github/workflows/maintenance.yml
@@ -1,6 +1,7 @@
name: Maintenance
on:
+ workflow_dispatch:
schedule:
- cron: 00 00 30 * * # At 00:00 on 30th every month
@@ -24,7 +25,7 @@ jobs:
run: chmod +x ./gradlew
- name: Check for release dependencies
- run: ./gradlew :daraja:dependencyUpdates -Drevision=release -DoutputFormatter=html -DreportfileName=dependencies_report -DoutputDir=build/reports --stacktrace
+ run: ./gradlew :daraja:dependencyUpdates -Drevision=release -DoutputFormatter=html -DreportfileName=dependencies_report --stacktrace
- name: Upload dependencies report artifact
uses: actions/upload-artifact@v3
diff --git a/README.md b/README.md
index 1edd3484..be1507d0 100644
--- a/README.md
+++ b/README.md
@@ -15,17 +15,21 @@
## Table of Content
- [Prerequisite](#prerequisite)
+- [Features](#features)
- [Usage](#usage)
- [Android - Kotlin](#android---kotlin)
- [Setting Up](#setting-up)
- [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
@@ -33,6 +37,22 @@ To get started, you’ll need to create an account on the Daraja API portal to u
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 the Daraja API portal to your project.
+## Features
+
+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] 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)
+- [ ] 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 paybill store.
+- [ ] Business Buy Goods - Pay for goods and services directly from your business account to a till number or merchant store number.
+
+
## Usage
# Android - Kotlin
@@ -65,15 +85,7 @@ After successfully creating an account on the Daraja API portal and creating a n
- Add your consumer secret, consumer key and pass key to your project. You can get them from the [Daraja API portal](https://developer.safaricom.co.ke/MyApps).
-```Kotlin
-object Constants {
- const val CONSUMER_SECRET="your_consumer_secret"
- const val CONSUMER_KEY="your_consumer_key"
- const val PASS_KEY="your_pass_key"
-}
-```
-
-> You should not add your daraja API environment variables in a production application because it is a vulnerability to expose your environment secrets/variables in your version control system. Ideally, you should add them to your `Local.properties` files as demonstrated in the [sample](https://github.com/VictorKabata/DarajaMultiplatform/tree/main/app-android) android application.
+> You should not add your daraja API environment variables in a production application because it is a vulnerability to expose your environment secrets/variables in your version control system. Ideally, you should add them to your `local.properties` files as demonstrated in the [sample](https://github.com/VictorKabata/DarajaMultiplatform/tree/main/app-android) android application.
- Create an instance of the Daraja object by passing the daraja environment variables. The daraja object provides functions to request for an access token and initiate M-Pesa express STK request.
@@ -88,14 +100,14 @@ val daraja: Daraja = Daraja.Builder()
> Network logging is enabled by default when using Daraja Multiplatform. in sandbox/testing mode. The logs can be accessed from the logcat in Android Studio under the `Daraja Multiplatform` tag.
-> Network logs are strictly disabled in production mode.
+> Network logs are disabled in production mode.
### 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 = daraja.requestAuthToken()
+val accessTokenResult: DarajaResult = daraja.authorization()
accessTokenResult
.onSuccess { accessToken ->
@@ -108,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 = daraja.initiateDarajaStk(
+val darajaPaymentResponse: DarajaResult = daraja.mpesaExpress(
businessShortCode = "174379",
amount = 1,
phoneNumber = "07xxxxxxxx",
@@ -123,18 +135,17 @@ val darajaPaymentResponse: DarajaResult = 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 = daraja.queryMpesaTransaction(
+val darajaTransactionResponse: DarajaResult = daraja.transactionStatus(
businessShortCode = "174379",
checkoutRequestID = "ws_CO_20122022180112029708374149"
)
@@ -142,12 +153,45 @@ val darajaTransactionResponse: DarajaResult = daraja.
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 = 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 = 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
### Setting Up
@@ -176,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
@@ -192,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",
@@ -216,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
diff --git a/app-android/build.gradle.kts b/app-android/build.gradle.kts
index 7be858a5..b1254c85 100644
--- a/app-android/build.gradle.kts
+++ b/app-android/build.gradle.kts
@@ -42,7 +42,7 @@ android {
compose = true
}
composeOptions {
- kotlinCompilerExtensionVersion = "1.4.6"
+ kotlinCompilerExtensionVersion = "1.5.7"
}
packagingOptions {
resources {
diff --git a/app-android/src/main/java/com/vickbt/app_android/ui/screens/home/HomeScreen.kt b/app-android/src/main/java/com/vickbt/app_android/ui/screens/home/HomeScreen.kt
index 720e2f2a..e5c9d558 100644
--- a/app-android/src/main/java/com/vickbt/app_android/ui/screens/home/HomeScreen.kt
+++ b/app-android/src/main/java/com/vickbt/app_android/ui/screens/home/HomeScreen.kt
@@ -16,6 +16,7 @@
package com.vickbt.app_android.ui.screens.home
+import android.util.Log
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -164,6 +165,7 @@ fun HomeScreen(viewModel: HomeViewModel = get()) {
Toast.makeText(context, "Success: $it", Toast.LENGTH_SHORT).show()
}?.onFailure {
Toast.makeText(context, "Error: ${it.errorMessage}", Toast.LENGTH_SHORT).show()
+ Log.e("VicKbt", "Error: $it")
}
}
diff --git a/app-android/src/main/java/com/vickbt/app_android/ui/screens/home/HomeViewModel.kt b/app-android/src/main/java/com/vickbt/app_android/ui/screens/home/HomeViewModel.kt
index fe3e8eb1..59bf955b 100644
--- a/app-android/src/main/java/com/vickbt/app_android/ui/screens/home/HomeViewModel.kt
+++ b/app-android/src/main/java/com/vickbt/app_android/ui/screens/home/HomeViewModel.kt
@@ -19,7 +19,7 @@ package com.vickbt.app_android.ui.screens.home
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vickbt.darajakmp.Daraja
-import com.vickbt.darajakmp.network.models.DarajaPaymentResponse
+import com.vickbt.darajakmp.network.models.MpesaExpressResponse
import com.vickbt.darajakmp.utils.DarajaResult
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -27,7 +27,7 @@ import kotlinx.coroutines.launch
class HomeViewModel constructor(private val daraja: Daraja) : ViewModel() {
- private val _mpesaResponse = MutableStateFlow?>(null)
+ private val _mpesaResponse = MutableStateFlow?>(null)
val mpesaResponse get() = _mpesaResponse.asStateFlow()
fun initiateMpesaPayment(
@@ -38,7 +38,7 @@ class HomeViewModel constructor(private val daraja: Daraja) : ViewModel() {
callbackUrl: String,
accountReference: String
) = viewModelScope.launch {
- val response = daraja.initiateMpesaExpressPayment(
+ val response = daraja.mpesaExpress(
businessShortCode = businessShortCode.trim(),
amount = amount,
phoneNumber = phoneNumber.trim(),
diff --git a/app-android/src/main/java/com/vickbt/app_android/ui/theme/Theme.kt b/app-android/src/main/java/com/vickbt/app_android/ui/theme/Theme.kt
index cda0bd7f..0438a959 100644
--- a/app-android/src/main/java/com/vickbt/app_android/ui/theme/Theme.kt
+++ b/app-android/src/main/java/com/vickbt/app_android/ui/theme/Theme.kt
@@ -50,7 +50,7 @@ private val LightColorScheme = lightColorScheme(
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
- */
+ */
)
@Composable
diff --git a/app-android/src/main/java/com/vickbt/app_android/ui/theme/Type.kt b/app-android/src/main/java/com/vickbt/app_android/ui/theme/Type.kt
index a8a12b31..ff58a6e5 100644
--- a/app-android/src/main/java/com/vickbt/app_android/ui/theme/Type.kt
+++ b/app-android/src/main/java/com/vickbt/app_android/ui/theme/Type.kt
@@ -46,5 +46,5 @@ val Typography = Typography(
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
- */
+ */
)
diff --git a/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/screens/home/HomeScreen.kt b/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/screens/home/HomeScreen.kt
index f9ea6bfa..2b5db3b7 100644
--- a/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/screens/home/HomeScreen.kt
+++ b/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/screens/home/HomeScreen.kt
@@ -128,7 +128,7 @@ fun HomeScreen() {
}
fun initiateMpesaStk(daraja: Daraja, tillNumber: String, amount: Int, phoneNumber: String) {
- daraja.initiateMpesaExpressPayment(
+ daraja.mpesaExpress(
businessShortCode = tillNumber,
amount = amount,
phoneNumber = phoneNumber,
diff --git a/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/theme/Theme.kt b/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/theme/Theme.kt
index a84e01ff..a5111015 100644
--- a/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/theme/Theme.kt
+++ b/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/theme/Theme.kt
@@ -39,7 +39,7 @@ private val LightColorScheme = lightColors(
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
- */
+ */
)
@Composable
diff --git a/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/theme/Type.kt b/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/theme/Type.kt
index 90dc4a52..a4705db9 100644
--- a/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/theme/Type.kt
+++ b/app-desktop/src/main/java/com/vickikbt/app_desktop/ui/theme/Type.kt
@@ -46,5 +46,5 @@ val Typography = Typography(
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
- */
+ */
)
diff --git a/app-iOS/app-iOS/ContentView.swift b/app-iOS/app-iOS/ContentView.swift
index 85e55853..bff2fcad 100644
--- a/app-iOS/app-iOS/ContentView.swift
+++ b/app-iOS/app-iOS/ContentView.swift
@@ -81,6 +81,7 @@ func initiateMpesaPayment(daraja:Daraja,
transactionDesc: String,
callbackUrl: String,
accountReference: String){
+
let response=daraja.initiateMpesaExpressPayment(businessShortCode: businessShortCode, amount: amount, phoneNumber: phoneNumber,transactionType: DarajaTransactionType.customerpaybillonline, transactionDesc: "M-Pesa payment", callbackUrl: "https://mydomain.com/path", accountReference: "Daraja KMP iOS")
response.onSuccess(action: {data in
@@ -89,8 +90,6 @@ func initiateMpesaPayment(daraja:Daraja,
.onFailure(action: {error in
print(error)
})
-
- print(response)
}
struct ContentView_Previews: PreviewProvider {
diff --git a/build.gradle.kts b/build.gradle.kts
index f673d7e7..1656a8b1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -6,13 +6,13 @@ plugins {
alias(libs.plugins.jvm) apply false
alias(libs.plugins.nativeCocoapod) apply false
- // alias(libs.plugins.ktLint)
+ alias(libs.plugins.ktLint)
alias(libs.plugins.detekt)
alias(libs.plugins.spotless)
}
subprojects {
- /*apply(plugin = "org.jlleitschuh.gradle.ktlint")
+ apply(plugin = "org.jlleitschuh.gradle.ktlint")
ktlint {
debug.set(true)
verbose.set(true)
@@ -22,8 +22,9 @@ subprojects {
filter {
enableExperimentalRules.set(true)
exclude { projectDir.toURI().relativize(it.file.toURI()).path.contains("/generated/") }
+ include("**/kotlin/**")
}
- }*/
+ }
apply(plugin = "io.gitlab.arturbosch.detekt")
detekt {
diff --git a/daraja/build.gradle.kts b/daraja/build.gradle.kts
index fc793f77..f5e9dcda 100644
--- a/daraja/build.gradle.kts
+++ b/daraja/build.gradle.kts
@@ -1,5 +1,6 @@
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
+import java.util.Locale
val dokkaOutputDir = buildDir.resolve("reports/dokka")
@@ -8,10 +9,12 @@ val snapshotsRepoUrl = uri("https://s01.oss.sonatype.org/content/repositories/sn
fun Project.get(key: String, defaultValue: String = "Invalid value $key") =
gradleLocalProperties(rootDir).getProperty(key)?.toString() ?: System.getenv(key)?.toString()
- ?: defaultValue
+ ?: defaultValue
fun isNonStable(version: String): Boolean {
- val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) }
+ val stableKeyword = listOf("RELEASE", "FINAL", "GA").any {
+ version.uppercase(Locale.getDefault()).contains(it)
+ }
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
val isStable = stableKeyword || regex.matches(version)
return isStable.not()
@@ -33,9 +36,9 @@ plugins {
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
kotlin {
- targetHierarchy.default()
+ kotlin.applyDefaultHierarchyTemplate()
- android {
+ androidTarget {
publishLibraryVariants("release", "debug")
}
@@ -83,7 +86,7 @@ kotlin {
sourceSets["androidMain"].dependencies {
implementation(libs.ktor.android)
}
- sourceSets["androidTest"].dependencies {}
+ sourceSets["androidUnitTest"].dependencies {}
sourceSets["iosMain"].dependencies {
implementation(libs.ktor.darwin)
@@ -130,7 +133,7 @@ tasks.withType {
gradleReleaseChannel = "current"
outputFormatter = "html"
- outputDir = "${project.rootDir}/build/reports"
+ outputDir = "${project.rootDir}/daraja/build/reports"
reportfileName = "dependencies_report"
}
@@ -148,11 +151,11 @@ val javadocJar = tasks.register("javadocJar") {
from(dokkaOutputDir)
}
-kover {
+koverReport {
verify {
rule {
- name = "Minimal line coverage rate in percents"
- bound { minValue = 60 }
+ isEnabled = false
+ bound { minValue = 20 }
}
}
}
@@ -162,8 +165,11 @@ publishing {
repositories {
maven {
name = "Sonatype"
- url = if (version.toString().endsWith("SNAPSHOT")) snapshotsRepoUrl
- else releasesRepoUrl
+ url = if (version.toString().endsWith("SNAPSHOT")) {
+ snapshotsRepoUrl
+ } else {
+ releasesRepoUrl
+ }
credentials {
username = project.get("OSSRH_USERNAME")
@@ -219,7 +225,9 @@ publishing {
val signingKey = project.get("SIGNING_PASSWORD")
useInMemoryPgpKeys(
- signingKeyId, signingKeyPassword, signingKey
+ signingKeyId,
+ signingKeyPassword,
+ signingKey
)
sign(publishing.publications)
}
diff --git a/daraja/daraja.podspec b/daraja/daraja.podspec
index 7d3ec92c..0e10d72d 100644
--- a/daraja/daraja.podspec
+++ b/daraja/daraja.podspec
@@ -11,6 +11,17 @@ Pod::Spec.new do |spec|
spec.ios.deployment_target = '14.1'
+ if !Dir.exist?('build/cocoapods/framework/DarajaMultiplatform.framework') || Dir.empty?('build/cocoapods/framework/DarajaMultiplatform.framework')
+ raise "
+
+ Kotlin framework 'DarajaMultiplatform' doesn't exist yet, so a proper Xcode project can't be generated.
+ 'pod install' should be executed after running ':generateDummyFramework' Gradle task:
+
+ ./gradlew :daraja:generateDummyFramework
+
+ Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
+ end
+
spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':daraja',
'PRODUCT_MODULE_NAME' => 'DarajaMultiplatform',
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt
index f37e15b0..9374a5ed 100644
--- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt
@@ -18,11 +18,15 @@ package com.vickbt.darajakmp
import com.vickbt.darajakmp.network.DarajaApiService
import com.vickbt.darajakmp.network.DarajaHttpClientFactory
-import com.vickbt.darajakmp.network.models.DarajaPaymentRequest
-import com.vickbt.darajakmp.network.models.DarajaPaymentResponse
+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.DarajaTransactionRequest
import com.vickbt.darajakmp.network.models.DarajaTransactionResponse
-import com.vickbt.darajakmp.network.models.QueryDarajaTransactionRequest
+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
@@ -34,7 +38,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlin.native.ObjCName
@@ -46,7 +49,7 @@ import kotlin.native.ObjCName
* @param environment Environment that Daraja API should use ie. Either [DarajaEnvironment.SANDBOX_ENVIRONMENT] (Sandbox Mode) or [DarajaEnvironment.PRODUCTION_ENVIRONMENT] (Production Mode)
* */
@ObjCName(swiftName = "Daraja")
-class Daraja constructor(
+class Daraja(
private val consumerKey: String?,
private val consumerSecret: String?,
private val passKey: String?,
@@ -122,10 +125,9 @@ class Daraja constructor(
*
* @return [DarajaToken]
* */
- fun requestAccessToken(): DarajaResult = runBlocking {
- withContext(ioCoroutineContext) {
- return@withContext darajaApiService.fetchAccessToken()
- }
+ @ObjCName(swiftName = "authorization")
+ fun authorization(): DarajaResult = runBlocking(Dispatchers.IO) {
+ darajaApiService.fetchAccessToken()
}
/**Initiate Mpesa Express payment of value provided in [amount] to the [businessShortCode] from the the [phoneNumber].
@@ -139,9 +141,10 @@ 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.
*
- * @return [DarajaPaymentResponse]
+ * @return [MpesaExpressResponse]
* */
- fun initiateMpesaExpressPayment(
+ @ObjCName(swiftName = "mpesaExpress")
+ fun mpesaExpress(
businessShortCode: String,
amount: Int,
phoneNumber: String,
@@ -149,7 +152,7 @@ class Daraja constructor(
transactionDesc: String,
callbackUrl: String,
accountReference: String? = null
- ): DarajaResult = runBlocking {
+ ): DarajaResult = runBlocking(Dispatchers.IO) {
val timestamp = Clock.System.now().getDarajaTimestamp()
val darajaPassword = getDarajaPassword(
@@ -158,7 +161,7 @@ class Daraja constructor(
timestamp = timestamp
)
- val darajaPaymentRequest = DarajaPaymentRequest(
+ val mpesaExpressRequest = MpesaExpressRequest(
businessShortCode = businessShortCode,
password = darajaPassword,
timestamp = timestamp,
@@ -166,15 +169,13 @@ class Daraja constructor(
amount = amount.toString(),
transactionType = transactionType.name,
phoneNumber = phoneNumber.getDarajaPhoneNumber(),
- callBackUrl = callbackUrl, // ToDo: Figure out how callback urls work
+ callBackUrl = callbackUrl,
accountReference = accountReference ?: businessShortCode,
partyA = phoneNumber,
partyB = businessShortCode
)
- withContext(ioCoroutineContext) {
- return@withContext darajaApiService.initiateMpesaStk(darajaPaymentRequest = darajaPaymentRequest)
- }
+ darajaApiService.initiateMpesaExpress(mpesaExpressRequest = mpesaExpressRequest)
}
/**Request the status of an Mpesa payment transaction
@@ -184,10 +185,11 @@ class Daraja constructor(
*
* @return [DarajaTransactionResponse]
* */
- fun queryMpesaTransaction(
+ @ObjCName(swiftName = "transactionStatus")
+ fun transactionStatus(
businessShortCode: String,
checkoutRequestID: String
- ): DarajaResult = runBlocking {
+ ): DarajaResult = runBlocking(Dispatchers.IO) {
val timestamp = Clock.System.now().getDarajaTimestamp()
val darajaPassword = getDarajaPassword(
shortCode = businessShortCode,
@@ -195,15 +197,56 @@ class Daraja constructor(
timestamp = timestamp
)
- val queryDarajaTransactionRequest = QueryDarajaTransactionRequest(
+ val darajaTransactionRequest = DarajaTransactionRequest(
businessShortCode = businessShortCode,
password = darajaPassword,
timestamp = timestamp,
checkoutRequestID = checkoutRequestID
)
- withContext(ioCoroutineContext) {
- return@withContext darajaApiService.queryTransaction(queryDarajaTransactionRequest)
- }
+ 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 = runBlocking(Dispatchers.IO) {
+ val c2BRegistrationRequest = C2BRegistrationRequest(
+ confirmationURL = confirmationURL,
+ validationURL = validationURL,
+ responseType = responseType?.name?.lowercase(),
+ shortCode = businessShortCode
+ )
+
+ darajaApiService.c2bRegistration(c2bRegistrationRequest = c2BRegistrationRequest)
+ }
+
+ fun c2b(
+ amount: Int,
+ billReferenceNumber: String,
+ transactionType: DarajaTransactionType,
+ phoneNumber: String,
+ businessShortCode: String
+ ): DarajaResult = runBlocking(Dispatchers.IO) {
+ val c2bRequest = C2BRequest(
+ amount = amount,
+ billReferenceNumber = billReferenceNumber,
+ commandID = transactionType.name,
+ phoneNumber = phoneNumber.getDarajaPhoneNumber().toLong(),
+ shortCode = if (transactionType.name == DarajaTransactionType.CustomerPayBillOnline.name) businessShortCode else billReferenceNumber
+ )
+
+ darajaApiService.c2b(c2bRequest = c2bRequest)
}
}
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaApiService.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaApiService.kt
index 2ca5fced..a9071c18 100644
--- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaApiService.kt
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaApiService.kt
@@ -16,11 +16,14 @@
package com.vickbt.darajakmp.network
-import com.vickbt.darajakmp.network.models.DarajaPaymentRequest
-import com.vickbt.darajakmp.network.models.DarajaPaymentResponse
+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.DarajaTransactionRequest
import com.vickbt.darajakmp.network.models.DarajaTransactionResponse
-import com.vickbt.darajakmp.network.models.QueryDarajaTransactionRequest
+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
@@ -46,11 +49,11 @@ internal class DarajaApiService constructor(
private val httpClient: HttpClient,
private val consumerKey: String,
private val consumerSecret: String,
- private val inMemoryCache: Cache = Cache.Builder()
+ private val inMemoryCache: Cache = Cache.Builder()
.expireAfterWrite(3600.toDuration(DurationUnit.SECONDS)).build()
) {
- /**Initiate API call using the [httpClient] provided by Ktor to fetch Daraja API access token
+ /** Initiate API call using the [httpClient] provided by Ktor to fetch Daraja API access token
* of type [DarajaToken]*/
internal suspend fun fetchAccessToken(): DarajaResult = darajaSafeApiCall {
val key = "$consumerKey:$consumerSecret"
@@ -68,7 +71,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(darajaPaymentRequest: DarajaPaymentRequest): DarajaResult =
+ internal suspend fun initiateMpesaExpress(mpesaExpressRequest: MpesaExpressRequest): DarajaResult =
darajaSafeApiCall {
val accessToken = inMemoryCache.get(1) {
fetchAccessToken().getOrThrow()
@@ -76,19 +79,43 @@ internal class DarajaApiService constructor(
return@darajaSafeApiCall httpClient.post(urlString = DarajaEndpoints.INITIATE_MPESA_EXPRESS) {
headers { append(HttpHeaders.Authorization, "Bearer ${accessToken.accessToken}") }
- setBody(darajaPaymentRequest)
+ setBody(mpesaExpressRequest)
}.body()
}
/**Initiate API call using the [httpClient] provided by Ktor to query the status of an Mpesa Express payment transaction*/
- internal suspend fun queryTransaction(queryDarajaTransactionRequest: QueryDarajaTransactionRequest): DarajaResult =
+ internal suspend fun queryTransaction(darajaTransactionRequest: DarajaTransactionRequest): DarajaResult =
darajaSafeApiCall {
val key = "$consumerKey:$consumerSecret"
val base64EncodedKey = key.encodeBase64()
return@darajaSafeApiCall httpClient.post(urlString = DarajaEndpoints.QUERY_MPESA_TRANSACTION) {
headers { append(HttpHeaders.Authorization, "Basic $base64EncodedKey") }
- setBody(queryDarajaTransactionRequest)
+ setBody(darajaTransactionRequest)
+ }.body()
+ }
+
+ internal suspend fun c2bRegistration(c2bRegistrationRequest: C2BRegistrationRequest): DarajaResult =
+ 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 =
+ 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()
}
}
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaHttpClientFactory.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaHttpClientFactory.kt
index e950556b..e57ee3ee 100644
--- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaHttpClientFactory.kt
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaHttpClientFactory.kt
@@ -22,7 +22,6 @@ import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier
import io.ktor.client.HttpClient
import io.ktor.client.plugins.addDefaultResponseValidation
-import io.ktor.client.plugins.cache.HttpCache
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel
@@ -57,11 +56,10 @@ internal class DarajaHttpClientFactory constructor(private val environment: Dara
}
}
- install(HttpCache)
-
install(ContentNegotiation) {
json(
Json {
+ prettyPrint = true
ignoreUnknownKeys = true
isLenient = true
}
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaSafeApiCall.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaSafeApiCall.kt
index 1b329a46..9a4b6bdd 100644
--- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaSafeApiCall.kt
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/DarajaSafeApiCall.kt
@@ -59,6 +59,6 @@ internal suspend fun parseNetworkError(
errorResponse: HttpResponse? = null,
exception: Exception? = null
): DarajaException {
- throw errorResponse?.body()
- ?: DarajaException(requestId = "0", errorCode = "0", errorMessage = exception?.message)
+ return errorResponse?.body()
+ ?: DarajaException(errorMessage = exception?.message)
}
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRegistrationRequest.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRegistrationRequest.kt
new file mode 100644
index 00000000..4f470319
--- /dev/null
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRegistrationRequest.kt
@@ -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
+)
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRequest.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRequest.kt
new file mode 100644
index 00000000..6519395e
--- /dev/null
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BRequest.kt
@@ -0,0 +1,41 @@
+/*
+ * 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 kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlin.native.ObjCName
+
+@ObjCName(swiftName = "C2BRequest")
+@Serializable
+/**Request C2B M-Pesa payment*/
+internal data class C2BRequest(
+ @SerialName("Amount")
+ val amount: Int,
+
+ @SerialName("BillRefNumber")
+ val billReferenceNumber: String,
+
+ @SerialName("CommandID")
+ val commandID: String,
+
+ @SerialName("Msisdn")
+ val phoneNumber: Long,
+
+ @SerialName("ShortCode")
+ val shortCode: String?
+)
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BResponse.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BResponse.kt
new file mode 100644
index 00000000..eba0e3ae
--- /dev/null
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/C2BResponse.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlin.native.ObjCName
+
+@ObjCName(swiftName = "C2BResponse")
+@Serializable
+/***/
+data class C2BResponse(
+ /**This is a global unique identifier for the transaction request returned by the API proxy upon successful request submission.*/
+ @SerialName("OriginatorCoversationID")
+ val originatorCoversationId: String,
+
+ /**It indicates whether Mobile Money accepts the request or not.*/
+ @SerialName("ResponseCode")
+ val responseCode: String,
+
+ /**This is the status of the request.*/
+ @SerialName("ResponseDescription")
+ val responseDescription: String
+)
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/QueryDarajaTransactionRequest.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/DarajaTransactionRequest.kt
similarity index 91%
rename from daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/QueryDarajaTransactionRequest.kt
rename to daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/DarajaTransactionRequest.kt
index 419d5182..cd58acfe 100644
--- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/QueryDarajaTransactionRequest.kt
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/DarajaTransactionRequest.kt
@@ -20,9 +20,9 @@ import kotlin.native.ObjCName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-@ObjCName(swiftName = "QueryDarajaTransactionRequest")
+@ObjCName(swiftName = "DarajaTransactionRequest")
@Serializable
-data class QueryDarajaTransactionRequest(
+internal data class DarajaTransactionRequest(
@SerialName("BusinessShortCode")
val businessShortCode: String,
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/DarajaPaymentRequest.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/MpesaExpressRequest.kt
similarity index 97%
rename from daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/DarajaPaymentRequest.kt
rename to daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/MpesaExpressRequest.kt
index af06e7f0..d3ac5d1e 100644
--- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/DarajaPaymentRequest.kt
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/MpesaExpressRequest.kt
@@ -16,16 +16,16 @@
package com.vickbt.darajakmp.network.models
-import kotlin.native.ObjCName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import kotlin.native.ObjCName
-@ObjCName(swiftName = "DarajaPaymentRequest")
+@ObjCName(swiftName = "MpesaExpressRequest")
@Serializable
/**
* Request body sent to Daraja API to request Mpesa Express payment.
* */
-data class DarajaPaymentRequest(
+internal data class MpesaExpressRequest(
/**This is organizations shortcode (Paybill or Buygoods - A 5 to 7 digit account number) used to identify an organization and receive the transaction.*/
@SerialName("BusinessShortCode")
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/DarajaPaymentResponse.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/MpesaExpressResponse.kt
similarity index 95%
rename from daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/DarajaPaymentResponse.kt
rename to daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/MpesaExpressResponse.kt
index af2326a0..a02f45ab 100644
--- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/DarajaPaymentResponse.kt
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/network/models/MpesaExpressResponse.kt
@@ -20,12 +20,12 @@ import kotlin.native.ObjCName
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
-@ObjCName(swiftName = "DarajaPaymentResponse")
+@ObjCName(swiftName = "MpesaExpressResponse")
@Serializable
/**
* Response returned by Daraja API on successful Mpesa Express payment initiation.
* */
-data class DarajaPaymentResponse(
+data class MpesaExpressResponse(
/**This is a global unique Identifier for any submitted payment request.*/
@SerialName("MerchantRequestID")
var merchantRequestID: String,
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaConfigs.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaConfigs.kt
index 65815bba..11eb0b55 100644
--- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaConfigs.kt
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaConfigs.kt
@@ -16,6 +16,8 @@
package com.vickbt.darajakmp.utils
+// ToDo: Add documentation
+
internal object DarajaEndpoints {
const val PROD_BASE_URL = "api.safaricom.co.ke"
const val SANDBOX_BASE_URL = "sandbox.safaricom.co.ke"
@@ -23,6 +25,8 @@ 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_TRANSACTION = "mpesa/stkpushquery/v1/query"
+ const val C2B_REGISTRATION_URL = "mpesa/c2b/v1/registerurl"
+ const val INITIATE_C2B = "mpesa/c2b/v1/simulate"
}
enum class DarajaTransactionType {
@@ -32,3 +36,7 @@ enum class DarajaTransactionType {
enum class DarajaEnvironment {
PRODUCTION_ENVIRONMENT, SANDBOX_ENVIRONMENT
}
+
+enum class C2BResponseType {
+ CANCELED, COMPLETED
+}
diff --git a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaResult.kt b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaResult.kt
index 448e9032..5e69f446 100644
--- a/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaResult.kt
+++ b/daraja/src/commonMain/kotlin/com/vickbt/darajakmp/utils/DarajaResult.kt
@@ -24,10 +24,11 @@ import kotlin.native.ObjCName
@ObjCName(swiftName = "DarajaResult")
sealed class DarajaResult {
@ObjCName(swiftName = "Success")
- data class Success(val data: T) : DarajaResult()
+ data class Success(@ObjCName(swiftName = "data") val data: T) : DarajaResult()
@ObjCName(swiftName = "Error")
- data class Failure(val exception: DarajaException) : DarajaResult()
+ data class Failure(@ObjCName(swiftName = "error") val exception: DarajaException) :
+ DarajaResult()
// object Loading : DarajaResult() ToDo
}
@@ -37,17 +38,24 @@ sealed class DarajaResult {
* @receiver [DarajaResult]
* */
internal fun DarajaResult.getOrNull(): T? {
- return if (this is DarajaResult.Success) this.data
- else null
+ return if (this is DarajaResult.Success) {
+ this.data
+ } else {
+ null
+ }
}
/**Returns exception of type [DarajaException] on failure
*
+ * @throws DarajaException
* @receiver [DarajaResult]
* */
internal fun DarajaResult.throwOnFailure(): DarajaException {
- return if (this is DarajaResult.Failure) this.exception
- else throw DarajaException()
+ return if (this is DarajaResult.Failure) {
+ this.exception
+ } else {
+ throw DarajaException()
+ }
}
/**Returns result of type [T] on success or exception of type [DarajaException] on failure
@@ -55,8 +63,11 @@ internal fun DarajaResult.throwOnFailure(): DarajaException {
* @receiver [DarajaResult]
* */
internal fun DarajaResult.getOrThrow(): T {
- return if (this is DarajaResult.Success) this.data
- else throw this.throwOnFailure()
+ return if (this is DarajaResult.Success) {
+ this.data
+ } else {
+ throw this.throwOnFailure()
+ }
}
/* ToDo
diff --git a/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/network/DarajaApiServiceTest.kt b/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/network/DarajaApiServiceTest.kt
index a64b783b..3323e827 100644
--- a/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/network/DarajaApiServiceTest.kt
+++ b/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/network/DarajaApiServiceTest.kt
@@ -16,31 +16,23 @@
package com.vickbt.darajakmp.network
-import com.vickbt.darajakmp.network.models.AccessToken400JSON
-import com.vickbt.darajakmp.network.models.DarajaException
-import com.vickbt.darajakmp.network.models.DarajaPaymentRequest
-import com.vickbt.darajakmp.network.models.DarajaPaymentResponse
import com.vickbt.darajakmp.network.models.DarajaToken
+import com.vickbt.darajakmp.network.models.DarajaTransactionRequest
import com.vickbt.darajakmp.network.models.DarajaTransactionResponse
-import com.vickbt.darajakmp.network.models.InvalidAccessTokenJSON
-import com.vickbt.darajakmp.network.models.MpesaExpress500JSON
-import com.vickbt.darajakmp.network.models.QueryDarajaTransactionRequest
+import com.vickbt.darajakmp.network.models.MpesaExpressRequest
+import com.vickbt.darajakmp.network.models.MpesaExpressResponse
import com.vickbt.darajakmp.utils.DarajaResult
import com.vickbt.darajakmp.utils.DarajaTransactionType
import io.github.reactivecircus.cache4k.Cache
import io.ktor.client.HttpClient
-import io.ktor.http.HttpStatusCode
+import kotlinx.coroutines.test.runTest
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
-import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertNull
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.runTest
-@OptIn(ExperimentalCoroutinesApi::class)
class DarajaApiServiceTest {
private val mockDarajaHttpClient = MockDarajaHttpClient()
@@ -52,10 +44,11 @@ class DarajaApiServiceTest {
private lateinit var darajaApiService: DarajaApiService
private val darajaToken = DarajaToken(
- accessToken = "wWAHdtiE4GCSGv2ocfzQ0WHefwAJ", expiresIn = "3599"
+ accessToken = "wWAHdtiE4GCSGv2ocfzQ0WHefwAJ",
+ expiresIn = "3599"
)
- private val darajaPaymentRequest = DarajaPaymentRequest(
+ private val mpesaExpressRequest = MpesaExpressRequest(
businessShortCode = "654321",
password = "password",
phoneNumber = "254708374149",
@@ -69,7 +62,7 @@ class DarajaApiServiceTest {
accountReference = "Account reference"
)
- private val queryDarajaTransactionRequest = QueryDarajaTransactionRequest(
+ private val darajaTransactionRequest = DarajaTransactionRequest(
businessShortCode = "654321",
password = "password",
timestamp = "timestamp",
@@ -80,7 +73,7 @@ class DarajaApiServiceTest {
fun setup() {
mockKtorHttpClient = mockDarajaHttpClient.mockDarajaHttpClient
- mockInMemoryCache = Cache.Builder().build()
+ mockInMemoryCache = Cache.Builder().build()
darajaApiService = DarajaApiService(
httpClient = mockKtorHttpClient,
@@ -122,38 +115,15 @@ class DarajaApiServiceTest {
assertEquals(expected = darajaToken, actual = cachedToken)
}
- @Test
- fun fetchAccessToken_400_failure_are_caught() = runTest {
- // given
- mockDarajaHttpClient.throwError(
- httpStatus = HttpStatusCode.BadRequest,
- response = AccessToken400JSON
- )
-
- // when - then
- val exception = assertFailsWith {
- darajaApiService.fetchAccessToken()
- }
-
- assertEquals(
- expected = exception,
- actual = DarajaException(
- requestId = "43301-58413611-1",
- errorCode = "400.008.01",
- errorMessage = "Invalid Authentication passed"
- )
- )
- }
-
@Test
fun initiateMpesaExpress_success_returns_darajaPaymentResponse() = runTest {
assertNull(mockInMemoryCache.get(1))
// when
val actualResult =
- darajaApiService.initiateMpesaStk(darajaPaymentRequest = darajaPaymentRequest)
+ darajaApiService.initiateMpesaExpress(mpesaExpressRequest = mpesaExpressRequest)
val expectedResult = DarajaResult.Success(
- DarajaPaymentResponse(
+ MpesaExpressResponse(
merchantRequestID = "6093-85819535-1",
checkoutRequestID = "ws_CO_16122022001707470708374149",
responseCode = "0",
@@ -167,57 +137,11 @@ class DarajaApiServiceTest {
assertNotNull(mockInMemoryCache.get(1))
}
- @Test
- fun initiateMpesaExpress_500_failure_are_caught() = runTest {
- // given
- mockDarajaHttpClient.throwError(
- httpStatus = HttpStatusCode.InternalServerError,
- response = MpesaExpress500JSON
- )
-
- // when - then
- val exception = assertFailsWith {
- darajaApiService.queryTransaction(queryDarajaTransactionRequest = queryDarajaTransactionRequest)
- }
-
- assertEquals(
- expected = exception,
- actual = DarajaException(
- requestId = "119414-258858845-1",
- errorCode = "500.001.1001",
- errorMessage = "Unable to lock subscriber, a transaction is already in process for the current subscriber"
- )
- )
- }
-
- @Test
- fun initiateMpesaExpress_404_failure_are_caught() = runTest {
- // given
- mockDarajaHttpClient.throwError(
- httpStatus = HttpStatusCode.Unauthorized,
- response = InvalidAccessTokenJSON
- )
-
- // when - then
- val exception = assertFailsWith {
- darajaApiService.queryTransaction(queryDarajaTransactionRequest = queryDarajaTransactionRequest)
- }
-
- assertEquals(
- expected = exception,
- actual = DarajaException(
- requestId = "16813-15-1",
- errorCode = "404.001.04",
- errorMessage = "Invalid Access Token"
- )
- )
- }
-
@Test
fun queryTransaction_success_returns_darajaTransactionResponse() = runTest {
// when
val actualResult =
- darajaApiService.queryTransaction(queryDarajaTransactionRequest = queryDarajaTransactionRequest)
+ darajaApiService.queryTransaction(darajaTransactionRequest = darajaTransactionRequest)
val expectedResult = DarajaResult.Success(
DarajaTransactionResponse(
responseCode = "0",
diff --git a/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/network/MockDarajaHttpClient.kt b/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/network/MockDarajaHttpClient.kt
index 44f368fe..def8e2cd 100644
--- a/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/network/MockDarajaHttpClient.kt
+++ b/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/network/MockDarajaHttpClient.kt
@@ -55,12 +55,16 @@ internal class MockDarajaHttpClient {
when (request.url.fullPath) {
"/${DarajaEndpoints.REQUEST_ACCESS_TOKEN}" -> {
respond(
- responseContent ?: AccessToken200JSON, httpStatusCode, responseHeaders
+ responseContent ?: AccessToken200JSON,
+ httpStatusCode,
+ responseHeaders
)
}
"/${DarajaEndpoints.INITIATE_MPESA_EXPRESS}" -> {
respond(
- responseContent ?: MpesaExpress200JSON, httpStatusCode, responseHeaders
+ responseContent ?: MpesaExpress200JSON,
+ httpStatusCode,
+ responseHeaders
)
}
"/${DarajaEndpoints.QUERY_MPESA_TRANSACTION}" -> {
diff --git a/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/utils/DarajaResultTest.kt b/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/utils/DarajaResultTest.kt
index 7a34fde7..2c7ee7c5 100644
--- a/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/utils/DarajaResultTest.kt
+++ b/daraja/src/commonTest/kotlin/com/vickbt/darajakmp/utils/DarajaResultTest.kt
@@ -62,15 +62,6 @@ class DarajaResultTest {
assertEquals(expected = "Success", actual = result)
}
- /*@Test
- fun darajaResult_getOrThrow_returns_exception_on_error() {
- val result = DarajaResult.Failure(DarajaException()).getOrThrow()
-
- assertFailsWith {
- result
- }
- }*/
-
@Test
fun darajaResult_onSuccess_returns_data_on_success() {
val result = DarajaResult.Success(data = "Success")
@@ -86,7 +77,7 @@ class DarajaResultTest {
val result = DarajaResult.Failure(darajaException)
result.onSuccess {
- assertNull(it) // ToDo: Unreachable code
+ assertNull(it)
}
}
@@ -125,9 +116,7 @@ class DarajaResultTest {
fun darajaResult_onSuccess_onFailure_on_error() {
val result = DarajaResult.Failure(darajaException)
- result.onSuccess {
- assertNull(it) // ToDo: Unreachable code
- }.onFailure {
+ result.onFailure {
assertNotNull(it)
assertEquals(expected = it, actual = darajaException)
}
diff --git a/gradle.properties b/gradle.properties
index f73ebd02..39b7dcee 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -7,10 +7,10 @@ kotlin.code.style=official
#Android
android.useAndroidX=true
android.nonTransitiveRClass=true
+android.disableAutomaticComponentCreation=true
#MPP
kotlin.mpp.enableCInteropCommonization=true
-kotlin.native.binary.memoryModel=experimental
kotlin.native.ignoreDisabledTargets=true
kotlin.mpp.stability.nowarn=true
kotlin.mpp.androidSourceSetLayoutVersion1.nowarn=true
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f25967e3..e9cf3c40 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,25 +1,25 @@
[versions]
-kotlin = "1.8.20"
+kotlin = "1.9.21"
gradle = "7.4.1"
-ktLint = "10.3.0"
+ktLint = "11.6.0"
detekt = "1.19.0"
spotless = "6.2.2"
nativeCocoapod = "1.9.0"
-dokka = "1.8.20"
-kover = "0.6.1"
+dokka = "1.9.10"
+kover = "0.7.5"
mulitplatformSwiftPackage = "2.0.3"
-gradleVersionUpdate = "0.45.0"
+gradleVersionUpdate = "0.50.0"
# Kotlin Multiplatform Version
kotlinxCoroutines = "1.7.3"
-kotlinxSerializationJson = "1.5.1"
-kotlinxDateTime = "0.4.0"
+kotlinxSerializationJson = "1.6.2"
+kotlinxDateTime = "0.5.0"
napier = "2.6.1"
-ktor = "2.3.2"
+ktor = "2.3.7"
kotlinxTestResources = "0.2.2"
-composeMultiplatform = "1.5.0-beta01"
-cache4k = "0.9.0"
-mockative = "1.3.1"
+composeMultiplatform = "1.5.11"
+cache4k = "0.11.0" # 0.12.0 breaks build
+mockative = "2.0.1"
[plugins]
ktLint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktLint" }