richie-education 3.1.3-dev12 → 3.1.3-dev15

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.
@@ -8,6 +8,7 @@ import useOpenEdxProfile from 'hooks/useOpenEdxProfile';
8
8
  import { usePaymentSchedule } from 'hooks/usePaymentSchedule';
9
9
  import { Spinner } from 'components/Spinner';
10
10
  import WithdrawRightCheckbox from 'components/SaleTunnel/WithdrawRightCheckbox';
11
+ import { ProductType } from 'types/Joanie';
11
12
 
12
13
  const messages = defineMessages({
13
14
  title: {
@@ -54,6 +55,7 @@ const messages = defineMessages({
54
55
  });
55
56
 
56
57
  export const SaleTunnelInformation = () => {
58
+ const { product } = useSaleTunnelContext();
57
59
  return (
58
60
  <div className="sale-tunnel__main__column sale-tunnel__information">
59
61
  <div>
@@ -70,7 +72,7 @@ export const SaleTunnelInformation = () => {
70
72
  </div>
71
73
  </div>
72
74
  <div>
73
- <PaymentScheduleBlock />
75
+ {product.type === ProductType.CREDENTIAL && <PaymentScheduleBlock />}
74
76
  <Total />
75
77
  <WithdrawRightCheckbox />
76
78
  </div>
@@ -108,7 +110,7 @@ const Total = () => {
108
110
  </div>
109
111
  <div className="block-title">
110
112
  <FormattedNumber
111
- value={offer?.discounted_price || product.price}
113
+ value={offer?.rules.discounted_price || product.price}
112
114
  style="currency"
113
115
  currency={product.price_currency}
114
116
  />
@@ -182,7 +182,9 @@ describe.each([
182
182
  nbApiCalls += 1; // useProductOrder call.
183
183
  nbApiCalls += 1; // get user account call.
184
184
  nbApiCalls += 1; // get user preferences call.
185
- nbApiCalls += 1; // product payment-schedule call
185
+ if (product.type === ProductType.CREDENTIAL) {
186
+ nbApiCalls += 1; // product payment-schedule call
187
+ }
186
188
  await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
187
189
 
188
190
  const user = userEvent.setup({ delay: null });
@@ -269,7 +271,9 @@ describe.each([
269
271
  nbApiCalls += 1; // useProductOrder get order with filters
270
272
  nbApiCalls += 1; // get user account call.
271
273
  nbApiCalls += 1; // get user preferences call.
272
- nbApiCalls += 1; // get product payment schedule.
274
+ if (product.type === ProductType.CREDENTIAL) {
275
+ nbApiCalls += 1; // get product payment schedule.
276
+ }
273
277
  await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
274
278
 
275
279
  const user = userEvent.setup({ delay: null });
@@ -403,36 +407,41 @@ describe.each([
403
407
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
404
408
  });
405
409
 
406
- await screen.findByRole('heading', {
407
- level: 4,
408
- name: 'Payment schedule',
409
- });
410
+ if (product.type === ProductType.CREDENTIAL) {
411
+ await screen.findByRole('heading', {
412
+ level: 4,
413
+ name: 'Payment schedule',
414
+ });
410
415
 
411
- const scheduleTable = screen.getByRole('table');
412
- const scheduleTableRows = within(scheduleTable).getAllByRole('row');
413
- expect(scheduleTableRows).toHaveLength(schedule.length);
416
+ const scheduleTable = screen.getByRole('table');
417
+ const scheduleTableRows = within(scheduleTable).getAllByRole('row');
418
+ expect(scheduleTableRows).toHaveLength(schedule.length);
414
419
 
415
- scheduleTableRows.forEach((row, index) => {
416
- const installment = schedule[index];
417
- // A first column should show the installment index
418
- within(row).getByRole('cell', {
419
- name: (index + 1).toString(),
420
- });
421
- // A 2nd column should show the installment amount
422
- within(row).getByRole('cell', {
423
- name: formatPrice(installment.amount, installment.currency),
424
- });
425
- // A 3rd column should show the installment withdraw date
426
- within(row).getByRole('cell', {
427
- name: `Withdrawn on ${intl.formatDate(installment.due_date, {
428
- ...DEFAULT_DATE_FORMAT,
429
- })}`,
430
- });
431
- // A 4th column should show the installment state
432
- within(row).getByRole('cell', {
433
- name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
420
+ scheduleTableRows.forEach((row, index) => {
421
+ const installment = schedule[index];
422
+ // A first column should show the installment index
423
+ within(row).getByRole('cell', {
424
+ name: (index + 1).toString(),
425
+ });
426
+ // A 2nd column should show the installment amount
427
+ within(row).getByRole('cell', {
428
+ name: formatPrice(installment.amount, installment.currency),
429
+ });
430
+ // A 3rd column should show the installment withdraw date
431
+ within(row).getByRole('cell', {
432
+ name: `Withdrawn on ${intl.formatDate(installment.due_date, {
433
+ ...DEFAULT_DATE_FORMAT,
434
+ })}`,
435
+ });
436
+ // A 4th column should show the installment state
437
+ within(row).getByRole('cell', {
438
+ name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
439
+ });
434
440
  });
435
- });
441
+ } else {
442
+ expect(screen.queryByRole('heading', { level: 4, name: 'Payment schedule' })).toBeNull();
443
+ expect(screen.queryByRole('table')).toBeNull();
444
+ }
436
445
 
437
446
  const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
438
447
  expect($totalAmount).toHaveTextContent(
@@ -449,8 +458,10 @@ describe.each([
449
458
  price: 840,
450
459
  price_currency: 'EUR',
451
460
  }).one(),
452
- discounted_price: 800,
453
- discount_rate: 0.3,
461
+ rules: {
462
+ discounted_price: 800,
463
+ discount_rate: 0.3,
464
+ },
454
465
  }).one();
455
466
  const { product } = offer;
456
467
 
@@ -468,41 +479,43 @@ describe.each([
468
479
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
469
480
  });
470
481
 
471
- await screen.findByRole('heading', {
472
- level: 4,
473
- name: 'Payment schedule',
474
- });
482
+ if (product.type === ProductType.CREDENTIAL) {
483
+ await screen.findByRole('heading', { level: 4, name: 'Payment schedule' });
475
484
 
476
- const scheduleTable = screen.getByRole('table');
477
- const scheduleTableRows = within(scheduleTable).getAllByRole('row');
478
- expect(scheduleTableRows).toHaveLength(schedule.length);
485
+ const scheduleTable = screen.getByRole('table');
486
+ const scheduleTableRows = within(scheduleTable).getAllByRole('row');
487
+ expect(scheduleTableRows).toHaveLength(schedule.length);
479
488
 
480
- scheduleTableRows.forEach((row, index) => {
481
- const installment = schedule[index];
482
- // A first column should show the installment index
483
- within(row).getByRole('cell', {
484
- name: (index + 1).toString(),
485
- });
486
- // A 2nd column should show the installment amount
487
- within(row).getByRole('cell', {
488
- name: formatPrice(installment.amount, installment.currency),
489
- });
490
- // A 3rd column should show the installment withdraw date
491
- within(row).getByRole('cell', {
492
- name: `Withdrawn on ${intl.formatDate(installment.due_date, {
493
- ...DEFAULT_DATE_FORMAT,
494
- })}`,
495
- });
496
- // A 4th column should show the installment state
497
- within(row).getByRole('cell', {
498
- name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
489
+ scheduleTableRows.forEach((row, index) => {
490
+ const installment = schedule[index];
491
+ // A first column should show the installment index
492
+ within(row).getByRole('cell', {
493
+ name: (index + 1).toString(),
494
+ });
495
+ // A 2nd column should show the installment amount
496
+ within(row).getByRole('cell', {
497
+ name: formatPrice(installment.amount, installment.currency),
498
+ });
499
+ // A 3rd column should show the installment withdraw date
500
+ within(row).getByRole('cell', {
501
+ name: `Withdrawn on ${intl.formatDate(installment.due_date, {
502
+ ...DEFAULT_DATE_FORMAT,
503
+ })}`,
504
+ });
505
+ // A 4th column should show the installment state
506
+ within(row).getByRole('cell', {
507
+ name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
508
+ });
499
509
  });
500
- });
510
+ } else {
511
+ expect(screen.queryByRole('heading', { level: 4, name: 'Payment schedule' })).toBeNull();
512
+ expect(screen.queryByRole('table')).toBeNull();
513
+ }
501
514
 
502
515
  const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
503
516
  expect($totalAmount).toHaveTextContent(
504
517
  'Total' +
505
- formatPrice(offer!.discounted_price!, product.price_currency).replace(
518
+ formatPrice(offer!.rules.discounted_price!, product.price_currency).replace(
506
519
  /(\u202F|\u00a0)/g,
507
520
  ' ',
508
521
  ),
@@ -182,16 +182,21 @@ export interface OfferLight {
182
182
  created_on: string;
183
183
  }
184
184
 
185
- export interface Offer extends OfferLight {
186
- is_withdrawable: boolean;
185
+ export interface OfferRule {
187
186
  discounted_price: Nullable<number>;
188
187
  discount_rate: Nullable<number>;
189
188
  discount_amount: Nullable<number>;
190
189
  discount_start: Nullable<string>;
191
190
  discount_end: Nullable<string>;
192
191
  description: Nullable<string>;
193
- nb_seats_available: Nullable<number>;
194
- seats: Nullable<number>;
192
+ nb_available_seats: Nullable<number>;
193
+ has_seat_limit: boolean;
194
+ has_seats_left: boolean;
195
+ }
196
+
197
+ export interface Offer extends OfferLight {
198
+ is_withdrawable: boolean;
199
+ rules: OfferRule;
195
200
  }
196
201
  export function isOffer(entity: CourseListItem | OfferLight | RichieCourse): entity is OfferLight {
197
202
  return 'course' in entity && 'product' in entity;
@@ -317,14 +317,17 @@ export const OfferFactory = factory((): Offer => {
317
317
  product: ProductFactory().one(),
318
318
  organizations: OrganizationFactory().many(1),
319
319
  is_withdrawable: true,
320
- discounted_price: null,
321
- discount_rate: null,
322
- discount_amount: null,
323
- discount_start: null,
324
- discount_end: null,
325
- description: null,
326
- seats: null,
327
- nb_seats_available: null,
320
+ rules: {
321
+ discounted_price: null,
322
+ discount_rate: null,
323
+ discount_amount: null,
324
+ discount_start: null,
325
+ discount_end: null,
326
+ description: null,
327
+ nb_available_seats: null,
328
+ has_seat_limit: false,
329
+ has_seats_left: true,
330
+ },
328
331
  };
329
332
  });
330
333
 
@@ -29,11 +29,7 @@ interface CourseProductItemFooterProps {
29
29
  }
30
30
 
31
31
  const CourseProductItemFooter = ({ course, offer, canPurchase }: CourseProductItemFooterProps) => {
32
- // eslint-disable-next-line @typescript-eslint/naming-convention
33
- const { seats, nb_seats_available } = offer;
34
- const hasSeatsLimit = seats && nb_seats_available !== undefined;
35
- const hasNoSeatsAvailable = hasSeatsLimit && nb_seats_available === 0;
36
- if (hasNoSeatsAvailable)
32
+ if (!offer.rules.has_seats_left)
37
33
  return (
38
34
  <p className="product-widget__footer__message">
39
35
  <FormattedMessage {...messages.noSeatsAvailable} />
@@ -50,11 +46,11 @@ const CourseProductItemFooter = ({ course, offer, canPurchase }: CourseProductIt
50
46
  disabled={!canPurchase}
51
47
  buttonProps={{ fullWidth: true }}
52
48
  />
53
- {hasSeatsLimit && (
49
+ {offer.rules.has_seat_limit && (
54
50
  <p className="product-widget__footer__message">
55
51
  <FormattedMessage
56
52
  {...messages.nbSeatsAvailable}
57
- values={{ nb: offer.nb_seats_available }}
53
+ values={{ nb: offer.rules.nb_available_seats }}
58
54
  />
59
55
  </p>
60
56
  )}
@@ -153,11 +153,13 @@ describe('CourseProductItem', () => {
153
153
  price: 840,
154
154
  price_currency: 'EUR',
155
155
  }).one(),
156
- discounted_price: 800,
157
- discount_rate: 0.3,
158
- description: 'Year 2023 discount',
159
- discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
160
- discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
156
+ rules: {
157
+ discounted_price: 800,
158
+ discount_rate: 0.3,
159
+ description: 'Year 2023 discount',
160
+ discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
161
+ discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
162
+ },
161
163
  }).one();
162
164
  const { product } = offer;
163
165
  fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
@@ -186,7 +188,7 @@ describe('CourseProductItem', () => {
186
188
  const discountedPriceLabel = screen.getByText('Discounted price:');
187
189
  expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
188
190
  const discountedPrice = screen.getByText(
189
- priceFormatter(product.price_currency, offer.discounted_price!).replace(
191
+ priceFormatter(product.price_currency, offer.rules.discounted_price!).replace(
190
192
  /(\u202F|\u00a0)/g,
191
193
  ' ',
192
194
  ),
@@ -211,11 +213,13 @@ describe('CourseProductItem', () => {
211
213
  price: 840,
212
214
  price_currency: 'EUR',
213
215
  }).one(),
214
- discounted_price: 800,
215
- discount_amount: 40,
216
- description: 'Year 2023 discount',
217
- discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
218
- discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
216
+ rules: {
217
+ discounted_price: 800,
218
+ discount_amount: 40,
219
+ description: 'Year 2023 discount',
220
+ discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
221
+ discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
222
+ },
219
223
  }).one();
220
224
  const { product } = offer;
221
225
  fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
@@ -244,7 +248,7 @@ describe('CourseProductItem', () => {
244
248
  const discountedPriceLabel = screen.getByText('Discounted price:');
245
249
  expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
246
250
  const discountedPrice = screen.getByText(
247
- priceFormatter(product.price_currency, offer.discounted_price!).replace(
251
+ priceFormatter(product.price_currency, offer.rules.discounted_price!).replace(
248
252
  /(\u202F|\u00a0)/g,
249
253
  ' ',
250
254
  ),
@@ -774,8 +778,10 @@ describe('CourseProductItem', () => {
774
778
 
775
779
  it('renders a warning message that tells that no seats are left', async () => {
776
780
  const offer = OfferFactory({
777
- seats: 2,
778
- nb_seats_available: 0,
781
+ rules: {
782
+ nb_available_seats: 0,
783
+ has_seats_left: false,
784
+ },
779
785
  }).one();
780
786
  const { product } = offer;
781
787
  const order = CredentialOrderFactory({
@@ -27,8 +27,10 @@ const render = (args: CourseProductItemProps, options?: Maybe<{ order: Credentia
27
27
  price: 840,
28
28
  price_currency: 'EUR',
29
29
  }).one(),
30
- discounted_price: 800,
31
- discount_rate: 0.3,
30
+ rules: {
31
+ discounted_price: 800,
32
+ discount_rate: 0.3,
33
+ },
32
34
  }).one(),
33
35
  { overwriteRoutes: true },
34
36
  );
@@ -103,7 +103,7 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
103
103
  return null;
104
104
  }
105
105
 
106
- if (offer.discounted_price) {
106
+ if (offer.rules.discounted_price != null) {
107
107
  return (
108
108
  <>
109
109
  <span id="original-price" className="offscreen">
@@ -122,7 +122,7 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
122
122
  <ins aria-describedby="discount-price" className="product-widget__price-discount">
123
123
  <FormattedNumber
124
124
  currency={product.price_currency}
125
- value={offer.discounted_price}
125
+ value={offer.rules.discounted_price}
126
126
  style="currency"
127
127
  />
128
128
  </ins>
@@ -133,7 +133,7 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
133
133
  return (
134
134
  <FormattedNumber currency={product.price_currency} value={product.price} style="currency" />
135
135
  );
136
- }, [canPurchase, offer.discounted_price, product.price]);
136
+ }, [canPurchase, offer.rules.discounted_price, product.price]);
137
137
 
138
138
  return (
139
139
  <header className="product-widget__header">
@@ -144,37 +144,40 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
144
144
  {hasPurchased && <FormattedMessage {...messages.purchased} />}
145
145
  {displayPrice}
146
146
  </strong>
147
- {offer?.description && (
148
- <p className="product-widget__header-description">{offer.description}</p>
147
+ {offer?.rules.description && (
148
+ <p className="product-widget__header-description">{offer.rules.description}</p>
149
149
  )}
150
- {offer?.discounted_price && (
150
+ {offer?.rules.discounted_price && (
151
151
  <p className="product-widget__header-discount">
152
- {offer.discount_rate ? (
152
+ {offer.rules.discount_rate ? (
153
153
  <span className="product-widget__header-discount-rate">
154
- <FormattedNumber value={-offer.discount_rate} style="percent" />
154
+ <FormattedNumber value={-offer.rules.discount_rate} style="percent" />
155
155
  </span>
156
156
  ) : (
157
157
  <span className="product-widget__header-discount-amount">
158
158
  <FormattedNumber
159
159
  currency={product.price_currency}
160
- value={-offer.discount_amount!}
160
+ value={-offer.rules.discount_amount!}
161
161
  style="currency"
162
162
  />
163
163
  </span>
164
164
  )}
165
- {offer.discount_start && (
165
+ {offer.rules.discount_start && (
166
166
  <span className="product-widget__header-discount-date">
167
167
  &nbsp;
168
168
  <FormattedMessage
169
169
  {...messages.from}
170
- values={{ from: formatDate(offer.discount_start) }}
170
+ values={{ from: formatDate(offer.rules.discount_start) }}
171
171
  />
172
172
  </span>
173
173
  )}
174
- {offer.discount_end && (
174
+ {offer.rules.discount_end && (
175
175
  <span className="product-widget__header-discount-date">
176
176
  &nbsp;
177
- <FormattedMessage {...messages.to} values={{ to: formatDate(offer.discount_end) }} />
177
+ <FormattedMessage
178
+ {...messages.to}
179
+ values={{ to: formatDate(offer.rules.discount_end) }}
180
+ />
178
181
  </span>
179
182
  )}
180
183
  </p>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "3.1.3-dev12",
3
+ "version": "3.1.3-dev15",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {