richie-education 3.1.3-dev2 → 3.1.3-dev24

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 (87) 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/index.spec.tsx +2 -0
  6. package/js/components/CourseGlimpse/index.tsx +2 -0
  7. package/js/components/CourseGlimpse/utils.ts +29 -30
  8. package/js/components/CourseGlimpseList/utils.ts +2 -2
  9. package/js/components/PurchaseButton/index.tsx +3 -3
  10. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +1 -3
  11. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +13 -1
  12. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +9 -7
  13. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
  14. package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
  15. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  16. package/js/components/SaleTunnel/index.spec.tsx +171 -29
  17. package/js/components/SaleTunnel/index.stories.tsx +17 -3
  18. package/js/components/SaleTunnel/index.tsx +2 -2
  19. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  20. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  21. package/js/hooks/useContractArchive/index.ts +3 -3
  22. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  23. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  24. package/js/hooks/useCourseProducts.ts +4 -8
  25. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  26. package/js/hooks/useOffering/index.ts +32 -0
  27. package/js/hooks/useTeacherCoursesSearch/index.tsx +2 -2
  28. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  29. package/js/pages/DashboardCourses/index.spec.tsx +14 -14
  30. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -14
  31. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -9
  32. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  33. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  34. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  35. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  36. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  37. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  38. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  39. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  40. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  41. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  42. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -17
  43. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  44. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  45. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -3
  46. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  47. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  48. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -4
  49. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  50. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  51. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  52. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  53. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  54. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  55. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  56. package/js/pages/TeacherDashboardTraining/index.spec.tsx +21 -29
  57. package/js/pages/TeacherDashboardTraining/index.tsx +12 -16
  58. package/js/types/Course.ts +2 -0
  59. package/js/types/Joanie.ts +34 -29
  60. package/js/types/index.ts +4 -2
  61. package/js/utils/ProductHelper/index.ts +1 -5
  62. package/js/utils/test/factories/joanie.ts +17 -25
  63. package/js/utils/test/factories/richie.ts +6 -2
  64. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  65. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  66. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  67. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  68. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  69. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  70. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  71. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  72. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  73. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -24
  74. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +18 -21
  75. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  76. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  77. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  78. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +19 -34
  79. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +34 -8
  80. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +3 -3
  81. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/_styles.scss +9 -0
  82. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +186 -140
  83. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +11 -2
  84. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +111 -24
  85. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
  86. package/package.json +1 -1
  87. package/js/hooks/useCourseProductRelation/index.ts +0 -44
@@ -11,7 +11,7 @@ import {
11
11
  UserFactory,
12
12
  } from 'utils/test/factories/richie';
13
13
  import JoanieSessionProvider from 'contexts/SessionContext/JoanieSessionProvider';
14
- import { CourseProductRelationFactory, OrganizationFactory } from 'utils/test/factories/joanie';
14
+ import { OfferingFactory, OrganizationFactory } from 'utils/test/factories/joanie';
15
15
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
16
16
  import { expectNoSpinner } from 'utils/test/expectSpinner';
17
17
  import { DashboardBreadcrumbsProvider } from 'widgets/Dashboard/contexts/DashboardBreadcrumbsContext';
@@ -46,15 +46,9 @@ describe('components/TeacherDashboardTrainingLoader', () => {
46
46
  });
47
47
 
