richie-education 2.28.2-dev39 → 2.28.2-dev53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/js/api/joanie.ts +12 -16
  2. package/js/api/lms/dummy.ts +1 -12
  3. package/js/components/ContractFrame/AbstractContractFrame.spec.tsx +16 -9
  4. package/js/components/ContractFrame/AbstractContractFrame.tsx +28 -23
  5. package/js/components/ContractFrame/LearnerContractFrame.tsx +2 -2
  6. package/js/components/ContractFrame/_styles.scss +6 -14
  7. package/js/components/CreditCardSelector/index.spec.tsx +7 -7
  8. package/js/components/CreditCardSelector/index.tsx +2 -2
  9. package/js/components/DownloadContractButton/index.spec.tsx +1 -1
  10. package/js/components/OpenEdxFullNameForm/index.spec.tsx +229 -0
  11. package/js/components/OpenEdxFullNameForm/index.tsx +7 -7
  12. package/js/components/PaymentInterfaces/LyraPopIn.tsx +2 -2
  13. package/js/components/PaymentInterfaces/PayplugLightbox.tsx +1 -1
  14. package/js/components/PaymentInterfaces/__mocks__/index.tsx +1 -4
  15. package/js/components/PaymentInterfaces/types.ts +5 -2
  16. package/js/components/PurchaseButton/index.spec.tsx +69 -37
  17. package/js/components/SaleTunnel/AddressSelector/index.spec.tsx +2 -1
  18. package/js/components/SaleTunnel/CertificateSaleTunnel/index.tsx +2 -2
  19. package/js/components/SaleTunnel/CredentialSaleTunnel/index.tsx +6 -10
  20. package/js/components/SaleTunnel/GenericSaleTunnel.tsx +75 -41
  21. package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +0 -30
  22. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/_styles.scss +12 -0
  23. package/js/components/SaleTunnel/SaleTunnelSavePaymentMethod/index.tsx +160 -0
  24. package/js/components/SaleTunnel/SaleTunnelSuccess/index.tsx +15 -29
  25. package/js/components/SaleTunnel/Sponsors/SaleTunnelSponsors.tsx +5 -0
  26. package/js/components/SaleTunnel/SubscriptionButton/_styles.scss +7 -0
  27. package/js/components/SaleTunnel/SubscriptionButton/index.tsx +201 -0
  28. package/js/components/SaleTunnel/_styles.scss +10 -1
  29. package/js/components/SaleTunnel/hooks/useTerms.tsx +0 -77
  30. package/js/components/SaleTunnel/index.credential.spec.tsx +12 -21
  31. package/js/components/SaleTunnel/index.full-process.spec.tsx +110 -48
  32. package/js/components/SaleTunnel/index.spec.tsx +330 -779
  33. package/js/components/SignContractButton/index.omniscientOrders.spec.tsx +16 -11
  34. package/js/components/SignContractButton/index.spec.tsx +16 -20
  35. package/js/components/SignContractButton/index.tsx +3 -1
  36. package/js/hooks/useCreditCards/index.spec.tsx +70 -6
  37. package/js/hooks/useCreditCards/index.ts +49 -11
  38. package/js/hooks/useOrders/index.spec.tsx +322 -0
  39. package/js/hooks/{useOrders.ts → useOrders/index.ts} +40 -14
  40. package/js/hooks/useProductOrder/index.spec.tsx +77 -60
  41. package/js/hooks/useProductOrder/index.tsx +2 -2
  42. package/js/hooks/useResources/useResourcesRoot.ts +1 -0
  43. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.spec.tsx +1 -1
  44. package/js/pages/DashboardCreditCardsManagement/CreditCardBrandLogo.tsx +4 -2
  45. package/js/pages/TeacherDashboardContractsLayout/components/ContractActionsBar/index.spec.tsx +8 -5
  46. package/js/pages/TeacherDashboardContractsLayout/components/SignOrganizationContractButton/index.spec.tsx +8 -9
  47. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.spec.tsx +1 -1
  48. package/js/pages/TeacherDashboardCourseLearnersLayout/components/CourseLearnerDataGrid/index.tsx +1 -6
  49. package/js/settings/settings.test.ts +11 -2
  50. package/js/types/Joanie.ts +49 -34
  51. package/js/utils/OrderHelper/index.ts +38 -42
  52. package/js/utils/test/factories/joanie.ts +36 -51
  53. package/js/widgets/Dashboard/components/DashboardItem/CourseEnrolling/index.tsx +8 -18
  54. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +26 -32
  55. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -6
  56. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +7 -6
  57. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +9 -10
  58. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.spec.tsx +3 -1
  59. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +6 -7
  60. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +28 -8
  61. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal/index.tsx +2 -5
  62. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +18 -71
  63. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +34 -35
  64. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +27 -24
  65. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +18 -73
  66. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +32 -16
  67. package/js/widgets/Dashboard/components/DashboardOrderLoader/index.tsx +3 -11
  68. package/js/widgets/Dashboard/components/Signature/SignatureDummy.tsx +25 -3
  69. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/EnrollableCourseRunList.tsx +2 -6
  70. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseProductCourseRuns/index.spec.tsx +7 -14
  71. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.spec.tsx +7 -5
  72. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/CourseRunItem/index.tsx +5 -7
  73. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +242 -332
  74. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +12 -13
  75. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +10 -21
  76. package/js/widgets/SyllabusCourseRunsList/components/CourseRunEnrollment/index.joanie.spec.tsx +2 -2
  77. package/package.json +1 -1
  78. package/scss/components/_index.scss +2 -1
  79. package/js/components/PaymentButton/_styles.scss +0 -27
  80. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +0 -333
  81. package/js/components/SaleTunnel/SaleTunnelNotValidated/index.tsx +0 -70
  82. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/components/ProductSignatureHeader/index.tsx +0 -41
