richie-education 3.1.3-dev11 → 3.1.3-dev12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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 +2 -2
  9. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  10. package/js/components/SaleTunnel/index.spec.tsx +5 -5
  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 +17 -19
  52. package/js/utils/test/factories/joanie.ts +3 -3
  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 +9 -13
  68. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +63 -87
  69. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +2 -2
  70. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +20 -34
  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,7 +148,7 @@ 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',
@@ -162,11 +159,8 @@ describe('CourseProductItem', () => {
162
159
  discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
163
160
  discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
164
161
  }).one();
165
- const { product } = relation;
166
- fetchMock.get(
167
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
168
- relation,
169
- );
162
+ const { product } = offer;
163
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
170
164
 
171
165
  render(
172
166
  <CourseProductItem
@@ -192,7 +186,7 @@ describe('CourseProductItem', () => {
192
186
  const discountedPriceLabel = screen.getByText('Discounted price:');
193
187
  expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
194
188
  const discountedPrice = screen.getByText(
195
- priceFormatter(product.price_currency, relation.discounted_price!).replace(
189
+ priceFormatter(product.price_currency, offer.discounted_price!).replace(
196
190
  /(\u202F|\u00a0)/g,
197
191
  ' ',
198
192
  ),
@@ -212,7 +206,7 @@ describe('CourseProductItem', () => {
212
206
  });
213
207
 
214
208
  it('renders discount amount for anonymous user', async () => {
215
- const relation = CourseProductRelationFactory({
209
+ const offer = OfferFactory({
216
210
  product: CredentialProductFactory({
217
211
  price: 840,
218
212
  price_currency: 'EUR',
@@ -223,11 +217,8 @@ describe('CourseProductItem', () => {
223
217
  discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
224
218
  discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
225
219
  }).one();
226
- const { product } = relation;
227
- fetchMock.get(
228
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
229
- relation,
230
- );
220
+ const { product } = offer;
221
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
231
222
 
232
223
  render(
233
224
  <CourseProductItem
@@ -253,7 +244,7 @@ describe('CourseProductItem', () => {
253
244
  const discountedPriceLabel = screen.getByText('Discounted price:');
254
245
  expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
255
246
  const discountedPrice = screen.getByText(
256
- priceFormatter(product.price_currency, relation.discounted_price!).replace(
247
+ priceFormatter(product.price_currency, offer.discounted_price!).replace(
257
248
  /(\u202F|\u00a0)/g,
258
249
  ' ',
259
250
  ),
@@ -273,16 +264,13 @@ describe('CourseProductItem', () => {
273
264
  });
274
265
 
275
266
  it('does not render <CertificateItem /> if product do not have a certificate', async () => {
276
- const relation = CourseProductRelationFactory({
267
+ const offer = OfferFactory({
277
268
  product: ProductFactory({
278
269
  certificate_definition: undefined,
279
270
  }).one(),
280
271
  }).one();
281
- const { product } = relation;
282
- fetchMock.get(
283
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
284
- relation,
285
- );
272
+ const { product } = offer;
273
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
286
274
 
287
275
  render(
288
276
  <CourseProductItem
@@ -300,16 +288,16 @@ describe('CourseProductItem', () => {
300
288
  });
301
289
 
302
290
  it('renders product information in compact mode', async () => {
303
- const relation = CourseProductRelationFactory().one();
291
+ const offer = OfferFactory().one();
304
292
  fetchMock.get(
305
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
306
- relation,
293
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
294
+ offer,
307
295
  );
308
296
 
309
297
  const { container } = render(
310
298
  <CourseProductItem
311
299
  course={PacedCourseFactory({ code: '00000' }).one()}
312
- productId={relation.product.id}
300
+ productId={offer.product.id}
313
301
  compact
314
302
  />,
315
303
  { queryOptions: { client: createTestQueryClient({ user: null }) } },
@@ -317,14 +305,14 @@ describe('CourseProductItem', () => {
317
305
 
318
306
  // In the header, we should display the product title, the product price
319
307
  // and product date range and languages
320
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
308
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
321
309
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
322
310
  // but we want to it to visually look like a h6
323
311
 
324
312
  const $price = screen.getByText(
325
313
  // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
326
314
  // 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(
315
+ priceFormatter(offer.product.price_currency, offer.product.price).replace(
328
316
  /(\u202F|\u00a0)/g,
329
317
  ' ',
330
318
  ),
@@ -340,7 +328,7 @@ describe('CourseProductItem', () => {
340
328
  expect($productWidgetContent).not.toBeInTheDocument();
341
329
 
342
330
  // - Any target courses information should be displayed
343
- relation.product.target_courses.forEach((course) => {
331
+ offer.product.target_courses.forEach((course) => {
344
332
  const $item = screen.queryByTestId(`course-item-${course.code}`);
345
333
  expect($item).not.toBeInTheDocument();
346
334
  });
@@ -349,7 +337,7 @@ describe('CourseProductItem', () => {
349
337
  expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
350
338
 
351
339
  // - Render a login button
352
- screen.getByRole('button', { name: `Login to purchase "${relation.product.title}"` });
340
+ screen.getByRole('button', { name: `Login to purchase "${offer.product.title}"` });
353
341
  // - Does not render PurchaseButton cta
354
342
  expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
355
343
  });
@@ -357,8 +345,8 @@ describe('CourseProductItem', () => {
357
345
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
358
346
  'renders product informations for %s order',
359
347
  async (state) => {
360
- const relation = CourseProductRelationFactory().one();
361
- const { product } = relation;
348
+ const offer = OfferFactory().one();
349
+ const { product } = offer;
362
350
  const order = CredentialOrderFactory({
363
351
  product_id: product.id,
364
352
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -368,7 +356,7 @@ describe('CourseProductItem', () => {
368
356
 
369
357
  fetchMock.get(
370
358
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
371
- relation,
359
+ offer,
372
360
  );
373
361
  const orderQueryParameters = {
374
362
  course_code: order.course.code,
@@ -382,13 +370,13 @@ describe('CourseProductItem', () => {
382
370
  render(
383
371
  <CourseProductItem
384
372
  course={PacedCourseFactory({ code: '00000' }).one()}
385
- productId={relation.product.id}
373
+ productId={offer.product.id}
386
374
  />,
387
375
  );
388
376
 
389
377
  // In the header, we should display the product title, the product price
390
378
  // and product date range and languages
391
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
379
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
392
380
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
393
381
  // but we want to it to visually look like a h6
394
382
 
@@ -418,8 +406,8 @@ describe('CourseProductItem', () => {
418
406
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
419
407
  'renders product informations for %s order in compact mode',
420
408
  async (state) => {
421
- const relation = CourseProductRelationFactory().one();
422
- const { product } = relation;
409
+ const offer = OfferFactory().one();
410
+ const { product } = offer;
423
411
  const order = CredentialOrderFactory({
424
412
  product_id: product.id,
425
413
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -429,7 +417,7 @@ describe('CourseProductItem', () => {
429
417
 
430
418
  fetchMock.get(
431
419
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
432
- relation,
420
+ offer,
433
421
  );
434
422
  const orderQueryParameters = {
435
423
  course_code: order.course.code,
@@ -443,14 +431,14 @@ describe('CourseProductItem', () => {
443
431
  render(
444
432
  <CourseProductItem
445
433
  course={PacedCourseFactory({ code: '00000' }).one()}
446
- productId={relation.product.id}
434
+ productId={offer.product.id}
447
435
  compact
448
436
  />,
449
437
  );
450
438
 
451
439
  // In the header, we should display the product title, the product price
452
440
  // and product date range and languages
453
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
441
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
454
442
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
455
443
  // but we want to it to visually look like a h6
456
444
 
@@ -460,7 +448,7 @@ describe('CourseProductItem', () => {
460
448
  expect($enrolledInfo.classList.contains('h6')).toBe(true);
461
449
 
462
450
  // - Any target courses information should be displayed
463
- relation.product.target_courses.forEach((course) => {
451
+ offer.product.target_courses.forEach((course) => {
464
452
  const $item = screen.queryByTestId(`course-item-${course.code}`);
465
453
  expect($item).not.toBeInTheDocument();
466
454
  });
@@ -471,8 +459,8 @@ describe('CourseProductItem', () => {
471
459
  );
472
460
 
473
461
  it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
474
- const relation = CourseProductRelationFactory().one();
475
- const { product } = relation;
462
+ const offer = OfferFactory().one();
463
+ const { product } = offer;
476
464
  const order = CredentialOrderFactory({
477
465
  product_id: product.id,
478
466
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -480,10 +468,7 @@ describe('CourseProductItem', () => {
480
468
  state,
481
469
  }).one();
482
470
 
483
- fetchMock.get(
484
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
485
- relation,
486
- );
471
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
487
472
  const orderQueryParameters = {
488
473
  course_code: order.course.code,
489
474
  product_id: order.product_id,
@@ -533,17 +518,17 @@ describe('CourseProductItem', () => {
533
518
  it.each(ENROLLABLE_ORDER_STATES)(
534
519
  'renders product informations for a %s order in compact mode',
535
520
  async (state) => {
536
- const relation = CourseProductRelationFactory().one();
521
+ const offer = OfferFactory().one();
537
522
  const order: CredentialOrder = CredentialOrderFactory({
538
- product_id: relation.product.id,
523
+ product_id: offer.product.id,
539
524
  course: PacedCourseFactory({ code: '00000' }).one(),
540
- target_courses: relation.product.target_courses,
525
+ target_courses: offer.product.target_courses,
541
526
  state,
542
527
  }).one();
543
528
 
544
529
  fetchMock.get(
545
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
546
- relation,
530
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
531
+ offer,
547
532
  );
548
533
  const orderQueryParameters = {
549
534
  product_id: order.product_id,
@@ -557,14 +542,14 @@ describe('CourseProductItem', () => {
557
542
 
558
543
  render(
559
544
  <CourseProductItem
560
- productId={relation.product.id}
545
+ productId={offer.product.id}
561
546
  course={PacedCourseFactory({ code: '00000' }).one()}
562
547
  compact
563
548
  />,
564
549
  );
565
550
 
566
551
  // Wait for product information to be fetched
567
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
552
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
568
553
 
569
554
  // - In place of product price, a label should be displayed
570
555
  const $enrolledInfo = await screen.findByText('Purchased');
@@ -601,8 +586,8 @@ describe('CourseProductItem', () => {
601
586
  );
602
587
 
603
588
  it('renders enrollment information when user is enrolled to a course run', async () => {
604
- const relation = CourseProductRelationFactory().one();
605
- const { product } = relation;
589
+ const offer = OfferFactory().one();
590
+ const { product } = offer;
606
591
  // - Create an order with an active enrollment
607
592
  const enrollment: Enrollment = EnrollmentFactory({
608
593
  course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
@@ -614,10 +599,7 @@ describe('CourseProductItem', () => {
614
599
  target_enrollments: [enrollment],
615
600
  }).one();
616
601
 
617
- fetchMock.get(
618
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
619
- relation,
620
- );
602
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
621
603
  const orderQueryParameters = {
622
604
  product_id: order.product_id,
623
605
  course_code: order.course?.code,
@@ -668,8 +650,8 @@ describe('CourseProductItem', () => {
668
650
  it.each(PURCHASABLE_ORDER_STATES)(
669
651
  'renders sale tunnel button if user already has a %s order',
670
652
  async (state) => {
671
- const relation = CourseProductRelationFactory().one();
672
- const { product } = relation;
653
+ const offer = OfferFactory().one();
654
+ const { product } = offer;
673
655
  const order = CredentialOrderFactory({
674
656
  product_id: product.id,
675
657
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -678,7 +660,7 @@ describe('CourseProductItem', () => {
678
660
  }).one();
679
661
  fetchMock.get(
680
662
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
681
- relation,
663
+ offer,
682
664
  );
683
665
  const orderQueryParameters = {
684
666
  product_id: order.product_id,
@@ -709,7 +691,7 @@ describe('CourseProductItem', () => {
709
691
  expect($price.classList.contains('h6')).toBe(true);
710
692
 
711
693
  // - Render all target courses information
712
- relation.product.target_courses.forEach((course) => {
694
+ offer.product.target_courses.forEach((course) => {
713
695
  const $item = screen.getByTestId(`course-item-${course.code}`);
714
696
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
715
697
  // but we want to it to visually look like a h5
@@ -724,12 +706,9 @@ describe('CourseProductItem', () => {
724
706
  );
725
707
 
726
708
  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
- );
709
+ const offer = OfferFactory().one();
710
+ const { product } = offer;
711
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
733
712
  const orderQueryParameters = {
734
713
  product_id: product.id,
735
714
  course_code: '00000',
@@ -759,7 +738,7 @@ describe('CourseProductItem', () => {
759
738
  expect($price.classList.contains('h6')).toBe(true);
760
739
 
761
740
  // - Render all target courses information
762
- relation.product.target_courses.forEach((course) => {
741
+ offer.product.target_courses.forEach((course) => {
763
742
  const $item = screen.getByTestId(`course-item-${course.code}`);
764
743
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
765
744
  // but we want to it to visually look like a h5
@@ -773,7 +752,7 @@ describe('CourseProductItem', () => {
773
752
  });
774
753
 
775
754
  it('renders error message when product fetching has failed', async () => {
776
- const { product } = CourseProductRelationFactory().one();
755
+ const { product } = OfferFactory().one();
777
756
 
778
757
  fetchMock.get(
779
758
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -794,21 +773,18 @@ describe('CourseProductItem', () => {
794
773
  });
795
774
 
796
775
  it('renders a warning message that tells that no seats are left', async () => {
797
- const relation = CourseProductRelationFactory({
776
+ const offer = OfferFactory({
798
777
  seats: 2,
799
778
  nb_seats_available: 0,
800
779
  }).one();
801
- const { product } = relation;
780
+ const { product } = offer;
802
781
  const order = CredentialOrderFactory({
803
782
  product_id: product.id,
804
783
  course: PacedCourseFactory({ code: '00000' }).one(),
805
784
  target_courses: product.target_courses,
806
785
  state: OrderState.DRAFT,
807
786
  }).one();
808
- fetchMock.get(
809
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
810
- relation,
811
- );
787
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
812
788
  const orderQueryParameters = {
813
789
  product_id: order.product_id,
814
790
  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,7 +22,7 @@ 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',
@@ -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.discounted_price) {
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.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.discounted_price, product.price]);
144
137
 
145
138
  return (
146
139
  <header className="product-widget__header">
@@ -151,40 +144,37 @@ 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?.description && (
148
+ <p className="product-widget__header-description">{offer.description}</p>
156
149
  )}
157
- {courseProductRelation?.discounted_price && (
150
+ {offer?.discounted_price && (
158
151
  <p className="product-widget__header-discount">
159
- {courseProductRelation.discount_rate ? (
152
+ {offer.discount_rate ? (
160
153
  <span className="product-widget__header-discount-rate">
161
- <FormattedNumber value={-courseProductRelation.discount_rate} style="percent" />
154
+ <FormattedNumber value={-offer.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.discount_amount!}
168
161
  style="currency"
169
162
  />
170
163
  </span>
171
164
  )}
172
- {courseProductRelation.discount_start && (
165
+ {offer.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.discount_start) }}
178
171
  />
179
172
  </span>
180
173
  )}
181
- {courseProductRelation.discount_end && (
174
+ {offer.discount_end && (
182
175
  <span className="product-widget__header-discount-date">
183
176
  &nbsp;
184
- <FormattedMessage
185
- {...messages.to}
186
- values={{ to: formatDate(courseProductRelation.discount_end) }}
187
- />
177
+ <FormattedMessage {...messages.to} values={{ to: formatDate(offer.discount_end) }} />
188
178
  </span>
189
179
  )}
190
180
  </p>
@@ -246,12 +236,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
246
236
  const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
247
237
  // FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
248
238
  // CredentialOrder
249
- const { item: courseProductRelation, states: productQueryStates } = useCourseProduct({
239
+ const { item: offer, states: productQueryStates } = useCourseProduct({
250
240
  product_id: productId,
251
241
  course_id: course.code,
252
242
  });
253
243
 
254
- const product = courseProductRelation?.product;
244
+ const product = offer?.product;
255
245
  const { item: productOrder, states: orderQueryStates } = useProductOrder({
256
246
  productId,
257
247
  courseCode: course.code,
@@ -309,18 +299,14 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
309
299
  <Header
310
300
  product={product}
311
301
  order={order}
312
- courseProductRelation={courseProductRelation}
302
+ offer={offer}
313
303
  canPurchase={canPurchase}
314
304
  hasPurchased={hasPurchased}
315
305
  compact={compact}
316
306
  />
317
307
  {canShowContent && <Content product={product} order={order} />}
318
308
  <footer className="product-widget__footer">
319
- <CourseProductItemFooter
320
- course={course}
321
- courseProductRelation={courseProductRelation}
322
- canPurchase={canPurchase}
323
- />
309
+ <CourseProductItemFooter course={course} offer={offer} canPurchase={canPurchase} />
324
310
  </footer>
325
311
  </>
326
312
  )}
@@ -22,8 +22,8 @@ import {
22
22
  import SyllabusCourseRunsList from 'widgets/SyllabusCourseRunsList/index';
23
23
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
24
24
  import { CourseRun, Priority } from 'types';
25
- import { CourseProductRelation } from 'types/Joanie';
26
- import { CourseProductRelationFactory } from 'utils/test/factories/joanie';
25
+ import { Offer } from 'types/Joanie';
26
+ import { OfferFactory } from 'utils/test/factories/joanie';
27
27
  import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
28
28
  import { StringHelper } from 'utils/StringHelper';
29
29
  import { computeStates } from 'utils/CourseRuns';
@@ -211,9 +211,9 @@ describe('<SyllabusCourseRunsList/>', () => {
211
211
  });
212
212
  };
213
213
 
214
- const expectCourseProduct = async (container: HTMLElement, relation: CourseProductRelation) => {
214
+ const expectCourseProduct = async (container: HTMLElement, offer: Offer) => {
215
215
  const heading = await findByRole(container, 'heading', {
216
- name: relation.product.title,
216
+ name: offer.product.title,
217
217
  });
218
218
  expect(Array.from(heading.classList)).toContain('product-widget__title');
219
219
  };
@@ -383,9 +383,9 @@ describe('<SyllabusCourseRunsList/>', () => {
383
383
 
384
384
  it('has one opened product', async () => {
385
385
  const course = PacedCourseFactory().one();
386
- const relation = CourseProductRelationFactory().one();
387
- const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${relation.product.id}/`;
388
- fetchMock.get(resourceLink, relation);
386
+ const offer = OfferFactory().one();
387
+ const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${offer.product.id}/`;
388
+ fetchMock.get(resourceLink, offer);
389
389
 
390
390
  const courseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
391
391
  resource_link: resourceLink,
@@ -406,7 +406,7 @@ describe('<SyllabusCourseRunsList/>', () => {
406
406
  expect(getHeaderContainer().querySelectorAll('.course-detail__run-descriptions').length).toBe(
407
407
  1,
408
408
  );
409
- await expectCourseProduct(getHeaderContainer(), relation);
409
+ await expectCourseProduct(getHeaderContainer(), offer);
410
410
 
411
411
  // Portal.
412
412
  expectEmptyPortalContainer();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "3.1.3-dev11",
3
+ "version": "3.1.3-dev12",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {