richie-education 3.1.2 → 3.1.3-dev12

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 (80) 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/CredentialSaleTunnel/index.tsx +1 -3
  8. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -1
  9. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +2 -2
  10. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
  11. package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
  12. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  13. package/js/components/SaleTunnel/index.spec.tsx +75 -0
  14. package/js/components/SaleTunnel/index.stories.tsx +0 -1
  15. package/js/components/SaleTunnel/index.tsx +2 -2
  16. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  17. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  18. package/js/hooks/useContractArchive/index.ts +3 -3
  19. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  20. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  21. package/js/hooks/useCourseProducts.ts +4 -8
  22. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  23. package/js/hooks/useOffer/index.ts +32 -0
  24. package/js/hooks/useTeacherCoursesSearch/index.tsx +4 -4
  25. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  26. package/js/pages/DashboardCourses/index.spec.tsx +14 -17
  27. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +8 -14
  28. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -12
  29. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  30. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  31. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  32. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  33. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  34. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  35. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  36. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  37. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  38. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  39. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -23
  40. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  41. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  42. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -6
  43. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  44. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  45. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -7
  46. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  47. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  48. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  49. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  50. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  51. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  52. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  53. package/js/pages/TeacherDashboardTraining/index.spec.tsx +25 -33
  54. package/js/pages/TeacherDashboardTraining/index.tsx +12 -20
  55. package/js/types/Joanie.ts +26 -30
  56. package/js/utils/ProductHelper/index.ts +1 -5
  57. package/js/utils/test/factories/joanie.ts +12 -25
  58. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  59. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  60. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  61. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  62. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  63. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  64. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  65. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  66. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  67. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -27
  68. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +16 -25
  69. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  70. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  71. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  72. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +23 -38
  73. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +27 -7
  74. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +1 -1
  75. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +174 -158
  76. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +9 -2
  77. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +108 -28
  78. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
  79. package/package.json +1 -1
  80. 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
- orderGroup?: Joanie.OrderGroup;
45
+ offer?: Joanie.Offer;
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
+ offer,
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
+ offer={offer}
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,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 { Address, CreditCard, Order, OrderState, Product } from 'types/Joanie';
13
+ import { Address, Offer, CreditCard, Order, OrderState, Product } from 'types/Joanie';
14
14
  import useProductOrder from 'hooks/useProductOrder';
15
15
  import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
16
16
  import WebAnalyticsAPIHandler from 'api/web-analytics';
@@ -26,6 +26,7 @@ export interface SaleTunnelContextType {
26
26
  order?: Order;
27
27
  product: Product;
28
28
  webAnalyticsEventKey: string;
29
+ offer?: Offer;
29
30
 
30
31
  // internal
31
32
  step: SaleTunnelStep;
@@ -113,6 +114,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
113
114
  webAnalyticsEventKey: props.eventKey,
114
115
  order,
115
116
  product: props.product,
117
+ offer: props.offer,
116
118
  props,
117
119
  billingAddress,
118
120
  setBillingAddress,
@@ -99,7 +99,7 @@ const Email = () => {
99
99
  };
100
100
 
101
101
  const Total = () => {
102
- const { product } = useSaleTunnelContext();
102
+ const { product, offer } = useSaleTunnelContext();
103
103
  return (
104
104
  <div className="sale-tunnel__total">
105
105
  <div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
@@ -108,7 +108,7 @@ const Total = () => {
108
108
  </div>
109
109
  <div className="block-title">
110
110
  <FormattedNumber
111
- value={product.price}
111
+ value={offer?.discounted_price || product.price}
112
112
  style="currency"
113
113
  currency={product.price_currency}
114
114
  />
@@ -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
  });
@@ -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,6 +15,7 @@ import {
15
15
  AddressFactory,
16
16
  CertificateOrderFactory,
17
17
  CertificateProductFactory,
18
+ OfferFactory,
18
19
  CredentialOrderFactory,
19
20
  CredentialProductFactory,
20
21
  CreditCardFactory,
@@ -432,6 +433,80 @@ describe.each([
432
433
  name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
433
434
  });
434
435
  });
436
+
437
+ const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
438
+ expect($totalAmount).toHaveTextContent(
439
+ 'Total' + formatPrice(product.price, product.price_currency).replace(/(\u202F|\u00a0)/g, ' '),
440
+ );
441
+ });
442
+
443
+ it('should show the product payment schedule with discounted price', async () => {
444
+ const intl = createIntl({ locale: 'en' });
445
+ const schedule = PaymentInstallmentFactory().many(2);
446
+
447
+ const offer = OfferFactory({
448
+ product: ProductFactory({
449
+ price: 840,
450
+ price_currency: 'EUR',
451
+ }).one(),
452
+ discounted_price: 800,
453
+ discount_rate: 0.3,
454
+ }).one();
455
+ const { product } = offer;
456
+
457
+ fetchMock
458
+ .get(
459
+ `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
460
+ [],
461
+ )
462
+ .get(
463
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
464
+ schedule,
465
+ );
466
+
467
+ render(<Wrapper product={product} offer={offer} isWithdrawable={true} />, {
468
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
469
+ });
470
+
471
+ await screen.findByRole('heading', {
472
+ level: 4,
473
+ name: 'Payment schedule',
474
+ });
475
+
476
+ const scheduleTable = screen.getByRole('table');
477
+ const scheduleTableRows = within(scheduleTable).getAllByRole('row');
478
+ expect(scheduleTableRows).toHaveLength(schedule.length);
479
+
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('_', ' '))!,
499
+ });
500
+ });
501
+
502
+ const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
503
+ expect($totalAmount).toHaveTextContent(
504
+ 'Total' +
505
+ formatPrice(offer!.discounted_price!, product.price_currency).replace(
506
+ /(\u202F|\u00a0)/g,
507
+ ' ',
508
+ ),
509
+ );
435
510
  });
436
511
 
437
512
  it('should show a walkthrough to explain the subscription process', async () => {
@@ -15,7 +15,6 @@ export default {
15
15
  isWithdrawable: true,
16
16
  // enrollment?: Enrollment;
17
17
  // product: CredentialProduct | CertificateProduct;
18
- // orderGroup?: OrderGroup;
19
18
  // onFinish?: (order: Order) => void;
20
19
  };
21
20
  return (
@@ -2,10 +2,10 @@ import { ModalProps } from '@openfun/cunningham-react';
2
2
  import {
3
3
  CertificateProduct,
4
4
  CourseLight,
5
+ Offer,
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
+ offer?: Offer;
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, 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;
@@ -1,5 +1,5 @@
1
1
  import { useJoanieApi } from 'contexts/JoanieApiContext';
2
- import { CourseProductRelation, Organization } from 'types/Joanie';
2
+ import { Offer, 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
+ offerId?: Offer['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
+ offer_id: offerId,
61
61
  });
62
62
 
63
63
  return extractArchiveId(response.url);