Skip to content

Commit ba1c798

Browse files
committed
5976 Fetch, store, delete WCPayCharge from Storage
1 parent 324fb8b commit ba1c798

File tree

2 files changed

+251
-2
lines changed

2 files changed

+251
-2
lines changed

Yosemite/Yosemite/Stores/CardPresentPaymentStore.swift

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,18 @@ private extension CardPresentPaymentStore {
526526
func fetchCharge(siteID: Int64, chargeID: String, completion: @escaping (Result<WCPayCharge, Error>) -> Void) {
527527
switch usingBackend {
528528
case .wcpay:
529-
remote.fetchCharge(for: siteID, chargeID: chargeID, completion: completion)
529+
remote.fetchCharge(for: siteID, chargeID: chargeID) { result in
530+
switch result {
531+
case .success(let charge):
532+
self.upsertChargeInBackground(readonlyCharge: charge)
533+
completion(.success(charge))
534+
case .failure(let error):
535+
if case .noSuchChargeError(_) = WCPayChargesError(underlyingError: error) {
536+
self.deleteCharge(siteID: siteID, chargeID: chargeID)
537+
}
538+
completion(.failure(error))
539+
}
540+
}
530541
case .stripe:
531542
break; /// not implemented
532543
}
@@ -552,6 +563,45 @@ private extension CardPresentPaymentStore {
552563
storage.deleteObject(storageAccount)
553564
storage.saveIfNeeded()
554565
}
566+
567+
func upsertChargeInBackground(readonlyCharge: WCPayCharge) {
568+
let storage = storageManager.viewStorage
569+
let storageWCPayCharge = storage.loadWCPayCharge(siteID: readonlyCharge.siteID, chargeID: readonlyCharge.id) ??
570+
storage.insertNewObject(ofType: Storage.WCPayCharge.self)
571+
572+
switch readonlyCharge.paymentMethodDetails {
573+
case .cardPresent(let details):
574+
let storageCardPresentDetails = storageWCPayCharge.cardPresentDetails ??
575+
storage.insertNewObject(ofType: Storage.WCPayCardPresentPaymentDetails.self)
576+
let storageReceiptDetails = storageCardPresentDetails.receipt ?? storage.insertNewObject(ofType: Storage.WCPayCardPresentReceiptDetails.self)
577+
storageCardPresentDetails.update(with: details)
578+
storageReceiptDetails.update(with: details.receipt)
579+
storageCardPresentDetails.receipt = storageReceiptDetails
580+
storageWCPayCharge.cardPresentDetails = storageCardPresentDetails
581+
storageWCPayCharge.cardDetails = nil
582+
case .card(let details):
583+
let storageCardDetails = storageWCPayCharge.cardDetails ?? storage.insertNewObject(ofType: Storage.WCPayCardPaymentDetails.self)
584+
storageCardDetails.update(with: details)
585+
storageWCPayCharge.cardDetails = storageCardDetails
586+
storageWCPayCharge.cardPresentDetails = nil
587+
case .unknown:
588+
storageWCPayCharge.cardDetails = nil
589+
storageWCPayCharge.cardPresentDetails = nil
590+
break
591+
}
592+
593+
storageWCPayCharge.update(with: readonlyCharge)
594+
}
595+
596+
func deleteCharge(siteID: Int64, chargeID: String) {
597+
let storage = storageManager.viewStorage
598+
guard let charge = storage.loadWCPayCharge(siteID: siteID, chargeID: chargeID) else {
599+
return
600+
}
601+
602+
storage.deleteObject(charge)
603+
storage.saveIfNeeded()
604+
}
555605
}
556606

557607
public enum PaymentGatewayAccountError: Error, LocalizedError {
@@ -616,3 +666,49 @@ public protocol CardReaderCapableRemote {
616666

617667
extension WCPayRemote: CardReaderCapableRemote {}
618668
extension StripeRemote: CardReaderCapableRemote {}
669+
670+
// MARK: - WCPayChargesError
671+
public enum WCPayChargesError: Error, LocalizedError {
672+
case noSuchChargeError(message: String)
673+
case otherError(error: AnyError)
674+
675+
init(underlyingError error: Error) {
676+
guard case let DotcomError.unknown(code, message) = error,
677+
let message = message else {
678+
self = .otherError(error: error.toAnyError)
679+
return
680+
}
681+
682+
/// See if we recognize this DotcomError code
683+
///
684+
self = ErrorCode(rawValue: code)?.error(message: message) ?? .otherError(error: error.toAnyError)
685+
}
686+
687+
enum ErrorCode: String {
688+
case getChargeError = "wcpay_get_charge"
689+
case unknown
690+
691+
func error(message: String) -> WCPayChargesError? {
692+
switch self {
693+
case .getChargeError:
694+
guard message.starts(with: "Error: No such charge") else {
695+
return nil
696+
}
697+
return .noSuchChargeError(message: message)
698+
default:
699+
return nil
700+
}
701+
}
702+
}
703+
704+
public var errorDescription: String? {
705+
switch self {
706+
case .noSuchChargeError(let message):
707+
/// Return the message directly from the store
708+
/// "Error: No such charge: 'ch_3KMVapErrorERROR'"
709+
return message
710+
case .otherError(let error):
711+
return error.localizedDescription
712+
}
713+
}
714+
}

Yosemite/YosemiteTests/Stores/CardPresentPaymentStoreTests.swift

Lines changed: 154 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ final class CardPresentPaymentStoreTests: XCTestCase {
4343
///
4444
private let sampleChargeID = "ch_3KMVap2EdyGr1FMV1uKJEWtg"
4545

46+
/// Testing Charge ID for card transaction
47+
///
48+
private let sampleCardChargeID = "ch_3KMuym2EdyGr1FMV0uQZeFqm"
49+
4650
/// Testing Charge ID for error
4751
///
4852
private let sampleErrorChargeID = "ch_3KMVapErrorERROR"
@@ -432,6 +436,96 @@ final class CardPresentPaymentStoreTests: XCTestCase {
432436
XCTAssertEqual(charge.id, sampleChargeID)
433437
}
434438

439+
func test_fetchWCPayCharge_inserts_charge_in_storage() throws {
440+
let store = CardPresentPaymentStore(dispatcher: dispatcher,
441+
storageManager: storageManager,
442+
network: network,
443+
cardReaderService: mockCardReaderService,
444+
allowStripeIPP: false)
445+
446+
network.simulateResponse(requestUrlSuffix: "payments/charges/\(sampleChargeID)",
447+
filename: "wcpay-charge-card-present")
448+
449+
let result: Result<Yosemite.WCPayCharge, Error> = waitFor { [self] promise in
450+
let action = CardPresentPaymentAction.fetchWCPayCharge(siteID: self.sampleSiteID, chargeID: self.sampleChargeID, onCompletion: { result in
451+
promise(result)
452+
})
453+
store.onAction(action)
454+
}
455+
XCTAssertTrue(result.isSuccess)
456+
457+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCharge.self, matching: nil) == 1)
458+
459+
let storageCharge = viewStorage.loadWCPayCharge(siteID: sampleSiteID, chargeID: sampleChargeID)
460+
461+
XCTAssertEqual(storageCharge?.siteID, sampleSiteID)
462+
XCTAssertEqual(storageCharge?.chargeID, sampleChargeID)
463+
XCTAssertEqual(storageCharge?.status, "succeeded")
464+
}
465+
466+
func test_fetchWCPayCharge_inserts_card_present_charge_details_in_storage() throws {
467+
let store = CardPresentPaymentStore(dispatcher: dispatcher,
468+
storageManager: storageManager,
469+
network: network,
470+
cardReaderService: mockCardReaderService,
471+
allowStripeIPP: false)
472+
473+
network.simulateResponse(requestUrlSuffix: "payments/charges/\(sampleChargeID)",
474+
filename: "wcpay-charge-card-present")
475+
476+
let result: Result<Yosemite.WCPayCharge, Error> = waitFor { [self] promise in
477+
let action = CardPresentPaymentAction.fetchWCPayCharge(siteID: self.sampleSiteID, chargeID: self.sampleChargeID, onCompletion: { result in
478+
promise(result)
479+
})
480+
store.onAction(action)
481+
}
482+
XCTAssertTrue(result.isSuccess)
483+
484+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCharge.self, matching: nil) == 1)
485+
486+
let storageCharge = viewStorage.loadWCPayCharge(siteID: sampleSiteID, chargeID: sampleChargeID)
487+
488+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCardPaymentDetails.self, matching: nil) == 0)
489+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCardPresentPaymentDetails.self, matching: nil) == 1)
490+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCardPresentReceiptDetails.self, matching: nil) == 1)
491+
492+
let storedDetails = storageCharge?.cardPresentDetails
493+
XCTAssertEqual(storedDetails?.receipt?.applicationPreferredName, "Stripe Credit")
494+
XCTAssertEqual(storedDetails?.last4, "9969")
495+
XCTAssertNil(storageCharge?.cardDetails)
496+
}
497+
498+
func test_fetchWCPayCharge_inserts_card_charge_details_in_storage() throws {
499+
let store = CardPresentPaymentStore(dispatcher: dispatcher,
500+
storageManager: storageManager,
501+
network: network,
502+
cardReaderService: mockCardReaderService,
503+
allowStripeIPP: false)
504+
505+
network.simulateResponse(requestUrlSuffix: "payments/charges/\(sampleCardChargeID)",
506+
filename: "wcpay-charge-card")
507+
508+
let result: Result<Yosemite.WCPayCharge, Error> = waitFor { [self] promise in
509+
let action = CardPresentPaymentAction.fetchWCPayCharge(siteID: self.sampleSiteID, chargeID: self.sampleCardChargeID, onCompletion: { result in
510+
promise(result)
511+
})
512+
store.onAction(action)
513+
}
514+
XCTAssertTrue(result.isSuccess)
515+
516+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCharge.self, matching: nil) == 1)
517+
518+
let storageCharge = viewStorage.loadWCPayCharge(siteID: sampleSiteID, chargeID: sampleCardChargeID)
519+
520+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCardPaymentDetails.self, matching: nil) == 1)
521+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCardPresentPaymentDetails.self, matching: nil) == 0)
522+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCardPresentReceiptDetails.self, matching: nil) == 0)
523+
524+
let storedDetails = storageCharge?.cardDetails
525+
XCTAssertEqual(storedDetails?.last4, "1111")
526+
XCTAssertNil(storageCharge?.cardPresentDetails)
527+
}
528+
435529
/// Verifies that the store hits the network when fetching a charge, and propagates errors.
436530
///
437531
func test_fetchWCPayCharge_returns_error_on_failure() {
@@ -442,7 +536,7 @@ final class CardPresentPaymentStoreTests: XCTestCase {
442536
allowStripeIPP: false)
443537

444538
network.simulateResponse(requestUrlSuffix: "payments/charges/\(sampleErrorChargeID)",
445-
filename: "wcpay-customer-error")
539+
filename: "wcpay-charge-error")
446540
let result: Result<Yosemite.WCPayCharge, Error> = waitFor { [self] promise in
447541
let action = CardPresentPaymentAction.fetchWCPayCharge(siteID: self.sampleSiteID, chargeID: self.sampleErrorChargeID, onCompletion: { result in
448542
promise(result)
@@ -451,4 +545,63 @@ final class CardPresentPaymentStoreTests: XCTestCase {
451545
}
452546
XCTAssertTrue(result.isFailure)
453547
}
548+
549+
/// Verifies that the store deletes the charge if it's gone from the remote.
550+
///
551+
func test_fetchWCPayCharge_deletes_existing_charge_on_no_such_charge_failure() {
552+
let charge = viewStorage.insertNewObject(ofType: Storage.WCPayCharge.self)
553+
let networkCharge = WCPayCharge.fake().copy(siteID: sampleSiteID, id: sampleErrorChargeID)
554+
charge.update(with: networkCharge)
555+
let otherCharge = viewStorage.insertNewObject(ofType: Storage.WCPayCharge.self)
556+
let otherNetworkCharge = WCPayCharge.fake().copy(siteID: sampleSiteID, id: sampleChargeID)
557+
otherCharge.update(with: otherNetworkCharge)
558+
559+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCharge.self, matching: nil) == 2)
560+
let store = CardPresentPaymentStore(dispatcher: dispatcher,
561+
storageManager: storageManager,
562+
network: network,
563+
cardReaderService: mockCardReaderService,
564+
allowStripeIPP: false)
565+
566+
network.simulateResponse(requestUrlSuffix: "payments/charges/\(sampleErrorChargeID)",
567+
filename: "wcpay-charge-error")
568+
let _: Result<Yosemite.WCPayCharge, Error> = waitFor { [self] promise in
569+
let action = CardPresentPaymentAction.fetchWCPayCharge(siteID: self.sampleSiteID, chargeID: self.sampleErrorChargeID, onCompletion: { result in
570+
promise(result)
571+
})
572+
store.onAction(action)
573+
}
574+
XCTAssertEqual(viewStorage.countObjects(ofType: Storage.WCPayCharge.self, matching: nil), 1)
575+
576+
let storageCharge = viewStorage.firstObject(ofType: Storage.WCPayCharge.self)
577+
XCTAssertEqual(storageCharge, otherCharge)
578+
}
579+
580+
/// Verifies that the store doesn't delete charges just for any old error.
581+
///
582+
func test_fetchWCPayCharge_does_not_delete_existing_charge_on_unknown_failure() {
583+
let charge = viewStorage.insertNewObject(ofType: Storage.WCPayCharge.self)
584+
let networkCharge = WCPayCharge.fake().copy(siteID: sampleSiteID, id: sampleErrorChargeID)
585+
charge.update(with: networkCharge)
586+
let otherCharge = viewStorage.insertNewObject(ofType: Storage.WCPayCharge.self)
587+
let otherNetworkCharge = WCPayCharge.fake().copy(siteID: sampleSiteID, id: sampleChargeID)
588+
otherCharge.update(with: otherNetworkCharge)
589+
590+
XCTAssert(viewStorage.countObjects(ofType: Storage.WCPayCharge.self, matching: nil) == 2)
591+
let store = CardPresentPaymentStore(dispatcher: dispatcher,
592+
storageManager: storageManager,
593+
network: network,
594+
cardReaderService: mockCardReaderService,
595+
allowStripeIPP: false)
596+
network.simulateError(requestUrlSuffix: "payments/charges/\(sampleErrorChargeID)",
597+
error: DotcomError.unknown(code: "beep", message: "boop"))
598+
599+
let _: Result<Yosemite.WCPayCharge, Error> = waitFor { [self] promise in
600+
let action = CardPresentPaymentAction.fetchWCPayCharge(siteID: self.sampleSiteID, chargeID: self.sampleErrorChargeID, onCompletion: { result in
601+
promise(result)
602+
})
603+
store.onAction(action)
604+
}
605+
XCTAssertEqual(viewStorage.countObjects(ofType: Storage.WCPayCharge.self, matching: nil), 2)
606+
}
454607
}

0 commit comments

Comments
 (0)