richie-education 2.28.0 → 2.28.2-dev23

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 (36) hide show
  1. package/i18n/locales/fr-FR.json +10 -10
  2. package/js/components/AddressesManagement/AddressForm/validationSchema.spec.ts +2 -2
  3. package/js/components/PaymentInterfaces/LyraPopIn.tsx +10 -3
  4. package/js/components/PaymentInterfaces/types.ts +1 -1
  5. package/js/components/PurchaseButton/index.spec.tsx +9 -9
  6. package/js/components/PurchaseButton/index.tsx +2 -1
  7. package/js/components/SaleTunnel/GenericPaymentButton/index.tsx +13 -3
  8. package/js/components/SaleTunnel/hooks/useTerms.tsx +2 -2
  9. package/js/components/SaleTunnel/index.credential.spec.tsx +2 -2
  10. package/js/components/SaleTunnel/index.full-process.spec.tsx +11 -4
  11. package/js/components/SaleTunnel/index.spec.tsx +2 -2
  12. package/js/components/SaleTunnel/index.stories.tsx +3 -2
  13. package/js/components/SaleTunnel/index.tsx +2 -2
  14. package/js/translations/fr-FR.json +1 -1
  15. package/js/types/commonDataProps.ts +0 -1
  16. package/js/types/index.ts +6 -0
  17. package/js/utils/CourseRunHelper/index.spec.ts +35 -0
  18. package/js/utils/CourseRunHelper/index.ts +13 -0
  19. package/js/utils/react-query/useSessionQuery/index.ts +1 -1
  20. package/js/utils/test/factories/richie.ts +16 -2
  21. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.spec.tsx +35 -5
  22. package/js/widgets/Dashboard/components/DashboardItem/Enrollment/ProductCertificateFooter/index.tsx +11 -10
  23. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/CourseProductItemFooter/index.tsx +3 -2
  24. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.spec.tsx +32 -30
  25. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.stories.tsx +5 -6
  26. package/js/widgets/SyllabusCourseRunsList/components/CourseProductItem/index.tsx +3 -2
  27. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.login.spec.tsx +5 -3
  28. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.logout.spec.tsx +5 -3
  29. package/js/widgets/SyllabusCourseRunsList/components/CourseWishButton/index.tsx +2 -2
  30. package/js/widgets/SyllabusCourseRunsList/components/SyllabusAsideList/index.tsx +11 -4
  31. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRun/index.tsx +20 -9
  32. package/js/widgets/SyllabusCourseRunsList/components/SyllabusCourseRunCompacted/index.tsx +111 -0
  33. package/js/widgets/SyllabusCourseRunsList/index.spec.tsx +311 -15
  34. package/js/widgets/SyllabusCourseRunsList/index.tsx +24 -8
  35. package/package.json +44 -44
  36. package/tsconfig.json +1 -1
@@ -125,7 +125,7 @@
125
125
  },
126
126
  "components.ContractStatus.learnerSignedOn": {
127
127
  "description": "Label for the date of sign of a training contract by the learner",
128
- "message": "Vous avez signé ce contrat de formation. Signée le {date}"
128
+ "message": "Vous avez signé ce contrat de formation. Signé le {date}"
129
129
  },
130
130
  "components.ContractStatus.organizationSignedOn": {
131
131
  "description": "Label for the date of sign of a training contract by the organization",
@@ -253,7 +253,7 @@
253
253
  },
254
254
  "components.CourseRunEnrollment.enroll": {
255
255
  "description": "CTA for users who can enroll in the course run or could enroll if they logged in.",
256
- "message": "S’inscrire maintenant"
256
+ "message": "Je m'inscris"
257
257
  },
258
258
  "components.CourseRunEnrollment.enrolled": {
259
259
  "description": "Help text for users who see the \"Go to course\" CTA on course run enrollment",
@@ -281,7 +281,7 @@
281
281
  },
282
282
  "components.CourseRunEnrollment.loginToEnroll": {
283
283
  "description": "Helper text in the enroll button for non logged in users",
284
- "message": "Connectez-vous pour vous inscrire"
284
+ "message": "Je me connecte pour m’inscrire"
285
285
  },
