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.
Files changed (82) hide show
  1. package/js/api/joanie.ts +12 -16
  2. package/js/api/lms/dummy.ts +1 -12
  3. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  4. package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
  5. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  6. package/js/components/ContractFrame/_styles.scss +6 -14
  7. package/js/components/CreditCardSelector/index.spec.tsx +7 -7
  8. package/js/components/CreditCardSelector/index.tsx +2 -2
  9. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  10. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  11. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  12. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  13. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  14. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  15. package/js/components/PaymentInterfaces/types.ts +5 -2
  16. package/js/components/PurchaseButton/index.spec.tsx +69 -37
  17. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
  18. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  19. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  20. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +75 -41
  21. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
  22. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  23. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
  24. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  25. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +5 -0
  26. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  27. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
  28. package/js/components/SaleTunnel/_styles.scss +10 -1
  29. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  30. package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
  31. package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
  32. package/js/components/SaleTunnel/index.spec.tsx +330 -779
  33. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  34. package/js/components/SignContractButton/index.spec.tsx +16 -20
  35. package/js/components/SignContractButton/index.tsx +3 -1
  36. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  37. package/js/hooks/useCreditCards/index.ts +49 -11
  38. package/js/hooks/useOrders/index.spec.tsx +322 -0
  39. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  40. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  41. package/js/hooks/useProductOrder/index.tsx +2 -2
  42. package/js/hooks/useResources/useResourcesRoot.ts +1 -0
  43. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  44. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  45. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  46. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  47. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  48. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  49. package/js/settings/settings.test.ts +11 -2
  50. package/js/types/Joanie.ts +49 -34
  51. package/js/utils/OrderHelper/index.ts +38 -42
  52. package/js/utils/test/factories/joanie.ts +36 -51
  53. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  54. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
  55. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
  56. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +7 -6
  57. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
  58. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
  59. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
  60. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +28 -8
  61. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +2 -5
  62. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  63. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
  64. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
  65. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  66. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  67. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
  68. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  69. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  70. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  71. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  72. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  73. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  74. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  75. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  76. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  77. package/package.json +1 -1
  78. package/scss/components/_index.scss +2 -1
  79. package/js/components/PaymentButton/_styles.scss +0 -27
  80. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
  81. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  82. 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 { CertificateProduct, CourseLight, OrderState, ProductType } from 'types/Joanie';
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.CertificateOrderWithOneClickPaymentFactory().one();
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.VALIDATED,
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('should not display purchase button for a course run with submitted order.', () => {
155
- const order = OrderEnrollmentFactory({
156
- certificate_id: undefined,
157
- product_id: product.id,
158
- state: OrderState.SUBMITTED,
159
- }).one();
160
- const enrollment = EnrollmentFactory({
161
- orders: [order],
162
- course_run: CourseRunFactory({ course }).one(),
163
- }).one();
164
- render(<ProductCertificateFooter product={product} enrollment={enrollment} />);
165
- expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
166
- expect(screen.queryByTestId('PurchaseButton__cta')).not.toBeInTheDocument();
167
- });
168
-
169
- it('should display purchase button for a course run with pending order.', () => {
170
- const order = OrderEnrollmentFactory({
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({
@@ -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 { CertificateProduct, Enrollment, OrderState, ProductType } from 'types/Joanie';
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?.state === OrderState.VALIDATED ? (
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?.state === OrderState.VALIDATED ? (
64
- order.certificate_id && (
68
+ {OrderHelper.isActive(order) ? (
69
+ order!.certificate_id && (
65
70
  <DownloadCertificateButton
66
71
  className="dashboard-item__button"
67
- certificateId={order.certificate_id}
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?.state === OrderState.SUBMITTED}
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('Completed');
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('Completed');
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 { payment_info: paymentInfo, ...order } = CredentialOrderWithPaymentFactory().one();
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 during payment');
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, product?.contract_definition);
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?.title ?? ''}
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?.contract_definition!}
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={needsSignature}
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, product.contract_definition);
399
+ const needsSignature = OrderHelper.orderNeedsSignature(order);
401
400
  return (
402
401
  <div
403
402
  id={`dashboard-item-contract-${order.id}`}
@@ -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: undefined }).one(),
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 enroll in your course runs!',
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
 
@@ -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
- // TODO: At the moment is automatically selects the default credit card but it must select the credit card used to
96
- // buy the order.
97
- const [creditCard, setCreditCard] = useState<CreditCard>();
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={setCreditCard}
120
+ creditCard={selectedCreditCard || creditCard}
121
+ setCreditCard={setSelectedCreditCard}
102
122
  quickRemove={false}
103
123
  allowEdit={false}
104
124
  />
@@ -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, OrderState } from 'types/Joanie';
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 () => {
@@ -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('<DashboardItemOrder/>', () => {
12
+ describe('<OrderStateLearnerMessage/>', () => {
17
13
  it.each([
18
- [OrderState.DRAFT, 'Draft'],
19
- [OrderState.SUBMITTED, 'Submitted'],
20
- [OrderState.PENDING, 'Pending'],
14
+ [OrderState.ASSIGNED, 'Pending'],
21
15
  [OrderState.CANCELED, 'Canceled'],
22
- ])(
23
- 'should display message from order state: %s when order have no contract',
24
- (state, expectedMessage) => {
25
- const order = CredentialOrderFactory({ state }).one();
26
- render(<OrderStateLearnerMessage order={order} />, {
27
- wrapper: ({ children }: PropsWithChildren) => <IntlWrapper>{children}</IntlWrapper>,
28
- });
29
- expect(screen.getByText(expectedMessage)).toBeInTheDocument();
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 validated order that have a generated certificate', () => {
33
+ it('should display message for completed order that have a generated certificate', () => {
83
34
  const order = CredentialOrderFactory({
84
- state: OrderState.VALIDATED,
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.statusCompleted), {
42
+ screen.getByText(intl.formatMessage(messages.statusPassed), {
96
43
  exact: false,
97
44
  }),
98
45
  );
@@ -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.OrderStateMessage.statusDraft',
6
+ id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusDraft',
7
7
  description: 'Status shown on the dashboard order item when order is draft.',
8
- defaultMessage: 'Draft',
8
+ defaultMessage: 'Pending',
9
9
  },
10
- statusSubmitted: {
11
- id: 'components.DashboardItem.Order.OrderStateMessage.statusSubmitted',
12
- description: 'Status shown on the dashboard order item when order is submitted.',
13
- defaultMessage: 'Submitted',
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.OrderStateMessage.statusPending',
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
- statusOnGoing: {
21
- id: 'components.DashboardItem.Order.OrderStateMessage.statusOnGoing',
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.OrderStateMessage.statusCompleted',
28
- description:
29
- 'Status shown on the dashboard order item when order is validated with certificate',
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.OrderStateMessage.statusWaitingSignature',
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.OrderStateMessage.statusWaitingCounterSignature',
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.OrderStateMessage.statusCanceled',
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.OrderStateMessage.statusNoPayment',
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: 'Failed payment',
56
+ defaultMessage: 'First direct debit has failed',
59
57
  },
60
58
  statusFailedPayment: {
61
- id: 'components.DashboardItem.Order.OrderStateMessage.statusFailedPayment',
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: 'Failed payment',
61
+ defaultMessage: 'Last direct debit has failed',
64
62
  },
65
- statusOther: {
66
- id: 'components.DashboardItem.Order.OrderStateMessage.statusOther',
67
- description: 'Status shown on the dashboard order item when order status is unknown',
68
- defaultMessage: '{state}',
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