richie-education 3.1.3-dev3 → 3.1.3-dev31

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 (93) hide show
  1. package/.storybook/__mocks__/utils/context.ts +4 -0
  2. package/js/api/joanie.ts +8 -8
  3. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -19
  4. package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
  5. package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
  6. package/js/components/CourseGlimpse/index.spec.tsx +18 -0
  7. package/js/components/CourseGlimpse/index.stories.tsx +75 -4
  8. package/js/components/CourseGlimpse/index.tsx +4 -0
  9. package/js/components/CourseGlimpse/utils.ts +35 -30
  10. package/js/components/CourseGlimpseList/utils.ts +2 -2
  11. package/js/components/PurchaseButton/index.tsx +3 -3
  12. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +1 -3
  13. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +13 -1
  14. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
  15. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
  16. package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
  17. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  18. package/js/components/SaleTunnel/index.spec.tsx +171 -29
  19. package/js/components/SaleTunnel/index.stories.tsx +17 -3
  20. package/js/components/SaleTunnel/index.tsx +2 -2
  21. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  22. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  23. package/js/hooks/useContractArchive/index.ts +3 -3
  24. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  25. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  26. package/js/hooks/useCourseProducts.ts +4 -8
  27. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  28. package/js/hooks/useOffering/index.ts +32 -0
  29. package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
  30. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  31. package/js/pages/DashboardCourses/index.spec.tsx +14 -14
  32. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
  33. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
  34. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  35. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  36. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  37. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  38. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  39. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  40. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  41. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  42. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  43. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  44. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
  45. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  46. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  47. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
  48. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  49. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  50. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
  51. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  52. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  53. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  54. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  55. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  56. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  57. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  58. package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
  59. package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
  60. package/js/types/Course.ts +4 -0
  61. package/js/types/Joanie.ts +36 -29
  62. package/js/types/index.ts +6 -2
  63. package/js/utils/ProductHelper/index.ts +1 -5
  64. package/js/utils/test/factories/joanie.ts +19 -25
  65. package/js/utils/test/factories/richie.ts +10 -2
  66. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  67. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  68. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  69. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  70. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  71. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  72. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  73. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  74. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  75. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
  76. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
  77. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  78. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  79. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  80. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +19 -34
  81. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +35 -8
  82. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +3 -3
  83. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
  84. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +186 -140
  85. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +11 -2
  86. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +111 -24
  87. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
  88. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +14 -0
  89. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +14 -0
  90. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +54 -8
  91. package/package.json +1 -1
  92. package/scss/objects/_course_glimpses.scss +16 -0
  93. package/js/hooks/useCourseProductRelation/index.ts +0 -44
