richie-education 2.29.2 → 2.29.3-dev48

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 (127) hide show
  1. package/i18n/locales/ar-SA.json +193 -125
  2. package/i18n/locales/es-ES.json +193 -125
  3. package/i18n/locales/fa-IR.json +193 -125
  4. package/i18n/locales/fr-CA.json +193 -125
  5. package/i18n/locales/fr-FR.json +193 -125
  6. package/i18n/locales/ko-KR.json +193 -125
  7. package/i18n/locales/pt-PT.json +193 -125
  8. package/i18n/locales/ru-RU.json +193 -125
  9. package/i18n/locales/vi-VN.json +193 -125
  10. package/js/api/joanie.ts +42 -17
  11. package/js/api/lms/dummy.ts +1 -12
  12. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  13. package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
  14. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  15. package/js/components/ContractFrame/_styles.scss +6 -14
  16. package/js/components/ContractStatus/index.spec.tsx +1 -1
  17. package/js/components/ContractStatus/index.tsx +1 -1
  18. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.spec.tsx +15 -45
  19. package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/index.tsx +19 -26
  20. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  21. package/js/components/Form/utils.ts +2 -1
  22. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  23. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  24. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  25. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  26. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  27. package/js/components/PaymentInterfaces/types.ts +5 -2
  28. package/js/components/PaymentScheduleGrid/_styles.scss +13 -0
  29. package/js/components/PaymentScheduleGrid/index.tsx +50 -70
  30. package/js/components/PurchaseButton/index.spec.tsx +84 -37
  31. package/js/components/PurchaseButton/index.tsx +9 -27
  32. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -2
  33. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  34. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  35. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +105 -48
  36. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +16 -20
  37. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  38. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +157 -0
  39. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  40. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.scss +4 -5
  41. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +39 -11
  42. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  43. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +202 -0
  44. package/js/components/SaleTunnel/_styles.scss +16 -5
  45. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  46. package/js/components/SaleTunnel/index.credential.spec.tsx +14 -25
  47. package/js/components/SaleTunnel/index.full-process.spec.tsx +114 -48
  48. package/js/components/SaleTunnel/index.spec.tsx +333 -717
  49. package/js/components/SaleTunnel/index.tsx +2 -1
  50. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  51. package/js/components/SignContractButton/index.spec.tsx +16 -20
  52. package/js/components/SignContractButton/index.tsx +3 -1
  53. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  54. package/js/hooks/useCreditCards/index.ts +49 -11
  55. package/js/hooks/useOrders/index.spec.tsx +322 -0
  56. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  57. package/js/hooks/usePaymentSchedule.tsx +23 -0
  58. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  59. package/js/hooks/useProductOrder/index.tsx +2 -2
  60. package/js/hooks/useResources/useResourcesRoot.ts +4 -3
  61. package/js/index.tsx +2 -0
  62. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  63. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  64. package/js/pages/DashboardOrderLayout/index.spec.tsx +10 -1
  65. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  66. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  67. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  68. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  69. package/js/settings/settings.prod.ts +0 -2
  70. package/js/settings/settings.test.ts +16 -2
  71. package/js/translations/ar-SA.json +1 -1
  72. package/js/translations/es-ES.json +1 -1
  73. package/js/translations/fa-IR.json +1 -1
  74. package/js/translations/fr-CA.json +1 -1
  75. package/js/translations/fr-FR.json +1 -1
  76. package/js/translations/ko-KR.json +1 -1
  77. package/js/translations/pt-PT.json +1 -1
  78. package/js/translations/ru-RU.json +1 -1
  79. package/js/translations/vi-VN.json +1 -1
  80. package/js/types/Joanie.ts +81 -32
  81. package/js/utils/OrderHelper/index.ts +46 -38
  82. package/js/utils/PaymentScheduleHelper/index.spec.tsx +78 -0
  83. package/js/utils/PaymentScheduleHelper/index.ts +30 -0
  84. package/js/utils/ProductHelper/index.spec.ts +322 -166
  85. package/js/utils/ProductHelper/index.ts +32 -0
  86. package/js/utils/test/factories/joanie.ts +72 -69
  87. package/js/widgets/Dashboard/components/DashboardItem/Contract/index.spec.tsx +2 -2
  88. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  89. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.spec.tsx +23 -0
  90. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/DashboardItemEnrollment.tsx +23 -1
  91. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +145 -34
  92. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +175 -37
  93. package/js/widgets/Dashboard/components/DashboardItem/Order/CertificateItem/index.tsx +51 -0
  94. package/js/widgets/Dashboard/components/DashboardItem/Order/ContractItem/index.tsx +52 -0
  95. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemNotResumable.spec.tsx +109 -0
  96. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +219 -5
  97. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +111 -245
  98. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +61 -8
  99. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +17 -7
  100. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemSavePaymentMethod.spec.tsx +116 -0
  101. package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +175 -0
  102. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/_styles.scss +7 -0
  103. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +127 -0
  104. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +216 -0
  105. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  106. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +40 -25
  107. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +28 -22
  108. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  109. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  110. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +150 -0
  111. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +32 -14
  112. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  113. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  114. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  115. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  116. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  117. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  118. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  119. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  120. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  121. package/package.json +45 -44
  122. package/scss/components/_index.scss +4 -2
  123. package/js/components/PaymentButton/_styles.scss +0 -27
  124. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -340
  125. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  126. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
  127. /package/js/components/{SaleTunnel/CreditCardSelector → CreditCardSelector}/_styles.scss +0 -0
@@ -1,21 +1,16 @@
1
- import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
2
- import { Button } from '@openfun/cunningham-react';
3
- import classNames from 'classnames';
1
+ import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
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';
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';
19
14
  import { DashboardSubItemsList } from '../DashboardSubItemsList';
20
15
  import { DashboardItemCourseEnrolling } from '../CourseEnrolling';
21
16
  import { DashboardItem } from '../index';
@@ -28,55 +23,32 @@ const messages = defineMessages({
28
23
  description: 'Button that redirects to the order details',
29
24
  defaultMessage: 'View details',
30
25
  },
31
- loadingCertificate: {
32
- id: 'components.DashboardItemOrder.loadingCertificate',
33
- description: 'Accessible label displayed while certificate is being fetched on the dashboard.',
34
- defaultMessage: 'Loading certificate...',
35
- },
36
26
  syllabusLinkLabel: {
37
27
  id: 'components.DashboardItemOrder.syllabusLinkLabel',
38
28
  description: 'Syllabus link label on order details',
39
29
  defaultMessage: 'Go to syllabus',
40
30
  },
41
- contactDescription: {
42
- id: 'components.DashboardItemOrder.contactDescription',
43
- description: 'Description of the contact information for the organization',
44
- defaultMessage: 'Your training reference is {name} - {email}.',
45
- },
46
- contactButton: {
47
- id: 'components.DashboardItemOrder.contactButton',
48
- description: 'Button to contact the organization',
49
- defaultMessage: 'Contact',
50
- },
51
- organizationHeader: {
52
- id: 'components.DashboardItemOrder.organizationHeader',
53
- description: 'Header of the organization section',
54
- defaultMessage: 'This training is provided by',
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',
55
35
  },
56
- organizationLogoAlt: {
57
- id: 'components.DashboardItemOrder.organizationLogoAlt',
58
- description: 'Alt text for the organization logo',
59
- defaultMessage: 'Logo of the organization',
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.',
60
40
  },
61
- trainingContractTitle: {
62
- id: 'components.DashboardItemOrder.trainingContractTitle',
63
- description: 'Title of the training contract section',
64
- defaultMessage: 'Training contract',
41
+ missingPaymentMethodCTA: {
42
+ id: 'components.DashboardItemOrder.missingPaymentMethodCTA',
43
+ description: 'CTA label displayed when the order is missing a payment method',
44
+ defaultMessage: 'Define',
65
45
  },
66
- organizationMailContactLabel: {
67
- id: 'components.DashboardItemOrder.organizationMailContactLabel',
68
- description: 'Label for the organization mail contact',
69
- defaultMessage: 'Email',
70
- },
71
- organizationPhoneContactLabel: {
72
- id: 'components.DashboardItemOrder.organizationPhoneContactLabel',
73
- description: 'Label for the organization phone contact',
74
- defaultMessage: 'Phone',
75
- },
76
- organizationDpoContactLabel: {
77
- id: 'components.DashboardItemOrder.organizationDpoContactLabel',
78
- description: 'Label for the organization DPO contact',
79
- defaultMessage: 'Data protection email',
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.',
80
52
  },
81
53
  });
