richie-education 3.3.1-dev9 → 3.3.2-dev10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/i18n/locales/ar-SA.json +29 -13
- package/i18n/locales/es-ES.json +29 -13
- package/i18n/locales/fa-IR.json +29 -13
- package/i18n/locales/fr-CA.json +29 -13
- package/i18n/locales/fr-FR.json +29 -13
- package/i18n/locales/ko-KR.json +29 -13
- package/i18n/locales/pt-PT.json +29 -13
- package/i18n/locales/ru-RU.json +29 -13
- package/i18n/locales/vi-VN.json +29 -13
- package/js/api/auth/keycloak.spec.ts +1 -0
- package/js/api/auth/keycloak.ts +5 -1
- package/js/api/joanie.ts +20 -0
- package/js/api/lms/openedx-fonzie-keycloak.spec.ts +35 -2
- package/js/api/lms/openedx-fonzie-keycloak.ts +26 -0
- package/js/api/lms/openedx-hawthorn.spec.ts +34 -2
- package/js/api/lms/openedx-hawthorn.ts +4 -1
- package/js/components/PurchaseButton/index.spec.tsx +12 -0
- package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +3 -0
- package/js/components/SaleTunnel/GenericSaleTunnel.tsx +11 -0
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular.tsx +141 -52
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +3 -2
- package/js/components/SaleTunnel/SubscriptionButton/index.tsx +6 -1
- package/js/components/SaleTunnel/index.credential.spec.tsx +108 -1
- package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +5 -1
- package/js/components/SaleTunnel/index.full-process-b2c.spec.tsx +9 -0
- package/js/components/SaleTunnel/index.spec.tsx +122 -3
- package/js/hooks/useDeepLink.tsx +21 -0
- package/js/pages/DashboardBatchOrders/index.spec.tsx +103 -0
- package/js/pages/DashboardKeycloakProfile/index.spec.tsx +77 -0
- package/js/pages/DashboardKeycloakProfile/index.tsx +93 -0
- package/js/pages/DashboardPreferences/index.spec.tsx +141 -0
- package/js/pages/DashboardPreferences/index.tsx +7 -1
- package/js/translations/ar-SA.json +1 -1
- package/js/translations/es-ES.json +1 -1
- package/js/translations/fa-IR.json +1 -1
- package/js/translations/fr-CA.json +1 -1
- package/js/translations/fr-FR.json +1 -1
- package/js/translations/ko-KR.json +1 -1
- package/js/translations/pt-PT.json +1 -1
- package/js/translations/ru-RU.json +1 -1
- package/js/translations/vi-VN.json +1 -1
- package/js/types/Joanie.ts +8 -1
- package/js/utils/test/factories/joanie.ts +8 -2
- package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/BatchOrderPaymentManager.tsx +15 -27
- package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.tsx +7 -12
- package/js/widgets/Dashboard/index.spec.tsx +4 -3
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusAsideList/index.tsx +8 -27
- package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +41 -17
- package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +37 -4
- package/js/widgets/cunningham-fr-FR-locale.json +80 -0
- package/js/widgets/index.tsx +6 -1
- package/package.json +2 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fetchMock from 'fetch-mock';
|
|
2
2
|
import { screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
3
4
|
import queryString from 'query-string';
|
|
4
5
|
import {
|
|
5
6
|
RichieContextFactory as mockRichieContextFactory,
|
|
@@ -99,7 +100,11 @@ describe('SaleTunnel / Credential', () => {
|
|
|
99
100
|
.post('https://joanie.endpoint/api/v1.0/orders/', order)
|
|
100
101
|
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
101
102
|
overwriteRoutes: true,
|
|
102
|
-
})
|
|
103
|
+
})
|
|
104
|
+
.get(
|
|
105
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
106
|
+
{},
|
|
107
|
+
);
|
|
103
108
|
|
|
104
109
|
render(<Wrapper product={product} course={course} />, {
|
|
105
110
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
@@ -115,4 +120,106 @@ describe('SaleTunnel / Credential', () => {
|
|
|
115
120
|
// - Payment button should not be disabled.
|
|
116
121
|
expect($button.disabled).toBe(false);
|
|
117
122
|
});
|
|
123
|
+
|
|
124
|
+
it('should display CPF payment option and redirect to deepLink when deepLink is available', async () => {
|
|
125
|
+
const course = PacedCourseFactory().one();
|
|
126
|
+
const product = CredentialProductFactory().one();
|
|
127
|
+
const billingAddress: Joanie.Address = AddressFactory({ is_main: true }).one();
|
|
128
|
+
const deepLink = 'https://placeholder.com/course/1';
|
|
129
|
+
const orderQueryParameters = {
|
|
130
|
+
course_code: course.code,
|
|
131
|
+
product_id: product.id,
|
|
132
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
fetchMock
|
|
136
|
+
.get(
|
|
137
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
138
|
+
[],
|
|
139
|
+
)
|
|
140
|
+
.get(
|
|
141
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
142
|
+
[],
|
|
143
|
+
)
|
|
144
|
+
.get(
|
|
145
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
146
|
+
{ deep_link: deepLink },
|
|
147
|
+
)
|
|
148
|
+
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
149
|
+
overwriteRoutes: true,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
window.open = jest.fn();
|
|
153
|
+
const user = userEvent.setup({ delay: null });
|
|
154
|
+
|
|
155
|
+
render(<Wrapper product={product} course={course} />, {
|
|
156
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await screen.findByRole('heading', { level: 3, name: /payment method/i });
|
|
160
|
+
|
|
161
|
+
// - By default, credit card payment should be selected.
|
|
162
|
+
expect(screen.getByRole('radio', { name: /credit card payment/i })).toBeChecked();
|
|
163
|
+
expect(screen.getByRole('radio', { name: /my training account \(cpf\)/i })).not.toBeChecked();
|
|
164
|
+
|
|
165
|
+
await user.click(screen.getByRole('radio', { name: /my training account \(cpf\)/i }));
|
|
166
|
+
|
|
167
|
+
// - CPF description and redirect button should be visible.
|
|
168
|
+
expect(
|
|
169
|
+
screen.getByText(/pay for your training using your personal training account/i),
|
|
170
|
+
).toBeInTheDocument();
|
|
171
|
+
const cpfButton = screen.getByRole('link', { name: /go to mon compte formation/i });
|
|
172
|
+
|
|
173
|
+
await user.click(cpfButton);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should not display CPF payment option when deepLink is null', async () => {
|
|
177
|
+
const course = PacedCourseFactory().one();
|
|
178
|
+
const product = CredentialProductFactory().one();
|
|
179
|
+
const billingAddress: Joanie.Address = AddressFactory({ is_main: true }).one();
|
|
180
|
+
const orderQueryParameters = {
|
|
181
|
+
course_code: course.code,
|
|
182
|
+
product_id: product.id,
|
|
183
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
fetchMock
|
|
187
|
+
.get(
|
|
188
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
189
|
+
[],
|
|
190
|
+
)
|
|
191
|
+
.get(
|
|
192
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
193
|
+
[],
|
|
194
|
+
)
|
|
195
|
+
.get(
|
|
196
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
197
|
+
{ deep_link: null },
|
|
198
|
+
)
|
|
199
|
+
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
200
|
+
overwriteRoutes: true,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
render(<Wrapper product={product} course={course} />, {
|
|
204
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// - wait for address to be loaded.
|
|
208
|
+
await screen.findByText(getAddressLabel(billingAddress));
|
|
209
|
+
|
|
210
|
+
// - Payment method section and CPF option should not be rendered.
|
|
211
|
+
expect(
|
|
212
|
+
screen.queryByRole('heading', { level: 3, name: /payment method/i }),
|
|
213
|
+
).not.toBeInTheDocument();
|
|
214
|
+
expect(
|
|
215
|
+
screen.queryByRole('radio', { name: /my training account \(cpf\)/i }),
|
|
216
|
+
).not.toBeInTheDocument();
|
|
217
|
+
expect(screen.queryByRole('radio', { name: /credit card payment/i })).not.toBeInTheDocument();
|
|
218
|
+
expect(
|
|
219
|
+
screen.queryByRole('link', { name: /go to mon compte formation/i }),
|
|
220
|
+
).not.toBeInTheDocument();
|
|
221
|
+
|
|
222
|
+
// - Classic billing information section should be displayed.
|
|
223
|
+
expect(screen.getByText(/this information will be used for billing/i)).toBeInTheDocument();
|
|
224
|
+
});
|
|
118
225
|
});
|
|
@@ -66,6 +66,10 @@ const setupBatchOrderMocks = (params: {
|
|
|
66
66
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
67
67
|
paymentPlan,
|
|
68
68
|
);
|
|
69
|
+
fetchMock.get(
|
|
70
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
71
|
+
{},
|
|
72
|
+
);
|
|
69
73
|
fetchMock.get(`https://joanie.endpoint/api/v1.0/enrollments/`, []);
|
|
70
74
|
fetchMock.get(
|
|
71
75
|
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify({
|
|
@@ -365,7 +369,7 @@ describe('SaleTunnel', () => {
|
|
|
365
369
|
discounted_price: 0,
|
|
366
370
|
discount: '-100%',
|
|
367
371
|
payment_schedule: undefined,
|
|
368
|
-
|
|
372
|
+
skip_contract_inputs: true,
|
|
369
373
|
}).one();
|
|
370
374
|
fetchMock.get(
|
|
371
375
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/?voucher_code=DISCOUNT100`,
|
|
@@ -116,6 +116,10 @@ describe('SaleTunnel', () => {
|
|
|
116
116
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
117
117
|
paymentPlan,
|
|
118
118
|
);
|
|
119
|
+
fetchMock.get(
|
|
120
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
121
|
+
{},
|
|
122
|
+
);
|
|
119
123
|
fetchMock.get(`https://joanie.endpoint/api/v1.0/enrollments/`, []);
|
|
120
124
|
const orderQueryParameters = {
|
|
121
125
|
product_id: product.id,
|
|
@@ -281,6 +285,11 @@ describe('SaleTunnel', () => {
|
|
|
281
285
|
paymentPlanVoucher,
|
|
282
286
|
{ overwriteRoutes: true },
|
|
283
287
|
);
|
|
288
|
+
fetchMock.get(
|
|
289
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
290
|
+
{},
|
|
291
|
+
{ overwriteRoutes: true },
|
|
292
|
+
);
|
|
284
293
|
await user.type(screen.getByLabelText('Voucher code'), 'DISCOUNT30');
|
|
285
294
|
await user.click(screen.getByRole('button', { name: 'Validate' }));
|
|
286
295
|
screen.getByRole('heading', { name: 'Payment schedule' });
|
|
@@ -175,6 +175,10 @@ describe.each([
|
|
|
175
175
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
176
176
|
paymentPlan,
|
|
177
177
|
)
|
|
178
|
+
.get(
|
|
179
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
180
|
+
{},
|
|
181
|
+
)
|
|
178
182
|
.post('https://joanie.endpoint/api/v1.0/orders/', order)
|
|
179
183
|
.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, order)
|
|
180
184
|
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
@@ -188,6 +192,7 @@ describe.each([
|
|
|
188
192
|
nbApiCalls += 1; // get user account call.
|
|
189
193
|
nbApiCalls += 1; // get user preferences call.
|
|
190
194
|
nbApiCalls += 1; // product payment-schedule call
|
|
195
|
+
nbApiCalls += 1; // product deep-link call
|
|
191
196
|
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
192
197
|
|
|
193
198
|
const user = userEvent.setup({ delay: null });
|
|
@@ -263,6 +268,10 @@ describe.each([
|
|
|
263
268
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
264
269
|
paymentPlan,
|
|
265
270
|
)
|
|
271
|
+
.get(
|
|
272
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
273
|
+
{},
|
|
274
|
+
)
|
|
266
275
|
.post('https://joanie.endpoint/api/v1.0/orders/', deferred.promise)
|
|
267
276
|
.get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
|
|
268
277
|
overwriteRoutes: true,
|
|
@@ -275,6 +284,7 @@ describe.each([
|
|
|
275
284
|
nbApiCalls += 1; // get user account call.
|
|
276
285
|
nbApiCalls += 1; // get user preferences call.
|
|
277
286
|
nbApiCalls += 1; // get paymentPlan call.
|
|
287
|
+
nbApiCalls += 1; // get deep-link call.
|
|
278
288
|
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
279
289
|
|
|
280
290
|
const user = userEvent.setup({ delay: null });
|
|
@@ -333,6 +343,10 @@ describe.each([
|
|
|
333
343
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
334
344
|
paymentPlan,
|
|
335
345
|
)
|
|
346
|
+
.get(
|
|
347
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
348
|
+
{},
|
|
349
|
+
)
|
|
336
350
|
.get('https://joanie.endpoint/api/v1.0/offerings/get-organizations/', []);
|
|
337
351
|
|
|
338
352
|
render(<Wrapper paymentPlan={paymentPlan} product={product} isWithdrawable={true} />, {
|
|
@@ -379,6 +393,10 @@ describe.each([
|
|
|
379
393
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
380
394
|
paymentPlan,
|
|
381
395
|
)
|
|
396
|
+
.get(
|
|
397
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
398
|
+
{},
|
|
399
|
+
)
|
|
382
400
|
.get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard], {
|
|
383
401
|
overwriteRoutes: true,
|
|
384
402
|
})
|
|
@@ -390,7 +408,7 @@ describe.each([
|
|
|
390
408
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
391
409
|
});
|
|
392
410
|
|
|
393
|
-
nbApiCalls +=
|
|
411
|
+
nbApiCalls += 4;
|
|
394
412
|
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
395
413
|
|
|
396
414
|
await screen.findByTestId('sale-tunnel-save-payment-method-step');
|
|
@@ -415,6 +433,10 @@ describe.each([
|
|
|
415
433
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
416
434
|
paymentPlan,
|
|
417
435
|
)
|
|
436
|
+
.get(
|
|
437
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
438
|
+
{},
|
|
439
|
+
)
|
|
418
440
|
.get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard], {
|
|
419
441
|
overwriteRoutes: true,
|
|
420
442
|
})
|
|
@@ -426,7 +448,7 @@ describe.each([
|
|
|
426
448
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
427
449
|
});
|
|
428
450
|
|
|
429
|
-
nbApiCalls +=
|
|
451
|
+
nbApiCalls += 4;
|
|
430
452
|
await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
|
|
431
453
|
|
|
432
454
|
await screen.findByTestId('sale-tunnel-sign-step');
|
|
@@ -446,6 +468,10 @@ describe.each([
|
|
|
446
468
|
.get(
|
|
447
469
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
448
470
|
paymentPlan,
|
|
471
|
+
)
|
|
472
|
+
.get(
|
|
473
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
474
|
+
{},
|
|
449
475
|
);
|
|
450
476
|
|
|
451
477
|
render(<Wrapper paymentPlan={paymentPlan} product={product} isWithdrawable={true} />, {
|
|
@@ -537,6 +563,10 @@ describe.each([
|
|
|
537
563
|
.get(
|
|
538
564
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
539
565
|
paymentPlan,
|
|
566
|
+
)
|
|
567
|
+
.get(
|
|
568
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
569
|
+
{},
|
|
540
570
|
);
|
|
541
571
|
render(
|
|
542
572
|
<Wrapper
|
|
@@ -585,6 +615,10 @@ describe.each([
|
|
|
585
615
|
.get(
|
|
586
616
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
587
617
|
paymentPlan,
|
|
618
|
+
)
|
|
619
|
+
.get(
|
|
620
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
621
|
+
{},
|
|
588
622
|
);
|
|
589
623
|
|
|
590
624
|
render(
|
|
@@ -659,6 +693,10 @@ describe.each([
|
|
|
659
693
|
.get(
|
|
660
694
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
661
695
|
paymentPlan,
|
|
696
|
+
)
|
|
697
|
+
.get(
|
|
698
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
699
|
+
{},
|
|
662
700
|
);
|
|
663
701
|
|
|
664
702
|
render(<Wrapper paymentPlan={paymentPlan} product={product} isWithdrawable={true} />, {
|
|
@@ -679,6 +717,10 @@ describe.each([
|
|
|
679
717
|
.get(
|
|
680
718
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
681
719
|
paymentPlan,
|
|
720
|
+
)
|
|
721
|
+
.get(
|
|
722
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
723
|
+
{},
|
|
682
724
|
);
|
|
683
725
|
|
|
684
726
|
render(<Wrapper paymentPlan={paymentPlan} product={product} isWithdrawable={false} />, {
|
|
@@ -699,6 +741,10 @@ describe.each([
|
|
|
699
741
|
.get(
|
|
700
742
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
701
743
|
paymentPlan,
|
|
744
|
+
)
|
|
745
|
+
.get(
|
|
746
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
747
|
+
{},
|
|
702
748
|
);
|
|
703
749
|
|
|
704
750
|
render(<Wrapper paymentPlan={paymentPlan} product={product} isWithdrawable={true} />, {
|
|
@@ -719,6 +765,10 @@ describe.each([
|
|
|
719
765
|
.get(
|
|
720
766
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
721
767
|
paymentPlan,
|
|
768
|
+
)
|
|
769
|
+
.get(
|
|
770
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
771
|
+
{},
|
|
722
772
|
);
|
|
723
773
|
|
|
724
774
|
render(<Wrapper paymentPlan={paymentPlan} product={product} isWithdrawable={false} />, {
|
|
@@ -762,6 +812,10 @@ describe.each([
|
|
|
762
812
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
763
813
|
paymentPlan,
|
|
764
814
|
)
|
|
815
|
+
.get(
|
|
816
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
817
|
+
{},
|
|
818
|
+
)
|
|
765
819
|
.get(
|
|
766
820
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/?voucher_code=DISCOUNT30`,
|
|
767
821
|
{
|
|
@@ -807,7 +861,7 @@ describe.each([
|
|
|
807
861
|
const paymentPlanVoucher = PaymentPlanFactory({
|
|
808
862
|
discounted_price: 0.0,
|
|
809
863
|
discount: '-100%',
|
|
810
|
-
|
|
864
|
+
skip_contract_inputs: true,
|
|
811
865
|
}).one();
|
|
812
866
|
const product = ProductFactory().one();
|
|
813
867
|
fetchMock
|
|
@@ -819,6 +873,10 @@ describe.each([
|
|
|
819
873
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
820
874
|
paymentPlan,
|
|
821
875
|
)
|
|
876
|
+
.get(
|
|
877
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
878
|
+
{},
|
|
879
|
+
)
|
|
822
880
|
.get(
|
|
823
881
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/?voucher_code=DISCOUNT100`,
|
|
824
882
|
paymentPlanVoucher,
|
|
@@ -871,6 +929,10 @@ describe.each([
|
|
|
871
929
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
872
930
|
paymentPlan,
|
|
873
931
|
)
|
|
932
|
+
.get(
|
|
933
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
934
|
+
{},
|
|
935
|
+
)
|
|
874
936
|
.get(
|
|
875
937
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/?voucher_code=DISCOUNT30`,
|
|
876
938
|
paymentPlanVoucher,
|
|
@@ -940,6 +1002,7 @@ describe('SaleTunnel with Keycloak backend', () => {
|
|
|
940
1002
|
|
|
941
1003
|
richieUser = UserFactory({
|
|
942
1004
|
username: 'John Doe',
|
|
1005
|
+
full_name: 'John Doe',
|
|
943
1006
|
email: 'johndoe@example.com',
|
|
944
1007
|
}).one();
|
|
945
1008
|
|
|
@@ -999,6 +1062,10 @@ describe('SaleTunnel with Keycloak backend', () => {
|
|
|
999
1062
|
.get(
|
|
1000
1063
|
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
1001
1064
|
paymentPlan,
|
|
1065
|
+
)
|
|
1066
|
+
.get(
|
|
1067
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
1068
|
+
{},
|
|
1002
1069
|
);
|
|
1003
1070
|
|
|
1004
1071
|
render(<Wrapper product={product} isWithdrawable={true} paymentPlan={paymentPlan} />, {
|
|
@@ -1032,4 +1099,56 @@ describe('SaleTunnel with Keycloak backend', () => {
|
|
|
1032
1099
|
// No OpenEdx profile API calls should have been made
|
|
1033
1100
|
expect(fetchMock.calls().filter(([url]) => url.includes('/api/user/v1/'))).toHaveLength(0);
|
|
1034
1101
|
});
|
|
1102
|
+
|
|
1103
|
+
it('should render keycloak account info when using fonzie-keycloak backend', async () => {
|
|
1104
|
+
// Switch to fonzie-keycloak backend
|
|
1105
|
+
const ctx = require('utils/context').default;
|
|
1106
|
+
ctx.authentication.backend = 'fonzie-keycloak';
|
|
1107
|
+
|
|
1108
|
+
const product = CredentialProductFactory().one();
|
|
1109
|
+
const paymentPlan = PaymentPlanFactory().one();
|
|
1110
|
+
|
|
1111
|
+
fetchMock
|
|
1112
|
+
.get(
|
|
1113
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify({
|
|
1114
|
+
course_code: course.code,
|
|
1115
|
+
product_id: product.id,
|
|
1116
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
1117
|
+
})}`,
|
|
1118
|
+
[],
|
|
1119
|
+
)
|
|
1120
|
+
.get(
|
|
1121
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
1122
|
+
paymentPlan,
|
|
1123
|
+
)
|
|
1124
|
+
.get(
|
|
1125
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
|
|
1126
|
+
{},
|
|
1127
|
+
);
|
|
1128
|
+
|
|
1129
|
+
render(<Wrapper product={product} isWithdrawable={true} paymentPlan={paymentPlan} />, {
|
|
1130
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
// Should display the "Account name" heading (keycloak flow)
|
|
1134
|
+
await screen.findByRole('heading', { level: 4, name: 'Account name' });
|
|
1135
|
+
|
|
1136
|
+
// Should display the username from the session
|
|
1137
|
+
screen.getByText(richieUser.username);
|
|
1138
|
+
|
|
1139
|
+
// Should display the email from the session
|
|
1140
|
+
screen.getByText(richieUser.email!);
|
|
1141
|
+
|
|
1142
|
+
// Should display the keycloak account update link
|
|
1143
|
+
const updateLink = screen.getByRole('link', {
|
|
1144
|
+
name: 'please update your account',
|
|
1145
|
+
});
|
|
1146
|
+
expect(updateLink).toHaveAttribute('href', mockAccountUpdateUrl);
|
|
1147
|
+
|
|
1148
|
+
// Should NOT render the OpenEdx full name form
|
|
1149
|
+
expect(screen.queryByLabelText('First name and last name')).not.toBeInTheDocument();
|
|
1150
|
+
|
|
1151
|
+
// No OpenEdx profile API calls should have been made
|
|
1152
|
+
expect(fetchMock.calls().filter(([url]) => url.includes('/api/user/v1/'))).toHaveLength(0);
|
|
1153
|
+
});
|
|
1035
1154
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
|
+
import { OfferingDeepLink } from 'types/Joanie';
|
|
4
|
+
import { HttpError } from 'utils/errors/HttpError';
|
|
5
|
+
|
|
6
|
+
type DeepLinkFilters = {
|
|
7
|
+
course_code: string;
|
|
8
|
+
product_id: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const useDeepLink = (filters: DeepLinkFilters) => {
|
|
12
|
+
const api = useJoanieApi();
|
|
13
|
+
return useQuery<OfferingDeepLink, HttpError>({
|
|
14
|
+
queryKey: ['courses-products', ...Object.values(filters), 'deep-link'],
|
|
15
|
+
queryFn: () =>
|
|
16
|
+
api.courses.products.deepLink.get({
|
|
17
|
+
id: filters.product_id,
|
|
18
|
+
course_id: filters.course_code,
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -234,4 +234,107 @@ describe('<DashboardBatchOrders/>', () => {
|
|
|
234
234
|
|
|
235
235
|
await screen.findByText('Completed');
|
|
236
236
|
});
|
|
237
|
+
|
|
238
|
+
it('allows retrying payment after aborting the payment tunnel', async () => {
|
|
239
|
+
const batchOrder = BatchOrderReadFactory({
|
|
240
|
+
payment_method: PaymentMethod.CARD_PAYMENT,
|
|
241
|
+
state: BatchOrderState.PENDING,
|
|
242
|
+
total: 200,
|
|
243
|
+
currency: 'EUR',
|
|
244
|
+
}).one();
|
|
245
|
+
|
|
246
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/batch-orders/?page=1&page_size=${perPage}`, {
|
|
247
|
+
results: [batchOrder],
|
|
248
|
+
count: 1,
|
|
249
|
+
next: null,
|
|
250
|
+
previous: null,
|
|
251
|
+
});
|
|
252
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/batch-orders/`, [batchOrder]);
|
|
253
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/`, batchOrder);
|
|
254
|
+
|
|
255
|
+
render(<DashboardTest initialRoute={LearnerDashboardPaths.BATCH_ORDERS} />, {
|
|
256
|
+
wrapper: BaseJoanieAppWrapper,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await expectNoSpinner('Loading batch orders...');
|
|
260
|
+
await screen.findByText('Payment required');
|
|
261
|
+
|
|
262
|
+
// Open modal and start payment
|
|
263
|
+
await userEvent.click(await screen.findByRole('button', { name: 'Pay €200.00' }));
|
|
264
|
+
|
|
265
|
+
fetchMock.post(
|
|
266
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/submit-for-payment/`,
|
|
267
|
+
{ payment_id: 'payment_id', provider: 'payment_provider', url: 'payment_url' },
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const firstModal = await screen.findByRole('dialog');
|
|
271
|
+
await userEvent.click(within(firstModal).getByRole('button', { name: 'Pay €200.00' }));
|
|
272
|
+
|
|
273
|
+
// Close payment modal
|
|
274
|
+
await screen.findByTestId('payment-abort');
|
|
275
|
+
fetchMock.get(
|
|
276
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/?page=1&page_size=${perPage}`,
|
|
277
|
+
{
|
|
278
|
+
results: [{ ...batchOrder, state: BatchOrderState.PROCESS_PAYMENT }],
|
|
279
|
+
count: 1,
|
|
280
|
+
next: null,
|
|
281
|
+
previous: null,
|
|
282
|
+
},
|
|
283
|
+
{ overwriteRoutes: true },
|
|
284
|
+
);
|
|
285
|
+
fetchMock.get(
|
|
286
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/`,
|
|
287
|
+
{ ...batchOrder, state: BatchOrderState.PROCESS_PAYMENT },
|
|
288
|
+
{ overwriteRoutes: true },
|
|
289
|
+
);
|
|
290
|
+
await userEvent.click(screen.getByTestId('payment-abort'));
|
|
291
|
+
|
|
292
|
+
const modalAfterAbort = await screen.findByRole('dialog');
|
|
293
|
+
await userEvent.click(within(modalAfterAbort).getByRole('button', { name: 'close' }));
|
|
294
|
+
await waitFor(() => expect(screen.queryByRole('dialog')).not.toBeInTheDocument());
|
|
295
|
+
|
|
296
|
+
await screen.findByText('Payment required');
|
|
297
|
+
await screen.findByRole('button', { name: 'Pay €200.00' });
|
|
298
|
+
|
|
299
|
+
// Retry payment successfully
|
|
300
|
+
await userEvent.click(await screen.findByRole('button', { name: 'Pay €200.00' }));
|
|
301
|
+
|
|
302
|
+
fetchMock.post(
|
|
303
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/submit-for-payment/`,
|
|
304
|
+
{ payment_id: 'payment_id', provider: 'payment_provider', url: 'payment_url' },
|
|
305
|
+
{ overwriteRoutes: true },
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
const retryModal = await screen.findByRole('dialog');
|
|
309
|
+
await userEvent.click(within(retryModal).getByRole('button', { name: 'Pay €200.00' }));
|
|
310
|
+
|
|
311
|
+
await screen.findByTestId('payment-success');
|
|
312
|
+
|
|
313
|
+
fetchMock.get(
|
|
314
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/?page=1&page_size=${perPage}`,
|
|
315
|
+
{
|
|
316
|
+
results: [{ ...batchOrder, state: BatchOrderState.COMPLETED }],
|
|
317
|
+
count: 1,
|
|
318
|
+
next: null,
|
|
319
|
+
previous: null,
|
|
320
|
+
},
|
|
321
|
+
{ overwriteRoutes: true },
|
|
322
|
+
);
|
|
323
|
+
fetchMock.get(
|
|
324
|
+
`https://joanie.endpoint/api/v1.0/batch-orders/${batchOrder.id}/`,
|
|
325
|
+
{ ...batchOrder, state: BatchOrderState.COMPLETED },
|
|
326
|
+
{ overwriteRoutes: true },
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
await userEvent.click(screen.getByTestId('payment-success'));
|
|
330
|
+
|
|
331
|
+
await waitFor(() => {
|
|
332
|
+
expect(mockMessageModal).toHaveBeenCalledWith(
|
|
333
|
+
expect.objectContaining({ title: 'Payment successful' }),
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
expect(screen.queryByRole('button', { name: /Pay/ })).not.toBeInTheDocument();
|
|
338
|
+
await screen.findByText('Completed');
|
|
339
|
+
});
|
|
237
340
|
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { screen } from '@testing-library/dom';
|
|
2
|
+
import {
|
|
3
|
+
UserFactory,
|
|
4
|
+
RichieContextFactory as mockRichieContextFactory,
|
|
5
|
+
} from 'utils/test/factories/richie';
|
|
6
|
+
import { render } from 'utils/test/render';
|
|
7
|
+
import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
|
|
8
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
9
|
+
import { User } from 'types/User';
|
|
10
|
+
import { AuthenticationApi } from 'api/authentication';
|
|
11
|
+
import { APIAuthentication } from 'types/api';
|
|
12
|
+
import DashboardKeycloakProfile, { DEFAULT_DISPLAYED_FORM_VALUE } from '.';
|
|
13
|
+
|
|
14
|
+
jest.mock('utils/context', () => ({
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: mockRichieContextFactory({
|
|
17
|
+
authentication: {
|
|
18
|
+
endpoint: 'https://endpoint.test',
|
|
19
|
+
backend: 'fonzie',
|
|
20
|
+
},
|
|
21
|
+
joanie_backend: {
|
|
22
|
+
endpoint: 'https://joanie.endpoint',
|
|
23
|
+
},
|
|
24
|
+
}).one(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe('pages.DashboardKeycloakProfile', () => {
|
|
28
|
+
let richieUser: User;
|
|
29
|
+
let originalAccount: APIAuthentication['account'];
|
|
30
|
+
const mockAccountUpdateUrl = 'https://keycloak.test/auth/realms/richie/account';
|
|
31
|
+
setupJoanieSession();
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
richieUser = UserFactory().one();
|
|
35
|
+
originalAccount = AuthenticationApi!.account;
|
|
36
|
+
AuthenticationApi!.account = {
|
|
37
|
+
get: () => ({
|
|
38
|
+
username: richieUser.username,
|
|
39
|
+
email: richieUser.email,
|
|
40
|
+
firstName: null,
|
|
41
|
+
lastName: null,
|
|
42
|
+
}),
|
|
43
|
+
updateUrl: () => mockAccountUpdateUrl,
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
AuthenticationApi!.account = originalAccount;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should render profile information with full_name', async () => {
|
|
52
|
+
render(<DashboardKeycloakProfile />, {
|
|
53
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(screen.getByText('Profile')).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText('Account information')).toBeInTheDocument();
|
|
58
|
+
|
|
59
|
+
expect(await screen.findByDisplayValue(richieUser.full_name!)).toBeInTheDocument();
|
|
60
|
+
expect(screen.getByDisplayValue(richieUser.email!)).toBeInTheDocument();
|
|
61
|
+
|
|
62
|
+
const editLink = screen.getByRole('link', { name: 'Edit your profile' });
|
|
63
|
+
expect(editLink).toBeInTheDocument();
|
|
64
|
+
expect(editLink).toHaveAttribute('href', mockAccountUpdateUrl);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should fallback to username when full_name is empty', async () => {
|
|
68
|
+
const userWithoutFullName = UserFactory({ full_name: undefined, email: undefined }).one();
|
|
69
|
+
|
|
70
|
+
render(<DashboardKeycloakProfile />, {
|
|
71
|
+
queryOptions: { client: createTestQueryClient({ user: userWithoutFullName }) },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(await screen.findByDisplayValue(userWithoutFullName.username)).toBeInTheDocument();
|
|
75
|
+
expect(screen.getByLabelText('Account email')).toHaveValue(DEFAULT_DISPLAYED_FORM_VALUE);
|
|
76
|
+
});
|
|
77
|
+
});
|