richie-education 3.1.3-dev8 → 3.2.1-dev1

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.
Files changed (115) hide show
  1. package/.storybook/__mocks__/utils/context.ts +4 -0
  2. package/i18n/locales/ar-SA.json +30 -10
  3. package/i18n/locales/es-ES.json +30 -10
  4. package/i18n/locales/fa-IR.json +30 -10
  5. package/i18n/locales/fr-CA.json +31 -11
  6. package/i18n/locales/fr-FR.json +32 -12
  7. package/i18n/locales/ko-KR.json +30 -10
  8. package/i18n/locales/pt-PT.json +30 -10
  9. package/i18n/locales/ru-RU.json +30 -10
  10. package/i18n/locales/vi-VN.json +30 -10
  11. package/js/api/joanie.ts +8 -8
  12. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
  13. package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
  14. package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
  15. package/js/components/CourseGlimpse/index.spec.tsx +18 -0
  16. package/js/components/CourseGlimpse/index.stories.tsx +75 -4
  17. package/js/components/CourseGlimpse/index.tsx +4 -0
  18. package/js/components/CourseGlimpse/utils.ts +35 -30
  19. package/js/components/CourseGlimpseList/utils.ts +2 -2
  20. package/js/components/PurchaseButton/index.tsx +3 -3
  21. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +6 -3
  22. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
  23. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  24. package/js/components/SaleTunnel/index.spec.tsx +131 -64
  25. package/js/components/SaleTunnel/index.stories.tsx +17 -2
  26. package/js/components/SaleTunnel/index.tsx +2 -2
  27. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  28. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  29. package/js/hooks/useContractArchive/index.ts +3 -3
  30. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  31. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  32. package/js/hooks/useCourseProducts.ts +4 -8
  33. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  34. package/js/hooks/useOffering/index.ts +32 -0
  35. package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
  36. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  37. package/js/pages/DashboardCourses/index.spec.tsx +14 -14
  38. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
  39. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
  40. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  41. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  42. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  43. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  44. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  45. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  46. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  47. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  48. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  49. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  50. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
  51. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  52. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  53. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
  54. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  55. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  56. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
  57. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  58. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  59. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  60. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  61. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  62. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  63. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  64. package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
  65. package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
  66. package/js/settings/index.ts +1 -0
  67. package/js/settings/settings.prod.ts +1 -0
  68. package/js/translations/ar-SA.json +1 -1
  69. package/js/translations/es-ES.json +1 -1
  70. package/js/translations/fa-IR.json +1 -1
  71. package/js/translations/fr-CA.json +1 -1
  72. package/js/translations/fr-FR.json +1 -1
  73. package/js/translations/ko-KR.json +1 -1
  74. package/js/translations/pt-PT.json +1 -1
  75. package/js/translations/ru-RU.json +1 -1
  76. package/js/translations/vi-VN.json +1 -1
  77. package/js/types/Course.ts +4 -0
  78. package/js/types/Joanie.ts +31 -22
  79. package/js/types/index.ts +6 -2
  80. package/js/utils/test/factories/joanie.ts +18 -11
  81. package/js/utils/test/factories/richie.ts +10 -2
  82. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  83. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  84. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  85. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  86. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  87. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  88. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  89. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  90. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  91. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
  92. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
  93. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  94. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  95. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  96. package/js/widgets/Slider/components/SlidePanel.tsx +9 -0
  97. package/js/widgets/Slider/index.stories.tsx +53 -0
  98. package/js/widgets/Slider/index.tsx +21 -2
  99. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +10 -14
  100. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +8 -1
  101. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +2 -2
  102. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
  103. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +116 -75
  104. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
  105. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +29 -30
  106. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
  107. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +36 -2
  108. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +36 -2
  109. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +58 -8
  110. package/package.json +2 -1
  111. package/scss/colors/_theme.scss +3 -0
  112. package/scss/components/templates/richie/slider/_slider.scss +19 -0
  113. package/scss/objects/_blogpost_glimpses.scss +5 -0
  114. package/scss/objects/_course_glimpses.scss +16 -0
  115. package/js/hooks/useCourseProductRelation/index.ts +0 -44
