Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Always mark failed order as pending before re-attempting a payment #14746

Draft
wants to merge 4 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -930,23 +930,6 @@ private extension OrderDetailsViewModel {
}
}

extension OrderDetailsViewModel {
/// Marks the order as pending if the WooCommerce version is eligible to send a receipt after payment.
/// Orders can be set to failed when payment fails.
/// We need to set it back to pending order when collecting payment to trigger all the related notifications when payment turns to failed again.
///
func markOrderPaymentPending() {
guard order.status != .pending, featureFlagService.isFeatureFlagEnabled(.sendReceiptAfterPayment) else {
return
}

let action = OrderAction.updateOrderStatus(siteID: order.siteID,
orderID: order.orderID,
status: .pending, onCompletion: { _ in })
stores.dispatch(action)
}
}

// MARK: - Notices
extension OrderDetailsViewModel {
private func displayReceiptRetrievalErrorNotice(for order: Order,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -248,16 +248,19 @@ private extension CollectOrderPaymentUseCase {
order.datePaid == nil
}

func checkOrderIsStillEligibleForPayment(alertProvider paymentAlerts: any CardReaderTransactionAlertsProviding<AlertPresenter.AlertDetails>,
onPaymentCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> (),
onCheckCompletion: @escaping (Result<Void, Error>) -> Void) {
func setOrderPendingAndCheckPaymentEligibility(alertProvider paymentAlerts: any CardReaderTransactionAlertsProviding<AlertPresenter.AlertDetails>,
onPaymentCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> (),
onCheckCompletion: @escaping (Result<Void, Error>) -> Void) {
alertsPresenter.present(viewModel: paymentAlerts.validatingOrder(onCancel: { [weak self] in
self?.cancelPayment(from: .paymentValidatingOrder) {
onPaymentCompletion(.failure(CollectOrderPaymentUseCaseError.flowCanceledByUser))
}
}))

let action = OrderAction.retrieveOrderRemotely(siteID: order.siteID, orderID: order.orderID) { [weak self] result in
/// Retrieves order information to check payment availaibility and sets the order to pending.
/// We need to set order order to pending when collecting payment to trigger all the related failure notifications when payment turns to failed.
///
let action = OrderAction.updateOrder(siteID: siteID, order: order.copy(status: .pending), giftCard: nil, fields: [.status]) { [weak self] result in
guard let self = self else { return }

switch result {
Expand Down Expand Up @@ -292,7 +295,7 @@ private extension CollectOrderPaymentUseCase {
paymentGatewayAccount: PaymentGatewayAccount,
channel: PaymentChannel,
onCompletion: @escaping (Result<CardPresentCapturedPaymentData, Error>) -> ()) {
checkOrderIsStillEligibleForPayment(alertProvider: paymentAlerts, onPaymentCompletion: onCompletion) { [weak self] result in
setOrderPendingAndCheckPaymentEligibility(alertProvider: paymentAlerts, onPaymentCompletion: onCompletion) { [weak self] result in
guard let self = self else { return }
switch result {
case .failure(let error):
Expand Down Expand Up @@ -526,7 +529,7 @@ private extension CollectOrderPaymentUseCase {
receiptState: receiptState,
tryAgain: { [weak self] in
guard let self = self else { return }
self.checkOrderIsStillEligibleForPayment(alertProvider: paymentAlerts, onPaymentCompletion: onCompletion) { result in
self.setOrderPendingAndCheckPaymentEligibility(alertProvider: paymentAlerts, onPaymentCompletion: onCompletion) { result in
switch result {
case .failure(let error):
return self.checkThenHandlePaymentFailureAndRetryPayment(error,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -729,8 +729,6 @@ private extension OrderDetailsViewController {
@objc private func collectPaymentTapped() {
collectPayment()

viewModel.markOrderPaymentPending()

// Track tapped event
ServiceLocator.analytics.track(event: WooAnalyticsEvent.Orders.collectPaymentTapped(flow: .orderDetails))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase {
private func setUpUseCase(order: Order) {
stores.whenReceivingAction(ofType: OrderAction.self) { action in
switch action {
case .retrieveOrderRemotely(_, _, let completion):
case .updateOrder(_, _, _, _, let completion):
completion(.success(order))
default:
break
Expand Down Expand Up @@ -145,7 +145,7 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase {

stores.whenReceivingAction(ofType: OrderAction.self) { action in
switch action {
case .retrieveOrderRemotely(_, _, let completion):
case .updateOrder(_, _, _, _, let completion):
completion(.success(order))
default:
break
Expand Down Expand Up @@ -189,7 +189,7 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase {
var markOrderAsPaidLocallyAction: (siteID: Int64, orderID: Int64)?
stores.whenReceivingAction(ofType: OrderAction.self) { action in
switch action {
case .retrieveOrderRemotely(_, _, let completion):
case .updateOrder(_, _, _, _, let completion):
completion(.success(Order.fake().copy(siteID: self.defaultSiteID, orderID: self.defaultOrderID, total: "1.5")))
case .markOrderAsPaidLocally(let siteID, let orderID, _, _):
markOrderAsPaidLocallyAction = (siteID: siteID, orderID: orderID)
Expand Down Expand Up @@ -222,7 +222,7 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase {
var markOrderAsPaidLocallyAction: (siteID: Int64, orderID: Int64)?
stores.whenReceivingAction(ofType: OrderAction.self) { action in
switch action {
case .retrieveOrderRemotely(_, _, let completion):
case .updateOrder(_, _, _, _, let completion):
completion(.success(Order.fake().copy(siteID: self.defaultSiteID, orderID: self.defaultOrderID, total: "1.5")))
case .markOrderAsPaidLocally(let siteID, let orderID, _, _):
markOrderAsPaidLocallyAction = (siteID: siteID, orderID: orderID)
Expand Down Expand Up @@ -361,6 +361,74 @@ final class CollectOrderPaymentUseCaseTests: XCTestCase {
// Then
XCTAssertEqual(mockPaymentOrchestrator.spyChannel, .pos)
}

func test_collectPayment_when_order_failed_then_marked_as_pending() async throws {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for adding the test cases!

// Given we have a failed order
let order = Order.fake().copy(siteID: defaultSiteID, orderID: defaultOrderID, status: .failed, total: "1.5")
setUpUseCase(order: order)
let interacPaymentMethod = PaymentMethod.interacPresent(details: .fake())
let intent = PaymentIntent.fake().copy(charges: [.fake().copy(paymentMethod: interacPaymentMethod)])
mockSuccessfulCardPresentPaymentActions(intent: intent,
capturedPaymentData: CardPresentCapturedPaymentData(paymentMethod: interacPaymentMethod,
receiptParameters: .fake()))

var orderStatus: OrderStatusEnum?
stores.whenReceivingAction(ofType: OrderAction.self) { action in
switch action {
case .updateOrder(_, let order, _, _, let completion):
orderStatus = order.status
completion(.success(order))
default:
break
}
}

// When we make a successful payment
waitFor { promise in
self.useCase.collectPayment(using: .bluetoothScan, channel: .storeManagement, onFailure: { _ in }, onCancel: {}, onPaymentCompletion: {
promise(())
}, onCompleted: {})
self.mockPreflightController.completeConnection(reader: MockCardReader.wisePad3(), gatewayID: Mocks.paymentGatewayAccount)
}

// Then the order status should be updated to pending
XCTAssertEqual(orderStatus, .pending)
}

func test_collectPayment_when_order_pending_then_not_marked_as_pending() async throws {
// Given we have a pending order
let order = Order.fake().copy(siteID: defaultSiteID, orderID: defaultOrderID, status: .pending, total: "1.5")

setUpUseCase(order: order)
let interacPaymentMethod = PaymentMethod.interacPresent(details: .fake())
let intent = PaymentIntent.fake().copy(charges: [.fake().copy(paymentMethod: interacPaymentMethod)])
mockSuccessfulCardPresentPaymentActions(intent: intent,
capturedPaymentData: CardPresentCapturedPaymentData(paymentMethod: interacPaymentMethod,
receiptParameters: .fake()))

var orderStatus: OrderStatusEnum?
stores.whenReceivingAction(ofType: OrderAction.self) { action in
switch action {
case let .updateOrderStatus(_, _, status, _):
orderStatus = status
case .updateOrder(_, _, _, _, let completion):
completion(.success(order))
default:
break
}
}

// When we make a successful payment
waitFor { promise in
self.useCase.collectPayment(using: .bluetoothScan, channel: .storeManagement, onFailure: { _ in }, onCancel: {}, onPaymentCompletion: {
promise(())
}, onCompleted: {})
self.mockPreflightController.completeConnection(reader: MockCardReader.wisePad3(), gatewayID: Mocks.paymentGatewayAccount)
}

// Then the order status should not be updated
XCTAssertEqual(orderStatus, nil)
}
}

private extension CollectOrderPaymentUseCaseTests {
Expand Down