richie-education 3.5.1-dev2 → 3.5.2-dev6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/i18n/locales/ar-SA.json +4 -0
  2. package/i18n/locales/es-ES.json +4 -0
  3. package/i18n/locales/fa-IR.json +4 -0
  4. package/i18n/locales/fr-CA.json +4 -0
  5. package/i18n/locales/fr-FR.json +5 -1
  6. package/i18n/locales/ko-KR.json +4 -0
  7. package/i18n/locales/pt-PT.json +4 -0
  8. package/i18n/locales/ru-RU.json +4 -0
  9. package/i18n/locales/vi-VN.json +4 -0
  10. package/js/api/joanie.ts +5 -0
  11. package/js/components/SaleTunnel/index.spec.tsx +1 -1
  12. package/js/hooks/useLearnerCoursesSearch/index.tsx +17 -4
  13. package/js/hooks/useProductOrder/index.spec.tsx +1 -0
  14. package/js/pages/DashboardCourses/index.spec.tsx +40 -8
  15. package/js/translations/ar-SA.json +1 -1
  16. package/js/translations/es-ES.json +1 -1
  17. package/js/translations/fa-IR.json +1 -1
  18. package/js/translations/fr-CA.json +1 -1
  19. package/js/translations/fr-FR.json +1 -1
  20. package/js/translations/ko-KR.json +1 -1
  21. package/js/translations/pt-PT.json +1 -1
  22. package/js/translations/ru-RU.json +1 -1
  23. package/js/translations/vi-VN.json +1 -1
  24. package/js/types/Joanie.ts +18 -1
  25. package/js/utils/OrderHelper/index.spec.ts +75 -0
  26. package/js/utils/OrderHelper/index.ts +14 -2
  27. package/js/utils/test/factories/joanie.ts +10 -0
  28. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +213 -1
  29. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +133 -31
  30. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.spec.tsx +22 -0
  31. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrder.tsx +29 -13
  32. package/js/widgets/Dashboard/components/DashboardItem/Order/DashboardItemOrderContract.useUnionResource.cache.spec.tsx +0 -2
  33. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.spec.tsx +12 -0
  34. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateLearnerMessage/index.tsx +11 -0
  35. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateMessage/index.tsx +5 -1
  36. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.spec.tsx +12 -0
  37. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderStateTeacherMessage/index.tsx +11 -0
  38. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderWithdrawalModal/index.spec.tsx +139 -0
  39. package/js/widgets/Dashboard/components/DashboardItem/Order/OrderWithdrawalModal/index.tsx +259 -0
  40. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.spec.tsx +102 -0
  41. package/js/widgets/Dashboard/components/DashboardItem/Order/OrganizationBlock/index.tsx +84 -2
  42. package/js/widgets/Dashboard/components/DashboardItem/_styles.scss +9 -1
  43. package/js/widgets/Dashboard/index.spec.tsx +0 -1
  44. package/package.json +1 -1
@@ -1587,6 +1587,10 @@
1587
1587
  "description": "Label of the submit button on openEdx full name form",
1588
1588
  "message": "Submit"
1589
1589
  },
1590
+ "components.OrganizationQuotesTable.batchOrder.seatsCount": {
1591
+ "description": "Text displayed for seats count with pluralization in batch order",
1592
+ "message": "{seats} {seats, plural, one {seat} other {seats}}"
1593
+ },
1590
1594
  "components.OrganizationQuotesTable.batchOrderId": {
1591
1595
  "description": "Label for the batch order reference (id)",
1592
1596
  "message": "Batch order id: {id}"
@@ -1587,6 +1587,10 @@
1587
1587
  "description": "Label of the submit button on openEdx full name form",
1588
1588
  "message": "Submit"
1589
1589
  },