@@ -8,6 +8,7 @@ import { useState } from 'react';
8
8
  import { OrderState, Product, ProductType, NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
9
9
  import {
10
10
  RichieContextFactory as mockRichieContextFactory,
11
+ CourseStateFactory,
11
12
  UserFactory,
12
13
  PacedCourseFactory,
13
14
  } from 'utils/test/factories/richie';
@@ -15,13 +16,15 @@ import {
15
16
  AddressFactory,
16
17
  CertificateOrderFactory,
17
18
  CertificateProductFactory,
18
- CourseProductRelationFactory,
19
+ OfferingFactory,
20
+ CourseRunFactory,
19
21
  CredentialOrderFactory,
20
22
  CredentialProductFactory,
21
23
  CreditCardFactory,
22
24
  EnrollmentFactory,
23
25
  PaymentInstallmentFactory,
24
26
  } from 'utils/test/factories/joanie';
27
+ import { Priority } from 'types';
25
28
  import { render } from 'utils/test/render';
26
29
  import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
27
30
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
@@ -97,7 +100,7 @@ describe.each([
97
100
  return (
98
101
  <SaleTunnel
99
102
  {...props}
100
- enrollment={enrollment}
103
+ enrollment={props.enrollment ?? enrollment}
101
104
  course={productType === ProductType.CREDENTIAL ? course : undefined}
102
105
  isOpen={open}
103
106
  onClose={() => setOpen(false)}
@@ -182,7 +185,9 @@ describe.each([
182
185
  nbApiCalls += 1; // useProductOrder call.
183
186
  nbApiCalls += 1; // get user account call.
184
187
  nbApiCalls += 1; // get user preferences call.
185
- nbApiCalls += 1; // product payment-schedule call
188
+ if (product.type === ProductType.CREDENTIAL) {
189
+ nbApiCalls += 1; // product payment-schedule call
190
+ }
186
191
  await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
187
192
 
188
193
  const user = userEvent.setup({ delay: null });
@@ -269,7 +274,9 @@ describe.each([
269
274
  nbApiCalls += 1; // useProductOrder get order with filters
270
275
  nbApiCalls += 1; // get user account call.
271
276
  nbApiCalls += 1; // get user preferences call.
272
- nbApiCalls += 1; // get product payment schedule.
277
+ if (product.type === ProductType.CREDENTIAL) {
278
+ nbApiCalls += 1; // get product payment schedule.
279
+ }
273
280
  await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
274
281
 
275
282
  const user = userEvent.setup({ delay: null });
@@ -403,36 +410,41 @@ describe.each([
403
410
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
404
411
  });
405
412
 
406
- await screen.findByRole('heading', {
407
- level: 4,
408
- name: 'Payment schedule',
409
- });
413
+ if (product.type === ProductType.CREDENTIAL) {
414
+ await screen.findByRole('heading', {
415
+ level: 4,
416
+ name: 'Payment schedule',
417
+ });
410
418
 
411
- const scheduleTable = screen.getByRole('table');
412
- const scheduleTableRows = within(scheduleTable).getAllByRole('row');
413
- expect(scheduleTableRows).toHaveLength(schedule.length);
419
+ const scheduleTable = screen.getByRole('table');
420
+ const scheduleTableRows = within(scheduleTable).getAllByRole('row');
421
+ expect(scheduleTableRows).toHaveLength(schedule.length);
414
422
 
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('_', ' '))!,
423
+ scheduleTableRows.forEach((row, index) => {
424
+ const installment = schedule[index];
425
+ // A first column should show the installment index
426
+ within(row).getByRole('cell', {
427
+ name: (index + 1).toString(),
428
+ });
429
+ // A 2nd column should show the installment amount
430
+ within(row).getByRole('cell', {
431
+ name: formatPrice(installment.amount, installment.currency),
432
+ });
433
+ // A 3rd column should show the installment withdraw date
434
+ within(row).getByRole('cell', {
435
+ name: `Withdrawn on ${intl.formatDate(installment.due_date, {
436
+ ...DEFAULT_DATE_FORMAT,
437
+ })}`,
438
+ });
439
+ // A 4th column should show the installment state
440
+ within(row).getByRole('cell', {
441
+ name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
442
+ });
434
443
  });
435
- });
444
+ } else {
445
+ expect(screen.queryByRole('heading', { level: 4, name: 'Payment schedule' })).toBeNull();
446
+ expect(screen.queryByRole('table')).toBeNull();
447
+ }
436
448
 
437
449
  const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
438
450
  expect($totalAmount).toHaveTextContent(
@@ -440,19 +452,72 @@ describe.each([
440
452
  );
441
453
  });
442
454
 
455
+ // Fixes the issue : https://github.com/openfun/richie/issues/2645
456
+ it('should show the certificate product total with discounted price', async () => {
457
+ const product = ProductFactory({
458
+ price: 600,
459
+ target_courses: [course],
460
+ }).one();
461
+ const enrollmentDiscounted = EnrollmentFactory({
462
+ course_run: CourseRunFactory({
463
+ state: CourseStateFactory({ priority: Priority.ONGOING_OPEN }).one(),
464
+ course,
465
+ }).one(),
466
+ offerings: [
467
+ OfferingFactory({
468
+ product,
469
+ rules: {
470
+ discounted_price: 540,
471
+ discount_rate: 0.1,
472
+ },
473
+ }).one(),
474
+ ],
475
+ }).one();
476
+
477
+ if (product.type === ProductType.CERTIFICATE) {
478
+ enrollmentDiscounted.offerings[0].product = product;
479
+
480
+ fetchMock.get(
481
+ `https://joanie.endpoint/api/v1.0/orders/?enrollment_id=${enrollmentDiscounted.id}&product_id=${product.id}&state=pending&state=pending_payment&state=no_payment&state=failed_payment&state=completed&state=draft&state=assigned&state=to_sign&state=signing&state=to_save_payment_method`,
482
+ {
483
+ results: [],
484
+ next: null,
485
+ previous: null,
486
+ count: 0,
487
+ },
488
+ );
489
+
490
+ render(
491
+ <Wrapper product={product} enrollment={enrollmentDiscounted} isWithdrawable={true} />,
492
+ { queryOptions: { client: createTestQueryClient({ user: richieUser }) } },
493
+ );
494
+
495
+ const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
496
+ expect($totalAmount).toHaveTextContent(
497
+ 'Total' +
498
+ formatPrice(
499
+ enrollmentDiscounted.offerings[0].rules?.discounted_price!,
500
+ product.price_currency,
501
+ ).replace(/(\u202F|\u00a0)/g, ' '),
502
+ );
503
+ }
504
+ });
505
+
443
506
  it('should show the product payment schedule with discounted price', async () => {
444
507
  const intl = createIntl({ locale: 'en' });
445
508
  const schedule = PaymentInstallmentFactory().many(2);
446
509
 
447
- const relation = CourseProductRelationFactory({
510
+ const offering = OfferingFactory({
448
511
  product: ProductFactory({
449
512
  price: 840,
450
513
  price_currency: 'EUR',
451
514
  }).one(),
452
- discounted_price: 800,
453
- discount_rate: 0.3,
515
+ rules: {
516
+ discounted_price: 800,
517
+ discount_rate: 0.3,
518
+ },
454
519
  }).one();
455
- const { product } = relation;
520
+ const { product } = offering;
456
521
 
457
522
  fetchMock
458
523
  .get(
@@ -464,45 +529,47 @@ describe.each([
464
529
  schedule,
465
530
  );
466
531
 
467
- render(<Wrapper product={product} courseProductRelation={relation} isWithdrawable={true} />, {
532
+ render(<Wrapper product={product} offering={offering} isWithdrawable={true} />, {
468
533
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
469
534
  });
470
535
 
471
- await screen.findByRole('heading', {
472
- level: 4,
473
- name: 'Payment schedule',
474
- });
536
+ if (product.type === ProductType.CREDENTIAL) {
537
+ await screen.findByRole('heading', { level: 4, name: 'Payment schedule' });
475
538
 
476
- const scheduleTable = screen.getByRole('table');
477
- const scheduleTableRows = within(scheduleTable).getAllByRole('row');
478
- expect(scheduleTableRows).toHaveLength(schedule.length);
539
+ const scheduleTable = screen.getByRole('table');
540
+ const scheduleTableRows = within(scheduleTable).getAllByRole('row');
541
+ expect(scheduleTableRows).toHaveLength(schedule.length);
479
542
 
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('_', ' '))!,
543
+ scheduleTableRows.forEach((row, index) => {
544
+ const installment = schedule[index];
545
+ // A first column should show the installment index
546
+ within(row).getByRole('cell', {
547
+ name: (index + 1).toString(),
548
+ });
549
+ // A 2nd column should show the installment amount
550
+ within(row).getByRole('cell', {
551
+ name: formatPrice(installment.amount, installment.currency),
552
+ });
553
+ // A 3rd column should show the installment withdraw date
554
+ within(row).getByRole('cell', {
555
+ name: `Withdrawn on ${intl.formatDate(installment.due_date, {
556
+ ...DEFAULT_DATE_FORMAT,
557
+ })}`,
558
+ });
559
+ // A 4th column should show the installment state
560
+ within(row).getByRole('cell', {
561
+ name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
562
+ });
499
563
  });
500
- });
564
+ } else {
565
+ expect(screen.queryByRole('heading', { level: 4, name: 'Payment schedule' })).toBeNull();
566
+ expect(screen.queryByRole('table')).toBeNull();
567
+ }
501
568
 
502
569
  const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
503
570
  expect($totalAmount).toHaveTextContent(
504
571
  'Total' +
505
- formatPrice(relation!.discounted_price!, product.price_currency).replace(
572
+ formatPrice(offering!.rules!.discounted_price!, product.price_currency).replace(
506
573
  /(\u202F|\u00a0)/g,
507
574
  ' ',
508
575
  ),
@@ -1,6 +1,11 @@
1
1
  import { StoryObj, Meta } from '@storybook/react';
2
2
  import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
3
- import { ProductFactory } from 'utils/test/factories/joanie';
3
+ import {
4
+ CertificateProductFactory,
5
+ EnrollmentFactory,
6
+ OfferingFactory,
7
+ ProductFactory,
8
+ } from 'utils/test/factories/joanie';
4
9
  import { PacedCourseFactory } from 'utils/test/factories/richie';
5
10
  import { SaleTunnel, SaleTunnelProps } from './index';
6
11
 
@@ -28,6 +33,16 @@ export default {
28
33
 
29
34
  type Story = StoryObj<typeof SaleTunnel>;
30
35
 
31
- export const Default: Story = {
36
+ export const Credential: Story = {
32
37
  args: {},
33
38
  };
39
+
40
+ export const CertificateDiscount: Story = {
41
+ args: {
42
+ product: CertificateProductFactory({ price: 100, price_currency: 'EUR' }).one(),
43
+ course: PacedCourseFactory().one(),
44
+ enrollment: EnrollmentFactory({
45
+ offerings: OfferingFactory({ rules: { discounted_price: 80 } }).many(1),
46
+ }).one(),
47
+ },
48
+ };
@@ -2,7 +2,7 @@ import { ModalProps } from '@openfun/cunningham-react';
2
2
  import {
3
3
  CertificateProduct,
4
4
  CourseLight,
5
- CourseProductRelation,
5
+ Offering,
6
6
  CredentialProduct,
7
7
  Enrollment,
8
8
  Order,
@@ -16,7 +16,7 @@ import { PacedCourse } from 'types';
16
16
 
17
17
  export interface SaleTunnelProps extends Pick<ModalProps, 'isOpen' | 'onClose'> {
18
18
  product: Product;
19
- courseProductRelation?: CourseProductRelation;
19
+ offering?: Offering;
20
20
  organizations?: Organization[];
21
21
  isWithdrawable: boolean;
22
22
  course?: PacedCourse | CourseLight;
@@ -1,7 +1,7 @@
1
1
  import { screen } from '@testing-library/react';
2
2
  import fetchMock from 'fetch-mock';
3
3
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
4
- import { CourseListItemFactory, CourseProductRelationFactory } from 'utils/test/factories/joanie';
4
+ import { CourseListItemFactory, OfferingFactory } from 'utils/test/factories/joanie';
5
5
  import { render } from 'utils/test/render';
6
6
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
7
7
  import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
@@ -35,7 +35,7 @@ describe('components/TeacherDashboardCourseList', () => {
35
35
  });
36
36
 
37
37
  it('should render loading more state', async () => {
38
- const trainings = CourseProductRelationFactory().many(2);
38
+ const trainings = OfferingFactory().many(2);
39
39
  const courses = CourseListItemFactory().many(2);
40
40
  const courseAndProductList = [...courses, ...trainings];
41
41
 
@@ -60,7 +60,7 @@ describe('components/TeacherDashboardCourseList', () => {
60
60
  });
61
61
 
62
62
  it('should render courses and products list', async () => {
63
- const trainings = CourseProductRelationFactory().many(2);
63
+ const trainings = OfferingFactory().many(2);
64
64
  const courses = CourseListItemFactory().many(2);
65
65
  const courseAndProductList = [...courses, ...trainings];
66
66
 
@@ -6,7 +6,7 @@ import { CourseGlimpseList, getCourseGlimpseListProps } from 'components/CourseG
6
6
  import { Spinner } from 'components/Spinner';
7
7
  import context from 'utils/context';
8
8
  import { useIntersectionObserver } from 'hooks/useIntersectionObserver';
9
- import { CourseListItem, CourseProductRelationLight } from 'types/Joanie';
9
+ import { CourseListItem, OfferingLight } from 'types/Joanie';
10
10
  import Banner from 'components/Banner';
11
11
 
12
12
  const messages = defineMessages({
@@ -31,7 +31,7 @@ interface TeacherDashboardCourseListProps {
31
31
  titleTranslated?: string;
32
32
  organizationId?: string;
33
33
  loadMore: () => void;
34
- courseAndProductList?: (CourseListItem | CourseProductRelationLight)[];
34
+ courseAndProductList?: (CourseListItem | OfferingLight)[];
35
35
  isLoadingMore?: boolean;
36
36
  hasMore?: boolean;
37
37
  isNewSearchLoading?: boolean;
@@ -1,5 +1,5 @@
1
1
  import { useJoanieApi } from 'contexts/JoanieApiContext';
2
- import { CourseProductRelation, Organization } from 'types/Joanie';
2
+ import { Offering, Organization } from 'types/Joanie';
3
3
  import { browserDownloadFromBlob } from 'utils/download';
4
4
  import { HttpStatusCode } from 'utils/errors/HttpError';
5
5
  import { handle } from 'utils/errors/handle';
@@ -53,11 +53,11 @@ const useContractArchive = () => {
53
53
  },
54
54
  create: async (
55
55
  organizationId?: Organization['id'],
56
- courseProductRelationId?: CourseProductRelation['id'],
56
+ offeringId?: Offering['id'],
57
57
  ): Promise<string> => {
58
58
  const response = await api.user.contracts.zip_archive.create({
59
59
  organization_id: organizationId,
60
- course_product_relation_id: courseProductRelationId,
60
+ offering_id: offeringId,
61
61
  });
62
62
 
63
63
  return extractArchiveId(response.url);
@@ -2,13 +2,13 @@ import { renderHook, waitFor } from '@testing-library/react';
2
2
  import { QueryClient } from '@tanstack/react-query';
3
3
  import fetchMock from 'fetch-mock';
4
4
  import { PropsWithChildren } from 'react';
5
- import { CourseListItem, CourseProductRelation } from 'types/Joanie';
5
+ import { CourseListItem, Offering } from 'types/Joanie';
6
6
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
7
7
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
8
8
  import { SessionProvider } from 'contexts/SessionContext';
9
9
  import { getRoutes } from 'api/joanie';
10
10
  import { mockPaginatedResponse } from 'utils/test/mockPaginatedResponse';
11
- import { CourseListItemFactory, CourseProductRelationFactory } from 'utils/test/factories/joanie';
11
+ import { CourseListItemFactory, OfferingFactory } from 'utils/test/factories/joanie';
12
12
  import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
13
13
  import { useCourseProductUnion } from '.';
14
14
 
@@ -41,12 +41,12 @@ const renderUseCourseProductUnion = ({ organizationId }: { organizationId?: stri
41
41
 
42
42
  describe('useCourseProductUnion', () => {
43
43
  let courseList: CourseListItem[];
44
- let courseProductRelationList: CourseProductRelation[];
44
+ let offeringList: Offering[];
45
45
  let nbApiCalls: number;
46
46
 
47
47
  beforeEach(() => {
48
48
  courseList = CourseListItemFactory().many(6);
49
- courseProductRelationList = CourseProductRelationFactory().many(6);
49
+ offeringList = OfferingFactory().many(6);
50
50
 
51
51
  fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', [], { overwriteRoutes: true });
52
52
  fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', [], { overwriteRoutes: true });
@@ -59,38 +59,38 @@ describe('useCourseProductUnion', () => {
59
59
  fetchMock.restore();
60
60
  });
61
61
 
62
- it('should call courses and coursesProductRelation endpoints', async () => {
62
+ it('should call courses and offering endpoints', async () => {
63
63
  const ROUTES = getRoutes();
64
64
  const coursesUrl = ROUTES.courses.get.replace(':id/', '');
65
- const courseProductRelationsUrl = ROUTES.courseProductRelations.get.replace(':id/', '');
65
+ const offeringsUrl = ROUTES.offerings.get.replace(':id/', '');
66
66
  fetchMock.get(
67
67
  `${coursesUrl}?has_listed_course_runs=true&page=1&page_size=${PER_PAGE}`,
68
68
  mockPaginatedResponse(courseList.slice(0, PER_PAGE), 0, false),
69
69
  );
70
70
  fetchMock.get(
71
- `${courseProductRelationsUrl}?page=1&page_size=${PER_PAGE}`,
72
- mockPaginatedResponse(courseProductRelationList.slice(0, PER_PAGE), 0, false),
71
+ `${offeringsUrl}?page=1&page_size=${PER_PAGE}`,
72
+ mockPaginatedResponse(offeringList.slice(0, PER_PAGE), 0, false),
73
73
  );
74
74
  const { result } = renderUseCourseProductUnion();
75
75
  await waitFor(() => expect(result.current.isLoading).toBe(false));
76
76
  expect(result.current.data.length).toBe(PER_PAGE);
77
77
  nbApiCalls += 1; // courses page 1
78
- nbApiCalls += 1; // course product relations page 1
78
+ nbApiCalls += 1; // offerings page 1
79
79
  const calledUrls = fetchMock.calls().map((call) => call[0]);
80
80
  expect(calledUrls).toHaveLength(nbApiCalls);
81
81
  expect(calledUrls).toContain(
82
82
  `${coursesUrl}?has_listed_course_runs=true&page=1&page_size=${PER_PAGE}`,
83
83
  );
84
- expect(calledUrls).toContain(`${courseProductRelationsUrl}?page=1&page_size=${PER_PAGE}`);
84
+ expect(calledUrls).toContain(`${offeringsUrl}?page=1&page_size=${PER_PAGE}`);
85
85
  }, 25000);
86
86
 
87
- it('should call organization courses and organization coursesProductRelation endpoints', async () => {
87
+ it('should call organization courses and organization offering endpoints', async () => {
88
88
  const organizationId = 'DUMMY_ORGANIZATION_ID';
89
89
  const ROUTES = getRoutes();
90
90
  const organizationCoursesUrl = ROUTES.organizations.courses.get
91
91
  .replace(':organization_id', organizationId)
92
92
  .replace(':id/', '');
93
- const organizationCourseProductRelationsUrl = ROUTES.organizations.courseProductRelations.get
93
+ const organizationOfferingsUrl = ROUTES.organizations.offerings.get
94
94
  .replace(':organization_id', organizationId)
95
95
  .replace(':id/', '');
96
96
  fetchMock.get(
@@ -98,21 +98,19 @@ describe('useCourseProductUnion', () => {
98
98
  mockPaginatedResponse(courseList.slice(0, PER_PAGE), 0, false),
99
99
  );
100
100
  fetchMock.get(
101
- `${organizationCourseProductRelationsUrl}?page=1&page_size=${PER_PAGE}`,
102
- mockPaginatedResponse(courseProductRelationList.slice(0, PER_PAGE), 0, false),
101
+ `${organizationOfferingsUrl}?page=1&page_size=${PER_PAGE}`,
102
+ mockPaginatedResponse(offeringList.slice(0, PER_PAGE), 0, false),
103
103
  );
104
104
  const { result } = renderUseCourseProductUnion({ organizationId: 'DUMMY_ORGANIZATION_ID' });
105
105
  await waitFor(() => expect(result.current.isLoading).toBe(false));
106
106
  expect(result.current.data.length).toBe(PER_PAGE);
107
107
  nbApiCalls += 1; // courses page 1
108
- nbApiCalls += 1; // course product relations page 1
108
+ nbApiCalls += 1; // offerings page 1
109
109
  const calledUrls = fetchMock.calls().map((call) => call[0]);
110
110
  expect(calledUrls).toHaveLength(nbApiCalls);
111
111
  expect(calledUrls).toContain(
112
112
  `${organizationCoursesUrl}?has_listed_course_runs=true&page=1&page_size=${PER_PAGE}`,
113
113
  );
114
- expect(calledUrls).toContain(
115
- `${organizationCourseProductRelationsUrl}?page=1&page_size=${PER_PAGE}`,
116
- );
114
+ expect(calledUrls).toContain(`${organizationOfferingsUrl}?page=1&page_size=${PER_PAGE}`);
117
115
  });
118
116
  });
@@ -3,11 +3,11 @@ import { useJoanieApi } from 'contexts/JoanieApiContext';
3
3
  import {
4
4
  CourseListItem,
5
5
  Product,
6
- CourseProductRelation,
6
+ Offering,
7
7
  CourseQueryFilters,
8
- CourseProductRelationQueryFilters,
8
+ OfferingQueryFilters,
9
9
  ProductType,
10
- CourseProductRelationLight,
10
+ OfferingLight,
11
11
  } from 'types/Joanie';
12
12
  import useUnionResource, { ResourceUnionPaginationProps } from 'hooks/useUnionResource';
13
13
 
@@ -41,9 +41,9 @@ export const useCourseProductUnion = ({
41
41
  const api = useJoanieApi();
42
42
  return useUnionResource<
43
43
  CourseListItem,
44
- CourseProductRelation | CourseProductRelationLight,
44
+ Offering | OfferingLight,
45
45
  CourseQueryFilters,
46
- CourseProductRelationQueryFilters
46
+ OfferingQueryFilters
47
47
  >({
48
48
  queryAConfig: {
49
49
  queryKey: ['user', 'courses'],
@@ -51,8 +51,8 @@ export const useCourseProductUnion = ({
51
51
  filters: { query, organization_id: organizationId, has_listed_course_runs: true },
52
52
  },
53
53
  queryBConfig: {
54
- queryKey: ['user', 'course_product_relations'],
55
- fn: api.courseProductRelations.get,
54
+ queryKey: ['user', 'offerings'],
55
+ fn: api.offerings.get,
56
56
  filters: { query, organization_id: organizationId, product_type: productType },
57
57
  },
58
58
  perPage,
@@ -1,5 +1,5 @@
1
1
  import { defineMessages } from 'react-intl';
2
- import { API, CourseProductQueryFilters, CourseProductRelation, Product } from 'types/Joanie';
2
+ import { API, CourseProductQueryFilters, Offering, Product } from 'types/Joanie';
3
3
  import { QueryOptions, useResourcesCustom, UseResourcesProps } from 'hooks/useResources';
4
4
  import { useJoanieApi } from 'contexts/JoanieApiContext';
5
5
 
@@ -19,11 +19,7 @@ export const messages = defineMessages({
19
19
  /**
20
20
  * Joanie Api hook to retrieve a product through its id and a course code.
21
21
  */
22
- const props: UseResourcesProps<
23
- CourseProductRelation,
24
- CourseProductQueryFilters,
25
- API['courses']['products']
26
- > = {
22
+ const props: UseResourcesProps<Offering, CourseProductQueryFilters, API['courses']['products']> = {
27
23
  queryKey: ['courses-products'],
28
24
  apiInterface: () => useJoanieApi().courses.products,
29
25
  messages,
@@ -31,11 +27,11 @@ const props: UseResourcesProps<
31
27
 
32
28
  export const useCourseProduct = (
33
29
  filters: Omit<CourseProductQueryFilters, 'id'> & { product_id: Product['id'] },
34
- queryOptions?: QueryOptions<CourseProductRelation>,
30
+ queryOptions?: QueryOptions<Offering>,
35
31
  ) => {
36
32
  const { product_id: productId, ...queryfilters } = filters;
37
33
  const enabled = !!productId && !!queryfilters.course_id;
38
- const resources = useResourcesCustom<CourseProductRelation, CourseProductQueryFilters>({
34
+ const resources = useResourcesCustom<Offering, CourseProductQueryFilters>({
39
35
  ...props,
40
36
  filters: { id: productId, ...queryfilters },
41
37
  queryOptions: { ...queryOptions, enabled },
@@ -1,6 +1,6 @@
1
1
  import { useParams, useSearchParams } from 'react-router';
2
2
  import { useOrganizations } from 'hooks/useOrganizations';
3
- import { CourseProductRelation, Organization } from 'types/Joanie';
3
+ import { Offering, Organization } from 'types/Joanie';
4
4
 
5
5
  /**
6
6
  * return organization id with this priority:
@@ -9,17 +9,14 @@ import { CourseProductRelation, Organization } from 'types/Joanie';
9
9
  * * first organization of user's organizations
10
10
  */
11
11
  const useDefaultOrganizationId = () => {
12
- const {
13
- organizationId: routeOrganizationId,
14
- courseProductRelationId: routeCourseProductRelationId,
15
- } = useParams<{
12
+ const { organizationId: routeOrganizationId, offeringId: routeOfferingId } = useParams<{
16
13
  organizationId?: Organization['id'];
17
- courseProductRelationId: CourseProductRelation['id'];
14
+ offeringId: Offering['id'];
18
15
  }>();
19
16
  const [searchParams] = useSearchParams();
20
17
  const queryOrganizationId = searchParams.get('organization_id') || undefined;
21
18
  const { items: organizations } = useOrganizations(
22
- { course_product_relation_id: routeCourseProductRelationId },
19
+ { offering_id: routeOfferingId },
23
20
  {
24
21
  enabled: !routeOrganizationId && !queryOrganizationId,
25
22
  },
@@ -0,0 +1,32 @@
1
+ import { defineMessages } from 'react-intl';
2
+ import { useJoanieApi } from 'contexts/JoanieApiContext';
3
+ import { API, Offering, OfferingQueryFilters } from 'types/Joanie';
4
+ import { useResource, useResources, UseResourcesProps } from 'hooks/useResources';
5
+
6
+ const messages = defineMessages({
7
+ errorGet: {
8
+ id: 'hooks.useOfferings.errorGet',
9
+ description: 'Error message shown to the user when offering fetch request fails.',
10
+ defaultMessage: 'An error occurred while fetching trainings. Please retry later.',
11
+ },
12
+ errorNotFound: {
13
+ id: 'hooks.useOfferings.errorNotFound',
14
+ description: 'Error message shown to the user when no offering matches.',
15
+ defaultMessage: 'Cannot find the training.',
16
+ },
17
+ });
18
+
19
+ /**
20
+ * Joanie Api hook to retrieve/create/update/delete course
21
+ * owned by the authenticated user.
22
+ */
23
+ const props: UseResourcesProps<Offering, OfferingQueryFilters, API['offerings']> = {
24
+ queryKey: ['offerings'],
25
+ apiInterface: () => useJoanieApi().offerings,
26
+ session: true,
27
+ messages,
28
+ };
29
+
30
+ export const useOfferings = useResources<Offering, OfferingQueryFilters, API['offerings']>(props);
31
+
32
+ export const useOffering = useResource<Offering, OfferingQueryFilters>(props);
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useParams, useSearchParams } from 'react-router';
3
3
  import { useCourseProductUnion } from 'hooks/useCourseProductUnion';
4
- import { CourseListItem, CourseProductRelationLight, ProductType } from 'types/Joanie';
4
+ import { CourseListItem, OfferingLight, ProductType } from 'types/Joanie';
5
5
  import { Maybe, Nullable } from 'types/utils';
6
6
 
7
7
  const useTeacherCoursesSearch = () => {
@@ -9,7 +9,7 @@ const useTeacherCoursesSearch = () => {
9
9
  const [searchParams, setSearchParams] = useSearchParams();
10
10
  const [count, setCount] = useState<Maybe<number>>(0);
11
11
  const [courseAndProductList, setCourseAndProductList] = useState<
12
- (CourseListItem | CourseProductRelationLight)[]
12
+ (CourseListItem | OfferingLight)[]
13
13
  >([]);
14
14
  const [isNewSearchLoading, setIsNewSearchLoading] = useState(false);
15
15
  const query = searchParams.get('query') || undefined;