pscbonline-ios/PSCBOnline/Sources/PSCBOnlineClient.swift

307 lines
10 KiB
Swift
Raw Normal View History

2024-07-08 15:20:00 +03:00
//
// 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)
}
}