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
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import fetchMock from 'fetch-mock';
|
|
2
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
3
|
+
import { IntlProvider } from 'react-intl';
|
|
4
|
+
import { PropsWithChildren } from 'react';
|
|
5
|
+
import { act, renderHook } from '@testing-library/react';
|
|
6
|
+
import { waitFor } from '@testing-library/dom';
|
|
7
|
+
import { faker } from '@faker-js/faker';
|
|
8
|
+
import { RichieContextFactory as mockRichieContextFactory } from 'utils/test/factories/richie';
|
|
9
|
+
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
10
|
+
import { SessionProvider } from 'contexts/SessionContext';
|
|
11
|
+
import { CertificateOrderFactory } from 'utils/test/factories/joanie';
|
|
12
|
+
import { Deferred } from 'utils/test/deferred';
|
|
13
|
+
import { HttpStatusCode } from 'utils/errors/HttpError';
|
|
14
|
+
import { useOmniscientOrder, useOmniscientOrders, useOrder, useOrders } from '.';
|
|
15
|
+
|
|
16
|
+
jest.mock('utils/context', () => ({
|
|
17
|
+
__esModule: true,
|
|
18
|
+
default: mockRichieContextFactory({
|
|
19
|
+
authentication: { backend: 'fonzie', endpoint: 'https://demo.endpoint' },
|
|
20
|
+
joanie_backend: { endpoint: 'https://joanie.endpoint' },
|
|
21
|
+
}).one(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe('useOrders', () => {
|
|
25
|
+
const Wrapper = ({ children }: PropsWithChildren) => {
|
|
26
|
+
return (
|
|
27
|
+
<QueryClientProvider client={createTestQueryClient({ user: true })}>
|
|
28
|
+
<IntlProvider locale="en">
|
|
29
|
+
<SessionProvider>{children}</SessionProvider>
|
|
30
|
+
</IntlProvider>
|
|
31
|
+
</QueryClientProvider>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', []);
|
|
37
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/addresses/', []);
|
|
38
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', []);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
fetchMock.restore();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('omniscient', () => {
|
|
46
|
+
it('retrieves all the orders', async () => {
|
|
47
|
+
const orders = CertificateOrderFactory().many(5);
|
|
48
|
+
const deferrer = new Deferred<typeof orders>();
|
|
49
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', deferrer.promise, {
|
|
50
|
+
overwriteRoutes: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const { result } = renderHook(useOmniscientOrders, {
|
|
54
|
+
wrapper: Wrapper,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await waitFor(() => {
|
|
58
|
+
expect(result.current.states.fetching).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
expect(result.current.states.isPending).toBe(true);
|
|
61
|
+
expect(result.current.states.isFetched).toBe(false);
|
|
62
|
+
expect(result.current.states.creating).toBe(false);
|
|
63
|
+
expect(result.current.items).toEqual([]);
|
|
64
|
+
|
|
65
|
+
await act(async () => {
|
|
66
|
+
deferrer.resolve(orders);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await waitFor(() => {
|
|
70
|
+
expect(result.current.states.fetching).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
expect(result.current.states.isPending).toBe(false);
|
|
73
|
+
expect(result.current.states.isFetched).toBe(true);
|
|
74
|
+
expect(result.current.states.creating).toBe(false);
|
|
75
|
+
expect(result.current.items).toEqual(orders);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('retrieves a specific order omnisciently', async () => {
|
|
79
|
+
const orders = CertificateOrderFactory().many(5);
|
|
80
|
+
const deferrer = new Deferred<typeof orders>();
|
|
81
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', deferrer.promise, {
|
|
82
|
+
overwriteRoutes: true,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const { result } = renderHook(() => useOmniscientOrder(orders[2].id), {
|
|
86
|
+
wrapper: Wrapper,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await waitFor(() => {
|
|
90
|
+
expect(result.current.states.fetching).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
expect(result.current.states.isPending).toBe(true);
|
|
93
|
+
expect(result.current.states.isFetched).toBe(false);
|
|
94
|
+
expect(result.current.states.creating).toBe(false);
|
|
95
|
+
expect(result.current.item).toEqual(undefined);
|
|
96
|
+
|
|
97
|
+
await act(async () => {
|
|
98
|
+
deferrer.resolve(orders);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await waitFor(() => {
|
|
102
|
+
expect(result.current.states.fetching).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
expect(result.current.states.isPending).toBe(false);
|
|
105
|
+
expect(result.current.states.isFetched).toBe(true);
|
|
106
|
+
expect(result.current.states.creating).toBe(false);
|
|
107
|
+
expect(result.current.item).toEqual(orders[2]);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('cancels an order', async () => {
|
|
111
|
+
const deferrer = new Deferred();
|
|
112
|
+
const id = faker.string.uuid();
|
|
113
|
+
fetchMock.post(`https://joanie.endpoint/api/v1.0/orders/${id}/cancel/`, deferrer.promise);
|
|
114
|
+
|
|
115
|
+
const { result } = renderHook(() => useOmniscientOrders(undefined, { enabled: false }), {
|
|
116
|
+
wrapper: Wrapper,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(result.current).not.toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await act(async () => {
|
|
124
|
+
result.current.methods.cancel(id);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
expect(result.current.states.isPending).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
expect(result.current.states.fetching).toBe(false);
|
|
131
|
+
expect(result.current.states.cancelling).toBe(true);
|
|
132
|
+
|
|
133
|
+
await act(async () => {
|
|
134
|
+
deferrer.resolve(HttpStatusCode.OK);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(result.current.states.isPending).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
expect(result.current.states.cancelling).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('manages cancel mutation failure', async () => {
|
|
144
|
+
const id = faker.string.uuid();
|
|
145
|
+
fetchMock.post(
|
|
146
|
+
`https://joanie.endpoint/api/v1.0/orders/${id}/cancel/`,
|
|
147
|
+
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const { result } = renderHook(() => useOmniscientOrders(undefined, { enabled: false }), {
|
|
151
|
+
wrapper: Wrapper,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await waitFor(() => {
|
|
155
|
+
expect(result.current).not.toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
expect(result.current.states.error).toBe(undefined);
|
|
159
|
+
|
|
160
|
+
await act(async () => {
|
|
161
|
+
await expect(result.current.methods.cancel(id)).rejects.toThrow('Internal Server Error');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(result.current.states.error).toBe('Cannot cancel the order.');
|
|
165
|
+
expect(result.current.states.isPending).toBe(false);
|
|
166
|
+
expect(result.current.states.cancelling).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("set an order's payment method", async () => {
|
|
170
|
+
const deferrer = new Deferred();
|
|
171
|
+
const id = faker.string.uuid();
|
|
172
|
+
const creditCardId = faker.string.uuid();
|
|
173
|
+
fetchMock.post(
|
|
174
|
+
`https://joanie.endpoint/api/v1.0/orders/${id}/payment-method/`,
|
|
175
|
+
deferrer.promise,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const { result } = renderHook(() => useOmniscientOrders(undefined, { enabled: false }), {
|
|
179
|
+
wrapper: Wrapper,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await waitFor(() => {
|
|
183
|
+
expect(result.current).not.toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
await act(async () => {
|
|
187
|
+
result.current.methods.set_payment_method({
|
|
188
|
+
id,
|
|
189
|
+
credit_card_id: creditCardId,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await waitFor(() => {
|
|
194
|
+
expect(result.current.states.isPending).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
expect(result.current.states.fetching).toBe(false);
|
|
197
|
+
expect(result.current.states.settingPaymentMethod).toBe(true);
|
|
198
|
+
|
|
199
|
+
await act(async () => {
|
|
200
|
+
deferrer.resolve(HttpStatusCode.OK);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
expect(result.current.states.isPending).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
expect(result.current.states.settingPaymentMethod).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('manages set payment method mutation failure', async () => {
|
|
210
|
+
const id = faker.string.uuid();
|
|
211
|
+
const creditCardId = faker.string.uuid();
|
|
212
|
+
fetchMock.post(
|
|
213
|
+
`https://joanie.endpoint/api/v1.0/orders/${id}/payment-method/`,
|
|
214
|
+
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const { result } = renderHook(() => useOmniscientOrders(undefined, { enabled: false }), {
|
|
218
|
+
wrapper: Wrapper,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await waitFor(() => {
|
|
222
|
+
expect(result.current).not.toBeNull();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result.current.states.error).toBe(undefined);
|
|
226
|
+
|
|
227
|
+
await act(async () => {
|
|
228
|
+
await expect(
|
|
229
|
+
result.current.methods.set_payment_method({ id, credit_card_id: creditCardId }),
|
|
230
|
+
).rejects.toThrow('Internal Server Error');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
expect(result.current.states.error).toBe("Cannot set the order's payment method.");
|
|
234
|
+
expect(result.current.states.isPending).toBe(false);
|
|
235
|
+
expect(result.current.states.cancelling).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('non-omniscient', () => {
|
|
240
|
+
it('retrieves all the orders', async () => {
|
|
241
|
+
const orders = CertificateOrderFactory().many(5);
|
|
242
|
+
const deferrer = new Deferred<typeof orders>();
|
|
243
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/orders/', deferrer.promise, {
|
|
244
|
+
overwriteRoutes: true,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const { result } = renderHook(useOrders, {
|
|
248
|
+
wrapper: Wrapper,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await waitFor(() => {
|
|
252
|
+
expect(result.current.states.fetching).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
expect(result.current.states.isPending).toBe(true);
|
|
255
|
+
expect(result.current.states.isFetched).toBe(false);
|
|
256
|
+
expect(result.current.states.creating).toBe(false);
|
|
257
|
+
expect(result.current.items).toEqual([]);
|
|
258
|
+
|
|
259
|
+
await act(async () => {
|
|
260
|
+
deferrer.resolve(orders);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await waitFor(() => {
|
|
264
|
+
expect(result.current.states.fetching).toBe(false);
|
|
265
|
+
});
|
|
266
|
+
expect(result.current.states.isPending).toBe(false);
|
|
267
|
+
expect(result.current.states.isFetched).toBe(true);
|
|
268
|
+
expect(result.current.states.creating).toBe(false);
|
|
269
|
+
expect(result.current.items).toEqual(orders);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('retrieves a specific order', async () => {
|
|
273
|
+
const order = CertificateOrderFactory().one();
|
|
274
|
+
const deferrer = new Deferred<typeof order>();
|
|
275
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/orders/${order.id}/`, deferrer.promise);
|
|
276
|
+
|
|
277
|
+
const { result } = renderHook(() => useOrder(order.id), {
|
|
278
|
+
wrapper: Wrapper,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await waitFor(() => {
|
|
282
|
+
expect(result.current.states.fetching).toBe(true);
|
|
283
|
+
});
|
|
284
|
+
expect(result.current.states.isPending).toBe(true);
|
|
285
|
+
expect(result.current.states.isFetched).toBe(false);
|
|
286
|
+
expect(result.current.states.creating).toBe(false);
|
|
287
|
+
expect(result.current.item).toEqual(undefined);
|
|
288
|
+
|
|
289
|
+
await act(async () => {
|
|
290
|
+
deferrer.resolve(order);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await waitFor(() => {
|
|
294
|
+
expect(result.current.states.fetching).toBe(false);
|
|
295
|
+
});
|
|
296
|
+
expect(result.current.states.isPending).toBe(false);
|
|
297
|
+
expect(result.current.states.isFetched).toBe(true);
|
|
298
|
+
expect(result.current.states.creating).toBe(false);
|
|
299
|
+
expect(result.current.item).toEqual(order);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('has a method to cancel an order', async () => {
|
|
303
|
+
const { result } = renderHook(() => useOrders(undefined, { enabled: false }), {
|
|
304
|
+
wrapper: Wrapper,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
await waitFor(() => {
|
|
308
|
+
expect(result.current.methods.cancel).not.toBeUndefined();
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('has a method to set a payment method', async () => {
|
|
313
|
+
const { result } = renderHook(() => useOrders(undefined, { enabled: false }), {
|
|
314
|
+
wrapper: Wrapper,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await waitFor(() => {
|
|
318
|
+
expect(result.current.methods.set_payment_method).not.toBeUndefined();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { defineMessages } from 'react-intl';
|
|
1
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
2
3
|
import {
|
|
3
4
|
API,
|
|
4
5
|
CertificateOrder,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
} from 'types/Joanie';
|
|
13
14
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
14
15
|
import { useSessionMutation } from 'utils/react-query/useSessionMutation';
|
|
15
|
-
import { QueryOptions, useResource, useResourcesCustom, UseResourcesProps } from '
|
|
16
|
+
import { QueryOptions, useResource, useResourcesCustom, UseResourcesProps } from '../useResources';
|
|
16
17
|
|
|
17
18
|
export type OrderResourcesQuery = PaginatedResourceQuery & {
|
|
18
19
|
course_code?: CourseLight['code'];
|
|
@@ -36,6 +37,16 @@ const messages = defineMessages({
|
|
|
36
37
|
description: 'Error message shown to the user when no orders matches.',
|
|
37
38
|
defaultMessage: 'Cannot find the orders.',
|
|
38
39
|
},
|
|
40
|
+
errorCancel: {
|
|
41
|
+
id: 'hooks.useOrders.errorCancel',
|
|
42
|
+
description: 'Error message shown to the user when cancel mutation failed.',
|
|
43
|
+
defaultMessage: 'Cannot cancel the order.',
|
|
44
|
+
},
|
|
45
|
+
errorSetPaymentMethod: {
|
|
46
|
+
id: 'hooks.useOrders.errorSetPaymentMethod',
|
|
47
|
+
description: 'Error message shown to the user when set payment method mutation failed.',
|
|
48
|
+
defaultMessage: "Cannot set the order's payment method.",
|
|
49
|
+
},
|
|
39
50
|
});
|
|
40
51
|
|
|
41
52
|
function omniscientFiltering(
|
|
@@ -73,25 +84,40 @@ const useOrdersBase =
|
|
|
73
84
|
filters?: OrderResourcesQuery,
|
|
74
85
|
queryOptions?: QueryOptions<CredentialOrder | CertificateOrder>,
|
|
75
86
|
) => {
|
|
87
|
+
const intl = useIntl();
|
|
76
88
|
const custom = useResourcesCustom({ ...props, filters, queryOptions });
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
const queryClient = useQueryClient();
|
|
90
|
+
const api = props.apiInterface();
|
|
91
|
+
const onSuccess = async () => {
|
|
92
|
+
custom.methods.setError(undefined);
|
|
93
|
+
await custom.methods.invalidate();
|
|
94
|
+
props.onMutationSuccess?.(queryClient);
|
|
95
|
+
};
|
|
96
|
+
const cancelHandler = useSessionMutation({
|
|
97
|
+
mutationFn: api.cancel,
|
|
98
|
+
onSuccess,
|
|
99
|
+
onError: () => custom.methods.setError(intl.formatMessage(messages.errorCancel)),
|
|
82
100
|
});
|
|
83
|
-
const
|
|
84
|
-
mutationFn:
|
|
85
|
-
onSuccess
|
|
86
|
-
|
|
87
|
-
},
|
|
101
|
+
const setPaymentMethodHandler = useSessionMutation({
|
|
102
|
+
mutationFn: api.set_payment_method,
|
|
103
|
+
onSuccess,
|
|
104
|
+
onError: () => custom.methods.setError(intl.formatMessage(messages.errorSetPaymentMethod)),
|
|
88
105
|
});
|
|
106
|
+
|
|
89
107
|
return {
|
|
90
108
|
...custom,
|
|
91
109
|
methods: {
|
|
92
110
|
...custom.methods,
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
cancel: cancelHandler.mutateAsync,
|
|
112
|
+
set_payment_method: setPaymentMethodHandler.mutateAsync,
|
|
113
|
+
},
|
|
114
|
+
states: {
|
|
115
|
+
...custom.states,
|
|
116
|
+
cancelling: cancelHandler.isPending,
|
|
117
|
+
settingPaymentMethod: setPaymentMethodHandler.isPending,
|
|
118
|
+
isPending: [custom.states, cancelHandler, setPaymentMethodHandler].some(
|
|
119
|
+
(value) => value?.isPending,
|
|
120
|
+
),
|
|
95
121
|
},
|
|
96
122
|
};
|
|
97
123
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
3
|
+
import { PaymentSchedule } from 'types/Joanie';
|
|
4
|
+
import { Nullable } from 'types/utils';
|
|
5
|
+
|
|
6
|
+
type PaymentScheduleFilters = {
|
|
7
|
+
course_code: string;
|
|
8
|
+
product_id: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const usePaymentSchedule = (filters: PaymentScheduleFilters) => {
|
|
12
|
+
const queryKey = ['courses-products', ...Object.values(filters), 'payment-schedule'];
|
|
13
|
+
|
|
14
|
+
const api = useJoanieApi();
|
|
15
|
+
return useQuery<Nullable<PaymentSchedule>, Error>({
|
|
16
|
+
queryKey,
|
|
17
|
+
queryFn: () =>
|
|
18
|
+
api.courses.products.paymentSchedule.get({
|
|
19
|
+
id: filters.product_id,
|
|
20
|
+
course_id: filters.course_code,
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
};
|
|
@@ -9,7 +9,7 @@ import { CourseLightFactory, CredentialOrderFactory } from 'utils/test/factories
|
|
|
9
9
|
import { SessionProvider } from 'contexts/SessionContext';
|
|
10
10
|
import { Deferred } from 'utils/test/deferred';
|
|
11
11
|
import { createTestQueryClient } from 'utils/test/createTestQueryClient';
|
|
12
|
-
import {
|
|
12
|
+
import { NOT_CANCELED_ORDER_STATES, OrderState } from 'types/Joanie';
|
|
13
13
|
import useProductOrder from '.';
|
|
14
14
|
|
|
15
15
|
jest.mock('utils/context', () => ({
|
|
@@ -45,69 +45,86 @@ describe('useProductOrder', () => {
|
|
|
45
45
|
);
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
-
it.each(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
it.each([
|
|
49
|
+
OrderState.DRAFT,
|
|
50
|
+
OrderState.ASSIGNED,
|
|
51
|
+
OrderState.TO_SIGN,
|
|
52
|
+
OrderState.SIGNING,
|
|
53
|
+
OrderState.TO_SAVE_PAYMENT_METHOD,
|
|
54
|
+
OrderState.PENDING,
|
|
55
|
+
OrderState.PENDING_PAYMENT,
|
|
56
|
+
OrderState.NO_PAYMENT,
|
|
57
|
+
OrderState.FAILED_PAYMENT,
|
|
58
|
+
OrderState.COMPLETED,
|
|
59
|
+
])('should retrieves the last order when order.state is %s', async (currentState) => {
|
|
60
|
+
// the most recent order of accepted state will be return
|
|
61
|
+
const order = CredentialOrderFactory({
|
|
62
|
+
state: currentState,
|
|
63
|
+
created_on: new Date().toISOString(),
|
|
64
|
+
course: CourseLightFactory({ code: '00000' }).one(),
|
|
65
|
+
}).one();
|
|
66
|
+
const ordersByState = NOT_CANCELED_ORDER_STATES.filter((state) => state !== currentState).map(
|
|
67
|
+
(state) =>
|
|
68
|
+
CredentialOrderFactory({
|
|
69
|
+
state,
|
|
70
|
+
created_on: faker.date.past({ years: 1 }).toISOString(),
|
|
71
|
+
course: CourseLightFactory({ code: '00000' }).one(),
|
|
72
|
+
product_id: order.product_id,
|
|
73
|
+
}).one(),
|
|
74
|
+
);
|
|
75
|
+
ordersByState.push(order);
|
|
67
76
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
const responseDeferred = new Deferred();
|
|
78
|
+
const url =
|
|
79
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
80
|
+
'?course_code=00000' +
|
|
81
|
+
`&product_id=${order.product_id}` +
|
|
82
|
+
`&state=${OrderState.PENDING}` +
|
|
83
|
+
`&state=${OrderState.PENDING_PAYMENT}` +
|
|
84
|
+
`&state=${OrderState.NO_PAYMENT}` +
|
|
85
|
+
`&state=${OrderState.FAILED_PAYMENT}` +
|
|
86
|
+
`&state=${OrderState.COMPLETED}` +
|
|
87
|
+
`&state=${OrderState.DRAFT}` +
|
|
88
|
+
`&state=${OrderState.ASSIGNED}` +
|
|
89
|
+
`&state=${OrderState.TO_SIGN}` +
|
|
90
|
+
`&state=${OrderState.SIGNING}` +
|
|
91
|
+
`&state=${OrderState.TO_SAVE_PAYMENT_METHOD}`;
|
|
92
|
+
fetchMock.get(url, responseDeferred.promise);
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
94
|
+
const { result } = renderHook(
|
|
95
|
+
() => useProductOrder({ productId: order.product_id, courseCode: '00000' }),
|
|
96
|
+
{
|
|
97
|
+
wrapper: Wrapper,
|
|
98
|
+
},
|
|
99
|
+
);
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
expect(result.current.states.isPending).toBe(true);
|
|
95
|
-
expect(result.current.states.error).toBe(undefined);
|
|
101
|
+
await waitFor(() => {
|
|
102
|
+
expect(result.current.states.fetching).toBe(true);
|
|
103
|
+
expect(result.current.item).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
nbApiCalls += 1; // call orders from useProductOrder
|
|
106
|
+
const calledUrls = fetchMock.calls().map((call) => call[0]);
|
|
107
|
+
expect(calledUrls).toHaveLength(nbApiCalls);
|
|
108
|
+
expect(calledUrls).toContain(url);
|
|
109
|
+
expect(result.current.states.creating).toBe(false);
|
|
110
|
+
expect(result.current.states.deleting).toBeUndefined();
|
|
111
|
+
expect(result.current.states.updating).toBeUndefined();
|
|
112
|
+
expect(result.current.states.isPending).toBe(true);
|
|
113
|
+
expect(result.current.states.error).toBe(undefined);
|
|
96
114
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
115
|
+
await act(async () => {
|
|
116
|
+
responseDeferred.resolve(ordersByState);
|
|
117
|
+
});
|
|
100
118
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(result.current.states.fetching).toBe(false);
|
|
121
|
+
expect(result.current.item).toEqual(order);
|
|
122
|
+
});
|
|
105
123
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
);
|
|
124
|
+
expect(result.current.states.creating).toBe(false);
|
|
125
|
+
expect(result.current.states.deleting).toBeUndefined();
|
|
126
|
+
expect(result.current.states.updating).toBeUndefined();
|
|
127
|
+
expect(result.current.states.isPending).toBe(false);
|
|
128
|
+
expect(result.current.states.error).toBe(undefined);
|
|
129
|
+
});
|
|
113
130
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import { useOrders } from 'hooks/useOrders';
|
|
3
|
-
import {
|
|
3
|
+
import { NOT_CANCELED_ORDER_STATES } from 'types/Joanie';
|
|
4
4
|
|
|
5
5
|
interface UseProductOrderProps {
|
|
6
6
|
courseCode?: string;
|
|
@@ -12,7 +12,7 @@ const useProductOrder = ({ courseCode, enrollmentId, productId }: UseProductOrde
|
|
|
12
12
|
course_code: courseCode,
|
|
13
13
|
enrollment_id: enrollmentId,
|
|
14
14
|
product_id: productId,
|
|
15
|
-
state:
|
|
15
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
const order = useMemo(() => {
|
|
@@ -59,6 +59,7 @@ const emptyArray: never[] = [];
|
|
|
59
59
|
* @param queryOptions - Pass custom options to react-query.
|
|
60
60
|
* @param localized - Is the resource local-dependent ? If so, the query will be invalidated on locale change.
|
|
61
61
|
* @param resourceMessages - Custom messages to use for this resource.
|
|
62
|
+
* @param onMutationSuccess - Custom callback triggered on mutation success.
|
|
62
63
|
*/
|
|
63
64
|
export const useResourcesRoot = <
|
|
64
65
|
TData extends Resource,
|
|
@@ -136,21 +137,21 @@ export const useResourcesRoot = <
|
|
|
136
137
|
const mutation = (session ? useSessionMutation : useMutation) as typeof useMutation;
|
|
137
138
|
|
|
138
139
|
const writeHandlers = {
|
|
139
|
-
create: api
|
|
140
|
+
create: api?.create
|
|
140
141
|
? mutation({
|
|
141
142
|
mutationFn: api.create,
|
|
142
143
|
onSuccess,
|
|
143
144
|
onError: () => setError(intl.formatMessage(actualMessages.errorCreate)),
|
|
144
145
|
})
|
|
145
146
|
: undefined,
|
|
146
|
-
update: api
|
|
147
|
+
update: api?.update
|
|
147
148
|
? mutation({
|
|
148
149
|
mutationFn: api.update,
|
|
149
150
|
onSuccess,
|
|
150
151
|
onError: () => setError(intl.formatMessage(actualMessages.errorUpdate)),
|
|
151
152
|
})
|
|
152
153
|
: undefined,
|
|
153
|
-
delete: api
|
|
154
|
+
delete: api?.delete
|
|
154
155
|
? mutation({
|
|
155
156
|
mutationFn: api.delete,
|
|
156
157
|
onSuccess,
|
package/js/index.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import 'core-js/modules/es.array.iterator';
|
|
|
11
11
|
import 'core-js/modules/es.promise';
|
|
12
12
|
import { IntlProvider } from 'react-intl';
|
|
13
13
|
import { QueryClientProvider } from '@tanstack/react-query';
|
|
14
|
+
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
14
15
|
import countries from 'i18n-iso-countries';
|
|
15
16
|
import { createRoot } from 'react-dom/client';
|
|
16
17
|
import createQueryClient from 'utils/react-query/createQueryClient';
|
|
@@ -116,6 +117,7 @@ async function render() {
|
|
|
116
117
|
const reactRoot = createRoot(rootContainer);
|
|
117
118
|
reactRoot.render(
|
|
118
119
|
<QueryClientProvider client={queryClient}>
|
|
120
|
+
<ReactQueryDevtools initialIsOpen={false} />
|
|
119
121
|
<IntlProvider locale={locale} messages={translatedMessages} defaultLocale="en-US">
|
|
120
122
|
<Root richieReactSpots={richieReactSpots} />
|
|
121
123
|
</IntlProvider>
|
|
@@ -23,7 +23,7 @@ describe('CreditCardBrandLogo', () => {
|
|
|
23
23
|
render(<CreditCardBrandLogo creditCard={creditCard} />);
|
|
24
24
|
expect(screen.getByRole('presentation')).toHaveAttribute(
|
|
25
25
|
'src',
|
|
26
|
-
'/static/richie/images/components/DashboardCreditCardsManagement/
|
|
26
|
+
'/static/richie/images/components/DashboardCreditCardsManagement/logo_cb.svg',
|
|
27
27
|
);
|
|
28
28
|
});
|
|
29
29
|
});
|
|
@@ -8,8 +8,10 @@ export const CreditCardBrandLogo = ({
|
|
|
8
8
|
creditCard: CreditCard;
|
|
9
9
|
variant?: 'default' | 'inline';
|
|
10
10
|
}) => {
|
|
11
|
-
const creditCardBrand = Object.values<string>(CreditCardBrand).includes(
|
|
12
|
-
|
|
11
|
+
const creditCardBrand = Object.values<string>(CreditCardBrand).includes(
|
|
12
|
+
creditCard.brand.toLowerCase(),
|
|
13
|
+
)
|
|
14
|
+
? creditCard.brand.toLowerCase()
|
|
13
15
|
: CreditCardBrand.CB;
|
|
14
16
|
|
|
15
17
|
return (
|