286
286
  "components.CourseRunEnrollment.unenroll": {
287
287
  "description": "Help text below the \"Unenroll now\" CTA when an enrollment attempt has already failed.",
@@ -1209,7 +1209,7 @@
1209
1209
  },
1210
1210
  "components.SaleTunnel.loginToPurchase": {
1211
1211
  "description": "Label displayed inside the product's CTA when user is not logged in",
1212
- "message": "Connectez-vous pour acheter {product}"
1212
+ "message": "Je me connecte pour m’inscrire {product}"
1213
1213
  },
1214
1214
  "components.SaleTunnel.noCourseRunToPurchaseCertificate": {
1215
1215
  "description": "Label displayed below the product's CTA when there is no courseRun",
@@ -1261,7 +1261,7 @@
1261
1261
  },
1262
1262
  "components.SaleTunnelSuccess.successDetailMessage": {
1263
1263
  "description": "Text to remind that order's invoice will be send by email soon",
1264
- "message": "Vous allez recevoir votre facture par mail dans quelques instants."
1264
+ "message": "Vous allez recevoir votre preuve de paiement par mail dans quelques instants."
1265
1265
  },
1266
1266
  "components.SaleTunnelSuccess.successDetailSignatureMessage": {
1267
1267
  "description": "Text to remind that order needs to be signed",
@@ -1281,7 +1281,7 @@
1281
1281
  },
1282
1282
  "components.SaleTunnelSuccessNotValidated.description": {
1283
1283
  "description": "Text to remind that order's invoice will be send by email soon",
1284
- "message": "Votre paiement a été accepté mais la validation de votre commande prend plus de temps que prévu, vous pouvez fermer cette fenêtre et revenir plus tard. Vous allez recevoir votre facture par mail dans quelques instants."
1284
+ "message": "Votre paiement a été accepté mais la validation de votre commande prend plus de temps que prévu, vous pouvez fermer cette fenêtre et revenir plus tard. Vous allez recevoir votre preuve de paiement par mail dans quelques instants."
1285
1285
  },
1286
1286
  "components.SaleTunnelSuccessNotValidated.title": {
1287
1287
  "description": "Message to confirm that order has been created",
@@ -1429,7 +1429,7 @@
1429
1429
  },
1430
1430
  "components.SyllabusCourseRun.coursePeriod": {
1431
1431
  "description": "Course date of an opened course run block",
1432
- "message": "From {startDate} {endDate, select, undefined {} other {to {endDate}}}"
1432
+ "message": "Du {startDate} {endDate, select, undefined {} other {au {endDate}}}"
1433
1433
  },
1434
1434
  "components.SyllabusCourseRun.enrollment": {
1435
1435
  "description": "Title of the enrollment dates section of an opened course run block",
@@ -1437,7 +1437,7 @@
1437
1437
  },
1438
1438
  "components.SyllabusCourseRun.enrollmentPeriod": {
1439
1439
  "description": "Enrollment date of an opened course run block",
1440
- "message": "From {startDate} {endDate, select, undefined {} other {to {endDate}}}"
1440
+ "message": "Du {startDate} {endDate, select, undefined {} other {au {endDate}}}"
1441
1441
  },
1442
1442
  "components.SyllabusCourseRun.languages": {
1443
1443
  "description": "Title of the languages section of an opened course run block",
@@ -2025,7 +2025,7 @@
2025
2025
  },
2026
2026
  "utils.ContractHelper.learnerSigned": {
2027
2027
  "description": "Label for signed contract status in learner point of view",
2028
- "message": "Signée"
2028
+ "message": "Signé"
2029
2029
  },
2030
2030
  "utils.ContractHelper.learnerUnsigned": {
2031
2031
  "description": "Label for unsigned contract status in learner point of view",
@@ -2037,7 +2037,7 @@
2037
2037
  },
2038
2038
  "utils.ContractHelper.organizationSigned": {
2039
2039
  "description": "Label for signed contract status in organization point of view",
2040
- "message": "Signée"
2040
+ "message": "Signé"
2041
2041
  },
