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
- voucherError: {
77
- id: 'components.SaleTunnel.Information.voucher.error',
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(false);
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(true);
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: boolean;
209
- setVoucherError: (value: boolean) => void;
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(false);
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
- <FormattedMessage {...messages.voucherError} />
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 an error when submit an invalid voucher', async () => {
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>, Error>({
17
+ return useQuery<Nullable<PaymentPlan>, HttpError>({
17
18
  queryKey,
18
19
  queryFn: () =>
19
20
  api.courses.products.paymentPlan.get({
@@ -31,5 +31,6 @@ export enum HttpStatusCode {
31
31
  FORBIDDEN = 403,
32
32
  NOT_FOUND = 404,
33
33
  CONFLICT = 409,
34
+ TOO_MANY_REQUESTS = 429,
34
35
  INTERNAL_SERVER_ERROR = 500,
35
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "3.2.1-dev10",
3
+ "version": "3.2.1-dev11",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {