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
@@ -23,7 +23,7 @@ describe('<ContractStatus />', () => {
23
23
  );
24
24
 
25
25
  expect(
26
- screen.queryByText('You have to sign this training contract to access your training.'),
26
+ screen.queryByText('You have to sign this training contract to finalize your subscription.'),
27
27
  ).toBeInTheDocument();
28
28
 
29
29
  expect(screen.queryByText(/You signed this training contract/)).not.toBeInTheDocument();
@@ -15,7 +15,7 @@ const messages = defineMessages({
15
15
  id: 'components.ContractStatus.organizationSignedOn',
16
16
  },
17
17
  waitingLearnerSignature: {
18
- defaultMessage: 'You have to sign this training contract to access your training.',
18
+ defaultMessage: 'You have to sign this training contract to finalize your subscription.',
19
19
  description: 'Label displayed when a training contract need to be signed by the learner',
20
20
  id: 'components.ContractStatus.waitingSignature',
21
21
  },
@@ -1,13 +1,12 @@
1
1
  import c from 'classnames';
2
2
  import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
3
- import { useMemo } from 'react';
4
3
  import { Button, ButtonProps, useModal } from '@openfun/cunningham-react';
5
4
  import { useSession } from 'contexts/SessionContext';
6
5
  import * as Joanie from 'types/Joanie';
7
- import { isOpenedCourseRunCertificate, isOpenedCourseRunCredential } from 'utils/CourseRuns';
8
6
  import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel';
9
- import { Organization } from 'types/Joanie';
7
+ import { CourseLight, Organization } from 'types/Joanie';
10
8
  import { PacedCourse } from 'types';
9
+ import { ProductHelper } from 'utils/ProductHelper';
11
10
 
12
11
  const messages = defineMessages({
13
12
  loginToPurchase: {
@@ -53,7 +52,7 @@ interface PurchaseButtonPropsBase {
53
52
 
54
53
  interface CredentialPurchaseButtonProps extends PurchaseButtonPropsBase {
55
54
  product: Joanie.CredentialProduct;
56
- course: PacedCourse;
55
+ course: PacedCourse | CourseLight;
57
56
  enrollment?: undefined;
58
57
  }
59
58
 
@@ -77,26 +76,9 @@ const PurchaseButton = ({
77
76
  const intl = useIntl();
78
77
  const { user, login } = useSession();
79
78
 
80
- const hasAtLeastOneCourseRun = useMemo(() => {
81
- if (product.type === Joanie.ProductType.CERTIFICATE) {
82
- if (!enrollment?.course_run) {
83
- throw new Error(
84
- 'Unable to instanciate PurchaseButton with a product CERTIFICATE without the according CourseRun.',
85
- );
86
- }
87
- return isOpenedCourseRunCertificate(enrollment.course_run.state);
88
- }
89
- return (
90
- product.target_courses.length > 0 &&
91
- product.target_courses.every(({ course_runs }) =>
92
- course_runs.some((targetCourseRun) => isOpenedCourseRunCredential(targetCourseRun.state)),
93
- )
94
- );
95
- }, [product]);
96
-
97
- const hasAtLeastOneRemainingOrder =
98
- typeof product?.remaining_order_count !== 'number' || product.remaining_order_count > 0;
99
- const isPurchasable = hasAtLeastOneRemainingOrder && hasAtLeastOneCourseRun;
79
+ const hasOpenedTargetCourse = ProductHelper.hasOpenedTargetCourse(product, enrollment);
80
+ const hasRemainingSeat = ProductHelper.hasRemainingSeats(product);
81
+ const isPurchasable = hasRemainingSeat && hasOpenedTargetCourse;
100
82
 
101
83
  const saleTunnelModal = useModal({
102
84
  isOpenDefault: false,
@@ -121,7 +103,7 @@ const PurchaseButton = ({
121
103
  data-testid="PurchaseButton__cta"
122
104
  className={c('purchase-button__cta', className)}
123
105
  onClick={() => {
124
- if (hasAtLeastOneCourseRun) {
106
+ if (hasOpenedTargetCourse) {
125
107
  saleTunnelModal.open();
126
108
  }
127
109
  }}
@@ -136,7 +118,7 @@ const PurchaseButton = ({
136
118
  >
137
119
  {product.call_to_action}
138
120
  </Button>
139
- {!hasAtLeastOneCourseRun && (
121
+ {!hasOpenedTargetCourse && (
140
122
  <p className="purchase-button__no-course-run">
141
123
  <FormattedMessage
142
124
  {...(product.type === Joanie.ProductType.CREDENTIAL
@@ -145,7 +127,7 @@ const PurchaseButton = ({
145
127
  />
146
128
  </p>
147
129
  )}
148
- {hasAtLeastOneCourseRun && !hasAtLeastOneRemainingOrder && (
130
+ {hasOpenedTargetCourse && !hasRemainingSeat && (
149
131
  <p className="purchase-button__no-course-run">
150
132
  <FormattedMessage {...messages.noRemainingOrder} />
151
133
  </p>
@@ -1,6 +1,7 @@
1
1
  import { ModalProps } from '@openfun/cunningham-react';
2
2
  import {
3
3
  CertificateProduct,
4
+ CourseLight,
4
5
  CredentialProduct,
5
6
  Enrollment,
6
7
  Order,
@@ -17,7 +18,7 @@ export interface SaleTunnelProps extends Pick<ModalProps, 'isOpen' | 'onClose'>
17
18
  product: Product;
18
19
  organizations?: Organization[];
19
20
 
20
- course?: PacedCourse;
21
+ course?: PacedCourse | CourseLight;
21
22
  enrollment?: Enrollment;
22
23
  orderGroup?: OrderGroup;
23
24
  onFinish?: (order: Order) => void;
@@ -1,7 +1,8 @@
1
1
  import { findByRole, render, screen, waitFor } from '@testing-library/react';
2
2
  import fetchMock from 'fetch-mock';
3
+ import queryString from 'query-string';
3
4
  import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
4
- import { CredentialOrder } from 'types/Joanie';
5
+ import { CredentialOrder, NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
5
6
  import { CredentialOrderFactory, TargetCourseFactory } from 'utils/test/factories/joanie';
6
7
  import { mockCourseProductWithOrder } from 'utils/test/mockCourseProductWithOrder';
7
8
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
@@ -54,6 +55,14 @@ describe('<DashboardOrderLayout />', () => {
54
55
  { results: [order], next: null, previous: null, count: null },
55
56
  { overwriteRoutes: true },
56
57
  );
58
+ const orderQueryParameters = {
59
+ course_code: order.course.code,
60
+ product_id: order.product_id,
61
+ state: NOT_CANCELED_ORDER_STATES,
62
+ };
63
+ const queryParams = queryString.stringify(orderQueryParameters);
64
+ const url = `https://joanie.endpoint/api/v1.0/orders/?${queryParams}`;
65
+ fetchMock.get(url, [order]);
57
66
 
58
67
  render(WrapperWithDashboard(LearnerDashboardPaths.ORDER.replace(':orderId', order.id)));
59
68
 
@@ -1,183 +1,339 @@
1
1
  import { createIntl } from 'react-intl';
2
- import { CourseRunFactory, ProductFactory, TargetCourseFactory } from 'utils/test/factories/joanie';
2
+ import {
3
+ CertificateProductFactory,
4
+ CourseRunFactory,
5
+ CredentialProductFactory,
6
+ EnrollmentFactory,
7
+ ProductFactory,
8
+ TargetCourseFactory,
9
+ } from 'utils/test/factories/joanie';
10
+ import { CourseStateFactory } from 'utils/test/factories/richie';
11
+ import { Priority } from 'types';
3
12
  import { ProductHelper } from '.';
4
13
 
5
14
  describe('ProductHelper', () => {
6
- it('should return the product date range processed from course runs dates', () => {
7
- const product = ProductFactory({
8
- target_courses: [
9
- TargetCourseFactory({
10
- course_runs: [
11
- CourseRunFactory({
12
- start: new Date('2023-08-13').toISOString(),
13
- end: new Date('2024-04-27').toISOString(),
14
- }).one(),
15
- CourseRunFactory({
16
- start: new Date('2023-09-17').toISOString(),
17
- end: new Date('2024-05-29').toISOString(),
18
- }).one(),
19
- ],
20
- }).one(),
21
- TargetCourseFactory({
22
- course_runs: [
23
- CourseRunFactory({
24
- start: new Date('2024-01-19').toISOString(),
25
- end: new Date('2025-02-17').toISOString(),
26
- }).one(),
27
- ],
28
- }).one(),
29
- ],
30
- }).one();
31
-
32
- expect(ProductHelper.getDateRange(product)).toEqual([
33
- new Date('2023-08-13'),
34
- new Date('2025-02-17'),
35
- ]);
15
+ beforeEach(() => {
16
+ jest.resetAllMocks();
36
17
  });
37
18
 
38
- it('should return undefined minDate when there is a course run with an undefined start date', () => {
39
- const product = ProductFactory({
40
- target_courses: [
41
- TargetCourseFactory({
42
- course_runs: [
43
- CourseRunFactory({
44
- start: undefined,
45
- end: new Date('2024-04-27').toISOString(),
46
- }).one(),
47
- CourseRunFactory({
48
- start: new Date('2023-09-17').toISOString(),
49
- end: new Date('2024-05-29').toISOString(),
50
- }).one(),
51
- ],
52
- }).one(),
53
- TargetCourseFactory({
54
- course_runs: [
55
- CourseRunFactory({
56
- start: new Date('2024-01-19').toISOString(),
57
- end: new Date('2025-02-17').toISOString(),
58
- }).one(),
59
- ],
60
- }).one(),
61
- ],
62
- }).one();
63
-
64
- expect(ProductHelper.getDateRange(product)).toEqual([undefined, new Date('2025-02-17')]);
65
- });
19
+ describe('getDateRange', () => {
20
+ it('should return the product date range processed from course runs dates', () => {
21
+ const product = ProductFactory({
22
+ target_courses: [
23
+ TargetCourseFactory({
24
+ course_runs: [
25
+ CourseRunFactory({
26
+ start: new Date('2023-08-13').toISOString(),
27
+ end: new Date('2024-04-27').toISOString(),
28
+ }).one(),
29
+ CourseRunFactory({
30
+ start: new Date('2023-09-17').toISOString(),
31
+ end: new Date('2024-05-29').toISOString(),
32
+ }).one(),
33
+ ],
34
+ }).one(),
35
+ TargetCourseFactory({
36
+ course_runs: [
37
+ CourseRunFactory({
38
+ start: new Date('2024-01-19').toISOString(),
39
+ end: new Date('2025-02-17').toISOString(),
40
+ }).one(),
41
+ ],
42
+ }).one(),
43
+ ],
44
+ }).one();
45
+
46
+ expect(ProductHelper.getDateRange(product)).toEqual([
47
+ new Date('2023-08-13'),
48
+ new Date('2025-02-17'),
49
+ ]);
50
+ });
51
+
52
+ it('should return undefined minDate when there is a course run with an undefined start date', () => {
53
+ const product = ProductFactory({
54
+ target_courses: [
55
+ TargetCourseFactory({
56
+ course_runs: [
57
+ CourseRunFactory({
58
+ start: undefined,
59
+ end: new Date('2024-04-27').toISOString(),
60
+ }).one(),
61
+ CourseRunFactory({
62
+ start: new Date('2023-09-17').toISOString(),
63
+ end: new Date('2024-05-29').toISOString(),
64
+ }).one(),
65
+ ],
66
+ }).one(),
67
+ TargetCourseFactory({
68
+ course_runs: [
69
+ CourseRunFactory({
70
+ start: new Date('2024-01-19').toISOString(),
71
+ end: new Date('2025-02-17').toISOString(),
72
+ }).one(),
73
+ ],
74
+ }).one(),
75
+ ],
76
+ }).one();
77
+
78
+ expect(ProductHelper.getDateRange(product)).toEqual([undefined, new Date('2025-02-17')]);
79
+ });
80
+
81
+ it('should return undefined maxDate when there is a course run with an undefined end date', () => {
82
+ const product = ProductFactory({
83
+ target_courses: [
84
+ TargetCourseFactory({
85
+ course_runs: [
86
+ CourseRunFactory({
87
+ start: new Date('2023-08-13').toISOString(),
88
+ end: undefined,
89
+ }).one(),
90
+ CourseRunFactory({
91
+ start: new Date('2023-09-17').toISOString(),
92
+ end: new Date('2024-05-29').toISOString(),
93
+ }).one(),
94
+ ],
95
+ }).one(),
96
+ TargetCourseFactory({
97
+ course_runs: [
98
+ CourseRunFactory({
99
+ start: new Date('2024-01-19').toISOString(),
100
+ end: new Date('2025-02-17').toISOString(),
101
+ }).one(),
102
+ ],
103
+ }).one(),
104
+ ],
105
+ }).one();
66
106
 
67
- it('should return undefined maxDate when there is a course run with an undefined end date', () => {
68
- const product = ProductFactory({
69
- target_courses: [
70
- TargetCourseFactory({
71
- course_runs: [
72
- CourseRunFactory({
73
- start: new Date('2023-08-13').toISOString(),
74
- end: undefined,
75
- }).one(),
76
- CourseRunFactory({
77
- start: new Date('2023-09-17').toISOString(),
78
- end: new Date('2024-05-29').toISOString(),
79
- }).one(),
80
- ],
81
- }).one(),
82
- TargetCourseFactory({
83
- course_runs: [
84
- CourseRunFactory({
85
- start: new Date('2024-01-19').toISOString(),
86
- end: new Date('2025-02-17').toISOString(),
87
- }).one(),
88
- ],
89
- }).one(),
90
- ],
91
- }).one();
92
-
93
- expect(ProductHelper.getDateRange(product)).toEqual([new Date('2023-08-13'), undefined]);
107
+ expect(ProductHelper.getDateRange(product)).toEqual([new Date('2023-08-13'), undefined]);
108
+ });
94
109
  });
95
110
 
96
- it('should return the product languages processed from course runs languages', () => {
97
- const product = ProductFactory({
98
- target_courses: [
99
- TargetCourseFactory({
100
- course_runs: [
101
- CourseRunFactory({
102
- languages: ['fr', 'en'],
103
- }).one(),
104
- CourseRunFactory({
105
- languages: ['fr', 'de'],
106
- }).one(),
107
- ],
108
- }).one(),
109
- TargetCourseFactory({
110
- course_runs: [
111
- CourseRunFactory({
112
- languages: ['fr', 'es'],
113
- }).one(),
114
- ],
115
- }).one(),
116
- ],
117
- }).one();
118
-
119
- expect(ProductHelper.getLanguages(product)).toEqual(['fr', 'en', 'de', 'es']);
111
+ describe('getLanguages', () => {
112
+ it('should return the product languages processed from course runs languages', () => {
113
+ const product = ProductFactory({
114
+ target_courses: [
115
+ TargetCourseFactory({
116
+ course_runs: [
117
+ CourseRunFactory({
118
+ languages: ['fr', 'en'],
119
+ }).one(),
120
+ CourseRunFactory({
121
+ languages: ['fr', 'de'],
122
+ }).one(),
123
+ ],
124
+ }).one(),
125
+ TargetCourseFactory({
126
+ course_runs: [
127
+ CourseRunFactory({
128
+ languages: ['fr', 'es'],
129
+ }).one(),
130
+ ],
131
+ }).one(),
132
+ ],
133
+ }).one();
134
+
135
+ expect(ProductHelper.getLanguages(product)).toEqual(['fr', 'en', 'de', 'es']);
136
+ });
137
+
138
+ it('should return an empty array when there is no language', () => {
139
+ const product = ProductFactory({
140
+ target_courses: [
141
+ TargetCourseFactory({
142
+ course_runs: [
143
+ CourseRunFactory({
144
+ languages: [],
145
+ }).one(),
146
+ ],
147
+ }).one(),
148
+ ],
149
+ }).one();
150
+
151
+ expect(ProductHelper.getLanguages(product)).toEqual([]);
152
+ });
153
+
154
+ it('should return sorted human readable languages according to the active language', () => {
155
+ const product = ProductFactory({
156
+ target_courses: [
157
+ TargetCourseFactory({
158
+ course_runs: [
159
+ CourseRunFactory({
160
+ languages: ['fr', 'en'],
161
+ }).one(),
162
+ CourseRunFactory({
163
+ languages: ['fr', 'de'],
164
+ }).one(),
165
+ ],
166
+ }).one(),
167
+ TargetCourseFactory({
168
+ course_runs: [
169
+ CourseRunFactory({
170
+ languages: ['fr', 'es'],
171
+ }).one(),
172
+ ],
173
+ }).one(),
174
+ ],
175
+ }).one();
176
+
177
+ const intl = createIntl({ locale: 'en' });
178
+ expect(ProductHelper.getLanguages(product, true, intl)).toEqual(
179
+ 'English, French, German and Spanish',
180
+ );
181
+ });
182
+
183
+ it('should return an empty string when there is no language', () => {
184
+ const product = ProductFactory({
185
+ target_courses: [
186
+ TargetCourseFactory({
187
+ course_runs: [
188
+ CourseRunFactory({
189
+ languages: [],
190
+ }).one(),
191
+ ],
192
+ }).one(),
193
+ ],
194
+ }).one();
195
+
196
+ const intl = createIntl({ locale: 'en' });
197
+ expect(ProductHelper.getLanguages(product, true, intl)).toEqual('');
198
+ });
120
199
  });
121
200
 
122
- it('should return an empty array when there is no language', () => {
123
- const product = ProductFactory({
124
- target_courses: [
125
- TargetCourseFactory({
126
- course_runs: [
127
- CourseRunFactory({
128
- languages: [],
129
- }).one(),
130
- ],
131
- }).one(),
132
- ],
133
- }).one();
134
-
135
- expect(ProductHelper.getLanguages(product)).toEqual([]);
201
+ describe('hasRemainingSeats', () => {
202
+ it('should return false when the product is undefined', () => {
203
+ expect(ProductHelper.hasRemainingSeats(undefined)).toBe(false);
204
+ });
205
+
206
+ it('should return true when the product has no order group', () => {
207
+ const product = ProductFactory({
208
+ remaining_order_count: null,
209
+ }).one();
210
+
211
+ expect(ProductHelper.hasRemainingSeats(product)).toBe(true);
212
+ });
213
+
214
+ it('should return true when the product has remaining seats', () => {
215
+ const product = ProductFactory({
216
+ remaining_order_count: 10,
217
+ }).one();
218
+
219
+ expect(ProductHelper.hasRemainingSeats(product)).toBe(true);
220
+ });
221
+
222
+ it('should return false when the product does not have remaining seats', () => {
223
+ const product = ProductFactory({
224
+ remaining_order_count: 0,
225
+ }).one();
226
+
227
+ expect(ProductHelper.hasRemainingSeats(product)).toBe(false);
228
+ });
136
229
  });
137
230
 
138
- it('should return sorted human readable languages according to the active language', () => {
139
- const product = ProductFactory({
140
- target_courses: [
141
- TargetCourseFactory({
142
- course_runs: [
143
- CourseRunFactory({
144
- languages: ['fr', 'en'],
145
- }).one(),
146
- CourseRunFactory({
147
- languages: ['fr', 'de'],
148
- }).one(),
149
- ],
150
- }).one(),
151
- TargetCourseFactory({
152
- course_runs: [
153
- CourseRunFactory({
154
- languages: ['fr', 'es'],
155
- }).one(),
156
- ],
157
- }).one(),
158
- ],
159
- }).one();
160
-
161
- const intl = createIntl({ locale: 'en' });
162
- expect(ProductHelper.getLanguages(product, true, intl)).toEqual(
163
- 'English, French, German and Spanish',
164
- );
231
+ describe('hasOpenedTargetCourse', () => {
232
+ const openedCourseRunFactory = CourseRunFactory({
233
+ state: CourseStateFactory({
234
+ priority: Priority.ONGOING_OPEN,
235
+ }).one(),
236
+ });
237
+
238
+ const closedCourseRunFactory = CourseRunFactory({
239
+ state: CourseStateFactory({
240
+ priority: Priority.ARCHIVED_CLOSED,
241
+ }).one(),
242
+ });
243
+
244
+ it('should return false when the product is undefined', () => {
245
+ expect(ProductHelper.hasOpenedTargetCourse(undefined)).toBe(false);
246
+ });
247
+
248
+ it('should throw an error when the product is a certificate and the enrollment is undefined', () => {
249
+ const product = CertificateProductFactory().one();
250
+
251
+ expect(() => ProductHelper.hasOpenedTargetCourse(product, undefined)).toThrowError(
252
+ 'Unable to check if the certificate product relies on an opened course run without enrollment.',
253
+ );
254
+ });
255
+
256
+ it('should return true when the product is a certificate and the related course run is opened', () => {
257
+ const product = CertificateProductFactory().one();
258
+ const courseRun = openedCourseRunFactory.one();
259
+ const enrollment = EnrollmentFactory({ course_run: courseRun }).one();
260
+
261
+ expect(ProductHelper.hasOpenedTargetCourse(product, enrollment)).toBe(true);
262
+ });
263
+
264
+ it('should return false when the product is a certificate and the related course run is not opened', () => {
265
+ const product = CertificateProductFactory().one();
266
+ const courseRun = closedCourseRunFactory.one();
267
+ const enrollment = EnrollmentFactory({ course_run: courseRun }).one();
268
+
269
+ expect(ProductHelper.hasOpenedTargetCourse(product, enrollment)).toBe(false);
270
+ });
271
+
272
+ it('should return false when the product is a credential and has not target courses', () => {
273
+ const product = CredentialProductFactory({ target_courses: [] }).one();
274
+
275
+ expect(ProductHelper.hasOpenedTargetCourse(product)).toBe(false);
276
+ });
277
+
278
+ it('should return false when the product is a credential and at least one target course has no opened course run', () => {
279
+ const targetCourseOpen = TargetCourseFactory({
280
+ course_runs: [openedCourseRunFactory.one(), closedCourseRunFactory.one()],
281
+ }).one();
282
+ const targetCourseClosed = TargetCourseFactory({
283
+ course_runs: [closedCourseRunFactory.one(), closedCourseRunFactory.one()],
284
+ }).one();
285
+ const product = CredentialProductFactory({
286
+ target_courses: [targetCourseOpen, targetCourseClosed],
287
+ }).one();
288
+
289
+ expect(ProductHelper.hasOpenedTargetCourse(product)).toBe(false);
290
+ });
291
+
292
+ it('should return false when the product is a credential and target courses has one opened course run', () => {
293
+ const targetCourseOpen = TargetCourseFactory({
294
+ course_runs: [openedCourseRunFactory.one(), closedCourseRunFactory.one()],
295
+ }).many(1);
296
+ const product = CredentialProductFactory({ target_courses: targetCourseOpen }).one();
297
+
298
+ expect(ProductHelper.hasOpenedTargetCourse(product)).toBe(true);
299
+ });
165
300
  });
166
301
 
167
- it('should return an empty string when there is no language', () => {
168
- const product = ProductFactory({
169
- target_courses: [
170
- TargetCourseFactory({
171
- course_runs: [
172
- CourseRunFactory({
173
- languages: [],
174
- }).one(),
175
- ],
176
- }).one(),
177
- ],
178
- }).one();
179
-
180
- const intl = createIntl({ locale: 'en' });
181
- expect(ProductHelper.getLanguages(product, true, intl)).toEqual('');
302
+ describe('isPurchasable', () => {
303
+ it('should return false when the product is undefined', () => {
304
+ expect(ProductHelper.isPurchasable(undefined)).toBe(false);
305
+ });
306
+
307
+ it('should return false when the product does not have opened target courses', () => {
308
+ jest.spyOn(ProductHelper, 'hasOpenedTargetCourse').mockReturnValue(false);
309
+ jest.spyOn(ProductHelper, 'hasRemainingSeats').mockReturnValue(true);
310
+ const product = ProductFactory().one();
311
+
312
+ expect(ProductHelper.isPurchasable(product)).toBe(false);
313
+ });
314
+
315
+ it('should return false when the product does not remaining seats', () => {
316
+ jest.spyOn(ProductHelper, 'hasOpenedTargetCourse').mockReturnValue(true);
317
+ jest.spyOn(ProductHelper, 'hasRemainingSeats').mockReturnValue(false);
318
+ const product = ProductFactory().one();
319
+
320
+ expect(ProductHelper.isPurchasable(product)).toBe(false);
321
+ });
322
+
323
+ it('should return false when the product does not have opened target courses and remaining seats', () => {
324
+ jest.spyOn(ProductHelper, 'hasOpenedTargetCourse').mockReturnValue(false);
325
+ jest.spyOn(ProductHelper, 'hasRemainingSeats').mockReturnValue(false);
326
+ const product = ProductFactory().one();
327
+
328
+ expect(ProductHelper.isPurchasable(product)).toBe(false);
329
+ });
330
+
331
+ it('should return true when the product has opened target courses and remaining seats', () => {
332
+ jest.spyOn(ProductHelper, 'hasOpenedTargetCourse').mockReturnValue(true);
333
+ jest.spyOn(ProductHelper, 'hasRemainingSeats').mockReturnValue(true);
334
+ const product = ProductFactory().one();
335
+
336
+ expect(ProductHelper.isPurchasable(product)).toBe(true);
337
+ });
182
338
  });
183
339
  });