From 5097e607090bcdf098335dd39a6aa6ef82250ad7 Mon Sep 17 00:00:00 2001 From: Christiaan Scheermeijer Date: Wed, 27 Mar 2024 09:49:47 +0100 Subject: [PATCH] fix(payment): missing feedback when submitting coupon --- .../src/controllers/CheckoutController.ts | 32 ++++++----------- .../cleeng/CleengCheckoutService.ts | 18 +++++++++- .../integrations/cleeng/CleengService.ts | 2 +- .../integrations/jwp/JWPCheckoutService.ts | 34 +++++++++++++------ .../components/CheckoutForm/CheckoutForm.tsx | 3 ++ .../AccountModal/forms/Checkout.tsx | 5 +-- 6 files changed, 58 insertions(+), 36 deletions(-) diff --git a/packages/common/src/controllers/CheckoutController.ts b/packages/common/src/controllers/CheckoutController.ts index 63362677e..ffe195d50 100644 --- a/packages/common/src/controllers/CheckoutController.ts +++ b/packages/common/src/controllers/CheckoutController.ts @@ -91,32 +91,22 @@ export default class CheckoutController { }; updateOrder = async (order: Order, paymentMethodId?: number, couponCode?: string | null): Promise => { - let response; - try { - response = await this.checkoutService.updateOrder({ order, paymentMethodId, couponCode }); - } catch (error: unknown) { - // TODO: we currently (falsely) assume that the only error caught is because the coupon is not valid, but there - // could be a network failure as well (JWPCheckoutService) - throw new FormValidationError({ couponCode: [i18next.t('account:checkout.coupon_not_valid')] }); - } + const response = await this.checkoutService.updateOrder({ order, paymentMethodId, couponCode }); - if (response.errors.length > 0) { - // clear the order when the order doesn't exist on the server - if (response.errors[0].includes(`Order with ${order.id} not found`)) { - useCheckoutStore.getState().setOrder(null); + if (response.responseData.order) { + useCheckoutStore.getState().setOrder(response.responseData?.order); } - - // TODO: this handles the `Coupon ${couponCode} not found` message (CleengCheckoutService) - if (response.errors[0].includes(`not found`)) { - throw new FormValidationError({ couponCode: [i18next.t('account:checkout.coupon_not_valid')] }); + } catch (error: unknown) { + if (error instanceof Error) { + if (error.message === 'Order not found') { + useCheckoutStore.getState().setOrder(null); + } else if (error.message === 'Invalid coupon code') { + throw new FormValidationError({ couponCode: [i18next.t('account:checkout.coupon_not_valid')] }); + } } - throw new FormValidationError({ form: response.errors }); - } - - if (response.responseData.order) { - useCheckoutStore.getState().setOrder(response.responseData?.order); + throw new FormValidationError({ form: [i18next.t('error:unknown_error')] }); } }; diff --git a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts index c0dcd4e9c..1dc3bce05 100644 --- a/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts +++ b/packages/common/src/services/integrations/cleeng/CleengCheckoutService.ts @@ -20,11 +20,13 @@ import type { PaymentWithPayPal, SwitchSubscription, UpdateOrder, + UpdateOrderResponse, UpdatePaymentWithPayPal, } from '../../../../types/checkout'; import CheckoutService from '../CheckoutService'; import { GET_CUSTOMER_IP } from '../../../modules/types'; import type { GetCustomerIP } from '../../../../types/get-customer-ip'; +import type { ServiceResponse } from '../../../../types/service'; import CleengService from './CleengService'; @@ -83,7 +85,21 @@ export default class CleengCheckoutService extends CheckoutService { }; updateOrder: UpdateOrder = async ({ order, ...payload }) => { - return this.cleengService.patch(`/orders/${order.id}`, JSON.stringify(payload), { authenticate: true }); + const response = await this.cleengService.patch>(`/orders/${order.id}`, JSON.stringify(payload), { + authenticate: true, + }); + + if (response.errors.length) { + if (response.errors[0].includes(`Order with ${order.id} not found`)) { + throw new Error('Order not found'); + } + + if (response.errors[0].includes(`Coupon ${payload.couponCode} not found`)) { + throw new Error('Invalid coupon code'); + } + } + + return response; }; getPaymentMethods: GetPaymentMethods = async () => { diff --git a/packages/common/src/services/integrations/cleeng/CleengService.ts b/packages/common/src/services/integrations/cleeng/CleengService.ts index 34d0ba298..7b724353d 100644 --- a/packages/common/src/services/integrations/cleeng/CleengService.ts +++ b/packages/common/src/services/integrations/cleeng/CleengService.ts @@ -193,7 +193,7 @@ export default class CleengService { return await resp.json(); } catch (error: unknown) { return { - errors: Array.of(error as string), + errors: Array.of(error instanceof Error ? error.message : String(error)), }; } }; diff --git a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts index 006071ce1..484eb393d 100644 --- a/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts +++ b/packages/common/src/services/integrations/jwp/JWPCheckoutService.ts @@ -21,6 +21,7 @@ import type { } from '../../../../types/checkout'; import CheckoutService from '../CheckoutService'; import type { ServiceResponse } from '../../../../types/service'; +import { isCommonError } from '../../../utils/api'; @injectable() export default class JWPCheckoutService extends CheckoutService { @@ -179,26 +180,37 @@ export default class JWPCheckoutService extends CheckoutService { voucherCode: `${couponCode}`, accessFeeId: order.id, }); - order.discount = { - applied: true, - type: 'coupon', - periods: response.data.discount_duration, - }; const discountedAmount = order.totalPrice - response.data.amount; - order.totalPrice = response.data.amount; - order.priceBreakdown.discountAmount = discountedAmount; - order.priceBreakdown.discountedPrice = discountedAmount; + const updatedOrder = { + ...order, + totalPrice: response.data.amount, + priceBreakdown: { + ...order.priceBreakdown, + discountedAmount, + discountedPrice: discountedAmount, + }, + discount: { + applied: true, + type: 'coupon', + periods: response.data.discount_duration, + }, + }; + return { errors: [], responseData: { message: 'successfully updated', - order: order, + order: updatedOrder, success: true, }, }; - } catch { - throw new Error('Invalid coupon code'); + } catch (error: unknown) { + if (isCommonError(error) && error.response.data.message === 'Voucher not found') { + throw new Error('Invalid coupon code'); + } + + throw new Error('An unknown error occurred'); } }; diff --git a/packages/ui-react/src/components/CheckoutForm/CheckoutForm.tsx b/packages/ui-react/src/components/CheckoutForm/CheckoutForm.tsx index a4733cf33..7213eee16 100644 --- a/packages/ui-react/src/components/CheckoutForm/CheckoutForm.tsx +++ b/packages/ui-react/src/components/CheckoutForm/CheckoutForm.tsx @@ -25,6 +25,7 @@ type Props = { onCouponInputChange: React.ChangeEventHandler; onRedeemCouponButtonClick: () => void; onCloseCouponFormClick: () => void; + error?: string; couponFormOpen: boolean; couponFormError?: string; couponFormApplied?: boolean; @@ -45,6 +46,7 @@ const CheckoutForm: React.FC = ({ offerType, onBackButtonClick, onPaymentMethodChange, + error, couponFormOpen, couponInputValue, couponFormError, @@ -89,6 +91,7 @@ const CheckoutForm: React.FC = ({ const orderTitle = offerType === 'svod' ? (offer.period === 'month' ? t('checkout.monthly') : t('checkout.yearly')) : offer.offerTitle; return (
+ {error ? {error} : null}

{t('checkout.payment_method')}

diff --git a/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx b/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx index df73a5cba..825da6582 100644 --- a/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx +++ b/packages/ui-react/src/containers/AccountModal/forms/Checkout.tsx @@ -53,10 +53,10 @@ const Checkout = () => { handleSubmit, } = useForm({ initialValues: { couponCode: '', paymentMethodId: paymentMethods?.[0]?.id?.toString() || '' }, - onSubmit: async ({ couponCode, paymentMethodId }) => { + onSubmit: ({ couponCode, paymentMethodId }) => { setShowCouponCodeSuccess(false); - return await updateOrder.mutateAsync({ couponCode, paymentMethodId: parseInt(paymentMethodId) }); + return updateOrder.mutateAsync({ couponCode, paymentMethodId: parseInt(paymentMethodId) }); }, onSubmitSuccess: ({ couponCode }): void => setShowCouponCodeSuccess(!!couponCode), onSubmitError: ({ error }) => { @@ -117,6 +117,7 @@ const Checkout = () => { order={order} offer={selectedOffer} offerType={offerType} + error={errors.form} onBackButtonClick={backButtonClickHandler} paymentMethods={paymentMethods} paymentMethodId={paymentMethodId}