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

MOB-875 Adding MobileMoney Charge endpoint #101

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
12 changes: 11 additions & 1 deletion Example/paystack-sdk-ios/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,17 @@ struct ContentView: View {
.padding()
}

func paymentDone(_ result: TransactionResult) {}
func paymentDone(_ result: TransactionResult) {

switch result {
case .completed(let chargeDetails):
print("Success: Transaction reference : \(chargeDetails.reference)")
case .cancelled:
print("Transaction was cancelled.")
case .error(error: let error, reference: let reference):
print("An error occured with \(reference!) : \(error.message)")
}
}
}

struct ContentView_Previews: PreviewProvider {
Expand Down
4 changes: 3 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ let package = Package(
resources: [
.copy("API/Transactions/Resources/VerifyAccessCode.json"),
.copy("API/Charge/Resources/ChargeAuthenticationResponse.json"),
.copy("API/Other/Resources/AddressStatesResponse.json")
.copy("API/Other/Resources/AddressStatesResponse.json"),
.copy("API/Charge/Resources/ChargeMobileMoneyResponse.json")

])
]
)
23 changes: 23 additions & 0 deletions Sources/PaystackSDK/API/Charge/Charge.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// swiftlint:disable file_length type_body_length line_length
Peter-John-paystack marked this conversation as resolved.
Show resolved Hide resolved
import Foundation

public extension Paystack {
private var service: ChargeService {
return ChargeServiceImplementation(config: config)
}

private var mobileMoneyService: MobileMoneyService {
return MobileMoneyServiceImplementation(config: config)
}

/// Continues the Charge flow by authenticating a user with an OTP
/// - Parameters:
/// - otp: The OTP sent to the user's device
Expand Down Expand Up @@ -73,4 +78,22 @@ public extension Paystack {
return Service(subscription)
}

/// Listens for a response after presenting a 3DS URL in a webview for authentication
/// - Parameter transactionId:The ID of the current transaction that is being authenticated
/// - Returns: A ``Service`` with the results of the authentication
func listenForMobileMoneyResponse(for transactionId: Int) -> Service<Charge3DSResponse> {
let channelName = "MOBILE_MONEY_\(transactionId)"
let subscription: any Subscription = PusherSubscription(channelName: channelName, eventName: "response")
return Service(subscription)
}

/// Initialize Mobile Money charge
/// - Parameters:
/// - mobileMoneyData: The data that needs to be passed in order to do a mobile money charge
/// - Returns: A ``Service`` with the ``MobileMoneyChargeResponse`` response
func chargeMobileMoney(with mobileMoneyData: MobileMoneyData) -> Service<MobileMoneyChargeResponse> {
let request = MobileMoneyChargeRequest(channelName: mobileMoneyData.channelName, amount: mobileMoneyData.amount, email: mobileMoneyData.email, phone: mobileMoneyData.phone, transaction: mobileMoneyData.transaction, provider: mobileMoneyData.provider)
return mobileMoneyService.postChargeMobileMoney(request)
}
}
// swiftlint:enable file_length type_body_length line_length
19 changes: 19 additions & 0 deletions Sources/PaystackSDK/API/Charge/MobileMoneyService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

protocol MobileMoneyService: PaystackService {
func postChargeMobileMoney(_ request: MobileMoneyChargeRequest) -> Service<MobileMoneyChargeResponse>
}

struct MobileMoneyServiceImplementation: MobileMoneyService {

var config: PaystackConfig

var parentPath: String {
return "charge"
}

func postChargeMobileMoney(_ request: MobileMoneyChargeRequest) -> Service<MobileMoneyChargeResponse> {
return post("/mobile_money", request)
.asService()
}
}
9 changes: 9 additions & 0 deletions Sources/PaystackSDK/Core/Models/MobileMoney.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import Foundation

