richie-education 3.1.3-dev3 → 3.1.3-dev30

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
@@ -13,3 +13,7 @@ let context = {
13
13
  (window as any).__richie_frontend_context__ = {
14
14
  context: RichieContextFactory(context).one(),
15
15
  };
16
+
17
+ (window as any).jest = {
18
+ fn: ((fnc: any) => fnc) as any,
19
+ };
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
+ offerings: {
131
+ get: `${baseUrl}/organizations/:organization_id/offerings/: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
+ offerings: {
160
+ get: `${baseUrl}/offerings/: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
+ offerings: {
474
+ get: (filters?: Joanie.OfferingQueryFilters) => {
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.offerings.get, filters)
478
+ : buildApiUrl(ROUTES.offerings.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, OfferingFactory, 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 { isOffering } from 'types/Joanie';
17
13
  import { Props } from './AbstractContractFrame';
18
14
  import { OrganizationContractFrame } from '.';
19
15
 
@@ -77,31 +73,29 @@ describe('OrganizationContractFrame', () => {
77
73
 
78
74
  it.each([
79
75
  {
80
- label: 'contractList: undefined, courseProductRelation: undefined',
76
+ label: 'contractList: undefined, offering: undefined',
81
77
  contractList: undefined,
82
- courseProductRelation: undefined,
78
+ offering: undefined,
83
79
  },
84
80
  {
85
- label: 'contractList: 2 Contract, courseProductRelation: undefined',
81
+ label: 'contractList: 2 Contract, offering: undefined',
86
82
  contractList: ContractFactory().many(2),
87
- courseProductRelation: undefined,
83
+ offering: undefined,
88
84
  },
89
85
  {
90
- label: 'contractList: undefined, courseProductRelation: one CourseProductRelation',
86
+ label: 'contractList: undefined, offering: one Offering',
91
87
  contractList: undefined,
92
- courseProductRelation: CourseProductRelationFactory().one(),
88
+ offering: OfferingFactory().one(),
93
89
  },
94
90
  ])(
95
91
  'should implement AbstractContractFrame for organization and $label',
96
- async ({ contractList, courseProductRelation }) => {
92
+ async ({ contractList, offering }) => {
97
93
  const organization = OrganizationFactory().one();
98
94
  const contracts = contractList || ContractFactory().many(2);
99
95
  const isOpen = faker.datatype.boolean();
100
96
 
101
97
  const invitationLinkQueryString =
102
- courseProductRelation && isCourseProductRelation(courseProductRelation)
103
- ? `?course_product_relation_ids=${courseProductRelation.id}`
104
- : '';
98
+ offering && isOffering(offering) ? `?offering_ids=${offering.id}` : '';
105
99
  const expectedUrls = {
106
100
  getInvitationLink: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts-signature-link/${invitationLinkQueryString}`,
107
101
  checkSignature: `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?id=${contracts[0].id}&id=${contracts[1].id}`,
@@ -133,9 +127,7 @@ describe('OrganizationContractFrame', () => {
133
127
  <Wrapper client={client}>
134
128
  <OrganizationContractFrame
135
129
  organizationId={organization.id}
136
- courseProductRelationIds={
137
- courseProductRelation ? [courseProductRelation.id] : undefined
138
- }
130
+ offeringIds={offering ? [offering.id] : undefined}
139
131
  isOpen={isOpen}
140
132
  onDone={handleDone}
141
133
  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, Offering } 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
+ offeringIds?: Offering['id'][];
13
13
  }
14
14
 
15
15
  const OrganizationContractFrame = ({
16
16
  organizationId,
17
- courseProductRelationIds = [],
17
+ offeringIds = [],
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
+ offering_ids: offeringIds,
33
33
  organization_id: organizationId,
34
34
  contracts_ids: contractIds,
35
35
  });
@@ -65,6 +65,35 @@ export const CourseGlimpseFooter: React.FC<{ course: CourseGlimpseCourse } & Com
65
65
  const offerIcon = `icon-offer-${offer}` as OfferIconType;
66
66
  const offerCertificateIcon = hasCertificateOffer && IconTypeEnum.SCHOOL;
67
67
  const offerPrice = hasEnrollmentOffer && course.price;
68
+ const discountedPrice = course.discounted_price ?? null;
69
+ const hasDiscount = discountedPrice !== null;
70
+
71
+ let $price = null;
72
+
73
+ if (offerPrice) {
74
+ if (hasDiscount) {
75
+ $price = (
76
+ <div className="offer_prices">
77
+ <span className="offer__price offer__price--striked">
78
+ <FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
79
+ </span>
80
+ <span className="offer__price offer__price--discounted">
81
+ <FormattedNumber
82
+ value={discountedPrice}
83
+ currency={course.price_currency}
84
+ style="currency"
85
+ />
86
+ </span>
87
+ </div>
88
+ );
89
+ } else {
90
+ $price = (
91
+ <span className="offer__price">
92
+ <FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
93
+ </span>
94
+ );
95
+ }
96
+ }
68
97
 
69
98
  return (
70
99
  <div className="course-glimpse-footer">
@@ -99,11 +128,7 @@ export const CourseGlimpseFooter: React.FC<{ course: CourseGlimpseCourse } & Com
99
128
  name={offerIcon}
100
129
  title={intl.formatMessage(courseOfferMessages[offer])}
101
130
  />
102
- {offerPrice && (
103
- <span className="offer__price">
104
- <FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
105
- </span>
106
- )}
131
+ {$price}
107
132
  </div>
108
133
  </div>
109
134
  );
@@ -59,6 +59,10 @@ describe('widgets/Search/components/CourseGlimpse', () => {
59
59
  certificate_offer: CourseCertificateOffer.FREE,
60
60
  certificate_price: null,
61
61
  price_currency: 'EUR',
62
+ discounted_price: null,
63
+ discount: null,
64
+ certificate_discounted_price: null,
65
+ certificate_discount: null,
62
66
  };
63
67
 
64
68
  const contextProps: CommonDataProps['context'] = RichieContextFactory().one();
@@ -168,6 +172,20 @@ describe('widgets/Search/components/CourseGlimpse', () => {
168
172
  );
169
173
  });
170
174
 
175
+ it('renders a course glimpse with a discount', () => {
176
+ const { container } = renderCourseGlimpse({
177
+ contextProps,
178
+ course: { ...course, price: 100.0, discount: '30%', discounted_price: 70.0 },
179
+ });
180
+
181
+ const prices = container.getElementsByClassName('offer_prices');
182
+ expect(prices.length).toBe(1);
183
+ expect(prices[0].children.length).toBe(2);
184
+ const discountedPrice = container.getElementsByClassName('offer__price--discounted');
185
+ expect(discountedPrice.length).toBe(1);
186
+ expect(discountedPrice[0]).toHaveTextContent('€70.00');
187
+ });
188
+
171
189
  it('does not show certificate offer if the course does not offer a certificate', () => {
172
190
  const { container } = renderCourseGlimpse({
173
191
  contextProps,
@@ -1,6 +1,7 @@
1
1
  import { Meta, StoryObj } from '@storybook/react';
2
- import { RichieContextFactory, CourseLightFactory } from 'utils/test/factories/richie';
3
- import { getCourseGlimpseProps, CourseGlimpse } from 'components/CourseGlimpse';
2
+ import { CourseLightFactory, RichieContextFactory } from 'utils/test/factories/richie';
3
+ import { CourseGlimpse, getCourseGlimpseProps } from 'components/CourseGlimpse';
4
+ import { CourseCertificateOffer, CourseOffer } from 'types/Course';
4
5
 
5
6
  export default {
6
7
  component: CourseGlimpse,
@@ -8,9 +9,79 @@ export default {
8
9
 
9
10
  type Story = StoryObj<typeof CourseGlimpse>;
10
11
 
12
+ const richieContext = RichieContextFactory().one();
13
+ const courseLight = CourseLightFactory().one();
14
+ const courseGlimpseCourse = getCourseGlimpseProps(courseLight);
15
+
11
16
  export const RichieCourse: Story = {
12
17
  args: {
13
- context: RichieContextFactory().one(),
14
- course: getCourseGlimpseProps(CourseLightFactory().one()),
18
+ context: richieContext,
19
+ course: { ...courseGlimpseCourse },
20
+ },
21
+ };
22
+
23
+ export const certificateProduct: Story = {
24
+ args: {
25
+ context: richieContext,
26
+ course: {
27
+ ...courseGlimpseCourse,
28
+ title: 'Certificate Product',
29
+ offer: CourseOffer.FREE,
30
+ price: null,
31
+ certificate_offer: CourseCertificateOffer.PAID,
32
+ certificate_price: 100,
33
+ discounted_price: null,
34
+ discount: null,
35
+ },
36
+ },
37
+ };
38
+
39
+ export const certificateProductDiscount: Story = {
40
+ args: {
41
+ context: richieContext,
42
+ course: {
43
+ ...courseGlimpseCourse,
44
+ title: 'Certificate Product with Discount',
45
+ offer: CourseOffer.FREE,
46
+ price: null,
47
+ certificate_offer: CourseCertificateOffer.PAID,
48
+ certificate_price: 100,
49
+ discounted_price: 80,
50
+ discount: '-20 €',
51
+ },
52
+ },
53
+ };
54
+
55
+ export const credentialProduct: Story = {
56
+ args: {
57
+ context: richieContext,
58
+ course: {
59
+ ...courseGlimpseCourse,
60
+ title: 'Credential Product',
61
+ icon: null,
62
+ offer: CourseOffer.PAID,
63
+ price: 100,
64
+ certificate_offer: null,
65
+ certificate_price: null,
66
+ discounted_price: null,
67
+ discount: null,
68
+ },
69
+ },
70
+ };
71
+
72
+ export const credentialProductDiscount: Story = {
73
+ args: {
74
+ context: richieContext,
75
+ course: {
76
+ ...courseGlimpseCourse,
77
+ title: 'Credential Product with Discount',
78
+ icon: null,
79
+ offer: CourseOffer.PAID,
80
+ price: 100,
81
+ certificate_offer: null,
82
+ certificate_price: null,
83
+ discounted_price: 80,
84
+ discount: '-20 €',
85
+ },
15
86
  },
16
87
  };
@@ -47,6 +47,10 @@ export interface CourseGlimpseCourse {
47
47
  certificate_price: Nullable<number>;
48
48
  price: Nullable<number>;
49
49
  price_currency: string;
50
+ discounted_price: Nullable<number>;
51
+ discount: Nullable<string>;
52
+ certificate_discounted_price: Nullable<number>;
53
+ certificate_discount: Nullable<string>;
50
54
  }
51
55
 
52
56
  export interface CourseGlimpseProps {
@@ -8,21 +8,21 @@ import {
8
8
  } from 'types/Course';
9
9
  import {
10
10
  CourseListItem as JoanieCourse,
11
- CourseProductRelationLight,
12
- isCourseProductRelation,
11
+ OfferingLight,
12
+ isOffering,
13
13
  ProductType,
14
14
  } from 'types/Joanie';
15
15
  import { TeacherDashboardPaths } from 'widgets/Dashboard/utils/teacherDashboardPaths';
16
16
  import { CourseGlimpseCourse } from '.';
17
17
 
18
- const getCourseGlimpsePropsFromCourseProductRelation = (
19
- courseProductRelation: CourseProductRelationLight,
18
+ const getCourseGlimpsePropsFromOffering = (
19
+ offering: OfferingLight,
20
20
  intl: IntlShape,
21
21
  organizationId?: string,
22
22
  ): CourseGlimpseCourse => {
23
23
  const courseRouteParams = {
24
- courseId: courseProductRelation.course.id,
25
- courseProductRelationId: courseProductRelation.id,
24
+ courseId: offering.course.id,
25
+ offeringId: offering.id,
26
26
  };
27
27
  const courseRoute = organizationId
28
28
  ? generatePath(TeacherDashboardPaths.ORGANIZATION_PRODUCT, {
@@ -31,35 +31,32 @@ const getCourseGlimpsePropsFromCourseProductRelation = (
31
31
  })
32
32
  : generatePath(TeacherDashboardPaths.COURSE_PRODUCT, courseRouteParams);
33
33
  return {
34
- id: courseProductRelation.id,
35
- code: courseProductRelation.course.code,
36
- title: courseProductRelation.product.title,
37
- cover_image: courseProductRelation.course.cover
34
+ id: offering.id,
35
+ code: offering.course.code,
36
+ title: offering.product.title,
37
+ cover_image: offering.course.cover
38
38
  ? {
39
- src: courseProductRelation.course.cover.src,
39
+ src: offering.course.cover.src,
40
40
  }
41
41
  : null,
42
42
  organization: {
43
- title: courseProductRelation.organizations[0].title,
44
- image: courseProductRelation.organizations[0].logo || null,
43
+ title: offering.organizations[0].title,
44
+ image: offering.organizations[0].logo || null,
45
45
  },
46
- product_id: courseProductRelation.product.id,
46
+ product_id: offering.product.id,
47
47
  course_route: courseRoute,
48
- state: courseProductRelation.product.state,
48
+ state: offering.product.state,
49
49
  certificate_offer:
50
- courseProductRelation.product.type === ProductType.CERTIFICATE
51
- ? CourseCertificateOffer.PAID
52
- : null,
53
- offer: courseProductRelation.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
50
+ offering.product.type === ProductType.CERTIFICATE ? CourseCertificateOffer.PAID : null,
51
+ offer: offering.product.type === ProductType.CREDENTIAL ? CourseOffer.PAID : null,
54
52
  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,
53
+ offering.product.type === ProductType.CERTIFICATE ? offering.product.price : null,
54
+ price: offering.product.type === ProductType.CREDENTIAL ? offering.product.price : null,
55
+ price_currency: offering.product.price_currency,
56
+ discounted_price: offering.product.discounted_price || null,
57
+ discount: offering.product.discount || null,
58
+ certificate_discounted_price: offering.product.certificate_discounted_price || null,
59
+ certificate_discount: offering.product.certificate_discount || null,
63
60
  };
64
61
  };
65
62
 
@@ -84,6 +81,10 @@ const getCourseGlimpsePropsFromRichieCourse = (course: RichieCourse): CourseGlim
84
81
  certificate_offer: course.certificate_offer,
85
82
  offer: course.offer,
86
83
  certificate_price: course.certificate_price,
84
+ certificate_discounted_price: course.certificate_discounted_price,
85
+ certificate_discount: course.certificate_discount,
86
+ discounted_price: course.discounted_price,
87
+ discount: course.discount,
87
88
  });
88
89
 
89
90
  const getCourseGlimpsePropsFromJoanieCourse = (
@@ -121,16 +122,20 @@ const getCourseGlimpsePropsFromJoanieCourse = (
121
122
  certificate_offer: null,
122
123
  offer: null,
123
124
  certificate_price: null,
125
+ discounted_price: null,
126
+ discount: null,
127
+ certificate_discounted_price: null,
128
+ certificate_discount: null,
124
129
  };
125
130
  };
126
131
 
127
132
  export const getCourseGlimpseProps = (
128
- course: RichieCourse | (JoanieCourse | CourseProductRelationLight),
133
+ course: RichieCourse | (JoanieCourse | OfferingLight),
129
134
  intl?: IntlShape,
130
135
  organizationId?: string,
131
136
  ): CourseGlimpseCourse => {
132
- if (isCourseProductRelation(course)) {
133
- return getCourseGlimpsePropsFromCourseProductRelation(course, intl!, organizationId);
137
+ if (isOffering(course)) {
138
+ return getCourseGlimpsePropsFromOffering(course, intl!, organizationId);
134
139
  }
135
140
 
136
141
  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 { OfferingLight, 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 | OfferingLight)[],
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
- orderGroup?: Joanie.OrderGroup;
45
+ offering?: Joanie.Offering;
46
46
  isWithdrawable: boolean;
47
47
  disabled?: boolean;
48
48
  className?: string;
@@ -66,8 +66,8 @@ interface CertificatePurchaseButtonProps extends PurchaseButtonPropsBase {
66
66
  const PurchaseButton = ({
67
67
  product,
68
68
  course,
69
+ offering,
69
70
  enrollment,
70
- orderGroup,
71
71
  isWithdrawable,
72
72
  organizations,
73
73
  disabled = false,
@@ -140,8 +140,8 @@ const PurchaseButton = ({
140
140
  {...saleTunnelModal}
141
141
  product={product}
142
142
  organizations={organizations}
143
+ offering={offering}
143
144
  enrollment={enrollment}
144
- orderGroup={orderGroup}
145
145
  course={course}
146
146
  isWithdrawable={isWithdrawable}
147
147
  onFinish={onFinish}
@@ -19,9 +19,7 @@ export const CredentialSaleTunnel = (props: CredentialSaleTunnelProps) => {
19
19
  );
20
20
  };
21
21
 
22
- const CredentialPaymentButton = ({
23
- course,
24
- }: Pick<CredentialSaleTunnelProps, 'course' | 'orderGroup'>) => {
22
+ const CredentialPaymentButton = ({ course }: Pick<CredentialSaleTunnelProps, 'course'>) => {
25
23
  return (
26
24
  <SubscriptionButton
27
25
  buildOrderPayload={(payload) => ({
@@ -10,7 +10,15 @@ import {
10
10
  } from 'react';
11
11
  import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
12
12
  import { SaleTunnelProps } from 'components/SaleTunnel/index';
13
- import { Address, CreditCard, Order, OrderState, Product } from 'types/Joanie';
13
+ import {
14
+ Address,
15
+ Enrollment,
16
+ Offering,
17
+ CreditCard,
18
+ Order,
19
+ OrderState,
20
+ Product,
21
+ } from 'types/Joanie';
14
22
  import useProductOrder from 'hooks/useProductOrder';
15
23
  import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
16
24
  import WebAnalyticsAPIHandler from 'api/web-analytics';
@@ -26,6 +34,8 @@ export interface SaleTunnelContextType {
26
34
  order?: Order;
27
35
  product: Product;
28
36
  webAnalyticsEventKey: string;
37
+ offering?: Offering;
38
+ enrollment?: Enrollment;
29
39
 
30
40
  // internal
31
41
  step: SaleTunnelStep;
@@ -113,6 +123,8 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
113
123
  webAnalyticsEventKey: props.eventKey,
114
124
  order,
115
125
  product: props.product,
126
+ offering: props.offering,
127
+ enrollment: props.enrollment,
116
128
  props,
117
129
  billingAddress,
118
130
  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,11 @@ const Email = () => {
99
101
  };
100
102
 
101
103
  const Total = () => {
102
- const { product } = useSaleTunnelContext();
104
+ const { product, offering, enrollment } = useSaleTunnelContext();
105
+ const totalPrice =
106
+ enrollment?.offerings?.[0]?.rules?.discounted_price ??
107
+ offering?.rules.discounted_price ??
108
+ product.price;
103
109
  return (
104
110
  <div className="sale-tunnel__total">
105
111
  <div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
@@ -107,11 +113,7 @@ const Total = () => {
107
113
  <FormattedMessage {...messages.totalLabel} />
108
114
  </div>
109
115
  <div className="block-title">
110
- <FormattedNumber
111
- value={product.price}
112
- style="currency"
113
- currency={product.price_currency}
114
- />
116
+ <FormattedNumber value={totalPrice} style="currency" currency={product.price_currency} />
115
117
  </div>
116
118
  </div>
117
119
  </div>
@@ -72,7 +72,7 @@ interface Props {
72
72
  buildOrderPayload: (
73
73
  payload: Pick<
74
74
  OrderCreationPayload,
75
- 'product_id' | 'billing_address' | 'order_group_id' | 'has_waived_withdrawal_right'
75
+ 'product_id' | 'billing_address' | 'has_waived_withdrawal_right'
76
76
  >,
77
77
  ) => OrderCreationPayload;
78
78
  }
@@ -124,7 +124,6 @@ const SubscriptionButton = ({ buildOrderPayload }: Props) => {
124
124
  const payload = buildOrderPayload({
125
125
  product_id: product.id,
126
126
  billing_address: billingAddress!,
127
- order_group_id: saleTunnelProps.orderGroup?.id,
128
127
  has_waived_withdrawal_right: hasWaivedWithdrawalRight,
129
128
  });
130
129
 
@@ -1,5 +1,5 @@
1
1
  import fetchMock from 'fetch-mock';
2
- import { act, fireEvent, screen, waitFor } from '@testing-library/react';
2
+ import { screen } from '@testing-library/react';
3
3
  import queryString from 'query-string';
4
4
  import {
5
5
  RichieContextFactory as mockRichieContextFactory,
@@ -11,11 +11,9 @@ import {
11
11
  AddressFactory,
12
12
  CredentialOrderFactory,
13
13
  CredentialProductFactory,
14
- OrderGroupFactory,
15
14
  } from 'utils/test/factories/joanie';
16
15
  import type * as Joanie from 'types/Joanie';
17
- import { Maybe } from 'types/utils';
18
- import { NOT_CANCELED_ORDER_STATES, OrderCredentialCreationPayload } from 'types/Joanie';
16
+ import { NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
19
17
  import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
20
18
  import { render } from 'utils/test/render';
21
19
  import { getAddressLabel } from 'components/SaleTunnel/AddressSelector';
@@ -83,11 +81,9 @@ describe('SaleTunnel / Credential', () => {
83
81
  it('should create an order with an order group', async () => {
84
82
  const course = PacedCourseFactory().one();
85
83
  const product = CredentialProductFactory().one();
86
- const orderGroup = OrderGroupFactory().one();
87
84
  const billingAddress: Joanie.Address = AddressFactory({ is_main: true }).one();
88
85
 
89
- let createOrderPayload: Maybe<OrderCredentialCreationPayload>;
90
- const order = CredentialOrderFactory({ order_group_id: orderGroup.id }).one();
86
+ const order = CredentialOrderFactory().one();
91
87
  const orderQueryParameters = {
92
88
  course_code: course.code,
93
89
  product_id: product.id,
@@ -100,15 +96,12 @@ describe('SaleTunnel / Credential', () => {
100
96
  `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
101
97
  [],
102
98
  )
103
- .post('https://joanie.endpoint/api/v1.0/orders/', (_, { body }) => {
104
- createOrderPayload = JSON.parse(body as any);
105
- return order;
106
- })
99
+ .post('https://joanie.endpoint/api/v1.0/orders/', order)
107
100
  .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
108
101
  overwriteRoutes: true,
109
102
  });
110
103
 
111
- render(<Wrapper product={product} course={course} orderGroup={orderGroup} />, {
104
+ render(<Wrapper product={product} course={course} />, {
112
105
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
113
106
  });
114
107
 
@@ -121,12 +114,5 @@ describe('SaleTunnel / Credential', () => {
121
114
 
122
115
  // - Payment button should not be disabled.
123
116
  expect($button.disabled).toBe(false);
124
-
125
- // - User clicks on pay button
126
- await act(async () => {
127
- fireEvent.click($button);
128
- });
129
-
130
- await waitFor(() => expect(createOrderPayload?.order_group_id).toEqual(orderGroup.id));
131
117
  });
132
118
  });