richie-education 3.1.3-dev11 → 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.
Files changed (73) hide show
  1. package/js/api/joanie.ts +8 -8
  2. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -20
  3. package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
  4. package/js/components/CourseGlimpse/utils.ts +22 -35
  5. package/js/components/CourseGlimpseList/utils.ts +2 -2
  6. package/js/components/PurchaseButton/index.tsx +3 -3
  7. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -10
  8. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +5 -3
  9. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  10. package/js/components/SaleTunnel/index.spec.tsx +76 -63
  11. package/js/components/SaleTunnel/index.tsx +2 -2
  12. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  13. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  14. package/js/hooks/useContractArchive/index.ts +3 -3
  15. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  16. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  17. package/js/hooks/useCourseProducts.ts +4 -8
  18. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  19. package/js/hooks/useOffer/index.ts +32 -0
  20. package/js/hooks/useTeacherCoursesSearch/index.tsx +4 -4
  21. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  22. package/js/pages/DashboardCourses/index.spec.tsx +14 -17
  23. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +8 -14
  24. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -12
  25. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  26. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  27. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  28. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  29. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  30. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  31. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  32. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  33. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  34. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  35. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -23
  36. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  37. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  38. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -6
  39. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  40. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  41. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -7
  42. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  43. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  44. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  45. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  46. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  47. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  48. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  49. package/js/pages/TeacherDashboardTraining/index.spec.tsx +25 -33
  50. package/js/pages/TeacherDashboardTraining/index.tsx +12 -20
  51. package/js/types/Joanie.ts +25 -22
  52. package/js/utils/test/factories/joanie.ts +14 -11
  53. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  54. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  55. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  56. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  57. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  58. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  59. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  60. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  61. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  62. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -27
  63. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +16 -25
  64. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  65. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  66. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  67. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +10 -18
  68. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +81 -99
  69. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
  70. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +20 -31
  71. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
  72. package/package.json +1 -1
  73. package/js/hooks/useCourseProductRelation/index.ts +0 -44
package/js/api/joanie.ts CHANGED
@@ -127,8 +127,8 @@ export const getRoutes = () => {
127
127
  },