// MARK: - MobileMoney
public struct MobileMoney: Codable {
let key: String
let value: String
let isNew: Bool
let phoneNumberRegex: String
}
1 change: 1 addition & 0 deletions Sources/PaystackSDK/Core/Models/Models/Channel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public enum Channel: String, Codable {
case card = "card"
case bank = "bank"
case ussd = "ussd"
case mobileMoney = "mobile_money"
case qr = "qr"
Peter-John-paystack marked this conversation as resolved.
Show resolved Hide resolved
case bankTransfer = "bank_transfer"
case unsupportedChannel
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation

// MARK: - MobileMoneyChargeRequest
struct MobileMoneyChargeRequest: Codable {
let channelName: String
let amount: Int
let email: String
let phone: String
let transaction: String
let provider: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation

// MARK: - MobileMoneyChargeResponse
public struct MobileMoneyChargeResponse: Codable {
let status: Bool
let message: String
let data: MobileMoneyChargeData
}

// MARK: - MobileMoneyChargeData
public struct MobileMoneyChargeData: Codable {
let transaction: String
let phone: String
let provider: String
let channelName: String
let display: Display

enum CodingKeys: String, CodingKey {
case transaction
case phone
case provider
case channelName
case display
}
}

// MARK: - Display
public struct Display: Codable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Peter-John-paystack The name should probably have a "MobileMoney" prefix to tie it more to the channel since we don't know if there'll be another "Display" object in the near future.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we could make it a child of the MobileMoneyChargeData struct.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can still make that change @michael-paystack Good catch.

let type: String
let message: String
let timer: Int
}
19 changes: 19 additions & 0 deletions Sources/PaystackSDK/Core/Models/Models/MobileMoneyData.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

public struct MobileMoneyData: Equatable {
let channelName: String
let amount: Int
let email: String
let phone: String
let transaction: String
let provider: String

public init(channelName: String, amount: Int, email: String, phone: String, transaction: String, provider: String) {
self.channelName = channelName
self.amount = amount
self.email = email
self.phone = phone
self.transaction = transaction
self.provider = provider
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ public struct ChannelOptions: Codable {
public var bankTransfer: [String]?
public var ussd: [String]?
public var qrCode: [String]?
public var mobileMoney: [MobileMoney]?

public init(bankTransfer: [String]? = nil, ussd: [String]? = nil, qrCode: [String]? = nil) {
public init(bankTransfer: [String]? = nil, ussd: [String]? = nil, qrCode: [String]? = nil, mobileMoney: [MobileMoney]? = nil) {
Peter-John-paystack marked this conversation as resolved.
Show resolved Hide resolved
self.bankTransfer = bankTransfer
self.ussd = ussd
self.qrCode = qrCode
self.mobileMoney = mobileMoney
}

enum CodingKeys: String, CodingKey {
case ussd
case qrCode = "qr"
case bankTransfer = "bank_transfer"
case mobileMoney = "mobile_money"
}
}
32 changes: 32 additions & 0 deletions Tests/PaystackSDKTests/API/Charge/ChargeTests.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// swiftlint:disable file_length type_body_length line_length
Peter-John-paystack marked this conversation as resolved.
Show resolved Hide resolved
import XCTest
@testable import PaystackCore

Expand Down Expand Up @@ -27,6 +28,21 @@ final class ChargeTests: PSTestCase {
_ = try serviceUnderTest.authenticateCharge(withOtp: "12345", accessCode: "abcde").sync()
}

func testMobileMoneyCharge() throws {
let mobileMoneyRequestBody = MobileMoneyChargeRequest(channelName: "MOBILE_MONEY_1504248187", amount: 1000, email: "[email protected]", phone: "0723362418", transaction: "1504248187", provider: "MPESA")

mockServiceExecutor
.expectURL("https://api.paystack.co/charge/mobile_money")
.expectMethod(.post)
.expectHeader("Authorization", "Bearer \(apiKey)")
.expectBody(mobileMoneyRequestBody)
.andReturn(json: "ChargeMobileMoneyResponse")

let mobileMoneyData = MobileMoneyData(channelName: "MOBILE_MONEY_1504248187", amount: 1000, email: "[email protected]", phone: "0723362418", transaction: "1504248187", provider: "MPESA")

_ = try serviceUnderTest.chargeMobileMoney(with: mobileMoneyData).sync()
}

func testAuthenticateChargeWithPhoneAuthentication() throws {
let phoneRequestBody = SubmitPhoneRequest(phone: "0111234567", accessCode: "abcde")

Expand Down Expand Up @@ -90,4 +106,20 @@ final class ChargeTests: PSTestCase {
_ = try serviceUnderTest.listenFor3DSResponse(for: transactionId).sync()
}

func testListenForMobileMoney() throws {
let transactionId = 1234
let mockSubscription = PusherSubscription(channelName: "MOBILE_MONEY_\(transactionId)",
eventName: "response")

// swiftlint:disable:next line_length
let responseString = "{\"redirecturl\":\"?trxref=2wdckavunc&reference=2wdckavunc\",\"trans\":\"1234\",\"trxref\":\"2wdckavunc\",\"reference\":\"2wdckavunc\",\"status\":\"success\",\"message\":\"Success\",\"response\":\"Approved\"}"

mockSubscriptionListener
.expectSubscription(mockSubscription)
.andReturnString(responseString)

_ = try serviceUnderTest.listenForMobileMoneyResponse(for: transactionId).sync()
}

}
// swiftlint:enable file_length type_body_length line_length
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"status": true,
"message": "Charge attempted",
"data": {
"transaction": "1504248187",
"phone": "0703362111",
"provider": "MPESA",
"channel_name": "MOBILE_MONEY_1504248187",
"display": {
"type": "pop",
"message": "Please complete authorization process on your mobile phone",
"timer": 60
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,27 @@
"card",
"qr",
"ussd",
"eft"
"eft",
"mobile_money"
],
"channel_options": {
"qr": [
"visa"
],
"mobile_money": [
{
"key": "MPESA",
"value": "M-PESA",
"isNew": true,
"phoneNumberRegex": "^\\+254(7([0-2]\\d|4\\d|5(7|8|9)|6(8|9)|9[0-9])|(11\\d))\\d{6}$"
},
{
"key": "MPESA_OFF",
"value": "M-PESA",
"isNew": false,
"phoneNumberRegex": "^\\+254(7([0-2]\\d|4\\d|5(7|8|9)|6(8|9)|9[0-9])|(11\\d))\\d{6}$"
}
],
"ussd": [
"737",
"822",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class ChargeRepositoryImplementationTests: PSTestCase {
let expectedResult = VerifyAccessCode(amount: 10000,
currency: "NGN",
accessCode: "Access_Code_Test",
paymentChannels: [.card, .qr, .ussd],
paymentChannels: [.card, .qr, .ussd, .mobileMoney],
domain: .test,
merchantName: "Test Merchant",
publicEncryptionKey: "test_encryption_key",
Expand Down