82
54
 
@@ -87,63 +59,33 @@ interface DashboardItemOrderProps {
87
59
  writable?: boolean;
88
60
  }
89
61
 
90
- interface DashboardItemOrderCertificateProps {
91
- order: CredentialOrder;
92
- product: Product;
93
- }
94
-
95
- const DashboardItemOrderCertificate = ({ order, product }: DashboardItemOrderCertificateProps) => {
96
- if (!order.certificate_id) {
97
- return (
98
- <DashboardItemCertificate
99
- certificateDefinition={product.certificate_definition}
100
- productType={product.type}
101
- mode="compact"
102
- />
103
- );
104
- }
105
- const certificate = useCertificate(order.certificate_id);
106
- return (
107
- <>
108
- {certificate.states.fetching && (
109
- <Spinner aria-labelledby="loading-certificate">
110
- <span id="loading-certificate">
111
- <FormattedMessage {...messages.loadingCertificate} />
112
- </span>
113
- </Spinner>
114
- )}
115
- {certificate.item && (
116
- <DashboardItemCertificate
117
- certificate={certificate.item}
118
- productType={product.type}
119
- mode="compact"
120
- />
121
- )}
122
- </>
123
- );
124
- };
125
-
126
62
  export const DashboardItemOrder = ({
127
63
  order,
128
64
  showDetailsButton = true,
129
65
  showCertificate,
130
66
  writable = false,
131
67
  }: DashboardItemOrderProps) => {
132
- const course = order.course as CourseLight;
133
-
68
+ const { course } = order;
134
69
  const intl = useIntl();
135
70
  const { item: courseProductRelation } = useCourseProduct({
136
71
  product_id: order.product_id,
137
72
  course_id: course.code,
138
73
  });
139
74
  const { product } = courseProductRelation || {};
140
- const needsSignature = OrderHelper.orderNeedsSignature(order, product?.contract_definition);
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;
80
+ const canEnroll = OrderHelper.allowEnrollment(order);
81
+
82
+ if (!product) return null;
141
83
 
142
84
  return (
143
85
  <div className="dashboard-item-order">
144
86
  <DashboardItem
145
87
  data-testid={`dashboard-item-order-${order.id}`}
146
- title={product?.title ?? ''}
88
+ title={product.title}
147
89
  code={'Ref. ' + course.code}
148
90
  imageUrl={course.cover?.src}
149
91
  more={
@@ -157,13 +99,19 @@ export const DashboardItemOrder = ({
157
99
  <>
158
100
  <div className="dashboard-item-order__footer">
159
101
  <div className="dashboard-item__block__status">
160
- <Icon name={IconTypeEnum.SCHOOL} />
161
- <OrderStateLearnerMessage
162
- order={order}
163
- contractDefinition={product?.contract_definition}
164
- />
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
+ )}
165
113
  </div>
166
- {showDetailsButton && (
114
+ {!isNotResumable && showDetailsButton && (
167
115
  <RouterButton
168
116
  size="small"
169
117
  className="dashboard-item__button"
@@ -174,168 +122,86 @@ export const DashboardItemOrder = ({
174
122
  </RouterButton>
175
123
  )}
176
124
  </div>
177
- {!writable && needsSignature && (
125
+ {!writable && isProductPurchasable && needsSignature && (
178
126
  <DashboardItemContract
179
127
  key={`DashboardItemOrderContract_${order.id}`}
180
128
  title={product.title}
181
129
  order={order}
182
- contract_definition={product?.contract_definition!}
130
+ contract_definition={product.contract_definition!}
183
131
  contract={order.contract}
184
132
  writable={writable}
185
133
  mode="compact"
186
134
  />
187
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
+ )}
188
169
  </>
189
170
  }
190
171
  >
191
- <DashboardSubItemsList
192
- subItems={order.target_courses?.map((targetCourse) => (
193
- <DashboardSubItem
194
- title={targetCourse.title}
195
- footer={
196
- <DashboardItemCourseEnrolling
197
- writable={writable}
198
- course={targetCourse}
199
- order={order}
200
- product={product}
201
- activeEnrollment={CoursesHelper.findActiveCourseEnrollmentInOrder(
202
- targetCourse,
203
- order,
204
- )}
205
- notEnrolledUrl={generatePath(LearnerDashboardPaths.ORDER, {
206
- orderId: order.id,
207
- })}
208
- hideEnrollButtons={needsSignature}
209
- />
210
- }
211
- />
212
- ))}
213
- />
214
- </DashboardItem>
215
- {showCertificate && !!product?.certificate_definition && (
216
- <DashboardItemOrderCertificate order={order} product={product} />
217
- )}
218
- {writable && <OrganizationBlock order={order} product={product} />}
219
- </div>
220
- );
221
- };
222
-
223
- const OrganizationBlock = ({ order, product }: { order: CredentialOrder; product: Product }) => {
224
- const { organization } = order;
225
- if (!organization) {
226
- return null;
227
- }
228
-
229
- const showContactsBlock =
230
- organization.contact_email || organization.contact_phone || organization.dpo_email;
231
-
232
- return (
233
- <div className="dashboard-splitted-card mt-s" data-testid="organization-block">
234
- <div className="dashboard-splitted-card__column order-organization__caption">
235
- <div className="dashboard-item-order__organization">
236
- <div className="dashboard-item-order__organization__header">
237
- <FormattedMessage {...messages.organizationHeader} />
238
- </div>
239
- <div
240
- className="dashboard-item-order__organization__logo"
241
- style={{
242
- backgroundImage: `url(${organization.logo?.src})`,
243
- }}
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
+ ))}
244
194
  />
245
- <div className="dashboard-item-order__organization__name">{organization.title}</div>
246
- </div>
247
- </div>
248
- <div className="dashboard-splitted-card__separator order-organization__separator" />
249
- <div className="dashboard-splitted-card__column order-organization__items">
250
- <ContractItem order={order} product={product} />
251
- {showContactsBlock && (
252
- <div className="dashboard-splitted-card__item">
253
- <div className="dashboard-splitted-card__item__title">Contacts</div>
254
- <div className="dashboard-splitted-card__item__description">
255
- {organization.contact_email && (
256
- <div className="organization-block__contact__item">
257
- <FormattedMessage {...messages.organizationMailContactLabel} />
258
- <Button
259
- size="small"
260
- color="tertiary"
261
- href={'mailto:' + (organization.contact_email ?? '')}
262
- >
263
- {organization.contact_email}
264
- </Button>
265
- </div>
266
- )}
267
- {organization.contact_phone && (
268
- <div className="organization-block__contact__item">
269
- <FormattedMessage {...messages.organizationPhoneContactLabel} />
270
- <Button
271
- size="small"
272
- color="tertiary"
273
- href={'tel:' + (organization.contact_phone ?? '')}
274
- >
275
- {organization.contact_phone}
276
- </Button>
277
- </div>
278
- )}
279
- {organization.dpo_email && (
280
- <div className="organization-block__contact__item">
281
- <FormattedMessage {...messages.organizationDpoContactLabel} />
282
- <Button
283
- size="small"
284
- color="tertiary"
285
- href={'mailto:' + (organization.dpo_email ?? '')}
286
- >
287
- {organization.dpo_email}
288
- </Button>
289
- </div>
290
- )}
291
- </div>
292
- </div>
293
195
  )}
294
- {organization.address && (
295
- <div className="dashboard-splitted-card__item dashboard-splitted-card__item__address">
296
- <div className="dashboard-splitted-card__item__title">Address</div>
297
- <div className="dashboard-splitted-card__item__description">
298
- <AddressView address={organization.address} />
299
- </div>
300
- </div>
301
- )}
302
- </div>
303
- </div>
304
- );
305
- };
306
-
307
- const ContractItem = ({ product, order }: { order: CredentialOrder; product: Product }) => {
308
- if (!product?.contract_definition) {
309
- return;
310
- }
311
-
312
- const needsSignature = OrderHelper.orderNeedsSignature(order, product.contract_definition);
313
- return (
314
- <div
315
- id={`dashboard-item-contract-${order.id}`}
316
- className="dashboard-splitted-card__item"
317
- data-testid={`dashboard-item-contract-${order.id}`}
318
- >
319
- <div
320
- className={classNames('dashboard-splitted-card__item__title', {
321
- 'dashboard-splitted-card__item__title--dot': needsSignature,
322
- })}
323
- >
324
- <span>
325
- <FormattedMessage {...messages.trainingContractTitle} />
326
- </span>
327
- </div>
328
- <div className="dashboard-splitted-card__item__description">
329
- <ContractStatus contract={order.contract} />
330
- </div>
331
- <div className="dashboard-splitted-card__item__actions">
332
- <SignContractButton
333
- order={order}
334
- contract={order.contract}
335
- writable={true}
336
- className="dashboard-item__button"
337
- />
338
- </div>
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
+ </>
204
+ )}
339
205
  </div>
340
206
  );
341
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 } 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();
@@ -124,6 +129,7 @@ describe('<DashboardItemOrder/> Contract', () => {
124
129
 
125
130
  it('renders a non-writable order with a contract not signed yet', async () => {
126
131
  const order = CredentialOrderFactory({
132
+ state: OrderState.TO_SIGN,
127
133
  target_courses: TargetCourseFactory().many(1),
128
134
  target_enrollments: [],
129
135
  contract: ContractFactory({ student_signed_on: undefined }).one(),
@@ -146,7 +152,7 @@ describe('<DashboardItemOrder/> Contract', () => {
146
152
 
147
153
  expect(screen.getByText('Ref. ' + (order.course as CourseLight).code)).toBeInTheDocument();
148
154
  expect(
149
- 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.'),
150
156
  ).toBeInTheDocument();
151
157
  expect(screen.getByText('Signature required')).toBeInTheDocument();
152
158
  expect(screen.getByRole('link', { name: 'Sign' })).toBeInTheDocument();
@@ -173,6 +179,15 @@ describe('<DashboardItemOrder/> Contract', () => {
173
179
  { overwriteRoutes: true },
174
180
  );
175
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
+
176
191
  render(
177
192
  <DashboardTest initialRoute={LearnerDashboardPaths.ORDER.replace(':orderId', order.id)} />,
178
193
  { wrapper: BaseJoanieAppWrapper },
@@ -186,6 +201,7 @@ describe('<DashboardItemOrder/> Contract', () => {
186
201
  });
187
202
  it('renders a writable order with a contract not signed yet', async () => {
188
203
  const order = CredentialOrderFactory({
204
+ state: OrderState.TO_SIGN,
189
205
  target_courses: TargetCourseFactory().many(1),
190
206
  target_enrollments: [],
191
207
  contract: null,
@@ -199,6 +215,15 @@ describe('<DashboardItemOrder/> Contract', () => {
199
215
  { overwriteRoutes: true },
200
216
  );
201
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
+
202
227
  render(
203
228
  <DashboardTest initialRoute={LearnerDashboardPaths.ORDER.replace(':orderId', order.id)} />,
204
229
  { wrapper: BaseJoanieAppWrapper },
@@ -210,7 +235,7 @@ describe('<DashboardItemOrder/> Contract', () => {
210
235
  expect(screen.getByText('Signature required')).toBeInTheDocument();
211
236
  expect(screen.getByRole('button', { name: 'Sign' })).toBeInTheDocument();
212
237
  expect(
213
- 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.'),
214
239
  ).toBeInTheDocument();
215
240
 
216
241
  expect(screen.queryByRole('button', { name: 'Download' })).not.toBeInTheDocument();
@@ -220,7 +245,15 @@ describe('<DashboardItemOrder/> Contract', () => {
220
245
  expect($enrollButtons).toHaveLength(order.target_courses[0].course_runs.length);
221
246
  $enrollButtons.forEach(($button) => expect($button).toBeDisabled());
222
247
 
223
- 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
+ );
224
257
  });
225
258
 
226
259
  it('renders a writable order with a signed contract', async () => {
@@ -240,6 +273,15 @@ describe('<DashboardItemOrder/> Contract', () => {
240
273
  { overwriteRoutes: true },
241
274
  );
242
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
+
243
285
  render(
244
286
  <DashboardTest initialRoute={LearnerDashboardPaths.ORDER.replace(':orderId', order.id)} />,
245
287
  { wrapper: BaseJoanieAppWrapper },
@@ -252,7 +294,9 @@ describe('<DashboardItemOrder/> Contract', () => {
252
294
  expect(screen.getByText('On going')).toBeInTheDocument();
253
295
  expect(screen.queryByRole('button', { name: 'Sign' })).not.toBeInTheDocument();
254
296
  expect(
255
- 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
+ ),
256
300
  ).not.toBeInTheDocument();
257
301
 
258
302
  expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
@@ -284,6 +328,15 @@ describe('<DashboardItemOrder/> Contract', () => {
284
328
  { overwriteRoutes: true },
285
329
  );
286
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
+
287
340
  const DOWNLOAD_URL = `https://joanie.endpoint/api/v1.0/contracts/${
288
341
  order.contract!.id
289
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,6 +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';
23
+ import { NOT_CANCELED_ORDER_STATES, OrderState } from 'types/Joanie';
22
24
 
23
25
  jest.mock('utils/context', () => ({
24
26
  __esModule: true,
@@ -49,9 +51,10 @@ describe('<DashboardItemOrder/> Contract', () => {
49
51
  describe('writable', () => {
50
52
  it('successfully sign a contract', async () => {
51
53
  const order = CredentialOrderFactory({
54
+ state: OrderState.TO_SIGN,
52
55
  target_courses: TargetCourseFactory().many(1),
53
56
  target_enrollments: [],
54
- contract: ContractFactory({ student_signed_on: undefined }).one(),
57
+ contract: ContractFactory({ student_signed_on: null }).one(),
55
58
  }).one();
56
59
 
57
60
  // learner dashboard course page do one call to course product relation per order
@@ -64,6 +67,16 @@ describe('<DashboardItemOrder/> Contract', () => {
64
67
  { overwriteRoutes: true },
65
68
  );
66
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
+
67
80
  // mock useUnionResources calls
68
81
  fetchMock.get('begin:https://joanie.endpoint/api/v1.0/enrollments/', {
69
82
  results: [],
@@ -82,6 +95,7 @@ describe('<DashboardItemOrder/> Contract', () => {
82
95
  `https://joanie.endpoint/api/v1.0/orders/${order.id}/submit_for_signature/`,
83
96
  submitDeferred.promise,
84
97
  );
98
+ fetchMock.post(`https://joanie.endpoint/api/v1.0/signature/notifications/`, 200);
85
99
 
86
100
  // delay: null is needed because as we are using fake timers it would mock the timers of
87
101
  // RTL too. See https://github.com/testing-library/user-event/issues/833.
@@ -92,11 +106,7 @@ describe('<DashboardItemOrder/> Contract', () => {
92
106
  });
93
107
 
94
108
  await expectNoSpinner('Loading orders and enrollments...');
95
-
96
- expect(
97
- await screen.findByRole('heading', { level: 5, name: product.title }),
98
- ).toBeInTheDocument();
99
-
109
+ await screen.findByRole('heading', { level: 5, name: product.title });
100
110
  // Make sure the sign button is shown.
101
111
  await user.click(screen.getByRole('link', { name: 'Sign' }));
102
112
 
@@ -212,7 +222,7 @@ describe('<DashboardItemOrder/> Contract', () => {
212
222
  // We have the success message.
213
223
  await screen.findByRole('heading', { name: 'Congratulations!' });
214
224
  screen.getByText(
215
- 'You will receive an email once your contract will be fully signed. You can now enroll in your course runs!',
225
+ 'You will receive an email once your contract will be fully signed. You can now finalize your subscription.',
216
226
  );
217
227
  const nextButton = screen.getByRole('button', { name: 'Next' });
218
228