richie-education 3.1.3-dev23 → 3.1.3-dev27
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/.storybook/__mocks__/utils/context.ts +4 -0
- package/js/components/CourseGlimpse/CourseGlimpseFooter.tsx +30 -5
- package/js/components/CourseGlimpse/index.spec.tsx +16 -0
- package/js/components/CourseGlimpse/index.stories.tsx +75 -4
- package/js/components/CourseGlimpse/index.tsx +2 -0
- package/js/components/CourseGlimpse/utils.ts +6 -0
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +11 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +6 -6
- package/js/components/SaleTunnel/index.spec.tsx +55 -1
- package/js/components/SaleTunnel/index.stories.tsx +17 -2
- package/js/types/Course.ts +2 -0
- package/js/types/Joanie.ts +2 -0
- package/js/types/index.ts +2 -0
- package/js/utils/test/factories/joanie.ts +2 -0
- package/js/utils/test/factories/richie.ts +4 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.stories.tsx +81 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +14 -0
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +14 -0
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +46 -0
- package/package.json +1 -1
- package/scss/objects/_course_glimpses.scss +15 -0
|
@@ -65,6 +65,35 @@ export const CourseGlimpseFooter: React.FC<{ course: CourseGlimpseCourse } & Com
|
|
|
65
65
|
const offerIcon = `icon-offer-${offer}` as OfferIconType;
|
|
66
66
|
const offerCertificateIcon = hasCertificateOffer && IconTypeEnum.SCHOOL;
|
|
67
67
|
const offerPrice = hasEnrollmentOffer && course.price;
|
|
68
|
+
const discountedPrice = course.discounted_price ?? null;
|
|
69
|
+
const hasDiscount = discountedPrice !== null;
|
|
70
|
+
|
|
71
|
+
let $price = null;
|
|
72
|
+
|
|
73
|
+
if (offerPrice) {
|
|
74
|
+
if (hasDiscount) {
|
|
75
|
+
$price = (
|
|
76
|
+
<div className="offer_prices">
|
|
77
|
+
<span className="offer__price offer__price--striked">
|
|
78
|
+
<FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
|
|
79
|
+
</span>
|
|
80
|
+
<span className="offer__price offer__price--discounted">
|
|
81
|
+
<FormattedNumber
|
|
82
|
+
value={discountedPrice}
|
|
83
|
+
currency={course.price_currency}
|
|
84
|
+
style="currency"
|
|
85
|
+
/>
|
|
86
|
+
</span>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
} else {
|
|
90
|
+
$price = (
|
|
91
|
+
<span className="offer__price">
|
|
92
|
+
<FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
|
|
93
|
+
</span>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
68
97
|
|
|
69
98
|
return (
|
|
70
99
|
<div className="course-glimpse-footer">
|
|
@@ -99,11 +128,7 @@ export const CourseGlimpseFooter: React.FC<{ course: CourseGlimpseCourse } & Com
|
|
|
99
128
|
name={offerIcon}
|
|
100
129
|
title={intl.formatMessage(courseOfferMessages[offer])}
|
|
101
130
|
/>
|
|
102
|
-
{
|
|
103
|
-
<span className="offer__price">
|
|
104
|
-
<FormattedNumber value={offerPrice} currency={course.price_currency} style="currency" />
|
|
105
|
-
</span>
|
|
106
|
-
)}
|
|
131
|
+
{$price}
|
|
107
132
|
</div>
|
|
108
133
|
</div>
|
|
109
134
|
);
|
|
@@ -61,6 +61,8 @@ describe('widgets/Search/components/CourseGlimpse', () => {
|
|
|
61
61
|
price_currency: 'EUR',
|
|
62
62
|
discounted_price: null,
|
|
63
63
|
discount: null,
|
|
64
|
+
certificate_discounted_price: null,
|
|
65
|
+
certificate_discount: null,
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
const contextProps: CommonDataProps['context'] = RichieContextFactory().one();
|
|
@@ -170,6 +172,20 @@ describe('widgets/Search/components/CourseGlimpse', () => {
|
|
|
170
172
|
);
|
|
171
173
|
});
|
|
172
174
|
|
|
175
|
+
it('renders a course glimpse with a discount', () => {
|
|
176
|
+
const { container } = renderCourseGlimpse({
|
|
177
|
+
contextProps,
|
|
178
|
+
course: { ...course, price: 100.0, discount: '30%', discounted_price: 70.0 },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const prices = container.getElementsByClassName('offer_prices');
|
|
182
|
+
expect(prices.length).toBe(1);
|
|
183
|
+
expect(prices[0].children.length).toBe(2);
|
|
184
|
+
const discountedPrice = container.getElementsByClassName('offer__price--discounted');
|
|
185
|
+
expect(discountedPrice.length).toBe(1);
|
|
186
|
+
expect(discountedPrice[0]).toHaveTextContent('€70.00');
|
|
187
|
+
});
|
|
188
|
+
|
|
173
189
|
it('does not show certificate offer if the course does not offer a certificate', () => {
|
|
174
190
|
const { container } = renderCourseGlimpse({
|
|
175
191
|
contextProps,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { CourseLightFactory, RichieContextFactory } from 'utils/test/factories/richie';
|
|
3
|
+
import { CourseGlimpse, getCourseGlimpseProps } from 'components/CourseGlimpse';
|
|
4
|
+
import { CourseCertificateOffer, CourseOffer } from 'types/Course';
|
|
4
5
|
|
|
5
6
|
export default {
|
|
6
7
|
component: CourseGlimpse,
|
|
@@ -8,9 +9,79 @@ export default {
|
|
|
8
9
|
|
|
9
10
|
type Story = StoryObj<typeof CourseGlimpse>;
|
|
10
11
|
|
|
12
|
+
const richieContext = RichieContextFactory().one();
|
|
13
|
+
const courseLight = CourseLightFactory().one();
|
|
14
|
+
const courseGlimpseCourse = getCourseGlimpseProps(courseLight);
|
|
15
|
+
|
|
11
16
|
export const RichieCourse: Story = {
|
|
12
17
|
args: {
|
|
13
|
-
context:
|
|
14
|
-
course:
|
|
18
|
+
context: richieContext,
|
|
19
|
+
course: { ...courseGlimpseCourse },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const certificateProduct: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
context: richieContext,
|
|
26
|
+
course: {
|
|
27
|
+
...courseGlimpseCourse,
|
|
28
|
+
title: 'Certificate Product',
|
|
29
|
+
offer: CourseOffer.FREE,
|
|
30
|
+
price: null,
|
|
31
|
+
certificate_offer: CourseCertificateOffer.PAID,
|
|
32
|
+
certificate_price: 100,
|
|
33
|
+
discounted_price: null,
|
|
34
|
+
discount: null,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const certificateProductDiscount: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
context: richieContext,
|
|
42
|
+
course: {
|
|
43
|
+
...courseGlimpseCourse,
|
|
44
|
+
title: 'Certificate Product with Discount',
|
|
45
|
+
offer: CourseOffer.FREE,
|
|
46
|
+
price: null,
|
|
47
|
+
certificate_offer: CourseCertificateOffer.PAID,
|
|
48
|
+
certificate_price: 100,
|
|
49
|
+
discounted_price: 80,
|
|
50
|
+
discount: '-20 €',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const credentialProduct: Story = {
|
|
56
|
+
args: {
|
|
57
|
+
context: richieContext,
|
|
58
|
+
course: {
|
|
59
|
+
...courseGlimpseCourse,
|
|
60
|
+
title: 'Credential Product',
|
|
61
|
+
icon: null,
|
|
62
|
+
offer: CourseOffer.PAID,
|
|
63
|
+
price: 100,
|
|
64
|
+
certificate_offer: null,
|
|
65
|
+
certificate_price: null,
|
|
66
|
+
discounted_price: null,
|
|
67
|
+
discount: null,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const credentialProductDiscount: Story = {
|
|
73
|
+
args: {
|
|
74
|
+
context: richieContext,
|
|
75
|
+
course: {
|
|
76
|
+
...courseGlimpseCourse,
|
|
77
|
+
title: 'Credential Product with Discount',
|
|
78
|
+
icon: null,
|
|
79
|
+
offer: CourseOffer.PAID,
|
|
80
|
+
price: 100,
|
|
81
|
+
certificate_offer: null,
|
|
82
|
+
certificate_price: null,
|
|
83
|
+
discounted_price: 80,
|
|
84
|
+
discount: '-20 €',
|
|
85
|
+
},
|
|
15
86
|
},
|
|
16
87
|
};
|
|
@@ -49,6 +49,8 @@ export interface CourseGlimpseCourse {
|
|
|
49
49
|
price_currency: string;
|
|
50
50
|
discounted_price: Nullable<number>;
|
|
51
51
|
discount: Nullable<string>;
|
|
52
|
+
certificate_discounted_price: Nullable<number>;
|
|
53
|
+
certificate_discount: Nullable<string>;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export interface CourseGlimpseProps {
|
|
@@ -55,6 +55,8 @@ const getCourseGlimpsePropsFromOffering = (
|
|
|
55
55
|
price_currency: offering.product.price_currency,
|
|
56
56
|
discounted_price: offering.product.discounted_price || null,
|
|
57
57
|
discount: offering.product.discount || null,
|
|
58
|
+
certificate_discounted_price: offering.product.certificate_discounted_price || null,
|
|
59
|
+
certificate_discount: offering.product.certificate_discount || null,
|
|
58
60
|
};
|
|
59
61
|
};
|
|
60
62
|
|
|
@@ -79,6 +81,8 @@ const getCourseGlimpsePropsFromRichieCourse = (course: RichieCourse): CourseGlim
|
|
|
79
81
|
certificate_offer: course.certificate_offer,
|
|
80
82
|
offer: course.offer,
|
|
81
83
|
certificate_price: course.certificate_price,
|
|
84
|
+
certificate_discounted_price: course.certificate_discounted_price,
|
|
85
|
+
certificate_discount: course.certificate_discount,
|
|
82
86
|
discounted_price: course.discounted_price,
|
|
83
87
|
discount: course.discount,
|
|
84
88
|
});
|
|
@@ -120,6 +124,8 @@ const getCourseGlimpsePropsFromJoanieCourse = (
|
|
|
120
124
|
certificate_price: null,
|
|
121
125
|
discounted_price: null,
|
|
122
126
|
discount: null,
|
|
127
|
+
certificate_discounted_price: null,
|
|
128
|
+
certificate_discount: null,
|
|
123
129
|
};
|
|
124
130
|
};
|
|
125
131
|
|
|
@@ -10,7 +10,15 @@ import {
|
|
|
10
10
|
} from 'react';
|
|
11
11
|
import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
|
|
12
12
|
import { SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
Address,
|
|
15
|
+
Enrollment,
|
|
16
|
+
Offering,
|
|
17
|
+
CreditCard,
|
|
18
|
+
Order,
|
|
19
|
+
OrderState,
|
|
20
|
+
Product,
|
|
21
|
+
} from 'types/Joanie';
|
|
14
22
|
import useProductOrder from 'hooks/useProductOrder';
|
|
15
23
|
import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
|
|
16
24
|
import WebAnalyticsAPIHandler from 'api/web-analytics';
|
|
@@ -27,6 +35,7 @@ export interface SaleTunnelContextType {
|
|
|
27
35
|
product: Product;
|
|
28
36
|
webAnalyticsEventKey: string;
|
|
29
37
|
offering?: Offering;
|
|
38
|
+
enrollment?: Enrollment;
|
|
30
39
|
|
|
31
40
|
// internal
|
|
32
41
|
step: SaleTunnelStep;
|
|
@@ -115,6 +124,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
|
|
|
115
124
|
order,
|
|
116
125
|
product: props.product,
|
|
117
126
|
offering: props.offering,
|
|
127
|
+
enrollment: props.enrollment,
|
|
118
128
|
props,
|
|
119
129
|
billingAddress,
|
|
120
130
|
setBillingAddress,
|
|
@@ -101,7 +101,11 @@ const Email = () => {
|
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
const Total = () => {
|
|
104
|
-
const { product, offering } = useSaleTunnelContext();
|
|
104
|
+
const { product, offering, enrollment } = useSaleTunnelContext();
|
|
105
|
+
const totalPrice =
|
|
106
|
+
enrollment?.offerings?.[0]?.rules?.discounted_price ??
|
|
107
|
+
offering?.rules.discounted_price ??
|
|
108
|
+
product.price;
|
|
105
109
|
return (
|
|
106
110
|
<div className="sale-tunnel__total">
|
|
107
111
|
<div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
|
|
@@ -109,11 +113,7 @@ const Total = () => {
|
|
|
109
113
|
<FormattedMessage {...messages.totalLabel} />
|
|
110
114
|
</div>
|
|
111
115
|
<div className="block-title">
|
|
112
|
-
<FormattedNumber
|
|
113
|
-
value={offering?.rules.discounted_price || product.price}
|
|
114
|
-
style="currency"
|
|
115
|
-
currency={product.price_currency}
|
|
116
|
-
/>
|
|
116
|
+
<FormattedNumber value={totalPrice} style="currency" currency={product.price_currency} />
|
|
117
117
|
</div>
|
|
118
118
|
</div>
|
|
119
119
|
</div>
|
|
@@ -8,6 +8,7 @@ import { useState } from 'react';
|
|
|
8
8
|
import { OrderState, Product, ProductType, NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
|
|
9
9
|
import {
|
|
10
10
|
RichieContextFactory as mockRichieContextFactory,
|
|
11
|
+
CourseStateFactory,
|
|
11
12
|
UserFactory,
|
|
12
13
|
PacedCourseFactory,
|
|
13
14
|
} from 'utils/test/factories/richie';
|
|
@@ -16,12 +17,14 @@ import {
|
|
|
16
17
|
CertificateOrderFactory,
|
|
17
18
|
CertificateProductFactory,
|
|
18
19
|
OfferingFactory,
|
|
20
|
+
CourseRunFactory,
|
|
19
21
|
CredentialOrderFactory,
|
|
20
22
|
CredentialProductFactory,
|
|
21
23
|
CreditCardFactory,
|
|
22
24
|
EnrollmentFactory,
|
|
23
25
|
PaymentInstallmentFactory,
|
|
24
26
|
} from 'utils/test/factories/joanie';
|
|
27
|
+
import { Priority } from 'types';
|
|
25
28
|
import { render } from 'utils/test/render';
|
|
26
29
|
import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
|
|
27
30
|
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
@@ -97,7 +100,7 @@ describe.each([
|
|
|
97
100
|
return (
|
|
98
101
|
<SaleTunnel
|
|
99
102
|
{...props}
|
|
100
|
-
enrollment={enrollment}
|
|
103
|
+
enrollment={props.enrollment ?? enrollment}
|
|
101
104
|
course={productType === ProductType.CREDENTIAL ? course : undefined}
|
|
102
105
|
isOpen={open}
|
|
103
106
|
onClose={() => setOpen(false)}
|
|
@@ -449,6 +452,57 @@ describe.each([
|
|
|
449
452
|
);
|
|
450
453
|
});
|
|
451
454
|
|
|
455
|
+
// Fixes the issue : https://github.com/openfun/richie/issues/2645
|
|
456
|
+
it('should show the certificate product total with discounted price', async () => {
|
|
457
|
+
const product = ProductFactory({
|
|
458
|
+
price: 600,
|
|
459
|
+
target_courses: [course],
|
|
460
|
+
}).one();
|
|
461
|
+
const enrollmentDiscounted = EnrollmentFactory({
|
|
462
|
+
course_run: CourseRunFactory({
|
|
463
|
+
state: CourseStateFactory({ priority: Priority.ONGOING_OPEN }).one(),
|
|
464
|
+
course,
|
|
465
|
+
}).one(),
|
|
466
|
+
offerings: [
|
|
467
|
+
OfferingFactory({
|
|
468
|
+
product,
|
|
469
|
+
rules: {
|
|
470
|
+
discounted_price: 540,
|
|
471
|
+
discount_rate: 0.1,
|
|
472
|
+
},
|
|
473
|
+
}).one(),
|
|
474
|
+
],
|
|
475
|
+
}).one();
|
|
476
|
+
|
|
477
|
+
if (product.type === ProductType.CERTIFICATE) {
|
|
478
|
+
enrollmentDiscounted.offerings[0].product = product;
|
|
479
|
+
|
|
480
|
+
fetchMock.get(
|
|
481
|
+
`https://joanie.endpoint/api/v1.0/orders/?enrollment_id=${enrollmentDiscounted.id}&product_id=${product.id}&state=pending&state=pending_payment&state=no_payment&state=failed_payment&state=completed&state=draft&state=assigned&state=to_sign&state=signing&state=to_save_payment_method`,
|
|
482
|
+
{
|
|
483
|
+
results: [],
|
|
484
|
+
next: null,
|
|
485
|
+
previous: null,
|
|
486
|
+
count: 0,
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
render(
|
|
491
|
+
<Wrapper product={product} enrollment={enrollmentDiscounted} isWithdrawable={true} />,
|
|
492
|
+
{ queryOptions: { client: createTestQueryClient({ user: richieUser }) } },
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
const $totalAmount = screen.getByTestId('sale-tunnel__total__amount');
|
|
496
|
+
expect($totalAmount).toHaveTextContent(
|
|
497
|
+
'Total' +
|
|
498
|
+
formatPrice(
|
|
499
|
+
enrollmentDiscounted.offerings[0].rules.discounted_price!,
|
|
500
|
+
product.price_currency,
|
|
501
|
+
).replace(/(\u202F|\u00a0)/g, ' '),
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
452
506
|
it('should show the product payment schedule with discounted price', async () => {
|
|
453
507
|
const intl = createIntl({ locale: 'en' });
|
|
454
508
|
const schedule = PaymentInstallmentFactory().many(2);
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { StoryObj, Meta } from '@storybook/react';
|
|
2
2
|
import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CertificateProductFactory,
|
|
5
|
+
EnrollmentFactory,
|
|
6
|
+
OfferingFactory,
|
|
7
|
+
ProductFactory,
|
|
8
|
+
} from 'utils/test/factories/joanie';
|
|
4
9
|
import { PacedCourseFactory } from 'utils/test/factories/richie';
|
|
5
10
|
import { SaleTunnel, SaleTunnelProps } from './index';
|
|
6
11
|
|
|
@@ -28,6 +33,16 @@ export default {
|
|
|
28
33
|
|
|
29
34
|
type Story = StoryObj<typeof SaleTunnel>;
|
|
30
35
|
|
|
31
|
-
export const
|
|
36
|
+
export const Credential: Story = {
|
|
32
37
|
args: {},
|
|
33
38
|
};
|
|
39
|
+
|
|
40
|
+
export const CertificateDiscount: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
product: CertificateProductFactory({ price: 100, price_currency: 'EUR' }).one(),
|
|
43
|
+
course: PacedCourseFactory().one(),
|
|
44
|
+
enrollment: EnrollmentFactory({
|
|
45
|
+
offerings: OfferingFactory({ rules: { discounted_price: 80 } }).many(1),
|
|
46
|
+
}).one(),
|
|
47
|
+
},
|
|
48
|
+
};
|
package/js/types/Course.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface Course extends Resource {
|
|
|
49
49
|
price_currency: string;
|
|
50
50
|
discounted_price: Nullable<number>;
|
|
51
51
|
discount: Nullable<string>;
|
|
52
|
+
certificate_discounted_price: Nullable<number>;
|
|
53
|
+
certificate_discount: Nullable<string>;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export function isRichieCourse(course: Course | JoanieCourse): course is Course {
|
package/js/types/Joanie.ts
CHANGED
|
@@ -151,6 +151,8 @@ export interface Product {
|
|
|
151
151
|
contract_definition?: ContractDefinition;
|
|
152
152
|
discounted_price: Nullable<number>;
|
|
153
153
|
discount: Nullable<string>;
|
|
154
|
+
certificate_discounted_price: Nullable<number>;
|
|
155
|
+
certificate_discount: Nullable<string>;
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
export interface CredentialProduct extends Product {
|
package/js/types/index.ts
CHANGED
|
@@ -85,6 +85,8 @@ export const CourseRunFactory = factory<CourseRun>(() => {
|
|
|
85
85
|
certificate_offer: certificateOffer,
|
|
86
86
|
discounted_price: null,
|
|
87
87
|
discount: null,
|
|
88
|
+
certificate_discounted_price: null,
|
|
89
|
+
certificate_discount: null,
|
|
88
90
|
};
|
|
89
91
|
});
|
|
90
92
|
|
|
@@ -248,5 +250,7 @@ export const CourseLightFactory = factory<Course>(() => {
|
|
|
248
250
|
price_currency: 'EUR',
|
|
249
251
|
discounted_price: null,
|
|
250
252
|
discount: null,
|
|
253
|
+
certificate_discounted_price: null,
|
|
254
|
+
certificate_discount: null,
|
|
251
255
|
};
|
|
252
256
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { CourseRunFactory, PacedCourseFactory } from 'utils/test/factories/richie';
|
|
3
|
+
import { StorybookHelper } from 'utils/StorybookHelper';
|
|
4
|
+
import { CourseCertificateOffer, CourseOffer } from '../../../../types/Course';
|
|
5
|
+
import { SyllabusCourseRun } from '.';
|
|
6
|
+
|
|
7
|
+
export default {
|
|
8
|
+
component: SyllabusCourseRun,
|
|
9
|
+
render: (args) => {
|
|
10
|
+
return StorybookHelper.wrapInApp(<SyllabusCourseRun {...args} />);
|
|
11
|
+
},
|
|
12
|
+
} as Meta<typeof SyllabusCourseRun>;
|
|
13
|
+
|
|
14
|
+
type Story = StoryObj<typeof SyllabusCourseRun>;
|
|
15
|
+
|
|
16
|
+
const courseRun = CourseRunFactory().one();
|
|
17
|
+
|
|
18
|
+
export const certificateSyllabusCourseRun: Story = {
|
|
19
|
+
args: {
|
|
20
|
+
courseRun: {
|
|
21
|
+
...courseRun,
|
|
22
|
+
title: 'Certificate Product',
|
|
23
|
+
price_currency: 'EUR',
|
|
24
|
+
offer: CourseOffer.FREE,
|
|
25
|
+
certificate_offer: CourseCertificateOffer.PAID,
|
|
26
|
+
certificate_price: 100,
|
|
27
|
+
discounted_price: null,
|
|
28
|
+
discount: null,
|
|
29
|
+
},
|
|
30
|
+
course: PacedCourseFactory().one(),
|
|
31
|
+
showLanguages: true,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
export const certificateDiscountSyllabusCourseRun: Story = {
|
|
35
|
+
args: {
|
|
36
|
+
courseRun: {
|
|
37
|
+
...courseRun,
|
|
38
|
+
title: 'Certificate Product',
|
|
39
|
+
price_currency: 'EUR',
|
|
40
|
+
offer: CourseOffer.FREE,
|
|
41
|
+
certificate_offer: CourseCertificateOffer.PAID,
|
|
42
|
+
certificate_price: 100,
|
|
43
|
+
certificate_discounted_price: 80,
|
|
44
|
+
certificate_discount: '-20 €',
|
|
45
|
+
},
|
|
46
|
+
course: PacedCourseFactory().one(),
|
|
47
|
+
showLanguages: true,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
export const credentialSyllabusCourseRun: Story = {
|
|
51
|
+
args: {
|
|
52
|
+
courseRun: {
|
|
53
|
+
...courseRun,
|
|
54
|
+
title: 'Certificate Product',
|
|
55
|
+
price_currency: 'EUR',
|
|
56
|
+
offer: CourseOffer.PAID,
|
|
57
|
+
price: 100,
|
|
58
|
+
certificate_offer: CourseCertificateOffer.FREE,
|
|
59
|
+
discounted_price: null,
|
|
60
|
+
discount: null,
|
|
61
|
+
},
|
|
62
|
+
course: PacedCourseFactory().one(),
|
|
63
|
+
showLanguages: true,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
export const credentialDiscountSyllabusCourseRun: Story = {
|
|
67
|
+
args: {
|
|
68
|
+
courseRun: {
|
|
69
|
+
...courseRun,
|
|
70
|
+
title: 'Certificate Product',
|
|
71
|
+
price_currency: 'EUR',
|
|
72
|
+
offer: CourseOffer.PAID,
|
|
73
|
+
price: 100,
|
|
74
|
+
certificate_offer: CourseCertificateOffer.FREE,
|
|
75
|
+
discounted_price: 80,
|
|
76
|
+
discount: '-20 €',
|
|
77
|
+
},
|
|
78
|
+
course: PacedCourseFactory().one(),
|
|
79
|
+
showLanguages: true,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
@@ -128,6 +128,13 @@ const OpenedCourseRun = ({
|
|
|
128
128
|
currency: courseRun.price_currency,
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
+
|
|
132
|
+
if ((courseRun.discounted_price ?? -1) >= 0) {
|
|
133
|
+
enrollmentPrice = intl.formatNumber(courseRun.discounted_price!, {
|
|
134
|
+
style: 'currency',
|
|
135
|
+
currency: courseRun.price_currency,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
if (courseRun.certificate_offer) {
|
|
@@ -144,6 +151,13 @@ const OpenedCourseRun = ({
|
|
|
144
151
|
currency: courseRun.price_currency,
|
|
145
152
|
});
|
|
146
153
|
}
|
|
154
|
+
|
|
155
|
+
if ((courseRun.certificate_discounted_price ?? -1) >= 0) {
|
|
156
|
+
certificatePrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
|
|
157
|
+
style: 'currency',
|
|
158
|
+
currency: courseRun.price_currency,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
147
161
|
}
|
|
148
162
|
|
|
149
163
|
return (
|
|
@@ -119,6 +119,13 @@ const OpenedSelfPacedCourseRun = ({
|
|
|
119
119
|
currency: courseRun.price_currency,
|
|
120
120
|
});
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
if ((courseRun.discounted_price ?? -1) >= 0) {
|
|
124
|
+
enrollmentPrice = intl.formatNumber(courseRun.discounted_price!, {
|
|
125
|
+
style: 'currency',
|
|
126
|
+
currency: courseRun.price_currency,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
122
129
|
}
|
|
123
130
|
|
|
124
131
|
if (courseRun.certificate_offer) {
|
|
@@ -135,6 +142,13 @@ const OpenedSelfPacedCourseRun = ({
|
|
|
135
142
|
currency: courseRun.price_currency,
|
|
136
143
|
});
|
|
137
144
|
}
|
|
145
|
+
|
|
146
|
+
if ((courseRun.certificate_discounted_price ?? -1) >= 0) {
|
|
147
|
+
certificatePrice = intl.formatNumber(courseRun.certificate_discounted_price!, {
|
|
148
|
+
style: 'currency',
|
|
149
|
+
currency: courseRun.price_currency,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
138
152
|
}
|
|
139
153
|
|
|
140
154
|
return (
|
|
@@ -1465,4 +1465,50 @@ describe('<SyllabusCourseRunsList/>', () => {
|
|
|
1465
1465
|
expect(content).not.toContain('The certification process is');
|
|
1466
1466
|
expect(content).not.toContain('<br>€59.99');
|
|
1467
1467
|
});
|
|
1468
|
+
|
|
1469
|
+
it('renders price discount on SyllabusCourseRun', async () => {
|
|
1470
|
+
const course = PacedCourseFactory().one();
|
|
1471
|
+
const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
|
|
1472
|
+
languages: ['en'],
|
|
1473
|
+
price_currency: 'EUR',
|
|
1474
|
+
offer: 'paid',
|
|
1475
|
+
price: 49.99,
|
|
1476
|
+
certificate_offer: undefined,
|
|
1477
|
+
certificate_price: undefined,
|
|
1478
|
+
discounted_price: 30.0,
|
|
1479
|
+
discount: '-20%',
|
|
1480
|
+
}).one();
|
|
1481
|
+
|
|
1482
|
+
render(
|
|
1483
|
+
<div className="course-detail__row course-detail__runs course-detail__runs--open">
|
|
1484
|
+
<SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
|
|
1485
|
+
</div>,
|
|
1486
|
+
);
|
|
1487
|
+
|
|
1488
|
+
const content = getHeaderContainer().innerHTML;
|
|
1489
|
+
expect(content).toContain('<dd>Paid access<br>€30.00</dd>');
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
it('renders certificate discount on SyllabusCourseRun', async () => {
|
|
1493
|
+
const course = PacedCourseFactory().one();
|
|
1494
|
+
const courseRun: CourseRun = CourseRunFactoryFromPriority(Priority.ONGOING_OPEN)({
|
|
1495
|
+
languages: ['en'],
|
|
1496
|
+
price_currency: 'EUR',
|
|
1497
|
+
offer: 'free',
|
|
1498
|
+
price: undefined,
|
|
1499
|
+
certificate_offer: 'paid',
|
|
1500
|
+
certificate_price: 100.0,
|
|
1501
|
+
certificate_discounted_price: 70.0,
|
|
1502
|
+
certificate_discount: '-30%',
|
|
1503
|
+
}).one();
|
|
1504
|
+
|
|
1505
|
+
render(
|
|
1506
|
+
<div className="course-detail__row course-detail__runs course-detail__runs--open">
|
|
1507
|
+
<SyllabusCourseRunCompacted courseRun={courseRun} course={course} showLanguages={false} />
|
|
1508
|
+
</div>,
|
|
1509
|
+
);
|
|
1510
|
+
|
|
1511
|
+
const content = getHeaderContainer().innerHTML;
|
|
1512
|
+
expect(content).toContain('<dd>Paid certificate<br>€70.00</dd>');
|
|
1513
|
+
});
|
|
1468
1514
|
});
|
package/package.json
CHANGED
|
@@ -396,6 +396,11 @@ $course-glimpse-content-padding-sides: 0.7rem !default;
|
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
.offer_prices {
|
|
400
|
+
display: flex;
|
|
401
|
+
flex-direction: column;
|
|
402
|
+
}
|
|
403
|
+
|
|
399
404
|
.offer__price {
|
|
400
405
|
$visibility: r-theme-val(course-glimpse, offer-price-visibility);
|
|
401
406
|
@if $visibility == hidden {
|
|
@@ -404,6 +409,16 @@ $course-glimpse-content-padding-sides: 0.7rem !default;
|
|
|
404
409
|
visibility: $visibility;
|
|
405
410
|
// Align vertically the price with the icon
|
|
406
411
|
margin-top: calc(1ex - 1cap);
|
|
412
|
+
|
|
413
|
+
&--striked,
|
|
414
|
+
&--discounted {
|
|
415
|
+
display: inline-block;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
&--striked {
|
|
419
|
+
text-decoration: line-through;
|
|
420
|
+
opacity: 0.5;
|
|
421
|
+
}
|
|
407
422
|
}
|
|
408
423
|
}
|
|
409
424
|
}
|