128
128
  organizations: {
129
129
  get: `${baseUrl}/organizations/:id/`,
130
- courseProductRelations: {
131
- get: `${baseUrl}/organizations/:organization_id/course-product-relations/:id/`,
130
+ offers: {
131
+ get: `${baseUrl}/organizations/:organization_id/offers/:id/`,
132
132
  },
133
133
  courses: {
134
134
  get: `${baseUrl}/organizations/:organization_id/courses/:id/`,
@@ -156,8 +156,8 @@ export const getRoutes = () => {
156
156
  courseRuns: {
157
157
  get: `${baseUrl}/course-runs/:id/`,
158
158
  },
159
- courseProductRelations: {
160
- get: `${baseUrl}/course-product-relations/:id/`,
159
+ offers: {
160
+ get: `${baseUrl}/offers/:id/`,
161
161
  },
162
162
  contractDefinitions: {
163
163
  previewTemplate: `${baseUrl}/contract_definitions/:id/preview_template/`,
@@ -470,12 +470,12 @@ const API = (): Joanie.API => {
470
470
  ).then(checkStatus);
471
471
  },
472
472
  },
473
- courseProductRelations: {
474
- get: (filters?: Joanie.CourseProductRelationQueryFilters) => {
473
+ offers: {
474
+ get: (filters?: Joanie.OfferQueryFilters) => {
475
475
  return fetchWithJWT(
476
476
  filters?.organization_id
477
- ? buildApiUrl(ROUTES.organizations.courseProductRelations.get, filters)
478
- : buildApiUrl(ROUTES.courseProductRelations.get, filters),
477
+ ? buildApiUrl(ROUTES.organizations.offers.get, filters)
478
+ : buildApiUrl(ROUTES.offers.get, filters),
479
479
  ).then(checkStatus);
480
480
  },
481
481
  },
@@ -6,14 +6,10 @@ import { IntlProvider } from 'react-intl';
6
6
  import fetchMock from 'fetch-mock';
7
7
  import { QueryStateFactory } from 'utils/test/factories/reactQuery';
8
8
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
9
- import {
10
- ContractFactory,
11
- CourseProductRelationFactory,
12
- OrganizationFactory,
13
- } from 'utils/test/factories/joanie';
9
+ import { ContractFactory, OfferFactory, OrganizationFactory } from 'utils/test/factories/joanie';
14
10
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
15
11
  import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
16
- import { isCourseProductRelation } from 'types/Joanie';
12
+ import { isOffer } from 'types/Joanie';
17
13
  import { Props } from './AbstractContractFrame';
18
14
  import { OrganizationContractFrame } from '.';
19
15
 
@@ -77,31 +73,28 @@ describe('OrganizationContractFrame', () => {
77
73
 
78
74
  it.each([
79
75
  {
80
- label: 'contractList: undefined, courseProductRelation: undefined',
76
+ label: 'contractList: undefined, offer: undefined',
81
77
  contractList: undefined,
82
- courseProductRelation: undefined,
78
+ offer: undefined,
83
79
  },
84
80
  {
85
- label: 'contractList: 2 Contract, courseProductRelation: undefined',
81
+ label: 'contractList: 2 Contract, offer: undefined',
86
82
  contractList: ContractFactory().many(2),
87
- courseProductRelation: undefined,
83
+ offer: undefined,
88
84
  },
89
85
  {
90
- label: 'contractList: undefined, courseProductRelation: one CourseProductRelation',
86
+ label: 'contractList: undefined, offer: one Offer',
91
87
  contractList: undefined,
92
- courseProductRelation: CourseProductRelationFactory().one(),
88
+ offer: OfferFactory().one(),
93
89
  },
94
90
  ])(
95
91
  'should implement AbstractContractFrame for organization and $label',
96
- async ({ contractList, courseProductRelation }) => {
92
+ async ({ contractList, offer }) => {
97
93
  const organization = OrganizationFactory().one();
98
94
  const contracts = contractList || ContractFactory().many(2);
99
95
  const isOpen = faker.datatype.boolean();
100
96
 
101
- const invitationLinkQueryString =
102
- courseProductRelation && isCourseProductRelation(courseProductRelation)
103
- ? `?course_product_relation_ids=${courseProductRelation.id}`
104
- : '';
97
+ const invitationLinkQueryString = offer && isOffer(offer) ? `?offer_ids=${offer.id}` : '';
105
98
  const expectedUrls = {
106
99
  getInvitationLink: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts-signature-link/${invitationLinkQueryString}`,
107
100
  checkSignature: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?id=${contracts[0].id}&id=${contracts[1].id}`,
@@ -133,9 +126,7 @@ describe('OrganizationContractFrame', () => {
133
126
  <Wrapper client={client}>
134
127
  <OrganizationContractFrame
135
128
  organizationId={organization.id}
136
- courseProductRelationIds={
137
- courseProductRelation ? [courseProductRelation.id] : undefined
138
- }
129
+ offerIds={offer ? [offer.id] : undefined}
139
130
  isOpen={isOpen}
140
131
  onDone={handleDone}
141
132
  onClose={handleClose}
@@ -4,17 +4,17 @@ import { useJoanieApi } from 'contexts/JoanieApiContext';
4
4
  import AbstractContractFrame, {
5
5
  AbstractProps,
6
6
  } from 'components/ContractFrame/AbstractContractFrame';
7
- import { Contract, CourseProductRelation } from 'types/Joanie';
7
+ import { Contract, Offer } from 'types/Joanie';
8
8
 
9
9
  interface Props extends AbstractProps {
10
10
  contractIds?: Contract['id'][];
11
11
  organizationId: string;
12
- courseProductRelationIds?: CourseProductRelation['id'][];
12
+ offerIds?: Offer['id'][];
13
13
  }
14
14
 
15
15
  const OrganizationContractFrame = ({
16
16
  organizationId,
17
- courseProductRelationIds = [],
17
+ offerIds = [],
18
18
  contractIds,
19
19
  onDone,
20
20
  ...props
@@ -29,7 +29,7 @@ const OrganizationContractFrame = ({
29
29
  be signed. We need to keep track of these ids to check if all contracts have been signed.
30
30
  */
31
31
  const response = await api.organizations.contracts.getSignatureLinks({
32
- course_product_relation_ids: courseProductRelationIds,
32
+ offer_ids: offerIds,
33
33
  organization_id: organizationId,
34
34
  contracts_ids: contractIds,
35
35
  });
@@ -6,23 +6,18 @@ import {
6
6
  Course as RichieCourse,
7
7
  isRichieCourse,
8
8
  } from 'types/Course';
9
- import {
10
- CourseListItem as JoanieCourse,
11
- CourseProductRelationLight,
12
- isCourseProductRelation,
13
- ProductType,
14
- } from 'types/Joanie';
9
+ import { CourseListItem as JoanieCourse, OfferLight, isOffer, ProductType } from 'types/Joanie';
15
10
  import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
16
11
  import { CourseGlimpseCourse } from '.';
17
12
 
18
- const getCourseGlimpsePropsFromCourseProductRelation = (
19
- courseProductRelation: CourseProductRelationLight,
13
+ const getCourseGlimpsePropsFromOffer = (
14
+ offer: OfferLight,
20
15
  intl: IntlShape,
21
16
  organizationId?: string,
22
17
  ): CourseGlimpseCourse => {
23
18
  const courseRouteParams = {
24
- courseId: courseProductRelation.course.id,
25
- courseProductRelationId: courseProductRelation.id,
19
+ courseId: offer.course.id,
20
+ offerId: offer.id,
26
21
  };
27
22
  const courseRoute = organizationId
28
23
  ? generatePath(TeacherDashboardPaths.ORGANIZATION_PRODUCT, {
@@ -31,35 +26,27 @@ const getCourseGlimpsePropsFromCourseProductRelation = (
31
26
  })
32
27
  : generatePath(TeacherDashboardPaths.COURSE_PRODUCT, courseRouteParams);
33
28
  return {
34
- id: courseProductRelation.id,
35
- code: courseProductRelation.course.code,
36
- title: courseProductRelation.product.title,
37
- cover_image: courseProductRelation.course.cover
29
+ id: offer.id,
30
+ code: offer.course.code,
31
+ title: offer.product.title,
32
+ cover_image: offer.course.cover
38
33
  ? {
39
- src: courseProductRelation.course.cover.src,
34
+ src: offer.course.cover.src,
40
35
  }
41
36
  : null,
42
37
  organization: {
43
- title: courseProductRelation.organizations[0].title,
44
- image: courseProductRelation.organizations[0].logo || null,
38
+ title: offer.organizations[0].title,
39
+ image: offer.organizations[0].logo || null,
45
40
  },
46
- product_id: courseProductRelation.product.id,
41
+ product_id: offer.product.id,
47
42
  course_route: courseRoute,
48
- state: courseProductRelation.product.state,
43
+ state: offer.product.state,
49
44
  certificate_offer:
50
- courseProductRelation.product.type === ProductType.CERTIFICATE
51
- ? CourseCertificateOffer.PAID
52
- : null,
53
- offer: courseProductRelation.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
54
- certificate_price:
55
- courseProductRelation.product.type === ProductType.CERTIFICATE
56
- ? courseProductRelation.product.price
57
- : null,
58
- price:
59
- courseProductRelation.product.type === ProductType.CREDENTIAL
60
- ? courseProductRelation.product.price
61
- : null,
62
- price_currency: courseProductRelation.product.price_currency,
45
+ offer.product.type === ProductType.CERTIFICATE ? CourseCertificateOffer.PAID : null,
46
+ offer: offer.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
47
+ certificate_price: offer.product.type === ProductType.CERTIFICATE ? offer.product.price : null,
48
+ price: offer.product.type === ProductType.CREDENTIAL ? offer.product.price : null,
49
+ price_currency: offer.product.price_currency,
63
50
  };
64
51
  };
65
52
 
@@ -125,12 +112,12 @@ const getCourseGlimpsePropsFromJoanieCourse = (
125
112
  };
126
113
 
127
114
  export const getCourseGlimpseProps = (
128
- course: RichieCourse | (JoanieCourse | CourseProductRelationLight),
115
+ course: RichieCourse | (JoanieCourse | OfferLight),
129
116
  intl?: IntlShape,
130
117
  organizationId?: string,
131
118
  ): CourseGlimpseCourse => {
132
- if (isCourseProductRelation(course)) {
133
- return getCourseGlimpsePropsFromCourseProductRelation(course, intl!, organizationId);
119
+ if (isOffer(course)) {
120
+ return getCourseGlimpsePropsFromOffer(course, intl!, organizationId);
134
121
  }
135
122
 
136
123
  if (isRichieCourse(course)) {
@@ -1,10 +1,10 @@
1
1
  import { IntlShape } from 'react-intl';
2
- import { CourseProductRelationLight, CourseListItem as JoanieCourse } from 'types/Joanie';
2
+ import { OfferLight, CourseListItem as JoanieCourse } from 'types/Joanie';
3
3
  import { Course as RichieCourse } from 'types/Course';
4
4
  import { CourseGlimpseCourse, getCourseGlimpseProps } from 'components/CourseGlimpse';
5
5
 
6
6
  export const getCourseGlimpseListProps = (
7
- courses: RichieCourse[] | (JoanieCourse | CourseProductRelationLight)[],
7
+ courses: RichieCourse[] | (JoanieCourse | OfferLight)[],
8
8
  intl?: IntlShape,
9
9
  organizationId?: string,
10
10
  ): CourseGlimpseCourse[] => {
@@ -42,7 +42,7 @@ const messages = defineMessages({
42
42
 
43
43
  interface PurchaseButtonPropsBase {
44
44
  product: Joanie.CredentialProduct | Joanie.CertificateProduct;
45
- courseProductRelation?: Joanie.CourseProductRelation;
45
+ offer?: Joanie.Offer;
46
46
  isWithdrawable: boolean;
47
47
  disabled?: boolean;
48
48
  className?: string;
@@ -66,7 +66,7 @@ interface CertificatePurchaseButtonProps extends PurchaseButtonPropsBase {
66
66
  const PurchaseButton = ({
67
67
  product,
68
68
  course,
69
- courseProductRelation,
69
+ offer,
70
70
  enrollment,
71
71
  isWithdrawable,
72
72
  organizations,
@@ -140,7 +140,7 @@ const PurchaseButton = ({
140
140
  {...saleTunnelModal}
141
141
  product={product}
142
142
  organizations={organizations}
143
- courseProductRelation={courseProductRelation}
143
+ offer={offer}
144
144
  enrollment={enrollment}
145
145
  course={course}
146
146
  isWithdrawable={isWithdrawable}
@@ -10,14 +10,7 @@ import {
10
10
  } from 'react';
11
11
  import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
12
12
  import { SaleTunnelProps } from 'components/SaleTunnel/index';
13
- import {
14
- Address,
15
- CourseProductRelation,
16
- CreditCard,
17
- Order,
18
- OrderState,
19
- Product,
20
- } from 'types/Joanie';
13
+ import { Address, Offer, CreditCard, Order, OrderState, Product } from 'types/Joanie';
21
14
  import useProductOrder from 'hooks/useProductOrder';
22
15
  import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
23
16
  import WebAnalyticsAPIHandler from 'api/web-analytics';
@@ -33,7 +26,7 @@ export interface SaleTunnelContextType {
33
26
  order?: Order;
34
27
  product: Product;
35
28
  webAnalyticsEventKey: string;
36
- relation?: CourseProductRelation;
29
+ offer?: Offer;
37
30
 
38
31
  // internal
39
32
  step: SaleTunnelStep;
@@ -121,7 +114,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
121
114
  webAnalyticsEventKey: props.eventKey,
122
115
  order,
123
116
  product: props.product,
124
- relation: props.courseProductRelation,
117
+ offer: props.offer,
125
118
  props,
126
119
  billingAddress,
127
120
  setBillingAddress,
@@ -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>
@@ -99,7 +101,7 @@ const Email = () => {
99
101
  };
100
102
 
101
103
  const Total = () => {
102
- const { product, relation } = useSaleTunnelContext();
104
+ const { product, offer } = useSaleTunnelContext();
103
105
  return (
104
106
  <div className="sale-tunnel__total">
105
107
  <div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
@@ -108,7 +110,7 @@ const Total = () => {
108
110
  </div>
109
111
  <div className="block-title">
110
112
  <FormattedNumber
111
- value={relation?.discounted_price || product.price}
113
+ value={offer?.rules.discounted_price || product.price}
112
114
  style="currency"
113
115
  currency={product.price_currency}
114
116
  />
@@ -15,7 +15,7 @@ import CourseProductItem from 'widgets/SyllabusCourseRunsList/components/CourseP
15
15
  import {
16
16
  AddressFactory,
17
17
  ContractFactory,
18
- CourseProductRelationFactory,
18
+ OfferFactory,
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 offer = OfferFactory({
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
+ offer,
112
112
  );
113
113
  fetchMock.get(
114
114
  `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
@@ -15,7 +15,7 @@ import {
15
15
  AddressFactory,
16
16
  CertificateOrderFactory,
17
17
  CertificateProductFactory,
18
- CourseProductRelationFactory,
18
+ OfferFactory,
19
19
  CredentialOrderFactory,
20
20
  CredentialProductFactory,
21
21
  CreditCardFactory,
@@ -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(
@@ -444,15 +453,17 @@ describe.each([
444
453
  const intl = createIntl({ locale: 'en' });
445
454
  const schedule = PaymentInstallmentFactory().many(2);
446
455
 
447
- const relation = CourseProductRelationFactory({
456
+ const offer = OfferFactory({
448
457
  product: ProductFactory({
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
- const { product } = relation;
466
+ const { product } = offer;
456
467
 
457
468
  fetchMock
458
469
  .get(
@@ -464,45 +475,47 @@ describe.each([
464
475
  schedule,
465
476
  );
466
477
 
467
- render(<Wrapper product={product} courseProductRelation={relation} isWithdrawable={true} />, {
478
+ render(<Wrapper product={product} offer={offer} isWithdrawable={true} />, {
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(relation!.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
  ),
@@ -2,7 +2,7 @@ import { ModalProps } from '@openfun/cunningham-react';
2
2
  import {
3
3
  CertificateProduct,
4
4
  CourseLight,
5
- CourseProductRelation,
5
+ Offer,
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
+ offer?: Offer;
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, OfferFactory } 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 = OfferFactory().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 = OfferFactory().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, OfferLight } 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 | OfferLight)[];
35
35
  isLoadingMore?: boolean;
36
36
  hasMore?: boolean;
37
37
  isNewSearchLoading?: boolean;