From 5eb18e25826c61b65e33700b1103491c447140e0 Mon Sep 17 00:00:00 2001 From: Oleh Hudeichuk Date: Mon, 6 Apr 2020 00:22:40 +0300 Subject: [PATCH] init ~ 0.2.1 --- .gitignore | 6 + Package.resolved | 16 ++ Package.swift | 25 +++ README.md | 3 + .../Models/Request/WFPCharge+Request.swift | 85 ++++++++++ .../Models/Response/WFPCharge+Response.swift | 37 ++++ Sources/WFPClient/HTTP/WFPHttpClient.swift | 31 ++++ .../WFPClient/HTTP/WFPHttpClientPrtcl.swift | 13 ++ Sources/WFPClient/HTTP/WFPHttpMethod.swift | 13 ++ Sources/WFPClient/Helpers/WFPCurrency.swift | 15 ++ Sources/WFPClient/Helpers/WFPError.swift | 21 +++ .../WFPMerchantTransactionSecureType.swift | 14 ++ .../Helpers/WFPMerchantTransactionType.swift | 13 ++ Sources/WFPClient/Helpers/WFPReasonCode.swift | 160 ++++++++++++++++++ .../Helpers/WFPTransactionStatus.swift | 40 +++++ .../Helpers/WFPTransactionType.swift | 12 ++ Sources/WFPClient/WFPClient.swift | 96 +++++++++++ 17 files changed, 600 insertions(+) create mode 100644 .gitignore create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/WFPClient/ApplePay/Models/Request/WFPCharge+Request.swift create mode 100644 Sources/WFPClient/ApplePay/Models/Response/WFPCharge+Response.swift create mode 100644 Sources/WFPClient/HTTP/WFPHttpClient.swift create mode 100644 Sources/WFPClient/HTTP/WFPHttpClientPrtcl.swift create mode 100644 Sources/WFPClient/HTTP/WFPHttpMethod.swift create mode 100644 Sources/WFPClient/Helpers/WFPCurrency.swift create mode 100644 Sources/WFPClient/Helpers/WFPError.swift create mode 100644 Sources/WFPClient/Helpers/WFPMerchantTransactionSecureType.swift create mode 100644 Sources/WFPClient/Helpers/WFPMerchantTransactionType.swift create mode 100644 Sources/WFPClient/Helpers/WFPReasonCode.swift create mode 100644 Sources/WFPClient/Helpers/WFPTransactionStatus.swift create mode 100644 Sources/WFPClient/Helpers/WFPTransactionType.swift create mode 100644 Sources/WFPClient/WFPClient.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5b99a56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +.swiftpm \ No newline at end of file diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..a31d387 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "SwiftExtensionsPack", + "repositoryURL": "https://github.com/nerzh/swift-extensions-pack.git", + "state": { + "branch": null, + "revision": "b77864367a0a75ddf1fb0dd2daf472e0163888fa", + "version": "0.3.0" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..6b9ddfd --- /dev/null +++ b/Package.swift @@ -0,0 +1,25 @@ +// swift-tools-version:5.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "WFPClient", + platforms: [.iOS(.v11)], + products: [ + .library( + name: "WFPClient", + targets: ["WFPClient"]), + ], + dependencies: [ + .package(name: "SwiftExtensionsPack", url: "https://github.com/nerzh/swift-extensions-pack.git", from: "0.3.0"), + ], + targets: [ + .target( + name: "WFPClient", + dependencies: [ + .product(name: "SwiftExtensionsPack", package: "SwiftExtensionsPack"), + ] + ) + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..767c9f5 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# WayForPay-Swift-Client + +A description of this package. diff --git a/Sources/WFPClient/ApplePay/Models/Request/WFPCharge+Request.swift b/Sources/WFPClient/ApplePay/Models/Request/WFPCharge+Request.swift new file mode 100644 index 0000000..cf105df --- /dev/null +++ b/Sources/WFPClient/ApplePay/Models/Request/WFPCharge+Request.swift @@ -0,0 +1,85 @@ +// +// WFPCharge+Request.swift +// PetsProjectApp +// +// Created by Oleh Hudeichuk on 04.04.2020. +// Copyright © 2020 Oleh Hudeichuk. All rights reserved. +// + +public struct WFPChargeRequest: Codable { + + public var apiVersion: Int + public var transactionType: WFPTransactionType + public var merchantAccount: String + public var merchantDomainName: String + public var orderReference: String + public var orderDate: Int64 + public var amount: Int + public var currency: WFPCurrency + public var productName: [String] + public var productPrice: [Int] + public var productCount: [Int] + public var clientFirstName: String + public var clientLastName: String + public var clientCountry: String + public var clientEmail: String + public var clientPhone: String + public var clientIpAddress: String + public var merchantSignature: String + public var merchantTransactionType: WFPMerchantTransactionType + public var merchantTransactionSecureType: WFPMerchantTransactionSecureType + public var applePayString: String + public var socialUri: String + /// max 1728000 sec; min 60 sec + public var holdTimeout: Int + + public init(apiVersion: Int = 1, + transactionType: WFPTransactionType, + merchantAccount: String, + merchantDomainName: String, + orderReference: String, + orderDate: Int64, + amount: Int = 0, + currency: WFPCurrency, + productName: [String] = [], + productPrice: [Int] = [], + productCount: [Int] = [], + clientFirstName: String, + clientLastName: String, + clientCountry: String, + clientEmail: String, + clientPhone: String, + clientIpAddress: String = "127.0.0.1", + merchantSignature: String = "", + merchantTransactionType: WFPMerchantTransactionType, + merchantTransactionSecureType: WFPMerchantTransactionSecureType, + applePayString: String = "", + socialUri: String = "", + holdTimeout: Int = 1_728_000 + ) { + self.apiVersion = apiVersion + self.transactionType = transactionType + self.merchantAccount = merchantAccount + self.merchantDomainName = merchantDomainName + self.orderReference = orderReference + self.orderDate = orderDate + self.amount = amount + self.currency = currency + self.productName = productName + self.productPrice = productPrice + self.productCount = productCount + self.clientFirstName = clientFirstName + self.clientLastName = clientLastName + self.clientCountry = clientCountry + self.clientEmail = clientEmail + self.clientPhone = clientPhone + self.clientIpAddress = clientIpAddress + self.merchantSignature = merchantSignature + self.merchantTransactionType = merchantTransactionType + self.merchantTransactionSecureType = merchantTransactionSecureType + self.applePayString = applePayString + self.socialUri = socialUri + self.holdTimeout = holdTimeout + } +} + diff --git a/Sources/WFPClient/ApplePay/Models/Response/WFPCharge+Response.swift b/Sources/WFPClient/ApplePay/Models/Response/WFPCharge+Response.swift new file mode 100644 index 0000000..a9dbef5 --- /dev/null +++ b/Sources/WFPClient/ApplePay/Models/Response/WFPCharge+Response.swift @@ -0,0 +1,37 @@ +// +// WFPCharge+Response.swift +// PetsProjectApp +// +// Created by Oleh Hudeichuk on 04.04.2020. +// Copyright © 2020 Oleh Hudeichuk. All rights reserved. +// + +public struct WFPChargeResponse: Codable { + + public var reason: String + public var reasonCode: WFPReasonCode + public var merchantAccount: String? + public var authTicket: String? + public var orderReference: String? + public var merchantSignature: String? + public var amount: Double? + public var currency: WFPCurrency? + public var authCode: String? + public var email: String? + public var phone: String? + public var createdDate: Int? + public var processingDate: Int? + public var cardPan: String? + public var cardType: String? + public var issuerBankCountry: String? + public var issuerBankName: String? + public var recToken: String? + public var transactionStatus: WFPTransactionStatus? + public var fee: Double? + public var paymentSystem: String? + public var merchantTransactionType: WFPMerchantTransactionType? + public var d3AcsUrl: String? + public var d3Md: String? + public var d3Pareq: String? + public var returnUrl: String? +} diff --git a/Sources/WFPClient/HTTP/WFPHttpClient.swift b/Sources/WFPClient/HTTP/WFPHttpClient.swift new file mode 100644 index 0000000..c699d70 --- /dev/null +++ b/Sources/WFPClient/HTTP/WFPHttpClient.swift @@ -0,0 +1,31 @@ +// +// File.swift +// +// +// Created by Oleh Hudeichuk on 05.04.2020. +// + +import Foundation +import SwiftExtensionsPack + +open class WFPHttpClient: WFPHttpClientPrtcl { + + public init() {} + + public func sendRequest(url: URL, + method: WFPHttpMethod, + body: Data, + _ handler: @escaping (Result) -> Void + ) throws { + try Net.sendRequest(url: url.absoluteString, + method: method.rawValue, + body: body + ) { (data, response, error) in + if let data = data { + handler(.success(data)) + } else if let error = error { + handler(.failure(error)) + } + } + } +} diff --git a/Sources/WFPClient/HTTP/WFPHttpClientPrtcl.swift b/Sources/WFPClient/HTTP/WFPHttpClientPrtcl.swift new file mode 100644 index 0000000..ea58e5c --- /dev/null +++ b/Sources/WFPClient/HTTP/WFPHttpClientPrtcl.swift @@ -0,0 +1,13 @@ +// +// WFPHttpClientPrtcl.swift +// +// +// Created by Oleh Hudeichuk on 05.04.2020. +// + +import Foundation + +public protocol WFPHttpClientPrtcl { + + func sendRequest(url: URL, method: WFPHttpMethod, body: Data, _ handler: @escaping (Result) -> Void) throws +} diff --git a/Sources/WFPClient/HTTP/WFPHttpMethod.swift b/Sources/WFPClient/HTTP/WFPHttpMethod.swift new file mode 100644 index 0000000..dff5b92 --- /dev/null +++ b/Sources/WFPClient/HTTP/WFPHttpMethod.swift @@ -0,0 +1,13 @@ +// +// WFPHttpMethod.swift +// +// +// Created by Oleh Hudeichuk on 05.04.2020. +// + +import Foundation + +public enum WFPHttpMethod: String { + case GET + case POST +} diff --git a/Sources/WFPClient/Helpers/WFPCurrency.swift b/Sources/WFPClient/Helpers/WFPCurrency.swift new file mode 100644 index 0000000..30be71c --- /dev/null +++ b/Sources/WFPClient/Helpers/WFPCurrency.swift @@ -0,0 +1,15 @@ +// +// WFPCurrency.swift +// PetsProjectApp +// +// Created by Oleh Hudeichuk on 04.04.2020. +// Copyright © 2020 Oleh Hudeichuk. All rights reserved. +// + +import Foundation + +public enum WFPCurrency: String, Codable { + case UAH + case USD + case EUR +} diff --git a/Sources/WFPClient/Helpers/WFPError.swift b/Sources/WFPClient/Helpers/WFPError.swift new file mode 100644 index 0000000..282b58b --- /dev/null +++ b/Sources/WFPClient/Helpers/WFPError.swift @@ -0,0 +1,21 @@ +// +// File.swift +// +// +// Created by Oleh Hudeichuk on 05.04.2020. +// + +import Foundation + +public enum WFPError: Error, CustomStringConvertible { + case codableError + + public var description: String { + switch self { + case .codableError: + return "Can't convert to codable model" + } + } + + public var localizedDescription: String { description } +} diff --git a/Sources/WFPClient/Helpers/WFPMerchantTransactionSecureType.swift b/Sources/WFPClient/Helpers/WFPMerchantTransactionSecureType.swift new file mode 100644 index 0000000..e3ad78b --- /dev/null +++ b/Sources/WFPClient/Helpers/WFPMerchantTransactionSecureType.swift @@ -0,0 +1,14 @@ +// +// WFPMerchantTransactionSecureType.swift +// +// +// Created by Oleh Hudeichuk on 05.04.2020. +// + +import Foundation + +public enum WFPMerchantTransactionSecureType: String, Codable { + case AUTO = "AUTO" + case ThreeDS = "3DS" + case NON3DS = "NON3DS" +} diff --git a/Sources/WFPClient/Helpers/WFPMerchantTransactionType.swift b/Sources/WFPClient/Helpers/WFPMerchantTransactionType.swift new file mode 100644 index 0000000..dbe59d5 --- /dev/null +++ b/Sources/WFPClient/Helpers/WFPMerchantTransactionType.swift @@ -0,0 +1,13 @@ +// +// WFPMerchantTransactionType.swift +// +// +// Created by Oleh Hudeichuk on 05.04.2020. +// + +import Foundation + +public enum WFPMerchantTransactionType: String, Codable { + case SALE + case AUTH +} diff --git a/Sources/WFPClient/Helpers/WFPReasonCode.swift b/Sources/WFPClient/Helpers/WFPReasonCode.swift new file mode 100644 index 0000000..e8dfc5f --- /dev/null +++ b/Sources/WFPClient/Helpers/WFPReasonCode.swift @@ -0,0 +1,160 @@ +// +// WFPReasonCode.swift +// +// +// Created by Oleh Hudeichuk on 05.04.2020. +// + +import Foundation + +public enum WFPReasonCode: Int, Codable { + case Ок = 1100 + case DeclinedToCardIssuer = 1101 + case BadCVV2 = 1102 + case ExpiredCard = 1103 + case InsufficientFunds = 1104 + case InvalidCard = 1105 + case ExceedWithdrawalFrequency = 1106 + case ThreeDsFail = 1108 + case FormatError = 1109 + case invalidCurrency = 1110 + case DuplicateOrderID = 1112 + case InvalidSignature = 1113 + case Fraud = 1114 + case ParameterIsMissing = 1115 + case TokenNotFound = 1116 + case APINotAllowed = 1117 + case MerchantRestriction = 1118 + case AuthenticationUnavailable = 1120 + case AccountNotFound = 1121 + case GateDeclined = 1122 + case RefundNotAllowed = 1123 + case CardholderSessionExpired = 1124 + case CardholderCanceledTheRequest = 1125 + case IllegalOrderState = 1126 + case OrderNotFound = 1127 + case RefundLimitExcended = 1128 + case ScriptError = 1129 + case InvalidAmount = 1130 + case TransactionInProcessing = 1131 + case TransactionIsDelayed = 1132 + case InvalidCommission = 1133 + case TransactionIsPending = 1134 + case CardLimitsFailed = 1135 + case MerchantBalanceIsVerySmall = 1136 + case InvalidConfirmationAmount = 1137 + case RefundInProcessing = 1138 + case ExternalDeclineWhileCredit = 1139 + case ExceedWithdrawalFrequencyWhileCredit = 1140 + case PartialVoidIsNotSupported = 1141 + case RefusedACredit = 1142 + case InvalidPhoneNumber = 1143 + case TransactionIsAwaitingDelivery = 1144 + case TransactionIsAwaitingCreditDecision = 1145 + case RestrictedCard = 1146 + case ClientIsNotFound = 1147 + case ClientIsNotLinked = 1148 + case ClientIsLocked = 1149 + case Wait3dsData = 5100 + + public var description: String { + switch self { + case .Ок: + return "Операция выполнена без ошибок" + case .DeclinedToCardIssuer: + return "Отказ Банка эмитента проводить операцию" + case .BadCVV2: + return "Неверный CVV код" + case .ExpiredCard: + return "Карта просрочена или неверно указан срок действия" + case .InsufficientFunds: + return "Недостаточно средств" + case .InvalidCard: + return "Введен неверный номер карты, либо карта в недопустимом состоянии." + case .ExceedWithdrawalFrequency: + return "Превышен лимит операций по карте - возможно карта не открыта для оплаты в интернет" + case .ThreeDsFail: + return "Невозможно выполнить 3DS транзакцию, либо неверный код подтверждения 3DS" + case .FormatError: + return "Ошибка на стороне мерчанта — неверно сформирована транзакция" + case .invalidCurrency: + return "Ошибка на стороне мерчанта- неверная валюта" + case .DuplicateOrderID: + return "Дублирующий orderid" + case .InvalidSignature: + return "Не правильная подпись мерчанта" + case .Fraud: + return "Фродовая транзакция согласно антифрод фильтров" + case .ParameterIsMissing: + return "Один или несколько обязательных параметров не переданы" + case .TokenNotFound: + return "Попытка списания с карты клиента по токену неуспешна - используется неверное значение" + case .APINotAllowed: + return "Данный API не разрешен к использованию для мерчанта" + case .MerchantRestriction: + return "Превышен лимит Магазина или транзакции запрещены Магазину" + case .AuthenticationUnavailable: + return "3-D Secure авторизация недоступна" + case .AccountNotFound: + return "Аккаунт не найден" + case .GateDeclined: + return "Отказ шлюза в выполнении операции" + case .RefundNotAllowed: + return "Возврат не может быть выполнен" + case .CardholderSessionExpired: + return "Сессия пользователя истекла" + case .CardholderCanceledTheRequest: + return "Транзакция отменена пользователем" + case .IllegalOrderState: + return "Попытка выполнения недопустимой операции для текущего состояния платежа" + case .OrderNotFound: + return "Транзакция не найдена" + case .RefundLimitExcended: + return "Превышено допустимое число попыток произвести возврат (Refund)" + case .ScriptError: + return "Ошибка сценария" + case .InvalidAmount: + return "Неправильная сумма" + case .TransactionInProcessing: + return "Заказ обрабатывается. Заказ все еще находится в процессе обработки платежным шлюзом" + case .TransactionIsDelayed: + return "Клиент решил отложить оплату, ему на почту отправлена ссылка для завершения платежа" + case .InvalidCommission: + return "Неверная комиссия" + case .TransactionIsPending: + return "Транзакция на проверке Antifraud" + case .CardLimitsFailed: + return "Превышен лимит по карте" + case .MerchantBalanceIsVerySmall: + return "Недостаточно средств на балансе мерчанта" + case .InvalidConfirmationAmount: + return "Неправильная сумма подтверждения верификации карты" + case .RefundInProcessing: + return "Запрос на возврат принят и будет проведен как только на балансе магазина будет достаточно денег для его проведения" + case .ExternalDeclineWhileCredit: + return "Отказ в зачислении средств на карту получателя" + case .ExceedWithdrawalFrequencyWhileCredit: + return "Превышен лимит при зачислении средств на карту получателя" + case .PartialVoidIsNotSupported: + return "Частичная отмена холда не доступна" + case .RefusedACredit: + return "Отказано в кредите" + case .InvalidPhoneNumber: + return "Неверный номер телефона" + case .TransactionIsAwaitingDelivery: + return "" + case .TransactionIsAwaitingCreditDecision: + return "Ожидание решения о предоставлении кредита" + case .RestrictedCard: + return "Карта заблокирована в Банке" + case .ClientIsNotFound: + return "Клиент не найден" + case .ClientIsNotLinked: + return "Клиент найден, но не привязан к текущему мерчанту" + case .ClientIsLocked: + return "Клиент временно заблокирован" + case .Wait3dsData: + return "Ожидание 3d secure верификации" + } + } +} diff --git a/Sources/WFPClient/Helpers/WFPTransactionStatus.swift b/Sources/WFPClient/Helpers/WFPTransactionStatus.swift new file mode 100644 index 0000000..db2b15a --- /dev/null +++ b/Sources/WFPClient/Helpers/WFPTransactionStatus.swift @@ -0,0 +1,40 @@ +// +// WFPTransactionStatus.swift +// +// +// Created by Oleh Hudeichuk on 05.04.2020. +// + +import Foundation + +public enum WFPTransactionStatus: String, Codable { + case InProcessing + case WaitingAuthComplete + case Approved + case Pending + case Expired + case Refunded = "Refunded/Voided" + case Declined + case RefundInProcessing + + public var description: String { + switch self { + case .Approved: + return "Успешный платеж" + case .InProcessing: + return "В обработке" + case .WaitingAuthComplete: + return "Успешный Hold" + case .Pending: + return "На проверке Antifraud" + case .Expired: + return "Истек срок оплаты" + case .Refunded: + return "Возврат" + case .Declined: + return "Отклонен" + case .RefundInProcessing: + return "Возврат в обработке" + } + } +} diff --git a/Sources/WFPClient/Helpers/WFPTransactionType.swift b/Sources/WFPClient/Helpers/WFPTransactionType.swift new file mode 100644 index 0000000..94152e5 --- /dev/null +++ b/Sources/WFPClient/Helpers/WFPTransactionType.swift @@ -0,0 +1,12 @@ +// +// WFPTransactionType.swift +// +// +// Created by Oleh Hudeichuk on 05.04.2020. +// + +import Foundation + +public enum WFPTransactionType: String, Codable { + case CHARGE +} diff --git a/Sources/WFPClient/WFPClient.swift b/Sources/WFPClient/WFPClient.swift new file mode 100644 index 0000000..f5d5c8f --- /dev/null +++ b/Sources/WFPClient/WFPClient.swift @@ -0,0 +1,96 @@ +// +// WayForPay.swift +// PetsProjectApp +// +// Created by Oleh Hudeichuk on 04.04.2020. +// Copyright © 2020 Oleh Hudeichuk. All rights reserved. +// + +import SwiftExtensionsPack +import Foundation + +open class WFPClient { + + public var apiURL: URL + public var httpClient: WFPHttpClientPrtcl + public var request: WFPChargeRequest + public var secretKey: String + public var isSigned: Bool { + !request.merchantSignature.isEmpty + } + + public init(apiURL: URL, httpClient: WFPHttpClientPrtcl = WFPHttpClient(), request: WFPChargeRequest, secretKey: String) { + self.apiURL = apiURL + self.request = request + self.secretKey = secretKey + self.httpClient = httpClient + } + + public func addProduct(name: String, price: Int, count: Int) { + request.productName.append(name) + request.productPrice.append(price) + request.productCount.append(count) + request.amount += price * count + } + + public func generateSignature() { + request.merchantSignature = generateSignatureString().hmac(algorithm: .MD5, key: secretKey) + } + + public func addApplePaymentData(json: String) { + request.applePayString = json + } + + public func addApplePaymentData(paymentData: Data) { + if let json = String(data: paymentData, encoding: .utf8) { + addApplePaymentData(json: json) + } + } + + public func sendPaymentRequest(_ handler: @escaping (Result) -> Void) throws { + let encodedData = try JSONEncoder().encode(request) + try httpClient.sendRequest(url: apiURL, + method: .POST, + body: encodedData + ) { (result) in + switch result { + case let .success(data): + if let response: WFPChargeResponse = try? JSONDecoder().decode(WFPChargeResponse.self, from: data) { + handler(.success(response)) + } else { + handler(.failure(WFPError.codableError)) + } + case let .failure(error): + handler(.failure(error)) + } + } + } + + private func generateSignatureString() -> String { + var string: String = "" + addSignItem(to: &string, request.merchantAccount) + addSignItem(to: &string, request.merchantDomainName) + addSignItem(to: &string, request.orderReference) + addSignItem(to: &string, String(request.orderDate)) + addSignItem(to: &string, String(request.amount)) + addSignItem(to: &string, request.currency.rawValue) + request.productName.forEach { (name) in + addSignItem(to: &string, name) + } + request.productCount.forEach { (count) in + addSignItem(to: &string, String(count)) + } + request.productPrice.forEach { (price) in + addSignItem(to: &string, String(price)) + } + string.remove(at: string.index(before: string.endIndex)) + + return string + } + + private func addSignItem(to: inout String, _ value: String) { + to.append(value) + to.append(";") + } +} +