richie-education 2.33.1-dev14 → 2.33.1-dev22
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 +5 -0
- package/js/api/lms/dummy.ts +12 -10
- package/js/components/PaymentScheduleGrid/_styles.scss +6 -5
- package/js/components/PaymentScheduleGrid/index.tsx +16 -0
- package/js/hooks/useCreditCards/index.spec.tsx +83 -0
- package/js/hooks/useCreditCards/index.ts +53 -1
- package/js/hooks/useCreditCardsManagement.tsx +1 -10
- package/js/hooks/useLearnerCoursesSearch/index.tsx +2 -2
- package/js/pages/DashboardCourses/index.spec.tsx +51 -7
- package/js/pages/DashboardCreditCardsManagement/DashboardCreditCardBox.tsx +3 -5
- package/js/pages/DashboardCreditCardsManagement/_styles.scss +11 -3
- package/js/pages/DashboardCreditCardsManagement/index.spec.tsx +46 -25
- package/js/pages/DashboardCreditCardsManagement/index.tsx +21 -37
- package/js/types/Joanie.ts +13 -1
- package/js/utils/OrderHelper/index.ts +9 -0
- package/js/utils/errors/HttpError.ts +1 -0
- package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +7 -1
- package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +14 -2
- package/js/widgets/Dashboard/components/DashboardItem/Order/Installment/index.tsx +3 -2
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx +12 -5
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +5 -0
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +10 -8
- package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +5 -0
- package/js/widgets/Dashboard/index.spec.tsx +7 -1
- package/package.json +1 -1
package/js/api/joanie.ts
CHANGED
|
@@ -83,6 +83,7 @@ export const getRoutes = () => {
|
|
|
83
83
|
update: `${baseUrl}/credit-cards/:id/`,
|
|
84
84
|
delete: `${baseUrl}/credit-cards/:id/`,
|
|
85
85
|
tokenize: `${baseUrl}/credit-cards/tokenize-card/`,
|
|
86
|
+
promote: `${baseUrl}/credit-cards/:id/promote/`,
|
|
86
87
|
},
|
|
87
88
|
addresses: {
|
|
88
89
|
get: `${baseUrl}/addresses/:id/`,
|
|
@@ -242,6 +243,10 @@ const API = (): Joanie.API => {
|
|
|
242
243
|
}).then(checkStatus),
|
|
243
244
|
tokenize: async () =>
|
|
244
245
|
fetchWithJWT(ROUTES.user.creditCards.tokenize, { method: 'POST' }).then(checkStatus),
|
|
246
|
+
promote: async (id) =>
|
|
247
|
+
fetchWithJWT(ROUTES.user.creditCards.promote.replace(':id', id), {
|
|
248
|
+
method: 'PATCH',
|
|
249
|
+
}).then(checkStatus),
|
|
245
250
|
},
|
|
246
251
|
addresses: {
|
|
247
252
|
get: (id?: string) => {
|
package/js/api/lms/dummy.ts
CHANGED
|
@@ -25,24 +25,26 @@ type JWTPayload = {
|
|
|
25
25
|
username: string;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
/* All JWT tokens will expire the
|
|
28
|
+
/* All JWT tokens will expire the 02 Feb 2026 ! */
|
|
29
29
|
const JOANIE_DEV_DEMO_USER_JWT_TOKENS = {
|
|
30
|
-
admin:
|
|
31
|
-
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzMzOTI4MjE0LCJpYXQiOjE3MDIzNjU1MjMsImp0aSI6IjRhMzQxZWVmMmVhOTRkNGFiMzQ5OThkOWE4ZDM5MTI0IiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImxhbmd1YWdlIjoiZW4tdXMiLCJ1c2VybmFtZSI6ImFkbWluIiwiZnVsbF9uYW1lIjoiIn0.rT8nymp8f4T7tIIXO-M5-ahXBwxoDNVqtaZIrb_GHuk',
|
|
32
30
|
user0:
|
|
33
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
31
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImQwZmU1Zjg5ZjFhYTQ4YmM5NDhmNWU4ODFkNTNhNTU2IiwiZW1haWwiOiJwc21pdGhAZXhhbXBsZS5vcmciLCJsYW5ndWFnZSI6ImVuLXVzIiwidXNlcm5hbWUiOiJ1c2VyMCIsImZ1bGxfbmFtZSI6Ik90aGVyIE93bmVyIn0.eCawfaCzpO7U7iUPC1TE_XYDiRjq_crI93GqE8Fj8zc',
|
|
34
32
|
user1:
|
|
35
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
33
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImIwYjk3YjZkZjFlMzRkMTg4NjFiMGFhMjcxYWI0YWU1IiwiZW1haWwiOiJzYW1wc29uYW5uYUBleGFtcGxlLm9yZyIsImxhbmd1YWdlIjoiZW4tdXMiLCJ1c2VybmFtZSI6InVzZXIxIiwiZnVsbF9uYW1lIjoiT3RoZXIgT3duZXIifQ.yd46_63iuw19zmzH8aVNRAVAvAE4VGH8W8BjmFs6PPU',
|
|
36
34
|
user2:
|
|
37
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
35
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImNmYzY2OTNmY2Q5ZTRlZGViM2Y2NzU1MTZhNDIzMTdiIiwiZW1haWwiOiJsb3BlemFtYmVyQGV4YW1wbGUub3JnIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoidXNlcjIiLCJmdWxsX25hbWUiOiJPdGhlciBPd25lciJ9.TlFILOXY-wK29M_BUgDKgjdOovSfEIlw5cNXed6ZV3w',
|
|
38
36
|
user3:
|
|
39
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
37
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6IjA4ZTcxZGJjYWIyMDRjMmZhZjgyMDVjZTRiNTliMjZiIiwiZW1haWwiOiJsb25nZWxpemFiZXRoQGV4YW1wbGUub3JnIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoidXNlcjMiLCJmdWxsX25hbWUiOiJPdGhlciBPd25lciJ9.8NxYyjc567lO2Yc7me-TQr8PNvKqB5VLRzHd1Z4vA4U',
|
|
40
38
|
user4:
|
|
41
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
39
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImVmZGRkM2Q0YTdmZDQ4ZmFhYmZkM2Q2OTI4YzMwM2U4IiwiZW1haWwiOiJqb25lc2plbm5pZmVyQGV4YW1wbGUub3JnIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoidXNlcjQiLCJmdWxsX25hbWUiOiJPdGhlciBPd25lciJ9.Wn5CKuNPn0s4B_76Mxd3zTKqdUMaZGV456bhZ-fDe-o',
|
|
42
40
|
organization_owner:
|
|
43
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
41
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6ImRiYmU2ZGExZjhmNDQzNDA4N2U2NzQ0YTIzM2JmNjFiIiwiZW1haWwiOiJkZXZlbG9wZXIrb3JnYW5pemF0aW9uX293bmVyQGV4YW1wbGUuY29tIiwibGFuZ3VhZ2UiOiJlbi11cyIsInVzZXJuYW1lIjoib3JnYW5pemF0aW9uX293bmVyIiwiZnVsbF9uYW1lIjoiT3JnYSBPd25lciJ9.a6QjOAOxCw7ZFKvg8OCcUaW8Xhbmfuqy3cwIqUCPfzE',
|
|
44
42
|
student_user:
|
|
45
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
|
|
43
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6IjNhMGExYjM0OWEwNDQxNTg5ODU4NGUwZjMwNTc5M2EwIiwiZW1haWwiOiJkZXZlbG9wZXIrc3R1ZGVudF91c2VyQGV4YW1wbGUuY29tIiwibGFuZ3VhZ2UiOiJmci1mciIsInVzZXJuYW1lIjoic3R1ZGVudF91c2VyIiwiZnVsbF9uYW1lIjoiXHUwMGM5dHVkaWFudCJ9.3VvjPXwtuNA684hSIem3X2uFD-4WH8fipVDXMsi1cAc',
|
|
44
|
+
second_student_user:
|
|
45
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6Ijg5ZDIyNDJjODRkODRiNThiZWVkYjg1NmU2MGNiM2FiIiwiZW1haWwiOiJkZXZlbG9wZXIrc2Vjb25kX3N0dWRlbnRfdXNlckBleGFtcGxlLmNvbSIsImxhbmd1YWdlIjoiZnItZnIiLCJ1c2VybmFtZSI6InNlY29uZF9zdHVkZW50X3VzZXIiLCJmdWxsX25hbWUiOiJcdTAwYzl0dWRpYW50IDAwMiJ9.p5p4Ku0w8mHortWW9TYHTJgORF9wnfCpq-6pvBRjU0Y',
|
|
46
|
+
admin:
|
|
47
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcwMjIzOTY5LCJpYXQiOjE3Mzg2ODc5NjksImp0aSI6Ijk4M2UzNmI5MTUzODQ2Mjg4ZGMxNWNjOTAwNDgwMDA4IiwiZW1haWwiOiJhZG1pbkBleGFtcGxlLmNvbSIsImxhbmd1YWdlIjoiZW4tdXMiLCJ1c2VybmFtZSI6ImFkbWluIiwiZnVsbF9uYW1lIjoiIn0.VuSqfh4l0vtIDSdkEgCNyciiOhlFlMAsf5u5snm2Avw',
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
export type DevDemoUser = keyof typeof JOANIE_DEV_DEMO_USER_JWT_TOKENS;
|
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
font-weight: var(--c--theme--font--weight--semibold);
|
|
23
23
|
font-size: rem-calc(12px);
|
|
24
24
|
|
|
25
|
-
&--
|
|
26
|
-
|
|
27
|
-
color: var(--c--theme--colors--
|
|
25
|
+
&--canceled,
|
|
26
|
+
&--refunded {
|
|
27
|
+
color: var(--c--theme--colors--white);
|
|
28
|
+
background-color: var(--c--theme--colors--grey59);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
&--paid {
|
|
@@ -37,8 +38,8 @@
|
|
|
37
38
|
color: var(--c--theme--colors--white);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
&--
|
|
41
|
-
&--
|
|
41
|
+
&--refused,
|
|
42
|
+
&--error {
|
|
42
43
|
background-color: var(--c--theme--colors--firebrick6);
|
|
43
44
|
color: var(--c--theme--colors--white);
|
|
44
45
|
}
|
|
@@ -22,6 +22,12 @@ export const stateMessages = defineMessages({
|
|
|
22
22
|
defaultMessage: 'Pending',
|
|
23
23
|
description: 'Label displayed for pending payment state',
|
|
24
24
|
},
|
|
25
|
+
[PaymentScheduleState.ERROR]: {
|
|
26
|
+
id: 'components.PaymentScheduleGrid.state.error',
|
|
27
|
+
defaultMessage: 'Pending',
|
|
28
|
+
description:
|
|
29
|
+
'Label displayed for error payment state. For learner we assume to display `pending`.',
|
|
30
|
+
},
|
|
25
31
|
[PaymentScheduleState.PAID]: {
|
|
26
32
|
id: 'components.PaymentScheduleGrid.state.paid',
|
|
27
33
|
defaultMessage: 'Paid',
|
|
@@ -32,6 +38,16 @@ export const stateMessages = defineMessages({
|
|
|
32
38
|
defaultMessage: 'Refused',
|
|
33
39
|
description: 'Label displayed for refused payment state',
|
|
34
40
|
},
|
|
41
|
+
[PaymentScheduleState.CANCELED]: {
|
|
42
|
+
id: 'components.PaymentScheduleGrid.state.canceled',
|
|
43
|
+
defaultMessage: 'Canceled',
|
|
44
|
+
description: 'Label displayed for canceled payment state',
|
|
45
|
+
},
|
|
46
|
+
[PaymentScheduleState.REFUNDED]: {
|
|
47
|
+
id: 'components.PaymentScheduleGrid.state.refunded',
|
|
48
|
+
defaultMessage: 'Refunded',
|
|
49
|
+
description: 'Label displayed for refunded payment state',
|
|
50
|
+
},
|
|
35
51
|
});
|
|
36
52
|
|
|
37
53
|
export const PaymentScheduleGrid = ({ schedule }: Props) => {
|
|
@@ -175,4 +175,87 @@ describe('useCreditCards', () => {
|
|
|
175
175
|
expect(result.current.states.isPending).toBe(false);
|
|
176
176
|
expect(result.current.states.tokenizing).toBe(false);
|
|
177
177
|
});
|
|
178
|
+
|
|
179
|
+
it('promotes a credit card', async () => {
|
|
180
|
+
const responseDeferred = new Deferred();
|
|
181
|
+
fetchMock.patch(
|
|
182
|
+
'https://joanie.endpoint/api/v1.0/credit-cards/1/promote/',
|
|
183
|
+
responseDeferred.promise,
|
|
184
|
+
);
|
|
185
|
+
const { result } = renderHook(() => useCreditCards(undefined, { enabled: false }), {
|
|
186
|
+
wrapper: Wrapper,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await waitFor(() => {
|
|
190
|
+
expect(result.current).not.toBeNull();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
result.current.methods.promote('1');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
await waitFor(() => {
|
|
198
|
+
expect(result.current.states.updating).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(result.current.states.deleting).toBe(false);
|
|
202
|
+
expect(result.current.states.tokenizing).toBe(false);
|
|
203
|
+
expect(result.current.states.fetching).toBe(false);
|
|
204
|
+
expect(result.current.states.isFetched).toBe(true);
|
|
205
|
+
expect(result.current.states.isPending).toBe(true);
|
|
206
|
+
expect(result.current.states.error).toBe(undefined);
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
responseDeferred.resolve({});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
expect(result.current.states.updating).toBe(false);
|
|
213
|
+
expect(result.current.states.isPending).toBe(false);
|
|
214
|
+
expect(result.current.states.error).toBe(undefined);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('manages error during credit card promotion', async () => {
|
|
218
|
+
fetchMock.patch(
|
|
219
|
+
'https://joanie.endpoint/api/v1.0/credit-cards/1/promote/',
|
|
220
|
+
HttpStatusCode.INTERNAL_SERVER_ERROR,
|
|
221
|
+
);
|
|
222
|
+
const { result } = renderHook(() => useCreditCards(undefined, { enabled: true }), {
|
|
223
|
+
wrapper: Wrapper,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
await waitFor(() => {
|
|
227
|
+
expect(result.current).not.toBeNull();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await act(async () => {
|
|
231
|
+
await expect(result.current.methods.promote('1')).rejects.toThrow('Internal Server Error');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
expect(result.current.states.error).toBe('Cannot set the credit card as default');
|
|
235
|
+
expect(result.current.states.isPending).toBe(false);
|
|
236
|
+
expect(result.current.states.updating).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('has a specific error when credit card deletion fails because it is used', async () => {
|
|
240
|
+
const creditCard = CreditCardFactory({
|
|
241
|
+
id: '1',
|
|
242
|
+
last_numbers: '1337',
|
|
243
|
+
}).one();
|
|
244
|
+
fetchMock.delete('https://joanie.endpoint/api/v1.0/credit-cards/1/', HttpStatusCode.CONFLICT);
|
|
245
|
+
const { result } = renderHook(() => useCreditCards(undefined, { enabled: true }), {
|
|
246
|
+
wrapper: Wrapper,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
await waitFor(() => {
|
|
250
|
+
expect(result.current).not.toBeNull();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
await act(async () => {
|
|
254
|
+
result.current.methods.delete(creditCard);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(result.current.states.error).toBe(
|
|
258
|
+
'Cannot delete the credit card •••• •••• •••• 1337 because it is used to pay at least one of your order.',
|
|
259
|
+
);
|
|
260
|
+
});
|
|
178
261
|
});
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { defineMessages, useIntl } from 'react-intl';
|
|
2
2
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
3
|
+
import { MutateOptions } from '@tanstack/query-core';
|
|
3
4
|
import { API, CreditCard } from 'types/Joanie';
|
|
4
5
|
import { useJoanieApi } from 'contexts/JoanieApiContext';
|
|
5
6
|
import { useSessionMutation } from 'utils/react-query/useSessionMutation';
|
|
7
|
+
import { HttpError, HttpStatusCode } from 'utils/errors/HttpError';
|
|
6
8
|
import {
|
|
7
9
|
QueryOptions,
|
|
8
10
|
ResourcesQuery,
|
|
@@ -27,6 +29,13 @@ const messages = defineMessages({
|
|
|
27
29
|
description: 'Error message shown to the user when credit card deletion request fails.',
|
|
28
30
|
defaultMessage: 'An error occurred while deleting the credit card. Please retry later.',
|
|
29
31
|
},
|
|
32
|
+
errorCannotDelete: {
|
|
33
|
+
id: 'hooks.useCreditCards.errorCannotDelete',
|
|
34
|
+
description:
|
|
35
|
+
'Error message shown to the user when trying to delete a credit card that is used to pay at least order.',
|
|
36
|
+
defaultMessage:
|
|
37
|
+
'Cannot delete the credit card •••• •••• •••• {last_numbers} because it is used to pay at least one of your order.',
|
|
38
|
+
},
|
|
30
39
|
errorTokenize: {
|
|
31
40
|
id: 'hooks.useCreditCards.errorTokenize',
|
|
32
41
|
description: 'Error message shown to the user when credit card tokenize request fails.',
|
|
@@ -37,6 +46,11 @@ const messages = defineMessages({
|
|
|
37
46
|
description: 'Error message shown to the user when no credit cards matches.',
|
|
38
47
|
defaultMessage: 'Cannot find the credit card',
|
|
39
48
|
},
|
|
49
|
+
errorPromote: {
|
|
50
|
+
id: 'hooks.useCreditCards.errorPromote',
|
|
51
|
+
description: 'Error message shown to the user when promoting a credit card fails.',
|
|
52
|
+
defaultMessage: 'Cannot set the credit card as default',
|
|
53
|
+
},
|
|
40
54
|
});
|
|
41
55
|
|
|
42
56
|
const useCreditCardResources =
|
|
@@ -55,17 +69,55 @@ const useCreditCardResources =
|
|
|
55
69
|
},
|
|
56
70
|
onError: () => custom.methods.setError(intl.formatMessage(messages.errorTokenize)),
|
|
57
71
|
});
|
|
72
|
+
const promoteHandler = mutation({
|
|
73
|
+
mutationFn: api.promote,
|
|
74
|
+
onSuccess: async () => {
|
|
75
|
+
custom.methods.setError(undefined);
|
|
76
|
+
custom.methods.invalidate();
|
|
77
|
+
props.onMutationSuccess?.(queryClient);
|
|
78
|
+
},
|
|
79
|
+
onError: () => custom.methods.setError(intl.formatMessage(messages.errorPromote)),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Override the default delete mutation to handle error more specifically.
|
|
84
|
+
* If the error is a 409, it means the credit card is used to pay at least one order
|
|
85
|
+
* and the user should be informed about that.
|
|
86
|
+
*/
|
|
87
|
+
const deleteMutateAsync = async (creditCard: CreditCard, options?: MutateOptions) => {
|
|
88
|
+
return custom.methods.delete(creditCard.id, {
|
|
89
|
+
...options,
|
|
90
|
+
onError: (error: HttpError, variables, context) => {
|
|
91
|
+
if (error.code === HttpStatusCode.CONFLICT) {
|
|
92
|
+
custom.methods.setError(
|
|
93
|
+
intl.formatMessage(messages.errorCannotDelete, {
|
|
94
|
+
last_numbers: creditCard.last_numbers,
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
custom.methods.setError(intl.formatMessage(messages.errorDelete));
|
|
99
|
+
}
|
|
100
|
+
options?.onError?.(error, variables, context);
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
};
|
|
58
104
|
|
|
59
105
|
return {
|
|
60
106
|
...custom,
|
|
61
107
|
methods: {
|
|
62
108
|
...custom.methods,
|
|
109
|
+
delete: deleteMutateAsync,
|
|
63
110
|
tokenize: tokenizeHandler.mutateAsync,
|
|
111
|
+
promote: promoteHandler.mutateAsync,
|
|
64
112
|
},
|
|
65
113
|
states: {
|
|
66
114
|
...custom.states,
|
|
67
|
-
isPending: [tokenizeHandler, custom.states].some(
|
|
115
|
+
isPending: [tokenizeHandler, promoteHandler, custom.states].some(
|
|
116
|
+
(value) => value?.isPending,
|
|
117
|
+
),
|
|
118
|
+
updating: custom.states.updating || promoteHandler.isPending,
|
|
68
119
|
tokenizing: tokenizeHandler.isPending,
|
|
120
|
+
promoting: promoteHandler.isPending,
|
|
69
121
|
},
|
|
70
122
|
};
|
|
71
123
|
};
|
|
@@ -5,11 +5,6 @@ import { CreditCard } from 'types/Joanie';
|
|
|
5
5
|
import { confirm } from 'utils/indirection/window';
|
|
6
6
|
|
|
7
7
|
const messages = defineMessages({
|
|
8
|
-
errorCannotRemoveMain: {
|
|
9
|
-
id: 'hooks.useCreditCardsManagement.errorCannotRemoveMain',
|
|
10
|
-
description: 'Error shown if a user tries to delete a main credit card',
|
|
11
|
-
defaultMessage: 'Cannot remove main credit card.',
|
|
12
|
-
},
|
|
13
8
|
deletionConfirmation: {
|
|
14
9
|
id: 'hooks.useCreditCardsManagement.deletionConfirmation',
|
|
15
10
|
description: 'Confirmation message shown to the user when he wants to delete a credit card',
|
|
@@ -23,15 +18,11 @@ export const useCreditCardsManagement = () => {
|
|
|
23
18
|
const creditCards = useCreditCards();
|
|
24
19
|
|
|
25
20
|
const safeDelete = (creditCard: CreditCard, options?: MutateOptions) => {
|
|
26
|
-
if (creditCard.is_main) {
|
|
27
|
-
creditCards.methods.setError(intl.formatMessage(messages.errorCannotRemoveMain));
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
21
|
const sure = confirm(intl.formatMessage(messages.deletionConfirmation));
|
|
31
22
|
if (!sure) {
|
|
32
23
|
return;
|
|
33
24
|
}
|
|
34
|
-
creditCards.methods.delete(creditCard
|
|
25
|
+
creditCards.methods.delete(creditCard, options);
|
|
35
26
|
};
|
|
36
27
|
|
|
37
28
|
return {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { useSearchParams } from 'react-router';
|
|
3
|
-
import { Enrollment, CredentialOrder,
|
|
3
|
+
import { Enrollment, CredentialOrder, ProductType, CANCELED_ORDER_STATES } from 'types/Joanie';
|
|
4
4
|
import { Maybe, Nullable } from 'types/utils';
|
|
5
5
|
import { useOrdersEnrollments } from 'pages/DashboardCourses/useOrdersEnrollments';
|
|
6
6
|
|
|
@@ -23,7 +23,7 @@ const useLearnerCoursesSearch = () => {
|
|
|
23
23
|
query,
|
|
24
24
|
orderFilters: {
|
|
25
25
|
product_type: [ProductType.CREDENTIAL],
|
|
26
|
-
state_exclude:
|
|
26
|
+
state_exclude: CANCELED_ORDER_STATES,
|
|
27
27
|
},
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -103,7 +103,13 @@ describe('<DashboardCourses/>', () => {
|
|
|
103
103
|
it('renders an empty placeholder', async () => {
|
|
104
104
|
const ordersDeferred = new Deferred();
|
|
105
105
|
fetchMock.get(
|
|
106
|
-
|
|
106
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
107
|
+
'?product_type=credential' +
|
|
108
|
+
'&state_exclude=canceled' +
|
|
109
|
+
'&state_exclude=refunding' +
|
|
110
|
+
'&state_exclude=refunded' +
|
|
111
|
+
'&page=1' +
|
|
112
|
+
`&page_size=${perPage}`,
|
|
107
113
|
ordersDeferred.promise,
|
|
108
114
|
);
|
|
109
115
|
const enrollmentsDeferred = new Deferred();
|
|
@@ -145,25 +151,57 @@ describe('<DashboardCourses/>', () => {
|
|
|
145
151
|
client,
|
|
146
152
|
);
|
|
147
153
|
fetchMock.get(
|
|
148
|
-
|
|
154
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
155
|
+
'?product_type=credential' +
|
|
156
|
+
'&state_exclude=canceled' +
|
|
157
|
+
'&state_exclude=refunding' +
|
|
158
|
+
'&state_exclude=refunded' +
|
|
159
|
+
'&page=1' +
|
|
160
|
+
`&page_size=${perPage}`,
|
|
149
161
|
{
|
|
150
162
|
results: orders.slice(0, perPage),
|
|
151
|
-
next:
|
|
163
|
+
next:
|
|
164
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
165
|
+
'?product_type=credential' +
|
|
166
|
+
'&state_exclude=canceled' +
|
|
167
|
+
'&state_exclude=refunding' +
|
|
168
|
+
'&state_exclude=refunded' +
|
|
169
|
+
'&page=2' +
|
|
170
|
+
`&page_size=${perPage}`,
|
|
152
171
|
previous: null,
|
|
153
172
|
count: orders.length,
|
|
154
173
|
},
|
|
155
174
|
);
|
|
156
175
|
fetchMock.get(
|
|
157
|
-
|
|
176
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
177
|
+
'?product_type=credential' +
|
|
178
|
+
'&state_exclude=canceled' +
|
|
179
|
+
'&state_exclude=refunding' +
|
|
180
|
+
'&state_exclude=refunded' +
|
|
181
|
+
'&page=2' +
|
|
182
|
+
`&page_size=${perPage}`,
|
|
158
183
|
{
|
|
159
184
|
results: orders.slice(perPage, perPage * 2),
|
|
160
|
-
next:
|
|
185
|
+
next:
|
|
186
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
187
|
+
'?product_type=credential' +
|
|
188
|
+
'&state_exclude=canceled' +
|
|
189
|
+
'&state_exclude=refunding' +
|
|
190
|
+
'&state_exclude=refunded' +
|
|
191
|
+
'&page=3' +
|
|
192
|
+
`&page_size=${perPage}`,
|
|
161
193
|
previous: null,
|
|
162
194
|
count: orders.length,
|
|
163
195
|
},
|
|
164
196
|
);
|
|
165
197
|
fetchMock.get(
|
|
166
|
-
|
|
198
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
199
|
+
'?product_type=credential' +
|
|
200
|
+
'&state_exclude=canceled' +
|
|
201
|
+
'&state_exclude=refunding' +
|
|
202
|
+
'&state_exclude=refunded' +
|
|
203
|
+
'&page=3' +
|
|
204
|
+
`&page_size=${perPage}`,
|
|
167
205
|
{
|
|
168
206
|
results: orders.slice(perPage * 2, perPage * 3),
|
|
169
207
|
next: null,
|
|
@@ -237,7 +275,13 @@ describe('<DashboardCourses/>', () => {
|
|
|
237
275
|
jest.spyOn(console, 'error').mockImplementation(noop);
|
|
238
276
|
const ordersDeferred = new Deferred();
|
|
239
277
|
fetchMock.get(
|
|
240
|
-
|
|
278
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
279
|
+
'?product_type=credential' +
|
|
280
|
+
'&state_exclude=canceled' +
|
|
281
|
+
'&state_exclude=refunding' +
|
|
282
|
+
'&state_exclude=refunded' +
|
|
283
|
+
'&page=1' +
|
|
284
|
+
`&page_size=${perPage}`,
|
|
241
285
|
ordersDeferred.promise,
|
|
242
286
|
);
|
|
243
287
|
fetchMock.get(
|
|
@@ -76,11 +76,9 @@ export const DashboardCreditCardBox = ({ creditCard, promote, edit, remove }: Pr
|
|
|
76
76
|
<FormattedMessage {...messages.edit} />
|
|
77
77
|
</Button>
|
|
78
78
|
</div>
|
|
79
|
-
{
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
</Button>
|
|
83
|
-
)}
|
|
79
|
+
<Button color="primary" onClick={() => remove(creditCard)}>
|
|
80
|
+
<FormattedMessage {...messages.delete} />
|
|
81
|
+
</Button>
|
|
84
82
|
</>
|
|
85
83
|
}
|
|
86
84
|
>
|
|
@@ -2,12 +2,23 @@
|
|
|
2
2
|
display: flex;
|
|
3
3
|
flex-direction: column;
|
|
4
4
|
gap: 2rem;
|
|
5
|
+
position: relative;
|
|
5
6
|
|
|
6
7
|
&__empty {
|
|
7
8
|
text-align: center;
|
|
8
9
|
color: r-theme-val(dashboard-credit-cards, empty-color);
|
|
9
10
|
margin: 0;
|
|
10
11
|
}
|
|
12
|
+
|
|
13
|
+
&__loading-overlay {
|
|
14
|
+
position: absolute;
|
|
15
|
+
pointer-events: none;
|
|
16
|
+
inset: 0;
|
|
17
|
+
display: grid;
|
|
18
|
+
place-items: center;
|
|
19
|
+
background-color: rgba(255, 255, 255, 0.8);
|
|
20
|
+
z-index: 9999;
|
|
21
|
+
}
|
|
11
22
|
}
|
|
12
23
|
|
|
13
24
|
.dashboard-credit-card {
|
|
@@ -34,9 +45,6 @@
|
|
|
34
45
|
}
|
|
35
46
|
}
|
|
36
47
|
|
|
37
|
-
&__data {
|
|
38
|
-
}
|
|
39
|
-
|
|
40
48
|
&__brand {
|
|
41
49
|
font-weight: bold;
|
|
42
50
|
|
|
@@ -191,7 +191,7 @@ describe('<DashboardCreditCardsManagement/>', () => {
|
|
|
191
191
|
});
|
|
192
192
|
|
|
193
193
|
it('deletes a credit card', async () => {
|
|
194
|
-
const creditCards = CreditCardFactory().many(
|
|
194
|
+
const creditCards = CreditCardFactory().many(2);
|
|
195
195
|
fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', creditCards);
|
|
196
196
|
render(<DashboardPreferences />, {
|
|
197
197
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
@@ -231,6 +231,46 @@ describe('<DashboardCreditCardsManagement/>', () => {
|
|
|
231
231
|
expect(screen.queryByText(creditCard.title!)).toBeNull();
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
it('deletes a main credit card', async () => {
|
|
235
|
+
const creditCard = CreditCardFactory({ is_main: true }).one();
|
|
236
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', [creditCard]);
|
|
237
|
+
render(<DashboardPreferences />, {
|
|
238
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
239
|
+
});
|
|
240
|
+
// No error is shown.
|
|
241
|
+
expect(screen.queryByText('An error occurred', { exact: false })).toBeNull();
|
|
242
|
+
|
|
243
|
+
// Find the delete button of the credit card.
|
|
244
|
+
await screen.findByText(creditCard.title!);
|
|
245
|
+
const creditCardContainer = screen.getByTestId('dashboard-credit-card__' + creditCard.id);
|
|
246
|
+
const deleteButton = getByRole(creditCardContainer, 'button', {
|
|
247
|
+
name: 'Delete',
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Mock delete route and the refresh route to returns `creditCards` without the first one.
|
|
251
|
+
const deleteUrl = 'https://joanie.endpoint/api/v1.0/credit-cards/' + creditCard.id + '/';
|
|
252
|
+
fetchMock.delete(deleteUrl, []);
|
|
253
|
+
fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', [], {
|
|
254
|
+
overwriteRoutes: true,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Clicking on the delete button calls delete API route.
|
|
258
|
+
expect(fetchMock.called(deleteUrl)).toBe(false);
|
|
259
|
+
// Clicking on the delete button calls window.confirm function.
|
|
260
|
+
expect(confirm).not.toHaveBeenCalled();
|
|
261
|
+
await act(async () => {
|
|
262
|
+
fireEvent.click(deleteButton);
|
|
263
|
+
});
|
|
264
|
+
expect(fetchMock.called(deleteUrl)).toBe(true);
|
|
265
|
+
expect(confirm).toHaveBeenCalled();
|
|
266
|
+
|
|
267
|
+
// No error is shown.
|
|
268
|
+
expect(screen.queryByText('An error occurred', { exact: false })).toBeNull();
|
|
269
|
+
|
|
270
|
+
// The address does not appear anymore in the list.
|
|
271
|
+
expect(screen.queryByText(creditCard.title!)).toBeNull();
|
|
272
|
+
});
|
|
273
|
+
|
|
234
274
|
it('promotes a credit card', async () => {
|
|
235
275
|
const creditCards = CreditCardFactory().many(5);
|
|
236
276
|
fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', creditCards);
|
|
@@ -251,8 +291,9 @@ describe('<DashboardCreditCardsManagement/>', () => {
|
|
|
251
291
|
expect(queryByText(creditCardContainer, 'Default credit card')).toBeNull();
|
|
252
292
|
|
|
253
293
|
// Mock the update url and the refresh URL to return the first credit card as main.
|
|
254
|
-
const
|
|
255
|
-
|
|
294
|
+
const promoteUrl =
|
|
295
|
+
'https://joanie.endpoint/api/v1.0/credit-cards/' + creditCard.id + '/promote/';
|
|
296
|
+
fetchMock.patch(promoteUrl, {});
|
|
256
297
|
fetchMock.get(
|
|
257
298
|
'https://joanie.endpoint/api/v1.0/credit-cards/',
|
|
258
299
|
[{ ...creditCard, is_main: true }, ...creditCards.splice(1)],
|
|
@@ -260,11 +301,11 @@ describe('<DashboardCreditCardsManagement/>', () => {
|
|
|
260
301
|
);
|
|
261
302
|
|
|
262
303
|
// Clicking on the promote button calls the update API route.
|
|
263
|
-
expect(fetchMock.called(
|
|
304
|
+
expect(fetchMock.called(promoteUrl)).toBe(false);
|
|
264
305
|
await act(async () => {
|
|
265
306
|
fireEvent.click(promoteButton);
|
|
266
307
|
});
|
|
267
|
-
expect(fetchMock.called(
|
|
308
|
+
expect(fetchMock.called(promoteUrl)).toBe(true);
|
|
268
309
|
|
|
269
310
|
// Assert that "Default credit card" is displayed on the credit card's box.
|
|
270
311
|
creditCardContainer = screen.getByTestId('dashboard-credit-card__' + creditCard.id);
|
|
@@ -298,26 +339,6 @@ describe('<DashboardCreditCardsManagement/>', () => {
|
|
|
298
339
|
);
|
|
299
340
|
});
|
|
300
341
|
|
|
301
|
-
it('cannot delete a main credit card', async () => {
|
|
302
|
-
const creditCards = CreditCardFactory().many(5);
|
|
303
|
-
const mainCreditCard = creditCards[3];
|
|
304
|
-
mainCreditCard.is_main = true;
|
|
305
|
-
fetchMock.get('https://joanie.endpoint/api/v1.0/credit-cards/', creditCards);
|
|
306
|
-
render(<DashboardPreferences />, {
|
|
307
|
-
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// The delete button is not displayed.
|
|
311
|
-
const creditCardContainer = await screen.findByTestId(
|
|
312
|
-
'dashboard-credit-card__' + mainCreditCard.id,
|
|
313
|
-
);
|
|
314
|
-
expect(
|
|
315
|
-
queryByRole(creditCardContainer, 'button', {
|
|
316
|
-
name: 'Delete',
|
|
317
|
-
}),
|
|
318
|
-
).toBeNull();
|
|
319
|
-
});
|
|
320
|
-
|
|
321
342
|
it('cannot promote a main credit card', async () => {
|
|
322
343
|
const creditCards = CreditCardFactory().many(5);
|
|
323
344
|
const mainCreditCard = creditCards[3];
|
|
@@ -13,11 +13,6 @@ const messages = defineMessages({
|
|
|
13
13
|
description: 'Title of the dashboard credit cards management block',
|
|
14
14
|
defaultMessage: 'Credit cards',
|
|
15
15
|
},
|
|
16
|
-
errorCannotPromoteMain: {
|
|
17
|
-
id: 'components.DashboardCreditCardsManagement.errorCannotPromoteMain',
|
|
18
|
-
description: 'Error shown if a user tries to promote a main credit card',
|
|
19
|
-
defaultMessage: 'Cannot promote main credit card.',
|
|
20
|
-
},
|
|
21
16
|
emptyList: {
|
|
22
17
|
id: 'components.DashboardCreditCardsManagement.emptyList',
|
|
23
18
|
description: 'Empty placeholder of the dashboard credit cards management block',
|
|
@@ -33,7 +28,7 @@ export const DashboardCreditCardsManagement = ({ onClickEdit }: Props) => {
|
|
|
33
28
|
const intl = useIntl();
|
|
34
29
|
const {
|
|
35
30
|
states: { error, isPending },
|
|
36
|
-
methods: {
|
|
31
|
+
methods: { promote, safeDelete },
|
|
37
32
|
...creditCards
|
|
38
33
|
} = useCreditCardsManagement();
|
|
39
34
|
|
|
@@ -54,40 +49,29 @@ export const DashboardCreditCardsManagement = ({ onClickEdit }: Props) => {
|
|
|
54
49
|
return creditCards.items.sort(sortByMainFirstThenByTitle);
|
|
55
50
|
}, [creditCards.items]);
|
|
56
51
|
|
|
57
|
-
const promote = (creditCard: CreditCard) => {
|
|
58
|
-
if (creditCard.is_main) {
|
|
59
|
-
setError(intl.formatMessage(messages.errorCannotPromoteMain));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
update({
|
|
63
|
-
...creditCard,
|
|
64
|
-
is_main: true,
|
|
65
|
-
});
|
|
66
|
-
};
|
|
67
|
-
|
|
68
52
|
return (
|
|
69
53
|
<DashboardCard header={<FormattedMessage {...messages.header} />}>
|
|
70
|
-
<div className="dashboard-credit-cards">
|
|
71
|
-
{isPending &&
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
<DashboardCreditCardBox
|
|
82
|
-
creditCard={creditCard}
|
|
83
|
-
key={creditCard.id}
|
|
84
|
-
edit={(_creditCard) => onClickEdit?.(_creditCard)}
|
|
85
|
-
promote={promote}
|
|
86
|
-
remove={safeDelete}
|
|
87
|
-
/>
|
|
88
|
-
))}
|
|
89
|
-
</>
|
|
54
|
+
<div className="dashboard-credit-cards" aria-busy={isPending}>
|
|
55
|
+
{isPending && (
|
|
56
|
+
<div className="dashboard-credit-cards__loading-overlay">
|
|
57
|
+
<Spinner />
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
{error && <Banner message={error} type={BannerType.ERROR} rounded />}
|
|
61
|
+
{!error && creditCardsList.length === 0 && (
|
|
62
|
+
<p className="dashboard-credit-cards__empty">
|
|
63
|
+
<FormattedMessage {...messages.emptyList} />
|
|
64
|
+
</p>
|
|
90
65
|
)}
|
|
66
|
+
{creditCardsList.map((creditCard) => (
|
|
67
|
+
<DashboardCreditCardBox
|
|
68
|
+
key={creditCard.id}
|
|
69
|
+
creditCard={creditCard}
|
|
70
|
+
edit={(instance) => onClickEdit?.(instance)}
|
|
71
|
+
promote={({ id }) => promote(id)}
|
|
72
|
+
remove={safeDelete}
|
|
73
|
+
/>
|
|
74
|
+
))}
|
|
91
75
|
</div>
|
|
92
76
|
</DashboardCard>
|
|
93
77
|
);
|
package/js/types/Joanie.ts
CHANGED
|
@@ -265,6 +265,8 @@ export interface EnrollmentLight {
|
|
|
265
265
|
export enum OrderState {
|
|
266
266
|
ASSIGNED = 'assigned',
|
|
267
267
|
CANCELED = 'canceled',
|
|
268
|
+
REFUNDING = 'refunding',
|
|
269
|
+
REFUNDED = 'refunded',
|
|
268
270
|
COMPLETED = 'completed',
|
|
269
271
|
DRAFT = 'draft',
|
|
270
272
|
FAILED_PAYMENT = 'failed_payment',
|
|
@@ -294,6 +296,12 @@ export const ACTIVE_ORDER_STATES = [
|
|
|
294
296
|
|
|
295
297
|
export const NOT_CANCELED_ORDER_STATES = [...ACTIVE_ORDER_STATES, ...PURCHASABLE_ORDER_STATES];
|
|
296
298
|
|
|
299
|
+
export const CANCELED_ORDER_STATES = [
|
|
300
|
+
OrderState.CANCELED,
|
|
301
|
+
OrderState.REFUNDING,
|
|
302
|
+
OrderState.REFUNDED,
|
|
303
|
+
];
|
|
304
|
+
|
|
297
305
|
export const ENROLLABLE_ORDER_STATES = [
|
|
298
306
|
OrderState.COMPLETED,
|
|
299
307
|
OrderState.PENDING_PAYMENT,
|
|
@@ -448,8 +456,11 @@ export interface OrderPaymentInfo {
|
|
|
448
456
|
}
|
|
449
457
|
|
|
450
458
|
export enum PaymentScheduleState {
|
|
451
|
-
|
|
459
|
+
CANCELED = 'canceled',
|
|
460
|
+
ERROR = 'error',
|
|
452
461
|
PAID = 'paid',
|
|
462
|
+
PENDING = 'pending',
|
|
463
|
+
REFUNDED = 'refunded',
|
|
453
464
|
REFUSED = 'refused',
|
|
454
465
|
}
|
|
455
466
|
|
|
@@ -591,6 +602,7 @@ interface APIUser {
|
|
|
591
602
|
get(): Promise<CreditCard[]>;
|
|
592
603
|
update(payload: CreditCard): Promise<CreditCard>;
|
|
593
604
|
tokenize(): Promise<Payment>;
|
|
605
|
+
promote(id: CreditCard['id']): Promise<void>;
|
|
594
606
|
};
|
|
595
607
|
orders: {
|
|
596
608
|
cancel(id: Order['id']): Promise<void>;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACTIVE_ORDER_STATES,
|
|
3
|
+
CANCELED_ORDER_STATES,
|
|
3
4
|
ENROLLABLE_ORDER_STATES,
|
|
4
5
|
NestedCourseOrder,
|
|
5
6
|
Order,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
export enum OrderStatus {
|
|
12
13
|
ASSIGNED = 'assigned',
|
|
13
14
|
CANCELED = 'canceled',
|
|
15
|
+
REFUNDED = 'refunded',
|
|
14
16
|
COMPLETED = 'completed',
|
|
15
17
|
DRAFT = 'draft',
|
|
16
18
|
FAILED_PAYMENT = 'failed_payment',
|
|
@@ -38,6 +40,8 @@ export class OrderHelper {
|
|
|
38
40
|
const orderStatusMap = {
|
|
39
41
|
[OrderState.ASSIGNED]: OrderStatus.ASSIGNED,
|
|
40
42
|
[OrderState.CANCELED]: OrderStatus.CANCELED,
|
|
43
|
+
[OrderState.REFUNDING]: OrderStatus.CANCELED,
|
|
44
|
+
[OrderState.REFUNDED]: OrderStatus.REFUNDED,
|
|
41
45
|
[OrderState.COMPLETED]: OrderStatus.COMPLETED,
|
|
42
46
|
[OrderState.DRAFT]: OrderStatus.DRAFT,
|
|
43
47
|
[OrderState.FAILED_PAYMENT]: OrderStatus.FAILED_PAYMENT,
|
|
@@ -91,6 +95,11 @@ export class OrderHelper {
|
|
|
91
95
|
return ACTIVE_ORDER_STATES.includes(order.state);
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
static isCanceled(order?: Order | NestedCourseOrder | OrderEnrollment) {
|
|
99
|
+
if (!order) return false;
|
|
100
|
+
return CANCELED_ORDER_STATES.includes(order.state);
|
|
101
|
+
}
|
|
102
|
+
|
|
94
103
|
static isPurchasable(order?: Order | NestedCourseOrder | OrderEnrollment) {
|
|
95
104
|
if (!order) return true;
|
|
96
105
|
return PURCHASABLE_ORDER_STATES.includes(order.state);
|
|
@@ -228,7 +228,13 @@ describe('<ProductCertificateFooter/>', () => {
|
|
|
228
228
|
// From https://github.com/openfun/richie/issues/2237
|
|
229
229
|
it('should hide purchase button after payment', async () => {
|
|
230
230
|
fetchMock.get(
|
|
231
|
-
|
|
231
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
232
|
+
'?product_type=credential' +
|
|
233
|
+
'&state_exclude=canceled' +
|
|
234
|
+
'&state_exclude=refunding' +
|
|
235
|
+
'&state_exclude=refunded' +
|
|
236
|
+
'&page=1' +
|
|
237
|
+
`&page_size=${PER_PAGE.useOrdersEnrollments}`,
|
|
232
238
|
{
|
|
233
239
|
results: [],
|
|
234
240
|
next: null,
|
|
@@ -86,7 +86,13 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
fetchMock.get(
|
|
89
|
-
'https://joanie.endpoint/api/v1.0/orders
|
|
89
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
90
|
+
'?product_type=credential' +
|
|
91
|
+
'&state_exclude=canceled' +
|
|
92
|
+
'&state_exclude=refunding' +
|
|
93
|
+
'&state_exclude=refunded' +
|
|
94
|
+
'&page=1' +
|
|
95
|
+
'&page_size=50',
|
|
90
96
|
{ results: [order], next: null, previous: null, count: 1 },
|
|
91
97
|
);
|
|
92
98
|
|
|
@@ -259,7 +265,13 @@ describe('<DashboardItemOrder/> Contract', () => {
|
|
|
259
265
|
|
|
260
266
|
// Go back to the list view to make sure the sign button is not shown anymore.
|
|
261
267
|
fetchMock.get(
|
|
262
|
-
'https://joanie.endpoint/api/v1.0/orders
|
|
268
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
269
|
+
'?product_type=credential' +
|
|
270
|
+
'&state_exclude=canceled' +
|
|
271
|
+
'&state_exclude=refunding' +
|
|
272
|
+
'&state_exclude=refunded' +
|
|
273
|
+
'&page=1' +
|
|
274
|
+
'&page_size=50',
|
|
263
275
|
{ results: [signedOrder], next: null, previous: null, count: 1 },
|
|
264
276
|
{ overwriteRoutes: true },
|
|
265
277
|
);
|
|
@@ -61,6 +61,7 @@ type Props = {
|
|
|
61
61
|
|
|
62
62
|
const Installment = ({ order }: Props) => {
|
|
63
63
|
const isActive = OrderHelper.isActive(order);
|
|
64
|
+
const isCanceled = OrderHelper.isCanceled(order);
|
|
64
65
|
const failedInstallment = PaymentScheduleHelper.getFailedInstallment(order.payment_schedule);
|
|
65
66
|
const needsPaymentMethod = order.state === OrderState.TO_SAVE_PAYMENT_METHOD;
|
|
66
67
|
const shouldDisplayDot = needsPaymentMethod || !!failedInstallment;
|
|
@@ -80,13 +81,13 @@ const Installment = ({ order }: Props) => {
|
|
|
80
81
|
<FormattedMessage {...messages.paymentTitle} />
|
|
81
82
|
</span>
|
|
82
83
|
</div>
|
|
83
|
-
{!isActive && !needsPaymentMethod && (
|
|
84
|
+
{!isActive && !isCanceled && !needsPaymentMethod && (
|
|
84
85
|
<p className="dashboard-splitted-card__item__description">
|
|
85
86
|
<FormattedMessage {...messages.paymentInactiveDescription} />
|
|
86
87
|
</p>
|
|
87
88
|
)}
|
|
88
89
|
<PaymentMethodManager order={order} />
|
|
89
|
-
{isActive && <InstallmentManager order={order} />}
|
|
90
|
+
{(isActive || isCanceled) && <InstallmentManager order={order} />}
|
|
90
91
|
</div>
|
|
91
92
|
);
|
|
92
93
|
};
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderPaymentDetailsModal/index.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
12
12
|
import { useState, useEffect } from 'react';
|
|
13
13
|
import { PaymentScheduleGrid } from 'components/PaymentScheduleGrid';
|
|
14
|
-
import { CreditCard, Order } from 'types/Joanie';
|
|
14
|
+
import { ACTIVE_ORDER_STATES, CreditCard, Order, OrderState } from 'types/Joanie';
|
|
15
15
|
import { CreditCardSelector } from 'components/CreditCardSelector';
|
|
16
16
|
import { OrderPaymentRetryModal } from 'widgets/Dashboard/components/DashboardItem/Order/OrderPaymentRetryModal';
|
|
17
17
|
import { Maybe } from 'types/utils';
|
|
@@ -54,14 +54,21 @@ export const OrderPaymentDetailsModal = ({ order, ...props }: PaymentModalProps)
|
|
|
54
54
|
const intl = useIntl();
|
|
55
55
|
const retryModal = useModal();
|
|
56
56
|
const failedInstallment = PaymentScheduleHelper.getFailedInstallment(order.payment_schedule);
|
|
57
|
+
const showPaymentMethod = ACTIVE_ORDER_STATES.filter<OrderState>(
|
|
58
|
+
(state) => state !== OrderState.COMPLETED,
|
|
59
|
+
).includes(order.state);
|
|
57
60
|
|
|
58
61
|
return (
|
|
59
62
|
<>
|
|
60
63
|
<Modal {...props} size={ModalSize.MEDIUM} title={intl.formatMessage(messages.title)}>
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
{showPaymentMethod && (
|
|
65
|
+
<>
|
|
66
|
+
<h3 className="order-payment-details__title mb-s">
|
|
67
|
+
<FormattedMessage {...messages.paymentMethodTitle} />
|
|
68
|
+
</h3>
|
|
69
|
+
<CreditCardSelectorWrapper selectedCreditCardId={order.credit_card_id} />
|
|
70
|
+
</>
|
|
71
|
+
)}
|
|
65
72
|
<h3 className="order-payment-details__title mb-s mt-b">
|
|
66
73
|
<FormattedMessage {...messages.scheduleTitle} />
|
|
67
74
|
</h3>
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx
CHANGED
|
@@ -66,6 +66,11 @@ export const messages = defineMessages<MessageKeys>({
|
|
|
66
66
|
'Status shown on the dashboard order item when order is completed and has a certificate',
|
|
67
67
|
defaultMessage: 'Successfully completed',
|
|
68
68
|
},
|
|
69
|
+
statusRefunded: {
|
|
70
|
+
id: 'components.DashboardItem.Order.OrderStateLearnerMessage.statusRefunded',
|
|
71
|
+
description: 'Status shown on the dashboard order item when order is refunded',
|
|
72
|
+
defaultMessage: 'Refunded',
|
|
73
|
+
},
|
|
69
74
|
});
|
|
70
75
|
|
|
71
76
|
const OrderStateLearnerMessage = (props: OrderStateMessageBaseProps) => {
|
|
@@ -10,18 +10,19 @@ export interface OrderStateMessageBaseProps {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
export type MessageKeys =
|
|
13
|
-
| 'statusDraft'
|
|
14
13
|
| 'statusAssigned'
|
|
14
|
+
| 'statusCanceled'
|
|
15
|
+
| 'statusCompleted'
|
|
16
|
+
| 'statusDraft'
|
|
17
|
+
| 'statusFailedPayment'
|
|
18
|
+
| 'statusNoPayment'
|
|
19
|
+
| 'statusPassed'
|
|
15
20
|
| 'statusPending'
|
|
16
21
|
| 'statusPendingPayment'
|
|
17
|
-
| '
|
|
18
|
-
| 'statusWaitingSignature'
|
|
22
|
+
| 'statusRefunded'
|
|
19
23
|
| 'statusWaitingCounterSignature'
|
|
20
24
|
| 'statusWaitingPaymentMethod'
|
|
21
|
-
| '
|
|
22
|
-
| 'statusNoPayment'
|
|
23
|
-
| 'statusFailedPayment'
|
|
24
|
-
| 'statusPassed';
|
|
25
|
+
| 'statusWaitingSignature';
|
|
25
26
|
|
|
26
27
|
interface OrderStateMessageProps extends OrderStateMessageBaseProps {
|
|
27
28
|
messages: Record<MessageKeys, MessageDescriptor>;
|
|
@@ -37,13 +38,14 @@ const OrderStateMessage = ({ order, messages }: OrderStateMessageProps) => {
|
|
|
37
38
|
const orderStatusMessagesMap = {
|
|
38
39
|
[OrderStatus.ASSIGNED]: messages.statusAssigned,
|
|
39
40
|
[OrderStatus.CANCELED]: messages.statusCanceled,
|
|
40
|
-
[OrderStatus.DRAFT]: messages.statusDraft,
|
|
41
41
|
[OrderStatus.COMPLETED]: messages.statusCompleted,
|
|
42
|
+
[OrderStatus.DRAFT]: messages.statusDraft,
|
|
42
43
|
[OrderStatus.FAILED_PAYMENT]: messages.statusFailedPayment,
|
|
43
44
|
[OrderStatus.NO_PAYMENT]: messages.statusNoPayment,
|
|
44
45
|
[OrderStatus.PASSED]: messages.statusPassed,
|
|
45
46
|
[OrderStatus.PENDING]: messages.statusPending,
|
|
46
47
|
[OrderStatus.PENDING_PAYMENT]: messages.statusPendingPayment,
|
|
48
|
+
[OrderStatus.REFUNDED]: messages.statusRefunded,
|
|
47
49
|
[OrderStatus.WAITING_COUNTER_SIGNATURE]: messages.statusWaitingCounterSignature,
|
|
48
50
|
[OrderStatus.WAITING_PAYMENT_METHOD]: messages.statusWaitingPaymentMethod,
|
|
49
51
|
[OrderStatus.WAITING_SIGNATURE]: messages.statusWaitingSignature,
|
package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx
CHANGED
|
@@ -67,6 +67,11 @@ export const messages = defineMessages<MessageKeys>({
|
|
|
67
67
|
'Status shown on the dashboard order item when order is completed with certificate',
|
|
68
68
|
defaultMessage: 'Certified',
|
|
69
69
|
},
|
|
70
|
+
statusRefunded: {
|
|
71
|
+
id: 'components.DashboardItem.Order.OrderStateTeacherMessage.statusRefunded',
|
|
72
|
+
description: 'Status shown on the dashboard order item when order is refunded',
|
|
73
|
+
defaultMessage: 'Refunded',
|
|
74
|
+
},
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
const OrderStateTeacherMessage = (props: OrderStateMessageBaseProps) => {
|
|
@@ -49,7 +49,13 @@ describe('<Dashboard />', () => {
|
|
|
49
49
|
{ count: 0, results: [] },
|
|
50
50
|
);
|
|
51
51
|
fetchMock.get(
|
|
52
|
-
'https://joanie.endpoint/api/v1.0/orders
|
|
52
|
+
'https://joanie.endpoint/api/v1.0/orders/' +
|
|
53
|
+
'?product_type=credential' +
|
|
54
|
+
'&state_exclude=canceled' +
|
|
55
|
+
'&state_exclude=refunding' +
|
|
56
|
+
'&state_exclude=refunded' +
|
|
57
|
+
'&page=1' +
|
|
58
|
+
'&page_size=50',
|
|
53
59
|
{
|
|
54
60
|
count: 0,
|
|
55
61
|
results: [],
|