Skip to content

Commit f389a71

Browse files
jonathanreveillekernicPanel
authored andcommitted
🐛(frontend) discounted price in sales tunnel for certificate product
When we open the sales tunnel to purchase a certificate product in a course that we are enrolled, when a discount is present for the product offer, we would not see the discounted price. With the latest developments on OfferingRules, we can now access that price. When it comes to credentials, it works as usual, and when there is no discount, the initial product price is shown. Fix #2645
1 parent 0be7807 commit f389a71

File tree

6 files changed

+95
-10
lines changed

6 files changed

+95
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ Versioning](https://semver.org/spec/v2.0.0.html).
2929
### Fixed
3030

3131
- Prevent indexation failure when any thumbnail generation fails
32+
- Display discounted price for the purchase of a certificate product
33+
from student dashboard in sale tunnel informations
3234

3335
## [3.1.2] - 2025-05-22
3436

src/frontend/.storybook/__mocks__/utils/context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,7 @@ let context = {
1313
(window as any).__richie_frontend_context__ = {
1414
context: RichieContextFactory(context).one(),
1515
};
16+
17+
(window as any).jest = {
18+
fn: ((fnc: any) => fnc) as any,
19+
};

src/frontend/js/components/SaleTunnel/GenericSaleTunnel.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ import {
1010
} from 'react';
1111
import { SaleTunnelSponsors } from 'components/SaleTunnel/Sponsors/SaleTunnelSponsors';
1212
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';
1422
import useProductOrder from 'hooks/useProductOrder';
1523
import { SaleTunnelSuccess } from 'components/SaleTunnel/SaleTunnelSuccess';
1624
import WebAnalyticsAPIHandler from 'api/web-analytics';
@@ -27,6 +35,7 @@ export interface SaleTunnelContextType {
2735
product: Product;
2836
webAnalyticsEventKey: string;
2937
offering?: Offering;
38+
enrollment?: Enrollment;
3039

3140
// internal
3241
step: SaleTunnelStep;
@@ -115,6 +124,7 @@ export const GenericSaleTunnel = (props: GenericSaleTunnelProps) => {
115124
order,
116125
product: props.product,
117126
offering: props.offering,
127+
enrollment: props.enrollment,
118128
props,
119129
billingAddress,
120130
setBillingAddress,

src/frontend/js/components/SaleTunnel/SaleTunnelInformation/index.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,19 +101,19 @@ const Email = () => {
101101
};
102102

103103
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;
105109
return (
106110
<div className="sale-tunnel__total">
107111
<div className="sale-tunnel__total__amount mt-t" data-testid="sale-tunnel__total__amount">
108112
<div className="block-title">
109113
<FormattedMessage {...messages.totalLabel} />
110114
</div>
111115
<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} />
117117
</div>
118118
</div>
119119
</div>

src/frontend/js/components/SaleTunnel/index.spec.tsx

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { useState } from 'react';
88
import { OrderState, Product, ProductType, NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
99
import {
1010
RichieContextFactory as mockRichieContextFactory,
11+
CourseStateFactory,
1112
UserFactory,
1213
PacedCourseFactory,
1314
} from 'utils/test/factories/richie';
@@ -16,12 +17,14 @@ import {
1617
CertificateOrderFactory,
1718
CertificateProductFactory,
1819
OfferingFactory,
20+
CourseRunFactory,
1921
CredentialOrderFactory,
2022
CredentialProductFactory,
2123
CreditCardFactory,
2224
EnrollmentFactory,
2325
PaymentInstallmentFactory,
2426
} from 'utils/test/factories/joanie';
27+
import { Priority } from 'types';
2528
import { render } from 'utils/test/render';
2629
import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
2730
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
@@ -97,7 +100,7 @@ describe.each([
97100
return (
98101
<SaleTunnel
99102
{...props}
100-
enrollment={enrollment}
103+
enrollment={props.enrollment ?? enrollment}
101104
course={productType === ProductType.CREDENTIAL ? course : undefined}
102105
isOpen={open}
103106
onClose={() => setOpen(false)}
@@ -449,6 +452,57 @@ describe.each([
449452
);
450453
});
451454

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+
452506
it('should show the product payment schedule with discounted price', async () => {
453507
const intl = createIntl({ locale: 'en' });
454508
const schedule = PaymentInstallmentFactory().many(2);

src/frontend/js/components/SaleTunnel/index.stories.tsx

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { StoryObj, Meta } from '@storybook/react';
22
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';
49
import { PacedCourseFactory } from 'utils/test/factories/richie';
510
import { SaleTunnel, SaleTunnelProps } from './index';
611

@@ -28,6 +33,16 @@ export default {
2833

2934
type Story = StoryObj<typeof SaleTunnel>;
3035

31-
export const Default: Story = {
36+
export const Credential: Story = {
3237
args: {},
3338
};
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+
};

0 commit comments

Comments
 (0)