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.
@@ -13,3 +13,7 @@ let context = {
13
13
  (window as any).__richie_frontend_context__ = {
14
14
  context: RichieContextFactory(context).one(),
15
15
  };
16
+
17
+ (window as any).jest = {
18
+ fn: ((fnc: any) => fnc) as any,
19
+ };
@@ -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
- {offerPrice && (
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 { RichieContextFactory, CourseLightFactory } from 'utils/test/factories/richie';
3
- import { getCourseGlimpseProps, CourseGlimpse } from 'components/CourseGlimpse';
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: RichieContextFactory().one(),
14
- course: getCourseGlimpseProps(CourseLightFactory().one()),
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 { Address, Offering, CreditCard, Order, OrderState, Product } from 'types/Joanie';
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 { ProductFactory } from 'utils/test/factories/joanie';
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 Default: Story = {
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
+ };
@@ -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 {
@@ -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
@@ -42,6 +42,8 @@ export interface CourseRun {
42
42
  certificate_offer?: string;
43
43
  discounted_price: Nullable<number>;
44
44
  discount: Nullable<string>;
45
+ certificate_discounted_price: Nullable<number>;
46
+ certificate_discount: Nullable<string>;
45
47
  }
46
48
 
47
49
  export enum Priority {
@@ -207,6 +207,8 @@ export const CredentialProductFactory = factory((): CredentialProduct => {
207
207
  instructions: null,
208
208
  discounted_price: null,
209
209
  discount: null,
210
+ certificate_discounted_price: null,
211
+ certificate_discount: null,
210
212
  };
211
213
  });
212
214
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "3.1.3-dev23",
3
+ "version": "3.1.3-dev27",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {
@@ -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
  }