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