richie-education 3.2.1-dev10 → 3.2.1-dev11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -10,6 +10,7 @@ import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
|
|
|
10
10
|
import WithdrawRightCheckbox from 'components/SaleTunnel/WithdrawRightCheckbox';
|
|
11
11
|
import { PaymentSchedule, ProductType } from 'types/Joanie';
|
|
12
12
|
import { usePaymentPlan } from 'hooks/usePaymentPlan';
|
|
13
|
+
import { HttpError } from 'utils/errors/HttpError';
|
|
13
14
|
|
|
14
15
|
const messages = defineMessages({
|
|
15
16
|
title: {
|
|
@@ -73,11 +74,16 @@ const messages = defineMessages({
|
|
|
73
74
|
description: 'Delete text for the voucher',
|
|
74
75
|
defaultMessage: 'Delete this voucher',
|
|
75
76
|
},
|
|
76
|
-
|
|
77
|
-
id: 'components.SaleTunnel.Information.voucher.
|
|
77
|
+
voucherErrorInvalid: {
|
|
78
|
+
id: 'components.SaleTunnel.Information.voucher.errorInvalid',
|
|
78
79
|
description: 'Error when voucher is invalid',
|
|
79
80
|
defaultMessage: 'The submitted voucher code is not valid.',
|
|
80
81
|
},
|
|
82
|
+
voucherErrorTooManyRequests: {
|
|
83
|
+
id: 'components.SaleTunnel.Information.voucher.errorTooManyRequests',
|
|
84
|
+
description: 'Error when user has tried too many vouchers',
|
|
85
|
+
defaultMessage: 'Too many attempts. Please try again later.',
|
|
86
|
+
},
|
|
81
87
|
discount: {
|
|
82
88
|
id: 'components.SaleTunnel.Information.voucher.discount',
|
|
83
89
|
description: 'Discount description',
|
|
@@ -87,7 +93,7 @@ const messages = defineMessages({
|
|
|
87
93
|
|
|
88
94
|
export const SaleTunnelInformation = () => {
|
|
89
95
|
const { props, product, voucherCode, setVoucherCode } = useSaleTunnelContext();
|
|
90
|
-
const [voucherError, setVoucherError] = useState(
|
|
96
|
+
const [voucherError, setVoucherError] = useState<HttpError | null>(null);
|
|
91
97
|
const query = usePaymentPlan({
|
|
92
98
|
course_code: props.course?.code ?? props.enrollment!.course_run.course.code,
|
|
93
99
|
product_id: props.product.id,
|
|
@@ -106,7 +112,7 @@ export const SaleTunnelInformation = () => {
|
|
|
106
112
|
useEffect(() => {
|
|
107
113
|
if (query.error && voucherCode) {
|
|
108
114
|
setVoucherCode('');
|
|
109
|
-
setVoucherError(
|
|
115
|
+
setVoucherError(query.error);
|
|
110
116
|
}
|
|
111
117
|
}, [query.error, voucherCode, setVoucherCode]);
|
|
112
118
|
|
|
@@ -205,15 +211,15 @@ const Voucher = ({
|
|
|
205
211
|
setVoucherError,
|
|
206
212
|
}: {
|
|
207
213
|
discount?: string;
|
|
208
|
-
voucherError:
|
|
209
|
-
setVoucherError: (value:
|
|
214
|
+
voucherError: HttpError | null;
|
|
215
|
+
setVoucherError: (value: HttpError | null) => void;
|
|
210
216
|
}) => {
|
|
211
217
|
const intl = useIntl();
|
|
212
218
|
const { voucherCode, setVoucherCode } = useSaleTunnelContext();
|
|
213
219
|
const [voucher, setVoucher] = useState('');
|
|
214
220
|
const handleVoucher = (e: ChangeEvent<HTMLInputElement>) => setVoucher(e.target.value);
|
|
215
221
|
const submitVoucher = () => {
|
|
216
|
-
setVoucherError(
|
|
222
|
+
setVoucherError(null);
|
|
217
223
|
setVoucherCode(voucher);
|
|
218
224
|
setVoucher('');
|
|
219
225
|
};
|
|
@@ -261,7 +267,11 @@ const Voucher = ({
|
|
|
261
267
|
)}
|
|
262
268
|
{voucherError && (
|
|
263
269
|
<Alert type={VariantType.ERROR} className="mt-s">
|
|
264
|
-
|
|
270
|
+
{voucherError.code === 429 ? (
|
|
271
|
+
<FormattedMessage {...messages.voucherErrorTooManyRequests} />
|
|
272
|
+
) : (
|
|
273
|
+
<FormattedMessage {...messages.voucherErrorInvalid} />
|
|
274
|
+
)}
|
|
265
275
|
</Alert>
|
|
266
276
|
)}
|
|
267
277
|
</div>
|
|
@@ -690,9 +690,13 @@ describe.each([
|
|
|
690
690
|
});
|
|
691
691
|
});
|
|
692
692
|
|
|
693
|
-
it('should show
|
|
693
|
+
it('should show appropriate error messages for invalid vouchers', async () => {
|
|
694
694
|
const user = userEvent.setup({ delay: null });
|
|
695
695
|
const paymentPlan = PaymentPlanFactory().one();
|
|
696
|
+
const paymentPlanVoucher = PaymentPlanFactory({
|
|
697
|
+
discounted_price: 70,
|
|
698
|
+
discount: '-30%',
|
|
699
|
+
}).one();
|
|
696
700
|
const product = ProductFactory().one();
|
|
697
701
|
fetchMock
|
|
698
702
|
.get(
|
|
@@ -711,6 +715,19 @@ describe.each([
|
|
|
711
715
|
detail: 'No Voucher matches the given query.',
|
|
712
716
|
},
|
|
713
717
|
},
|
|
718
|
+
)
|
|
719
|
+
.get(
|
|
720
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/?voucher_code=DISCOUNT29`,
|
|
721
|
+
{
|
|
722
|
+
status: 429,
|
|
723
|
+
body: {
|
|
724
|
+
detail: 'Too many attempts. Please try again later.',
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
)
|
|
728
|
+
.get(
|
|
729
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/?voucher_code=DISCOUNT10`,
|
|
730
|
+
paymentPlanVoucher,
|
|
714
731
|
);
|
|
715
732
|
render(<Wrapper paymentPlan={paymentPlan} product={product} isWithdrawable={true} />, {
|
|
716
733
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
@@ -719,5 +736,13 @@ describe.each([
|
|
|
719
736
|
await user.type(screen.getByLabelText('Voucher code'), 'DISCOUNT30');
|
|
720
737
|
await user.click(screen.getByRole('button', { name: 'Validate' }));
|
|
721
738
|
expect(await screen.findByText('The submitted voucher code is not valid.')).toBeInTheDocument();
|
|
739
|
+
await user.type(screen.getByLabelText('Voucher code'), 'DISCOUNT29');
|
|
740
|
+
await user.click(screen.getByRole('button', { name: 'Validate' }));
|
|
741
|
+
expect(
|
|
742
|
+
await screen.findByText('Too many attempts. Please try again later.'),
|
|
743
|
+
).toBeInTheDocument();
|
|
744
|
+
await user.type(screen.getByLabelText('Voucher code'), 'DISCOUNT10');
|
|
745
|
+
await user.click(screen.getByRole('button', { name: 'Validate' }));
|
|
746
|
+
expect(await screen.findByText('Discount applied')).toBeInTheDocument();
|
|
722
747
|
});
|
|
723
748
|
});
|
|
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
|
|
|
2
2
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
3
|
import { PaymentPlan } from 'types/Joanie';
|
|
4
4
|
import { Nullable } from 'types/utils';
|
|
5
|
+
import { HttpError } from 'utils/errors/HttpError';
|
|
5
6
|
|
|
6
7
|
type PaymentPlanFilters = {
|
|
7
8
|
course_code: string;
|
|
@@ -13,7 +14,7 @@ export const usePaymentPlan = (filters: PaymentPlanFilters) => {
|
|
|
13
14
|
const queryKey = ['courses-products', ...Object.values(filters), 'payment-plan'];
|
|
14
15
|
|
|
15
16
|
const api = useJoanieApi();
|
|
16
|
-
return useQuery<Nullable<PaymentPlan>,
|
|
17
|
+
return useQuery<Nullable<PaymentPlan>, HttpError>({
|
|
17
18
|
queryKey,
|
|
18
19
|
queryFn: () =>
|
|
19
20
|
api.courses.products.paymentPlan.get({
|