richie-education 3.1.2 → 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 (80) 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/CredentialSaleTunnel/index.tsx +1 -3
  8. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +3 -1
  9. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +2 -2
  10. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +1 -2
  11. package/js/components/SaleTunnel/index.credential.spec.tsx +5 -19
  12. package/js/components/SaleTunnel/index.full-process.spec.tsx +3 -3
  13. package/js/components/SaleTunnel/index.spec.tsx +75 -0
  14. package/js/components/SaleTunnel/index.stories.tsx +0 -1
  15. package/js/components/SaleTunnel/index.tsx +2 -2
  16. package/js/components/TeacherDashboardCourseList/index.spec.tsx +3 -3
  17. package/js/components/TeacherDashboardCourseList/index.tsx +2 -2
  18. package/js/hooks/useContractArchive/index.ts +3 -3
  19. package/js/hooks/useCourseProductUnion/index.spec.tsx +16 -18
  20. package/js/hooks/useCourseProductUnion/index.ts +7 -7
  21. package/js/hooks/useCourseProducts.ts +4 -8
  22. package/js/hooks/useDefaultOrganizationId/index.tsx +4 -7
  23. package/js/hooks/useOffer/index.ts +32 -0
  24. package/js/hooks/useTeacherCoursesSearch/index.tsx +4 -4
  25. package/js/hooks/useTeacherPendingContractsCount/index.ts +4 -4
  26. package/js/pages/DashboardCourses/index.spec.tsx +14 -17
  27. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.spec.tsx +8 -14
  28. package/js/pages/TeacherDashboardContractsLayout/TeacherDashboardContracts/index.tsx +4 -12
  29. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.spec.tsx +11 -11
  30. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.timer.spec.tsx +10 -13
  31. package/js/pages/TeacherDashboardContractsLayout/components/BulkDownloadContractButton/index.tsx +4 -4
  32. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +20 -28
  33. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.tsx +8 -11
  34. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +6 -6
  35. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.tsx +4 -4
  36. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.spec.tsx +7 -7
  37. package/js/pages/TeacherDashboardContractsLayout/hooks/useCheckContractArchiveExists/index.tsx +5 -5
  38. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.spec.ts +21 -28
  39. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/contractArchiveLocalStorage.ts +13 -23
  40. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.spec.tsx +11 -13
  41. package/js/pages/TeacherDashboardContractsLayout/hooks/useDownloadContractArchive/index.tsx +6 -6
  42. package/js/pages/TeacherDashboardContractsLayout/hooks/useHasContractToDownload/index.tsx +3 -6
  43. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.spec.tsx +16 -16
  44. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractFilters/index.tsx +4 -4
  45. package/js/pages/TeacherDashboardContractsLayout/hooks/useTeacherContractsToSign.tsx +4 -7
  46. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.spec.tsx +21 -21
  47. package/js/pages/TeacherDashboardCourseLearnersLayout/hooks/useCourseLearnersFilters/index.ts +5 -10
  48. package/js/pages/TeacherDashboardCourseLearnersLayout/index.spec.tsx +61 -79
  49. package/js/pages/TeacherDashboardCourseLearnersLayout/index.tsx +1 -1
  50. package/js/pages/TeacherDashboardCoursesLoader/index.spec.tsx +11 -11
  51. package/js/pages/TeacherDashboardOrganizationCourseLoader/index.spec.tsx +11 -11
  52. package/js/pages/TeacherDashboardTraining/TeacherDashboardTrainingLoader.tsx +7 -7
  53. package/js/pages/TeacherDashboardTraining/index.spec.tsx +25 -33
  54. package/js/pages/TeacherDashboardTraining/index.tsx +12 -20
  55. package/js/types/Joanie.ts +26 -30
  56. package/js/utils/ProductHelper/index.ts +1 -5
  57. package/js/utils/test/factories/joanie.ts +12 -25
  58. package/js/utils/test/mockCourseProductWithOrder.ts +4 -4
  59. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +1 -1
  60. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +1 -1
  61. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +3 -3
  62. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +1 -1
  63. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +4 -4
  64. package/js/widgets/Dashboard/components/DashboardItem/stories.mock.ts +1 -1
  65. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.spec.tsx +23 -28
  66. package/js/widgets/Dashboard/components/DashboardSidebar/components/ContractNavLink/index.tsx +4 -8
  67. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.spec.tsx +17 -27
  68. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/index.tsx +16 -25
  69. package/js/widgets/Dashboard/components/TeacherDashboardCourseSidebar/utils.ts +4 -4
  70. package/js/widgets/Dashboard/components/TeacherDashboardOrganizationSidebar/index.tsx +3 -7
  71. package/js/widgets/Dashboard/utils/teacherDashboardPaths.tsx +4 -4
  72. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +23 -38
  73. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/_styles.scss +27 -7
  74. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/CourseRunList.tsx +1 -1
  75. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +174 -158
  76. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +9 -2
  77. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +108 -28
  78. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +8 -8
  79. package/package.json +1 -1
  80. package/js/hooks/useCourseProductRelation/index.ts +0 -44
