307 lines
10 KiB
Swift
307 lines
10 KiB
Swift
|
//
|
||
|
// OOSAPIClient.swift
|
||
|
// PSCB-OOS-iOS
|
||
|
//
|
||
|
// Created by Antonov Ilia on 12.10.2020.
|
||
|
//
|
||
|
|
||
|
import Foundation
|
||
|
import PassKit
|
||
|
|
||
|
// MARK: - Environments
|
||
|
|
||
|
public enum BackendEnvironment: String {
|
||
|
|
||
|
case production = "oos.pscb.ru"
|
||
|
case sandbox = "oosdemo.pscb.ru"
|
||
|
|
||
|
}
|
||
|
|
||
|
// MARK: - Request status
|
||
|
|
||
|
/// Backend request status
|
||
|
/// Consists of two possible states: `.success` and `.failure`.
|
||
|
///
|
||
|
/// This one represents if request to the backend server was successful
|
||
|
public enum RequestStatus {
|
||
|
case success, failure
|
||
|
}
|
||
|
|
||
|
// MARK: - Response wrapper
|
||
|
|
||
|
/// Backend response
|
||
|
public struct BackendResponse {
|
||
|
|
||
|
/// Represents request status
|
||
|
public let status: RequestStatus
|
||
|
|
||
|
/// Hold error information if request is a `.failure`
|
||
|
public let error: Error?
|
||
|
|
||
|
/// Holds response information if request is a `.success`
|
||
|
public let response: Response?
|
||
|
}
|
||
|
|
||
|
// MARK: - Alias for completion handler
|
||
|
|
||
|
/// Callback for `OOSAPIClient.send` method
|
||
|
public typealias APICompletionHandler = (BackendResponse) -> Void
|
||
|
|
||
|
/// Callback for another `OOSAPIClient.send` method which fires after certain guards ensure success or failure
|
||
|
/// to simplify API usage
|
||
|
public typealias PostResponseHandler = (Error?, Response?) -> Void
|
||
|
|
||
|
// MARK: - API Integration errors
|
||
|
|
||
|
/// Possible erros when dealing with OOS backend
|
||
|
public enum OOSErrors: Error {
|
||
|
case noData
|
||
|
case parse(Error)
|
||
|
case cause(Error)
|
||
|
|
||
|
/// Consists of error code and error description
|
||
|
case backend(String, String)
|
||
|
}
|
||
|
|
||
|
// MARK: - API Client
|
||
|
|
||
|
/// Implementation of OOS Merchant API HTTP protocol.
|
||
|
final public class PSCBOnlineClient {
|
||
|
|
||
|
private lazy var urlSession: URLSession = {
|
||
|
URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
|
||
|
}()
|
||
|
|
||
|
private lazy var url: URL = {
|
||
|
var components = URLComponents()
|
||
|
components.scheme = "https"
|
||
|
components.host = self.environment.rawValue
|
||
|
components.path = "/merchantApi/payShpa"
|
||
|
|
||
|
return components.url!
|
||
|
}()
|
||
|
|
||
|
private let decoder = JSONDecoders.iso8601DateAwareDecoder()
|
||
|
|
||
|
// init:
|
||
|
|
||
|
let environment: BackendEnvironment
|
||
|
let marketPlaceId: String
|
||
|
let signingKey: String
|
||
|
|
||
|
/// Initializes `OOSAPIClient`
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - environment: Backend environment. Which one to use. For testing environment use `.sandbox`
|
||
|
/// - marketPlaceId: Your OOS market-place ID
|
||
|
/// - siginingKey: Your OOS signing key
|
||
|
public init(environment: BackendEnvironment, marketPlaceId: String, signingKey: String) {
|
||
|
self.environment = environment
|
||
|
self.marketPlaceId = marketPlaceId
|
||
|
self.signingKey = signingKey
|
||
|
}
|
||
|
|
||
|
// impl:
|
||
|
|
||
|
/// Creates instance of `RequestWrapper` for a backend to process from given PKPayment instance and other details
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - payment: authorized PKPayment instance
|
||
|
/// - amount: total order amount
|
||
|
/// - orderId: Unique merchant order ID
|
||
|
///
|
||
|
/// - Returns: RequestWrapper instance for backend
|
||
|
public func makeRequestWithPayment(payment: PKPayment, amount: Decimal, orderId: String) throws -> RequestWrapper {
|
||
|
return try makeRequestWithPayment(pkPayment: payment, payment: Payment(amount: amount, orderId: orderId))
|
||
|
}
|
||
|
|
||
|
/// Creates instance of `RequestWrapper` for a backend to process from given `PKPayment` instance and backend `Payment` instance
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - pkPayment: authorized `PKPayment` instance
|
||
|
/// - payment: backend `Payment` details
|
||
|
///
|
||
|
/// - Returns: RequestWrapper instance for backend
|
||
|
public func makeRequestWithPayment(pkPayment: PKPayment, payment: Payment) throws -> RequestWrapper {
|
||
|
// Get card data cryptogram from payment token
|
||
|
let cardDataCrypto = try pkPayment.token.toCryptogramString()
|
||
|
|
||
|
// Backend request wrapper
|
||
|
let requestWrapper = RequestWrapper(
|
||
|
marketPlaceId: self.marketPlaceId,
|
||
|
payment: payment,
|
||
|
cardData: cardDataCrypto
|
||
|
)
|
||
|
|
||
|
// Fail early
|
||
|
try requestWrapper.assumeSerializes()
|
||
|
|
||
|
return requestWrapper
|
||
|
}
|
||
|
|
||
|
/// Creates instance of `RequestWrapper` for a backend to process from raw card data instead of ApplePay token
|
||
|
///
|
||
|
/// Example:
|
||
|
/// ```
|
||
|
/// let card = CardData(
|
||
|
/// pan: "409444400001234",
|
||
|
/// expiryYear: 2025,
|
||
|
/// expiryMonth: 12,
|
||
|
/// cvCode: "000",
|
||
|
/// cardholder: "JOHN DOE"
|
||
|
/// )
|
||
|
///
|
||
|
/// let payment = Payment(amount: Decimal(1500), orderId: "XC-12345")
|
||
|
/// let request = try apiClient.createRequestWithCardData(card: card, payment: payment)
|
||
|
///
|
||
|
/// // Send to backend:
|
||
|
/// apiClient.send(request) { (response) in /code/ }
|
||
|
/// ```
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - cardData: an instance of card data.
|
||
|
public func makeRequestWithCardData(card: CardData, payment: Payment) throws -> RequestWrapper {
|
||
|
let cryptogram = try card.toCryptgramString()
|
||
|
let request = RequestWrapper(
|
||
|
marketPlaceId: self.marketPlaceId,
|
||
|
payment: payment,
|
||
|
cardData: cryptogram
|
||
|
)
|
||
|
|
||
|
// Fail-early compile
|
||
|
try request.assumeSerializes()
|
||
|
|
||
|
return request
|
||
|
}
|
||
|
|
||
|
/// Signs and sends compiled `RequestWrapper` to the backend server
|
||
|
/// Fires `APICompletionHandler` once requests succeeds or fails
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - request: `RequestWrapper` created from `createRequestWithPayment(...)`
|
||
|
/// - completionHander: `APICompletionHandler` a callback for when requests succeeds or fails
|
||
|
///
|
||
|
public func send(_ request: RequestWrapper, completionHandler: @escaping APICompletionHandler) {
|
||
|
// Calculate signature and get HTTP body at the same time
|
||
|
let httpBody = try! request.serializeToString()
|
||
|
let signature = calculateSignature(httpBody)
|
||
|
|
||
|
// Request params
|
||
|
let url = self.url
|
||
|
var req = URLRequest(url: url)
|
||
|
|
||
|
// Set request parameters
|
||
|
req.httpMethod = "POST"
|
||
|
req.httpBody = httpBody.data(using: .utf8)
|
||
|
req.setValue(signature, forHTTPHeaderField: "Signature")
|
||
|
|
||
|
print(">> Request JSON: \(String(describing: String(data: req.httpBody!, encoding: .utf8)))")
|
||
|
|
||
|
let task = urlSession.dataTask(with: req) { (data, res, err) in
|
||
|
guard err == nil else {
|
||
|
let error = self.err_requestError(req, error: err)
|
||
|
completionHandler(error)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
guard let data = data else {
|
||
|
let error = self.failure(req, error: OOSErrors.noData)
|
||
|
completionHandler(error)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
print("<< Response data: \(String(data: data, encoding: .utf8) ?? "N/A")")
|
||
|
|
||
|
do {
|
||
|
let response = try self.decoder.decode(Response.self, from: data)
|
||
|
|
||
|
// Finaly success
|
||
|
let success = BackendResponse(status: .success, error: nil, response: response)
|
||
|
|
||
|
completionHandler(success)
|
||
|
} catch let error {
|
||
|
print("Failed to do task on request \(req)")
|
||
|
print(error.localizedDescription)
|
||
|
|
||
|
// Reading and parsing errors
|
||
|
let failure = self.failure(req, error: .parse(error))
|
||
|
completionHandler(failure)
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
// Fire task
|
||
|
task.resume()
|
||
|
}
|
||
|
|
||
|
/// Signs and sends compiled `RequestWrapper` to the backend server
|
||
|
/// Fires `PostResponseHandler` once requests succeeds or fails.
|
||
|
/// `PostResponseHandler` accepts two arguments: `Error?` and `Response?`.
|
||
|
///
|
||
|
/// If requests succeeds and payment in desired state `Error?` will always be `nil`.
|
||
|
/// `Response?` presents on some errors and on all successes.
|
||
|
///
|
||
|
/// This is an utility method to reduce boilerplate for necessary checks.
|
||
|
///
|
||
|
/// - Parameters:
|
||
|
/// - request: `RequestWrapper` created from `createRequestWithPayment(...)`
|
||
|
/// - completionHander: `APICompletionHandler` a callback for when requests succeeds or fails
|
||
|
///
|
||
|
public func send(_ request: RequestWrapper, responseHandler: @escaping PostResponseHandler) {
|
||
|
// Construct a closure wrapper with predefined guards and checks
|
||
|
let wrapper: APICompletionHandler = { (backendResponse) in
|
||
|
// HTTP request succeeded?
|
||
|
guard backendResponse.status == .success else {
|
||
|
print("Failed to execute request due to error: \(String(describing: backendResponse.error))")
|
||
|
|
||
|
responseHandler(OOSErrors.cause(backendResponse.error!), nil)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Backend returned valid response?
|
||
|
guard let response = backendResponse.response else {
|
||
|
print("Backend returned empty body")
|
||
|
|
||
|
responseHandler(OOSErrors.noData, nil)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Backend payment succeeded?
|
||
|
guard response.status == .success else {
|
||
|
print("Unable to process payment")
|
||
|
|
||
|
let responseError = response.error
|
||
|
let backendError = OOSErrors.backend(responseError?.code ?? "FAILED", responseError?.description ?? "")
|
||
|
|
||
|
// At this point we can pass response too
|
||
|
responseHandler(backendError, response)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Finally succeeds
|
||
|
responseHandler(nil, response)
|
||
|
}
|
||
|
|
||
|
// Send using more lower-level method above
|
||
|
send(request, completionHandler: wrapper)
|
||
|
}
|
||
|
|
||
|
// MARK: - Private
|
||
|
|
||
|
private func err_requestError(_ req: URLRequest, error: Error?) -> BackendResponse {
|
||
|
print("Failed to do task on request \(req) with error: \(String(describing: error))")
|
||
|
return BackendResponse(status: .failure, error: OOSErrors.cause(error!), response: nil)
|
||
|
}
|
||
|
|
||
|
private func failure(_ req: URLRequest, error: OOSErrors) -> BackendResponse {
|
||
|
print("Request \(req) failed with error: \(String(describing: error))")
|
||
|
return BackendResponse(status: .failure, error: error, response: nil)
|
||
|
}
|
||
|
|
||
|
private func calculateSignature(_ jsonString: String) -> String {
|
||
|
let composite = (jsonString + self.signingKey)
|
||
|
return DigestHelper.sha256String(composite)
|
||
|
}
|
||
|
|
||
|
}
|