richie-education 2.28.2-dev26 → 2.28.2-dev53
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 +42 -17
- package/js/api/lms/dummy.ts +1 -12
- package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
- package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
- package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
- package/js/components/ContractFrame/_styles.scss +6 -14
- package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.spec.tsx +15 -45
- package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.tsx +17 -24
- package/js/components/DownloadContractButton/index.spec.tsx +1 -1
- package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
- package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
- package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
- package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
- package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
- package/js/components/PaymentInterfaces/types.ts +5 -2
- package/js/components/PaymentScheduleGrid/_styles.scss +13 -0
- package/js/components/PaymentScheduleGrid/index.tsx +50 -70
- package/js/components/PurchaseButton/index.spec.tsx +84 -37
- package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
- package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
- package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +80 -27
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +16 -20
- package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
- package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
- package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
- package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.scss +4 -5
- package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +39 -11
- package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
- package/js/components/SaleTunnel/_styles.scss +16 -5
- package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
- package/js/components/SaleTunnel/index.credential.spec.tsx +14 -25
- package/js/components/SaleTunnel/index.full-process.spec.tsx +116 -48
- package/js/components/SaleTunnel/index.spec.tsx +334 -717
- package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
- package/js/components/SignContractButton/index.spec.tsx +16 -20
- package/js/components/SignContractButton/index.tsx +3 -1
- package/js/hooks/useCreditCards/index.spec.tsx +70 -6
- package/js/hooks/useCreditCards/index.ts +49 -11
- package/js/hooks/useOrders/index.spec.tsx +322 -0
- package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
- package/js/hooks/usePaymentSchedule.tsx +23 -0
- package/js/hooks/useProductOrder/index.spec.tsx +77 -60
- package/js/hooks/useProductOrder/index.tsx +2 -2
- package/js/hooks/useResources/useResourcesRoot.ts +4 -3
- package/js/index.tsx +2 -0
- package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
- package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
- package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
- package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
- package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
- package/js/settings/settings.test.ts +11 -2
- package/js/types/Joanie.ts +77 -31
- package/js/utils/OrderHelper/index.ts +47 -38
- package/js/utils/test/factories/joanie.ts +66 -68
- package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +114 -5
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +99 -12
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +7 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +126 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +209 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +40 -25
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +28 -22
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
- package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
- package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
- package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
- package/package.json +2 -1
- package/scss/components/_index.scss +4 -2
- package/js/components/PaymentButton/_styles.scss +0 -27
- package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -338
- package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
- /package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/_styles.scss +0 -0
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
CourseRun,
|
|
10
10
|
CredentialOrder,
|
|
11
11
|
Enrollment,
|
|
12
|
-
Product,
|
|
13
12
|
} from 'types/Joanie';
|
|
14
13
|
import { Spinner } from 'components/Spinner';
|
|
15
14
|
import Banner, { BannerType } from 'components/Banner';
|
|
@@ -68,10 +67,11 @@ const messages = defineMessages({
|
|
|
68
67
|
description: 'Text displayed when course runs list is loading',
|
|
69
68
|
id: 'components.DashboardItemCourseEnrollingRun.courseRunsLoading',
|
|
70
69
|
},
|
|
71
|
-
|
|
72
|
-
id: 'components.DashboardItemCourseEnrollingRun.
|
|
73
|
-
description:
|
|
74
|
-
|
|
70
|
+
cannotEnroll: {
|
|
71
|
+
id: 'components.DashboardItemCourseEnrollingRun.cannotEnroll',
|
|
72
|
+
description:
|
|
73
|
+
'Message displayed as disabled button title when the order state does not allow enrollment.',
|
|
74
|
+
defaultMessage: 'You cannot enroll yet to this training.',
|
|
75
75
|
},
|
|
76
76
|
});
|
|
77
77
|
|
|
@@ -81,7 +81,6 @@ interface DashboardItemCourseEnrollingProps {
|
|
|
81
81
|
course: AbstractCourse;
|
|
82
82
|
activeEnrollment?: Enrollment;
|
|
83
83
|
order?: CredentialOrder;
|
|
84
|
-
product?: Product;
|
|
85
84
|
writable: boolean;
|
|
86
85
|
hideEnrollButtons?: boolean;
|
|
87
86
|
icon?: boolean;
|
|
@@ -93,7 +92,6 @@ export const DashboardItemCourseEnrolling = ({
|
|
|
93
92
|
activeEnrollment,
|
|
94
93
|
writable,
|
|
95
94
|
order,
|
|
96
|
-
product,
|
|
97
95
|
icon = false,
|
|
98
96
|
notEnrolledUrl = '#',
|
|
99
97
|
hideEnrollButtons,
|
|
@@ -121,7 +119,6 @@ export const DashboardItemCourseEnrolling = ({
|
|
|
121
119
|
course={course}
|
|
122
120
|
enrollments={CoursesHelper.findCourseEnrollmentsInOrder(course, order)}
|
|
123
121
|
order={order}
|
|
124
|
-
product={product}
|
|
125
122
|
/>
|
|
126
123
|
)}
|
|
127
124
|
</div>
|
|
@@ -132,14 +129,12 @@ interface DashboardItemCourseEnrollingRunsProps {
|
|
|
132
129
|
course: AbstractCourse;
|
|
133
130
|
enrollments: Enrollment[];
|
|
134
131
|
order?: CredentialOrder;
|
|
135
|
-
product?: Product;
|
|
136
132
|
}
|
|
137
133
|
|
|
138
134
|
const DashboardItemCourseEnrollingRuns = ({
|
|
139
135
|
course,
|
|
140
136
|
enrollments,
|
|
141
137
|
order,
|
|
142
|
-
product,
|
|
143
138
|
}: DashboardItemCourseEnrollingRunsProps) => {
|
|
144
139
|
const { enroll, isLoading, error } = useEnroll(enrollments, order);
|
|
145
140
|
|
|
@@ -176,7 +171,6 @@ const DashboardItemCourseEnrollingRuns = ({
|
|
|
176
171
|
selected={data.selected}
|
|
177
172
|
enroll={() => enroll(data.courseRun)}
|
|
178
173
|
order={order}
|
|
179
|
-
product={product}
|
|
180
174
|
/>
|
|
181
175
|
))}
|
|
182
176
|
{isLoading && (
|
|
@@ -200,7 +194,6 @@ interface DashboardItemCourseEnrollingRunProps {
|
|
|
200
194
|
selected: boolean;
|
|
201
195
|
enroll: () => void;
|
|
202
196
|
order?: CredentialOrder | CertificateOrder;
|
|
203
|
-
product?: Product;
|
|
204
197
|
}
|
|
205
198
|
|
|
206
199
|
export const DashboardItemCourseEnrollingRun = ({
|
|
@@ -208,14 +201,11 @@ export const DashboardItemCourseEnrollingRun = ({
|
|
|
208
201
|
selected,
|
|
209
202
|
enroll,
|
|
210
203
|
order,
|
|
211
|
-
product,
|
|
212
204
|
}: DashboardItemCourseEnrollingRunProps) => {
|
|
213
205
|
const intl = useIntl();
|
|
214
206
|
const formatDate = useDateFormat();
|
|
215
207
|
const courseRunPeriodMessage = useCourseRunPeriodMessage(courseRun, selected);
|
|
216
|
-
const
|
|
217
|
-
? OrderHelper.orderNeedsSignature(order, product?.contract_definition)
|
|
218
|
-
: false;
|
|
208
|
+
const canEnroll = OrderHelper.allowEnrollment(order);
|
|
219
209
|
const isOpenedForEnrollment = useMemo(
|
|
220
210
|
() => courseRun.state.priority < Priority.FUTURE_NOT_YET_OPEN,
|
|
221
211
|
[courseRun],
|
|
@@ -266,11 +256,11 @@ export const DashboardItemCourseEnrollingRun = ({
|
|
|
266
256
|
) : (
|
|
267
257
|
<div>
|
|
268
258
|
<Button
|
|
269
|
-
disabled={!isOpenedForEnrollment ||
|
|
259
|
+
disabled={!isOpenedForEnrollment || !canEnroll}
|
|
270
260
|
color="tertiary"
|
|
271
261
|
size="small"
|
|
272
262
|
onClick={enroll}
|
|
273
|
-
title={
|
|
263
|
+
title={!canEnroll ? intl.formatMessage(messages.cannotEnroll) : ''}
|
|
274
264
|
>
|
|
275
265
|
<FormattedMessage {...messages.enrollRun} />
|
|
276
266
|
</Button>
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { screen, waitForElementToBeRemoved } from '@testing-library/react';
|
|
2
2
|
import fetchMock from 'fetch-mock';
|
|
3
3
|
import userEvent from '@testing-library/user-event';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
CertificateProduct,
|
|
6
|
+
CourseLight,
|
|
7
|
+
OrderState,
|
|
8
|
+
ProductType,
|
|
9
|
+
PURCHASABLE_ORDER_STATES,
|
|
10
|
+
} from 'types/Joanie';
|
|
5
11
|
import {
|
|
6
12
|
CourseStateFactory,
|
|
7
13
|
RichieContextFactory as mockRichieContextFactory,
|
|
@@ -43,7 +49,7 @@ jest.mock('components/SaleTunnel', () => ({
|
|
|
43
49
|
return;
|
|
44
50
|
}
|
|
45
51
|
setTimeout(() => {
|
|
46
|
-
const order = Factories.
|
|
52
|
+
const order = Factories.CertificateOrderFactory().one();
|
|
47
53
|
onFinish?.(order);
|
|
48
54
|
}, 100);
|
|
49
55
|
}, [isOpen]);
|
|
@@ -135,7 +141,7 @@ describe('<ProductCertificateFooter/>', () => {
|
|
|
135
141
|
it('should display download button for a course run with certificate.', () => {
|
|
136
142
|
const order = OrderEnrollmentFactory({
|
|
137
143
|
certificate_id: 'FAKE_CERTIFICATE_ID',
|
|
138
|
-
state: OrderState.
|
|
144
|
+
state: OrderState.COMPLETED,
|
|
139
145
|
product_id: product.id,
|
|
140
146
|
}).one();
|
|
141
147
|
const enrollment = EnrollmentFactory({
|
|
@@ -151,35 +157,23 @@ describe('<ProductCertificateFooter/>', () => {
|
|
|
151
157
|
expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
|
|
152
158
|
});
|
|
153
159
|
|
|
154
|
-
it
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
certificate_id: undefined,
|
|
172
|
-
product_id: product.id,
|
|
173
|
-
state: OrderState.PENDING,
|
|
174
|
-
}).one();
|
|
175
|
-
const enrollment = EnrollmentFactory({
|
|
176
|
-
orders: [order],
|
|
177
|
-
course_run: CourseRunFactory({ course }).one(),
|
|
178
|
-
}).one();
|
|
179
|
-
render(<ProductCertificateFooter product={product} enrollment={enrollment} />);
|
|
180
|
-
expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
|
|
181
|
-
expect(screen.getByTestId('PurchaseButton__cta')).toBeInTheDocument();
|
|
182
|
-
});
|
|
160
|
+
it.each(PURCHASABLE_ORDER_STATES)(
|
|
161
|
+
'should display purchase button for a course run with %s order.',
|
|
162
|
+
(state) => {
|
|
163
|
+
const order = OrderEnrollmentFactory({
|
|
164
|
+
certificate_id: undefined,
|
|
165
|
+
product_id: product.id,
|
|
166
|
+
state,
|
|
167
|
+
}).one();
|
|
168
|
+
const enrollment = EnrollmentFactory({
|
|
169
|
+
orders: [order],
|
|
170
|
+
course_run: CourseRunFactory({ course }).one(),
|
|
171
|
+
}).one();
|
|
172
|
+
render(<ProductCertificateFooter product={product} enrollment={enrollment} />);
|
|
173
|
+
expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
|
|
174
|
+
expect(screen.getByTestId('PurchaseButton__cta')).toBeInTheDocument();
|
|
175
|
+
},
|
|
176
|
+
);
|
|
183
177
|
|
|
184
178
|
it('should not display button (download or purchase) for a course run with order but without certificate.', () => {
|
|
185
179
|
const order = OrderEnrollmentFactory({
|
package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx
CHANGED
|
@@ -2,7 +2,12 @@ import { FormattedMessage, defineMessages } from 'react-intl';
|
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import PurchaseButton from 'components/PurchaseButton';
|
|
4
4
|
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
CertificateProduct,
|
|
7
|
+
Enrollment,
|
|
8
|
+
ProductType,
|
|
9
|
+
PURCHASABLE_ORDER_STATES,
|
|
10
|
+
} from 'types/Joanie';
|
|
6
11
|
import DownloadCertificateButton from 'components/DownloadCertificateButton';
|
|
7
12
|
import { useCertificate } from 'hooks/useCertificates';
|
|
8
13
|
import { isOpenedCourseRunCertificate } from 'utils/CourseRuns';
|
|
@@ -51,7 +56,7 @@ const ProductCertificateFooter = ({ product, enrollment }: ProductCertificateFoo
|
|
|
51
56
|
<div className="dashboard-item__course-enrolling__infos">
|
|
52
57
|
<div className="dashboard-item__block__status">
|
|
53
58
|
<Icon name={IconTypeEnum.CERTIFICATE} />
|
|
54
|
-
{order
|
|
59
|
+
{OrderHelper.isActive(order) ? (
|
|
55
60
|
<>
|
|
56
61
|
{product.certificate_definition.title + '. '}
|
|
57
62
|
<CertificateStatus certificate={certificate} productType={product.type} />
|
|
@@ -60,11 +65,11 @@ const ProductCertificateFooter = ({ product, enrollment }: ProductCertificateFoo
|
|
|
60
65
|
<FormattedMessage {...messages.buyProductCertificateLabel} />
|
|
61
66
|
)}
|
|
62
67
|
</div>
|
|
63
|
-
{order
|
|
64
|
-
order
|
|
68
|
+
{OrderHelper.isActive(order) ? (
|
|
69
|
+
order!.certificate_id && (
|
|
65
70
|
<DownloadCertificateButton
|
|
66
71
|
className="dashboard-item__button"
|
|
67
|
-
certificateId={order
|
|
72
|
+
certificateId={order!.certificate_id}
|
|
68
73
|
/>
|
|
69
74
|
)
|
|
70
75
|
) : (
|
|
@@ -73,7 +78,7 @@ const ProductCertificateFooter = ({ product, enrollment }: ProductCertificateFoo
|
|
|
73
78
|
product={product}
|
|
74
79
|
enrollment={enrollment}
|
|
75
80
|
buttonProps={{ size: 'small' }}
|
|
76
|
-
disabled={order
|
|
81
|
+
disabled={order && !PURCHASABLE_ORDER_STATES.includes(order.state)}
|
|
77
82
|
onFinish={(o) => {
|
|
78
83
|
/**
|
|
79
84
|
* As we do not refetch enrollments in DashboardCourses after SaleTunnel cache invalidation (to avoid
|
|
@@ -21,11 +21,19 @@ import {
|
|
|
21
21
|
CertificateFactory,
|
|
22
22
|
CourseLightFactory,
|
|
23
23
|
CourseRunFactory,
|
|
24
|
-
EnrollmentFactory,
|
|
25
24
|
CredentialOrderFactory,
|
|
25
|
+
EnrollmentFactory,
|
|
26
|
+
PaymentFactory,
|
|
26
27
|
TargetCourseFactory,
|
|
27
28
|
} from 'utils/test/factories/joanie';
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
Certificate,
|
|
31
|
+
CourseLight,
|
|
32
|
+
CourseRun,
|
|
33
|
+
CredentialOrder,
|
|
34
|
+
OrderState,
|
|
35
|
+
PaymentScheduleState,
|
|
36
|
+
} from 'types/Joanie';
|
|
29
37
|
import { resolveAll } from 'utils/resolveAll';
|
|
30
38
|
import { confirm } from 'utils/indirection/window';
|
|
31
39
|
import { Priority } from 'types';
|
|
@@ -40,6 +48,7 @@ import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
|
40
48
|
import { render } from 'utils/test/render';
|
|
41
49
|
import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
|
|
42
50
|
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
|
|
51
|
+
import { OrderHelper } from 'utils/OrderHelper';
|
|
43
52
|
import { DashboardTest } from '../../DashboardTest';
|
|
44
53
|
import { DashboardItemOrder } from './DashboardItemOrder';
|
|
45
54
|
|
|
@@ -53,8 +62,15 @@ jest.mock('utils/context', () => ({
|
|
|
53
62
|
|
|
54
63
|
jest.mock('utils/indirection/window', () => ({
|
|
55
64
|
confirm: jest.fn(() => true),
|
|
65
|
+
matchMedia: () => ({
|
|
66
|
+
matches: true,
|
|
67
|
+
addListener: jest.fn(),
|
|
68
|
+
removeListener: jest.fn(),
|
|
69
|
+
}),
|
|
56
70
|
}));
|
|
57
71
|
|
|
72
|
+
jest.mock('../../../../../components/PaymentInterfaces');
|
|
73
|
+
|
|
58
74
|
describe('<DashboardItemOrder/>', () => {
|
|
59
75
|
setupJoanieSession();
|
|
60
76
|
|
|
@@ -75,7 +91,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
75
91
|
|
|
76
92
|
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
77
93
|
await screen.findByText('Ref. ' + (order.course as CourseLight).code);
|
|
78
|
-
await screen.findByText('Pending');
|
|
94
|
+
await screen.findByText('Pending for the first direct debit');
|
|
79
95
|
await screen.findByRole('link', { name: 'View details' });
|
|
80
96
|
});
|
|
81
97
|
|
|
@@ -103,7 +119,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
103
119
|
|
|
104
120
|
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
105
121
|
await screen.findByText('Ref. ' + (order.course as CourseLight).code);
|
|
106
|
-
await screen.findByText('
|
|
122
|
+
await screen.findByText('Successfully completed');
|
|
107
123
|
await screen.findByRole('link', { name: 'View details' });
|
|
108
124
|
await expectSpinner('Loading certificate...');
|
|
109
125
|
deferred.resolve(certificate);
|
|
@@ -122,7 +138,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
122
138
|
|
|
123
139
|
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
124
140
|
await screen.findByText('Ref. ' + (order.course as CourseLight).code);
|
|
125
|
-
await screen.findByText('
|
|
141
|
+
await screen.findByText('Successfully completed');
|
|
126
142
|
await screen.findByRole('link', { name: 'View details' });
|
|
127
143
|
await expectNoSpinner('Loading certificate ...');
|
|
128
144
|
});
|
|
@@ -852,4 +868,97 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
852
868
|
within(block).getByText(new RegExp(order.organization?.address?.postcode!));
|
|
853
869
|
within(block).getByText(new RegExp(order.organization?.address?.country!));
|
|
854
870
|
});
|
|
871
|
+
|
|
872
|
+
it('renders a writable order with failed payment and retry it successfully', async () => {
|
|
873
|
+
const order = CredentialOrderFactory().one();
|
|
874
|
+
const paymentInfo = PaymentFactory().one();
|
|
875
|
+
|
|
876
|
+
const validOrder = { ...order };
|
|
877
|
+
validOrder.payment_schedule = [
|
|
878
|
+
{ ...order.payment_schedule![0] },
|
|
879
|
+
{ ...order.payment_schedule![1] },
|
|
880
|
+
{ ...order.payment_schedule![2] },
|
|
881
|
+
];
|
|
882
|
+
fetchMock
|
|
883
|
+
.post(
|
|
884
|
+
`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit_installment_payment/`,
|
|
885
|
+
paymentInfo,
|
|
886
|
+
)
|
|
887
|
+
.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, validOrder);
|
|
888
|
+
|
|
889
|
+
order.state = OrderState.FAILED_PAYMENT;
|
|
890
|
+
order.payment_schedule![1].state = PaymentScheduleState.REFUSED;
|
|
891
|
+
|
|
892
|
+
const formatPrice = (price: number, currency: string) =>
|
|
893
|
+
new Intl.NumberFormat('en', {
|
|
894
|
+
currency,
|
|
895
|
+
style: 'currency',
|
|
896
|
+
}).format(price);
|
|
897
|
+
|
|
898
|
+
const { product } = mockCourseProductWithOrder(order);
|
|
899
|
+
fetchMock.get(
|
|
900
|
+
'https://joanie.endpoint/api/v1.0/orders/',
|
|
901
|
+
{ results: [order], next: null, previous: null, count: null },
|
|
902
|
+
{ overwriteRoutes: true },
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
render(
|
|
906
|
+
<DashboardTest initialRoute={LearnerDashboardPaths.ORDER.replace(':orderId', order.id)} />,
|
|
907
|
+
{ wrapper: BaseJoanieAppWrapper },
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
911
|
+
screen.getByText(/a payment failed, please update your payment method/i);
|
|
912
|
+
const failedInstallment = OrderHelper.getFailedInstallment(order)!;
|
|
913
|
+
const button = screen.getByRole('button', {
|
|
914
|
+
name: 'Pay ' + formatPrice(failedInstallment.amount, failedInstallment.currency),
|
|
915
|
+
});
|
|
916
|
+
const user = userEvent.setup();
|
|
917
|
+
|
|
918
|
+
await user.click(button);
|
|
919
|
+
|
|
920
|
+
// Retry modal is shown.
|
|
921
|
+
screen.getByText('Retry payment');
|
|
922
|
+
screen.getByText(
|
|
923
|
+
/The payment failed, please choose another payment method or add a new one during the payment/,
|
|
924
|
+
);
|
|
925
|
+
screen.getByText('Use another credit card');
|
|
926
|
+
|
|
927
|
+
// Prepare for cache invalidation.
|
|
928
|
+
fetchMock.get(
|
|
929
|
+
'https://joanie.endpoint/api/v1.0/orders/',
|
|
930
|
+
{ results: [validOrder], next: null, previous: null, count: null },
|
|
931
|
+
{ overwriteRoutes: true },
|
|
932
|
+
);
|
|
933
|
+
|
|
934
|
+
// Click on pay button.
|
|
935
|
+
const payButton = screen.getByTestId('order-payment-retry-modal-submit-button');
|
|
936
|
+
expect(payButton.innerHTML.replace(' ', ' ')).toEqual(
|
|
937
|
+
'Pay ' +
|
|
938
|
+
formatPrice(failedInstallment.amount, failedInstallment.currency).replace(
|
|
939
|
+
/(\u202F|\u00a0)/g,
|
|
940
|
+
' ',
|
|
941
|
+
),
|
|
942
|
+
);
|
|
943
|
+
await user.click(payButton);
|
|
944
|
+
// Pay via mocked payment interface
|
|
945
|
+
screen.getByText('Payment interface component');
|
|
946
|
+
await user.click(screen.getByTestId('payment-success'));
|
|
947
|
+
|
|
948
|
+
// Make sure retry modal is closed.
|
|
949
|
+
expect(screen.queryByText('Retry payment')).not.toBeInTheDocument();
|
|
950
|
+
|
|
951
|
+
// Success modal is shown, close it.
|
|
952
|
+
screen.getByText('Payment successful');
|
|
953
|
+
screen.getByText('The payment was successful');
|
|
954
|
+
const okButton = screen.getByRole('button', { name: 'Ok' });
|
|
955
|
+
await user.click(okButton);
|
|
956
|
+
|
|
957
|
+
// Warning alert is not shown anymore.
|
|
958
|
+
await waitFor(() => {
|
|
959
|
+
expect(
|
|
960
|
+
screen.queryByText(/a payment failed, please update your payment method/i),
|
|
961
|
+
).not.toBeInTheDocument();
|
|
962
|
+
});
|
|
963
|
+
});
|
|
855
964
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FormattedMessage, useIntl
|
|
2
|
-
import { Button } from '@openfun/cunningham-react';
|
|
1
|
+
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
2
|
+
import { Alert, Button, useModal, VariantType } from '@openfun/cunningham-react';
|
|
3
3
|
import classNames from 'classnames';
|
|
4
4
|
import { generatePath } from 'react-router-dom';
|
|
5
5
|
import { CourseLight, CredentialOrder, Product } from 'types/Joanie';
|
|
@@ -16,6 +16,8 @@ import ContractStatus from 'components/ContractStatus';
|
|
|
16
16
|
import SignContractButton from 'components/SignContractButton';
|
|
17
17
|
import { AddressView } from 'components/Address';
|
|
18
18
|
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
|
|
19
|
+
import { OrderPaymentDetailsModal } from 'widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal';
|
|
20
|
+
import { OrderPaymentRetryModal } from 'widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal';
|
|
19
21
|
import { DashboardSubItemsList } from '../DashboardSubItemsList';
|
|
20
22
|
import { DashboardItemCourseEnrolling } from '../CourseEnrolling';
|
|
21
23
|
import { DashboardItem } from '../index';
|
|
@@ -78,6 +80,31 @@ const messages = defineMessages({
|
|
|
78
80
|
description: 'Label for the organization DPO contact',
|
|
79
81
|
defaultMessage: 'Data protection email',
|
|
80
82
|
},
|
|
83
|
+
paymentTitle: {
|
|
84
|
+
id: 'components.DashboardItemOrder.paymentTitle',
|
|
85
|
+
description: 'Label for the payment block',
|
|
86
|
+
defaultMessage: 'Payment',
|
|
87
|
+
},
|
|
88
|
+
paymentLabel: {
|
|
89
|
+
id: 'components.DashboardItemOrder.paymentLabel',
|
|
90
|
+
description: 'Label for the payment block',
|
|
91
|
+
defaultMessage: 'You can see and manage all installments.',
|
|
92
|
+
},
|
|
93
|
+
paymentButton: {
|
|
94
|
+
id: 'components.DashboardItemOrder.paymentButton',
|
|
95
|
+
description: 'Button label for the payment block',
|
|
96
|
+
defaultMessage: 'Manage payment',
|
|
97
|
+
},
|
|
98
|
+
paymentNeededMessage: {
|
|
99
|
+
id: 'components.DashboardItemOrder.paymentNeededMessage',
|
|
100
|
+
description: 'Message displayed when payment is needed',
|
|
101
|
+
defaultMessage: 'A payment failed, please update your payment method',
|
|
102
|
+
},
|
|
103
|
+
paymentNeededButton: {
|
|
104
|
+
id: 'components.DashboardItemOrder.paymentNeededButton',
|
|
105
|
+
description: 'Button label for the payment needed message',
|
|
106
|
+
defaultMessage: 'Pay {amount}',
|
|
107
|
+
},
|
|
81
108
|
});
|
|
82
109
|
|
|
83
110
|
interface DashboardItemOrderProps {
|
|
@@ -137,13 +164,16 @@ export const DashboardItemOrder = ({
|
|
|
137
164
|
course_id: course.code,
|
|
138
165
|
});
|
|
139
166
|
const { product } = courseProductRelation || {};
|
|
140
|
-
const needsSignature = OrderHelper.orderNeedsSignature(order
|
|
167
|
+
const needsSignature = OrderHelper.orderNeedsSignature(order);
|
|
168
|
+
const canEnroll = OrderHelper.allowEnrollment(order);
|
|
169
|
+
|
|
170
|
+
if (!product) return null;
|
|
141
171
|
|
|
142
172
|
return (
|
|
143
173
|
<div className="dashboard-item-order">
|
|
144
174
|
<DashboardItem
|
|
145
175
|
data-testid={`dashboard-item-order-${order.id}`}
|
|
146
|
-
title={product
|
|
176
|
+
title={product.title}
|
|
147
177
|
code={'Ref. ' + course.code}
|
|
148
178
|
imageUrl={course.cover?.src}
|
|
149
179
|
more={
|
|
@@ -158,10 +188,7 @@ export const DashboardItemOrder = ({
|
|
|
158
188
|
<div className="dashboard-item-order__footer">
|
|
159
189
|
<div className="dashboard-item__block__status">
|
|
160
190
|
<Icon name={IconTypeEnum.SCHOOL} />
|
|
161
|
-
<OrderStateLearnerMessage
|
|
162
|
-
order={order}
|
|
163
|
-
contractDefinition={product?.contract_definition}
|
|
164
|
-
/>
|
|
191
|
+
<OrderStateLearnerMessage order={order} />
|
|
165
192
|
</div>
|
|
166
193
|
{showDetailsButton && (
|
|
167
194
|
<RouterButton
|
|
@@ -179,7 +206,7 @@ export const DashboardItemOrder = ({
|
|
|
179
206
|
key={`DashboardItemOrderContract_${order.id}`}
|
|
180
207
|
title={product.title}
|
|
181
208
|
order={order}
|
|
182
|
-
contract_definition={product
|
|
209
|
+
contract_definition={product.contract_definition!}
|
|
183
210
|
contract={order.contract}
|
|
184
211
|
writable={writable}
|
|
185
212
|
mode="compact"
|
|
@@ -197,7 +224,6 @@ export const DashboardItemOrder = ({
|
|
|
197
224
|
writable={writable}
|
|
198
225
|
course={targetCourse}
|
|
199
226
|
order={order}
|
|
200
|
-
product={product}
|
|
201
227
|
activeEnrollment={CoursesHelper.findActiveCourseEnrollmentInOrder(
|
|
202
228
|
targetCourse,
|
|
203
229
|
order,
|
|
@@ -205,7 +231,7 @@ export const DashboardItemOrder = ({
|
|
|
205
231
|
notEnrolledUrl={generatePath(LearnerDashboardPaths.ORDER, {
|
|
206
232
|
orderId: order.id,
|
|
207
233
|
})}
|
|
208
|
-
hideEnrollButtons={
|
|
234
|
+
hideEnrollButtons={!canEnroll}
|
|
209
235
|
/>
|
|
210
236
|
}
|
|
211
237
|
/>
|
|
@@ -299,17 +325,78 @@ const OrganizationBlock = ({ order, product }: { order: CredentialOrder; product
|
|
|
299
325
|
</div>
|
|
300
326
|
</div>
|
|
301
327
|
)}
|
|
328
|
+
<Installment order={order} />
|
|
302
329
|
</div>
|
|
303
330
|
</div>
|
|
304
331
|
);
|
|
305
332
|
};
|
|
306
333
|
|
|
334
|
+
const Installment = ({ order }: { order: CredentialOrder }) => {
|
|
335
|
+
const modal = useModal();
|
|
336
|
+
const retryModal = useModal();
|
|
337
|
+
const failedInstallment = OrderHelper.getFailedInstallment(order);
|
|
338
|
+
const intl = useIntl();
|
|
339
|
+
|
|
340
|
+
const pay = async () => {
|
|
341
|
+
retryModal.open();
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
<>
|
|
346
|
+
<div className="dashboard-splitted-card__item">
|
|
347
|
+
<div
|
|
348
|
+
className={classNames('dashboard-splitted-card__item__title', {
|
|
349
|
+
'dashboard-splitted-card__item__title--dot': !!failedInstallment,
|
|
350
|
+
})}
|
|
351
|
+
>
|
|
352
|
+
<span>
|
|
353
|
+
<FormattedMessage {...messages.paymentTitle} />
|
|
354
|
+
</span>
|
|
355
|
+
</div>
|
|
356
|
+
{failedInstallment && (
|
|
357
|
+
<Alert
|
|
358
|
+
className="mb-t"
|
|
359
|
+
type={VariantType.ERROR}
|
|
360
|
+
buttons={
|
|
361
|
+
<Button size="small" onClick={pay}>
|
|
362
|
+
<FormattedMessage
|
|
363
|
+
{...messages.paymentNeededButton}
|
|
364
|
+
values={{
|
|
365
|
+
amount: intl.formatNumber(failedInstallment.amount, {
|
|
366
|
+
style: 'currency',
|
|
367
|
+
currency: failedInstallment.currency,
|
|
368
|
+
}),
|
|
369
|
+
}}
|
|
370
|
+
/>
|
|
371
|
+
</Button>
|
|
372
|
+
}
|
|
373
|
+
>
|
|
374
|
+
<FormattedMessage {...messages.paymentNeededMessage} />
|
|
375
|
+
</Alert>
|
|
376
|
+
)}
|
|
377
|
+
<div className="dashboard-splitted-card__item__description">
|
|
378
|
+
<FormattedMessage {...messages.paymentLabel} />
|
|
379
|
+
</div>
|
|
380
|
+
<div className="dashboard-splitted-card__item__actions">
|
|
381
|
+
<Button size="small" color="secondary" onClick={modal.open}>
|
|
382
|
+
<FormattedMessage {...messages.paymentButton} />
|
|
383
|
+
</Button>
|
|
384
|
+
</div>
|
|
385
|
+
</div>
|
|
386
|
+
<OrderPaymentDetailsModal {...modal} order={order} />
|
|
387
|
+
{failedInstallment && (
|
|
388
|
+
<OrderPaymentRetryModal {...retryModal} installment={failedInstallment} order={order} />
|
|
389
|
+
)}
|
|
390
|
+
</>
|
|
391
|
+
);
|
|
392
|
+
};
|
|
393
|
+
|
|
307
394
|
const ContractItem = ({ product, order }: { order: CredentialOrder; product: Product }) => {
|
|
308
395
|
if (!product?.contract_definition) {
|
|
309
396
|
return;
|
|
310
397
|
}
|
|
311
398
|
|
|
312
|
-
const needsSignature = OrderHelper.orderNeedsSignature(order
|
|
399
|
+
const needsSignature = OrderHelper.orderNeedsSignature(order);
|
|
313
400
|
return (
|
|
314
401
|
<div
|
|
315
402
|
id={`dashboard-item-contract-${order.id}`}
|
package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { faker } from '@faker-js/faker';
|
|
|
3
3
|
import { screen } from '@testing-library/react';
|
|
4
4
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
5
5
|
import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest';
|
|
6
|
-
import { CourseLight } from 'types/Joanie';
|
|
6
|
+
import { CourseLight, OrderState } from 'types/Joanie';
|
|
7
7
|
import {
|
|
8
8
|
ContractDefinitionFactory,
|
|
9
9
|
ContractFactory,
|
|
@@ -124,6 +124,7 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
124
124
|
|
|
125
125
|
it('renders a non-writable order with a contract not signed yet', async () => {
|
|
126
126
|
const order = CredentialOrderFactory({
|
|
127
|
+
state: OrderState.TO_SIGN,
|
|
127
128
|
target_courses: TargetCourseFactory().many(1),
|
|
128
129
|
target_enrollments: [],
|
|
129
130
|
contract: ContractFactory({ student_signed_on: undefined }).one(),
|
|
@@ -186,6 +187,7 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
186
187
|
});
|
|
187
188
|
it('renders a writable order with a contract not signed yet', async () => {
|
|
188
189
|
const order = CredentialOrderFactory({
|
|
190
|
+
state: OrderState.TO_SIGN,
|
|
189
191
|
target_courses: TargetCourseFactory().many(1),
|
|
190
192
|
target_enrollments: [],
|
|
191
193
|
contract: null,
|
|
@@ -19,6 +19,7 @@ import { render } from 'utils/test/render';
|
|
|
19
19
|
import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
|
|
20
20
|
|
|
21
21
|
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
|
|
22
|
+
import { OrderState } from 'types/Joanie';
|
|
22
23
|
|
|
23
24
|
jest.mock('utils/context', () => ({
|
|
24
25
|
__esModule: true,
|
|
@@ -49,9 +50,10 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
49
50
|
describe('writable', () => {
|
|
50
51
|
it('successfully sign a contract', async () => {
|
|
51
52
|
const order = CredentialOrderFactory({
|
|
53
|
+
state: OrderState.TO_SIGN,
|
|
52
54
|
target_courses: TargetCourseFactory().many(1),
|
|
53
55
|
target_enrollments: [],
|
|
54
|
-
contract: ContractFactory({ student_signed_on:
|
|
56
|
+
contract: ContractFactory({ student_signed_on: null }).one(),
|
|
55
57
|
}).one();
|
|
56
58
|
|
|
57
59
|
// learner dashboard course page do one call to course product relation per order
|
|
@@ -82,6 +84,7 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
82
84
|
`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit_for_signature/`,
|
|
83
85
|
submitDeferred.promise,
|
|
84
86
|
);
|
|
87
|
+
fetchMock.post(`https://joanie.endpoint/api/v1.0/signature/notifications/`, 200);
|
|
85
88
|
|
|
86
89
|
// delay: null is needed because as we are using fake timers it would mock the timers of
|
|
87
90
|
// RTL too. See https://github.com/testing-library/user-event/issues/833.
|
|
@@ -92,11 +95,7 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
92
95
|
});
|
|
93
96
|
|
|
94
97
|
await expectNoSpinner('Loading orders and enrollments...');
|
|
95
|
-
|
|
96
|
-
expect(
|
|
97
|
-
await screen.findByRole('heading', { level: 5, name: product.title }),
|
|
98
|
-
).toBeInTheDocument();
|
|
99
|
-
|
|
98
|
+
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
100
99
|
// Make sure the sign button is shown.
|
|
101
100
|
await user.click(screen.getByRole('link', { name: 'Sign' }));
|
|
102
101
|
|
|
@@ -212,7 +211,7 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
212
211
|
// We have the success message.
|
|
213
212
|
await screen.findByRole('heading', { name: 'Congratulations!' });
|
|
214
213
|
screen.getByText(
|
|
215
|
-
'You will receive an email once your contract will be fully signed. You can now
|
|
214
|
+
'You will receive an email once your contract will be fully signed. You can now finalize your subscription.',
|
|
216
215
|
);
|
|
217
216
|
const nextButton = screen.getByRole('button', { name: 'Next' });
|
|
218
217
|
|