@@ -1,40 +1,38 @@
1
- import { act, cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
1
+ import { act, cleanup, screen, waitFor } from '@testing-library/react';
2
2
  import fetchMock from 'fetch-mock';
3
3
  import queryString from 'query-string';
4
4
  import userEvent from '@testing-library/user-event';
5
5
  import { within } from '@testing-library/dom';
6
6
  import { createIntl } from 'react-intl';
7
- import { OrderState, Product, ProductType } from 'types/Joanie';
7
+ import { useState } from 'react';
8
+ import { OrderState, Product, ProductType, NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
9
+ import {
10
+ RichieContextFactory as mockRichieContextFactory,
11
+ UserFactory,
12
+ PacedCourseFactory,
13
+ } from 'utils/test/factories/richie';
8
14
  import {
9
15
  AddressFactory,
10
- CertificateOrderWithOneClickPaymentFactory,
11
- CertificateOrderWithPaymentFactory,
16
+ CertificateOrderFactory,
12
17
  CertificateProductFactory,
13
- CredentialOrderWithOneClickPaymentFactory,
14
- CredentialOrderWithPaymentFactory,
18
+ CredentialOrderFactory,
15
19
  CredentialProductFactory,
16
20
  CreditCardFactory,
17
21
  EnrollmentFactory,
18
22
  PaymentInstallmentFactory,
19
23
  } from 'utils/test/factories/joanie';
20
- import {
21
- RichieContextFactory as mockRichieContextFactory,
22
- PacedCourseFactory,
23
- UserFactory,
24
- } from 'utils/test/factories/richie';
25
24
  import { render } from 'utils/test/render';
26
25
  import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel/index';
27
26
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
28
- import { HttpStatusCode } from 'utils/errors/HttpError';
27
+ import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
29
28
  import { getAddressLabel } from 'components/SaleTunnel/AddressSelector';
30
- import { ObjectHelper } from 'utils/ObjectHelper';
31
- import { PAYMENT_SETTINGS } from 'settings';
32
29
  import { User } from 'types/User';
33
30
  import { OpenEdxApiProfile } from 'types/openEdx';
34
31
  import { OpenEdxApiProfileFactory } from 'utils/test/factories/openEdx';
35
32
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
36
33
  import { StringHelper } from 'utils/StringHelper';
37
34
  import { DEFAULT_DATE_FORMAT } from 'hooks/useDateFormat';
35
+ import { Deferred } from 'utils/test/deferred';
38
36
 
39
37
  jest.mock('utils/context', () => ({
40
38
  __esModule: true,
@@ -55,648 +53,322 @@ jest.mock('utils/indirection/window', () => ({
55
53
  }),
56
54
  }));
57
55
 
58
- jest.mock('../PaymentInterfaces');
56
+ jest.mock('./SaleTunnelSavePaymentMethod', () => ({
57
+ __esModule: true,
58
+ default: () => <div data-testid="sale-tunnel-save-payment-method-step" />,
59
+ }));
60
+ jest.mock('components/ContractFrame/LearnerContractFrame', () => ({
61
+ __esModule: true,
62
+ default: () => <div data-testid="sale-tunnel-sign-step" />,
63
+ }));
59
64
 
60
65
  describe.each([
61
66
  {
62
67
  productType: ProductType.CREDENTIAL,
63
68
  ProductFactory: CredentialProductFactory,
64
- OrderWithOneClickPaymentFactory: CredentialOrderWithOneClickPaymentFactory,
65
- OrderWithPaymentFactory: CredentialOrderWithPaymentFactory,
69
+ OrderFactory: CredentialOrderFactory,
66
70
  },
67
71
  {
68
72
  productType: ProductType.CERTIFICATE,
69
73
  ProductFactory: CertificateProductFactory,
70
- OrderWithOneClickPaymentFactory: CertificateOrderWithOneClickPaymentFactory,
71
- OrderWithPaymentFactory: CertificateOrderWithPaymentFactory,
74
+ OrderFactory: CertificateOrderFactory,
72
75
  },
73
- ])(
74
- 'SaleTunnel for $productType product',
75
- ({ productType, ProductFactory, OrderWithOneClickPaymentFactory, OrderWithPaymentFactory }) => {
76
- let nbApiCalls: number;
77
-
78
- const course = PacedCourseFactory().one();
79
- const enrollment =
80
- productType === ProductType.CERTIFICATE
81
- ? EnrollmentFactory({ course_run: { course } }).one()
82
- : undefined;
83
-
84
- let richieUser: User;
85
- let openApiEdxProfile: OpenEdxApiProfile;
86
-
87
- const formatPrice = (price: number, currency: string) =>
88
- new Intl.NumberFormat('en', {
89
- currency,
90
- style: 'currency',
91
- }).format(price);
92
-
93
- const Wrapper = (props: Omit<SaleTunnelProps, 'isOpen' | 'onClose'>) => {
94
- return (
95
- <SaleTunnel
96
- {...props}
97
- enrollment={enrollment}
98
- course={productType === ProductType.CREDENTIAL ? course : undefined}
99
- isOpen={true}
100
- onClose={() => {}}
101
- />
102
- );
103
- };
104
-
105
- beforeEach(() => {
106
- jest.useFakeTimers();
107
- jest.clearAllTimers();
108
- jest.resetAllMocks();
109
-
110
- fetchMock.restore();
111
- sessionStorage.clear();
112
-
113
- nbApiCalls = 3;
114
-
115
- richieUser = UserFactory().one();
116
- openApiEdxProfile = OpenEdxApiProfileFactory({
117
- username: richieUser.username,
118
- email: richieUser.email,
119
- name: richieUser.full_name,
120
- }).one();
121
-
122
- const { 'pref-lang': prefLang, ...openEdxAccount } = openApiEdxProfile;
123
-
124
- fetchMock.get(
125
- `https://auth.test/api/user/v1/accounts/${richieUser.username}`,
126
- openEdxAccount,
127
- );
128
- fetchMock.get(`https://auth.test/api/user/v1/preferences/${richieUser.username}`, {
129
- 'pref-lang': prefLang,
130
- });
76
+ ])('SaleTunnel for $productType product', ({ productType, ProductFactory, OrderFactory }) => {
77
+ let nbApiCalls: number;
78
+
79
+ const course = PacedCourseFactory().one();
80
+ const enrollment =
81
+ productType === ProductType.CERTIFICATE
82
+ ? EnrollmentFactory({ course_run: { course } }).one()
83
+ : undefined;
84
+
85
+ let richieUser: User;
86
+ let openApiEdxProfile: OpenEdxApiProfile;
87
+
88
+ const formatPrice = (price: number, currency: string) =>
89
+ new Intl.NumberFormat('en', {
90
+ currency,
91
+ style: 'currency',
92
+ }).format(price);
93
+
94
+ const Wrapper = (props: Omit<SaleTunnelProps, 'isOpen' | 'onClose'>) => {
95
+ const [open, setOpen] = useState(true);
96
+ return (
97
+ <SaleTunnel
98
+ {...props}
99
+ enrollment={enrollment}
100
+ course={productType === ProductType.CREDENTIAL ? course : undefined}
101
+ isOpen={open}
102
+ onClose={() => setOpen(false)}
103
+ />
104
+ );
105
+ };
106
+
107
+ beforeEach(() => {
108
+ jest.useFakeTimers();
109
+ jest.clearAllTimers();
110
+ jest.resetAllMocks();
111
+
112
+ fetchMock.restore();
113
+ sessionStorage.clear();
114
+
115
+ nbApiCalls = 3;
116
+
117
+ richieUser = UserFactory().one();
118
+ openApiEdxProfile = OpenEdxApiProfileFactory({
119
+ username: richieUser.username,
120
+ email: richieUser.email,
121
+ name: richieUser.full_name,
122
+ }).one();
123
+
124
+ const { 'pref-lang': prefLang, ...openEdxAccount } = openApiEdxProfile;
125
+
126
+ fetchMock.get(`https://auth.test/api/user/v1/accounts/${richieUser.username}`, openEdxAccount);
127
+ fetchMock.get(`https://auth.test/api/user/v1/preferences/${richieUser.username}`, {
128
+ 'pref-lang': prefLang,
131
129
  });
130
+ });
132
131
 
133
- setupJoanieSession();
132
+ setupJoanieSession();
134
133
 
135
- afterEach(() => {
136
- act(() => {
137
- jest.runOnlyPendingTimers();
138
- });
139
- jest.useRealTimers();
140
- cleanup();
134
+ afterEach(() => {
135
+ act(() => {
136
+ jest.runOnlyPendingTimers();
141
137
  });
142
-
143
- const getFetchOrderQueryParams = (product: Product) => {
144
- return product.type === ProductType.CREDENTIAL
145
- ? {
146
- course_code: course.code,
147
- product_id: product.id,
148
- state: ['pending', 'validated', 'submitted'],
149
- }
150
- : {
151
- enrollment_id: enrollment?.id,
152
- product_id: product.id,
153
- state: ['pending', 'validated', 'submitted'],
154
- };
155
- };
156
-
157
- it('should create an order only the first time the payment interface is shown, and not after aborting', async () => {
158
- const product = ProductFactory().one();
159
- const billingAddress = AddressFactory({
160
- is_main: true,
161
- }).one();
162
- const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
163
-
164
- fetchMock
165
- .get(
166
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
167
- [],
168
- )
169
- .get(
170
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
171
- [],
172
- )
173
- .post('https://joanie.endpoint/api/v1.0/orders/', order)
174
- .patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
175
- paymentInfo,
176
- })
177
- .get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, {
178
- ...order,
179
- })
180
- .post(`https://joanie.endpoint/api/v1.0/orders/${order.id}/abort/`, HttpStatusCode.OK)
181
- .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
182
- overwriteRoutes: true,
183
- });
184
-
185
- render(<Wrapper product={product} />, {
186
- queryOptions: { client: createTestQueryClient({ user: richieUser }) },
187
- });
188
- nbApiCalls += 1; // useProductOrder call.
189
- nbApiCalls += 1; // get user account call.
190
- nbApiCalls += 1; // get user preferences call.
191
- await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
192
-
193
- const $terms = screen.getByLabelText(
194
- 'By checking this box, you accept the General Terms of Sale',
195
- );
196
- const user = userEvent.setup({ delay: null });
197
- await user.click($terms);
198
-
199
- const $button = screen.getByRole<HTMLButtonElement>('button', {
200
- name: `Subscribe`,
201
- });
202
- nbApiCalls += 1; // product payment-schedule call
203
-
204
- // - Payment button should not be disabled.
205
- expect($button.disabled).toBe(false);
206
-
207
- // - wait for address to be loaded.
208
- await screen.findByText(getAddressLabel(billingAddress));
209
-
210
- // - Order should not have been submitted yet
211
- expect(
212
- fetchMock
213
- .calls()
214
- .filter(
215
- (call) => call[0] === `https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`,
216
- ),
217
- ).toHaveLength(0);
218
-
219
- // - User clicks on pay button
220
- await user.click($button);
221
-
222
- // - Route to create order should have been called
223
- nbApiCalls += 1; // order post create (invalidate queries)
224
- nbApiCalls += 1; // order get (invalidate queries)
225
- nbApiCalls += 1; // useProductOrder call (invalidate from create)
226
- nbApiCalls += 1; // order submit (invalidate queries)
227
- nbApiCalls += 1; // order get (invalidate queries)
228
- nbApiCalls += 1; // useProductOrder call (invalidate from submit)
229
-
230
- await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
231
- expect(fetchMock.lastUrl()).toBe(
138
+ jest.useRealTimers();
139
+ cleanup();
140
+ });
141
+
142
+ const getFetchOrderQueryParams = (product: Product) => {
143
+ return product.type === ProductType.CREDENTIAL
144
+ ? {
145
+ course_code: course.code,
146
+ product_id: product.id,
147
+ state: NOT_CANCELED_ORDER_STATES,
148
+ }
149
+ : {
150
+ enrollment_id: enrollment?.id,
151
+ product_id: product.id,
152
+ state: NOT_CANCELED_ORDER_STATES,
153
+ };
154
+ };
155
+
156
+ it('should create an order when the user clicks on subscribe button', async () => {
157
+ const product = ProductFactory().one();
158
+ const billingAddress = AddressFactory({
159
+ is_main: true,
160
+ }).one();
161
+ const order = OrderFactory({ state: OrderState.TO_SAVE_PAYMENT_METHOD }).one();
162
+
163
+ fetchMock
164
+ .get(
232
165
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
233
- );
234
-
235
- // - Order should have been submitted once
236
- expect(
237
- fetchMock
238
- .calls()
239
- .filter(
240
- (call) => call[0] === `https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`,
241
- ),
242
- ).toHaveLength(1);
243
-
244
- // - Spinner should be displayed
245
- screen.getByText('Payment in progress');
246
-
247
- // - Payment interface should be displayed
248
- screen.getByText('Payment interface component');
249
-
250
- // - Simulate the payment aborting.
251
- fetchMock.get(
252
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
253
- [
254
- {
255
- ...order,
256
- state: OrderState.PENDING,
257
- },
258
- ],
259
- { overwriteRoutes: true },
260
- );
261
-
262
- await user.click(screen.getByTestId('payment-abort'));
263
-
264
- nbApiCalls += 1; // abort order.
265
- nbApiCalls += 1; // order get (invalidate queries)
266
- nbApiCalls += 1; // useProductOrder call (invalidate from create)
267
-
268
- await waitFor(() => {
269
- expect(fetchMock.calls()).toHaveLength(nbApiCalls);
166
+ [],
167
+ )
168
+ .get(
169
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
170
+ [],
171
+ )
172
+ .post('https://joanie.endpoint/api/v1.0/orders/', order)
173
+ .get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, order)
174
+ .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
175
+ overwriteRoutes: true,
270
176
  });
271
- expect(fetchMock.calls()[fetchMock.calls().length - 3][0]).toBe(
272
- `https://joanie.endpoint/api/v1.0/orders/${order.id}/abort/`,
273
- );
274
- expect(fetchMock.calls()[fetchMock.calls().length - 1][0]).toBe(
275
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
276
- );
277
-
278
- screen.getByText('You have aborted the payment.');
279
-
280
- // - User clicks on pay button again.
281
- await user.click($button);
282
-
283
- // - Spinner should be displayed
284
- await screen.findByText('Payment in progress');
285
-
286
- // - Payment interface should be displayed
287
- await screen.findByText('Payment interface component');
288
177
 
289
- // - Now we make sure the order is not created again and just submitted.
290
- nbApiCalls += 1; // order submit (invalidate queries)
291
- nbApiCalls += 1; // order get (invalidate queries)
292
- nbApiCalls += 1; // useProductOrder call (invalidate from submit)
178
+ render(<Wrapper product={product} />, {
179
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
180
+ });
181
+ nbApiCalls += 1; // useProductOrder call.
182
+ nbApiCalls += 1; // get user account call.
183
+ nbApiCalls += 1; // get user preferences call.
184
+ nbApiCalls += 1; // product payment-schedule call
185
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
293
186
 
294
- await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
187
+ const user = userEvent.setup({ delay: null });
295
188
 
296
- // - Order should not have been re-submitted
297
- expect(
298
- fetchMock
299
- .calls()
300
- .filter(
301
- (call) => call[0] === `https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`,
302
- ),
303
- ).toHaveLength(2);
304
- expect(fetchMock.lastUrl()).toBe(
305
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
306
- );
189
+ const $button = screen.getByRole<HTMLButtonElement>('button', {
190
+ name: `Subscribe`,
307
191
  });
308
192
 
309
- it('should render a payment button and not call the order creation route when there is a pending order', async () => {
310
- const product = ProductFactory().one();
311
- const billingAddress = AddressFactory({
312
- is_main: true,
313
- }).one();
314
- const creditCard = CreditCardFactory().one();
315
- const { payment_info: paymentInfo, ...order } = OrderWithOneClickPaymentFactory().one();
193
+ // - wait for address to be loaded.
194
+ await screen.findByText(getAddressLabel(billingAddress));
316
195
 
317
- const initialOrder = {
318
- ...order,
319
- state: OrderState.PENDING,
320
- };
321
- const orderSubmitted = {
322
- ...order,
323
- state: OrderState.SUBMITTED,
324
- };
196
+ // - Subscribe button should not be disabled.
197
+ expect($button.disabled).toBe(false);
325
198
 
199
+ // - Order should not have been created yet
200
+ expect(
326
201
  fetchMock
327
- .get(
328
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
329
- [initialOrder],
330
- )
331
- .get(
332
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
333
- [],
334
- )
335
- .post('https://joanie.endpoint/api/v1.0/orders/', order)
336
- .patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
337
- payment_info: paymentInfo,
338
- })
339
- .get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, orderSubmitted)
340
- .get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard], {
341
- overwriteRoutes: true,
342
- })
343
- .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
344
- overwriteRoutes: true,
345
- });
346
-
347
- render(<Wrapper product={product} />, {
348
- queryOptions: { client: createTestQueryClient({ user: richieUser }) },
349
- });
350
- await waitFor(() => {
351
- expect(screen.getByTestId('payment-button-order-loaded')).toBeInTheDocument();
352
- });
353
-
354
- const user = userEvent.setup({ delay: null });
355
- const $terms = screen.getByLabelText(
356
- 'By checking this box, you accept the General Terms of Sale',
357
- );
358
- await user.click($terms);
359
-
360
- nbApiCalls += 1; // useProductOrder get order with filters
361
- nbApiCalls += 1; // get user account call.
362
- nbApiCalls += 1; // get user preferences call.
363
- nbApiCalls += 1; // product payment schedule call.
364
- await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
365
- const $button = screen.getByRole('button', {
366
- name: `Subscribe`,
367
- }) as HTMLButtonElement;
368
-
369
- // - Payment button should not be disabled.
370
- expect($button.disabled).toBe(false);
371
-
372
- // - wait for address to be loaded.
373
- await screen.findByText(getAddressLabel(billingAddress));
374
-
375
- // - User clicks on Subscribe button
376
- fetchMock.get(
377
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
378
- [orderSubmitted],
379
- { overwriteRoutes: true },
380
- );
381
- await user.click($button);
382
-
383
- // - In real world condition the success callback is immediately called for one click payments.
384
- // - but here we need to click manually.
385
- const $success = await screen.findByTestId('payment-success');
386
- await user.click($success);
387
-
388
- // - Route to submit an existing order
389
- // - Furthermore, as payment succeeded immediately, order should have been refetched
390
- nbApiCalls += 1; // order submit
391
- nbApiCalls += 1; // order get (invalidate queries)
392
- nbApiCalls += 1; // useProductOrder call (invalidate from submit)
393
- nbApiCalls += 1; // order get on id
394
- expect(fetchMock.calls()).toHaveLength(nbApiCalls);
395
-
396
- const submitCall = fetchMock
397
202
  .calls()
398
- .find((call) => call[0] === `https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`);
399
- expect(submitCall).not.toBeUndefined();
400
- expect(JSON.parse(submitCall![1]!.body as string)).toEqual({
401
- billing_address: ObjectHelper.omit(billingAddress, 'id', 'is_main'),
402
- credit_card_id: creditCard.id,
403
- });
404
- expect(fetchMock.lastUrl()).toBe(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`);
405
-
406
- // - Spinner should be displayed
407
- screen.getByText('Payment in progress');
408
-
409
- // - Order should be polled until its state is validated
410
- fetchMock.get(
411
- `https://joanie.endpoint/api/v1.0/orders/${order.id}/`,
412
- {
413
- ...order,
414
- state: OrderState.VALIDATED,
415
- },
416
- {
417
- overwriteRoutes: true,
418
- },
419
- );
420
-
421
- // - Advance timer to one tick
422
- await act(async () => {
423
- jest.runOnlyPendingTimers();
424
- });
425
-
426
- // - Order should have been refetched
427
- nbApiCalls += 1; // order get on id
428
- nbApiCalls += 1; // orders get (invalidate queries)
429
- nbApiCalls += 1; // useProductOrder call (invalidate from success)
430
- nbApiCalls += 1; // orders get (invalidate queries)
431
- expect(fetchMock.calls()).toHaveLength(nbApiCalls);
432
-
433
- // - As order state is validated, success step is displayed
434
- screen.getByTestId('generic-sale-tunnel-success-step');
435
- screen.getByText('Congratulations!');
436
- // - And poller should be stopped
437
- await act(async () => {
438
- jest.runOnlyPendingTimers();
439
- });
440
- expect(fetchMock.calls()).toHaveLength(nbApiCalls);
441
- });
442
-
443
- it('should render SaleTunnelNotValidated if order is not validated after a given delay', async () => {
444
- const product = ProductFactory().one();
445
- const billingAddress = AddressFactory({
446
- is_main: true,
447
- }).one();
448
- const creditCard = CreditCardFactory().one();
449
- const { payment_info: paymentInfo, ...order } = OrderWithOneClickPaymentFactory().one();
450
- const orderSubmitted = {
451
- ...order,
452
- state: OrderState.SUBMITTED,
453
- };
454
-
203
+ .filter(
204
+ ([url, metadata]) =>
205
+ url === 'https://joanie.endpoint/api/v1.0/orders/' && metadata?.method === 'POST',
206
+ ),
207
+ ).toHaveLength(0);
208
+
209
+ // - User clicks on pay button
210
+ await user.click($button);
211
+
212
+ fetchMock.get(
213
+ `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
214
+ [order],
215
+ { overwriteRoutes: true },
216
+ );
217
+
218
+ // - Route to create order should have been called
219
+ nbApiCalls += 1; // order create
220
+ nbApiCalls += 1; // order get (invalidate queries)
221
+ nbApiCalls += 1; // useProductOrder call (invalidate from create)
222
+
223
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
224
+ expect(fetchMock.lastUrl()).toBe(
225
+ `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
226
+ );
227
+
228
+ // - Order should have been created once
229
+ expect(
455
230
  fetchMock
456
- .get(
457
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
458
- [orderSubmitted],
459
- )
460
- .get(
461
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
462
- [],
463
- )
464
- .post('https://joanie.endpoint/api/v1.0/orders/', order)
465
- .patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
466
- payment_info: paymentInfo,
467
- })
468
- .get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, orderSubmitted)
469
- .post(`https://joanie.endpoint/api/v1.0/orders/${order.id}/abort/`, HttpStatusCode.OK)
470
- .get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard], {
471
- overwriteRoutes: true,
472
- })
473
- .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
474
- overwriteRoutes: true,
475
- });
476
-
477
- render(<Wrapper product={product} />, {
478
- queryOptions: { client: createTestQueryClient({ user: richieUser }) },
479
- });
480
- await waitFor(() => {
481
- expect(screen.getByTestId('payment-button-order-loaded')).toBeInTheDocument();
482
- });
483
- nbApiCalls += 1; // fetcher order for userProductOrder
484
- nbApiCalls += 1; // get user account call.
485
- nbApiCalls += 1; // get user preferences call.
486
- nbApiCalls += 1; // get product payment schedule.
487
- const apiCalls = fetchMock.calls().map((call) => call[0]);
488
- expect(apiCalls).toContain(
489
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
490
- );
491
-
492
- const $terms = screen.getByLabelText(
493
- 'By checking this box, you accept the General Terms of Sale',
494
- );
495
- const user = userEvent.setup({ delay: null });
496
- await user.click($terms);
497
-
498
- const $button = screen.getByRole('button', {
499
- name: `Subscribe`,
500
- }) as HTMLButtonElement;
501
-
502
- // - wait for address to be loaded.
503
- await screen.findByText(getAddressLabel(billingAddress));
504
-
505
- // - Payment button should not be disabled.
506
- expect($button.disabled).toBe(false);
507
-
508
- // - User clicks on pay button
509
- await user.click($button);
510
-
511
- // - In real world condition the success callback is immediately called for one click payments.
512
- // - but here we need to click manually.
513
- const $success = await screen.findByTestId('payment-success');
514
- await user.click($success);
515
-
516
- // - Route to create order should have been called
517
- // - Furthermore, as payment succeeded immediately, order should have been refetched
518
- const onClickApiCalls = fetchMock.calls().splice(nbApiCalls);
519
- nbApiCalls += 1; // order submit
520
- nbApiCalls += 1; // orders get (invalidate queries)
521
- nbApiCalls += 1; // refetch order (submit invalidate)
522
- nbApiCalls += 1; // fetch validated order
523
- expect(fetchMock.calls()).toHaveLength(nbApiCalls);
524
-
525
- expect(onClickApiCalls[0][0]).toBe(
526
- `https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`,
527
- );
528
- expect(JSON.parse(onClickApiCalls[0][1]!.body as string)).toEqual({
529
- billing_address: ObjectHelper.omit(billingAddress, 'id', 'is_main'),
530
- credit_card_id: creditCard.id,
531
- });
532
- expect(onClickApiCalls[2][0]).toBe(
231
+ .calls()
232
+ .filter(
233
+ ([url, metadata]) =>
234
+ url === 'https://joanie.endpoint/api/v1.0/orders/' && metadata?.method === 'POST',
235
+ ),
236
+ ).toHaveLength(1);
237
+
238
+ // - Spinner should be displayed
239
+ screen.getByText('Order creation in progress');
240
+
241
+ // - Save payment step should be displayed
242
+ await screen.findByTestId('sale-tunnel-save-payment-method-step');
243
+ });
244
+
245
+ it('should render an error message when order creation fails', async () => {
246
+ const product = ProductFactory().one();
247
+ const billingAddress = AddressFactory({
248
+ is_main: true,
249
+ }).one();
250
+ const deferred = new Deferred();
251
+
252
+ fetchMock
253
+ .get(
533
254
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
534
- );
535
- expect(onClickApiCalls[3][0]).toBe(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`);
536
-
537
- // - Spinner should be displayed
538
- screen.getByText('Payment in progress');
539
-
540
- fetchMock.resetHistory();
541
- nbApiCalls = 0;
542
- // - Wait until order has been polled 29 times.
543
- await act(async () => {
544
- await jest.advanceTimersByTimeAsync(
545
- (PAYMENT_SETTINGS.pollLimit - 1) * PAYMENT_SETTINGS.pollInterval,
546
- );
255
+ [],
256
+ )
257
+ .get(
258
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
259
+ [],
260
+ )
261
+ .post('https://joanie.endpoint/api/v1.0/orders/', deferred.promise)
262
+ .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
263
+ overwriteRoutes: true,
547
264
  });
548
265
 
549
- await waitFor(async () => {
550
- expect(fetchMock.calls()).toHaveLength(PAYMENT_SETTINGS.pollLimit - 1);
551
- nbApiCalls += PAYMENT_SETTINGS.pollLimit - 1;
552
- });
266
+ render(<Wrapper product={product} />, {
267
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
268
+ });
269
+ nbApiCalls += 1; // useProductOrder get order with filters
270
+ nbApiCalls += 1; // get user account call.
271
+ nbApiCalls += 1; // get user preferences call.
272
+ nbApiCalls += 1; // get product payment schedule.
273
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
553
274
 
554
- // - This round should be the last
555
- await act(async () => {
556
- jest.runOnlyPendingTimers();
557
- nbApiCalls += 1; // last poll round
558
- });
275
+ const user = userEvent.setup({ delay: null });
559
276
 
560
- nbApiCalls += 1; // useProductOrder call (invalidate)
561
- nbApiCalls += 1; // orders get (invalidate queries)
277
+ const $button = screen.getByRole('button', {
278
+ name: `Subscribe`,
279
+ }) as HTMLButtonElement;
562
280
 
563
- // - The SaleTunnelNotValidated component should be rendered
564
- screen.getByTestId('generic-sale-tunnel-not-validated-step');
565
- screen.getByText("Sorry, you'll have to wait a little longer!");
281
+ // - wait for address to be loaded.
282
+ await screen.findByText(getAddressLabel(billingAddress));
566
283
 
567
- // If productType is credential, the button should redirect to the dashboard
568
- if (productType === ProductType.CREDENTIAL) {
569
- const $link = screen.getByRole('link', {
570
- name: 'Close',
571
- });
572
- expect($link.getAttribute('href')).toBe(`/en/dashboard/courses/orders/${order.id}`);
573
- } else {
574
- // Otherwise, the button should close the modal
575
- screen.getByRole('button', {
576
- name: 'Close',
577
- });
578
- }
284
+ // - As all information are provided, subscribe button should not be disabled.
285
+ expect($button.disabled).toBe(false);
579
286
 
580
- // - And poller should be stopped
581
- await act(async () => {
582
- jest.runOnlyPendingTimers();
583
- });
584
- expect(fetchMock.calls()).toHaveLength(nbApiCalls);
585
- }, 10000);
287
+ // - User clicks on subscribe button
288
+ await user.click($button);
586
289
 
587
- it('should render an error message when payment failed', async () => {
588
- const product = ProductFactory().one();
589
- const billingAddress = AddressFactory({
590
- is_main: true,
591
- }).one();
592
- const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
290
+ // - Route to create order should have been called
291
+ nbApiCalls += 1; // order post create (no query invalidation)
593
292
 
594
- fetchMock
595
- .get(
596
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
597
- [],
598
- )
599
- .get(
600
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
601
- [],
602
- )
603
- .post('https://joanie.endpoint/api/v1.0/orders/', order)
604
- .patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
605
- payment_info: paymentInfo,
606
- })
607
- .get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, {
608
- ...order,
609
- state: OrderState.SUBMITTED,
610
- })
611
- .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
612
- overwriteRoutes: true,
613
- });
293
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
614
294
 
615
- render(<Wrapper product={product} />, {
616
- queryOptions: { client: createTestQueryClient({ user: richieUser }) },
617
- });
618
- nbApiCalls += 1; // useProductOrder get order with filters
619
- nbApiCalls += 1; // get user account call.
620
- nbApiCalls += 1; // get user preferences call.
621
- nbApiCalls += 1; // get product payment schedule.
622
- await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
295
+ // - Spinner should be displayed and subscribe button should be disabled
296
+ screen.getByText('Order creation in progress');
297
+ expect($button.disabled).toBe(true);
623
298
 
624
- const $terms = screen.getByLabelText(
625
- 'By checking this box, you accept the General Terms of Sale',
299
+ // - Simulate the order creation has failed
300
+ await act(async () => {
301
+ deferred.reject(
302
+ new HttpError(HttpStatusCode.BAD_REQUEST, 'An error occurred during order creation.'),
626
303
  );
627
- const user = userEvent.setup({ delay: null });
628
- await user.click($terms);
629
-
630
- const $button = screen.getByRole('button', {
631
- name: `Subscribe`,
632
- }) as HTMLButtonElement;
633
-
634
- // - wait for address to be loaded.
635
- await screen.findByText(getAddressLabel(billingAddress));
636
-
637
- // - As all information are provided, payment button should not be disabled.
638
- expect($button.disabled).toBe(false);
639
-
640
- // - User clicks on pay button
641
- await user.click($button);
642
-
643
- // - Route to create order should have been called
644
- nbApiCalls += 1; // order post create (invalidate queries)
645
- nbApiCalls += 1; // order get (invalidate queries)
646
- nbApiCalls += 1; // useProductOrder call (invalidate from create)
647
- nbApiCalls += 1; // order submit (invalidate queries)
648
- nbApiCalls += 1; // order get (invalidate queries)
649
- nbApiCalls += 1; // useProductOrder call (invalidate from submit)
650
-
651
- await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
652
-
653
- // - Spinner should be displayed and payment button should be disabled
654
- screen.getByText('Payment in progress');
655
- expect($button.disabled).toBe(true);
304
+ });
656
305
 
657
- // - Payment interface should be displayed
658
- await screen.findByText('Payment interface component');
306
+ // - An error message should be displayed
307
+ const $error = screen.getByText('An error occurred during order creation. Please retry later.');
308
+ expect(document.activeElement).toBe($error);
659
309
 
660
- // - Simulate the payment has failed
661
- await act(async () => {
662
- fireEvent.click(screen.getByTestId('payment-failure'));
310
+ // - Payment button should have been restore to its idle state
311
+ expect($button.disabled).toBe(false);
312
+ screen.getByRole('button', {
313
+ name: 'Subscribe',
314
+ });
315
+ });
316
+
317
+ it('should start at the save payment method step if order is in state to_save_payment_method', async () => {
318
+ const product = ProductFactory().one();
319
+ const billingAddress = AddressFactory({
320
+ is_main: true,
321
+ }).one();
322
+ const creditCard = CreditCardFactory().one();
323
+ const order = OrderFactory({ state: OrderState.TO_SAVE_PAYMENT_METHOD }).one();
324
+
325
+ fetchMock
326
+ .get(
327
+ `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
328
+ [order],
329
+ )
330
+ .get(
331
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
332
+ [],
333
+ )
334
+ .get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard], {
335
+ overwriteRoutes: true,
336
+ })
337
+ .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
338
+ overwriteRoutes: true,
663
339
  });
