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
@@ -6,12 +6,11 @@ import {
6
6
  PacedCourseFactory,
7
7
  } from 'utils/test/factories/richie';
8
8
  import {
9
- CourseProductRelationFactory,
9
+ OfferingFactory,
10
10
  EnrollmentFactory,
11
11
  CredentialOrderFactory,
12
12
  ProductFactory,
13
- OrderGroupFullFactory,
14
- OrderGroupFactory,
13
+ CredentialProductFactory,
15
14
  } from 'utils/test/factories/joanie';
16
15
  import {
17
16
  CourseRun,
@@ -75,8 +74,8 @@ describe('CourseProductItem', () => {
75
74
  }).format(price);
76
75
 
77
76
  it('should display a loader until product is loaded', async () => {
78
- const relation = CourseProductRelationFactory().one();
79
- const { product } = relation;
77
+ const offering = OfferingFactory().one();
78
+ const { product } = offering;
80
79
  const productDeferred = new Deferred();
81
80
  fetchMock.get(
82
81
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -93,16 +92,16 @@ describe('CourseProductItem', () => {
93
92
 
94
93
  // - A loader should be displayed while product information are fetching
95
94
  await expectSpinner('Loading product information...');
96
- productDeferred.resolve(relation);
95
+ productDeferred.resolve(offering);
97
96
  await expectNoSpinner('Loading product information...');
98
97
  });
99
98
 
100
99
  it('renders product information for anonymous user', async () => {
101
- const relation = CourseProductRelationFactory().one();
102
- const { product } = relation;
100
+ const offering = OfferingFactory().one();
101
+ const { product } = offering;
103
102
  fetchMock.get(
104
103
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
105
- relation,
104
+ offering,
106
105
  );
107
106
 
108
107
  render(
@@ -132,7 +131,7 @@ describe('CourseProductItem', () => {
132
131
  ).not.toBeInTheDocument();
133
132
 
134
133
  // - Render all target courses information
135
- relation.product.target_courses.forEach((course) => {
134
+ offering.product.target_courses.forEach((course) => {
136
135
  const $item = screen.getByTestId(`course-item-${course.code}`);
137
136
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
138
137
  // but we want to it to visually look like a h5
@@ -151,16 +150,142 @@ describe('CourseProductItem', () => {
151
150
  expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
152
151
  });
153
152
 
153
+ it('renders discount rate for anonymous user', async () => {
154
+ const offering = OfferingFactory({
155
+ product: CredentialProductFactory({
156
+ price: 840,
157
+ price_currency: 'EUR',
158
+ }).one(),
159
+ rules: {
160
+ discounted_price: 800,
161
+ discount_rate: 0.3,
162
+ description: 'Year 2023 discount',
163
+ discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
164
+ discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
165
+ },
166
+ }).one();
167
+ const { product } = offering;
168
+ fetchMock.get(
169
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
170
+ offering,
171
+ );
172
+
173
+ render(
174
+ <CourseProductItem
175
+ course={PacedCourseFactory({ code: '00000' }).one()}
176
+ productId={product.id}
177
+ />,
178
+ { queryOptions: { client: createTestQueryClient({ user: null }) } },
179
+ );
180
+
181
+ await screen.findByRole('heading', { level: 3, name: product.title });
182
+
183
+ // - Render discount information
184
+ // Original price should be displayed as a del element
185
+ const originalPriceLabel = screen.getByText('Original price:');
186
+ expect(originalPriceLabel.classList.contains('offscreen')).toBe(true);
187
+ const originalPrice = screen.getByText(
188
+ priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
189
+ );
190
+ expect(originalPrice.tagName).toBe('DEL');
191
+ expect(originalPrice.getAttribute('aria-describedby')).toEqual(originalPriceLabel.id);
192
+
193
+ // Discounted price should be displayed as an ins element
194
+ const discountedPriceLabel = screen.getByText('Discounted price:');
195
+ expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
196
+ const discountedPrice = screen.getByText(
197
+ priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
198
+ /(\u202F|\u00a0)/g,
199
+ ' ',
200
+ ),
201
+ );
202
+ expect(discountedPrice.tagName).toBe('INS');
203
+ expect(discountedPrice.getAttribute('aria-describedby')).toEqual(discountedPriceLabel.id);
204
+
205
+ // Discount description should be displayed
206
+ screen.getByText('Year 2023 discount');
207
+
208
+ // Discount rate should be displayed
209
+ screen.getByText('-30%');
210
+
211
+ // Discount date range should be displayed
212
+ screen.getByText('from Jan 01, 2023');
213
+ screen.getByText('to Dec 31, 2023');
214
+ });
215
+
216
+ it('renders discount amount for anonymous user', async () => {
217
+ const offering = OfferingFactory({
218
+ product: CredentialProductFactory({
219
+ price: 840,
220
+ price_currency: 'EUR',
221
+ }).one(),
222
+ rules: {
223
+ discounted_price: 800,
224
+ discount_amount: 40,
225
+ description: 'Year 2023 discount',
226
+ discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
227
+ discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
228
+ },
229
+ }).one();
230
+ const { product } = offering;
231
+ fetchMock.get(
232
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
233
+ offering,
234
+ );
235
+
236
+ render(
237
+ <CourseProductItem
238
+ course={PacedCourseFactory({ code: '00000' }).one()}
239
+ productId={product.id}
240
+ />,
241
+ { queryOptions: { client: createTestQueryClient({ user: null }) } },
242
+ );
243
+
244
+ await screen.findByRole('heading', { level: 3, name: product.title });
245
+
246
+ // - Render discount information
247
+ // Original price should be displayed as a del element
248
+ const originalPriceLabel = screen.getByText('Original price:');
249
+ expect(originalPriceLabel.classList.contains('offscreen')).toBe(true);
250
+ const originalPrice = screen.getByText(
251
+ priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
252
+ );
253
+ expect(originalPrice.tagName).toBe('DEL');
254
+ expect(originalPrice.getAttribute('aria-describedby')).toEqual(originalPriceLabel.id);
255
+
256
+ // Discounted price should be displayed as an ins element
257
+ const discountedPriceLabel = screen.getByText('Discounted price:');
258
+ expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
259
+ const discountedPrice = screen.getByText(
260
+ priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
261
+ /(\u202F|\u00a0)/g,
262
+ ' ',
263
+ ),
264
+ );
265
+ expect(discountedPrice.tagName).toBe('INS');
266
+ expect(discountedPrice.getAttribute('aria-describedby')).toEqual(discountedPriceLabel.id);
267
+
268
+ // Discount description should be displayed
269
+ screen.getByText('Year 2023 discount');
270
+
271
+ // Discount rate should be displayed
272
+ screen.getByText(priceFormatter(product.price_currency, -40).replace(/(\u202F|\u00a0)/g, ' '));
273
+
274
+ // Discount date range should be displayed
275
+ screen.getByText('from Jan 01, 2023');
276
+ screen.getByText('to Dec 31, 2023');
277
+ });
278
+
154
279
  it('does not render <CertificateItem /> if product do not have a certificate', async () => {
155
- const relation = CourseProductRelationFactory({
280
+ const offering = OfferingFactory({
156
281
  product: ProductFactory({
157
282
  certificate_definition: undefined,
158
283
  }).one(),
159
284
  }).one();
160
- const { product } = relation;
285
+ const { product } = offering;
161
286
  fetchMock.get(
162
287
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
163
- relation,
288
+ offering,
164
289
  );
165
290
 
166
291
  render(
@@ -179,16 +304,16 @@ describe('CourseProductItem', () => {
179
304
  });
180
305
 
181
306
  it('renders product information in compact mode', async () => {
182
- const relation = CourseProductRelationFactory().one();
307
+ const offering = OfferingFactory().one();
183
308
  fetchMock.get(
184
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
185
- relation,
309
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
310
+ offering,
186
311
  );
187
312
 
188
313
  const { container } = render(
189
314
  <CourseProductItem
190
315
  course={PacedCourseFactory({ code: '00000' }).one()}
191
- productId={relation.product.id}
316
+ productId={offering.product.id}
192
317
  compact
193
318
  />,
194
319
  { queryOptions: { client: createTestQueryClient({ user: null }) } },
@@ -196,14 +321,14 @@ describe('CourseProductItem', () => {
196
321
 
197
322
  // In the header, we should display the product title, the product price
198
323
  // and product date range and languages
199
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
324
+ await screen.findByRole('heading', { level: 3, name: offering.product.title });
200
325
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
201
326
  // but we want to it to visually look like a h6
202
327
 
203
328
  const $price = screen.getByText(
204
329
  // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
205
330
  // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
206
- priceFormatter(relation.product.price_currency, relation.product.price).replace(
331
+ priceFormatter(offering.product.price_currency, offering.product.price).replace(
207
332
  /(\u202F|\u00a0)/g,
208
333
  ' ',
209
334
  ),
@@ -219,7 +344,7 @@ describe('CourseProductItem', () => {
219
344
  expect($productWidgetContent).not.toBeInTheDocument();
220
345
 
221
346
  // - Any target courses information should be displayed
222
- relation.product.target_courses.forEach((course) => {
347
+ offering.product.target_courses.forEach((course) => {
223
348
  const $item = screen.queryByTestId(`course-item-${course.code}`);
224
349
  expect($item).not.toBeInTheDocument();
225
350
  });
@@ -228,7 +353,7 @@ describe('CourseProductItem', () => {
228
353
  expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
229
354
 
230
355
  // - Render a login button
231
- screen.getByRole('button', { name: `Login to purchase "${relation.product.title}"` });
356
+ screen.getByRole('button', { name: `Login to purchase "${offering.product.title}"` });
232
357
  // - Does not render PurchaseButton cta
233
358
  expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
234
359
  });
@@ -236,8 +361,8 @@ describe('CourseProductItem', () => {
236
361
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
237
362
  'renders product informations for %s order',
238
363
  async (state) => {
239
- const relation = CourseProductRelationFactory().one();
240
- const { product } = relation;
364
+ const offering = OfferingFactory().one();
365
+ const { product } = offering;
241
366
  const order = CredentialOrderFactory({
242
367
  product_id: product.id,
243
368
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -247,7 +372,7 @@ describe('CourseProductItem', () => {
247
372
 
248
373
  fetchMock.get(
249
374
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
250
- relation,
375
+ offering,
251
376
  );
252
377
  const orderQueryParameters = {
253
378
  course_code: order.course.code,
@@ -261,13 +386,13 @@ describe('CourseProductItem', () => {
261
386
  render(
262
387
  <CourseProductItem
263
388
  course={PacedCourseFactory({ code: '00000' }).one()}
264
- productId={relation.product.id}
389
+ productId={offering.product.id}
265
390
  />,
266
391
  );
267
392
 
268
393
  // In the header, we should display the product title, the product price
269
394
  // and product date range and languages
270
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
395
+ await screen.findByRole('heading', { level: 3, name: offering.product.title });
271
396
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
272
397
  // but we want to it to visually look like a h6
273
398
 
@@ -297,8 +422,8 @@ describe('CourseProductItem', () => {
297
422
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
298
423
  'renders product informations for %s order in compact mode',
299
424
  async (state) => {
300
- const relation = CourseProductRelationFactory().one();
301
- const { product } = relation;
425
+ const offering = OfferingFactory().one();
426
+ const { product } = offering;
302
427
  const order = CredentialOrderFactory({
303
428
  product_id: product.id,
304
429
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -308,7 +433,7 @@ describe('CourseProductItem', () => {
308
433
 
309
434
  fetchMock.get(
310
435
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
311
- relation,
436
+ offering,
312
437
  );
313
438
  const orderQueryParameters = {
314
439
  course_code: order.course.code,
@@ -322,14 +447,14 @@ describe('CourseProductItem', () => {
322
447
  render(
323
448
  <CourseProductItem
324
449
  course={PacedCourseFactory({ code: '00000' }).one()}
325
- productId={relation.product.id}
450
+ productId={offering.product.id}
326
451
  compact
327
452
  />,
328
453
  );
329
454
 
330
455
  // In the header, we should display the product title, the product price
331
456
  // and product date range and languages
332
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
457
+ await screen.findByRole('heading', { level: 3, name: offering.product.title });
333
458
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
334
459
  // but we want to it to visually look like a h6
335
460
 
@@ -339,7 +464,7 @@ describe('CourseProductItem', () => {
339
464
  expect($enrolledInfo.classList.contains('h6')).toBe(true);
340
465
 
341
466
  // - Any target courses information should be displayed
342
- relation.product.target_courses.forEach((course) => {
467
+ offering.product.target_courses.forEach((course) => {
343
468
  const $item = screen.queryByTestId(`course-item-${course.code}`);
344
469
  expect($item).not.toBeInTheDocument();
345
470
  });
@@ -350,8 +475,8 @@ describe('CourseProductItem', () => {
350
475
  );
351
476
 
352
477
  it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
353
- const relation = CourseProductRelationFactory().one();
354
- const { product } = relation;
478
+ const offering = OfferingFactory().one();
479
+ const { product } = offering;
355
480
  const order = CredentialOrderFactory({
356
481
  product_id: product.id,
357
482
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -361,7 +486,7 @@ describe('CourseProductItem', () => {
361
486
 
362
487
  fetchMock.get(
363
488
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
364
- relation,
489
+ offering,
365
490
  );
366
491
  const orderQueryParameters = {
367
492
  course_code: order.course.code,
@@ -412,17 +537,17 @@ describe('CourseProductItem', () => {
412
537
  it.each(ENROLLABLE_ORDER_STATES)(
413
538
  'renders product informations for a %s order in compact mode',
414
539
  async (state) => {
415
- const relation = CourseProductRelationFactory().one();
540
+ const offering = OfferingFactory().one();
416
541
  const order: CredentialOrder = CredentialOrderFactory({
417
- product_id: relation.product.id,
542
+ product_id: offering.product.id,
418
543
  course: PacedCourseFactory({ code: '00000' }).one(),
419
- target_courses: relation.product.target_courses,
544
+ target_courses: offering.product.target_courses,
420
545
  state,
421
546
  }).one();
422
547
 
423
548
  fetchMock.get(
424
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
425
- relation,
549
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
550
+ offering,
426
551
  );
427
552
  const orderQueryParameters = {
428
553
  product_id: order.product_id,
@@ -436,14 +561,14 @@ describe('CourseProductItem', () => {
436
561
 
437
562
  render(
438
563
  <CourseProductItem
439
- productId={relation.product.id}
564
+ productId={offering.product.id}
440
565
  course={PacedCourseFactory({ code: '00000' }).one()}
441
566
  compact
442
567
  />,
443
568
  );
444
569
 
445
570
  // Wait for product information to be fetched
446
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
571
+ await screen.findByRole('heading', { level: 3, name: offering.product.title });
447
572
 
448
573
  // - In place of product price, a label should be displayed
449
574
  const $enrolledInfo = await screen.findByText('Purchased');
@@ -480,8 +605,8 @@ describe('CourseProductItem', () => {
480
605
  );
481
606
 
482
607
  it('renders enrollment information when user is enrolled to a course run', async () => {
483
- const relation = CourseProductRelationFactory().one();
484
- const { product } = relation;
608
+ const offering = OfferingFactory().one();
609
+ const { product } = offering;
485
610
  // - Create an order with an active enrollment
486
611
  const enrollment: Enrollment = EnrollmentFactory({
487
612
  course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
@@ -495,7 +620,7 @@ describe('CourseProductItem', () => {
495
620
 
496
621
  fetchMock.get(
497
622
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
498
- relation,
623
+ offering,
499
624
  );
500
625
  const orderQueryParameters = {
501
626
  product_id: order.product_id,
@@ -547,8 +672,8 @@ describe('CourseProductItem', () => {
547
672
  it.each(PURCHASABLE_ORDER_STATES)(
548
673
  'renders sale tunnel button if user already has a %s order',
549
674
  async (state) => {
550
- const relation = CourseProductRelationFactory().one();
551
- const { product } = relation;
675
+ const offering = OfferingFactory().one();
676
+ const { product } = offering;
552
677
  const order = CredentialOrderFactory({
553
678
  product_id: product.id,
554
679
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -557,7 +682,7 @@ describe('CourseProductItem', () => {
557
682
  }).one();
558
683
  fetchMock.get(
559
684
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
560
- relation,
685
+ offering,
561
686
  );
562
687
  const orderQueryParameters = {
563
688
  product_id: order.product_id,
@@ -588,7 +713,7 @@ describe('CourseProductItem', () => {
588
713
  expect($price.classList.contains('h6')).toBe(true);
589
714
 
590
715
  // - Render all target courses information
591
- relation.product.target_courses.forEach((course) => {
716
+ offering.product.target_courses.forEach((course) => {
592
717
  const $item = screen.getByTestId(`course-item-${course.code}`);
593
718
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
594
719
  // but we want to it to visually look like a h5
@@ -603,11 +728,11 @@ describe('CourseProductItem', () => {
603
728
  );
604
729
 
605
730
  it('renders sale tunnel button if user already has a canceled order', async () => {
606
- const relation = CourseProductRelationFactory().one();
607
- const { product } = relation;
731
+ const offering = OfferingFactory().one();
732
+ const { product } = offering;
608
733
  fetchMock.get(
609
734
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
610
- relation,
735
+ offering,
611
736
  );
612
737
  const orderQueryParameters = {
613
738
  product_id: product.id,
@@ -638,7 +763,7 @@ describe('CourseProductItem', () => {
638
763
  expect($price.classList.contains('h6')).toBe(true);
639
764
 
640
765
  // - Render all target courses information
641
- relation.product.target_courses.forEach((course) => {
766
+ offering.product.target_courses.forEach((course) => {
642
767
  const $item = screen.getByTestId(`course-item-${course.code}`);
643
768
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
644
769
  // but we want to it to visually look like a h5
@@ -652,7 +777,7 @@ describe('CourseProductItem', () => {
652
777
  });
653
778
 
654
779
  it('renders error message when product fetching has failed', async () => {
655
- const { product } = CourseProductRelationFactory().one();
780
+ const { product } = OfferingFactory().one();
656
781
 
657
782
  fetchMock.get(
658
783
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -673,10 +798,13 @@ describe('CourseProductItem', () => {
673
798
  });
674
799
 
675
800
  it('renders a warning message that tells that no seats are left', async () => {
676
- const relation = CourseProductRelationFactory({
677
- order_groups: [OrderGroupFullFactory().one()],
801
+ const offering = OfferingFactory({
802
+ rules: {
803
+ nb_available_seats: 0,
804
+ has_seats_left: false,
805
+ },
678
806
  }).one();
679
- const { product } = relation;
807
+ const { product } = offering;
680
808
  const order = CredentialOrderFactory({
681
809
  product_id: product.id,
682
810
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -685,7 +813,7 @@ describe('CourseProductItem', () => {
685
813
  }).one();
686
814
  fetchMock.get(
687
815
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
688
- relation,
816
+ offering,
689
817
  );
690
818
  const orderQueryParameters = {
691
819
  product_id: order.product_id,
@@ -710,86 +838,4 @@ describe('CourseProductItem', () => {
710
838
  expect(screen.queryByRole('button', { name: product.call_to_action })).not.toBeInTheDocument();
711
839
  screen.getByText('Sorry, no seats available for now');
712
840
  });
713
-
714
- it('renders one payment button when one of two order groups is full', async () => {
715
- const relation = CourseProductRelationFactory({
716
- order_groups: [OrderGroupFullFactory().one(), OrderGroupFactory().one()],
717
- }).one();
718
- const { product } = relation;
719
- const order = CredentialOrderFactory({
720
- product_id: product.id,
721
- course: PacedCourseFactory({ code: '00000' }).one(),
722
- target_courses: product.target_courses,
723
- state: OrderState.DRAFT,
724
- }).one();
725
- fetchMock.get(
726
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
727
- relation,
728
- );
729
- const orderQueryParameters = {
730
- product_id: order.product_id,
731
- course_code: order.course?.code,
732
- state: NOT_CANCELED_ORDER_STATES,
733
- };
734
- fetchMock.get(
735
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
736
- [order],
737
- );
738
-
739
- render(
740
- <CourseProductItem
741
- productId={product.id}
742
- course={PacedCourseFactory({ code: '00000' }).one()}
743
- />,
744
- );
745
-
746
- // wait for component to be fully loaded
747
- await screen.findByRole('heading', { level: 3, name: product.title });
748
-
749
- expect(screen.queryByText('Sorry, no seats available for now')).not.toBeInTheDocument();
750
- screen.getByRole('button', { name: product.call_to_action });
751
- screen.getByText(relation.order_groups[1].nb_available_seats + ' remaining seats');
752
- });
753
-
754
- it('renders mutliple payment button when there are multiple order groups', async () => {
755
- const relation = CourseProductRelationFactory({
756
- order_groups: [OrderGroupFactory().one(), OrderGroupFactory({ nb_available_seats: 1 }).one()],
757
- }).one();
758
- const { product } = relation;
759
- const order = CredentialOrderFactory({
760
- product_id: product.id,
761
- course: PacedCourseFactory({ code: '00000' }).one(),
762
- target_courses: product.target_courses,
763
- state: OrderState.DRAFT,
764
- }).one();
765
- fetchMock.get(
766
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
767
- relation,
768
- );
769
- const orderQueryParameters = {
770
- product_id: order.product_id,
771
- course_code: order.course?.code,
772
- state: NOT_CANCELED_ORDER_STATES,
773
- };
774
- fetchMock.get(
775
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
776
- [order],
777
- );
778
-
779
- render(
780
- <CourseProductItem
781
- productId={product.id}
782
- course={PacedCourseFactory({ code: '00000' }).one()}
783
- />,
784
- );
785
-
786
- // wait for component to be fully loaded
787
- await screen.findByRole('heading', { level: 3, name: product.title });
788
-
789
- expect(screen.queryByText('Sorry, no seats available for now')).not.toBeInTheDocument();
790
- expect(screen.getAllByTestId('PurchaseButton__cta')).toHaveLength(2);
791
- expect(screen.getAllByRole('button', { name: product.call_to_action })).toHaveLength(2);
792
- screen.getByText(relation.order_groups[0].nb_available_seats + ' remaining seats');
793
- screen.getByText('Last remaining seat!');
794
- });
795
841
  });
@@ -3,7 +3,7 @@ import { QueryClientProvider } from '@tanstack/react-query';
3
3
  import fetchMock from 'fetch-mock';
4
4
  import { StorybookHelper } from 'utils/StorybookHelper';
5
5
  import {
6
- CourseProductRelationFactory,
6
+ OfferingFactory,
7
7
  CourseRunFactory,
8
8
  CredentialOrderFactory,
9
9
  CredentialProductFactory,
@@ -22,7 +22,16 @@ const render = (args: CourseProductItemProps, options?: Maybe<{ order: Credentia
22
22
  fetchMock.get(`http://localhost:8071/api/v1.0/addresses/`, [], { overwriteRoutes: true });
23
23
  fetchMock.get(
24
24
  `http://localhost:8071/api/v1.0/courses/${args.course.code}/products/${args.productId}/`,
25
- CourseProductRelationFactory({ product: CredentialProductFactory().one() }).one(),
25
+ OfferingFactory({
26
+ product: CredentialProductFactory({
27
+ price: 840,
28
+ price_currency: 'EUR',
29
+ }).one(),
30
+ rules: {
31
+ discounted_price: 800,
32
+ discount_rate: 0.3,
33
+ },
34
+ }).one(),
26
35
  { overwriteRoutes: true },
27
36
  );
28
37
  fetchMock.get(