@@ -6,12 +6,11 @@ import {
6
6
  PacedCourseFactory,
7
7
  } from 'utils/test/factories/richie';
8
8
  import {
9
- CourseProductRelationFactory,
9
+ OfferFactory,
10
10
  EnrollmentFactory,
11
11
  CredentialOrderFactory,
12
12
  ProductFactory,
13
- OrderGroupFullFactory,
14
- OrderGroupFactory,
13
+ CredentialProductFactory,
15
14
  } from 'utils/test/factories/joanie';
16
15
  import {
17
16
  CourseRun,
@@ -75,8 +74,8 @@ describe('CourseProductItem', () => {
75
74
  }).format(price);
76
75
 
77
76
  it('should display a loader until product is loaded', async () => {
78
- const relation = CourseProductRelationFactory().one();
79
- const { product } = relation;
77
+ const offer = OfferFactory().one();
78
+ const { product } = offer;
80
79
  const productDeferred = new Deferred();
81
80
  fetchMock.get(
82
81
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -93,17 +92,14 @@ describe('CourseProductItem', () => {
93
92
 
94
93
  // - A loader should be displayed while product information are fetching
95
94
  await expectSpinner('Loading product information...');
96
- productDeferred.resolve(relation);
95
+ productDeferred.resolve(offer);
97
96
  await expectNoSpinner('Loading product information...');
98
97
  });
99
98
 
100
99
  it('renders product information for anonymous user', async () => {
101
- const relation = CourseProductRelationFactory().one();
102
- const { product } = relation;
103
- fetchMock.get(
104
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
105
- relation,
106
- );
100
+ const offer = OfferFactory().one();
101
+ const { product } = offer;
102
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
107
103
 
108
104
  render(
109
105
  <CourseProductItem
@@ -132,7 +128,7 @@ describe('CourseProductItem', () => {
132
128
  ).not.toBeInTheDocument();
133
129
 
134
130
  // - Render all target courses information
135
- relation.product.target_courses.forEach((course) => {
131
+ offer.product.target_courses.forEach((course) => {
136
132
  const $item = screen.getByTestId(`course-item-${course.code}`);
137
133
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
138
134
  // but we want to it to visually look like a h5
@@ -151,17 +147,130 @@ describe('CourseProductItem', () => {
151
147
  expect(screen.queryByTestId('PurchaseButton__cta')).toBeNull();
152
148
  });
153
149
 
150
+ it('renders discount rate for anonymous user', async () => {
151
+ const offer = OfferFactory({
152
+ product: CredentialProductFactory({
153
+ price: 840,
154
+ price_currency: 'EUR',
155
+ }).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(),
161
+ }).one();
162
+ const { product } = offer;
163
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
164
+
165
+ render(
166
+ <CourseProductItem
167
+ course={PacedCourseFactory({ code: '00000' }).one()}
168
+ productId={product.id}
169
+ />,
170
+ { queryOptions: { client: createTestQueryClient({ user: null }) } },
171
+ );
172
+
173
+ await screen.findByRole('heading', { level: 3, name: product.title });
174
+
175
+ // - Render discount information
176
+ // Original price should be displayed as a del element
177
+ const originalPriceLabel = screen.getByText('Original price:');
178
+ expect(originalPriceLabel.classList.contains('offscreen')).toBe(true);
179
+ const originalPrice = screen.getByText(
180
+ priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
181
+ );
182
+ expect(originalPrice.tagName).toBe('DEL');
183
+ expect(originalPrice.getAttribute('aria-describedby')).toEqual(originalPriceLabel.id);
184
+
185
+ // Discounted price should be displayed as an ins element
186
+ const discountedPriceLabel = screen.getByText('Discounted price:');
187
+ expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
188
+ const discountedPrice = screen.getByText(
189
+ priceFormatter(product.price_currency, offer.discounted_price!).replace(
190
+ /(\u202F|\u00a0)/g,
191
+ ' ',
192
+ ),
193
+ );
194
+ expect(discountedPrice.tagName).toBe('INS');
195
+ expect(discountedPrice.getAttribute('aria-describedby')).toEqual(discountedPriceLabel.id);
196
+
197
+ // Discount description should be displayed
198
+ screen.getByText('Year 2023 discount');
199
+
200
+ // Discount rate should be displayed
201
+ screen.getByText('-30%');
202
+
203
+ // Discount date range should be displayed
204
+ screen.getByText('from Jan 01, 2023');
205
+ screen.getByText('to Dec 31, 2023');
206
+ });
207
+
208
+ it('renders discount amount for anonymous user', async () => {
209
+ const offer = OfferFactory({
210
+ product: CredentialProductFactory({
211
+ price: 840,
212
+ price_currency: 'EUR',
213
+ }).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(),
219
+ }).one();
220
+ const { product } = offer;
221
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
222
+
223
+ render(
224
+ <CourseProductItem
225
+ course={PacedCourseFactory({ code: '00000' }).one()}
226
+ productId={product.id}
227
+ />,
228
+ { queryOptions: { client: createTestQueryClient({ user: null }) } },
229
+ );
230
+
231
+ await screen.findByRole('heading', { level: 3, name: product.title });
232
+
233
+ // - Render discount information
234
+ // Original price should be displayed as a del element
235
+ const originalPriceLabel = screen.getByText('Original price:');
236
+ expect(originalPriceLabel.classList.contains('offscreen')).toBe(true);
237
+ const originalPrice = screen.getByText(
238
+ priceFormatter(product.price_currency, product.price).replace(/(\u202F|\u00a0)/g, ' '),
239
+ );
240
+ expect(originalPrice.tagName).toBe('DEL');
241
+ expect(originalPrice.getAttribute('aria-describedby')).toEqual(originalPriceLabel.id);
242
+
243
+ // Discounted price should be displayed as an ins element
244
+ const discountedPriceLabel = screen.getByText('Discounted price:');
245
+ expect(discountedPriceLabel.classList.contains('offscreen')).toBe(true);
246
+ const discountedPrice = screen.getByText(
247
+ priceFormatter(product.price_currency, offer.discounted_price!).replace(
248
+ /(\u202F|\u00a0)/g,
249
+ ' ',
250
+ ),
251
+ );
252
+ expect(discountedPrice.tagName).toBe('INS');
253
+ expect(discountedPrice.getAttribute('aria-describedby')).toEqual(discountedPriceLabel.id);
254
+
255
+ // Discount description should be displayed
256
+ screen.getByText('Year 2023 discount');
257
+
258
+ // Discount rate should be displayed
259
+ screen.getByText(priceFormatter(product.price_currency, -40).replace(/(\u202F|\u00a0)/g, ' '));
260
+
261
+ // Discount date range should be displayed
262
+ screen.getByText('from Jan 01, 2023');
263
+ screen.getByText('to Dec 31, 2023');
264
+ });
265
+
154
266
  it('does not render <CertificateItem /> if product do not have a certificate', async () => {
155
- const relation = CourseProductRelationFactory({
267
+ const offer = OfferFactory({
156
268
  product: ProductFactory({
157
269
  certificate_definition: undefined,
158
270
  }).one(),
159
271
  }).one();
160
- const { product } = relation;
161
- fetchMock.get(
162
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
163
- relation,
164
- );
272
+ const { product } = offer;
273
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
165
274
 
166
275
  render(
167
276
  <CourseProductItem
@@ -179,16 +288,16 @@ describe('CourseProductItem', () => {
179
288
  });
180
289
 
181
290
  it('renders product information in compact mode', async () => {
182
- const relation = CourseProductRelationFactory().one();
291
+ const offer = OfferFactory().one();
183
292
  fetchMock.get(
184
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
185
- relation,
293
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
294
+ offer,
186
295
  );
187
296
 
188
297
  const { container } = render(
189
298
  <CourseProductItem
190
299
  course={PacedCourseFactory({ code: '00000' }).one()}
191
- productId={relation.product.id}
300
+ productId={offer.product.id}
192
301
  compact
193
302
  />,
194
303
  { queryOptions: { client: createTestQueryClient({ user: null }) } },
@@ -196,14 +305,14 @@ describe('CourseProductItem', () => {
196
305
 
197
306
  // In the header, we should display the product title, the product price
198
307
  // and product date range and languages
199
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
308
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
200
309
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
201
310
  // but we want to it to visually look like a h6
202
311
 
203
312
  const $price = screen.getByText(
204
313
  // the price formatter generates non-breaking spaces and getByText doesn't seem to handle that well, replace it
205
314
  // with a regular space. We replace NNBSP (\u202F) and NBSP (\u00a0) with a regular space
206
- priceFormatter(relation.product.price_currency, relation.product.price).replace(
315
+ priceFormatter(offer.product.price_currency, offer.product.price).replace(
207
316
  /(\u202F|\u00a0)/g,
208
317
  ' ',
209
318
  ),
@@ -219,7 +328,7 @@ describe('CourseProductItem', () => {
219
328
  expect($productWidgetContent).not.toBeInTheDocument();
220
329
 
221
330
  // - Any target courses information should be displayed
222
- relation.product.target_courses.forEach((course) => {
331
+ offer.product.target_courses.forEach((course) => {
223
332
  const $item = screen.queryByTestId(`course-item-${course.code}`);
224
333
  expect($item).not.toBeInTheDocument();
225
334
  });
@@ -228,7 +337,7 @@ describe('CourseProductItem', () => {
228
337
  expect(screen.queryByTestId('CertificateItem')).not.toBeInTheDocument();
229
338
 
230
339
  // - Render a login button
231
- screen.getByRole('button', { name: `Login to purchase "${relation.product.title}"` });
340
+ screen.getByRole('button', { name: `Login to purchase "${offer.product.title}"` });
232
341
  // - Does not render PurchaseButton cta
233
342
  expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
234
343
  });
@@ -236,8 +345,8 @@ describe('CourseProductItem', () => {
236
345
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
237
346
  'renders product informations for %s order',
238
347
  async (state) => {
239
- const relation = CourseProductRelationFactory().one();
240
- const { product } = relation;
348
+ const offer = OfferFactory().one();
349
+ const { product } = offer;
241
350
  const order = CredentialOrderFactory({
242
351
  product_id: product.id,
243
352
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -247,7 +356,7 @@ describe('CourseProductItem', () => {
247
356
 
248
357
  fetchMock.get(
249
358
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
250
- relation,
359
+ offer,
251
360
  );
252
361
  const orderQueryParameters = {
253
362
  course_code: order.course.code,
@@ -261,13 +370,13 @@ describe('CourseProductItem', () => {
261
370
  render(
262
371
  <CourseProductItem
263
372
  course={PacedCourseFactory({ code: '00000' }).one()}
264
- productId={relation.product.id}
373
+ productId={offer.product.id}
265
374
  />,
266
375
  );
267
376
 
268
377
  // In the header, we should display the product title, the product price
269
378
  // and product date range and languages
270
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
379
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
271
380
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
272
381
  // but we want to it to visually look like a h6
273
382
 
@@ -297,8 +406,8 @@ describe('CourseProductItem', () => {
297
406
  it.each([OrderState.PENDING, OrderState.NO_PAYMENT])(
298
407
  'renders product informations for %s order in compact mode',
299
408
  async (state) => {
300
- const relation = CourseProductRelationFactory().one();
301
- const { product } = relation;
409
+ const offer = OfferFactory().one();
410
+ const { product } = offer;
302
411
  const order = CredentialOrderFactory({
303
412
  product_id: product.id,
304
413
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -308,7 +417,7 @@ describe('CourseProductItem', () => {
308
417
 
309
418
  fetchMock.get(
310
419
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
311
- relation,
420
+ offer,
312
421
  );
313
422
  const orderQueryParameters = {
314
423
  course_code: order.course.code,
@@ -322,14 +431,14 @@ describe('CourseProductItem', () => {
322
431
  render(
323
432
  <CourseProductItem
324
433
  course={PacedCourseFactory({ code: '00000' }).one()}
325
- productId={relation.product.id}
434
+ productId={offer.product.id}
326
435
  compact
327
436
  />,
328
437
  );
329
438
 
330
439
  // In the header, we should display the product title, the product price
331
440
  // and product date range and languages
332
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
441
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
333
442
  // the price shouldn't be a heading to prevent misdirection for screen reader users,
334
443
  // but we want to it to visually look like a h6
335
444
 
@@ -339,7 +448,7 @@ describe('CourseProductItem', () => {
339
448
  expect($enrolledInfo.classList.contains('h6')).toBe(true);
340
449
 
341
450
  // - Any target courses information should be displayed
342
- relation.product.target_courses.forEach((course) => {
451
+ offer.product.target_courses.forEach((course) => {
343
452
  const $item = screen.queryByTestId(`course-item-${course.code}`);
344
453
  expect($item).not.toBeInTheDocument();
345
454
  });
@@ -350,8 +459,8 @@ describe('CourseProductItem', () => {
350
459
  );
351
460
 
352
461
  it.each(ENROLLABLE_ORDER_STATES)('renders product information for a %s order', async (state) => {
353
- const relation = CourseProductRelationFactory().one();
354
- const { product } = relation;
462
+ const offer = OfferFactory().one();
463
+ const { product } = offer;
355
464
  const order = CredentialOrderFactory({
356
465
  product_id: product.id,
357
466
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -359,10 +468,7 @@ describe('CourseProductItem', () => {
359
468
  state,
360
469
  }).one();
361
470
 
362
- fetchMock.get(
363
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
364
- relation,
365
- );
471
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
366
472
  const orderQueryParameters = {
367
473
  course_code: order.course.code,
368
474
  product_id: order.product_id,
@@ -412,17 +518,17 @@ describe('CourseProductItem', () => {
412
518
  it.each(ENROLLABLE_ORDER_STATES)(
413
519
  'renders product informations for a %s order in compact mode',
414
520
  async (state) => {
415
- const relation = CourseProductRelationFactory().one();
521
+ const offer = OfferFactory().one();
416
522
  const order: CredentialOrder = CredentialOrderFactory({
417
- product_id: relation.product.id,
523
+ product_id: offer.product.id,
418
524
  course: PacedCourseFactory({ code: '00000' }).one(),
419
- target_courses: relation.product.target_courses,
525
+ target_courses: offer.product.target_courses,
420
526
  state,
421
527
  }).one();
422
528
 
423
529
  fetchMock.get(
424
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${relation.product.id}/`,
425
- relation,
530
+ `https://joanie.endpoint/api/v1.0/courses/00000/products/${offer.product.id}/`,
531
+ offer,
426
532
  );
427
533
  const orderQueryParameters = {
428
534
  product_id: order.product_id,
@@ -436,14 +542,14 @@ describe('CourseProductItem', () => {
436
542
 
437
543
  render(
438
544
  <CourseProductItem
439
- productId={relation.product.id}
545
+ productId={offer.product.id}
440
546
  course={PacedCourseFactory({ code: '00000' }).one()}
441
547
  compact
442
548
  />,
443
549
  );
444
550
 
445
551
  // Wait for product information to be fetched
446
- await screen.findByRole('heading', { level: 3, name: relation.product.title });
552
+ await screen.findByRole('heading', { level: 3, name: offer.product.title });
447
553
 
448
554
  // - In place of product price, a label should be displayed
449
555
  const $enrolledInfo = await screen.findByText('Purchased');
@@ -480,8 +586,8 @@ describe('CourseProductItem', () => {
480
586
  );
481
587
 
482
588
  it('renders enrollment information when user is enrolled to a course run', async () => {
483
- const relation = CourseProductRelationFactory().one();
484
- const { product } = relation;
589
+ const offer = OfferFactory().one();
590
+ const { product } = offer;
485
591
  // - Create an order with an active enrollment
486
592
  const enrollment: Enrollment = EnrollmentFactory({
487
593
  course_run: product.target_courses[0]!.course_runs[0]! as CourseRun,
@@ -493,10 +599,7 @@ describe('CourseProductItem', () => {
493
599
  target_enrollments: [enrollment],
494
600
  }).one();
495
601
 
496
- fetchMock.get(
497
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
498
- relation,
499
- );
602
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
500
603
  const orderQueryParameters = {
501
604
  product_id: order.product_id,
502
605
  course_code: order.course?.code,
@@ -547,8 +650,8 @@ describe('CourseProductItem', () => {
547
650
  it.each(PURCHASABLE_ORDER_STATES)(
548
651
  'renders sale tunnel button if user already has a %s order',
549
652
  async (state) => {
550
- const relation = CourseProductRelationFactory().one();
551
- const { product } = relation;
653
+ const offer = OfferFactory().one();
654
+ const { product } = offer;
552
655
  const order = CredentialOrderFactory({
553
656
  product_id: product.id,
554
657
  course: PacedCourseFactory({ code: '00000' }).one(),
@@ -557,7 +660,7 @@ describe('CourseProductItem', () => {
557
660
  }).one();
558
661
  fetchMock.get(
559
662
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
560
- relation,
663
+ offer,
561
664
  );
562
665
  const orderQueryParameters = {
563
666
  product_id: order.product_id,
@@ -588,7 +691,7 @@ describe('CourseProductItem', () => {
588
691
  expect($price.classList.contains('h6')).toBe(true);
589
692
 
590
693
  // - Render all target courses information
591
- relation.product.target_courses.forEach((course) => {
694
+ offer.product.target_courses.forEach((course) => {
592
695
  const $item = screen.getByTestId(`course-item-${course.code}`);
593
696
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
594
697
  // but we want to it to visually look like a h5
@@ -603,12 +706,9 @@ describe('CourseProductItem', () => {
603
706
  );
604
707
 
605
708
  it('renders sale tunnel button if user already has a canceled order', async () => {
606
- const relation = CourseProductRelationFactory().one();
607
- const { product } = relation;
608
- fetchMock.get(
609
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
610
- relation,
611
- );
709
+ const offer = OfferFactory().one();
710
+ const { product } = offer;
711
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
612
712
  const orderQueryParameters = {
613
713
  product_id: product.id,
614
714
  course_code: '00000',
@@ -638,7 +738,7 @@ describe('CourseProductItem', () => {
638
738
  expect($price.classList.contains('h6')).toBe(true);
639
739
 
640
740
  // - Render all target courses information
641
- relation.product.target_courses.forEach((course) => {
741
+ offer.product.target_courses.forEach((course) => {
642
742
  const $item = screen.getByTestId(`course-item-${course.code}`);
643
743
  // the course title shouldn't be a heading to prevent misdirection for screen reader users,
644
744
  // but we want to it to visually look like a h5
@@ -652,7 +752,7 @@ describe('CourseProductItem', () => {
652
752
  });
653
753
 
654
754
  it('renders error message when product fetching has failed', async () => {
655
- const { product } = CourseProductRelationFactory().one();
755
+ const { product } = OfferFactory().one();
656
756
 
657
757
  fetchMock.get(
658
758
  `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
@@ -673,20 +773,18 @@ describe('CourseProductItem', () => {
673
773
  });
674
774
 
675
775
  it('renders a warning message that tells that no seats are left', async () => {
676
- const relation = CourseProductRelationFactory({
677
- order_groups: [OrderGroupFullFactory().one()],
776
+ const offer = OfferFactory({
777
+ seats: 2,
778
+ nb_seats_available: 0,
678
779
  }).one();
679
- const { product } = relation;
780
+ const { product } = offer;
680
781
  const order = CredentialOrderFactory({
681
782
  product_id: product.id,
682
783
  course: PacedCourseFactory({ code: '00000' }).one(),
683
784
  target_courses: product.target_courses,
684
785
  state: OrderState.DRAFT,
685
786
  }).one();
686
- fetchMock.get(
687
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
688
- relation,
689
- );
787
+ fetchMock.get(`https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`, offer);
690
788
  const orderQueryParameters = {
691
789
  product_id: order.product_id,
692
790
  course_code: order.course?.code,
@@ -710,86 +808,4 @@ describe('CourseProductItem', () => {
710
808
  expect(screen.queryByRole('button', { name: product.call_to_action })).not.toBeInTheDocument();
711
809
  screen.getByText('Sorry, no seats available for now');
712
810
  });
713
-
714
- it('renders one payment button when one of two order groups is full', async () => {
715
- const relation = CourseProductRelationFactory({
716
- order_groups: [OrderGroupFullFactory().one(), OrderGroupFactory().one()],
717
- }).one();
718
- const { product } = relation;
719
- const order = CredentialOrderFactory({
720
- product_id: product.id,
721
- course: PacedCourseFactory({ code: '00000' }).one(),
722
- target_courses: product.target_courses,
723
- state: OrderState.DRAFT,
724
- }).one();
725
- fetchMock.get(
726
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
727
- relation,
728
- );
729
- const orderQueryParameters = {
730
- product_id: order.product_id,
731
- course_code: order.course?.code,
732
- state: NOT_CANCELED_ORDER_STATES,
733
- };
734
- fetchMock.get(
735
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
736
- [order],
737
- );
738
-
739
- render(
740
- <CourseProductItem
741
- productId={product.id}
742
- course={PacedCourseFactory({ code: '00000' }).one()}
743
- />,
744
- );
745
-
746
- // wait for component to be fully loaded
747
- await screen.findByRole('heading', { level: 3, name: product.title });
748
-
749
- expect(screen.queryByText('Sorry, no seats available for now')).not.toBeInTheDocument();
750
- screen.getByRole('button', { name: product.call_to_action });
751
- screen.getByText(relation.order_groups[1].nb_available_seats + ' remaining seats');
752
- });
753
-
754
- it('renders mutliple payment button when there are multiple order groups', async () => {
755
- const relation = CourseProductRelationFactory({
756
- order_groups: [OrderGroupFactory().one(), OrderGroupFactory({ nb_available_seats: 1 }).one()],
757
- }).one();
758
- const { product } = relation;
759
- const order = CredentialOrderFactory({
760
- product_id: product.id,
761
- course: PacedCourseFactory({ code: '00000' }).one(),
762
- target_courses: product.target_courses,
763
- state: OrderState.DRAFT,
764
- }).one();
765
- fetchMock.get(
766
- `https://joanie.endpoint/api/v1.0/courses/00000/products/${product.id}/`,
767
- relation,
768
- );
769
- const orderQueryParameters = {
770
- product_id: order.product_id,
771
- course_code: order.course?.code,
772
- state: NOT_CANCELED_ORDER_STATES,
773
- };
774
- fetchMock.get(
775
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
776
- [order],
777
- );
778
-
779
- render(
780
- <CourseProductItem
781
- productId={product.id}
782
- course={PacedCourseFactory({ code: '00000' }).one()}
783
- />,
784
- );
785
-
786
- // wait for component to be fully loaded
787
- await screen.findByRole('heading', { level: 3, name: product.title });
788
-
789
- expect(screen.queryByText('Sorry, no seats available for now')).not.toBeInTheDocument();
790
- expect(screen.getAllByTestId('PurchaseButton__cta')).toHaveLength(2);
791
- expect(screen.getAllByRole('button', { name: product.call_to_action })).toHaveLength(2);
792
- screen.getByText(relation.order_groups[0].nb_available_seats + ' remaining seats');
793
- screen.getByText('Last remaining seat!');
794
- });
795
811
  });
@@ -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,14 @@ const render = (args: CourseProductItemProps, options?: Maybe<{ order: Credentia
22
22
  fetchMock.get(`http://localhost:8071/api/v1.0/addresses/`, [], { overwriteRoutes: true });
23
23
  fetchMock.get(
24
24
  `http://localhost:8071/api/v1.0/courses/${args.course.code}/products/${args.productId}/`,
25
- CourseProductRelationFactory({ product: CredentialProductFactory().one() }).one(),
25
+ OfferFactory({
26
+ product: CredentialProductFactory({
27
+ price: 840,
28
+ price_currency: 'EUR',
29
+ }).one(),
30
+ discounted_price: 800,
31
+ discount_rate: 0.3,
32
+ }).one(),
26
33
  { overwriteRoutes: true },
27
34
  );
28
35
  fetchMock.get(