diff --git a/Demo/Demo/SwiftUIComponents/VenmoPayments/VenmoPaymentView.swift b/Demo/Demo/SwiftUIComponents/VenmoPayments/VenmoPaymentView.swift index 6db56a6b9..7cef685d1 100644 --- a/Demo/Demo/SwiftUIComponents/VenmoPayments/VenmoPaymentView.swift +++ b/Demo/Demo/SwiftUIComponents/VenmoPayments/VenmoPaymentView.swift @@ -1,11 +1,51 @@ import SwiftUI +import CorePayments struct VenmoPaymentView: View { + @State private var selectedIntent: EligibilityIntent = .capture @StateObject var venmoPaymentsViewModel = VenmoPaymentsViewModel() var body: some View { - Text("Hello, Venmo!") + VStack(spacing: 16) { + Text("Hello, Venmo!") + Picker("Intent", selection: $selectedIntent) { + Text("AUTHORIZE").tag(EligibilityIntent.authorize) + Text("CAPTURE").tag(EligibilityIntent.capture) + } + .pickerStyle(SegmentedPickerStyle()) + ZStack { + Button("Check eligibility") { + Task { + do { + try await venmoPaymentsViewModel.getEligibility(selectedIntent) + } catch { + print("Error in getting payment token. \(error.localizedDescription)") + } + } + } + .buttonStyle(RoundedBlueButtonStyle()) + if case .loading = venmoPaymentsViewModel.state { + CircularProgressView() + } + } + if case .success = venmoPaymentsViewModel.state { + if venmoPaymentsViewModel.isVenmoEligible { + Text("Venmo is eligible! 🥳") + } else { + Text("Venmo is not eligible! 🫤") + } + } + if case .error(let message) = venmoPaymentsViewModel.state { + Text(message) + } + } + .padding() + .background( + RoundedRectangle(cornerRadius: 10) + .stroke(.gray, lineWidth: 2) + .padding(5) + ) } } diff --git a/Demo/Demo/ViewModels/VenmoPaymentsViewModel.swift b/Demo/Demo/ViewModels/VenmoPaymentsViewModel.swift index 07b7617d1..1c7e3b8dc 100644 --- a/Demo/Demo/ViewModels/VenmoPaymentsViewModel.swift +++ b/Demo/Demo/ViewModels/VenmoPaymentsViewModel.swift @@ -1,3 +1,36 @@ +import CorePayments import Foundation -class VenmoPaymentsViewModel: ObservableObject { } +class VenmoPaymentsViewModel: ObservableObject { + + let configManager = CoreConfigManager(domain: "Venmo Payments") + + @Published var state: CurrentState = .idle + + private var eligibilityResult: EligibilityResult? + + var isVenmoEligible: Bool { + eligibilityResult?.isVenmoEligible ?? false + } + + func getEligibility(_ intent: EligibilityIntent) async throws { + DispatchQueue.main.async { + self.state = .loading + } + do { + let config = try await configManager.getCoreConfig() + let eligibilityRequest = EligibilityRequest(currencyCode: "USD", intent: intent) + let eligibilityClient = EligibilityClient(config: config) + eligibilityResult = try? await eligibilityClient.check(eligibilityRequest) + + DispatchQueue.main.async { + self.state = .success + } + } catch { + DispatchQueue.main.async { + self.state = .error(message: error.localizedDescription) + } + print("failed in updating setup token. \(error.localizedDescription)") + } + } +} diff --git a/PayPal.xcodeproj/project.pbxproj b/PayPal.xcodeproj/project.pbxproj index 98da32f28..69a1d1369 100644 --- a/PayPal.xcodeproj/project.pbxproj +++ b/PayPal.xcodeproj/project.pbxproj @@ -35,6 +35,9 @@ 3BE7386D2B9A670400598F05 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3BE738662B9A593100598F05 /* PrivacyInfo.xcprivacy */; }; 3D1763A22720722A00652E1C /* CardResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D1763A12720722A00652E1C /* CardResult.swift */; }; 3DC42BA927187E8300B71645 /* ErrorResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DC42BA827187E8300B71645 /* ErrorResponse.swift */; }; + 459633162C46BD63002008EF /* EligibilityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 459633152C46BD63002008EF /* EligibilityIntent.swift */; }; + 459633182C46BD6F002008EF /* EligibilityResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 459633172C46BD6F002008EF /* EligibilityResponse.swift */; }; + 4596331A2C46BD7B002008EF /* SupportedPaymentMethods.swift in Sources */ = {isa = PBXBuildFile; fileRef = 459633192C46BD7B002008EF /* SupportedPaymentMethods.swift */; }; 45B063B42C4035E200E743F2 /* EligibilityClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B063B22C4035DB00E743F2 /* EligibilityClient.swift */; }; 45B063B52C4035E500E743F2 /* EligibilityResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B063B02C4034B700E743F2 /* EligibilityResult.swift */; }; 45B063B62C4035EA00E743F2 /* EligibilityRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B063AE2C40349300E743F2 /* EligibilityRequest.swift */; }; @@ -42,6 +45,7 @@ 45B063BA2C40456F00E743F2 /* EligibilityAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B063B92C40456F00E743F2 /* EligibilityAPI.swift */; }; 45B063BD2C40545100E743F2 /* EligibilityClient_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B063BC2C40545100E743F2 /* EligibilityClient_Tests.swift */; }; 45B063BF2C40549000E743F2 /* EligibilityAPI_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B063BE2C40549000E743F2 /* EligibilityAPI_Tests.swift */; }; + 45B063CA2C459F9900E743F2 /* MockEligibilityAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45B063C92C459F9900E743F2 /* MockEligibilityAPI.swift */; }; 53A2A4E228A182AC0093441C /* NativeCheckoutProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53A2A4E128A182AC0093441C /* NativeCheckoutProvider.swift */; }; 62D3FB292C3DB5130046563B /* CorePayments.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80B9F85126B8750000D67843 /* CorePayments.framework */; platformFilter = ios; }; 62D3FB4B2C3ED82D0046563B /* VenmoClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62D3FB492C3ED82D0046563B /* VenmoClient.swift */; }; @@ -253,6 +257,9 @@ 3D25238127344F330099E4EB /* NativeCheckoutStartable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NativeCheckoutStartable.swift; path = Sources/PayPalNativePayments/NativeCheckoutStartable.swift; sourceTree = SOURCE_ROOT; }; 3D25238B273979170099E4EB /* MockNativeCheckoutProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNativeCheckoutProvider.swift; sourceTree = ""; }; 3DC42BA827187E8300B71645 /* ErrorResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorResponse.swift; sourceTree = ""; }; + 459633152C46BD63002008EF /* EligibilityIntent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EligibilityIntent.swift; sourceTree = ""; }; + 459633172C46BD6F002008EF /* EligibilityResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EligibilityResponse.swift; sourceTree = ""; }; + 459633192C46BD7B002008EF /* SupportedPaymentMethods.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SupportedPaymentMethods.swift; sourceTree = ""; }; 45B063AE2C40349300E743F2 /* EligibilityRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityRequest.swift; sourceTree = ""; }; 45B063B02C4034B700E743F2 /* EligibilityResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityResult.swift; sourceTree = ""; }; 45B063B22C4035DB00E743F2 /* EligibilityClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityClient.swift; sourceTree = ""; }; @@ -260,6 +267,7 @@ 45B063B92C40456F00E743F2 /* EligibilityAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityAPI.swift; sourceTree = ""; }; 45B063BC2C40545100E743F2 /* EligibilityClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityClient_Tests.swift; sourceTree = ""; }; 45B063BE2C40549000E743F2 /* EligibilityAPI_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EligibilityAPI_Tests.swift; sourceTree = ""; }; + 45B063C92C459F9900E743F2 /* MockEligibilityAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockEligibilityAPI.swift; sourceTree = ""; }; 53A2A4E128A182AC0093441C /* NativeCheckoutProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeCheckoutProvider.swift; sourceTree = ""; }; 62D3FB132C3DB4D40046563B /* VenmoClient_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VenmoClient_Tests.swift; sourceTree = ""; }; 62D3FB2F2C3DB5130046563B /* VenmoPayments.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = VenmoPayments.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -570,31 +578,17 @@ path = Models; sourceTree = ""; }; - 62D3FB122C3DB4B30046563B /* VenmoPaymentsTests */ = { - isa = PBXGroup; - children = ( - 62D3FB132C3DB4D40046563B /* VenmoClient_Tests.swift */, - ); - path = VenmoPaymentsTests; - sourceTree = ""; - }; - 62D3FB4A2C3ED82D0046563B /* VenmoPayments */ = { - isa = PBXGroup; - children = ( - 62D3FB492C3ED82D0046563B /* VenmoClient.swift */, - ); - name = VenmoPayments; - path = Sources/VenmoPayments; - sourceTree = ""; - }; 45B063AD2C40346500E743F2 /* Eligibility */ = { isa = PBXGroup; children = ( 45B063B92C40456F00E743F2 /* EligibilityAPI.swift */, 45B063B22C4035DB00E743F2 /* EligibilityClient.swift */, + 459633152C46BD63002008EF /* EligibilityIntent.swift */, 45B063AE2C40349300E743F2 /* EligibilityRequest.swift */, + 459633172C46BD6F002008EF /* EligibilityResponse.swift */, 45B063B02C4034B700E743F2 /* EligibilityResult.swift */, 45B063B72C40440000E743F2 /* EligibilityVariables.swift */, + 459633192C46BD7B002008EF /* SupportedPaymentMethods.swift */, ); path = Eligibility; sourceTree = ""; @@ -608,6 +602,23 @@ path = Eligibility; sourceTree = ""; }; + 62D3FB122C3DB4B30046563B /* VenmoPaymentsTests */ = { + isa = PBXGroup; + children = ( + 62D3FB132C3DB4D40046563B /* VenmoClient_Tests.swift */, + ); + path = VenmoPaymentsTests; + sourceTree = ""; + }; + 62D3FB4A2C3ED82D0046563B /* VenmoPayments */ = { + isa = PBXGroup; + children = ( + 62D3FB492C3ED82D0046563B /* VenmoClient.swift */, + ); + name = VenmoPayments; + path = Sources/VenmoPayments; + sourceTree = ""; + }; 8036C1DE270F9BCF00C0F091 /* PaymentsCoreTests */ = { isa = PBXGroup; children = ( @@ -650,6 +661,7 @@ isa = PBXGroup; children = ( 802EFBD72A9685DF00AB709D /* MockTrackingEventsAPI.swift */, + 45B063C92C459F9900E743F2 /* MockEligibilityAPI.swift */, ); path = Mocks; sourceTree = ""; @@ -1482,6 +1494,7 @@ files = ( 808EEA81291321FE001B6765 /* AnalyticsEventData_Tests.swift in Sources */, 8036C1E5270F9BE700C0F091 /* Environment_Tests.swift in Sources */, + 45B063CA2C459F9900E743F2 /* MockEligibilityAPI.swift in Sources */, 80B96AAE2A980F6B00C62916 /* MockTrackingEventsAPI.swift in Sources */, 80FC261D29847AC7008EC841 /* HTTP_Tests.swift in Sources */, 45B063BD2C40545100E743F2 /* EligibilityClient_Tests.swift in Sources */, @@ -1498,6 +1511,7 @@ buildActionMask = 2147483647; files = ( E6022E802857C6BE008B0E27 /* GraphQLHTTPResponse.swift in Sources */, + 459633182C46BD6F002008EF /* EligibilityResponse.swift in Sources */, 80E643832A1EBBD2008FD705 /* HTTPResponse.swift in Sources */, 807C5E6929102D9800ECECD8 /* AnalyticsEventData.swift in Sources */, 80E237DF2A84434B00FF18CA /* HTTPRequest.swift in Sources */, @@ -1514,6 +1528,7 @@ 80E2FDC12A83535A0045593D /* TrackingEventsAPI.swift in Sources */, BEA100F226EFA7DE0036A6A5 /* Environment.swift in Sources */, BEA100F026EFA7C20036A6A5 /* NetworkingClientError.swift in Sources */, + 459633162C46BD63002008EF /* EligibilityIntent.swift in Sources */, 804E628629380B04004B9FEF /* AnalyticsService.swift in Sources */, 45B063BA2C40456F00E743F2 /* EligibilityAPI.swift in Sources */, BC04837427B2FC7300FA7B46 /* URLSession+URLSessionProtocol.swift in Sources */, @@ -1524,6 +1539,7 @@ 804E62822937EBCE004B9FEF /* HTTP.swift in Sources */, BC04836F27B2FB3600FA7B46 /* URLSessionProtocol.swift in Sources */, 065A4DBC26FCD8090007014A /* CoreSDKError.swift in Sources */, + 4596331A2C46BD7B002008EF /* SupportedPaymentMethods.swift in Sources */, BEA100E726EF9EDA0036A6A5 /* NetworkingClient.swift in Sources */, 807BF58F2A2A5D19002F32B3 /* HTTPResponseParser.swift in Sources */, 807D56AE2A869064009E591D /* GraphQLRequest.swift in Sources */, diff --git a/Sources/CorePayments/Eligibility/EligibilityAPI.swift b/Sources/CorePayments/Eligibility/EligibilityAPI.swift index ef6460379..b6328b670 100644 --- a/Sources/CorePayments/Eligibility/EligibilityAPI.swift +++ b/Sources/CorePayments/Eligibility/EligibilityAPI.swift @@ -1,16 +1,77 @@ import Foundation -final class EligibilityAPI { +/// API that return merchant's eligibility for different payment methods: Venmo, PayPal, PayPal Credit, Pay Later & credit card +class EligibilityAPI { // MARK: - Private Propertires private let coreConfig: CoreConfig private let networkingClient: NetworkingClient - // MARK: - Initializer + // MARK: - Initializers + /// Initialize the eligibility API to check for payment methods eligibility + /// - Parameter coreConfig: configuration object init(coreConfig: CoreConfig) { self.coreConfig = coreConfig self.networkingClient = NetworkingClient(coreConfig: coreConfig) } + + /// Exposed for injecting MockNetworkingClient in tests + init(coreConfig: CoreConfig, networkingClient: NetworkingClient) { + self.coreConfig = coreConfig + self.networkingClient = networkingClient + } + + // MARK: - Internal Methods + + /// Checks merchants eligibility for different payment methods. + /// - Returns: An `EligibilityResponse` containing the result of the eligibility check. + /// - Throws: An `Error` describing the failure. + + func check(_ eligibilityRequest: EligibilityRequest) async throws -> EligibilityResponse { + let variables = EligibilityVariables(eligibilityRequest: eligibilityRequest, clientID: coreConfig.clientID) + let graphQLRequest = GraphQLRequest(query: Self.rawQuery, variables: variables) + let httpResponse = try await networkingClient.fetch(request: graphQLRequest) + + return try HTTPResponseParser().parseGraphQL(httpResponse, as: EligibilityResponse.self) + } +} + +extension EligibilityAPI { + + static let rawQuery = """ + query getEligibility( + $clientId: String!, + $intent: FundingEligibilityIntent!, + $currency: SupportedCountryCurrencies!, + $enableFunding: [SupportedPaymentMethodsType] + ){ + fundingEligibility( + clientId: $clientId, + intent: $intent + currency: $currency, + enableFunding: $enableFunding){ + venmo{ + eligible + reasons + } + card{ + eligible + } + paypal{ + eligible + reasons + } + paylater{ + eligible + reasons + } + credit{ + eligible + reasons + } + } + } + """ } diff --git a/Sources/CorePayments/Eligibility/EligibilityClient.swift b/Sources/CorePayments/Eligibility/EligibilityClient.swift index bf0fb2055..4666723b3 100644 --- a/Sources/CorePayments/Eligibility/EligibilityClient.swift +++ b/Sources/CorePayments/Eligibility/EligibilityClient.swift @@ -1,9 +1,36 @@ import Foundation +/// The `EligibilityClient` class provides methods to check eligibility status based on provided requests. public final class EligibilityClient { + private let api: EligibilityAPI + private let config: CoreConfig + + // MARK: - Initializers + + /// Initializes a new instance of `EligibilityClient`. + /// - Parameter config: The core configuration needed for the client. + public init(config: CoreConfig) { + self.config = config + self.api = EligibilityAPI(coreConfig: config) + } + + /// Exposed for injecting MockEligibilityAPI in tests + init(config: CoreConfig, api: EligibilityAPI) { + self.config = config + self.api = api + } + + // MARK: - Public Methods + + /// Checks the eligibility based on the provided `EligibilityRequest`. + /// + /// This method calls the `EligibilityAPI` to perform the check and converts the response to `EligibilityResult`. + /// + /// - Parameter eligibilityRequest: The eligibility request containing the necessary data to perform the check. + /// - Throws: An error if the network request or parsing fails. + /// - Returns: An `EligibilityResult` containing the result of the eligibility check. public func check(_ eligibilityRequest: EligibilityRequest) async throws -> EligibilityResult { - // TODO: - Add logic - .init(isVenmoEligible: false) + try await api.check(eligibilityRequest).asResult } } diff --git a/Sources/CorePayments/Eligibility/EligibilityIntent.swift b/Sources/CorePayments/Eligibility/EligibilityIntent.swift new file mode 100644 index 000000000..e492dbc48 --- /dev/null +++ b/Sources/CorePayments/Eligibility/EligibilityIntent.swift @@ -0,0 +1,11 @@ +import Foundation + +/// Enum representing the possible intents for eligibility. +public enum EligibilityIntent: String { + + /// Represents the intent to capture a payment. + case capture = "CAPTURE" + + /// Represents the intent to authorize a payment. + case authorize = "AUTHORIZE" +} diff --git a/Sources/CorePayments/Eligibility/EligibilityRequest.swift b/Sources/CorePayments/Eligibility/EligibilityRequest.swift index fa51f04a6..7e78497e3 100644 --- a/Sources/CorePayments/Eligibility/EligibilityRequest.swift +++ b/Sources/CorePayments/Eligibility/EligibilityRequest.swift @@ -1,16 +1,28 @@ import Foundation +/// The `EligibilityRequest` structure includes the necessary parameters to make an eligibility check request. public struct EligibilityRequest { - let intent: String - let currency: String - let enableFunding: [String] + // MARK: - Internal Properties + + /// The currency code for the eligibility request. + let currencyCode: String + + /// The intent of the eligibility request. + let intent: EligibilityIntent + + /// An array of supported payment methods for the request. Defaults to `[.VENMO]`. + let enableFunding: [SupportedPaymentMethodsType] // MARK: - Initializer - public init(intent: String, currency: String, enableFunding: [String]) { + /// Creates an instance of a eligibility request + /// - Parameters: + /// - currencyCode: The currency code for the eligibility request. + /// - intent: The intent of the eligibility request. + public init(currencyCode: String, intent: EligibilityIntent) { + self.currencyCode = currencyCode self.intent = intent - self.currency = currency - self.enableFunding = enableFunding + self.enableFunding = [.venmo] } } diff --git a/Sources/CorePayments/Eligibility/EligibilityResponse.swift b/Sources/CorePayments/Eligibility/EligibilityResponse.swift new file mode 100644 index 000000000..a4c035b00 --- /dev/null +++ b/Sources/CorePayments/Eligibility/EligibilityResponse.swift @@ -0,0 +1,60 @@ +import Foundation + +struct EligibilityResponse: Decodable { + + let fundingEligibility: FundingEligibility + + var isVenmoEligible: Bool { + fundingEligibility.venmo.eligible + } + + var isCardEligible: Bool { + fundingEligibility.card.eligible + } + + var isPayPalEligible: Bool { + fundingEligibility.payPal.eligible + } + + var isPayLaterEligible: Bool { + fundingEligibility.payLater.eligible + } + + var isCreditEligible: Bool { + fundingEligibility.credit.eligible + } + + var asResult: EligibilityResult { + EligibilityResult( + isVenmoEligible: isVenmoEligible, + isCardEligible: isCardEligible, + isPayPalEligible: isPayPalEligible, + isPayLaterEligible: isPayLaterEligible, + isCreditEligible: isCreditEligible + ) + } +} + +struct FundingEligibility: Decodable { + + let venmo: SupportedPaymentMethodsTypeEligibility + let card: SupportedPaymentMethodsTypeEligibility + let payPal: SupportedPaymentMethodsTypeEligibility + let payLater: SupportedPaymentMethodsTypeEligibility + let credit: SupportedPaymentMethodsTypeEligibility + + // MARK: - Coding Keys + + private enum CodingKeys: String, CodingKey { + case venmo + case card + case payPal = "paypal" + case payLater = "paylater" + case credit + } +} + +struct SupportedPaymentMethodsTypeEligibility: Decodable { + + let eligible: Bool +} diff --git a/Sources/CorePayments/Eligibility/EligibilityResult.swift b/Sources/CorePayments/Eligibility/EligibilityResult.swift index e71109482..da20a7fd4 100644 --- a/Sources/CorePayments/Eligibility/EligibilityResult.swift +++ b/Sources/CorePayments/Eligibility/EligibilityResult.swift @@ -1,6 +1,20 @@ import Foundation +/// The `EligibilityResult` structure contains the eligibility status for payment methods. public struct EligibilityResult { - + + /// A boolean indicating if venmo is eligible. public let isVenmoEligible: Bool + + /// A boolean indicating if Card is eligible. + public let isCardEligible: Bool + + /// A boolean indicating if PayPal is eligible. + public let isPayPalEligible: Bool + + /// A boolean indicating if PayLater is eligible. + public let isPayLaterEligible: Bool + + /// A boolean indicating if credit is eligible. + public let isCreditEligible: Bool } diff --git a/Sources/CorePayments/Eligibility/EligibilityVariables.swift b/Sources/CorePayments/Eligibility/EligibilityVariables.swift index 8ab85393d..8211352e8 100644 --- a/Sources/CorePayments/Eligibility/EligibilityVariables.swift +++ b/Sources/CorePayments/Eligibility/EligibilityVariables.swift @@ -1,14 +1,28 @@ import Foundation -struct EligibilityVariables { +struct EligibilityVariables: Encodable { - private let eligibilityRequest: EligibilityRequest - private let clientID: String - - // MARK: - Initializer + let eligibilityRequest: EligibilityRequest + let clientID: String - init(eligibilityRequest: EligibilityRequest, clientID: String) { - self.eligibilityRequest = eligibilityRequest - self.clientID = clientID + // MARK: - Coding Keys + + private enum CodingKeys: String, CodingKey { + case clientID = "clientId" + case intent + case currency + case enableFunding + } + + // MARK: - Custom Encoder + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(clientID, forKey: .clientID) + try container.encode(eligibilityRequest.intent.rawValue, forKey: .intent) + try container.encode(eligibilityRequest.currencyCode, forKey: .currency) + + let enableFunding = eligibilityRequest.enableFunding.compactMap { $0.rawValue } + try container.encode(enableFunding, forKey: .enableFunding) } } diff --git a/Sources/CorePayments/Eligibility/SupportedPaymentMethods.swift b/Sources/CorePayments/Eligibility/SupportedPaymentMethods.swift new file mode 100644 index 000000000..e8a00347c --- /dev/null +++ b/Sources/CorePayments/Eligibility/SupportedPaymentMethods.swift @@ -0,0 +1,7 @@ +import Foundation + +enum SupportedPaymentMethodsType: String { + case venmo = "VENMO" + case credit = "CREDIT" + case payLater = "PAYLATER" +} diff --git a/UnitTests/PaymentsCoreTests/Eligibility/EligibilityAPI_Tests.swift b/UnitTests/PaymentsCoreTests/Eligibility/EligibilityAPI_Tests.swift index 6e554dce6..99cb62d43 100644 --- a/UnitTests/PaymentsCoreTests/Eligibility/EligibilityAPI_Tests.swift +++ b/UnitTests/PaymentsCoreTests/Eligibility/EligibilityAPI_Tests.swift @@ -1,7 +1,131 @@ import XCTest @testable import CorePayments +@testable import TestShared class EligibilityAPI_Tests: XCTestCase { - // TODO: to be implemented in a future PR + let mockClientID = "mockClientId" + let mockURLSession = MockURLSession() + let eligibilityRequest = EligibilityRequest(currencyCode: "SOME-CURRENCY", intent: .capture) + + var sut: EligibilityAPI! + var coreConfig: CoreConfig! + var mockNetworkingClient: MockNetworkingClient! + + override func setUp() { + super.setUp() + coreConfig = CoreConfig(clientID: mockClientID, environment: .sandbox) + let mockHTTP = MockHTTP(coreConfig: coreConfig) + mockNetworkingClient = MockNetworkingClient(http: mockHTTP) + sut = EligibilityAPI(coreConfig: coreConfig, networkingClient: mockNetworkingClient) + } + + override func tearDown() { + coreConfig = nil + mockNetworkingClient = nil + sut = nil + super.tearDown() + } + + func testCheck_constructsGraphQLRequest() async throws { + let rawQuery = """ + query getEligibility( + $clientId: String!, + $intent: FundingEligibilityIntent!, + $currency: SupportedCountryCurrencies!, + $enableFunding: [SupportedPaymentMethodsType] + ){ + fundingEligibility( + clientId: $clientId, + intent: $intent + currency: $currency, + enableFunding: $enableFunding){ + venmo{ + eligible + reasons + } + card{ + eligible + } + paypal{ + eligible + reasons + } + paylater{ + eligible + reasons + } + credit{ + eligible + reasons + } + } + } + """ + + _ = try? await sut.check(eligibilityRequest) + + XCTAssertEqual(mockNetworkingClient.capturedGraphQLRequest?.query, rawQuery) + XCTAssertNil(mockNetworkingClient.capturedGraphQLRequest?.queryNameForURL) + + let variables = mockNetworkingClient.capturedGraphQLRequest?.variables as! EligibilityVariables + XCTAssertEqual(variables.clientID, mockClientID) + XCTAssertEqual(variables.eligibilityRequest.currencyCode, "SOME-CURRENCY") + XCTAssertEqual(variables.eligibilityRequest.intent.rawValue, "CAPTURE") + } + + func testCheck_whenNetworkingClientError_returnsError() async { + mockNetworkingClient.stubHTTPError = CoreSDKError(code: 123, domain: "api-client-error", errorDescription: "error-desc") + + do { + _ = try await sut.check(eligibilityRequest) + XCTFail("Expected error throw.") + } catch { + let error = error as! CoreSDKError + XCTAssertEqual(error.domain, "api-client-error") + XCTAssertEqual(error.code, 123) + XCTAssertEqual(error.localizedDescription, "error-desc") + } + } + + func testCheck_whenSuccess_returnsParsedResponse() async throws { + let successsResponseJSON = """ + { + "data": { + "fundingEligibility": { + "venmo": { + "eligible": true, + "reasons": [] + }, + "card": { + "eligible": true + }, + "paypal": { + "eligible": true, + "reasons": [] + }, + "paylater": { + "eligible": true, + "reasons": [] + }, + "credit": { + "eligible": false, + "reasons": ["INELIGIBLE DUE TO PAYLATER ELIGIBLE"] + } + } + } + } + """ + + let data = successsResponseJSON.data(using: .utf8) + let stubbedHTTPResponse = HTTPResponse(status: 200, body: data) + mockNetworkingClient.stubHTTPResponse = stubbedHTTPResponse + + let response = try await sut.check(eligibilityRequest) + XCTAssertTrue(response.isVenmoEligible) + XCTAssertTrue(response.isCardEligible) + XCTAssertTrue(response.isPayPalEligible) + XCTAssertTrue(response.isPayLaterEligible) + XCTAssertFalse(response.isCreditEligible) + } } diff --git a/UnitTests/PaymentsCoreTests/Eligibility/EligibilityClient_Tests.swift b/UnitTests/PaymentsCoreTests/Eligibility/EligibilityClient_Tests.swift index 2f6aa9c5b..f0039ebb9 100644 --- a/UnitTests/PaymentsCoreTests/Eligibility/EligibilityClient_Tests.swift +++ b/UnitTests/PaymentsCoreTests/Eligibility/EligibilityClient_Tests.swift @@ -3,5 +3,65 @@ import XCTest class EligibilityClient_Tests: XCTestCase { - // TODO: to be implemented in a future PR + let mockClientID = "mockClientId" + let eligibilityRequest = EligibilityRequest(currencyCode: "SOME-CURRENCY", intent: .capture) + + var sut: EligibilityClient! + var coreConfig: CoreConfig! + var mockEligibilityAPI: MockEligibilityAPI! + + override func setUp() { + super.setUp() + coreConfig = CoreConfig(clientID: mockClientID, environment: .sandbox) + mockEligibilityAPI = MockEligibilityAPI(coreConfig: coreConfig) + sut = EligibilityClient(config: coreConfig, api: mockEligibilityAPI) + } + + override func tearDown() { + coreConfig = nil + mockEligibilityAPI = nil + sut = nil + super.tearDown() + } + + func testCheck_returnsSuccessResult() async throws { + mockEligibilityAPI.stubResponse = .init( + fundingEligibility: .init( + venmo: .init(eligible: true), + card: .init(eligible: true), + payPal: .init(eligible: true), + payLater: .init(eligible: false), + credit: .init(eligible: false) + ) + ) + + do { + let result = try await sut.check(eligibilityRequest) + XCTAssertTrue(result.isVenmoEligible) + XCTAssertTrue(result.isCardEligible) + XCTAssertTrue(result.isPayPalEligible) + XCTAssertFalse(result.isPayLaterEligible) + XCTAssertFalse(result.isCreditEligible) + } catch { + XCTFail("Expected no error.") + } + } + + func testCheck_returnsError() async throws { + mockEligibilityAPI.stubError = CoreSDKError( + code: 123, + domain: "client-error", + errorDescription: "Some client error description." + ) + + do { + _ = try await sut.check(eligibilityRequest) + XCTFail("Expected error throw.") + } catch { + let error = error as! CoreSDKError + XCTAssertEqual(error.domain, "client-error") + XCTAssertEqual(error.code, 123) + XCTAssertEqual(error.localizedDescription, "Some client error description.") + } + } } diff --git a/UnitTests/PaymentsCoreTests/Mocks/MockEligibilityAPI.swift b/UnitTests/PaymentsCoreTests/Mocks/MockEligibilityAPI.swift new file mode 100644 index 000000000..d788a7b62 --- /dev/null +++ b/UnitTests/PaymentsCoreTests/Mocks/MockEligibilityAPI.swift @@ -0,0 +1,24 @@ +import Foundation +@testable import CorePayments + +class MockEligibilityAPI: EligibilityAPI { + + var stubResponse: EligibilityResponse? + var stubError: Error? + + var capturedRequest: EligibilityRequest? + + override func check(_ eligibilityRequest: EligibilityRequest) async throws -> EligibilityResponse { + capturedRequest = eligibilityRequest + + if let stubError { + throw stubError + } + + if let stubResponse { + return stubResponse + } + + throw CoreSDKError(code: 0, domain: "", errorDescription: "Stubbed responses not implemented for this mock.") + } +}