richie-education 2.28.2-dev39 → 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 +12 -16
- 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/CreditCardSelector/index.spec.tsx +7 -7
- package/js/components/CreditCardSelector/index.tsx +2 -2
- 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/PurchaseButton/index.spec.tsx +69 -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 +75 -41
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
- 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.tsx +5 -0
- package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
- package/js/components/SaleTunnel/_styles.scss +10 -1
- package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
- package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
- package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
- package/js/components/SaleTunnel/index.spec.tsx +330 -779
- 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/useProductOrder/index.spec.tsx +77 -60
- package/js/hooks/useProductOrder/index.tsx +2 -2
- package/js/hooks/useResources/useResourcesRoot.ts +1 -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 +49 -34
- package/js/utils/OrderHelper/index.ts +38 -42
- package/js/utils/test/factories/joanie.ts +36 -51
- 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 +7 -6
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
- 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/index.tsx +28 -8
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +2 -5
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
- 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 +1 -1
- package/scss/components/_index.scss +2 -1
- package/js/components/PaymentButton/_styles.scss +0 -27
- package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
- package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
- package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
|
@@ -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
|
|
@@ -22,8 +22,8 @@ import {
|
|
|
22
22
|
CourseLightFactory,
|
|
23
23
|
CourseRunFactory,
|
|
24
24
|
CredentialOrderFactory,
|
|
25
|
-
CredentialOrderWithPaymentFactory,
|
|
26
25
|
EnrollmentFactory,
|
|
26
|
+
PaymentFactory,
|
|
27
27
|
TargetCourseFactory,
|
|
28
28
|
} from 'utils/test/factories/joanie';
|
|
29
29
|
import {
|
|
@@ -91,7 +91,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
91
91
|
|
|
92
92
|
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
93
93
|
await screen.findByText('Ref. ' + (order.course as CourseLight).code);
|
|
94
|
-
await screen.findByText('Pending');
|
|
94
|
+
await screen.findByText('Pending for the first direct debit');
|
|
95
95
|
await screen.findByRole('link', { name: 'View details' });
|
|
96
96
|
});
|
|
97
97
|
|
|
@@ -119,7 +119,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
119
119
|
|
|
120
120
|
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
121
121
|
await screen.findByText('Ref. ' + (order.course as CourseLight).code);
|
|
122
|
-
await screen.findByText('
|
|
122
|
+
await screen.findByText('Successfully completed');
|
|
123
123
|
await screen.findByRole('link', { name: 'View details' });
|
|
124
124
|
await expectSpinner('Loading certificate...');
|
|
125
125
|
deferred.resolve(certificate);
|
|
@@ -138,7 +138,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
138
138
|
|
|
139
139
|
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
140
140
|
await screen.findByText('Ref. ' + (order.course as CourseLight).code);
|
|
141
|
-
await screen.findByText('
|
|
141
|
+
await screen.findByText('Successfully completed');
|
|
142
142
|
await screen.findByRole('link', { name: 'View details' });
|
|
143
143
|
await expectNoSpinner('Loading certificate ...');
|
|
144
144
|
});
|
|
@@ -870,7 +870,8 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
870
870
|
});
|
|
871
871
|
|
|
872
872
|
it('renders a writable order with failed payment and retry it successfully', async () => {
|
|
873
|
-
const
|
|
873
|
+
const order = CredentialOrderFactory().one();
|
|
874
|
+
const paymentInfo = PaymentFactory().one();
|
|
874
875
|
|
|
875
876
|
const validOrder = { ...order };
|
|
876
877
|
validOrder.payment_schedule = [
|
|
@@ -921,7 +922,7 @@ describe('<DashboardItemOrder/>', () => {
|
|
|
921
922
|
screen.getByText(
|
|
922
923
|
/The payment failed, please choose another payment method or add a new one during the payment/,
|
|
923
924
|
);
|
|
924
|
-
screen.getByText('Use another credit card
|
|
925
|
+
screen.getByText('Use another credit card');
|
|
925
926
|
|
|
926
927
|
// Prepare for cache invalidation.
|
|
927
928
|
fetchMock.get(
|
|
@@ -164,13 +164,16 @@ export const DashboardItemOrder = ({
|
|
|
164
164
|
course_id: course.code,
|
|
165
165
|
});
|
|
166
166
|
const { product } = courseProductRelation || {};
|
|
167
|
-
const needsSignature = OrderHelper.orderNeedsSignature(order
|
|
167
|
+
const needsSignature = OrderHelper.orderNeedsSignature(order);
|
|
168
|
+
const canEnroll = OrderHelper.allowEnrollment(order);
|
|
169
|
+
|
|
170
|
+
if (!product) return null;
|
|
168
171
|
|
|
169
172
|
return (
|
|
170
173
|
<div className="dashboard-item-order">
|
|
171
174
|
<DashboardItem
|
|
172
175
|
data-testid={`dashboard-item-order-${order.id}`}
|
|
173
|
-
title={product
|
|
176
|
+
title={product.title}
|
|
174
177
|
code={'Ref. ' + course.code}
|
|
175
178
|
imageUrl={course.cover?.src}
|
|
176
179
|
more={
|
|
@@ -185,10 +188,7 @@ export const DashboardItemOrder = ({
|
|
|
185
188
|
<div className="dashboard-item-order__footer">
|
|
186
189
|
<div className="dashboard-item__block__status">
|
|
187
190
|
<Icon name={IconTypeEnum.SCHOOL} />
|
|
188
|
-
<OrderStateLearnerMessage
|
|
189
|
-
order={order}
|
|
190
|
-
contractDefinition={product?.contract_definition}
|
|
191
|
-
/>
|
|
191
|
+
<OrderStateLearnerMessage order={order} />
|
|
192
192
|
</div>
|
|
193
193
|
{showDetailsButton && (
|
|
194
194
|
<RouterButton
|
|
@@ -206,7 +206,7 @@ export const DashboardItemOrder = ({
|
|
|
206
206
|
key={`DashboardItemOrderContract_${order.id}`}
|
|
207
207
|
title={product.title}
|
|
208
208
|
order={order}
|
|
209
|
-
contract_definition={product
|
|
209
|
+
contract_definition={product.contract_definition!}
|
|
210
210
|
contract={order.contract}
|
|
211
211
|
writable={writable}
|
|
212
212
|
mode="compact"
|
|
@@ -224,7 +224,6 @@ export const DashboardItemOrder = ({
|
|
|
224
224
|
writable={writable}
|
|
225
225
|
course={targetCourse}
|
|
226
226
|
order={order}
|
|
227
|
-
product={product}
|
|
228
227
|
activeEnrollment={CoursesHelper.findActiveCourseEnrollmentInOrder(
|
|
229
228
|
targetCourse,
|
|
230
229
|
order,
|
|
@@ -232,7 +231,7 @@ export const DashboardItemOrder = ({
|
|
|
232
231
|
notEnrolledUrl={generatePath(LearnerDashboardPaths.ORDER, {
|
|
233
232
|
orderId: order.id,
|
|
234
233
|
})}
|
|
235
|
-
hideEnrollButtons={
|
|
234
|
+
hideEnrollButtons={!canEnroll}
|
|
236
235
|
/>
|
|
237
236
|
}
|
|
238
237
|
/>
|
|
@@ -397,7 +396,7 @@ const ContractItem = ({ product, order }: { order: CredentialOrder; product: Pro
|
|
|
397
396
|
return;
|
|
398
397
|
}
|
|
399
398
|
|
|
400
|
-
const needsSignature = OrderHelper.orderNeedsSignature(order
|
|
399
|
+
const needsSignature = OrderHelper.orderNeedsSignature(order);
|
|
401
400
|
return (
|
|
402
401
|
<div
|
|
403
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
|
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Alert,
|
|
3
3
|
Button,
|
|
4
|
+
Loader,
|
|
4
5
|
Modal,
|
|
5
6
|
ModalProps,
|
|
6
7
|
ModalSize,
|
|
@@ -8,12 +9,14 @@ import {
|
|
|
8
9
|
VariantType,
|
|
9
10
|
} from '@openfun/cunningham-react';
|
|
10
11
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
11
|
-
import { useState } from 'react';
|
|
12
|
+
import { useState, useEffect } from 'react';
|
|
12
13
|
import { PaymentScheduleGrid } from 'components/PaymentScheduleGrid';
|
|
13
14
|
import { CreditCard, Order } from 'types/Joanie';
|
|
14
15
|
import { CreditCardSelector } from 'components/CreditCardSelector';
|
|
15
16
|
import { OrderHelper } from 'utils/OrderHelper';
|
|
16
17
|
import { OrderPaymentRetryModal } from 'widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal';
|
|
18
|
+
import { Maybe } from 'types/utils';
|
|
19
|
+
import { useCreditCard } from 'hooks/useCreditCards';
|
|
17
20
|
|
|
18
21
|
const messages = defineMessages({
|
|
19
22
|
title: {
|
|
@@ -57,7 +60,7 @@ export const OrderPaymentDetailsModal = ({ order, ...props }: PaymentModalProps)
|
|
|
57
60
|
<h3 className="order-payment-details__title mb-s">
|
|
58
61
|
<FormattedMessage {...messages.paymentMethodTitle} />
|
|
59
62
|
</h3>
|
|
60
|
-
<CreditCardSelectorWrapper />
|
|
63
|
+
<CreditCardSelectorWrapper selectedCreditCardId={order.credit_card_id} />
|
|
61
64
|
<h3 className="order-payment-details__title mb-s mt-b">
|
|
62
65
|
<FormattedMessage {...messages.scheduleTitle} />
|
|
63
66
|
</h3>
|
|
@@ -91,14 +94,31 @@ export const OrderPaymentDetailsModal = ({ order, ...props }: PaymentModalProps)
|
|
|
91
94
|
);
|
|
92
95
|
};
|
|
93
96
|
|
|
94
|
-
const CreditCardSelectorWrapper = (
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const CreditCardSelectorWrapper = ({
|
|
98
|
+
selectedCreditCardId,
|
|
99
|
+
}: {
|
|
100
|
+
selectedCreditCardId: Maybe<string>;
|
|
101
|
+
}) => {
|
|
102
|
+
const {
|
|
103
|
+
item: creditCard,
|
|
104
|
+
states: { fetching },
|
|
105
|
+
} = useCreditCard(selectedCreditCardId);
|
|
106
|
+
const [selectedCreditCard, setSelectedCreditCard] = useState<Maybe<CreditCard>>(creditCard);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!selectedCreditCard && creditCard) {
|
|
110
|
+
setSelectedCreditCard(creditCard);
|
|
111
|
+
}
|
|
112
|
+
}, [creditCard]);
|
|
113
|
+
|
|
114
|
+
if (fetching) {
|
|
115
|
+
return <Loader size="small" />;
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
return (
|
|
99
119
|
<CreditCardSelector
|
|
100
|
-
creditCard={creditCard}
|
|
101
|
-
setCreditCard={
|
|
120
|
+
creditCard={selectedCreditCard || creditCard}
|
|
121
|
+
setCreditCard={setSelectedCreditCard}
|
|
102
122
|
quickRemove={false}
|
|
103
123
|
allowEdit={false}
|
|
104
124
|
/>
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@openfun/cunningham-react';
|
|
10
10
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
11
11
|
import { useRef, useState } from 'react';
|
|
12
|
-
import { CreditCard, Order, PaymentInstallment,
|
|
12
|
+
import { CreditCard, Order, PaymentInstallment, ACTIVE_ORDER_STATES } from 'types/Joanie';
|
|
13
13
|
import { CreditCardSelector } from 'components/CreditCardSelector';
|
|
14
14
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
15
15
|
import { Payment, PaymentErrorMessageId } from 'components/PaymentInterfaces/types';
|
|
@@ -111,10 +111,7 @@ export const OrderPaymentRetryModal = ({ installment, order, ...props }: Props)
|
|
|
111
111
|
|
|
112
112
|
const isOrderValidated = async (id: string): Promise<Boolean> => {
|
|
113
113
|
const orderToCheck = await API.user.orders.get({ id });
|
|
114
|
-
return (
|
|
115
|
-
orderToCheck?.state === OrderState.VALIDATED ||
|
|
116
|
-
orderToCheck?.state === OrderState.PENDING_PAYMENT
|
|
117
|
-
);
|
|
114
|
+
return orderToCheck !== null && ACTIVE_ORDER_STATES.includes(orderToCheck.state);
|
|
118
115
|
};
|
|
119
116
|
|
|
120
117
|
const settled = async () => {
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import React, { PropsWithChildren } from 'react';
|
|
2
2
|
import { screen } from '@testing-library/react';
|
|
3
3
|
import { createIntl } from 'react-intl';
|
|
4
|
-
import {
|
|
5
|
-
ContractDefinitionFactory,
|
|
6
|
-
ContractFactory,
|
|
7
|
-
CredentialOrderFactory,
|
|
8
|
-
} from 'utils/test/factories/joanie';
|
|
4
|
+
import { CredentialOrderFactory } from 'utils/test/factories/joanie';
|
|
9
5
|
import { OrderState } from 'types/Joanie';
|
|
10
6
|
import { render } from 'utils/test/render';
|
|
11
7
|
import { IntlWrapper } from 'utils/test/wrappers/IntlWrapper';
|
|
@@ -13,86 +9,37 @@ import OrderStateLearnerMessage, { messages } from '.';
|
|
|
13
9
|
|
|
14
10
|
const intl = createIntl({ locale: 'en' });
|
|
15
11
|
|
|
16
|
-
describe('<
|
|
12
|
+
describe('<OrderStateLearnerMessage/>', () => {
|
|
17
13
|
it.each([
|
|
18
|
-
[OrderState.
|
|
19
|
-
[OrderState.SUBMITTED, 'Submitted'],
|
|
20
|
-
[OrderState.PENDING, 'Pending'],
|
|
14
|
+
[OrderState.ASSIGNED, 'Pending'],
|
|
21
15
|
[OrderState.CANCELED, 'Canceled'],
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
it.each([
|
|
34
|
-
[OrderState.DRAFT, 'Draft'],
|
|
35
|
-
[OrderState.SUBMITTED, 'Submitted'],
|
|
36
|
-
[OrderState.PENDING, 'Pending'],
|
|
37
|
-
[OrderState.CANCELED, 'Canceled'],
|
|
38
|
-
])(
|
|
39
|
-
'should display message from order state: %s when order have no contract',
|
|
40
|
-
(state, expectedMessage) => {
|
|
41
|
-
const orderWithContract = CredentialOrderFactory({
|
|
42
|
-
state,
|
|
43
|
-
contract: ContractFactory().one(),
|
|
44
|
-
}).one();
|
|
45
|
-
render(<OrderStateLearnerMessage order={orderWithContract} />, {
|
|
46
|
-
wrapper: ({ children }: PropsWithChildren) => <IntlWrapper>{children}</IntlWrapper>,
|
|
47
|
-
});
|
|
48
|
-
expect(screen.getByText(expectedMessage)).toBeInTheDocument();
|
|
49
|
-
},
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
it('should display message for validated order that need learner signature', () => {
|
|
53
|
-
const order = CredentialOrderFactory({
|
|
54
|
-
state: OrderState.VALIDATED,
|
|
55
|
-
contract: null,
|
|
56
|
-
}).one();
|
|
57
|
-
|
|
58
|
-
const contractDefinition = ContractDefinitionFactory().one();
|
|
59
|
-
|
|
60
|
-
render(<OrderStateLearnerMessage order={order} contractDefinition={contractDefinition} />, {
|
|
61
|
-
wrapper: ({ children }: PropsWithChildren) => <IntlWrapper>{children}</IntlWrapper>,
|
|
62
|
-
});
|
|
63
|
-
expect(screen.getByText('Signature required')).toBeInTheDocument();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("should display message for validated order that don't have a generated certificate", () => {
|
|
67
|
-
const order = CredentialOrderFactory({
|
|
68
|
-
state: OrderState.VALIDATED,
|
|
69
|
-
contract: ContractFactory({ student_signed_on: new Date().toISOString() }).one(),
|
|
70
|
-
certificate_id: undefined,
|
|
71
|
-
}).one();
|
|
16
|
+
[OrderState.COMPLETED, 'On going'],
|
|
17
|
+
[OrderState.DRAFT, 'Pending'],
|
|
18
|
+
[OrderState.FAILED_PAYMENT, 'Last direct debit has failed'],
|
|
19
|
+
[OrderState.NO_PAYMENT, 'First direct debit has failed'],
|
|
20
|
+
[OrderState.PENDING, 'Pending for the first direct debit'],
|
|
21
|
+
[OrderState.PENDING_PAYMENT, 'On going'],
|
|
22
|
+
[OrderState.SIGNING, 'Signature required'],
|
|
23
|
+
[OrderState.TO_SAVE_PAYMENT_METHOD, 'Payment method is missing'],
|
|
24
|
+
[OrderState.TO_SIGN, 'Signature required'],
|
|
25
|
+
])('should display message from order state: %s', (state, expectedMessage) => {
|
|
26
|
+
const order = CredentialOrderFactory({ state }).one();
|
|
72
27
|
render(<OrderStateLearnerMessage order={order} />, {
|
|
73
28
|
wrapper: ({ children }: PropsWithChildren) => <IntlWrapper>{children}</IntlWrapper>,
|
|
74
29
|
});
|
|
75
|
-
expect(
|
|
76
|
-
screen.getByText(intl.formatMessage(messages.statusOnGoing), {
|
|
77
|
-
exact: false,
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
30
|
+
expect(screen.getByText(expectedMessage)).toBeInTheDocument();
|
|
80
31
|
});
|
|
81
32
|
|
|
82
|
-
it('should display message for
|
|
33
|
+
it('should display message for completed order that have a generated certificate', () => {
|
|
83
34
|
const order = CredentialOrderFactory({
|
|
84
|
-
state: OrderState.
|
|
85
|
-
contract: ContractFactory({
|
|
86
|
-
student_signed_on: new Date().toISOString(),
|
|
87
|
-
organization_signed_on: new Date().toISOString(),
|
|
88
|
-
}).one(),
|
|
35
|
+
state: OrderState.COMPLETED,
|
|
89
36
|
certificate_id: 'FAKE_CERTIFICATE_ID',
|
|
90
37
|
}).one();
|
|
91
38
|
render(<OrderStateLearnerMessage order={order} />, {
|
|
92
39
|
wrapper: ({ children }: PropsWithChildren) => <IntlWrapper>{children}</IntlWrapper>,
|
|
93
40
|
});
|
|
94
41
|
expect(
|
|
95
|
-
screen.getByText(intl.formatMessage(messages.
|
|
42
|
+
screen.getByText(intl.formatMessage(messages.statusPassed), {
|
|
96
43
|
exact: false,
|
|
97
44
|
}),
|
|
98
45
|
);
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx
CHANGED
|
@@ -1,71 +1,70 @@
|
|
|
1
1
|
import { defineMessages } from 'react-intl';
|
|
2
|
-
import OrderStateMessage, { OrderStateMessageBaseProps } from '../OrderStateMessage';
|
|
2
|
+
import OrderStateMessage, { OrderStateMessageBaseProps, MessageKeys } from '../OrderStateMessage';
|
|
3
3
|
|
|
4
|
-
export const messages = defineMessages({
|
|
4
|
+
export const messages = defineMessages<MessageKeys>({
|
|
5
5
|
statusDraft: {
|
|
6
|
-
id: 'components.DashboardItem.Order.
|
|
6
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusDraft',
|
|
7
7
|
description: 'Status shown on the dashboard order item when order is draft.',
|
|
8
|
-
defaultMessage: '
|
|
8
|
+
defaultMessage: 'Pending',
|
|
9
9
|
},
|
|
10
|
-
|
|
11
|
-
id: 'components.DashboardItem.Order.
|
|
12
|
-
description: 'Status shown on the dashboard order item when order is
|
|
13
|
-
defaultMessage: '
|
|
10
|
+
statusAssigned: {
|
|
11
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusAssigned',
|
|
12
|
+
description: 'Status shown on the dashboard order item when order is assigned.',
|
|
13
|
+
defaultMessage: 'Pending',
|
|
14
14
|
},
|
|
15
15
|
statusPending: {
|
|
16
|
-
id: 'components.DashboardItem.Order.
|
|
16
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusPending',
|
|
17
17
|
description: 'Status shown on the dashboard order item when order is pending.',
|
|
18
|
-
defaultMessage: 'Pending',
|
|
18
|
+
defaultMessage: 'Pending for the first direct debit',
|
|
19
19
|
},
|
|
20
|
-
|
|
21
|
-
id: 'components.DashboardItem.Order.
|
|
22
|
-
description:
|
|
23
|
-
'Status shown on the dashboard order item when order is validated with no certificate',
|
|
20
|
+
statusPendingPayment: {
|
|
21
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusPendingPayment',
|
|
22
|
+
description: 'Status shown on the dashboard order item when order is pending for payment',
|
|
24
23
|
defaultMessage: 'On going',
|
|
25
24
|
},
|
|
26
25
|
statusCompleted: {
|
|
27
|
-
id: 'components.DashboardItem.Order.
|
|
28
|
-
description:
|
|
29
|
-
|
|
30
|
-
defaultMessage: 'Completed',
|
|
31
|
-
},
|
|
32
|
-
statusPendingPayment: {
|
|
33
|
-
id: 'components.DashboardItem.Order.OrderStateMessage.statusPendingPayment',
|
|
34
|
-
description:
|
|
35
|
-
'Status shown on the dashboard order item when order is validated with certificate and pending payment',
|
|
36
|
-
defaultMessage: 'Completed',
|
|
26
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusCompleted',
|
|
27
|
+
description: 'Status shown on the dashboard order item when order is completed',
|
|
28
|
+
defaultMessage: 'On going',
|
|
37
29
|
},
|
|
38
30
|
statusWaitingSignature: {
|
|
39
|
-
id: 'components.DashboardItem.Order.
|
|
31
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusWaitingSignature',
|
|
40
32
|
description:
|
|
41
33
|
"Status shown on the dashboard order item when order is validated with contract's learner signature missing.",
|
|
42
34
|
defaultMessage: 'Signature required',
|
|
43
35
|
},
|
|
44
36
|
statusWaitingCounterSignature: {
|
|
45
|
-
id: 'components.DashboardItem.Order.
|
|
37
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusWaitingCounterSignature',
|
|
46
38
|
description:
|
|
47
39
|
"Status shown on the dashboard order item when order is validated with contract's organization signature missing.",
|
|
48
40
|
defaultMessage: 'On going',
|
|
49
41
|
},
|
|
42
|
+
statusWaitingPaymentMethod: {
|
|
43
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusWaitingPaymentMethod',
|
|
44
|
+
description:
|
|
45
|
+
'Status shown on the dashboard order item when order is in to_save_payment_method state.',
|
|
46
|
+
defaultMessage: 'Payment method is missing',
|
|
47
|
+
},
|
|
50
48
|
statusCanceled: {
|
|
51
|
-
id: 'components.DashboardItem.Order.
|
|
49
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusCanceled',
|
|
52
50
|
description: 'Status shown on the dashboard order item when order is canceled',
|
|
53
51
|
defaultMessage: 'Canceled',
|
|
54
52
|
},
|
|
55
53
|
statusNoPayment: {
|
|
56
|
-
id: 'components.DashboardItem.Order.
|
|
54
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusNoPayment',
|
|
57
55
|
description: 'Status shown on the dashboard order item when order is in no payment state',
|
|
58
|
-
defaultMessage: '
|
|
56
|
+
defaultMessage: 'First direct debit has failed',
|
|
59
57
|
},
|
|
60
58
|
statusFailedPayment: {
|
|
61
|
-
id: 'components.DashboardItem.Order.
|
|
59
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusFailedPayment',
|
|
62
60
|
description: 'Status shown on the dashboard order item when order is in failed payment state',
|
|
63
|
-
defaultMessage: '
|
|
61
|
+
defaultMessage: 'Last direct debit has failed',
|
|
64
62
|
},
|
|
65
|
-
|
|
66
|
-
id: 'components.DashboardItem.Order.
|
|
67
|
-
description:
|
|
68
|
-
|
|
63
|
+
statusPassed: {
|
|
64
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusPassed',
|
|
65
|
+
description:
|
|
66
|
+
'Status shown on the dashboard order item when order is completed and has a certificate',
|
|
67
|
+
defaultMessage: 'Successfully completed',
|
|
69
68
|
},
|
|
70
69
|
});
|
|
71
70
|
|