From 9f98d026dca799c59b5b9757aa35e06da8f6e3d7 Mon Sep 17 00:00:00 2001 From: Nicolas Bachschmidt Date: Thu, 9 Aug 2018 18:52:26 +0200 Subject: [PATCH] Use enum type for payment sources Payment sources are not limited to Sources but may also include Cards and Bank Accounts. The `StripePaymentSource` type is an enum that wraps a `StripeBankAccount`, a `StripeCard`, or a `StripeSource`. --- .../Stripe/Models/Sources/PaymentSource.swift | 98 ++++++++++++ .../Stripe/Models/Sources/SourceList.swift | 17 +- Tests/LinuxMain.swift | 9 +- Tests/StripeTests/PaymentSourceTests.swift | 145 ++++++++++++++++++ 4 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 Sources/Stripe/Models/Sources/PaymentSource.swift create mode 100644 Tests/StripeTests/PaymentSourceTests.swift diff --git a/Sources/Stripe/Models/Sources/PaymentSource.swift b/Sources/Stripe/Models/Sources/PaymentSource.swift new file mode 100644 index 0000000..3c3d97e --- /dev/null +++ b/Sources/Stripe/Models/Sources/PaymentSource.swift @@ -0,0 +1,98 @@ +// +// PaymentSource.swift +// Stripe +// +// Created by Nicolas Bachschmidt on 2018-08-09. +// + +/** + Payment Source objects + https://stripe.com/docs/api#customer_bank_account_object + https://stripe.com/docs/api#card_object + https://stripe.com/docs/api#source_object + */ + +public enum StripePaymentSource: StripeModel { + case bankAccount(StripeBankAccount) + case card(StripeCard) + case source(StripeSource) + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let object = try container.decode(String.self, forKey: .object) + switch object { + case "bank_account": + self = try .bankAccount(StripeBankAccount(from: decoder)) + case "card": + self = try .card(StripeCard(from: decoder)) + case "source": + self = try .source(StripeSource(from: decoder)) + default: + throw DecodingError.dataCorruptedError( + forKey: CodingKeys.object, + in: container, + debugDescription: "Unknown payment source \"\(object)\"" + ) + } + } + + public func encode(to encoder: Encoder) throws { + switch self { + case let .bankAccount(bankAccount): + try bankAccount.encode(to: encoder) + case let .card(card): + try card.encode(to: encoder) + case let .source(source): + try source.encode(to: encoder) + } + } + + public enum CodingKeys: String, CodingKey { + case object + } +} + +extension StripePaymentSource { + public var bankAccount: StripeBankAccount? { + guard case let .bankAccount(bankAccount) = self else { + return nil + } + return bankAccount + } + + public var card: StripeCard? { + guard case let .card(card) = self else { + return nil + } + return card + } + + public var source: StripeSource? { + guard case let .source(source) = self else { + return nil + } + return source + } + + public var id: String { + switch self { + case let .bankAccount(bankAccount): + return bankAccount.id + case let .card(card): + return card.id + case let .source(source): + return source.id + } + } + + public var object: String { + switch self { + case let .bankAccount(bankAccount): + return bankAccount.object + case let .card(card): + return card.object + case let .source(source): + return source.object + } + } +} diff --git a/Sources/Stripe/Models/Sources/SourceList.swift b/Sources/Stripe/Models/Sources/SourceList.swift index 8dbfe9b..22686d5 100644 --- a/Sources/Stripe/Models/Sources/SourceList.swift +++ b/Sources/Stripe/Models/Sources/SourceList.swift @@ -16,7 +16,7 @@ public struct StripeSourcesList: StripeModel { public var hasMore: Bool public var totalCount: Int public var url: String - public var data: [StripeSource] + public var data: [StripePaymentSource] public enum CodingKeys: String, CodingKey { case object @@ -26,3 +26,18 @@ public struct StripeSourcesList: StripeModel { case data } } + +extension StripeSourcesList { + + public var bankAccounts: [StripeBankAccount] { + return data.compactMap { $0.bankAccount } + } + + public var cards: [StripeCard] { + return data.compactMap { $0.card } + } + + public var sources: [StripeSource] { + return data.compactMap { $0.source } + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift index 38fbf35..fd89a71 100644 --- a/Tests/LinuxMain.swift +++ b/Tests/LinuxMain.swift @@ -1,4 +1,4 @@ -// Generated using Sourcery 0.7.2 — https://github.com/krzysztofzablocki/Sourcery +// Generated using Sourcery 0.13.1 — https://github.com/krzysztofzablocki/Sourcery // DO NOT EDIT import XCTest @@ -60,6 +60,12 @@ static var allTests = [ ] } +extension PaymentSourceTests { +static var allTests = [ + ("testSourceListIsProperlyParsed", testSourceListIsProperlyParsed), +] +} + extension ProductTests { static var allTests = [ ("testProductParsedProperly", testProductParsedProperly), @@ -123,6 +129,7 @@ XCTMain([ testCase(ErrorTests.allTests), testCase(InvoiceTests.allTests), testCase(OrderTests.allTests), + testCase(PaymentSourceTests.allTests), testCase(ProductTests.allTests), testCase(RefundTests.allTests), testCase(SKUTests.allTests), diff --git a/Tests/StripeTests/PaymentSourceTests.swift b/Tests/StripeTests/PaymentSourceTests.swift new file mode 100644 index 0000000..4d53f6c --- /dev/null +++ b/Tests/StripeTests/PaymentSourceTests.swift @@ -0,0 +1,145 @@ +// +// PaymentSourceTests.swift +// Stripe +// +// Created by Nicolas Bachschmidt on 2018-08-09. +// + +import XCTest +@testable import Stripe +@testable import Vapor + +class PaymentSourceTests: XCTestCase { + let sourceListString = """ +{ + "object": "list", + "total_count": 3, + "data": [ + { + "id": "card_1Cs8z3GaLcnLeFWiU9SDCez8", + "object": "card", + "address_city": null, + "address_country": null, + "address_line1": null, + "address_line1_check": null, + "address_line2": null, + "address_state": null, + "address_zip": null, + "address_zip_check": null, + "brand": "Visa", + "country": "FR", + "customer": "cus_D3t6eeIn7f2nYi", + "cvc_check": "pass", + "dynamic_last4": null, + "exp_month": 12, + "exp_year": 2029, + "fingerprint": "GhnGuMVuycvktkHE", + "funding": "credit", + "last4": "0003", + "metadata": { + }, + "name": null, + "tokenization_method": null, + "type": "Visa" + }, + { + "id": "src_1CxF9xGaLcnLeFWif1vfiSkS", + "object": "source", + "ach_credit_transfer": { + "account_number": "test_49b8d100bde6", + "bank_name": "TEST BANK", + "fingerprint": "0W7zV79lnzu4L4Ie", + "routing_number": "110000000", + "swift_code": "TSTEZ122" + }, + "amount": null, + "client_secret": "src_client_secret_DO1CKYXtHAFzKMNPY4Fsi7d9", + "created": 1533825041, + "currency": "usd", + "flow": "receiver", + "livemode": false, + "metadata": { + }, + "owner": { + "address": null, + "email": "jenny.rosen@example.com", + "name": null, + "phone": null, + "verified_address": null, + "verified_email": null, + "verified_name": null, + "verified_phone": null + }, + "receiver": { + "address": "110000000-test_49b8d100bde6", + "amount_charged": 0, + "amount_received": 1000, + "amount_returned": 0, + "refund_attributes_method": "email", + "refund_attributes_status": "missing" + }, + "statement_descriptor": null, + "status": "chargeable", + "type": "ach_credit_transfer", + "usage": "reusable" + }, + { + "id": "ba_1CxEzUGaLcnLeFWiz0fJrOVm", + "object": "bank_account", + "account_holder_name": "", + "account_holder_type": "individual", + "bank_name": "STRIPE TEST BANK", + "country": "US", + "currency": "usd", + "customer": "cus_D3t6eeIn7f2nYi", + "disabled": false, + "fingerprint": "xrXW6SzxS6Gjr4d7", + "last4": "6789", + "metadata": { + }, + "name": "", + "routing_number": "110000000", + "status": "new", + "validated": false, + "verified": false + } + ], + "has_more": false, + "url": "/v1/customers/cus_D3t6eeIn7f2nYi/sources" +} +""" + + func testSourceListIsProperlyParsed() throws { + do { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .secondsSince1970 + + let body = HTTPBody(string: sourceListString) + var headers: HTTPHeaders = [:] + headers.replaceOrAdd(name: .contentType, value: MediaType.json.description) + let request = HTTPRequest(headers: headers, body: body) + let futureOrder = try decoder.decode(StripeSourcesList.self, from: request, maxSize: 65_536, on: EmbeddedEventLoop()) + + futureOrder.do { list in + XCTAssertEqual(list.object, "list") + XCTAssertEqual(list.hasMore, false) + XCTAssertEqual(list.data.count, 3) + XCTAssertEqual(list.bankAccounts.count, 1) + XCTAssertEqual(list.cards.count, 1) + XCTAssertEqual(list.sources.count, 1) + XCTAssertEqual(list.data.map { $0.id }, [ + "card_1Cs8z3GaLcnLeFWiU9SDCez8", + "src_1CxF9xGaLcnLeFWif1vfiSkS", + "ba_1CxEzUGaLcnLeFWiz0fJrOVm", + ]) + XCTAssertEqual(list.data.map { $0.object }, ["card", "source", "bank_account"]) + + }.catch { (error) in + XCTFail("\(error)") + } + } + catch { + XCTFail("\(error)") + } + } +}