664
340
 
665
- // - An error message should be displayed
666
- const $error = screen.getByText('An error occurred during payment. Please retry later.');
667
- expect(document.activeElement).toBe($error);
668
- // - Payment interface should have been closed
669
- expect(screen.queryByText('Payment interface component')).toBeNull();
670
- // - Payment button should have been restore to its idle state
671
- expect($button.disabled).toBe(false);
672
- screen.getByRole('button', {
673
- name: 'Subscribe',
674
- });
341
+ render(<Wrapper product={product} />, {
342
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
675
343
  });
676
344
 
677
- it('should resubmit the order when user retry to pay after a payment failure', async () => {
345
+ nbApiCalls += 3;
346
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
347
+
348
+ await screen.findByTestId('sale-tunnel-save-payment-method-step');
349
+ });
350
+
351
+ it.each([OrderState.TO_SIGN, OrderState.SIGNING])(
352
+ 'should start at the sign step if order is in state %s',
353
+ async (state) => {
678
354
  const product = ProductFactory().one();
679
355
  const billingAddress = AddressFactory({
680
356
  is_main: true,
681
357
  }).one();
682
- const { payment_info: paymentInfo, ...order } = OrderWithPaymentFactory().one();
358
+ const creditCard = CreditCardFactory().one();
359
+ const order = OrderFactory({ state }).one();
683
360
 
684
361
  fetchMock
685
362
  .get(
686
363
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
687
- [],
364
+ [order],
688
365
  )
689
366
  .get(
690
367
  `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
691
368
  [],
692
369
  )
693
- .post('https://joanie.endpoint/api/v1.0/orders/', order)
694
- .patch(`https://joanie.endpoint/api/v1.0/orders/${order.id}/submit/`, {
695
- payment_info: paymentInfo,
696
- })
697
- .get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, {
698
- ...order,
699
- state: OrderState.SUBMITTED,
370
+ .get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard], {
371
+ overwriteRoutes: true,
700
372
  })
