Skip to content

Commit f901be8

Browse files
authored
Merge pull request #5303 from woocommerce/issue/4911-minimum-amount
[Mobile Payments] Cancel payment capture if order amount is below minimum (USD only)
2 parents 1735d48 + 4b03746 commit f901be8

File tree

4 files changed

+71
-0
lines changed

4 files changed

+71
-0
lines changed

Hardware/Hardware/CardReader/CardReaderServiceError.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,9 @@ public enum UnderlyingError: Error {
186186
/// The SDK will attempt to auto-disconnect for you and you should instruct your user to reconnect it.
187187
case readerSessionExpired
188188

189+
/// The underlying request returned an API error.
190+
case processorAPIError
191+
189192
/// Catch-all error case. Indicates there is something wrong with the
190193
/// internal state of the CardReaderService.
191194
case internalServiceError
@@ -305,6 +308,9 @@ updating the application or using a different reader
305308
case .readerSessionExpired:
306309
return NSLocalizedString("The card reader session has expired - please disconnect and reconnect the card reader and then try again",
307310
comment: "Error message when the card reader session has timed out.")
311+
case .processorAPIError:
312+
return NSLocalizedString("The payment can not be processed by the payment processor.",
313+
comment: "Error message when the payment can not be processed (i.e. order amount is below the minimum amount allowed.)")
308314
case .internalServiceError:
309315
return NSLocalizedString("Sorry, this payment couldn’t be processed",
310316
comment: "Error message when the card reader service experiences an unexpected internal service error.")

Hardware/Hardware/CardReader/StripeCardReader/UnderlyingError+Stripe.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ extension UnderlyingError {
7979
self = .requestTimedOut
8080
case ErrorCode.Code.sessionExpired.rawValue:
8181
self = .readerSessionExpired
82+
case ErrorCode.Code.stripeAPIError.rawValue:
83+
self = .processorAPIError
8284
default:
8385
self = .internalServiceError
8486
}

Hardware/HardwareTests/ErrorCodesTests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,10 @@ final class CardReaderServiceErrorTests: XCTestCase {
154154
XCTAssertEqual(.readerSessionExpired, domainError(stripeCode: 9060))
155155
}
156156

157+
func test_stripe_error_api_maps_to_stripeAPI() {
158+
XCTAssertEqual(.processorAPIError, domainError(stripeCode: 9020))
159+
}
160+
157161
func test_stripe_catch_all_error() {
158162
// Any error code not mapped to an specific error will be
159163
// mapped to `internalServiceError`

WooCommerce/Classes/ViewModels/CardPresentPayments/PaymentCaptureOrchestrator.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ final class PaymentCaptureOrchestrator {
2020
onClearMessage: @escaping () -> Void,
2121
onProcessingMessage: @escaping () -> Void,
2222
onCompletion: @escaping (Result<CardPresentReceiptParameters, Error>) -> Void) {
23+
/// Bail out if the order amount is below the minimum allowed:
24+
/// https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts
25+
guard isTotalAmountValid(order: order) else {
26+
DDLogError("💳 Error: failed to capture payment for order. Order amount is below minimum")
27+
onCompletion(.failure(minimumAmountError(order: order, minimumAmount: Constants.minimumAmount)))
28+
return
29+
}
2330
/// First ask the backend to create/assign a Stripe customer for the order
2431
///
2532
var customerID: String?
@@ -258,6 +265,30 @@ private extension PaymentCaptureOrchestrator {
258265
}
259266
}
260267

268+
private extension PaymentCaptureOrchestrator {
269+
enum Constants {
270+
/// Minimum order amount in USD:
271+
/// https://stripe.com/docs/currencies#minimum-and-maximum-charge-amounts
272+
static let minimumAmount = NSDecimalNumber(string: "0.5")
273+
}
274+
275+
func isTotalAmountValid(order: Order) -> Bool {
276+
guard let orderTotal = currencyFormatter.convertToDecimal(from: order.total) else {
277+
return false
278+
}
279+
280+
return orderTotal as Decimal >= Constants.minimumAmount as Decimal
281+
}
282+
283+
func minimumAmountError(order: Order, minimumAmount: NSDecimalNumber) -> Error {
284+
guard let minimum = currencyFormatter.formatAmount(minimumAmount, with: order.currency) else {
285+
return NotValidAmountError.other
286+
}
287+
288+
return NotValidAmountError.belowMinimumAmount(amount: minimum)
289+
}
290+
}
291+
261292
private extension PaymentCaptureOrchestrator {
262293
enum Localization {
263294
static let receiptDescription = NSLocalizedString("In-Person Payment for Order #%1$@ for %2$@",
@@ -268,3 +299,31 @@ private extension PaymentCaptureOrchestrator {
268299
+ "%2$@ - store name")
269300
}
270301
}
302+
303+
private extension PaymentCaptureOrchestrator {
304+
private enum NotValidAmountError: Error, LocalizedError {
305+
case belowMinimumAmount(amount: String)
306+
case other
307+
308+
public var errorDescription: String? {
309+
switch self {
310+
case .belowMinimumAmount(let amount):
311+
return String.localizedStringWithFormat(Localizations.belowMinimumAmount, amount)
312+
case .other:
313+
return Localizations.defaultMessage
314+
}
315+
}
316+
317+
enum Localizations {
318+
static let defaultMessage = NSLocalizedString(
319+
"Unable to process payment. Order total amount is not valid.",
320+
comment: "Error message when the order amount is not valid."
321+
)
322+
323+
static let belowMinimumAmount = NSLocalizedString(
324+
"Unable to process payment. Order total amount is below the minimum amount you can charge, which is %1$@",
325+
comment: "Error message when the order amount is below the minimum amount allowed."
326+
)
327+
}
328+
}
329+
}

0 commit comments

Comments
 (0)