richie-education 3.1.3-dev12 → 3.1.3-dev17

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 +12 -11
  3. package/js/components/ContractFrame/OrganizationContractFrame.tsx +4 -4
  4. package/js/components/CourseGlimpse/utils.ts +28 -22
  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 -3
  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 -16
  16. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  17. package/js/hooks/useCourseProducts.ts +4 -4
  18. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -4
  19. package/js/hooks/useOffering/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 +17 -14
  23. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +11 -8
  24. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +6 -3
  25. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  26. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -10
  27. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  28. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +5 -5
  29. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -8
  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 -21
  35. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +19 -13
  36. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -11
  37. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  38. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +6 -3
  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 +7 -4
  42. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  43. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -5
  44. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +55 -55
  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 -25
  50. package/js/pages/TeacherDashboardTraining/index.tsx +16 -12
  51. package/js/types/Joanie.ts +27 -20
  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 -23
  61. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -4
  62. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +20 -17
  63. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +22 -16
  64. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  65. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -3
  66. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  67. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +14 -14
  68. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +105 -75
  69. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +6 -4
  70. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +27 -20
  71. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
  72. package/package.json +1 -1
  73. package/js/hooks/useOffer/index.ts +0 -32
@@ -6,7 +6,7 @@ import {
6
6
  PacedCourseFactory,
7
7
  } from 'utils/test/factories/richie';
8
8
  import {
9
- OfferFactory,
9
+ OfferingFactory,
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 offer = OfferFactory().one();
78
- const { product } = offer;
77
+ const offering = OfferingFactory().one();
78
+ const { product } = offering;
79
79
  const productDeferred = new Deferred();
80
80
  fetchMock.get(
81
81
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -92,14 +92,17 @@ 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(offer);
95
+ productDeferred.resolve(offering);
96
96
  await expectNoSpinner('Loading product information...');
97
97
  });
98
98
 