1590
+ "components.OrganizationQuotesTable.batchOrder.seatsCount": {
1591
+ "description": "Text displayed for seats count with pluralization in batch order",
1592
+ "message": "{seats} {seats, plural, one {seat} other {seats}}"
1593
+ },
1590
1594
  "components.OrganizationQuotesTable.batchOrderId": {
1591
1595
  "description": "Label for the batch order reference (id)",
1592
1596
  "message": "Batch order id: {id}"
@@ -1587,6 +1587,10 @@
1587
1587
  "description": "Label of the submit button on openEdx full name form",
1588
1588
  "message": "Submit"
1589
1589
  },
1590
+ "components.OrganizationQuotesTable.batchOrder.seatsCount": {
1591
+ "description": "Text displayed for seats count with pluralization in batch order",
1592
+ "message": "{seats} {seats, plural, one {seat} other {seats}}"
1593
+ },
1590
1594
  "components.OrganizationQuotesTable.batchOrderId": {
1591
1595
  "description": "Label for the batch order reference (id)",
1592
1596
  "message": "Batch order id: {id}"
@@ -1587,6 +1587,10 @@
1587
1587
  "description": "Label of the submit button on openEdx full name form",
1588
1588
  "message": "Submit"
1589
1589
  },
1590
+ "components.OrganizationQuotesTable.batchOrder.seatsCount": {
1591
+ "description": "Text displayed for seats count with pluralization in batch order",
1592
+ "message": "{seats} {seats, plural, one {seat} other {seats}}"
1593
+ },
1590
1594
  "components.OrganizationQuotesTable.batchOrderId": {
1591
1595
  "description": "Label for the batch order reference (id)",
1592
1596
  "message": "Batch order id: {id}"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "16uca+": {
3
3
  "description": "",
4
- "message": "Sub \"{value}\""
4
+ "message": "Sous \"{value}\""
5
5
  },
6
6
  "9vqPaF": {
7
7
  "description": "",
@@ -1587,6 +1587,10 @@
1587
1587
  "description": "Label of the submit button on openEdx full name form",
1588
1588
  "message": "Sauvegarder"
1589
1589
  },
1590
+ "components.OrganizationQuotesTable.batchOrder.seatsCount": {
1591
+ "description": "Text displayed for seats count with pluralization in batch order",
1592
+ "message": "{seats} {seats, plural, one {place} other {places}}"
1593
+ },
1590
1594
  "components.OrganizationQuotesTable.batchOrderId": {
1591
1595
  "description": "Label for the batch order reference (id)",
1592
1596
  "message": "Référence de commande : {id}"
@@ -1587,6 +1587,10 @@
1587
1587
  "description": "Label of the submit button on openEdx full name form",
1588
1588
  "message": "Submit"
1589
1589
  },
1590
+ "components.OrganizationQuotesTable.batchOrder.seatsCount": {
1591
+ "description": "Text displayed for seats count with pluralization in batch order",
1592
+ "message": "{seats} {seats, plural, one {seat} other {seats}}"
1593
+ },
1590
1594
  "components.OrganizationQuotesTable.batchOrderId": {
1591
1595
  "description": "Label for the batch order reference (id)",
1592
1596
  "message": "Batch order id: {id}"
@@ -1587,6 +1587,10 @@
1587
1587
  "description": "Label of the submit button on openEdx full name form",
1588
1588
  "message": "Submeter"
1589
1589
  },
1590
+ "components.OrganizationQuotesTable.batchOrder.seatsCount": {
1591
+ "description": "Text displayed for seats count with pluralization in batch order",
1592
+ "message": "{seats} {seats, plural, one {seat} other {seats}}"
1593
+ },
1590
1594
  "components.OrganizationQuotesTable.batchOrderId": {
1591
1595
  "description": "Label for the batch order reference (id)",
1592
1596
  "message": "Batch order id: {id}"
@@ -1587,6 +1587,10 @@
1587
1587
  "description": "Label of the submit button on openEdx full name form",
1588
1588
  "message": "Submit"
1589
1589
  },
1590
+ "components.OrganizationQuotesTable.batchOrder.seatsCount": {
1591
+ "description": "Text displayed for seats count with pluralization in batch order",
1592
+ "message": "{seats} {seats, plural, one {seat} other {seats}}"
1593
+ },
1590
1594
  "components.OrganizationQuotesTable.batchOrderId": {
1591
1595
  "description": "Label for the batch order reference (id)",
1592
1596
  "message": "Batch order id: {id}"
@@ -1587,6 +1587,10 @@
1587
1587
  "description": "Label of the submit button on openEdx full name form",
1588
1588
  "message": "Submit"
1589
1589
  },
1590
+ "components.OrganizationQuotesTable.batchOrder.seatsCount": {
1591
+ "description": "Text displayed for seats count with pluralization in batch order",
1592
+ "message": "{seats} {seats, plural, one {seat} other {seats}}"
1593
+ },
1590
1594
  "components.OrganizationQuotesTable.batchOrderId": {
1591
1595
  "description": "Label for the batch order reference (id)",
1592
1596
  "message": "Batch order id: {id}"
package/js/api/joanie.ts CHANGED
@@ -101,6 +101,7 @@ export const getRoutes = () => {
101
101
  submit_for_signature: `${baseUrl}/orders/:id/submit_for_signature/`,
102
102
  submit_installment_payment: `${baseUrl}/orders/:id/submit-installment-payment/`,
103
103
  set_payment_method: `${baseUrl}/orders/:id/payment-method/`,
104
+ withdraw: `${baseUrl}/orders/:id/withdraw/`,
104
105
  },
105
106
  batchOrders: {
106
107
  get: `${baseUrl}/batch-orders/:id/`,
@@ -340,6 +341,10 @@ const API = (): Joanie.API => {
340
341
  method: 'POST',
341
342
  body: JSON.stringify(payload),
342
343
  }).then(checkStatus),
344
+ withdraw: async (id) =>
345
+ fetchWithJWT(ROUTES.user.orders.withdraw.replace(':id', id), {
346
+ method: 'POST',
347
+ }).then(checkStatus),
343
348
  },
344
349
  batchOrders: {
345
350
  create: async (payload) =>
@@ -552,7 +552,7 @@ describe.each([
552
552
  enrollmentDiscounted.offerings[0].product = product;
553
553
  fetchMock
554
554
  .get(
555
- `https://joanie.endpoint/api/v1.0/orders/?enrollment_id=${enrollmentDiscounted.id}&product_id=${product.id}&state=pending&state=pending_payment&state=no_payment&state=failed_payment&state=completed&state=draft&state=assigned&state=to_sign&state=signing&state=to_save_payment_method`,
555
+ `https://joanie.endpoint/api/v1.0/orders/?enrollment_id=${enrollmentDiscounted.id}&product_id=${product.id}&state=pending&state=pending_payment&state=no_payment&state=failed_payment&state=completed&state=pending_withdraw&state=draft&state=assigned&state=to_sign&state=signing&state=to_save_payment_method`,
556
556
  {
557
557
  results: [],
558
558
  next: null,
@@ -1,8 +1,18 @@
1
1
  import { useEffect, useState } from 'react';
2
2
  import { useSearchParams } from 'react-router';
3
- import { Enrollment, CredentialOrder, ProductType, CANCELED_ORDER_STATES } from 'types/Joanie';
3
+ import { Enrollment, CredentialOrder, ProductType, OrderState } from 'types/Joanie';
4
4
  import { Maybe, Nullable } from 'types/utils';
5
- import { useOrdersEnrollments } from 'pages/DashboardCourses/useOrdersEnrollments';
5
+ import { OrderHelper } from 'utils/OrderHelper';
6
+ import { isOrder, useOrdersEnrollments } from 'pages/DashboardCourses/useOrdersEnrollments';
7
+
8
+ /**
9
+ * Orders canceled following the buyer's withdrawal must stay visible in the dashboard
10
+ * (the learner needs to keep a trace of that legal action), unlike other cancellation
11
+ * causes. The API can't filter on that distinction yet, so we only exclude REFUNDING/
12
+ * REFUNDED server-side and drop the remaining non-withdrawn CANCELED orders here.
13
+ */
14
+ const isHiddenCanceledOrder = (item: CredentialOrder | Enrollment) =>
15
+ isOrder(item) && OrderHelper.isCanceled(item) && !OrderHelper.isWithdrawn(item);
6
16
 
7
17
  const useLearnerCoursesSearch = () => {
8
18
  const [searchParams, setSearchParams] = useSearchParams();
@@ -23,7 +33,7 @@ const useLearnerCoursesSearch = () => {
23
33
  query,
24
34
  orderFilters: {
25
35
  product_type: [ProductType.CREDENTIAL],
26
- state_exclude: CANCELED_ORDER_STATES,
36
+ state_exclude: [OrderState.REFUNDING, OrderState.REFUNDED],
27
37
  },
28
38
  });
29
39
 
@@ -41,7 +51,10 @@ const useLearnerCoursesSearch = () => {
41
51
  }
42
52
 
43
53
  if (isNewSearchLoading || data.length > orderAndEnrollmentList?.length) {
44
- setOrderAndEnrollmentList(data as (CredentialOrder | Enrollment)[]);
54
+ const visibleData = (data as (CredentialOrder | Enrollment)[]).filter(
55
+ (item) => !isHiddenCanceledOrder(item),
56
+ );
57
+ setOrderAndEnrollmentList(visibleData);
45
58
  setCount(currentCount);
46
59
  }
47
60
  }, [data.length, isLoading, isNewSearchLoading, query]);
@@ -84,6 +84,7 @@ describe('useProductOrder', () => {
84
84
  `&state=${OrderState.NO_PAYMENT}` +
85
85
  `&state=${OrderState.FAILED_PAYMENT}` +
86
86
  `&state=${OrderState.COMPLETED}` +
87
+ `&state=${OrderState.PENDING_WITHDRAW}` +
87
88
  `&state=${OrderState.DRAFT}` +
88
89
  `&state=${OrderState.ASSIGNED}` +
89
90
  `&state=${OrderState.TO_SIGN}` +
@@ -10,7 +10,7 @@ import {
10
10
  CredentialOrderFactory,
11
11
  } from 'utils/test/factories/joanie';
12
12
  import { createTestQueryClient } from 'utils/test/createTestQueryClient';
13
- import { CourseLight, Offering, Enrollment, CredentialOrder } from 'types/Joanie';
13
+ import { CourseLight, Offering, Enrollment, CredentialOrder, OrderState } from 'types/Joanie';
14
14
  import { expectNoSpinner, expectSpinner } from 'utils/test/expectSpinner';
15
15
  import { expectBannerError, expectBannerInfo, expectNoBannerInfo } from 'utils/test/expectBanner';
16
16
  import { Deferred } from 'utils/test/deferred';
@@ -105,7 +105,6 @@ describe('<DashboardCourses/>', () => {
105
105
  fetchMock.get(
106
106
  'https://joanie.endpoint/api/v1.0/orders/' +
107
107
  '?product_type=credential' +
108
- '&state_exclude=canceled' +
109
108
  '&state_exclude=refunding' +
110
109
  '&state_exclude=refunded' +
111
110
  '&page=1' +
@@ -153,7 +152,6 @@ describe('<DashboardCourses/>', () => {
153
152
  fetchMock.get(
154
153
  'https://joanie.endpoint/api/v1.0/orders/' +
155
154
  '?product_type=credential' +
156
- '&state_exclude=canceled' +
157
155
  '&state_exclude=refunding' +
158
156
  '&state_exclude=refunded' +
159
157
  '&page=1' +
@@ -163,7 +161,6 @@ describe('<DashboardCourses/>', () => {
163
161
  next:
164
162
  'https://joanie.endpoint/api/v1.0/orders/' +
165
163
  '?product_type=credential' +
166
- '&state_exclude=canceled' +
167
164
  '&state_exclude=refunding' +
168
165
  '&state_exclude=refunded' +
169
166
  '&page=2' +
@@ -175,7 +172,6 @@ describe('<DashboardCourses/>', () => {
175
172
  fetchMock.get(
176
173
  'https://joanie.endpoint/api/v1.0/orders/' +
177
174
  '?product_type=credential' +
178
- '&state_exclude=canceled' +
179
175
  '&state_exclude=refunding' +
180
176
  '&state_exclude=refunded' +
181
177
  '&page=2' +
@@ -185,7 +181,6 @@ describe('<DashboardCourses/>', () => {
185
181
  next:
186
182
  'https://joanie.endpoint/api/v1.0/orders/' +
187
183
  '?product_type=credential' +
188
- '&state_exclude=canceled' +
189
184
  '&state_exclude=refunding' +
190
185
  '&state_exclude=refunded' +
191
186
  '&page=3' +
@@ -197,7 +192,6 @@ describe('<DashboardCourses/>', () => {
197
192
  fetchMock.get(
198
193
  'https://joanie.endpoint/api/v1.0/orders/' +
199
194
  '?product_type=credential' +
200
- '&state_exclude=canceled' +
201
195
  '&state_exclude=refunding' +
202
196
  '&state_exclude=refunded' +
203
197
  '&page=3' +
@@ -271,13 +265,51 @@ describe('<DashboardCourses/>', () => {
271
265
  expect(loadMoreButton).toBeEnabled();
272
266
  }, 15000);
273
267
 
268
+ it('keeps a withdrawn order visible while filtering out other canceled orders', async () => {
269
+ const activeOrder = CredentialOrderFactory({
270
+ state: OrderState.COMPLETED,
271
+ created_on: '2026-01-03T00:00:00.000Z',
272
+ }).one();
273
+ const withdrawnOrder = CredentialOrderFactory({
274
+ state: OrderState.CANCELED,
275
+ created_on: '2026-01-02T00:00:00.000Z',
276
+ withdrawn_confirmation_at: '2026-01-02T00:00:00.000Z',
277
+ }).one();
278
+ const canceledOrder = CredentialOrderFactory({
279
+ state: OrderState.CANCELED,
280
+ created_on: '2026-01-01T00:00:00.000Z',
281
+ withdrawn_confirmation_at: null,
282
+ }).one();
283
+ const { orders, offerings } = mockOrders([activeOrder, withdrawnOrder, canceledOrder]);
284
+
285
+ fetchMock.get(
286
+ 'https://joanie.endpoint/api/v1.0/orders/' +
287
+ '?product_type=credential' +
288
+ '&state_exclude=refunding' +
289
+ '&state_exclude=refunded' +
290
+ '&page=1' +
291
+ `&page_size=${perPage}`,
292
+ { results: orders, next: null, previous: null, count: orders.length },
293
+ );
294
+ fetchMock.get(
295
+ `https://joanie.endpoint/api/v1.0/enrollments/?was_created_by_order=false&is_active=true&page=1&page_size=${perPage}`,
296
+ { results: [], next: null, previous: null, count: 0 },
297
+ );
298
+
299
+ render(<DashboardTest initialRoute={LearnerDashboardPaths.COURSES} />, {
300
+ wrapper: BaseJoanieAppWrapper,
301
+ });
302
+
303
+ await expectNoSpinner('Loading orders and enrollments...');
304
+ await waitFor(() => expectList([activeOrder, withdrawnOrder], offerings));
305
+ });
306
+
274
307
  it('shows an error', async () => {
275
308
  jest.spyOn(console, 'error').mockImplementation(noop);
276
309
  const ordersDeferred = new Deferred();
277
310
  fetchMock.get(
278
311
  'https://joanie.endpoint/api/v1.0/orders/' +
279
312
  '?product_type=credential' +
280
- '&state_exclude=canceled' +
281
313
  '&state_exclude=refunding' +
282
314
  '&state_exclude=refunded' +
283
315
  '&page=1' +