@@ -15,7 +15,7 @@ import CourseProductItem from 'widgets/SyllabusCourseRunsList/components/CourseP
15
15
  import {
16
16
  AddressFactory,
17
17
  ContractFactory,
18
- CourseProductRelationFactory,
18
+ OfferingFactory,
19
19
  CredentialOrderFactory,
20
20
  CreditCardFactory,
21
21
  PaymentFactory,
@@ -99,7 +99,7 @@ describe('SaleTunnel', () => {
99
99
  */
100
100
  const course = PacedCourseFactory().one();
101
101
  const product = ProductFactory().one();
102
- const relation = CourseProductRelationFactory({
102
+ const offering = OfferingFactory({
103
103
  course,
104
104
  product,
105
105
  is_withdrawable: false,
@@ -108,7 +108,7 @@ describe('SaleTunnel', () => {
108
108
 
109
109
  fetchMock.get(
110
110
  `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/`,
111
- relation,
111
+ offering,
112
112
  );
113
113
  fetchMock.get(
114
114
  `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
@@ -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,12 +16,15 @@ import {
15
16
  AddressFactory,
16
17
  CertificateOrderFactory,
17
18
  CertificateProductFactory,
19
+ OfferingFactory,
20
+ CourseRunFactory,
18
21
  CredentialOrderFactory,
19
22
  CredentialProductFactory,
20
23
  CreditCardFactory,
21
24
  EnrollmentFactory,
22
25
  PaymentInstallmentFactory,
23
26
  } from 'utils/test/factories/joanie';
27
+ import { Priority } from 'types';
24
28
  import { render } from 'utils/test/render';
25
29
  import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
26
30
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
@@ -96,7 +100,7 @@ describe.each([
96
100
  return (
97
101
  <SaleTunnel
98
102
  {...props}
99
- enrollment={enrollment}
103
+ enrollment={props.enrollment ?? enrollment}
100
104
  course={productType === ProductType.CREDENTIAL ? course : undefined}
101
105
  isOpen={open}
102
106
  onClose={() => setOpen(false)}
@@ -181,7 +185,9 @@ describe.each([
181
185
  nbApiCalls += 1; // useProductOrder call.
182
186
  nbApiCalls += 1; // get user account call.
183
187
  nbApiCalls += 1; // get user preferences call.
184
- nbApiCalls += 1; // product payment-schedule call
188
+ if (product.type === ProductType.CREDENTIAL) {
189
+ nbApiCalls += 1; // product payment-schedule call
190
+ }
185
191
  await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
186
192
 
187
193
  const user = userEvent.setup({ delay: null });
@@ -268,7 +274,9 @@ describe.each([
268
274
  nbApiCalls += 1; // useProductOrder get order with filters
269
275
  nbApiCalls += 1; // get user account call.
270
276
  nbApiCalls += 1; // get user preferences call.
271
- nbApiCalls += 1; // get product payment schedule.
277
+ if (product.type === ProductType.CREDENTIAL) {
278
+ nbApiCalls += 1; // get product payment schedule.
279
+ }
272
280
  await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
273
281
 
274
282
  const user = userEvent.setup({ delay: null });
@@ -402,36 +410,170 @@ describe.each([
402
410
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
403
411
  });
404
412
 
405
- await screen.findByRole('heading', {
406
- level: 4,
407
- name: 'Payment schedule',
408
- });
413
+ if (product.type === ProductType.CREDENTIAL) {
414
+ await screen.findByRole('heading', {
415
+ level: 4,
416
+ name: 'Payment schedule',
417
+ });
409
418
 
410
- const scheduleTable = screen.getByRole('table');
411
- const scheduleTableRows = within(scheduleTable).getAllByRole('row');
412
- expect(scheduleTableRows).toHaveLength(schedule.length);
419
+ const scheduleTable = screen.getByRole('table');
420
+ const scheduleTableRows = within(scheduleTable).getAllByRole('row');
421
+ expect(scheduleTableRows).toHaveLength(schedule.length);
413
422
 
414
- scheduleTableRows.forEach((row, index) => {
415
- const installment = schedule[index];
416
- // A first column should show the installment index
417
- within(row).getByRole('cell', {
418
- name: (index + 1).toString(),
419
- });
420
- // A 2nd column should show the installment amount
421
- within(row).getByRole('cell', {
422
- name: formatPrice(installment.amount, installment.currency),
423
- });
424
- // A 3rd column should show the installment withdraw date
425
- within(row).getByRole('cell', {
426
- name: `Withdrawn on ${intl.formatDate(installment.due_date, {
427
- ...DEFAULT_DATE_FORMAT,
428
- })}`,
429
- });
430
- // A 4th column should show the installment state
431
- within(row).getByRole('cell', {
432
- 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
+ });
433
443
  });
444
+ } else {
445
+ expect(screen.queryByRole('heading', { level: 4, name: 'Payment schedule' })).toBeNull();
446
+ expect(screen.queryByRole('table')).toBeNull();
447
+ }
448
+
449
+ const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
450
+ expect($totalAmount).toHaveTextContent(
451
+ 'Total' + formatPrice(product.price, product.price_currency).replace(/(\u202F|\u00a0)/g, ' '),
452
+ );
453
+ });
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
+
506
+ it('should show the product payment schedule with discounted price', async () => {
507
+ const intl = createIntl({ locale: 'en' });
508
+ const schedule = PaymentInstallmentFactory().many(2);
509
+
510
+ const offering = OfferingFactory({
511
+ product: ProductFactory({
512
+ price: 840,
513
+ price_currency: 'EUR',
514
+ }).one(),
515
+ rules: {
516
+ discounted_price: 800,
517
+ discount_rate: 0.3,
518
+ },
519
+ }).one();
520
+ const { product } = offering;
521
+
522
+ fetchMock
523
+ .get(
524
+ `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
525
+ [],
526
+ )
527
+ .get(
528
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
529
+ schedule,
530
+ );
531
+
532
+ render(<Wrapper product={product} offering={offering} isWithdrawable={true} />, {
533
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
434
534
  });
535
+
536
+ if (product.type === ProductType.CREDENTIAL) {
537
+ await screen.findByRole('heading', { level: 4, name: 'Payment schedule' });
538
+
539
+ const scheduleTable = screen.getByRole('table');
540
+ const scheduleTableRows = within(scheduleTable).getAllByRole('row');
541
+ expect(scheduleTableRows).toHaveLength(schedule.length);
542
+
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
+ });
563
+ });
564
+ } else {
565
+ expect(screen.queryByRole('heading', { level: 4, name: 'Payment schedule' })).toBeNull();
566
+ expect(screen.queryByRole('table')).toBeNull();
567
+ }
568
+
569
+ const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
570
+ expect($totalAmount).toHaveTextContent(
571
+ 'Total' +
572
+ formatPrice(offering!.rules.discounted_price!, product.price_currency).replace(
573
+ /(\u202F|\u00a0)/g,
574
+ ' ',
575
+ ),
576
+ );
435
577
  });
436
578
 
437
579
  it('should show a walkthrough to explain the subscription process', async () => {
@@ -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
 
@@ -15,7 +20,6 @@ export default {
15
20
  isWithdrawable: true,
16
21
  // enrollment?: Enrollment;
17
22
  // product: CredentialProduct | CertificateProduct;
18
- // orderGroup?: OrderGroup;
19
23
  // onFinish?: (order: Order) => void;
20
24
  };
21
25
  return (
@@ -29,6 +33,16 @@ export default {
29
33
 
30
34
  type Story = StoryObj<typeof SaleTunnel>;
31
35
 
32
- export const Default: Story = {
36
+ export const Credential: Story = {
33
37
  args: {},
34
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,10 +2,10 @@ import { ModalProps } from '@openfun/cunningham-react';
2
2
  import {
3
3
  CertificateProduct,
4
4
  CourseLight,
5
+ Offering,
5
6
  CredentialProduct,
6
7
  Enrollment,
7
8
  Order,
8
- OrderGroup,
9
9
  Organization,
10
10
  Product,
11
11
  ProductType,
@@ -16,11 +16,11 @@ import { PacedCourse } from 'types';
16
16
 
17
17
  export interface SaleTunnelProps extends Pick<ModalProps, 'isOpen' | 'onClose'> {
18
18
  product: Product;
19
+ offering?: Offering;
19
20
  organizations?: Organization[];
20
21
  isWithdrawable: boolean;
21
22
  course?: PacedCourse | CourseLight;
22
23
  enrollment?: Enrollment;
23
- orderGroup?: OrderGroup;
24
24
  onFinish?: (order: Order) => void;
25
25
  }
26
26
 
@@ -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;