richie-education 3.1.3-dev11 → 3.1.3-dev15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/js/api/joanie.ts +8 -8
  2. package/js/components/ContractFrame/OrganizationContractFrame.spec.tsx +11 -20
  3. package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
  4. package/js/components/CourseGlimpse/utils.ts +22 -35
  5. package/js/components/CourseGlimpseList/utils.ts +2 -2
  6. package/js/components/PurchaseButton/index.tsx +3 -3
  7. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -10
  8. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +5 -3
  9. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  10. package/js/components/SaleTunnel/index.spec.tsx +76 -63
  11. package/js/components/SaleTunnel/index.tsx +2 -2
  12. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  13. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  14. package/js/hooks/useContractArchive/index.ts +3 -3
  15. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  16. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  17. package/js/hooks/useCourseProducts.ts +4 -8
  18. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  19. package/js/hooks/useOffer/index.ts +32 -0
  20. package/js/hooks/useTeacherCoursesSearch/index.tsx +4 -4
  21. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  22. package/js/pages/DashboardCourses/index.spec.tsx +14 -17
  23. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +8 -14
  24. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -12
  25. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  26. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  27. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  28. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  29. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  30. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  31. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  32. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  33. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  34. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  35. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -23
  36. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  37. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  38. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -6
  39. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  40. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  41. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -7
  42. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  43. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  44. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  45. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  46. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  47. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  48. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  49. package/js/pages/TeacherDashboardTraining/index.spec.tsx +25 -33
  50. package/js/pages/TeacherDashboardTraining/index.tsx +12 -20
  51. package/js/types/Joanie.ts +25 -22
  52. package/js/utils/test/factories/joanie.ts +14 -11
  53. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  54. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  55. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  56. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  57. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  58. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  59. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  60. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  61. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  62. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -27
  63. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +16 -25
  64. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  65. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  66. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  67. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +10 -18
  68. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +81 -99
  69. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
  70. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +20 -31
  71. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
  72. package/package.json +1 -1
  73. package/js/hooks/useCourseProductRelation/index.ts +0 -44
@@ -6,7 +6,7 @@ import {
6
6
  PacedCourseFactory,
7
7
  } from 'utils/test/factories/richie';
