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.
Files changed (21) hide show
  1. package/js/components/ContractStatus/index.spec.tsx +1 -1
  2. package/js/components/ContractStatus/index.tsx +1 -1
  3. package/js/components/PurchaseButton/index.tsx +9 -27
  4. package/js/components/SaleTunnel/index.tsx +2 -1
  5. package/js/pages/DashboardOrderLayout/index.spec.tsx +10 -1
  6. package/js/utils/ProductHelper/index.spec.ts +322 -166
  7. package/js/utils/ProductHelper/index.ts +32 -0
  8. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.spec.tsx +2 -2
  9. package/js/widgets/Dashboard/components/DashboardItem/Order/CertificateItem/index.tsx +51 -0
  10. package/js/widgets/Dashboard/components/DashboardItem/Order/ContractItem/index.tsx +52 -0
  11. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemNotResumable.spec.tsx +109 -0
  12. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +105 -0
  13. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +103 -324
  14. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +59 -8
  15. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +12 -1
  16. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemSavePaymentMethod.spec.tsx +116 -0
  17. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +174 -0
  18. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +2 -1
  19. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +150 -0
  20. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +29 -3
  21. 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 { CourseLight, CredentialOrder, Product } from 'types/Joanie';
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 { OrderPaymentDetailsModal } from 'widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal';
20
- import { OrderPaymentRetryModal } from 'widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal';
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
- contactDescription: {
44
- id: 'components.DashboardItemOrder.contactDescription',
45
- description: 'Description of the contact information for the organization',
46
- defaultMessage: 'Your training reference is {name} - {email}.',
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
- organizationDpoContactLabel: {
79
- id: 'components.DashboardItemOrder.organizationDpoContactLabel',
80
- description: 'Label for the organization DPO contact',
81
- defaultMessage: 'Data protection email',
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
- paymentTitle: {
84
- id: 'components.DashboardItemOrder.paymentTitle',
85
- description: 'Label for the payment block',
86
- defaultMessage: 'Payment',
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
- 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}',
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.course as CourseLight;
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
- <Icon name={IconTypeEnum.SCHOOL} />
191
- <OrderStateLearnerMessage order={order} />
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
- <DashboardSubItemsList
219
- subItems={order.target_courses?.map((targetCourse) => (
220
- <DashboardSubItem
221
- title={targetCourse.title}
222
- footer={
223
- <DashboardItemCourseEnrolling
224
- writable={writable}
225
- course={targetCourse}
226
- order={order}
227
- activeEnrollment={CoursesHelper.findActiveCourseEnrollmentInOrder(
228
- targetCourse,
229
- order,
230
- )}
231
- notEnrolledUrl={generatePath(LearnerDashboardPaths.ORDER, {
232
- orderId: order.id,
233
- })}
234
- hideEnrollButtons={!canEnroll}
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
- <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} />
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
  };
@@ -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('You have to sign this training contract to access your training.'),
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('You have to sign this training contract to access your training.'),
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 access your training.'),
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 access your training.'),
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('You need to sign your contract before enrolling in a course run');
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('You have to sign this training contract to access your training.'),
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: [],