richie-education 2.29.1-dev32 → 2.29.1-dev37
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/components/ContractStatus/index.spec.tsx +1 -1
- package/js/components/ContractStatus/index.tsx +1 -1
- package/js/components/PurchaseButton/index.tsx +9 -27
- package/js/components/SaleTunnel/index.tsx +2 -1
- package/js/pages/DashboardOrderLayout/index.spec.tsx +10 -1
- package/js/utils/ProductHelper/index.spec.ts +322 -166
- package/js/utils/ProductHelper/index.ts +32 -0
- package/js/widgets/Dashboard/components/DashboardItem/Contract/index.spec.tsx +2 -2
- package/js/widgets/Dashboard/components/DashboardItem/Order/CertificateItem/index.tsx +51 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/ContractItem/index.tsx +52 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemNotResumable.spec.tsx +109 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +105 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +103 -324
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +59 -8
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +12 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemSavePaymentMethod.spec.tsx +116 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +174 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +2 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +150 -0
- package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +29 -3
- package/package.json +2 -2
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
2
|
-
import { Alert, Button, useModal, VariantType } from '@openfun/cunningham-react';
|
|
3
|
-
import classNames from 'classnames';
|
|
4
2
|
import { generatePath } from 'react-router-dom';
|
|
5
|
-
import {
|
|
3
|
+
import { CredentialOrder, OrderState } from 'types/Joanie';
|
|
6
4
|
import { Icon, IconTypeEnum } from 'components/Icon';
|
|
7
5
|
import { CoursesHelper } from 'utils/CoursesHelper';
|
|
8
|
-
import { useCertificate } from 'hooks/useCertificates';
|
|
9
|
-
import { Spinner } from 'components/Spinner';
|
|
10
6
|
import { DashboardSubItem } from 'widgets/Dashboard/components/DashboardItem/DashboardSubItem';
|
|
11
|
-
import { DashboardItemCertificate } from 'widgets/Dashboard/components/DashboardItem/Certificate';
|
|
12
7
|
import { RouterButton } from 'widgets/Dashboard/components/RouterButton';
|
|
13
8
|
import { useCourseProduct } from 'hooks/useCourseProducts';
|
|
14
9
|
import { OrderHelper } from 'utils/OrderHelper';
|
|
15
|
-
import ContractStatus from 'components/ContractStatus';
|
|
16
|
-
import SignContractButton from 'components/SignContractButton';
|
|
17
|
-
import { AddressView } from 'components/Address';
|
|
18
10
|
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
|
|
19
|
-
import
|
|
20
|
-
import
|
|
11
|
+
import OrganizationBlock from 'widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock';
|
|
12
|
+
import CertificateItem from 'widgets/Dashboard/components/DashboardItem/Order/CertificateItem';
|
|
13
|
+
import { ProductHelper } from 'utils/ProductHelper';
|
|
21
14
|
import { DashboardSubItemsList } from '../DashboardSubItemsList';
|
|
22
15
|
import { DashboardItemCourseEnrolling } from '../CourseEnrolling';
|
|
23
16
|
import { DashboardItem } from '../index';
|
|
@@ -30,80 +23,32 @@ const messages = defineMessages({
|
|
|
30
23
|
description: 'Button that redirects to the order details',
|
|
31
24
|
defaultMessage: 'View details',
|
|
32
25
|
},
|
|
33
|
-
loadingCertificate: {
|
|
34
|
-
id: 'components.DashboardItemOrder.loadingCertificate',
|
|
35
|
-
description: 'Accessible label displayed while certificate is being fetched on the dashboard.',
|
|
36
|
-
defaultMessage: 'Loading certificate...',
|
|
37
|
-
},
|
|
38
26
|
syllabusLinkLabel: {
|
|
39
27
|
id: 'components.DashboardItemOrder.syllabusLinkLabel',
|
|
40
28
|
description: 'Syllabus link label on order details',
|
|
41
29
|
defaultMessage: 'Go to syllabus',
|
|
42
30
|
},
|
|
43
|
-
|
|
44
|
-
id: 'components.DashboardItemOrder.
|
|
45
|
-
description: '
|
|
46
|
-
defaultMessage: '
|
|
47
|
-
},
|
|
48
|
-
contactButton: {
|
|
49
|
-
id: 'components.DashboardItemOrder.contactButton',
|
|
50
|
-
description: 'Button to contact the organization',
|
|
51
|
-
defaultMessage: 'Contact',
|
|
52
|
-
},
|
|
53
|
-
organizationHeader: {
|
|
54
|
-
id: 'components.DashboardItemOrder.organizationHeader',
|
|
55
|
-
description: 'Header of the organization section',
|
|
56
|
-
defaultMessage: 'This training is provided by',
|
|
57
|
-
},
|
|
58
|
-
organizationLogoAlt: {
|
|
59
|
-
id: 'components.DashboardItemOrder.organizationLogoAlt',
|
|
60
|
-
description: 'Alt text for the organization logo',
|
|
61
|
-
defaultMessage: 'Logo of the organization',
|
|
62
|
-
},
|
|
63
|
-
trainingContractTitle: {
|
|
64
|
-
id: 'components.DashboardItemOrder.trainingContractTitle',
|
|
65
|
-
description: 'Title of the training contract section',
|
|
66
|
-
defaultMessage: 'Training contract',
|
|
67
|
-
},
|
|
68
|
-
organizationMailContactLabel: {
|
|
69
|
-
id: 'components.DashboardItemOrder.organizationMailContactLabel',
|
|
70
|
-
description: 'Label for the organization mail contact',
|
|
71
|
-
defaultMessage: 'Email',
|
|
72
|
-
},
|
|
73
|
-
organizationPhoneContactLabel: {
|
|
74
|
-
id: 'components.DashboardItemOrder.organizationPhoneContactLabel',
|
|
75
|
-
description: 'Label for the organization phone contact',
|
|
76
|
-
defaultMessage: 'Phone',
|
|
31
|
+
missingPaymentMethodTitle: {
|
|
32
|
+
id: 'components.DashboardItemOrder.missingPaymentMethodTitle',
|
|
33
|
+
description: 'Main message displayed when the order is missing a payment method',
|
|
34
|
+
defaultMessage: 'A payment method is missing',
|
|
77
35
|
},
|
|
78
|
-
|
|
79
|
-
id: 'components.DashboardItemOrder.
|
|
80
|
-
description: '
|
|
81
|
-
defaultMessage: '
|
|
36
|
+
missingPaymentMethodDescription: {
|
|
37
|
+
id: 'components.DashboardItemOrder.missingPaymentMethodDescription',
|
|
38
|
+
description: 'Description message displayed when the order is missing a payment method',
|
|
39
|
+
defaultMessage: 'You must define a payment method to finalize your subscription.',
|
|
82
40
|
},
|
|
83
|
-
|
|
84
|
-
id: 'components.DashboardItemOrder.
|
|
85
|
-
description: '
|
|
86
|
-
defaultMessage: '
|
|
41
|
+
missingPaymentMethodCTA: {
|
|
42
|
+
id: 'components.DashboardItemOrder.missingPaymentMethodCTA',
|
|
43
|
+
description: 'CTA label displayed when the order is missing a payment method',
|
|
44
|
+
defaultMessage: 'Define',
|
|
87
45
|
},
|
|
88
|
-
|
|
89
|
-
id: 'components.DashboardItemOrder.
|
|
90
|
-
description:
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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}',
|
|
46
|
+
notResumable: {
|
|
47
|
+
id: 'components.DashboardItemOrder.notResumable',
|
|
48
|
+
description:
|
|
49
|
+
'Message displayed when the order subscription is not completed but the product is no more purchasable',
|
|
50
|
+
defaultMessage:
|
|
51
|
+
'The subscription process cannot be resumed. The related training is no more purchasable.',
|
|
107
52
|
},
|
|
108
53
|
});
|
|
109
54
|
|
|
@@ -114,50 +59,13 @@ interface DashboardItemOrderProps {
|
|
|
114
59
|
writable?: boolean;
|
|
115
60
|
}
|
|
116
61
|
|
|
117
|
-
interface DashboardItemOrderCertificateProps {
|
|
118
|
-
order: CredentialOrder;
|
|
119
|
-
product: Product;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const DashboardItemOrderCertificate = ({ order, product }: DashboardItemOrderCertificateProps) => {
|
|
123
|
-
if (!order.certificate_id) {
|
|
124
|
-
return (
|
|
125
|
-
<DashboardItemCertificate
|
|
126
|
-
certificateDefinition={product.certificate_definition}
|
|
127
|
-
productType={product.type}
|
|
128
|
-
mode="compact"
|
|
129
|
-
/>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
const certificate = useCertificate(order.certificate_id);
|
|
133
|
-
return (
|
|
134
|
-
<>
|
|
135
|
-
{certificate.states.fetching && (
|
|
136
|
-
<Spinner aria-labelledby="loading-certificate">
|
|
137
|
-
<span id="loading-certificate">
|
|
138
|
-
<FormattedMessage {...messages.loadingCertificate} />
|
|
139
|
-
</span>
|
|
140
|
-
</Spinner>
|
|
141
|
-
)}
|
|
142
|
-
{certificate.item && (
|
|
143
|
-
<DashboardItemCertificate
|
|
144
|
-
certificate={certificate.item}
|
|
145
|
-
productType={product.type}
|
|
146
|
-
mode="compact"
|
|
147
|
-
/>
|
|
148
|
-
)}
|
|
149
|
-
</>
|
|
150
|
-
);
|
|
151
|
-
};
|
|
152
|
-
|
|
153
62
|
export const DashboardItemOrder = ({
|
|
154
63
|
order,
|
|
155
64
|
showDetailsButton = true,
|
|
156
65
|
showCertificate,
|
|
157
66
|
writable = false,
|
|
158
67
|
}: DashboardItemOrderProps) => {
|
|
159
|
-
const course = order
|
|
160
|
-
|
|
68
|
+
const { course } = order;
|
|
161
69
|
const intl = useIntl();
|
|
162
70
|
const { item: courseProductRelation } = useCourseProduct({
|
|
163
71
|
product_id: order.product_id,
|
|
@@ -165,6 +73,10 @@ export const DashboardItemOrder = ({
|
|
|
165
73
|
});
|
|
166
74
|
const { product } = courseProductRelation || {};
|
|
167
75
|
const needsSignature = OrderHelper.orderNeedsSignature(order);
|
|
76
|
+
const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
|
|
77
|
+
const isActive = OrderHelper.isActive(order);
|
|
78
|
+
const isProductPurchasable = ProductHelper.isPurchasable(courseProductRelation?.product);
|
|
79
|
+
const isNotResumable = !isActive && !isProductPurchasable;
|
|
168
80
|
const canEnroll = OrderHelper.allowEnrollment(order);
|
|
169
81
|
|
|
170
82
|
if (!product) return null;
|
|
@@ -187,10 +99,19 @@ export const DashboardItemOrder = ({
|
|
|
187
99
|
<>
|
|
188
100
|
<div className="dashboard-item-order__footer">
|
|
189
101
|
<div className="dashboard-item__block__status">
|
|
190
|
-
|
|
191
|
-
|
|
102
|
+
{isNotResumable ? (
|
|
103
|
+
<>
|
|
104
|
+
<Icon name={IconTypeEnum.ROUND_CLOSE} size="small" />
|
|
105
|
+
<FormattedMessage {...messages.notResumable} />
|
|
106
|
+
</>
|
|
107
|
+
) : (
|
|
108
|
+
<>
|
|
109
|
+
<Icon name={IconTypeEnum.SCHOOL} />
|
|
110
|
+
<OrderStateLearnerMessage order={order} />
|
|
111
|
+
</>
|
|
112
|
+
)}
|
|
192
113
|
</div>
|
|
193
|
-
{showDetailsButton && (
|
|
114
|
+
{!isNotResumable && showDetailsButton && (
|
|
194
115
|
<RouterButton
|
|
195
116
|
size="small"
|
|
196
117
|
className="dashboard-item__button"
|
|
@@ -201,7 +122,7 @@ export const DashboardItemOrder = ({
|
|
|
201
122
|
</RouterButton>
|
|
202
123
|
)}
|
|
203
124
|
</div>
|
|
204
|
-
{!writable && needsSignature && (
|
|
125
|
+
{!writable && isProductPurchasable && needsSignature && (
|
|
205
126
|
<DashboardItemContract
|
|
206
127
|
key={`DashboardItemOrderContract_${order.id}`}
|
|
207
128
|
title={product.title}
|
|
@@ -212,217 +133,75 @@ export const DashboardItemOrder = ({
|
|
|
212
133
|
mode="compact"
|
|
213
134
|
/>
|
|
214
135
|
)}
|
|
136
|
+
{!writable && isProductPurchasable && needsPaymentMethod && (
|
|
137
|
+
<DashboardItem
|
|
138
|
+
title=""
|
|
139
|
+
data-testid={`dashboard-item-payment-method-${order.id}`}
|
|
140
|
+
mode="compact"
|
|
141
|
+
footer={
|
|
142
|
+
<>
|
|
143
|
+
<div className="dashboard-contract__body">
|
|
144
|
+
<Icon name={IconTypeEnum.CREDIT_CARD} />
|
|
145
|
+
<span>
|
|
146
|
+
<FormattedMessage {...messages.missingPaymentMethodTitle} />
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="dashboard-contract__footer">
|
|
150
|
+
<span className="dashboard-contract__footer__status">
|
|
151
|
+
<FormattedMessage {...messages.missingPaymentMethodDescription} />
|
|
152
|
+
</span>
|
|
153
|
+
<div>
|
|
154
|
+
<RouterButton
|
|
155
|
+
size="small"
|
|
156
|
+
href={generatePath(LearnerDashboardPaths.ORDER, {
|
|
157
|
+
orderId: order.id,
|
|
158
|
+
})}
|
|
159
|
+
className="dashboard-item__button"
|
|
160
|
+
>
|
|
161
|
+
<FormattedMessage {...messages.missingPaymentMethodCTA} />
|
|
162
|
+
</RouterButton>
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
</>
|
|
166
|
+
}
|
|
167
|
+
/>
|
|
168
|
+
)}
|
|
215
169
|
</>
|
|
216
170
|
}
|
|
217
171
|
>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
</DashboardItem>
|
|
241
|
-
{showCertificate && !!product?.certificate_definition && (
|
|
242
|
-
<DashboardItemOrderCertificate order={order} product={product} />
|
|
243
|
-
)}
|
|
244
|
-
{writable && <OrganizationBlock order={order} product={product} />}
|
|
245
|
-
</div>
|
|
246
|
-
);
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const OrganizationBlock = ({ order, product }: { order: CredentialOrder; product: Product }) => {
|
|
250
|
-
const { organization } = order;
|
|
251
|
-
if (!organization) {
|
|
252
|
-
return null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const showContactsBlock =
|
|
256
|
-
organization.contact_email || organization.contact_phone || organization.dpo_email;
|
|
257
|
-
|
|
258
|
-
return (
|
|
259
|
-
<div className="dashboard-splitted-card mt-s" data-testid="organization-block">
|
|
260
|
-
<div className="dashboard-splitted-card__column order-organization__caption">
|
|
261
|
-
<div className="dashboard-item-order__organization">
|
|
262
|
-
<div className="dashboard-item-order__organization__header">
|
|
263
|
-
<FormattedMessage {...messages.organizationHeader} />
|
|
264
|
-
</div>
|
|
265
|
-
<div
|
|
266
|
-
className="dashboard-item-order__organization__logo"
|
|
267
|
-
style={{
|
|
268
|
-
backgroundImage: `url(${organization.logo?.src})`,
|
|
269
|
-
}}
|
|
172
|
+
{!isNotResumable && (
|
|
173
|
+
<DashboardSubItemsList
|
|
174
|
+
subItems={order.target_courses?.map((targetCourse) => (
|
|
175
|
+
<DashboardSubItem
|
|
176
|
+
title={targetCourse.title}
|
|
177
|
+
footer={
|
|
178
|
+
<DashboardItemCourseEnrolling
|
|
179
|
+
writable={writable}
|
|
180
|
+
course={targetCourse}
|
|
181
|
+
order={order}
|
|
182
|
+
activeEnrollment={CoursesHelper.findActiveCourseEnrollmentInOrder(
|
|
183
|
+
targetCourse,
|
|
184
|
+
order,
|
|
185
|
+
)}
|
|
186
|
+
notEnrolledUrl={generatePath(LearnerDashboardPaths.ORDER, {
|
|
187
|
+
orderId: order.id,
|
|
188
|
+
})}
|
|
189
|
+
hideEnrollButtons={!canEnroll}
|
|
190
|
+
/>
|
|
191
|
+
}
|
|
192
|
+
/>
|
|
193
|
+
))}
|
|
270
194
|
/>
|
|
271
|
-
<div className="dashboard-item-order__organization__name">{organization.title}</div>
|
|
272
|
-
</div>
|
|
273
|
-
</div>
|
|
274
|
-
<div className="dashboard-splitted-card__separator order-organization__separator" />
|
|
275
|
-
<div className="dashboard-splitted-card__column order-organization__items">
|
|
276
|
-
<ContractItem order={order} product={product} />
|
|
277
|
-
{showContactsBlock && (
|
|
278
|
-
<div className="dashboard-splitted-card__item">
|
|
279
|
-
<div className="dashboard-splitted-card__item__title">Contacts</div>
|
|
280
|
-
<div className="dashboard-splitted-card__item__description">
|
|
281
|
-
{organization.contact_email && (
|
|
282
|
-
<div className="organization-block__contact__item">
|
|
283
|
-
<FormattedMessage {...messages.organizationMailContactLabel} />
|
|
284
|
-
<Button
|
|
285
|
-
size="small"
|
|
286
|
-
color="tertiary"
|
|
287
|
-
href={'mailto:' + (organization.contact_email ?? '')}
|
|
288
|
-
>
|
|
289
|
-
{organization.contact_email}
|
|
290
|
-
</Button>
|
|
291
|
-
</div>
|
|
292
|
-
)}
|
|
293
|
-
{organization.contact_phone && (
|
|
294
|
-
<div className="organization-block__contact__item">
|
|
295
|
-
<FormattedMessage {...messages.organizationPhoneContactLabel} />
|
|
296
|
-
<Button
|
|
297
|
-
size="small"
|
|
298
|
-
color="tertiary"
|
|
299
|
-
href={'tel:' + (organization.contact_phone ?? '')}
|
|
300
|
-
>
|
|
301
|
-
{organization.contact_phone}
|
|
302
|
-
</Button>
|
|
303
|
-
</div>
|
|
304
|
-
)}
|
|
305
|
-
{organization.dpo_email && (
|
|
306
|
-
<div className="organization-block__contact__item">
|
|
307
|
-
<FormattedMessage {...messages.organizationDpoContactLabel} />
|
|
308
|
-
<Button
|
|
309
|
-
size="small"
|
|
310
|
-
color="tertiary"
|
|
311
|
-
href={'mailto:' + (organization.dpo_email ?? '')}
|
|
312
|
-
>
|
|
313
|
-
{organization.dpo_email}
|
|
314
|
-
</Button>
|
|
315
|
-
</div>
|
|
316
|
-
)}
|
|
317
|
-
</div>
|
|
318
|
-
</div>
|
|
319
|
-
)}
|
|
320
|
-
{organization.address && (
|
|
321
|
-
<div className="dashboard-splitted-card__item dashboard-splitted-card__item__address">
|
|
322
|
-
<div className="dashboard-splitted-card__item__title">Address</div>
|
|
323
|
-
<div className="dashboard-splitted-card__item__description">
|
|
324
|
-
<AddressView address={organization.address} />
|
|
325
|
-
</div>
|
|
326
|
-
</div>
|
|
327
|
-
)}
|
|
328
|
-
<Installment order={order} />
|
|
329
|
-
</div>
|
|
330
|
-
</div>
|
|
331
|
-
);
|
|
332
|
-
};
|
|
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
195
|
)}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
</div>
|
|
386
|
-
<OrderPaymentDetailsModal {...modal} order={order} />
|
|
387
|
-
{failedInstallment && (
|
|
388
|
-
<OrderPaymentRetryModal {...retryModal} installment={failedInstallment} order={order} />
|
|
196
|
+
</DashboardItem>
|
|
197
|
+
{!isNotResumable && (
|
|
198
|
+
<>
|
|
199
|
+
{showCertificate && !!product?.certificate_definition && (
|
|
200
|
+
<CertificateItem order={order} product={product} />
|
|
201
|
+
)}
|
|
202
|
+
{writable && <OrganizationBlock order={order} product={product} />}
|
|
203
|
+
</>
|
|
389
204
|
)}
|
|
390
|
-
</>
|
|
391
|
-
);
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
const ContractItem = ({ product, order }: { order: CredentialOrder; product: Product }) => {
|
|
395
|
-
if (!product?.contract_definition) {
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
const needsSignature = OrderHelper.orderNeedsSignature(order);
|
|
400
|
-
return (
|
|
401
|
-
<div
|
|
402
|
-
id={`dashboard-item-contract-${order.id}`}
|
|
403
|
-
className="dashboard-splitted-card__item"
|
|
404
|
-
data-testid={`dashboard-item-contract-${order.id}`}
|
|
405
|
-
>
|
|
406
|
-
<div
|
|
407
|
-
className={classNames('dashboard-splitted-card__item__title', {
|
|
408
|
-
'dashboard-splitted-card__item__title--dot': needsSignature,
|
|
409
|
-
})}
|
|
410
|
-
>
|
|
411
|
-
<span>
|
|
412
|
-
<FormattedMessage {...messages.trainingContractTitle} />
|
|
413
|
-
</span>
|
|
414
|
-
</div>
|
|
415
|
-
<div className="dashboard-splitted-card__item__description">
|
|
416
|
-
<ContractStatus contract={order.contract} />
|
|
417
|
-
</div>
|
|
418
|
-
<div className="dashboard-splitted-card__item__actions">
|
|
419
|
-
<SignContractButton
|
|
420
|
-
order={order}
|
|
421
|
-
contract={order.contract}
|
|
422
|
-
writable={true}
|
|
423
|
-
className="dashboard-item__button"
|
|
424
|
-
/>
|
|
425
|
-
</div>
|
|
426
205
|
</div>
|
|
427
206
|
);
|
|
428
207
|
};
|
package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import fetchMock from 'fetch-mock';
|
|
2
2
|
import { faker } from '@faker-js/faker';
|
|
3
|
-
import { screen } from '@testing-library/react';
|
|
3
|
+
import { screen, within } from '@testing-library/react';
|
|
4
|
+
import queryString from 'query-string';
|
|
4
5
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
5
6
|
import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest';
|
|
6
|
-
import { CourseLight, OrderState } from 'types/Joanie';
|
|
7
|
+
import { CourseLight, NOT_CANCELED_ORDER_STATES, OrderState } from 'types/Joanie';
|
|
7
8
|
import {
|
|
8
9
|
ContractDefinitionFactory,
|
|
9
10
|
ContractFactory,
|
|
@@ -77,7 +78,9 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
77
78
|
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
78
79
|
|
|
79
80
|
expect(
|
|
80
|
-
screen.queryByText(
|
|
81
|
+
screen.queryByText(
|
|
82
|
+
'You have to sign this training contract to finalize your subscription.',
|
|
83
|
+
),
|
|
81
84
|
).not.toBeInTheDocument();
|
|
82
85
|
expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
|
|
83
86
|
expect(screen.getByText('On going')).toBeInTheDocument();
|
|
@@ -111,7 +114,9 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
111
114
|
await screen.findByRole('heading', { level: 5, name: product.title });
|
|
112
115
|
|
|
113
116
|
expect(
|
|
114
|
-
screen.queryByText(
|
|
117
|
+
screen.queryByText(
|
|
118
|
+
'You have to sign this training contract to finalize your subscription.',
|
|
119
|
+
),
|
|
115
120
|
).not.toBeInTheDocument();
|
|
116
121
|
expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
|
|
117
122
|
expect(screen.getByText('On going')).toBeInTheDocument();
|
|
@@ -147,7 +152,7 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
147
152
|
|
|
148
153
|
expect(screen.getByText('Ref. ' + (order.course as CourseLight).code)).toBeInTheDocument();
|
|
149
154
|
expect(
|
|
150
|
-
screen.getByText('You have to sign this training contract to
|
|
155
|
+
screen.getByText('You have to sign this training contract to finalize your subscription.'),
|
|
151
156
|
).toBeInTheDocument();
|
|
152
157
|
expect(screen.getByText('Signature required')).toBeInTheDocument();
|
|
153
158
|
expect(screen.getByRole('link', { name: 'Sign' })).toBeInTheDocument();
|
|
@@ -174,6 +179,15 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
174
179
|
{ overwriteRoutes: true },
|
|
175
180
|
);
|
|
176
181
|
|
|
182
|
+
const orderQueryParameters = {
|
|
183
|
+
course_code: order.course.code,
|
|
184
|
+
product_id: order.product_id,
|
|
185
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
186
|
+
};
|
|
187
|
+
const queryParams = queryString.stringify(orderQueryParameters);
|
|
188
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
|
|
189
|
+
fetchMock.get(url, [order]);
|
|
190
|
+
|
|
177
191
|
render(
|
|
178
192
|
<DashboardTest initialRoute={LearnerDashboardPaths.ORDER.replace(':orderId', order.id)} />,
|
|
179
193
|
{ wrapper: BaseJoanieAppWrapper },
|
|
@@ -201,6 +215,15 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
201
215
|
{ overwriteRoutes: true },
|
|
202
216
|
);
|
|
203
217
|
|
|
218
|
+
const orderQueryParameters = {
|
|
219
|
+
course_code: order.course.code,
|
|
220
|
+
product_id: order.product_id,
|
|
221
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
222
|
+
};
|
|
223
|
+
const queryParams = queryString.stringify(orderQueryParameters);
|
|
224
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
|
|
225
|
+
fetchMock.get(url, [order]);
|
|
226
|
+
|
|
204
227
|
render(
|
|
205
228
|
<DashboardTest initialRoute={LearnerDashboardPaths.ORDER.replace(':orderId', order.id)} />,
|
|
206
229
|
{ wrapper: BaseJoanieAppWrapper },
|
|
@@ -212,7 +235,7 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
212
235
|
expect(screen.getByText('Signature required')).toBeInTheDocument();
|
|
213
236
|
expect(screen.getByRole('button', { name: 'Sign' })).toBeInTheDocument();
|
|
214
237
|
expect(
|
|
215
|
-
screen.getByText('You have to sign this training contract to
|
|
238
|
+
screen.getByText('You have to sign this training contract to finalize your subscription.'),
|
|
216
239
|
).toBeInTheDocument();
|
|
217
240
|
|
|
218
241
|
expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
|
|
@@ -222,7 +245,15 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
222
245
|
expect($enrollButtons).toHaveLength(order.target_courses[0].course_runs.length);
|
|
223
246
|
$enrollButtons.forEach(($button) => expect($button).toBeDisabled());
|
|
224
247
|
|
|
225
|
-
await expectBannerError(
|
|
248
|
+
await expectBannerError(
|
|
249
|
+
'You have to sign your training contract to finalize your subscription.',
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
// The payment block should not display information about the payment schedule
|
|
253
|
+
const paymentBlock = screen.getByTestId('dashboard-item-payment-method');
|
|
254
|
+
within(paymentBlock).getByText(
|
|
255
|
+
'You will able to manage your payment installment here once your subscription is finalized.',
|
|
256
|
+
);
|
|
226
257
|
});
|
|
227
258
|
|
|
228
259
|
it('renders a writable order with a signed contract', async () => {
|
|
@@ -242,6 +273,15 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
242
273
|
{ overwriteRoutes: true },
|
|
243
274
|
);
|
|
244
275
|
|
|
276
|
+
const orderQueryParameters = {
|
|
277
|
+
course_code: order.course.code,
|
|
278
|
+
product_id: order.product_id,
|
|
279
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
280
|
+
};
|
|
281
|
+
const queryParams = queryString.stringify(orderQueryParameters);
|
|
282
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
|
|
283
|
+
fetchMock.get(url, [order]);
|
|
284
|
+
|
|
245
285
|
render(
|
|
246
286
|
<DashboardTest initialRoute={LearnerDashboardPaths.ORDER.replace(':orderId', order.id)} />,
|
|
247
287
|
{ wrapper: BaseJoanieAppWrapper },
|
|
@@ -254,7 +294,9 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
254
294
|
expect(screen.getByText('On going')).toBeInTheDocument();
|
|
255
295
|
expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
|
|
256
296
|
expect(
|
|
257
|
-
screen.queryByText(
|
|
297
|
+
screen.queryByText(
|
|
298
|
+
'You have to sign this training contract to finalize your subscription.',
|
|
299
|
+
),
|
|
258
300
|
).not.toBeInTheDocument();
|
|
259
301
|
|
|
260
302
|
expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
|
|
@@ -286,6 +328,15 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
286
328
|
{ overwriteRoutes: true },
|
|
287
329
|
);
|
|
288
330
|
|
|
331
|
+
const orderQueryParameters = {
|
|
332
|
+
course_code: order.course.code,
|
|
333
|
+
product_id: order.product_id,
|
|
334
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
335
|
+
};
|
|
336
|
+
const queryParams = queryString.stringify(orderQueryParameters);
|
|
337
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
|
|
338
|
+
fetchMock.get(url, [order]);
|
|
339
|
+
|
|
289
340
|
const DOWNLOAD_URL = `https://joanie.endpoint/api/v1.0/contracts/${
|
|
290
341
|
order.contract!.id
|
|
291
342
|
}/download/`;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fetchMock from 'fetch-mock';
|
|
4
4
|
import { act, screen, waitFor, within } from '@testing-library/react';
|
|
5
5
|
import userEvent from '@testing-library/user-event';
|
|
6
|
+
import queryString from 'query-string';
|
|
6
7
|
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
7
8
|
import { DashboardTest } from 'widgets/Dashboard/components/DashboardTest';
|
|
8
9
|
import {
|
|
@@ -19,7 +20,7 @@ import { render } from 'utils/test/render';
|
|
|
19
20
|
import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
|
|
20
21
|
|
|
21
22
|
import { LearnerDashboardPaths } from 'widgets/Dashboard/utils/learnerRoutesPaths';
|
|
22
|
-
import { OrderState } from 'types/Joanie';
|
|
23
|
+
import { NOT_CANCELED_ORDER_STATES, OrderState } from 'types/Joanie';
|
|
23
24
|
|
|
24
25
|
jest.mock('utils/context', () => ({
|
|
25
26
|
__esModule: true,
|
|
@@ -66,6 +67,16 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
66
67
|
{ overwriteRoutes: true },
|
|
67
68
|
);
|
|
68
69
|
|
|
70
|
+
// overwrite useProductOrder call
|
|
71
|
+
const orderQueryParameters = {
|
|
72
|
+
course_code: order.course.code,
|
|
73
|
+
product_id: order.product_id,
|
|
74
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
75
|
+
};
|
|
76
|
+
const queryParams = queryString.stringify(orderQueryParameters);
|
|
77
|
+
const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
|
|
78
|
+
fetchMock.get(url, [order]);
|
|
79
|
+
|
|
69
80
|
// mock useUnionResources calls
|
|
70
81
|
fetchMock.get('begin:https://joanie.endpoint/api/v1.0/enrollments/', {
|
|
71
82
|
results: [],
|