8
8
  import {
9
- CourseProductRelationFactory,
9
+ OfferFactory,
10
10
  EnrollmentFactory,
11
11
  CredentialOrderFactory,
12
12
  ProductFactory,
@@ -74,8 +74,8 @@ describe('CourseProductItem', () => {
74
74
  }).format(price);
75
75
 
76
76
  it('should display a loader until product is loaded', async () => {
77
- const relation = CourseProductRelationFactory().one();
78
- const { product } = relation;
77
+ const offer = OfferFactory().one();
78
+ const { product } = offer;
79
79
  const productDeferred = new Deferred();
80
80
  fetchMock.get(
81
81
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -92,17 +92,14 @@ describe('CourseProductItem', () => {
92
92
 
93
93
  // - A loader should be displayed while product information are fetching
94
94
  await expectSpinner('Loading product information...');
95
- productDeferred.resolve(relation);
95
+ productDeferred.resolve(offer);
96
96
  await expectNoSpinner('Loading product information...');
97
97
  });
98
98
 
99
99
  it('renders product information for anonymous user', async () => {
100
- const relation = CourseProductRelationFactory().one();
101
- const { product } = relation;
102
- fetchMock.get(
103
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
104
- relation,
105
- );
100
+ const offer = OfferFactory().one();
101
+ const { product } = offer;
102
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
106
103
 
107
104
  render(
108
105
  <CourseProductItem
@@ -131,7 +128,7 @@ describe('CourseProductItem', () => {
131
128
  ).not.toBeInTheDocument();
132
129
 
133
130
  // - Render all target courses information
134
- relation.product.target_courses.forEach((course) => {
131
+ offer.product.target_courses.forEach((course) => {
135
132
  const $item = screen.getByTestId(`course-item-${course.code}`);
136
133
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
137
134
  // but we want to it to visually look like a h5
@@ -151,22 +148,21 @@ describe('CourseProductItem', () => {
151
148
  });
152
149
 
153
150
  it('renders discount rate for anonymous user', async () => {
154
- const relation = CourseProductRelationFactory({
151
+ const offer = OfferFactory({
155
152
  product: CredentialProductFactory({
156
153
  price: 840,
157
154
  price_currency: 'EUR',
158
155
  }).one(),
159
- discounted_price: 800,
160
- discount_rate: 0.3,
161
- description: 'Year 2023 discount',
162
- discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
163
- discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
156
+ rules: {
157
+ discounted_price: 800,
158
+ discount_rate: 0.3,
159
+ description: 'Year 2023 discount',
160
+ discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
161
+ discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
162
+ },
164
163
  }).one();
165
- const { product } = relation;
166
- fetchMock.get(
167
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
168
- relation,
169
- );
164
+ const { product } = offer;
165
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
170
166
 
171
167
  render(
172
168
  <CourseProductItem
@@ -192,7 +188,7 @@ describe('CourseProductItem', () => {
192
188
  const discountedPriceLabel = screen.getByText('Discounted price:');
193
189
  expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
194
190
  const discountedPrice = screen.getByText(
195
- priceFormatter(product.price_currency, relation.discounted_price!).replace(
191
+ priceFormatter(product.price_currency, offer.rules.discounted_price!).replace(
196
192
  /(\u202F|\u00a0)/g,
197
193
  ' ',
198
194
  ),
@@ -212,22 +208,21 @@ describe('CourseProductItem', () => {
212
208
  });
213
209
 
214
210
  it('renders discount amount for anonymous user', async () => {
215
- const relation = CourseProductRelationFactory({
211
+ const offer = OfferFactory({
216
212
  product: CredentialProductFactory({
217
213
  price: 840,
218
214
  price_currency: 'EUR',
219
215
  }).one(),
220
- discounted_price: 800,
221
- discount_amount: 40,
222
- description: 'Year 2023 discount',
223
- discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
224
- discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
216
+ rules: {
217
+ discounted_price: 800,
218
+ discount_amount: 40,
219
+ description: 'Year 2023 discount',
220
+ discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
221
+ discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
222
+ },
225
223
  }).one();
226
- const { product } = relation;
227
- fetchMock.get(
228
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
229
- relation,
230
- );
224
+ const { product } = offer;
225
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
231
226
 
232
227
  render(
233
228
  <CourseProductItem
@@ -253,7 +248,7 @@ describe('CourseProductItem', () => {
253
248
  const discountedPriceLabel = screen.getByText('Discounted price:');
254
249
  expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
255
250
  const discountedPrice = screen.getByText(
256
- priceFormatter(product.price_currency, relation.discounted_price!).replace(
251
+ priceFormatter(product.price_currency, offer.rules.discounted_price!).replace(
257
252
  /(\u202F|\u00a0)/g,
258
253
  ' ',
259
254
  ),
@@ -273,16 +268,13 @@ describe('CourseProductItem', () => {
273
268
  });
274
269
 
275
270
  it('does not render <CertificateItem /> if product do not have a certificate', async () => {
276
- const relation = CourseProductRelationFactory({
271
+ const offer = OfferFactory({
277
272
  product: ProductFactory({
278
273
  certificate_definition: undefined,
279
274
  }).one(),
280
275
  }).one();
281
- const { product } = relation;
282
- fetchMock.get(
283
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
284
- relation,
285
- );
276
+ const { product } = offer;
277
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
286
278
 
287
279
  render(
288
280
  <CourseProductItem
@@ -300,16 +292,16 @@ describe('CourseProductItem', () => {
300
292
  });
301
293
 
302
294
  it('renders product information in compact mode', async () => {
303
- const relation = CourseProductRelationFactory().one();
295
+ const offer = OfferFactory().one();
304
296
  fetchMock.get(
305
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
306
- relation,
297
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
298
+ offer,
307
299
  );
308
300
 
309
301
  const { container } = render(
310
302
  <CourseProductItem
311
303
  course={PacedCourseFactory({ code: '00000' }).one()}
312
- productId={relation.product.id}
304
+ productId={offer.product.id}
313
305
  compact
314
306
  />,
315
307
  { queryOptions: { client: createTestQueryClient({ user: null }) } },
@@ -317,14 +309,14 @@ describe('CourseProductItem', () => {
317
309
 
318
310
  // In the header, we should display the product title, the product price
319
311
  // and product date range and languages
320
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
312
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
321
313
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
322
314
  // but we want to it to visually look like a h6
323
315
 
324
316
  const $price = screen.getByText(
325
317
  // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
326
318
  // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
327
- priceFormatter(relation.product.price_currency, relation.product.price).replace(
319
+ priceFormatter(offer.product.price_currency, offer.product.price).replace(
328
320
  /(\u202F|\u00a0)/g,
329
321
  ' ',
330
322
  ),
@@ -340,7 +332,7 @@ describe('CourseProductItem', () => {
340
332
  expect($productWidgetContent).not.toBeInTheDocument();
341
333
 
342
334
  // - Any target courses information should be displayed
343
- relation.product.target_courses.forEach((course) => {
335
+ offer.product.target_courses.forEach((course) => {
344
336
  const $item = screen.queryByTestId(`course-item-${course.code}`);
345
337
  expect($item).not.toBeInTheDocument();
346
338
  });
@@ -349,7 +341,7 @@ describe('CourseProductItem', () => {
349
341
  expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
350
342
 
351
343
  // - Render a login button
352
- screen.getByRole('button', { name: `Login to purchase "${relation.product.title}"` });
344
+ screen.getByRole('button', { name: `Login to purchase "${offer.product.title}"` });
353
345
  // - Does not render PurchaseButton cta
354
346
  expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
355
347
  });
@@ -357,8 +349,8 @@ describe('CourseProductItem', () => {
357
349
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
358
350
  'renders product informations for %s order',
359
351
  async (state) => {
360
- const relation = CourseProductRelationFactory().one();
361
- const { product } = relation;
352
+ const offer = OfferFactory().one();
353
+ const { product } = offer;
362
354
  const order = CredentialOrderFactory({
363
355
  product_id: product.id,
364
356
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -368,7 +360,7 @@ describe('CourseProductItem', () => {
368
360
 
369
361
  fetchMock.get(
370
362
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
371
- relation,
363
+ offer,
372
364
  );
373
365
  const orderQueryParameters = {
374
366
  course_code: order.course.code,
@@ -382,13 +374,13 @@ describe('CourseProductItem', () => {
382
374
  render(
383
375
  <CourseProductItem
384
376
  course={PacedCourseFactory({ code: '00000' }).one()}
385
- productId={relation.product.id}
377
+ productId={offer.product.id}
386
378
  />,
387
379
  );
388
380
 
389
381
  // In the header, we should display the product title, the product price
390
382
  // and product date range and languages
391
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
383
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
392
384
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
393
385
  // but we want to it to visually look like a h6
394
386
 
@@ -418,8 +410,8 @@ describe('CourseProductItem', () => {
418
410
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
419
411
  'renders product informations for %s order in compact mode',
420
412
  async (state) => {
421
- const relation = CourseProductRelationFactory().one();
422
- const { product } = relation;
413
+ const offer = OfferFactory().one();
414
+ const { product } = offer;
423
415
  const order = CredentialOrderFactory({
424
416
  product_id: product.id,
425
417
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -429,7 +421,7 @@ describe('CourseProductItem', () => {
429
421
 
430
422
  fetchMock.get(
431
423
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
432
- relation,
424
+ offer,
433
425
  );
434
426
  const orderQueryParameters = {
435
427
  course_code: order.course.code,
@@ -443,14 +435,14 @@ describe('CourseProductItem', () => {
443
435
  render(
444
436
  <CourseProductItem
445
437
  course={PacedCourseFactory({ code: '00000' }).one()}
446
- productId={relation.product.id}
438
+ productId={offer.product.id}
447
439
  compact
448
440
  />,
449
441
  );
450
442
 
451
443
  // In the header, we should display the product title, the product price
452
444
  // and product date range and languages
453
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
445
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
454
446
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
455
447
  // but we want to it to visually look like a h6
456
448
 
@@ -460,7 +452,7 @@ describe('CourseProductItem', () => {
460
452
  expect($enrolledInfo.classList.contains('h6')).toBe(true);
461
453
 
462
454
  // - Any target courses information should be displayed
463
- relation.product.target_courses.forEach((course) => {
455
+ offer.product.target_courses.forEach((course) => {
464
456
  const $item = screen.queryByTestId(`course-item-${course.code}`);
465
457
  expect($item).not.toBeInTheDocument();
466
458
  });
@@ -471,8 +463,8 @@ describe('CourseProductItem', () => {
471
463
  );
472
464
 
473
465
  it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
474
- const relation = CourseProductRelationFactory().one();
475
- const { product } = relation;
466
+ const offer = OfferFactory().one();
467
+ const { product } = offer;
476
468
  const order = CredentialOrderFactory({
477
469
  product_id: product.id,
478
470
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -480,10 +472,7 @@ describe('CourseProductItem', () => {
480
472
  state,
481
473
  }).one();
482
474
 
483
- fetchMock.get(
484
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
485
- relation,
486
- );
475
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
487
476
  const orderQueryParameters = {
488
477
  course_code: order.course.code,
489
478
  product_id: order.product_id,
@@ -533,17 +522,17 @@ describe('CourseProductItem', () => {
533
522
  it.each(ENROLLABLE_ORDER_STATES)(
534
523
  'renders product informations for a %s order in compact mode',
535
524
  async (state) => {
536
- const relation = CourseProductRelationFactory().one();
525
+ const offer = OfferFactory().one();
537
526
  const order: CredentialOrder = CredentialOrderFactory({
538
- product_id: relation.product.id,
527
+ product_id: offer.product.id,
539
528
  course: PacedCourseFactory({ code: '00000' }).one(),
540
- target_courses: relation.product.target_courses,
529
+ target_courses: offer.product.target_courses,
541
530
  state,
542
531
  }).one();
543
532
 
544
533
  fetchMock.get(
545
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
546
- relation,
534
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
535
+ offer,
547
536
  );
548
537
  const orderQueryParameters = {
549
538
  product_id: order.product_id,
@@ -557,14 +546,14 @@ describe('CourseProductItem', () => {
557
546
 
558
547
  render(
559
548
  <CourseProductItem
560
- productId={relation.product.id}
549
+ productId={offer.product.id}
561
550
  course={PacedCourseFactory({ code: '00000' }).one()}
562
551
  compact
563
552
  />,
564
553
  );
565
554
 
566
555
  // Wait for product information to be fetched
567
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
556
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
568
557
 
569
558
  // - In place of product price, a label should be displayed
570
559
  const $enrolledInfo = await screen.findByText('Purchased');
@@ -601,8 +590,8 @@ describe('CourseProductItem', () => {
601
590
  );
602
591
 
603
592
  it('renders enrollment information when user is enrolled to a course run', async () => {
604
- const relation = CourseProductRelationFactory().one();
605
- const { product } = relation;
593
+ const offer = OfferFactory().one();
594
+ const { product } = offer;
606
595
  // - Create an order with an active enrollment
607
596
  const enrollment: Enrollment = EnrollmentFactory({
608
597
  course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
@@ -614,10 +603,7 @@ describe('CourseProductItem', () => {
614
603
  target_enrollments: [enrollment],
615
604
  }).one();
616
605
 
617
- fetchMock.get(
618
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
619
- relation,
620
- );
606
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
621
607
  const orderQueryParameters = {
622
608
  product_id: order.product_id,
623
609
  course_code: order.course?.code,
@@ -668,8 +654,8 @@ describe('CourseProductItem', () => {
668
654
  it.each(PURCHASABLE_ORDER_STATES)(
669
655
  'renders sale tunnel button if user already has a %s order',
670
656
  async (state) => {
671
- const relation = CourseProductRelationFactory().one();
672
- const { product } = relation;
657
+ const offer = OfferFactory().one();
658
+ const { product } = offer;
673
659
  const order = CredentialOrderFactory({
674
660
  product_id: product.id,
675
661
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -678,7 +664,7 @@ describe('CourseProductItem', () => {
678
664
  }).one();
679
665
  fetchMock.get(
680
666
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
681
- relation,
667
+ offer,
682
668
  );
683
669
  const orderQueryParameters = {
684
670
  product_id: order.product_id,
@@ -709,7 +695,7 @@ describe('CourseProductItem', () => {
709
695
  expect($price.classList.contains('h6')).toBe(true);
710
696
 
711
697
  // - Render all target courses information
712
- relation.product.target_courses.forEach((course) => {
698
+ offer.product.target_courses.forEach((course) => {
713
699
  const $item = screen.getByTestId(`course-item-${course.code}`);
714
700
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
715
701
  // but we want to it to visually look like a h5
@@ -724,12 +710,9 @@ describe('CourseProductItem', () => {
724
710
  );
725
711
 
726
712
  it('renders sale tunnel button if user already has a canceled order', async () => {
727
- const relation = CourseProductRelationFactory().one();
728
- const { product } = relation;
729
- fetchMock.get(
730
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
731
- relation,
732
- );
713
+ const offer = OfferFactory().one();
714
+ const { product } = offer;
715
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
733
716
  const orderQueryParameters = {
734
717
  product_id: product.id,
735
718
  course_code: '00000',
@@ -759,7 +742,7 @@ describe('CourseProductItem', () => {
759
742
  expect($price.classList.contains('h6')).toBe(true);
760
743
 
761
744
  // - Render all target courses information
762
- relation.product.target_courses.forEach((course) => {
745
+ offer.product.target_courses.forEach((course) => {
763
746
  const $item = screen.getByTestId(`course-item-${course.code}`);
764
747
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
765
748
  // but we want to it to visually look like a h5
@@ -773,7 +756,7 @@ describe('CourseProductItem', () => {
773
756
  });
774
757
 
775
758
  it('renders error message when product fetching has failed', async () => {
776
- const { product } = CourseProductRelationFactory().one();
759
+ const { product } = OfferFactory().one();
777
760
 
778
761
  fetchMock.get(
779
762
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -794,21 +777,20 @@ describe('CourseProductItem', () => {
794
777
  });
795
778
 
796
779
  it('renders a warning message that tells that no seats are left', async () => {
797
- const relation = CourseProductRelationFactory({
798
- seats: 2,
799
- nb_seats_available: 0,
780
+ const offer = OfferFactory({
781
+ rules: {
782
+ nb_available_seats: 0,
783
+ has_seats_left: false,
784
+ },
800
785
  }).one();
801
- const { product } = relation;
786
+ const { product } = offer;
802
787
  const order = CredentialOrderFactory({
803
788
  product_id: product.id,
804
789
  course: PacedCourseFactory({ code: '00000' }).one(),
805
790
  target_courses: product.target_courses,
806
791
  state: OrderState.DRAFT,
807
792
  }).one();
808
- fetchMock.get(
809
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
810
- relation,
811
- );
793
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
812
794
  const orderQueryParameters = {
813
795
  product_id: order.product_id,
814
796
  course_code: order.course?.code,
@@ -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
+ OfferFactory,
7
7
  CourseRunFactory,
8
8
  CredentialOrderFactory,
9
9
  CredentialProductFactory,
@@ -22,13 +22,15 @@ 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({
25
+ OfferFactory({
26
26
  product: CredentialProductFactory({
27
27
  price: 840,
28
28
  price_currency: 'EUR',
29
29
  }).one(),
30
- discounted_price: 800,
31
- discount_rate: 0.3,
30
+ rules: {
31
+ discounted_price: 800,
32
+ discount_rate: 0.3,
33
+ },
32
34
  }).one(),
33
35
  { overwriteRoutes: true },
34
36
  );
@@ -1,7 +1,7 @@
1
1
  import { Children, useEffect, useMemo } from 'react';
2
2
  import { defineMessages, FormattedMessage, FormattedNumber, useIntl } from 'react-intl';
3
3
  import c from 'classnames';
4
- import { CourseProductRelation, CredentialOrder, Product, ProductType } from 'types/Joanie';
4
+ import { Offer, CredentialOrder, Product, ProductType } from 'types/Joanie';
5
5
  import { useCourseProduct } from 'hooks/useCourseProducts';
6
6
  import { Spinner } from 'components/Spinner';
7
7
  import { Icon, IconTypeEnum } from 'components/Icon';
@@ -77,16 +77,9 @@ type HeaderProps = {
77
77
  canPurchase: boolean;
78
78
  order: Maybe<CredentialOrder>;
79
79
  product: Product;
80
- courseProductRelation: CourseProductRelation;
80
+ offer: Offer;
81
81
  };
82
- const Header = ({
83
- product,
84
- order,
85
- courseProductRelation,
86
- hasPurchased,
87
- canPurchase,
88
- compact,
89
- }: HeaderProps) => {
82
+ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: HeaderProps) => {
90
83
  const intl = useIntl();
91
84
  const formatDate = useDateFormat();
92
85
 
@@ -110,7 +103,7 @@ const Header = ({
110
103
  return null;
111
104
  }
112
105
 
113
- if (courseProductRelation.discounted_price) {
106
+ if (offer.rules.discounted_price != null) {
114
107
  return (
115
108
  <>
116
109
  <span id="original-price" className="offscreen">
@@ -129,7 +122,7 @@ const Header = ({
129
122
  <ins aria-describedby="discount-price" className="product-widget__price-discount">
130
123
  <FormattedNumber
131
124
  currency={product.price_currency}
132
- value={courseProductRelation.discounted_price}
125
+ value={offer.rules.discounted_price}
133
126
  style="currency"
134
127
  />
135
128
  </ins>
@@ -140,7 +133,7 @@ const Header = ({
140
133
  return (
141
134
  <FormattedNumber currency={product.price_currency} value={product.price} style="currency" />
142
135
  );
143
- }, [canPurchase, courseProductRelation.discounted_price, product.price]);
136
+ }, [canPurchase, offer.rules.discounted_price, product.price]);
144
137
 
145
138
  return (
146
139
  <header className="product-widget__header">
@@ -151,39 +144,39 @@ const Header = ({
151
144
  {hasPurchased && <FormattedMessage {...messages.purchased} />}
152
145
  {displayPrice}
153
146
  </strong>
154
- {courseProductRelation?.description && (
155
- <p className="product-widget__header-description">{courseProductRelation.description}</p>
147
+ {offer?.rules.description && (
148
+ <p className="product-widget__header-description">{offer.rules.description}</p>
156
149
  )}
157
- {courseProductRelation?.discounted_price && (
150
+ {offer?.rules.discounted_price && (
158
151
  <p className="product-widget__header-discount">
159
- {courseProductRelation.discount_rate ? (
152
+ {offer.rules.discount_rate ? (
160
153
  <span className="product-widget__header-discount-rate">
161
- <FormattedNumber value={-courseProductRelation.discount_rate} style="percent" />
154
+ <FormattedNumber value={-offer.rules.discount_rate} style="percent" />
162
155
  </span>
163
156
  ) : (
164
157
  <span className="product-widget__header-discount-amount">
165
158
  <FormattedNumber
166
159
  currency={product.price_currency}
167
- value={-courseProductRelation.discount_amount!}
160
+ value={-offer.rules.discount_amount!}
168
161
  style="currency"
169
162
  />
170
163
  </span>
171
164
  )}
172
- {courseProductRelation.discount_start && (
165
+ {offer.rules.discount_start && (
173
166
  <span className="product-widget__header-discount-date">
174
167
  &nbsp;
175
168
  <FormattedMessage
176
169
  {...messages.from}
177
- values={{ from: formatDate(courseProductRelation.discount_start) }}
170
+ values={{ from: formatDate(offer.rules.discount_start) }}
178
171
  />
179
172
  </span>
180
173
  )}
181
- {courseProductRelation.discount_end && (
174
+ {offer.rules.discount_end && (
182
175
  <span className="product-widget__header-discount-date">
183
176
  &nbsp;
184
177
  <FormattedMessage
185
178
  {...messages.to}
186
- values={{ to: formatDate(courseProductRelation.discount_end) }}
179
+ values={{ to: formatDate(offer.rules.discount_end) }}
187
180
  />
188
181
  </span>
189
182
  )}
@@ -246,12 +239,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
246
239
  const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
247
240
  // FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
248
241
  // CredentialOrder
249
- const { item: courseProductRelation, states: productQueryStates } = useCourseProduct({
242
+ const { item: offer, states: productQueryStates } = useCourseProduct({
250
243
  product_id: productId,
251
244
  course_id: course.code,
252
245
  });
253
246
 
254
- const product = courseProductRelation?.product;
247
+ const product = offer?.product;
255
248
  const { item: productOrder, states: orderQueryStates } = useProductOrder({
256
249
  productId,
257
250
  courseCode: course.code,
@@ -309,18 +302,14 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
309
302
  <Header
310
303
  product={product}
311
304
  order={order}
312
- courseProductRelation={courseProductRelation}
305
+ offer={offer}
313
306
  canPurchase={canPurchase}
314
307
  hasPurchased={hasPurchased}
315
308
  compact={compact}
316
309
  />
317
310
  {canShowContent && <Content product={product} order={order} />}
318
311
  <footer className="product-widget__footer">
319
- <CourseProductItemFooter
320
- course={course}
321
- courseProductRelation={courseProductRelation}
322
- canPurchase={canPurchase}
323
- />
312
+ <CourseProductItemFooter course={course} offer={offer} canPurchase={canPurchase} />
324
313
  </footer>
325
314
  </>
326
315
  )}