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

[Woo POS] Consider an order that is already paid as success in CollectOrderPaymentUseCase #13178

Merged
merged 5 commits into from
Jun 28, 2024

Conversation

jaclync
Copy link
Contributor

@jaclync jaclync commented Jun 26, 2024

Closes: #13128
Based on #13165

Description

As capture_terminal_payment could succeed on the server side but fail returning the response, and if order sync still fails after the graceful handling in #13165, the app/POS results in a server error state. After retrying by canceling and collecting payment again, it currently results in an "already paid" error state.

server-side error after retrying - already paid error
IMG_0390 IMG_0391

If we know that the order is already paid, transitioning to the success state would be a better UX instead of a non-retryable error state in POS. The changes in this PR should only affect POS and not the existing app, since the order might not always be up to date.

Steps to reproduce

Adapted from #13165:

Existing IPP experience (no changes are expected)

  1. Set up Proxyman or Charles to intercept traffic for your test device
  2. Launch the app using a store eligible for IPP
  3. Navigate to Orders > +
  4. Add a new order, and tap Collect Payment
  5. Select Card reader and connect to the reader if needed
  6. Enable breakpoints in your proxy for the response of POST requests to https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/*/rest-api/ and GET requests to https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/*/rest-api/?json=true&path=/wc/v3/orders/*
  7. Tap your card on the reader
  8. Abort the capture_terminal_payment response when the breakpoint is hit (equivalent to poor network conditions meaning it was lost.)
  9. Shortly after, abort the orders/* response when the breakpoint is hit --> the server-side error state should be shown
  10. Dismiss the error alert
  11. Tap Collect Payment for the same order again --> the "already paid" error alert should be shown

New POS experience

  1. Set up Proxyman or Charles to intercept traffic for your test device
  2. Launch the app using a store eligible for IPP
  3. Navigate to Menu > Point of Sale mode
  4. Add an item to your basket and tap Checkout
  5. Enable breakpoints in your proxy for the response of POST requests to https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/*/rest-api/ and GET requests to https://public-api.wordpress.com/rest/v1.1/jetpack-blogs/*/rest-api/?json=true&path=/wc/v3/orders/*
  6. Shortly after, abort the orders/* response when the breakpoint is hit --> the server-side error state should be shown
  7. Tap Cancel payment and then Collect payment again
  8. Tap your card on the reader
  9. Observe that the payment shows as successful

Testing information

Screenshots

After this PR, the success state should be shown:


  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

@jaclync jaclync added type: task An internally driven task. feature: POS labels Jun 26, 2024
@jaclync jaclync added this to the 19.3 milestone Jun 26, 2024
@jaclync jaclync changed the base branch from trunk to issue/13162-handle-failures-from-capture-terminal-payment-gracefully June 26, 2024 20:48
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Jun 26, 2024

WooCommerce iOS📲 You can test the changes from this Pull Request in WooCommerce iOS by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS WooCommerce iOS
Build Numberpr13178-0555475
Version19.2
Bundle IDcom.automattic.alpha.woocommerce
Commit0555475
App Center BuildWooCommerce - Prototype Builds #9804
Automatticians: You can use our internal self-serve MC tool to give yourself access to App Center if needed.

@jaclync jaclync requested a review from joshheald June 26, 2024 21:03
Base automatically changed from issue/13162-handle-failures-from-capture-terminal-payment-gracefully to trunk June 27, 2024 09:48
Copy link
Contributor

@joshheald joshheald left a comment

Choose a reason for hiding this comment

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

I don't think this is the right approach, because of the change in the existing app.

I think there, we should still show an "order already paid" error, and adapt it in the POS only.

More detail in the in-line comment.

@@ -267,7 +267,8 @@ private extension CollectOrderPaymentUseCase {
}

guard self.isOrderAwaitingPayment() else {
return onCheckCompletion(.failure(CollectOrderPaymentUseCaseError.orderAlreadyPaid))
return onPaymentCompletion(.success(CardPresentCapturedPaymentData(paymentMethod: .unknown,
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should return success here, in the existing flows. The error is clearer for some of the situations which can come up.

For POS, where we're really confident that the order's just been created, we know that the most likely (perhaps only possible) reason for hitting this error is after retry after the missed network connection as covered in #13165. In that case, the shopper will have just tapped their card, so it's not confusing to see a success payment.

For the existing flow, where we rely more on storage and orders can be much older, it's not so safe to show success here. A merchant could have an outdated copy of the order from storage, tap Collect Payment, then immediately see that the order's complete without the customer tapping their card, nor any explanation of how it's complete.

In that situation, they're likely to think that the app is wrong and that they need to redo the order. The error would have explained it much more clearly.

The best place to do this for the new experience is probably in CardPresentPaymentAlertPresenterAdaptor.present, e.g. with a case like this one:

    case .paymentError(error: CollectOrderPaymentUseCaseError.orderAlreadyPaid, _, _),
                .paymentErrorNonRetryable(error: CollectOrderPaymentUseCaseError.orderAlreadyPaid, _):
            paymentEventSubject.send(.show(eventDetails: .paymentSuccess(done: {})))

It probably needs to do more than just that, e.g. we'll need to clean up something in the facade, but that's my idea for a starting point... although from a quick try, it does look like it "just works".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we should return success here, in the existing flows. The error is clearer for some of the situations which can come up.

For POS, where we're really confident that the order's just been created, we know that the most likely (perhaps only possible) reason for hitting this error is after retry after the missed network connection as covered in #13165. In that case, the shopper will have just tapped their card, so it's not confusing to see a success payment.

For the existing flow, where we rely more on storage and orders can be much older, it's not so safe to show success here. A merchant could have an outdated copy of the order from storage, tap Collect Payment, then immediately see that the order's complete without the customer tapping their card, nor any explanation of how it's complete.

In that situation, they're likely to think that the app is wrong and that they need to redo the order. The error would have explained it much more clearly.

Thanks a lot for the explanation about why this change shouldn't be applied to the existing flow, it makes sense. @staskus and I had a pair programming session to work on this issue, and your suggested fix works from my testing! (I tested that this scenario results in success for POS and remains an error in the existing flow in order creation).

It probably needs to do more than just that, e.g. we'll need to clean up something in the facade, but that's my idea for a starting point... although from a quick try, it does look like it "just works".

For this, I'd like to check with you. There are also 2 other TODO comments about calling the facade's cancelPayment for further cleanup. I tried triggering CardPresentPaymentService.cancelPayment in the event's cancelPayment closure, but the current implementation of cancellation is to also call the event's cancelPayment closure:

.paymentError(_, _, cancelPayment: let cancelPayment),
.paymentErrorNonRetryable(_, cancelPayment: let cancelPayment):
cancelPayment()

In CardPresentPaymentCollectOrderPaymentUseCaseAdaptor, we could also change how we handle the cancellation. An option is to cancel the payment intent, which I think we could do for non-retryable payment errors (there could be exceptions that I didn't know of). Does this sound like the right way to go, canceling the payment intent for non-retryable payment errors (moving this line to the .processing cases below)?

For retryable payment errors, is there any cleanup we should do? In place of this TODO comment.

If this is a bit too much to take on before your AFK, I can create a separate task for this further cancelation cleanup for later.

Copy link
Contributor

Choose a reason for hiding this comment

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

For this case I don't think it would be appropriate or needed to call cancelPayment. The orderAlreadyPaid error is thrown before we create the payment intent, so there's nothing to cancel yet.

From our testing, it's working without further cleanup, so that's great. It makes sense actually, because we throw away the CollectOrderPaymentUseCase when the merchant taps New transaction.

However, CollectOrderPaymentUseCase doesn't know anything about this success, so we're tracking this as a payment failure in our analytics... or actually two failures:

CleanShot 2024-06-28 at 12 06 37@2x

We're not really dealing with this yet, but it would be nice to track success if we can. This can be delayed but let's capture it in a ticket. It's fine to track the failures, but we should track the success as well at the end.

I think perhaps we should defer those TODOs for after my AFK – we'll have a new project to tighten everything up and deal with the TODOs... I can't dig in to them now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

However, CollectOrderPaymentUseCase doesn't know anything about this success, so we're tracking this as a payment failure in our analytics... or actually two failures:

CleanShot 2024-06-28 at 12 06 37@2x

We're not really dealing with this yet, but it would be nice to track success if we can. This can be delayed but let's capture it in a ticket. It's fine to track the failures, but we should track the success as well at the end.

Good catch on the tracking side, yea we'd want to make sure to track the success state for this case. I created a draft task for this, currently in the "Blocked" column because we don't have an analytics plan yet. I suggested tracking POS success when the view model receives the payment success event and is about to show the success UI.

@jaclync jaclync marked this pull request as ready for review June 27, 2024 17:56
@jaclync
Copy link
Contributor Author

jaclync commented Jun 27, 2024

Thanks for working on the changes and pushing the commits @staskus, I added a commit to move the handling of the "already paid" error before the default error handling because Swift executes switch cases in order.

@joshheald I've updated the PR & description if you get a chance to review it! If not, feel free to just briefly share what you think makes the most sense - leaving the PR open, creating a separate issue for further cleanup, etc.

@malinajirka malinajirka modified the milestones: 19.3, 19.4 Jun 28, 2024
@jaclync
Copy link
Contributor Author

jaclync commented Jun 28, 2024

Thanks for reviewing & testing this Josh 🙏 merging it now!

* trunk: (97 commits)
  Update naming for analytics card to display a stat and list of top performers
  Add generic stats data formatting helper to format amounts with currency
  Bump version number
  Fix rubocop violation
  Refine error wording
  Allow `finalize_release` lane to be retried on CI
  Bump version number
  Update metadata translations
  Update app translations – `Localizable.strings`
  Update tests for GoogleAdsStore
  Export GoogleAdsCampaign from Yosemite
  Update Google ads action and store to support fetching campaigns
  Fix swiftlint issue
  Update MockGoogleListingsAndAdsRemote
  Regenerate fake and copiable
  Add support for requesting paginated stats data
  Add support for decoding paginated campaign stats
  Support stats responses with a subset of totals fields
  Always send fields and orderby params with stats request
  feat: use reverse-DNS keys in localized strings
  ...
@jaclync jaclync enabled auto-merge June 28, 2024 14:12
@jaclync jaclync merged commit 656b86c into trunk Jun 28, 2024
21 of 22 checks passed
@jaclync jaclync deleted the feat/13128-handle-already-paid branch June 28, 2024 14:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature: POS type: task An internally driven task.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Woo POS] Improve handling of error when trying to pay for an already paid order
5 participants