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.
Files changed (52) hide show
  1. package/i18n/locales/ar-SA.json +29 -13
  2. package/i18n/locales/es-ES.json +29 -13
  3. package/i18n/locales/fa-IR.json +29 -13
  4. package/i18n/locales/fr-CA.json +29 -13
  5. package/i18n/locales/fr-FR.json +29 -13
  6. package/i18n/locales/ko-KR.json +29 -13
  7. package/i18n/locales/pt-PT.json +29 -13
  8. package/i18n/locales/ru-RU.json +29 -13
  9. package/i18n/locales/vi-VN.json +29 -13
  10. package/js/api/auth/keycloak.spec.ts +1 -0
  11. package/js/api/auth/keycloak.ts +5 -1
  12. package/js/api/joanie.ts +20 -0
  13. package/js/api/lms/openedx-fonzie-keycloak.spec.ts +35 -2
  14. package/js/api/lms/openedx-fonzie-keycloak.ts +26 -0
  15. package/js/api/lms/openedx-hawthorn.spec.ts +34 -2
  16. package/js/api/lms/openedx-hawthorn.ts +4 -1
  17. package/js/components/PurchaseButton/index.spec.tsx +12 -0
  18. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +3 -0
  19. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +11 -0
  20. package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationSingular.tsx +141 -52
  21. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +3 -2
  22. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +6 -1
  23. package/js/components/SaleTunnel/index.credential.spec.tsx +108 -1
  24. package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +5 -1
  25. package/js/components/SaleTunnel/index.full-process-b2c.spec.tsx +9 -0
  26. package/js/components/SaleTunnel/index.spec.tsx +122 -3
  27. package/js/hooks/useDeepLink.tsx +21 -0
  28. package/js/pages/DashboardBatchOrders/index.spec.tsx +103 -0
  29. package/js/pages/DashboardKeycloakProfile/index.spec.tsx +77 -0
  30. package/js/pages/DashboardKeycloakProfile/index.tsx +93 -0
  31. package/js/pages/DashboardPreferences/index.spec.tsx +141 -0
  32. package/js/pages/DashboardPreferences/index.tsx +7 -1
  33. package/js/translations/ar-SA.json +1 -1
  34. package/js/translations/es-ES.json +1 -1
  35. package/js/translations/fa-IR.json +1 -1
  36. package/js/translations/fr-CA.json +1 -1
  37. package/js/translations/fr-FR.json +1 -1
  38. package/js/translations/ko-KR.json +1 -1
  39. package/js/translations/pt-PT.json +1 -1
  40. package/js/translations/ru-RU.json +1 -1
  41. package/js/translations/vi-VN.json +1 -1
  42. package/js/types/Joanie.ts +8 -1
  43. package/js/utils/test/factories/joanie.ts +8 -2
  44. package/js/widgets/Dashboard/components/DashboardItem/BatchOrder/BatchOrderPaymentModal/BatchOrderPaymentManager.tsx +15 -27
  45. package/js/widgets/Dashboard/components/LearnerDashboardSidebar/index.tsx +7 -12
  46. package/js/widgets/Dashboard/index.spec.tsx +4 -3
  47. package/js/widgets/SyllabusCourseRunsList/components/SyllabusAsideList/index.tsx +8 -27
  48. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +41 -17
  49. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +37 -4
  50. package/js/widgets/cunningham-fr-FR-locale.json +80 -0
  51. package/js/widgets/index.tsx +6 -1
  52. 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
- from_batch_order: true,
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 += 3;
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 += 3;
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
- from_batch_order: true,
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
+ });