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