701
373
  .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
702
374
  overwriteRoutes: true,
@@ -705,202 +377,81 @@ describe.each([
705
377
  render(<Wrapper product={product} />, {
706
378
  queryOptions: { client: createTestQueryClient({ user: richieUser }) },
707
379
  });
708
- nbApiCalls += 1; // useProductOrder get order with filters
709
- nbApiCalls += 1; // get user account call.
710
- nbApiCalls += 1; // get user preferences call.
711
- nbApiCalls += 1; // product payment schedule call.
712
- await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
713
-
714
- const $terms = screen.getByLabelText(
715
- 'By checking this box, you accept the General Terms of Sale',
716
- );
717
- const user = userEvent.setup({ delay: null });
718
- await user.click($terms);
719
-
720
- const $button = screen.getByRole('button', {
721
- name: `Subscribe`,
722
- }) as HTMLButtonElement;
723
-
724
- // - wait for address to be loaded.
725
- await screen.findByText(getAddressLabel(billingAddress));
726
380
 
727
- // - User clicks on pay button
728
- await user.click($button);
381
+ nbApiCalls += 3;
382
+ await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
729
383
 
730
- // - Update the response of order list to react to order creation.
731
- fetchMock.get(
384
+ await screen.findByTestId('sale-tunnel-sign-step');
385
+ },
386
+ );
387
+
388
+ it('should show the product payment schedule', async () => {
389
+ const intl = createIntl({ locale: 'en' });
390
+ const product = ProductFactory().one();
391
+ const schedule = PaymentInstallmentFactory().many(2);
392
+ fetchMock
393
+ .get(
732
394
  `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
733
- [
734
- {
735
- ...order,
736
- state: OrderState.SUBMITTED,
737
- },
738
- ],
739
- { overwriteRoutes: true },
395
+ [],
396
+ )
397
+ .get(
398
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
399
+ schedule,
740
400
  );
741
401
 
742
- // - Route to create order should have been called
743
- nbApiCalls += 1; // order post create (invalidate queries)
744
- nbApiCalls += 1; // order get (invalidate queries)
745
- nbApiCalls += 1; // useProductOrder call (invalidate from create)
746
- nbApiCalls += 1; // order submit (invalidate queries)
747
- nbApiCalls += 1; // order get (invalidate queries)
748
- nbApiCalls += 1; // useProductOrder call (invalidate from submit)
749
-
750
- await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
751
-
752
- // - Payment interface should be displayed
753
- await screen.findByText('Payment interface component');
754
-
755
- // - Simulate the payment has failed
756
- await user.click(screen.getByTestId('payment-failure'));
757
-
758
- // - An error message should be displayed
759
- const $error = screen.getByText('An error occurred during payment. Please retry later.');
760
- expect(document.activeElement).toBe($error);
761
-
762
- // - Payment button should have been restore to its idle state
763
- expect($button.disabled).toBe(false);
764
- screen.getByRole('button', {
765
- name: `Subscribe`,
766
- });
767
-
768
- // - User clicks on pay button again
769
- await user.click($button);
770
-
771
- nbApiCalls += 1; // order submit (invalidate queries)
772
- nbApiCalls += 1; // order get (invalidate queries)
773
- nbApiCalls += 1; // useProductOrder call (invalidate from submit)
774
- await waitFor(() => expect(fetchMock.calls()).toHaveLength(nbApiCalls));
402
+ render(<Wrapper product={product} />, {
403
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
775
404
  });
776
405
 
777
- it('should show an error if user does not accept the terms', async () => {
778
- const product = ProductFactory().one();
779
- const billingAddress = AddressFactory({ is_main: true }).one();
780
-
781
- fetchMock
782
- .get(
783
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
784
- [],
785
- )
786
- .get(
787
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
788
- [],
789
- )
790
- .get('https://joanie.endpoint/api/v1.0/addresses/', [billingAddress], {
791
- overwriteRoutes: true,
792
- });
793
-
794
- render(<Wrapper product={product} />, {
795
- queryOptions: { client: createTestQueryClient({ user: richieUser }) },
796
- });
797
-
798
- const $button = screen.getByRole('button', {
799
- name: 'Subscribe',
800
- }) as HTMLButtonElement;
801
-
802
- // - As all information are provided, payment button should not be disabled.
803
- expect($button.disabled).toBe(false);
804
-
805
- expect(screen.queryByText('You must accept the terms')).not.toBeInTheDocument();
806
-
807
- // - User clicks on pay button
808
- await act(async () => {
809
- fireEvent.click($button);
810
- });
811
-
812
- expect(screen.getByText('You must accept the terms.')).toBeInTheDocument();
406
+ await screen.findByRole('heading', {
407
+ level: 4,
408
+ name: 'Payment schedule',
813
409
  });
814
410
 
815
- it('should show a link to the platform terms and conditions', async () => {
816
- const product = ProductFactory().one();
817
-
818
- fetchMock
819
- .get(
820
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
821
- [],
822
- )
823
- .get(
824
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
825
- [],
826
- );
411
+ const scheduleTable = screen.getByRole('table');
412
+ const scheduleTableRows = within(scheduleTable).getAllByRole('row');
413
+ expect(scheduleTableRows).toHaveLength(schedule.length);
827
414
 
828
- render(<Wrapper product={product} />, {
829
- queryOptions: { client: createTestQueryClient({ user: richieUser }) },
415
+ scheduleTableRows.forEach((row, index) => {
416
+ const installment = schedule[index];
417
+ // A first column should show the installment index
418
+ within(row).getByRole('cell', {
419
+ name: (index + 1).toString(),
830
420
  });
831
-
832
- const $terms = screen.getByRole('link', { name: 'General Terms of Sale' });
833
- expect($terms).toHaveAttribute('href', '/en/about/terms-and-conditions/');
834
- });
835
-
836
- it('should show the product payment schedule', async () => {
837
- const intl = createIntl({ locale: 'en' });
838
- const product = ProductFactory().one();
839
- const schedule = PaymentInstallmentFactory().many(2);
840
- fetchMock
841
- .get(
842
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
843
- [],
844
- )
845
- .get(
846
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
847
- schedule,
848
- );
849
-
850
- render(<Wrapper product={product} />, {
851
- queryOptions: { client: createTestQueryClient({ user: richieUser }) },
421
+ // A 2nd column should show the installment amount
422
+ within(row).getByRole('cell', {
423
+ name: formatPrice(installment.amount, installment.currency),
852
424
  });
853
-
854
- await screen.findByRole('heading', {
855
- level: 4,
856
- name: 'Payment schedule',
425
+ // A 3rd column should show the installment withdraw date
426
+ within(row).getByRole('cell', {
427
+ name: `Withdrawn on ${intl.formatDate(installment.due_date, {
428
+ ...DEFAULT_DATE_FORMAT,
429
+ })}`,
857
430
  });
858
-
859
- const scheduleTable = screen.getByRole('table');
860
- const scheduleTableRows = within(scheduleTable).getAllByRole('row');
861
- expect(scheduleTableRows).toHaveLength(schedule.length);
862
-
863
- scheduleTableRows.forEach((row, index) => {
864
- const installment = schedule[index];
865
- // A first column should show the installment index
866
- within(row).getByRole('cell', {
867
- name: (index + 1).toString(),
868
- });
869
- // A 2nd column should show the installment amount
870
- within(row).getByRole('cell', {
871
- name: formatPrice(installment.amount, installment.currency),
872
- });
873
- // A 3rd column should show the installment withdraw date
874
- within(row).getByRole('cell', {
875
- name: `Withdrawn on ${intl.formatDate(installment.due_date, {
876
- ...DEFAULT_DATE_FORMAT,
877
- })}`,
878
- });
879
- // A 4th column should show the installment state
880
- within(row).getByRole('cell', {
881
- name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
882
- });
431
+ // A 4th column should show the installment state
432
+ within(row).getByRole('cell', {
433
+ name: StringHelper.capitalizeFirst(installment.state.replace('_', ' '))!,
883
434
  });
884
435
  });
436
+ });
885
437
 
886
- it('should show a walkthrough to explain the subscription process', async () => {
887
- const product = ProductFactory().one();
888
- const schedule = PaymentInstallmentFactory().many(2);
889
- fetchMock
890
- .get(
891
- `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
892
- [],
893
- )
894
- .get(
895
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
896
- schedule,
897
- );
898
-
899
- render(<Wrapper product={product} />, {
900
- queryOptions: { client: createTestQueryClient({ user: richieUser }) },
901
- });
438
+ it('should show a walkthrough to explain the subscription process', async () => {
439
+ const product = ProductFactory().one();
440
+ const schedule = PaymentInstallmentFactory().many(2);
441
+ fetchMock
442
+ .get(
443
+ `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
444
+ [],
445
+ )
446
+ .get(
447
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-schedule/`,
448
+ schedule,
449
+ );
902
450
 
903
- screen.getByTestId('walkthrough-banner');
451
+ render(<Wrapper product={product} />, {
452
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
904
453
  });
905
- },
906
- );
454
+
455
+ screen.getByTestId('walkthrough-banner');
456
+ });
457
+ });