Skip to content

Commit

Permalink
Merge pull request #137 from VictorKabata/develop
Browse files Browse the repository at this point in the history
Develop -> Main
  • Loading branch information
VictorKabata authored Nov 11, 2024
2 parents 88bfc5b + 765faf1 commit 130483d
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 125 deletions.
60 changes: 60 additions & 0 deletions .github/contributing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Contribution Guidelines

Thank you for your interest in contributing to Daraja Multiplatform! This project is aims to streamline the integration of M-Pesa services across multiple platforms by providing a simplified interface to the [Daraja API](https://developer.safaricom.co.ke/APIs). All kinds of contributions are welcome, whether it's fixing bugs, adding new features, or improving documentation.

## Getting Started

To get started with contributing to this project:

- Familiarize yourself with the project by going over the project [documentation](https://victorkabata.github.io/DarajaMultiplatform/) and exploring the [codebase](https://github.com/VictorKabata/DarajaMultiplatform/tree/main).
- Click the ["Fork"](https://github.com/VictorKabata/DarajaMultiplatform/fork) button on the Daraja Multiplatform repository page on GitHub. This creates a copy of the repository in your own account.
- Clone your forked repository to your local machine using Git.
```curl
git clone https://github.io/your-username/DarajaMultiplatform.git
```
- Follow the [project's setup instructions]() to set up the development enviroment.

## How to Contribute

### Reporting Bugs

If you encounter a bug, please [create a new issue](https://github.com/VictorKabata/DarajaMultiplatform/issues/new/choose) and include as much detail as possible, such as:

- Steps to reproduce the issue.
- Expected outcome vs actual outcome.
- Relevant screenshots or error logs (if applicable).

### Suggesting Features
For feature requests, [submit a new issue](https://github.com/VictorKabata/DarajaMultiplatform/issues/new/choose) and outline:
- The feature you’re proposing.
- Why it would improve the project.
- Any ideas on implementation(if available).

### Making Changes
If you'd like to contribute code:
- Open an issue to discuss your idea and get feedback before you begin.
- Create a new branch for your specific contribution with a descriptive name that reflects the changes you plan to make.
```bash
git checkout -b feature/your-feature-name
```

- Make your changes: Write clear, concise, and well-documented code. Use clear and concise commit messages that describe the changes you've made in each commit to maintain a clean commit history.
- Ensure your changes don't break any existing functionalities by running the tests provided in the project and adding new test cases.
- Push your changes to your forked repository branch and open a pull request from your branch to the __develop__ branch of the upstream repository. All pull requests description must follow this [PR template](https://github.com/VictorKabata/DarajaMultiplatform/blob/develop/.github/pull_request_template.md).

### Code Style

This project uses ktLint and detekt to enforce code style and quality standards in all Kotlin files. Following these standards helps maintain a consistent codebase and makes the code easier to read and review.

Run the lint check commands locally before commiting changes to catch any linting issues early

```bash
./gradlew :daraja:ktlintFormat
```

```bash
./gradlew :daraja:detekt
```

## License
By contributing, you agree that your contributions will be licensed under the same license as the project.
12 changes: 11 additions & 1 deletion .github/workflows/publish_kmp_lib.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,14 @@ jobs:
POM_SCM_URL: ${{ secrets.POM_SCM_URL }}
SIGNING_ID: ${{ secrets.SIGNING_ID }}
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}

- name: Create Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
prerelease: true
tag_name: ${{ github.event.inputs.versionName }}
name: Release ${{ github.event.inputs.versionName }}
7 changes: 1 addition & 6 deletions .github/workflows/publish_swift_package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ name: Create and Publish Swift Package

on:
workflow_dispatch:
inputs:
iOSVersionName:
description: 'iOS Version Name (eg. 0.9.0)'
required: true

jobs:
build:
Expand Down Expand Up @@ -60,5 +56,4 @@ jobs:
source-directory: 'swiftpackage'
destination-github-username: 'VictorKabata'
destination-repository-name: 'DarajaSwiftPackage'
create-target-branch-if-needed: true
target-branch: ${{ github.event.inputs.iOSVersionName }}
create-target-branch-if-needed: true
125 changes: 13 additions & 112 deletions daraja/src/commonMain/kotlin/com/vickbt/darajakmp/Daraja.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,17 @@ package com.vickbt.darajakmp

import com.vickbt.darajakmp.network.DarajaApiService
import com.vickbt.darajakmp.network.DarajaHttpClientFactory
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.DarajaException
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.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.DarajaResult
import com.vickbt.darajakmp.utils.DarajaTransactionCode
import com.vickbt.darajakmp.utils.DarajaTransactionType
import com.vickbt.darajakmp.utils.capitalize
import com.vickbt.darajakmp.utils.getDarajaPassword
import com.vickbt.darajakmp.utils.getDarajaPhoneNumber
import com.vickbt.darajakmp.utils.getDarajaTimestamp
Expand All @@ -56,9 +48,9 @@ import kotlin.native.ObjCName
* */
@ObjCName(swiftName = "Daraja")
class Daraja(
private val consumerKey: String?,
private val consumerSecret: String?,
private val passKey: String?,
private val consumerKey: String,
private val consumerSecret: String,
private val passKey: String,
private val environment: DarajaEnvironment? = DarajaEnvironment.SANDBOX_ENVIRONMENT,
) {
private val darajaHttpClientFactory: HttpClient =
Expand All @@ -74,10 +66,10 @@ class Daraja(
* @param [environment]
* */
data class Builder(
@ObjCName(swiftName = "consumerKey") private var consumerKey: String? = null,
@ObjCName(swiftName = "consumerSecret") private var consumerSecret: String? = null,
@ObjCName(swiftName = "passKey") private var passKey: String? = null,
@ObjCName(swiftName = "darajaEnvironment") private var environment: DarajaEnvironment? = null,
@ObjCName(swiftName = "consumerKey") private var consumerKey: String,
@ObjCName(swiftName = "consumerSecret") private var consumerSecret: String,
@ObjCName(swiftName = "passKey") private var passKey: String,
@ObjCName(swiftName = "darajaEnvironment") private var environment: DarajaEnvironment = DarajaEnvironment.SANDBOX_ENVIRONMENT,
) {
/**Provides [consumerKey] provided by Daraja API
*
Expand All @@ -101,10 +93,10 @@ class Daraja(
fun setPassKey(passKey: String) = apply { this.passKey = passKey }

/**Set Daraja API environment to Sandbox/Testing mode*/
fun isSandbox() = apply { this.environment = DarajaEnvironment.SANDBOX_ENVIRONMENT }
fun setSandboxEnvironment() = apply { this.environment = DarajaEnvironment.SANDBOX_ENVIRONMENT }

/**Set Daraja API environment to Production/Live mode*/
fun isProduction() = apply { this.environment = DarajaEnvironment.PRODUCTION_ENVIRONMENT }
fun setProductionEnvironment() = apply { this.environment = DarajaEnvironment.PRODUCTION_ENVIRONMENT }

/**Create an instance of [Daraja] object with [consumerKey], [consumerSecret] and [passKey] provided*/
@ObjCName(swiftName = "init")
Expand All @@ -122,11 +114,9 @@ class Daraja(
DarajaApiService(
httpClient = darajaHttpClientFactory,
consumerKey =
consumerKey
?: throw DarajaException(errorMessage = "Consumer key is null"),
consumerKey,
consumerSecret =
consumerSecret
?: throw DarajaException(errorMessage = "Consumer secret is null"),
consumerSecret,
)

/**Request access token that is used to authenticate to Daraja APIs
Expand Down Expand Up @@ -168,7 +158,7 @@ class Daraja(
val darajaPassword =
getDarajaPassword(
shortCode = businessShortCode,
passkey = passKey ?: throw DarajaException(errorMessage = "Pass key is null"),
passkey = passKey,
timestamp = timestamp,
)

Expand Down Expand Up @@ -205,7 +195,7 @@ class Daraja(
val darajaPassword =
getDarajaPassword(
shortCode = businessShortCode,
passkey = passKey ?: "",
passkey = passKey,
timestamp = timestamp,
)

Expand Down Expand Up @@ -261,93 +251,4 @@ class Daraja(

darajaApiService.generateDynamicQr(dynamicQrRequest = dynamicQrRequest)
}

/**Request the status of an Mpesa payment transaction
*
* @param [businessShortCode] This is organizations shortcode (Paybill or Buy Goods - 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.
*
* @return [DarajaTransactionResponse]
* */
@ObjCName(swiftName = "transactionStatus")
internal fun transactionStatus(
businessShortCode: String,
checkoutRequestID: String,
): DarajaResult<DarajaTransactionResponse> =
runBlocking(Dispatchers.IO) {
val timestamp = Clock.System.now().getDarajaTimestamp()
val darajaPassword =
getDarajaPassword(
shortCode = businessShortCode,
passkey = passKey ?: throw DarajaException(errorMessage = "Pass key is null"),
timestamp = timestamp,
)

val darajaTransactionRequest =
DarajaTransactionRequest(
businessShortCode = businessShortCode,
password = darajaPassword,
timestamp = timestamp,
checkoutRequestID = checkoutRequestID,
)

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 [C2BRegistrationResponse]
* */
fun c2bRegistration(
businessShortCode: String,
confirmationURL: String,
validationURL: String,
responseType: C2BResponseType = C2BResponseType.COMPLETED,
): DarajaResult<C2BResponse> =
runBlocking(Dispatchers.IO) {
val c2BRegistrationRequest =
C2BRegistrationRequest(
confirmationURL = confirmationURL,
validationURL = validationURL,
responseType =
if (validationURL.isEmpty()) {
C2BResponseType.COMPLETED.name.capitalize()
} else {
responseType.name.capitalize()
},
shortCode = businessShortCode,
)

darajaApiService.c2bRegistration(c2bRegistrationRequest = c2BRegistrationRequest)
}

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

darajaApiService.c2b(c2bRequest = c2bRequest)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ internal class DarajaApiService(
private val consumerSecret: String,
private val inMemoryCache: Cache<Long, DarajaToken> =
Cache.Builder<Long, DarajaToken>()
.expireAfterWrite(3600.seconds).build(),
.expireAfterWrite(3000.seconds).build(),
) {
/** Initiate API call using the [httpClient] provided by Ktor to fetch Daraja API access token
* of type [DarajaToken]*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import io.ktor.util.network.UnresolvedAddressException
* an instance of [DarajaResult] with exception of type [DarajaException] on failure
*
* @return [DarajaResult] Returns data of type [T] on success
* @throws DarajaException Throws expception of type [DarajaException] on failure
* @throws DarajaException Throws exception of type [DarajaException] on failure
* */
internal suspend fun <T : Any> darajaSafeApiCall(apiCall: suspend () -> T): DarajaResult<T> =
try {
Expand All @@ -52,6 +52,11 @@ internal suspend fun <T : Any> darajaSafeApiCall(apiCall: suspend () -> T): Dara
DarajaResult.Failure(exception = error)
}

fun some() =
runCatching {
}.map {
}

/**Generate [DarajaException] from network or system error when making network calls
*
* @throws [DarajaException]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import kotlin.native.ObjCName
* @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.
* @param [shortCode] A unique number is tagged to an M-PESA pay bill/till number of the organization.
* */
data class C2BRegistrationRequest(
internal data class C2BRegistrationRequest(
@SerialName("ConfirmationURL")
val confirmationURL: String,
@SerialName("ValidationURL")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import kotlin.native.ObjCName
@ObjCName(swiftName = "C2BRequest")
@Serializable
/**Request C2B M-Pesa payment*/
data class C2BRequest(
internal data class C2BRequest(
@SerialName("Amount")
val amount: Int,
@SerialName("BillRefNumber")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import kotlin.native.ObjCName
* @param [responseCode] It indicates whether Mobile Money accepts the request or not.
* @param [responseDescription] This is the status of the request.
* */
data class C2BResponse(
internal data class C2BResponse(
@SerialName("OriginatorCoversationID")
val originatorConversationId: String,
@SerialName("ResponseCode")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ enum class DarajaEnvironment {
SANDBOX_ENVIRONMENT,
}

enum class C2BResponseType {
internal enum class C2BResponseType {
CANCELED,
COMPLETED,
}
Expand Down

0 comments on commit 130483d

Please sign in to comment.