99
99
  it('renders product information for anonymous user', async () => {
100
- const offer = OfferFactory().one();
101
- const { product } = offer;
102
- fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
100
+ const offering = OfferingFactory().one();
101
+ const { product } = offering;
102
+ fetchMock.get(
103
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
104
+ offering,
105
+ );
103
106
 
104
107
  render(
105
108
  <CourseProductItem
@@ -128,7 +131,7 @@ describe('CourseProductItem', () => {
128
131
  ).not.toBeInTheDocument();
129
132
 
130
133
  // - Render all target courses information
131
- offer.product.target_courses.forEach((course) => {
134
+ offering.product.target_courses.forEach((course) => {
132
135
  const $item = screen.getByTestId(`course-item-${course.code}`);
133
136
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
134
137
  // but we want to it to visually look like a h5
@@ -148,19 +151,24 @@ describe('CourseProductItem', () => {
148
151
  });
149
152
 
150
153
  it('renders discount rate for anonymous user', async () => {
151
- const offer = OfferFactory({
154
+ const offering = OfferingFactory({
152
155
  product: CredentialProductFactory({
153
156
  price: 840,
154
157
  price_currency: 'EUR',
155
158
  }).one(),
156
- discounted_price: 800,
157
- discount_rate: 0.3,
158
- description: 'Year 2023 discount',
159
- discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
160
- discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
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
+ },
161
166
  }).one();
162
- const { product } = offer;
163
- fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
167
+ const { product } = offering;
168
+ fetchMock.get(
169
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
170
+ offering,
171
+ );
164
172
 
165
173
  render(
166
174
  <CourseProductItem
@@ -186,7 +194,7 @@ describe('CourseProductItem', () => {
186
194
  const discountedPriceLabel = screen.getByText('Discounted price:');
187
195
  expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
188
196
  const discountedPrice = screen.getByText(
189
- priceFormatter(product.price_currency, offer.discounted_price!).replace(
197
+ priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
190
198
  /(\u202F|\u00a0)/g,
191
199
  ' ',
192
200
  ),
@@ -206,19 +214,24 @@ describe('CourseProductItem', () => {
206
214
  });
207
215
 
208
216
  it('renders discount amount for anonymous user', async () => {
209
- const offer = OfferFactory({
217
+ const offering = OfferingFactory({
210
218
  product: CredentialProductFactory({
211
219
  price: 840,
212
220
  price_currency: 'EUR',
213
221
  }).one(),
214
- discounted_price: 800,
215
- discount_amount: 40,
216
- description: 'Year 2023 discount',
217
- discount_start: new Date('2023-01-01T00:00:00Z').toISOString(),
218
- discount_end: new Date('2023-12-31T23:59:59Z').toISOString(),
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
+ },
219
229
  }).one();
220
- const { product } = offer;
221
- fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
230
+ const { product } = offering;
231
+ fetchMock.get(
232
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
233
+ offering,
234
+ );
222
235
 
223
236
  render(
224
237
  <CourseProductItem
@@ -244,7 +257,7 @@ describe('CourseProductItem', () => {
244
257
  const discountedPriceLabel = screen.getByText('Discounted price:');
245
258
  expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
246
259
  const discountedPrice = screen.getByText(
247
- priceFormatter(product.price_currency, offer.discounted_price!).replace(
260
+ priceFormatter(product.price_currency, offering.rules.discounted_price!).replace(
248
261
  /(\u202F|\u00a0)/g,
249
262
  ' ',
250
263
  ),
@@ -264,13 +277,16 @@ describe('CourseProductItem', () => {
264
277
  });
265
278
 
266
279
  it('does not render <CertificateItem /> if product do not have a certificate', async () => {
267
- const offer = OfferFactory({
280
+ const offering = OfferingFactory({
268
281
  product: ProductFactory({
269
282
  certificate_definition: undefined,
270
283
  }).one(),
271
284
  }).one();
272
- const { product } = offer;
273
- fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
285
+ const { product } = offering;
286
+ fetchMock.get(
287
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
288
+ offering,
289
+ );
274
290
 
275
291
  render(
276
292
  <CourseProductItem
@@ -288,16 +304,16 @@ describe('CourseProductItem', () => {
288
304
  });
289
305
 
290
306
  it('renders product information in compact mode', async () => {
291
- const offer = OfferFactory().one();
307
+ const offering = OfferingFactory().one();
292
308
  fetchMock.get(
293
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
294
- offer,
309
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
310
+ offering,
295
311
  );
296
312
 
297
313
  const { container } = render(
298
314
  <CourseProductItem
299
315
  course={PacedCourseFactory({ code: '00000' }).one()}
300
- productId={offer.product.id}
316
+ productId={offering.product.id}
301
317
  compact
302
318
  />,
303
319
  { queryOptions: { client: createTestQueryClient({ user: null }) } },
@@ -305,14 +321,14 @@ describe('CourseProductItem', () => {
305
321
 
306
322
  // In the header, we should display the product title, the product price
307
323
  // and product date range and languages
308
- await screen.findByRole('heading', { level: 3, name: offer.product.title });
324
+ await screen.findByRole('heading', { level: 3, name: offering.product.title });
309
325
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
310
326
  // but we want to it to visually look like a h6
311
327
 
312
328
  const $price = screen.getByText(
313
329
  // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
314
330
  // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
315
- priceFormatter(offer.product.price_currency, offer.product.price).replace(
331
+ priceFormatter(offering.product.price_currency, offering.product.price).replace(
316
332
  /(\u202F|\u00a0)/g,
317
333
  ' ',
318
334
  ),
@@ -328,7 +344,7 @@ describe('CourseProductItem', () => {
328
344
  expect($productWidgetContent).not.toBeInTheDocument();
329
345
 
330
346
  // - Any target courses information should be displayed
331
- offer.product.target_courses.forEach((course) => {
347
+ offering.product.target_courses.forEach((course) => {
332
348
  const $item = screen.queryByTestId(`course-item-${course.code}`);
333
349
  expect($item).not.toBeInTheDocument();
334
350
  });
@@ -337,7 +353,7 @@ describe('CourseProductItem', () => {
337
353
  expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
338
354
 
339
355
  // - Render a login button
340
- screen.getByRole('button', { name: `Login to purchase "${offer.product.title}"` });
356
+ screen.getByRole('button', { name: `Login to purchase "${offering.product.title}"` });
341
357
  // - Does not render PurchaseButton cta
342
358
  expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
343
359
  });
@@ -345,8 +361,8 @@ describe('CourseProductItem', () => {
345
361
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
346
362
  'renders product informations for %s order',
347
363
  async (state) => {
348
- const offer = OfferFactory().one();
349
- const { product } = offer;
364
+ const offering = OfferingFactory().one();
365
+ const { product } = offering;
350
366
  const order = CredentialOrderFactory({
351
367
  product_id: product.id,
352
368
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -356,7 +372,7 @@ describe('CourseProductItem', () => {
356
372
 
357
373
  fetchMock.get(
358
374
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
359
- offer,
375
+ offering,
360
376
  );
361
377
  const orderQueryParameters = {
362
378
  course_code: order.course.code,
@@ -370,13 +386,13 @@ describe('CourseProductItem', () => {
370
386
  render(
371
387
  <CourseProductItem
372
388
  course={PacedCourseFactory({ code: '00000' }).one()}
373
- productId={offer.product.id}
389
+ productId={offering.product.id}
374
390
  />,
375
391
  );
376
392
 
377
393
  // In the header, we should display the product title, the product price
378
394
  // and product date range and languages
379
- await screen.findByRole('heading', { level: 3, name: offer.product.title });
395
+ await screen.findByRole('heading', { level: 3, name: offering.product.title });
380
396
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
381
397
  // but we want to it to visually look like a h6
382
398
 
@@ -406,8 +422,8 @@ describe('CourseProductItem', () => {
406
422
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
407
423
  'renders product informations for %s order in compact mode',
408
424
  async (state) => {
409
- const offer = OfferFactory().one();
410
- const { product } = offer;
425
+ const offering = OfferingFactory().one();
426
+ const { product } = offering;
411
427
  const order = CredentialOrderFactory({
412
428
  product_id: product.id,
413
429
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -417,7 +433,7 @@ describe('CourseProductItem', () => {
417
433
 
418
434
  fetchMock.get(
419
435
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
420
- offer,
436
+ offering,
421
437
  );
422
438
  const orderQueryParameters = {
423
439
  course_code: order.course.code,
@@ -431,14 +447,14 @@ describe('CourseProductItem', () => {
431
447
  render(
432
448
  <CourseProductItem
433
449
  course={PacedCourseFactory({ code: '00000' }).one()}
434
- productId={offer.product.id}
450
+ productId={offering.product.id}
435
451
  compact
436
452
  />,
437
453
  );
438
454
 
439
455
  // In the header, we should display the product title, the product price
440
456
  // and product date range and languages
441
- await screen.findByRole('heading', { level: 3, name: offer.product.title });
457
+ await screen.findByRole('heading', { level: 3, name: offering.product.title });
442
458
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
443
459
  // but we want to it to visually look like a h6
444
460
 
@@ -448,7 +464,7 @@ describe('CourseProductItem', () => {
448
464
  expect($enrolledInfo.classList.contains('h6')).toBe(true);
449
465
 
450
466
  // - Any target courses information should be displayed
451
- offer.product.target_courses.forEach((course) => {
467
+ offering.product.target_courses.forEach((course) => {
452
468
  const $item = screen.queryByTestId(`course-item-${course.code}`);
453
469
  expect($item).not.toBeInTheDocument();
454
470
  });
@@ -459,8 +475,8 @@ describe('CourseProductItem', () => {
459
475
  );
460
476
 
461
477
  it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
462
- const offer = OfferFactory().one();
463
- const { product } = offer;
478
+ const offering = OfferingFactory().one();
479
+ const { product } = offering;
464
480
  const order = CredentialOrderFactory({
465
481
  product_id: product.id,
466
482
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -468,7 +484,10 @@ describe('CourseProductItem', () => {
468
484
  state,
469
485
  }).one();
470
486
 
471
- fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
487
+ fetchMock.get(
488
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
489
+ offering,
490
+ );
472
491
  const orderQueryParameters = {
473
492
  course_code: order.course.code,
474
493
  product_id: order.product_id,
@@ -518,17 +537,17 @@ describe('CourseProductItem', () => {
518
537
  it.each(ENROLLABLE_ORDER_STATES)(
519
538
  'renders product informations for a %s order in compact mode',
520
539
  async (state) => {
521
- const offer = OfferFactory().one();
540
+ const offering = OfferingFactory().one();
522
541
  const order: CredentialOrder = CredentialOrderFactory({
523
- product_id: offer.product.id,
542
+ product_id: offering.product.id,
524
543
  course: PacedCourseFactory({ code: '00000' }).one(),
525
- target_courses: offer.product.target_courses,
544
+ target_courses: offering.product.target_courses,
526
545
  state,
527
546
  }).one();
528
547
 
529
548
  fetchMock.get(
530
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
531
- offer,
549
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offering.product.id}/`,
550
+ offering,
532
551
  );
533
552
  const orderQueryParameters = {
534
553
  product_id: order.product_id,
@@ -542,14 +561,14 @@ describe('CourseProductItem', () => {
542
561
 
543
562
  render(
544
563
  <CourseProductItem
545
- productId={offer.product.id}
564
+ productId={offering.product.id}
546
565
  course={PacedCourseFactory({ code: '00000' }).one()}
547
566
  compact
548
567
  />,
549
568
  );
550
569
 
551
570
  // Wait for product information to be fetched
552
- await screen.findByRole('heading', { level: 3, name: offer.product.title });
571
+ await screen.findByRole('heading', { level: 3, name: offering.product.title });
553
572
 
554
573
  // - In place of product price, a label should be displayed
555
574
  const $enrolledInfo = await screen.findByText('Purchased');
@@ -586,8 +605,8 @@ describe('CourseProductItem', () => {
586
605
  );
587
606
 
588
607
  it('renders enrollment information when user is enrolled to a course run', async () => {
589
- const offer = OfferFactory().one();
590
- const { product } = offer;
608
+ const offering = OfferingFactory().one();
609
+ const { product } = offering;
591
610
  // - Create an order with an active enrollment
592
611
  const enrollment: Enrollment = EnrollmentFactory({
593
612
  course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
@@ -599,7 +618,10 @@ describe('CourseProductItem', () => {
599
618
  target_enrollments: [enrollment],
600
619
  }).one();
601
620
 
602
- fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
621
+ fetchMock.get(
622
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
623
+ offering,
624
+ );
603
625
  const orderQueryParameters = {
604
626
  product_id: order.product_id,
605
627
  course_code: order.course?.code,
@@ -650,8 +672,8 @@ describe('CourseProductItem', () => {
650
672
  it.each(PURCHASABLE_ORDER_STATES)(
651
673
  'renders sale tunnel button if user already has a %s order',
652
674
  async (state) => {
653
- const offer = OfferFactory().one();
654
- const { product } = offer;
675
+ const offering = OfferingFactory().one();
676
+ const { product } = offering;
655
677
  const order = CredentialOrderFactory({
656
678
  product_id: product.id,
657
679
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -660,7 +682,7 @@ describe('CourseProductItem', () => {
660
682
  }).one();
661
683
  fetchMock.get(
662
684
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
663
- offer,
685
+ offering,
664
686
  );
665
687
  const orderQueryParameters = {
666
688
  product_id: order.product_id,
@@ -691,7 +713,7 @@ describe('CourseProductItem', () => {
691
713
  expect($price.classList.contains('h6')).toBe(true);
692
714
 
693
715
  // - Render all target courses information
694
- offer.product.target_courses.forEach((course) => {
716
+ offering.product.target_courses.forEach((course) => {
695
717
  const $item = screen.getByTestId(`course-item-${course.code}`);
696
718
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
697
719
  // but we want to it to visually look like a h5
@@ -706,9 +728,12 @@ describe('CourseProductItem', () => {
706
728
  );
707
729
 
708
730
  it('renders sale tunnel button if user already has a canceled order', async () => {
709
- const offer = OfferFactory().one();
710
- const { product } = offer;
711
- fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
731
+ const offering = OfferingFactory().one();
732
+ const { product } = offering;
733
+ fetchMock.get(
734
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
735
+ offering,
736
+ );
712
737
  const orderQueryParameters = {
713
738
  product_id: product.id,
714
739
  course_code: '00000',
@@ -738,7 +763,7 @@ describe('CourseProductItem', () => {
738
763
  expect($price.classList.contains('h6')).toBe(true);
739
764
 
740
765
  // - Render all target courses information
741
- offer.product.target_courses.forEach((course) => {
766
+ offering.product.target_courses.forEach((course) => {
742
767
  const $item = screen.getByTestId(`course-item-${course.code}`);
743
768
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
744
769
  // but we want to it to visually look like a h5
@@ -752,7 +777,7 @@ describe('CourseProductItem', () => {
752
777
  });
753
778
 
754
779
  it('renders error message when product fetching has failed', async () => {
755
- const { product } = OfferFactory().one();
780
+ const { product } = OfferingFactory().one();
756
781
 
757
782
  fetchMock.get(
758
783
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -773,18 +798,23 @@ describe('CourseProductItem', () => {
773
798
  });
774
799
 
775
800
  it('renders a warning message that tells that no seats are left', async () => {
776
- const offer = OfferFactory({
777
- seats: 2,
778
- nb_seats_available: 0,
801
+ const offering = OfferingFactory({
802
+ rules: {
803
+ nb_available_seats: 0,
804
+ has_seats_left: false,
805
+ },
779
806
  }).one();
780
- const { product } = offer;
807
+ const { product } = offering;
781
808
  const order = CredentialOrderFactory({
782
809
  product_id: product.id,
783
810
  course: PacedCourseFactory({ code: '00000' }).one(),
784
811
  target_courses: product.target_courses,
785
812
  state: OrderState.DRAFT,
786
813
  }).one();
787
- fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
814
+ fetchMock.get(
815
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
816
+ offering,
817
+ );
788
818
  const orderQueryParameters = {
789
819
  product_id: order.product_id,
790
820
  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
- OfferFactory,
6
+ OfferingFactory,
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
- OfferFactory({
25
+ OfferingFactory({
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 { Offer, CredentialOrder, Product, ProductType } from 'types/Joanie';
4
+ import { Offering, 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,9 +77,9 @@ type HeaderProps = {
77
77
  canPurchase: boolean;
78
78
  order: Maybe<CredentialOrder>;
79
79
  product: Product;
80
- offer: Offer;
80
+ offering: Offering;
81
81
  };
82
- const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: HeaderProps) => {
82
+ const Header = ({ product, order, offering, hasPurchased, canPurchase, compact }: HeaderProps) => {
83
83
  const intl = useIntl();
84
84
  const formatDate = useDateFormat();
85
85
 
@@ -103,7 +103,7 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
103
103
  return null;
104
104
  }
105
105
 
106
- if (offer.discounted_price) {
106
+ if (offering.rules.discounted_price != null) {
107
107
  return (
108
108
  <>
109
109
  <span id="original-price" className="offscreen">
@@ -122,7 +122,7 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
122
122
  <ins aria-describedby="discount-price" className="product-widget__price-discount">
123
123
  <FormattedNumber
124
124
  currency={product.price_currency}
125
- value={offer.discounted_price}
125
+ value={offering.rules.discounted_price}
126
126
  style="currency"
127
127
  />
128
128
  </ins>
@@ -133,7 +133,7 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
133
133
  return (
134
134
  <FormattedNumber currency={product.price_currency} value={product.price} style="currency" />
135
135
  );
136
- }, [canPurchase, offer.discounted_price, product.price]);
136
+ }, [canPurchase, offering.rules.discounted_price, product.price]);
137
137
 
138
138
  return (
139
139
  <header className="product-widget__header">
@@ -144,37 +144,40 @@ const Header = ({ product, order, offer, hasPurchased, canPurchase, compact }: H
144
144
  {hasPurchased && <FormattedMessage {...messages.purchased} />}
145
145
  {displayPrice}
146
146
  </strong>
147
- {offer?.description && (
148
- <p className="product-widget__header-description">{offer.description}</p>
147
+ {offering?.rules.description && (
148
+ <p className="product-widget__header-description">{offering.rules.description}</p>
149
149
  )}
150
- {offer?.discounted_price && (
150
+ {offering?.rules.discounted_price && (
151
151
  <p className="product-widget__header-discount">
152
- {offer.discount_rate ? (
152
+ {offering.rules.discount_rate ? (
153
153
  <span className="product-widget__header-discount-rate">
154
- <FormattedNumber value={-offer.discount_rate} style="percent" />
154
+ <FormattedNumber value={-offering.rules.discount_rate} style="percent" />
155
155
  </span>
156
156
  ) : (
157
157
  <span className="product-widget__header-discount-amount">
158
158
  <FormattedNumber
159
159
  currency={product.price_currency}
160
- value={-offer.discount_amount!}
160
+ value={-offering.rules.discount_amount!}
161
161
  style="currency"
162
162
  />
163
163
  </span>
164
164
  )}
165
- {offer.discount_start && (
165
+ {offering.rules.discount_start && (
166
166
  <span className="product-widget__header-discount-date">
167
167
  &nbsp;
168
168
  <FormattedMessage
169
169
  {...messages.from}
170
- values={{ from: formatDate(offer.discount_start) }}
170
+ values={{ from: formatDate(offering.rules.discount_start) }}
171
171
  />
172
172
  </span>
173
173
  )}
174
- {offer.discount_end && (
174
+ {offering.rules.discount_end && (
175
175
  <span className="product-widget__header-discount-date">
176
176
  &nbsp;
177
- <FormattedMessage {...messages.to} values={{ to: formatDate(offer.discount_end) }} />
177
+ <FormattedMessage
178
+ {...messages.to}
179
+ values={{ to: formatDate(offering.rules.discount_end) }}
180
+ />
178
181
  </span>
179
182
  )}
180
183
  </p>
@@ -236,12 +239,12 @@ const Content = ({ product, order }: { product: Product; order?: CredentialOrder
236
239
  const CourseProductItem = ({ productId, course, compact = false }: CourseProductItemProps) => {
237
240
  // FIXME(rlecellier): useCourseProduct need's a filter on product.type that only return
238
241
  // CredentialOrder
239
- const { item: offer, states: productQueryStates } = useCourseProduct({
242
+ const { item: offering, states: productQueryStates } = useCourseProduct({
240
243
  product_id: productId,
241
244
  course_id: course.code,
242
245
  });
243
246
 
244
- const product = offer?.product;
247
+ const product = offering?.product;
245
248
  const { item: productOrder, states: orderQueryStates } = useProductOrder({
246
249
  productId,
247
250
  courseCode: course.code,
@@ -299,14 +302,18 @@ const CourseProductItem = ({ productId, course, compact = false }: CourseProduct
299
302
  <Header
300
303
  product={product}
301
304
  order={order}
302
- offer={offer}
305
+ offering={offering}
303
306
  canPurchase={canPurchase}
304
307
  hasPurchased={hasPurchased}
305
308
  compact={compact}
306
309
  />
307
310
  {canShowContent && <Content product={product} order={order} />}
308
311
  <footer className="product-widget__footer">
309
- <CourseProductItemFooter course={course} offer={offer} canPurchase={canPurchase} />
312
+ <CourseProductItemFooter
313
+ course={course}
314
+ offering={offering}
315
+ canPurchase={canPurchase}
316
+ />
310
317
  </footer>
311
318
  </>
312
319
  )}
@@ -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 { Offer } from 'types/Joanie';
26
- import { OfferFactory } from 'utils/test/factories/joanie';
25
+ import { Offering } from 'types/Joanie';
26
+ import { OfferingFactory } 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, offer: Offer) => {
214
+ const expectCourseProduct = async (container: HTMLElement, offering: Offering) => {
215
215
  const heading = await findByRole(container, 'heading', {
216
- name: offer.product.title,
216
+ name: offering.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 offer = OfferFactory().one();
387
- const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${offer.product.id}/`;
388
- fetchMock.get(resourceLink, offer);
386
+ const offering = OfferingFactory().one();
387
+ const resourceLink = `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${offering.product.id}/`;
388
+ fetchMock.get(resourceLink, offering);
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(), offer);
409
+ await expectCourseProduct(getHeaderContainer(), offering);
410
410
 
411
411
  // Portal.
412
412
  expectEmptyPortalContainer();