48
48
  it('should render TeacherDashboardTrainingLoader page', async () => {
49
- const courseProductRelation = CourseProductRelationFactory().one();
50
- fetchMock.get(
51
- `https://joanie.endpoint/api/v1.0/organizations/?course_product_relation_id=${courseProductRelation.id}`,
52
- [],
53
- );
54
- fetchMock.get(
55
- `https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
56
- courseProductRelation,
57
- );
49
+ const offering = OfferingFactory().one();
50
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/organizations/?offering_id=${offering.id}`, []);
51
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/offerings/${offering.id}/`, offering);
58
52
 
59
53
  const user = UserFactory().one();
60
54
  render(
@@ -67,12 +61,12 @@ describe('components/TeacherDashboardTrainingLoader', () => {
67
61
  router={createMemoryRouter(
68
62
  [
69
63
  {
70
- path: ':courseProductRelationId',
64
+ path: ':offeringId',
71
65
  element: <TeacherDashboardTrainingLoader />,
72
66
  },
73
67
  ],
74
68
  {
75
- initialEntries: [`/${courseProductRelation.id}`],
69
+ initialEntries: [`/${offering.id}`],
76
70
  },
77
71
  )}
78
72
  />
@@ -88,12 +82,10 @@ describe('components/TeacherDashboardTrainingLoader', () => {
88
82
  await expectNoSpinner('Loading course...');
89
83
 
90
84
  nbApiCalls += 1; // organizations api call
91
- nbApiCalls += 1; // course-product-relations api call
85
+ nbApiCalls += 1; // offerings api call
92
86
  const calledUrls = fetchMock.calls().map((call) => call[0]);
93
87
  expect(calledUrls).toHaveLength(nbApiCalls);
94
- expect(calledUrls).toContain(
95
- `https://joanie.endpoint/api/v1.0/course-product-relations/${courseProductRelation.id}/`,
96
- );
88
+ expect(calledUrls).toContain(`https://joanie.endpoint/api/v1.0/offerings/${offering.id}/`);
97
89
 
98
90
  // main titles
99
91
  expect(
@@ -103,27 +95,27 @@ describe('components/TeacherDashboardTrainingLoader', () => {
103
95
  ).toBeInTheDocument();
104
96
 
105
97
  expect(
106
- screen.getAllByRole('heading', { name: capitalize(courseProductRelation.product.title) }),
98
+ screen.getAllByRole('heading', { name: capitalize(offering.product.title) }),
107
99
  ).toHaveLength(2);
108
100
 
109
- const nbCourseRun = courseProductRelation.product.target_courses.reduce(
101
+ const nbCourseRun = offering.product.target_courses.reduce(
110
102
  (acc, course) => acc + course.course_runs.length,
111
103
  0,
112
104
  );
113
105
  expect(screen.getAllByRole('link', { name: 'Go to course area' })).toHaveLength(nbCourseRun);
114
106
  });
115
107
 
116
- it('should fetch course product relation with organization id if there is one in the path', async () => {
108
+ it('should fetch offering with organization id if there is one in the path', async () => {
117
109
  const organization = OrganizationFactory().one();
118
- const courseProductRelation = CourseProductRelationFactory({
110
+ const offering = OfferingFactory({
119
111
  organizations: [organization],
120
112
  }).one();
121
113
  fetchMock.get(
122
- `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/course-product-relations/${courseProductRelation.id}/`,
123
- courseProductRelation,
114
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/${offering.id}/`,
115
+ offering,
124
116
  );
125
117
  fetchMock.get(
126
- `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?course_product_relation_id=${courseProductRelation.id}&signature_state=half_signed&page=1&page_size=25`,
118
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/contracts/?offering_id=${offering.id}&signature_state=half_signed&page=1&page_size=25`,
127
119
  [],
128
120
  );
129
121
 
@@ -138,12 +130,12 @@ describe('components/TeacherDashboardTrainingLoader', () => {
138
130
  router={createMemoryRouter(
139
131
  [
140
132
  {
141
- path: '/:organizationId/:courseProductRelationId',
133
+ path: '/:organizationId/:offeringId',
142
134
  element: <TeacherDashboardTrainingLoader />,
143
135
  },
144
136
  ],
145
137
  {
146
- initialEntries: [`/${organization.id}/${courseProductRelation.id}`],
138
+ initialEntries: [`/${organization.id}/${offering.id}`],
147
139
  },
148
140
  )}
149
141
  />
@@ -159,11 +151,11 @@ describe('components/TeacherDashboardTrainingLoader', () => {
159
151
  await expectNoSpinner('Loading course...');
160
152
 
161
153
  nbApiCalls += 1; // contracts api call
162
- nbApiCalls += 1; // course-product-relations api call
154
+ nbApiCalls += 1; // offerings api call
163
155
  const calledUrls = fetchMock.calls().map((call) => call[0]);
164
156
  expect(calledUrls).toHaveLength(nbApiCalls);
165
157
  expect(calledUrls).toContain(
166
- `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/course-product-relations/${courseProductRelation.id}/`,
158
+ `https://joanie.endpoint/api/v1.0/organizations/${organization.id}/offerings/${offering.id}/`,
167
159
  );
168
160
 
169
161
  // main titles
@@ -174,10 +166,10 @@ describe('components/TeacherDashboardTrainingLoader', () => {
174
166
  ).toBeInTheDocument();
175
167
 
176
168
  expect(
177
- screen.getAllByRole('heading', { name: capitalize(courseProductRelation.product.title) }),
169
+ screen.getAllByRole('heading', { name: capitalize(offering.product.title) }),
178
170
  ).toHaveLength(2);
179
171
 
180
- const nbCourseRun = courseProductRelation.product.target_courses.reduce(
172
+ const nbCourseRun = offering.product.target_courses.reduce(
181
173
  (acc, course) => acc + course.course_runs.length,
182
174
  0,
183
175
  );
@@ -6,37 +6,33 @@ import { DashboardLayout } from 'widgets/Dashboard/components/DashboardLayout';
6
6
  import { DashboardCard } from 'widgets/Dashboard/components/DashboardCard';
7
7
  import { Icon, IconTypeEnum } from 'components/Icon';
8
8
  import Banner, { BannerType } from 'components/Banner';
9
- import { CourseProductRelation } from 'types/Joanie';
9
+ import { Offering } from 'types/Joanie';
10
10
 
11
11
  const messages = defineMessages({
12
- errorNoCourseProductRelation: {
12
+ errorNoOffering: {
13
13
  defaultMessage: "This product doesn't exist",
14
- description: 'Message displayed when requested course product relation is not found',
15
- id: 'components.TeacherDashboardTraining.errorNoCourseProductRelation',
14
+ description: 'Message displayed when requested offering is not found',
15
+ id: 'components.TeacherDashboardTraining.errorNoOffering',
16
16
  },
17
17
  });
18
18
 
19
19
  interface TeacherDashboardTrainingProps {
20
- courseProductRelation: CourseProductRelation;
20
+ offering: Offering;
21
21
  }
22
22
 
23
- export const TeacherDashboardTraining = ({
24
- courseProductRelation,
25
- }: TeacherDashboardTrainingProps) => {
23
+ export const TeacherDashboardTraining = ({ offering }: TeacherDashboardTrainingProps) => {
26
24
  const intl = useIntl();
27
- return courseProductRelation ? (
25
+ return offering ? (
28
26
  <div className="teacher-course-page">
29
27
  <DashboardCard
30
28
  className="icon-arrow-right-rounded"
31
29
  header={
32
30
  <div>
33
31
  <div className="dashboard__title_container--small">
34
- <h2 className="dashboard__title--large">
35
- {capitalize(courseProductRelation.product.title)}
36
- </h2>
32
+ <h2 className="dashboard__title--large">{capitalize(offering.product.title)}</h2>
37
33
  </div>
38
- {courseProductRelation.product.description && (
39
- <div className="dashboard__quote">{courseProductRelation.product.description}</div>
34
+ {offering.product.description && (
35
+ <div className="dashboard__quote">{offering.product.description}</div>
40
36
  )}
41
37
  </div>
42
38
  }
@@ -44,7 +40,7 @@ export const TeacherDashboardTraining = ({
44
40
  fullWidth
45
41
  />
46
42
  <DashboardLayout.NestedSection>
47
- {courseProductRelation.product.target_courses.map((course) => (
43
+ {offering.product.target_courses.map((course) => (
48
44
  <DashboardLayout.Section key={`course_target_${course.code}`}>
49
45
  <DashboardCard
50
46
  className="icon-arrow-right-rounded"
@@ -67,7 +63,7 @@ export const TeacherDashboardTraining = ({
67
63
  </div>
68
64
  ) : (
69
65
  <Banner
70
- message={intl.formatMessage(messages.errorNoCourseProductRelation)}
66
+ message={intl.formatMessage(messages.errorNoOffering)}
71
67
  type={BannerType.ERROR}
72
68
  rounded
73
69
  />
@@ -47,6 +47,8 @@ export interface Course extends Resource {
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>;
50
52
  }
51
53
 
52
54
  export function isRichieCourse(course: Course | JoanieCourse): course is Course {
@@ -38,7 +38,7 @@ export interface Organization {
38
38
  }
39
39
 
40
40
  export interface OrganizationResourceQuery extends ResourcesQuery {
41
- course_product_relation_id?: CourseProductRelation['id'];
41
+ offering_id?: Offering['id'];
42
42
  }
43
43
 
44
44
  export interface ContractDefinition {
@@ -149,6 +149,8 @@ export interface Product {
149
149
  state: CourseState;
150
150
  instructions: Nullable<string>;
151
151
  contract_definition?: ContractDefinition;
152
+ discounted_price: Nullable<number>;
153
+ discount: Nullable<string>;
152
154
  }
153
155
 
154
156
  export interface CredentialProduct extends Product {
@@ -174,7 +176,7 @@ export interface DefinitionResourcesProduct {
174
176
  contract_definition_id: Nullable<ContractDefinition['id']>;
175
177
  }
176
178
 
177
- export interface CourseProductRelationLight {
179
+ export interface OfferingLight {
178
180
  id: string;
179
181
  course: CourseLight;
180
182
  organizations: Organization[];
@@ -182,13 +184,25 @@ export interface CourseProductRelationLight {
182
184
  created_on: string;
183
185
  }
184
186
 
185
- export interface CourseProductRelation extends CourseProductRelationLight {
186
- order_groups: OrderGroup[];
187
+ export interface OfferingRule {
188
+ discounted_price: Nullable<number>;
189
+ discount_rate: Nullable<number>;
190
+ discount_amount: Nullable<number>;
191
+ discount_start: Nullable<string>;
192
+ discount_end: Nullable<string>;
193
+ description: Nullable<string>;
194
+ nb_available_seats: Nullable<number>;
195
+ has_seat_limit: boolean;
196
+ has_seats_left: boolean;
197
+ }
198
+
199
+ export interface Offering extends OfferingLight {
187
200
  is_withdrawable: boolean;
201
+ rules: OfferingRule;
188
202
  }
189
- export function isCourseProductRelation(
190
- entity: CourseListItem | CourseProductRelationLight | RichieCourse,
191
- ): entity is CourseProductRelationLight {
203
+ export function isOffering(
204
+ entity: CourseListItem | OfferingLight | RichieCourse,
205
+ ): entity is OfferingLight {
192
206
  return 'course' in entity && 'product' in entity;
193
207
  }
194
208
 
@@ -233,7 +247,7 @@ export interface Enrollment {
233
247
  was_created_by_order: boolean;
234
248
  created_on: string;
235
249
  orders: OrderEnrollment[];
236
- product_relations: CourseProductRelation[];
250
+ offerings: Offering[];
237
251
  certificate_id: Nullable<string>;
238
252
  }
239
253
  export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj is Enrollment => {
@@ -247,7 +261,7 @@ export const isEnrollment = (obj: unknown | Enrollment | OpenEdXEnrollment): obj
247
261
  'was_created_by_order' in obj &&
248
262
  'created_on' in obj &&
249
263
  'orders' in obj &&
250
- 'product_relations' in obj &&
264
+ 'offerings' in obj &&
251
265
  'certificate_id' in obj
252
266
  );
253
267
  };
@@ -325,7 +339,6 @@ export interface Order {
325
339
  enrollment: Nullable<EnrollmentLight>;
326
340
  organization_id: Organization['id'];
327
341
  organization: Organization;
328
- order_group_id?: OrderGroup['id'];
329
342
  payment_schedule?: PaymentSchedule;
330
343
  credit_card_id?: CreditCard['id'];
331
344
  }
@@ -398,18 +411,11 @@ export interface NestedCourseOrder {
398
411
 
399
412
  export interface CourseOrderResourceQuery extends PaginatedResourceQuery {
400
413
  course_id?: CourseListItem['id'];
401
- course_product_relation_id?: CourseProductRelation['id'];
414
+ offering_id?: Offering['id'];
402
415
  organization_id?: Organization['id'];
403
416
  product_id?: Product['id'];
404
417
  }
405
418
 
406
- export interface OrderGroup {
407
- id: string;
408
- is_active: boolean;
409
- nb_seats: number;
410
- nb_available_seats: number;
411
- }
412
-
413
419
  export enum CreditCardBrand {
414
420
  MASTERCARD = 'mastercard',
415
421
  MAESTRO = 'maestro',
@@ -481,7 +487,6 @@ export interface AddressCreationPayload extends Omit<Address, 'id' | 'is_main'>
481
487
 
482
488
  interface AbstractOrderProductCreationPayload {
483
489
  product_id: Product['id'];
484
- order_group_id?: OrderGroup['id'];
485
490
  billing_address: Omit<Address, 'id' | 'is_main'>;
486
491
  has_waived_withdrawal_right: boolean;
487
492
  }
@@ -541,8 +546,8 @@ export interface CourseProductQueryFilters extends ResourcesQuery {
541
546
  id?: Product['id'];
542
547
  course_id?: CourseListItem['id'];
543
548
  }
544
- export interface CourseProductRelationQueryFilters extends PaginatedResourceQuery {
545
- id?: CourseProductRelation['id'];
549
+ export interface OfferingQueryFilters extends PaginatedResourceQuery {
550
+ id?: Offering['id'];
546
551
  organization_id?: Organization['id'];
547
552
  product_type?: ProductType;
548
553
  query?: string;
@@ -555,7 +560,7 @@ export enum ContractState {
555
560
  }
556
561
  export interface ContractResourceQuery extends PaginatedResourceQuery {
557
562
  organization_id?: Organization['id'];
558
- course_product_relation_id?: CourseProductRelation['id'];
563
+ offering_id?: Offering['id'];
559
564
  contract_ids?: Contract['id'][];
560
565
  signature_state?: ContractState;
561
566
  }
@@ -563,7 +568,7 @@ export interface ContractResourceQuery extends PaginatedResourceQuery {
563
568
  export interface OrganizationContractSignatureLinksFilters {
564
569
  contracts_ids?: string[];
565
570
  organization_id: Organization['id'];
566
- course_product_relation_ids?: CourseProductRelation['id'][];
571
+ offering_ids?: Offering['id'][];
567
572
  }
568
573
 
569
574
  export interface ContractInvitationLinkResponse {
@@ -657,10 +662,10 @@ interface APIUser {
657
662
  check: (id: string) => Promise<Response>;
658
663
  create: ({
659
664
  organization_id,
660
- course_product_relation_id,
665
+ offering_id,
661
666
  }: {
662
667
  organization_id?: Organization['id'];
663
- course_product_relation_id?: CourseProductRelation['id'];
668
+ offering_id?: Offering['id'];
664
669
  }) => Promise<{ url: string }>;
665
670
  get: (id: string) => Promise<File>;
666
671
  };
@@ -676,7 +681,7 @@ export interface API {
676
681
  ? Promise<Nullable<CourseListItem>>
677
682
  : Promise<PaginatedResponse<CourseListItem>>;
678
683
  products: {
679
- get(filters?: CourseProductQueryFilters): Promise<Nullable<CourseProductRelation>>;
684
+ get(filters?: CourseProductQueryFilters): Promise<Nullable<Offering>>;
680
685
  paymentSchedule: {
681
686
  get(filters?: CourseProductQueryFilters): Promise<Nullable<PaymentSchedule>>;
682
687
  };
@@ -709,12 +714,12 @@ export interface API {
709
714
  filters?: CourseRunFilters,
710
715
  ): CourseRunFilters extends { id: string } ? Promise<Nullable<CourseRun>> : Promise<CourseRun>;
711
716
  };
712
- courseProductRelations: {
717
+ offerings: {
713
718
  get<Filters extends PaginatedResourceQuery = PaginatedResourceQuery>(
714
719
  filters?: Filters,
715
720
  ): Filters extends { id: string }
716
- ? Promise<Nullable<CourseProductRelation>>
717
- : Promise<PaginatedResponse<CourseProductRelationLight>>;
721
+ ? Promise<Nullable<Offering>>
722
+ : Promise<PaginatedResponse<OfferingLight>>;
718
723
  };
719
724
  contractDefinitions: {
720
725
  previewTemplate(id: string): Promise<File>;
package/js/types/index.ts CHANGED
@@ -35,11 +35,13 @@ export interface CourseRun {
35
35
  title?: string;
36
36
  snapshot?: string;
37
37
  display_mode: CourseRunDisplayMode;
38
- price?: number;
38
+ price?: Nullable<number>;
39
39
  price_currency?: string;
40
40
  offer?: string;
41
- certificate_price?: number;
41
+ certificate_price?: Nullable<number>;
42
42
  certificate_offer?: string;
43
+ discounted_price: Nullable<number>;
44
+ discount: Nullable<string>;
43
45
  }
44
46
 
45
47
  export enum Priority {
@@ -1,5 +1,5 @@
1
1
  import { IntlShape } from 'react-intl';
2
- import { CourseProductRelation, Product, TargetCourse } from 'types/Joanie';
2
+ import { Product, TargetCourse } from 'types/Joanie';
3
3
  import { Maybe } from 'types/utils';
4
4
  import { IntlHelper } from 'utils/IntlHelper';
5
5
  import * as Joanie from 'types/Joanie';
@@ -44,10 +44,6 @@ export class ProductHelper {
44
44
  return IntlHelper.getLocalizedLanguages(uniqueLanguages, intl);
45
45
  }
46
46
 
47
- static getActiveOrderGroups(courseProductRelation: CourseProductRelation) {
48
- return courseProductRelation.order_groups?.filter((orderGroup) => orderGroup.is_active);
49
- }
50
-
51
47
  static hasRemainingSeats(product: Maybe<Product>) {
52
48
  if (!product) return false;
53
49
  return typeof product?.remaining_order_count !== 'number' || product.remaining_order_count > 0;
@@ -12,7 +12,7 @@ import {
12
12
  CourseLight,
13
13
  CourseListItem,
14
14
  CourseProduct,
15
- CourseProductRelation,
15
+ Offering,
16
16
  CourseRun,
17
17
  CredentialOrder,
18
18
  CredentialProduct,
@@ -28,7 +28,6 @@ import {
28
28
  NestedCredentialOrder,
29
29
  Order,
30
30
  OrderEnrollment,
31
- OrderGroup,
32
31
  PaymentInstallment,
33
32
  OrderLite,
34
33
  OrderState,
@@ -90,7 +89,7 @@ export const EnrollmentFactory = factory((): Enrollment => {
90
89
  id: faker.string.uuid(),
91
90
  course_run: CourseRunWithCourseFactory().one(),
92
91
  is_active: true,
93
- product_relations: CourseProductRelationFactory().many(1),
92
+ offerings: OfferingFactory().many(1),
94
93
  state: EnrollmentState.SET,
95
94
  was_created_by_order: false,
96
95
  created_on: faker.date.past({ years: 1 }).toISOString(),
@@ -197,7 +196,7 @@ export const CredentialProductFactory = factory((): CredentialProduct => {
197
196
  created_on: faker.date.past().toISOString(),
198
197
  title: FactoryHelper.sequence((counter) => `Certificate Product ${counter}`),
199
198
  type: ProductType.CREDENTIAL,
200
- price: faker.number.int(),
199
+ price: faker.number.int({ min: 1, max: 1000, multipleOf: 10 }),
201
200
  price_currency: faker.finance.currencyCode(),
202
201
  call_to_action: faker.lorem.words(3),
203
202
  certificate_definition: CertificationDefinitionFactory().one(),
@@ -206,6 +205,8 @@ export const CredentialProductFactory = factory((): CredentialProduct => {
206
205
  remaining_order_count: faker.number.int({ min: 1, max: 100 }),
207
206
  state: CourseStateFactory().one(),
208
207
  instructions: null,
208
+ discounted_price: null,
209
+ discount: null,
209
210
  };
210
211
  });
211
212
 
@@ -294,25 +295,6 @@ export const CourseLightFactory = factory((): CourseLight => {
294
295
  };
295
296
  });
296
297
 
297
- export const OrderGroupFactory = factory((): OrderGroup => {
298
- const seats = faker.number.int({ min: 5, max: 100 });
299
- return {
300
- id: faker.string.uuid(),
301
- is_active: true,
302
- nb_seats: seats,
303
- nb_available_seats: faker.number.int({ min: 2, max: seats }),
304
- };
305
- });
306
-
307
- export const OrderGroupFullFactory = factory((): OrderGroup => {
308
- return {
309
- id: faker.string.uuid(),
310
- is_active: true,
311
- nb_seats: faker.number.int({ min: 5, max: 100 }),
312
- nb_available_seats: 0,
313
- };
314
- });
315
-
316
298
  export const NestedCourseOrderFactory = factory((): NestedCourseOrder => {
317
299
  return {
318
300
  id: faker.string.uuid(),
@@ -329,15 +311,25 @@ export const NestedCourseOrderFactory = factory((): NestedCourseOrder => {
329
311
  };
330
312
  });
331
313
 
332
- export const CourseProductRelationFactory = factory((): CourseProductRelation => {
314
+ export const OfferingFactory = factory((): Offering => {
333
315
  return {
334
316
  id: faker.string.uuid(),
335
317
  created_on: faker.date.past().toISOString(),
336
318
  course: CourseFactory().one(),
337
319
  product: ProductFactory().one(),
338
320
  organizations: OrganizationFactory().many(1),
339
- order_groups: [],
340
321
  is_withdrawable: true,
322
+ rules: {
323
+ discounted_price: null,
324
+ discount_rate: null,
325
+ discount_amount: null,
326
+ discount_start: null,
327
+ discount_end: null,
328
+ description: null,
329
+ nb_available_seats: null,
330
+ has_seat_limit: false,
331
+ has_seats_left: true,
332
+ },
341
333
  };
342
334
  });
343
335
 
@@ -60,11 +60,11 @@ export const CourseRunFactory = factory<CourseRun>(() => {
60
60
  certificateOfferValues[Math.floor(Math.random() * certificateOfferValues.length)];
61
61
  const currency = faker.finance.currency().code;
62
62
  const price = [OfferType.FREE, OfferType.PARTIALLY_FREE].includes(offer)
63
- ? 0
63
+ ? null
64
64
  : parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
65
65
  const certificatePrice =
66
66
  certificateOffer === OfferType.FREE
67
- ? 0
67
+ ? null
68
68
  : parseFloat(faker.finance.amount({ min: 1, max: 100, symbol: currency, autoFormat: true }));
69
69
  return {
70
70
  id: faker.number.int(),
@@ -83,6 +83,8 @@ export const CourseRunFactory = factory<CourseRun>(() => {
83
83
  offer,
84
84
  certificate_price: certificatePrice,
85
85
  certificate_offer: certificateOffer,
86
+ discounted_price: null,
87
+ discount: null,
86
88
  };
87
89
  });
88
90
 
@@ -244,5 +246,7 @@ export const CourseLightFactory = factory<Course>(() => {
244
246
  certificate_price: null,
245
247
  price: null,
246
248
  price_currency: 'EUR',
249
+ discounted_price: null,
250
+ discount: null,
247
251
  };
248
252
  });
@@ -3,14 +3,14 @@ import { CredentialOrder } from 'types/Joanie';
3
3
  import {
4
4
  ContractDefinitionFactory,
5
5
  CourseFactory,
6
- CourseProductRelationFactory,
6
+ OfferingFactory,
7
7
  ProductFactory,
8
8
  } from 'utils/test/factories/joanie';
9
9
 
10
10
  export const mockCourseProductWithOrder = (order: CredentialOrder) => {
11
11
  const courseCode = order.course.code;
12
12
  const productId = order.product_id;
13
- const relation = CourseProductRelationFactory({
13
+ const offering = OfferingFactory({
14
14
  product: ProductFactory({
15
15
  id: order.product_id,
16
16
  contract_definition: order.contract ? ContractDefinitionFactory().one() : undefined,
@@ -22,7 +22,7 @@ export const mockCourseProductWithOrder = (order: CredentialOrder) => {
22
22
 
23
23
  fetchMock.get(
24
24
  `https://joanie.endpoint/api/v1.0/courses/${courseCode}/products/${productId}/`,
25
- relation,
25
+ offering,
26
26
  );
27
- return relation;
27
+ return offering;
28
28
  };
@@ -33,7 +33,7 @@ export const DashboardItemEnrollment = ({ enrollment }: DashboardItemCourseRunPr
33
33
  </div>
34
34
  </div>,
35
35
  ];
36
- enrollment.product_relations.forEach(({ product, is_withdrawable }) => {
36
+ enrollment.offerings.forEach(({ product, is_withdrawable }) => {
37
37
  if (isCertificateProduct(product)) {
38
38
  partialFooterList.push(
39
39
  <ProductCertificateFooter
@@ -249,7 +249,7 @@ describe('<ProductCertificateFooter/>', () => {
249
249
  course,
250
250
  }).one(),
251
251
  }).one();
252
- enrollment.product_relations[0].product = CertificateProductFactory().one();
252
+ enrollment.offerings[0].product = CertificateProductFactory().one();
253
253
 
254
254
  fetchMock.get(
255
255
  `https://joanie.endpoint/api/v1.0/enrollments/?was_created_by_order=false&is_active=true&page=1&page_size=${PER_PAGE.useOrdersEnrollments}`,
@@ -67,15 +67,15 @@ export const DashboardItemOrder = ({
67
67
  }: DashboardItemOrderProps) => {
68
68
  const { course } = order;
69
69
  const intl = useIntl();
70
- const { item: courseProductRelation } = useCourseProduct({
70
+ const { item: offering } = useCourseProduct({
71
71
  product_id: order.product_id,
72
72
  course_id: course.code,
73
73
  });
74
- const { product } = courseProductRelation || {};
74
+ const { product } = offering || {};
75
75
  const needsSignature = OrderHelper.orderNeedsSignature(order);
76
76
  const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
77
77
  const isActive = OrderHelper.isActive(order);
78
- const isProductPurchasable = ProductHelper.isPurchasable(courseProductRelation?.product);
78
+ const isProductPurchasable = ProductHelper.isPurchasable(offering?.product);
79
79
  const isNotResumable = !isActive && !isProductPurchasable;
80
80
  const canEnroll = OrderHelper.allowEnrollment(order);
81
81
 
@@ -57,7 +57,7 @@ describe('<DashboardItemOrder/> Contract', () => {
57
57
  contract: ContractFactory({ student_signed_on: null }).one(),
58
58
  }).one();
59
59
 
60
- // learner dashboard course page do one call to course product relation per order
60
+ // learner dashboard course page do one call to offering per order
61
61
  const { product } = mockCourseProductWithOrder(order);
62
62
 
63
63
  // overwrite useOmniscientOrders call
@@ -94,7 +94,7 @@ const Installment = ({ order }: Props) => {
94
94
 
95
95
  const PaymentMethodManager = ({ order }: Props) => {
96
96
  const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
97
- const { item: relation, states } = useCourseProduct({
97
+ const { item: offering, states } = useCourseProduct({
98
98
  course_id: order.course.code,
99
99
  product_id: order.product_id,
100
100
  });
@@ -118,9 +118,9 @@ const PaymentMethodManager = ({ order }: Props) => {
118
118
  )}
119
119
  <SaleTunnel
120
120
  {...modal}
121
- product={relation.product as CredentialProduct}
122
- course={relation.course}
123
- isWithdrawable={relation.is_withdrawable}
121
+ product={offering.product as CredentialProduct}
122
+ course={offering.course}
123
+ isWithdrawable={offering.is_withdrawable}
124
124
  />
125
125
  </>
126
126
  );
@@ -25,6 +25,6 @@ export const enrollment: Enrollment = {
25
25
  },
26
26
  languages: ['en'],
27
27
  },
28
- product_relations: [],
28
+ offerings: [],
29
29
  certificate_id: null,
30
30
  };