2042
2042
  "utils.ContractHelper.organizationUnsigned": {
2043
2043
  "description": "Label for unsigned contract status in organization point of view",
@@ -15,7 +15,7 @@ describe('validationSchema', () => {
15
15
  first_name: faker.person.firstName(),
16
16
  last_name: faker.person.lastName(),
17
17
  postcode: faker.location.zipCode(),
18
- title: faker.lorem.word(),
18
+ title: faker.lorem.word({ length: { min: 2, max: 15 } }),
19
19
  save: false,
20
20
  };
21
21
 
@@ -130,7 +130,7 @@ describe('validationSchema', () => {
130
130
  result.current.setValue('first_name', faker.person.firstName());
131
131
  result.current.setValue('last_name', faker.person.lastName());
132
132
  result.current.setValue('postcode', faker.location.zipCode());
133
- result.current.setValue('title', faker.lorem.word());
133
+ result.current.setValue('title', faker.lorem.word({ length: { min: 2, max: 15 } }));
134
134
  result.current.trigger();
135
135
  });
136
136
 
@@ -25,11 +25,13 @@ const LyraPopIn = ({
25
25
  const intl = useIntl();
26
26
  const shouldAbort = useRef<Boolean>(true);
27
27
 
28
- const handleError = (error?: Error) => {
29
- if (error) handle(`[LyraPopIn] - ${error}`);
28
+ const handleError = (error?: Error | string) => {
29
+ if (error && typeof error === 'string') handle(`[LyraPopIn] - ${error}`);
30
30
 
31
31
  if (shouldAbort.current) {
32
32
  onError(PaymentErrorMessageId.ERROR_ABORTING);
33
+ } else if (typeof error === 'string') {
34
+ onError(error);
33
35
  } else {
34
36
  onError(PaymentErrorMessageId.ERROR_DEFAULT);
35
37
  }
@@ -95,8 +97,13 @@ const LyraPopIn = ({
95
97
  // Do not close the pop-in if the error is a invalid data error (CLIENT_3XX).
96
98
  // https://docs.lyra.com/fr/rest/V4.0/javascript/features/js_error_management.html#client004
97
99
  if (!error.errorCode.startsWith('CLIENT_3')) {
100
+ shouldAbort.current = false;
98
101
  await KR.closePopin(formId);
99
- handleError();
102
+ let errorMessages = error.errorMessage;
103
+ if (error.detailedErrorMessage) {
104
+ errorMessages += `: ${error.detailedErrorMessage}`;
105
+ }
106
+ handleError(errorMessages);
100
107
  }
101
108
  };
102
109
 
@@ -42,5 +42,5 @@ export type Payment = DummyPayment | PayplugPayment | LyraPayment;
42
42
 
43
43
  export type PaymentInterfaceProps<P extends Payment = Payment> = P & {
44
44
  onSuccess: () => void;
45
- onError: (messageId: PaymentErrorMessageId) => void;
45
+ onError: (messageId: string | PaymentErrorMessageId) => void;
46
46
  };
@@ -6,12 +6,12 @@ import userEvent from '@testing-library/user-event';
6
6
  import { CunninghamProvider } from '@openfun/cunningham-react';
7
7
  import {
8
8
  CourseStateFactory,
9
+ PacedCourseFactory,
9
10
  UserFactory,
10
11
  RichieContextFactory as mockRichieContextFactory,
11
12
  } from 'utils/test/factories/richie';
12
13
  import {
13
14
  CertificateProductFactory,
14
- CourseLightFactory,
15
15
  EnrollmentFactory,
16
16
  ProductFactory,
17
17
  } from 'utils/test/factories/joanie';
@@ -89,7 +89,7 @@ describe('PurchaseButton', () => {
89
89
  <PurchaseButton
90
90
  product={product}
91
91
  disabled={false}
92
- course={CourseLightFactory({ code: '00000' }).one()}
92
+ course={PacedCourseFactory({ code: '00000' }).one()}
93
93
  />
94
94
  </Wrapper>,
95
95
  );
@@ -112,7 +112,7 @@ describe('PurchaseButton', () => {
112
112
  <PurchaseButton
113
113
  product={product}
114
114
  disabled={false}
115
- course={CourseLightFactory({ code: courseCode }).one()}
115
+ course={PacedCourseFactory({ code: courseCode }).one()}
116
116
  />
117
117
  </Wrapper>,
118
118
  );
@@ -146,7 +146,7 @@ describe('PurchaseButton', () => {
146
146
  <PurchaseButton
147
147
  product={product}
148
148
  disabled={false}
149
- course={CourseLightFactory({ code: courseCode }).one()}
149
+ course={PacedCourseFactory({ code: courseCode }).one()}
150
150
  />
151
151
  </Wrapper>,
152
152
  );
@@ -181,7 +181,7 @@ describe('PurchaseButton', () => {
181
181
  <PurchaseButton
182
182
  product={product}
183
183
  disabled={false}
184
- course={CourseLightFactory({ code: courseCode }).one()}
184
+ course={PacedCourseFactory({ code: courseCode }).one()}
185
185
  />
186
186
  </Wrapper>,
187
187
  );
@@ -214,7 +214,7 @@ describe('PurchaseButton', () => {
214
214
  <PurchaseButton
215
215
  product={product}
216
216
  disabled={false}
217
- course={CourseLightFactory({ code: courseCode }).one()}
217
+ course={PacedCourseFactory({ code: courseCode }).one()}
218
218
  />
219
219
  </Wrapper>,
220
220
  );
@@ -251,7 +251,7 @@ describe('PurchaseButton', () => {
251
251
  <PurchaseButton
252
252
  product={product}
253
253
  disabled={false}
254
- course={CourseLightFactory({ code: courseCode }).one()}
254
+ course={PacedCourseFactory({ code: courseCode }).one()}
255
255
  />
256
256
  </Wrapper>,
257
257
  );
@@ -384,7 +384,7 @@ describe('PurchaseButton', () => {
384
384
  <PurchaseButton
385
385
  product={product}
386
386
  disabled={false}
387
- course={CourseLightFactory({ code: courseCode }).one()}
387
+ course={PacedCourseFactory({ code: courseCode }).one()}
388
388
  />
389
389
  </Wrapper>,
390
390
  );
@@ -414,7 +414,7 @@ describe('PurchaseButton', () => {
414
414
  <PurchaseButton
415
415
  product={product}
416
416
  disabled={true}
417
- course={CourseLightFactory({ code: courseCode }).one()}
417
+ course={PacedCourseFactory({ code: courseCode }).one()}
418
418
  />
419
419
  </Wrapper>,
420
420
  );
@@ -7,6 +7,7 @@ import * as Joanie from 'types/Joanie';
7
7
  import { isOpenedCourseRunCertificate, isOpenedCourseRunCredential } from 'utils/CourseRuns';
8
8
  import { SaleTunnel, SaleTunnelProps } from 'components/SaleTunnel';
9
9
  import { Organization } from 'types/Joanie';
10
+ import { PacedCourse } from 'types';
10
11
 
11
12
  const messages = defineMessages({
12
13
  loginToPurchase: {
@@ -52,7 +53,7 @@ interface PurchaseButtonPropsBase {
52
53
 
53
54
  interface CredentialPurchaseButtonProps extends PurchaseButtonPropsBase {
54
55
  product: Joanie.CredentialProduct;
55
- course: Joanie.CourseLight;
56
+ course: PacedCourse;
56
57
  enrollment?: undefined;
57
58
  }
58
59
 
@@ -97,7 +97,7 @@ export const GenericPaymentButton = ({ buildOrderPayload }: Props) => {
97
97
  const { methods: orderMethods } = useOrders(undefined, { enabled: false });
98
98
  const [payment, setPayment] = useState<PaymentInfo>();
99
99
  const [state, setState] = useState<ComponentStates>(ComponentStates.IDLE);
100
- const [error, setError] = useState<PaymentErrorMessageId>();
100
+ const [error, setError] = useState<PaymentErrorMessageId | string>();
101
101
  const hasPaymentId = (p: Maybe<Payment>): p is Extract<Payment, PaymentWithId> => {
102
102
  return Boolean(p?.hasOwnProperty('payment_id'));
103
103
  };
@@ -256,7 +256,9 @@ export const GenericPaymentButton = ({ buildOrderPayload }: Props) => {
256
256
  checkOrderValidity();
257
257
  };
258
258
 
259
- const handleError = (messageId: PaymentErrorMessageId = PaymentErrorMessageId.ERROR_DEFAULT) => {
259
+ const handleError = (
260
+ messageId: PaymentErrorMessageId | string = PaymentErrorMessageId.ERROR_DEFAULT,
261
+ ) => {
260
262
  setState(ComponentStates.ERROR);
261
263
  setError(messageId);
262
264
  };
@@ -274,6 +276,8 @@ export const GenericPaymentButton = ({ buildOrderPayload }: Props) => {
274
276
  .catch(() => {
275
277
  handleError();
276
278
  });
279
+ } else if (error && !messages.hasOwnProperty(error)) {
280
+ orderMethods.invalidate();
277
281
  } else if (state === ComponentStates.ERROR) {
278
282
  setPayment(undefined);
279
283
  }
@@ -320,7 +324,13 @@ export const GenericPaymentButton = ({ buildOrderPayload }: Props) => {
320
324
  )}
321
325
  {state === ComponentStates.ERROR && (
322
326
  <p className="payment-button__error" id="sale-tunnel-payment-error" tabIndex={-1}>
323
- <FormattedMessage {...messages[error || PaymentErrorMessageId.ERROR_DEFAULT]} />
327
+ {!error || messages.hasOwnProperty(error) ? (
328
+ <FormattedMessage
329
+ {...messages[(error as PaymentErrorMessageId) || PaymentErrorMessageId.ERROR_DEFAULT]}
330
+ />
331
+ ) : (
332
+ error
333
+ )}
324
334
  </p>
325
335
  )}
326
336
  </>
@@ -29,8 +29,8 @@ export const useTerms = ({
29
29
  error,
30
30
  }: {
31
31
  product: Product;
32
- onError: (error: PaymentErrorMessageId) => void;
33
- error?: PaymentErrorMessageId;
32
+ onError: (error: PaymentErrorMessageId | string) => void;
33
+ error?: PaymentErrorMessageId | string;
34
34
  }) => {
35
35
  const intl = useIntl();
36
36
  const [termsAccepted, setTermsAccepted] = useState(false);
@@ -2,12 +2,12 @@ import fetchMock from 'fetch-mock';
2
2
  import { act, fireEvent, screen, waitFor } from '@testing-library/react';
3
3
  import {
4
4
  RichieContextFactory as mockRichieContextFactory,
5
+ PacedCourseFactory,
5
6
  UserFactory,
6
7
  } from 'utils/test/factories/richie';
7
8
  import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
8
9
  import {
9
10
  AddressFactory,
10
- CourseFactory,
11
11
  CredentialOrderWithPaymentFactory,
12
12
  CredentialProductFactory,
13
13
  OrderGroupFactory,
@@ -86,7 +86,7 @@ describe('SaleTunnel / Credential', () => {
86
86
  });
87
87
 
88
88
  it('should create an order with an order group', async () => {
89
- const course = CourseFactory().one();
89
+ const course = PacedCourseFactory().one();
90
90
  const product = CredentialProductFactory().one();
91
91
  const orderGroup = OrderGroupFactory().one();
92
92
  const billingAddress: Joanie.Address = AddressFactory({ is_main: true }).one();
@@ -5,6 +5,7 @@ import userEvent from '@testing-library/user-event';
5
5
  import countries from 'i18n-iso-countries';
6
6
  import {
7
7
  RichieContextFactory as mockRichieContextFactory,
8
+ PacedCourseFactory,
8
9
  UserFactory,
9
10
  } from 'utils/test/factories/richie';
10
11
  import { render } from 'utils/test/render';
@@ -12,8 +13,8 @@ import { setupJoanieSession } from 'utils/test/wrappers/JoanieAppWrapper';
12
13
  import CourseProductItem from 'widgets/SyllabusCourseRunsList/components/CourseProductItem';
13
14
  import {
14
15
  AddressFactory,
15
- CourseProductRelationFactory,
16
16
  CredentialOrderWithPaymentFactory,
17
+ CourseProductRelationFactory,
17
18
  } from 'utils/test/factories/joanie';
18
19
  import { ACTIVE_ORDER_STATES, CourseRun } from 'types/Joanie';
19
20
  import { Priority } from 'types';
@@ -101,9 +102,15 @@ describe('SaleTunnel', () => {
101
102
  [],
102
103
  );
103
104
 
104
- render(<CourseProductItem productId={product.id} course={course} />, {
105
- queryOptions: { client: createTestQueryClient({ user: richieUser }) },
106
- });
105
+ render(
106
+ <CourseProductItem
107
+ productId={product.id}
108
+ course={PacedCourseFactory({ id: course.id, code: course.code }).one()}
109
+ />,
110
+ {
111
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
112
+ },
113
+ );
107
114
 
108
115
  // Wait for product information to be fetched
109
116
  await screen.findByRole('heading', { level: 3, name: product.title });
@@ -8,7 +8,6 @@ import {
8
8
  CertificateOrderWithOneClickPaymentFactory,
9
9
  CertificateOrderWithPaymentFactory,
10
10
  CertificateProductFactory,
11
- CourseFactory,
12
11
  CredentialOrderWithOneClickPaymentFactory,
13
12
  CredentialOrderWithPaymentFactory,
14
13
  CredentialProductFactory,
@@ -17,6 +16,7 @@ import {
17
16
  } from 'utils/test/factories/joanie';
18
17
  import {
19
18
  RichieContextFactory as mockRichieContextFactory,
19
+ PacedCourseFactory,
20
20
  UserFactory,
21
21
  } from 'utils/test/factories/richie';
22
22
  import { render } from 'utils/test/render';
@@ -70,7 +70,7 @@ describe.each([
70
70
  ({ productType, ProductFactory, OrderWithOneClickPaymentFactory, OrderWithPaymentFactory }) => {
71
71
  let nbApiCalls: number;
72
72
 
73
- const course = CourseFactory().one();
73
+ const course = PacedCourseFactory().one();
74
74
  const enrollment =
75
75
  productType === ProductType.CERTIFICATE ? EnrollmentFactory().one() : undefined;
76
76
 
@@ -1,6 +1,7 @@
1
1
  import { StoryObj, Meta } from '@storybook/react';
2
2
  import { BaseJoanieAppWrapper } from 'utils/test/wrappers/BaseJoanieAppWrapper';
3
- import { CourseFactory, ProductFactory } from 'utils/test/factories/joanie';
3
+ import { ProductFactory } from 'utils/test/factories/joanie';
4
+ import { PacedCourseFactory } from 'utils/test/factories/richie';
4
5
  import { SaleTunnel, SaleTunnelProps } from './index';
5
6
 
6
7
  export default {
@@ -10,7 +11,7 @@ export default {
10
11
  isOpen: true,
11
12
  product: ProductFactory().one(),
12
13
  onClose: () => {},
13
- course: CourseFactory().one(),
14
+ course: PacedCourseFactory().one(),
14
15
  // enrollment?: Enrollment;
15
16
  // product: CredentialProduct | CertificateProduct;
16
17
  // orderGroup?: OrderGroup;
@@ -1,7 +1,6 @@
1
1
  import { ModalProps } from '@openfun/cunningham-react';
2
2
  import {
3
3
  CertificateProduct,
4
- CourseLight,
5
4
  CredentialProduct,
6
5
  Enrollment,
7
6
  Order,
@@ -12,12 +11,13 @@ import {
12
11
  } from 'types/Joanie';
13
12
  import { CredentialSaleTunnel } from 'components/SaleTunnel/CredentialSaleTunnel';
14
13
  import { CertificateSaleTunnel } from 'components/SaleTunnel/CertificateSaleTunnel';
14
+ import { PacedCourse } from 'types';
15
15
 
16
16
  export interface SaleTunnelProps extends Pick<ModalProps, 'isOpen' | 'onClose'> {
17
17
  product: Product;
18
18
  organizations?: Organization[];
19
19
 
20
- course?: CourseLight;
20
+ course?: PacedCourse;
21
21
  enrollment?: Enrollment;
22
22
  orderGroup?: OrderGroup;
23
23
  onFinish?: (order: Order) => void;