richie-education 3.2.2-dev52 → 3.2.2-dev56
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/i18n/locales/ar-SA.json +20 -0
- package/i18n/locales/es-ES.json +20 -0
- package/i18n/locales/fa-IR.json +20 -0
- package/i18n/locales/fr-CA.json +20 -0
- package/i18n/locales/fr-FR.json +25 -5
- package/i18n/locales/ko-KR.json +20 -0
- package/i18n/locales/pt-PT.json +20 -0
- package/i18n/locales/ru-RU.json +20 -0
- package/i18n/locales/vi-VN.json +20 -0
- package/js/components/SaleTunnel/SaleTunnelInformation/SaleTunnelInformationGroup.tsx +3 -1
- package/js/components/SaleTunnel/SaleTunnelInformation/index.tsx +26 -20
- package/js/components/SaleTunnel/index.full-process-b2b.spec.tsx +209 -53
- package/js/components/SaleTunnel/index.spec.tsx +43 -0
- package/js/translations/ar-SA.json +1 -1
- package/js/translations/es-ES.json +1 -1
- package/js/translations/fa-IR.json +1 -1
- package/js/translations/fr-CA.json +1 -1
- package/js/translations/fr-FR.json +1 -1
- package/js/translations/ko-KR.json +1 -1
- package/js/translations/pt-PT.json +1 -1
- package/js/translations/ru-RU.json +1 -1
- package/js/translations/vi-VN.json +1 -1
- package/package.json +1 -1
|
@@ -46,6 +46,44 @@ jest.mock('utils/indirection/window', () => ({
|
|
|
46
46
|
|
|
47
47
|
jest.mock('../PaymentInterfaces');
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Setup common mocks for B2B batch order tests
|
|
51
|
+
*/
|
|
52
|
+
const setupBatchOrderMocks = (params: {
|
|
53
|
+
course: any;
|
|
54
|
+
product: any;
|
|
55
|
+
offering: any;
|
|
56
|
+
paymentPlan: any;
|
|
57
|
+
organizations?: any[] | any;
|
|
58
|
+
}) => {
|
|
59
|
+
const { course, product, offering, paymentPlan, organizations } = params;
|
|
60
|
+
|
|
61
|
+
fetchMock.get(
|
|
62
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/`,
|
|
63
|
+
offering,
|
|
64
|
+
);
|
|
65
|
+
fetchMock.get(
|
|
66
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
67
|
+
paymentPlan,
|
|
68
|
+
);
|
|
69
|
+
fetchMock.get(`https://joanie.endpoint/api/v1.0/enrollments/`, []);
|
|
70
|
+
fetchMock.get(
|
|
71
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify({
|
|
72
|
+
product_id: product.id,
|
|
73
|
+
course_code: course.code,
|
|
74
|
+
state: NOT_CANCELED_ORDER_STATES,
|
|
75
|
+
})}`,
|
|
76
|
+
[],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (organizations) {
|
|
80
|
+
fetchMock.get(
|
|
81
|
+
`https://joanie.endpoint/api/v1.0/offerings/${offering.id}/get-organizations/`,
|
|
82
|
+
organizations,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
49
87
|
describe('SaleTunnel', () => {
|
|
50
88
|
let richieUser: User;
|
|
51
89
|
let openApiEdxProfile: OpenEdxApiProfile;
|
|
@@ -86,27 +124,7 @@ describe('SaleTunnel', () => {
|
|
|
86
124
|
const paymentPlan = PaymentPlanFactory().one();
|
|
87
125
|
const organizations = OrganizationFactory().many(3);
|
|
88
126
|
|
|
89
|
-
|
|
90
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/`,
|
|
91
|
-
offering,
|
|
92
|
-
);
|
|
93
|
-
fetchMock.get(
|
|
94
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
95
|
-
paymentPlan,
|
|
96
|
-
);
|
|
97
|
-
fetchMock.get(`https://joanie.endpoint/api/v1.0/enrollments/`, []);
|
|
98
|
-
fetchMock.get(
|
|
99
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify({
|
|
100
|
-
product_id: product.id,
|
|
101
|
-
course_code: course.code,
|
|
102
|
-
state: NOT_CANCELED_ORDER_STATES,
|
|
103
|
-
})}`,
|
|
104
|
-
[],
|
|
105
|
-
);
|
|
106
|
-
fetchMock.get(
|
|
107
|
-
`https://joanie.endpoint/api/v1.0/offerings/${offering.id}/get-organizations/`,
|
|
108
|
-
organizations,
|
|
109
|
-
);
|
|
127
|
+
setupBatchOrderMocks({ course, product, offering, paymentPlan, organizations });
|
|
110
128
|
|
|
111
129
|
render(<CourseProductItem productId={product.id} course={course} />, {
|
|
112
130
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
@@ -304,24 +322,13 @@ describe('SaleTunnel', () => {
|
|
|
304
322
|
}).one();
|
|
305
323
|
const paymentPlan = PaymentPlanFactory().one();
|
|
306
324
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
offering,
|
|
310
|
-
);
|
|
311
|
-
fetchMock.get(
|
|
312
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
313
|
-
paymentPlan,
|
|
314
|
-
);
|
|
315
|
-
fetchMock.get(`https://joanie.endpoint/api/v1.0/enrollments/`, []);
|
|
325
|
+
setupBatchOrderMocks({ course, product, offering, paymentPlan });
|
|
326
|
+
|
|
316
327
|
const orderQueryParameters = {
|
|
317
328
|
product_id: product.id,
|
|
318
329
|
course_code: course.code,
|
|
319
330
|
state: NOT_CANCELED_ORDER_STATES,
|
|
320
331
|
};
|
|
321
|
-
fetchMock.get(
|
|
322
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(orderQueryParameters)}`,
|
|
323
|
-
[],
|
|
324
|
-
);
|
|
325
332
|
|
|
326
333
|
render(<CourseProductItem productId={product.id} course={course} />, {
|
|
327
334
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
@@ -423,27 +430,13 @@ describe('SaleTunnel', () => {
|
|
|
423
430
|
product: { id: product.id, title: product.title },
|
|
424
431
|
}).one();
|
|
425
432
|
|
|
426
|
-
|
|
427
|
-
|
|
433
|
+
setupBatchOrderMocks({
|
|
434
|
+
course,
|
|
435
|
+
product,
|
|
428
436
|
offering,
|
|
429
|
-
);
|
|
430
|
-
fetchMock.get(
|
|
431
|
-
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
432
437
|
paymentPlan,
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
fetchMock.get(
|
|
436
|
-
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify({
|
|
437
|
-
product_id: product.id,
|
|
438
|
-
course_code: course.code,
|
|
439
|
-
state: NOT_CANCELED_ORDER_STATES,
|
|
440
|
-
})}`,
|
|
441
|
-
[],
|
|
442
|
-
);
|
|
443
|
-
fetchMock.get(
|
|
444
|
-
`https://joanie.endpoint/api/v1.0/offerings/${offering.id}/get-organizations/`,
|
|
445
|
-
offeringOrganization,
|
|
446
|
-
);
|
|
438
|
+
organizations: offeringOrganization,
|
|
439
|
+
});
|
|
447
440
|
|
|
448
441
|
render(<CourseProductItem productId={product.id} course={course} />, {
|
|
449
442
|
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
@@ -573,4 +566,167 @@ describe('SaleTunnel', () => {
|
|
|
573
566
|
'Unable to create the order: the maximum number of available seats for this offering has been reached. Please contact support for more information.',
|
|
574
567
|
);
|
|
575
568
|
}, 30000);
|
|
569
|
+
|
|
570
|
+
it('tests optional fields can be filled and cleared without breaking validation', async () => {
|
|
571
|
+
const course = PacedCourseFactory().one();
|
|
572
|
+
const product = ProductFactory().one();
|
|
573
|
+
const offering = OfferingFactory({ course, product, is_withdrawable: false }).one();
|
|
574
|
+
const paymentPlan = PaymentPlanFactory().one();
|
|
575
|
+
const organizations = OrganizationFactory().many(3);
|
|
576
|
+
|
|
577
|
+
setupBatchOrderMocks({ course, product, offering, paymentPlan, organizations });
|
|
578
|
+
|
|
579
|
+
render(<CourseProductItem productId={product.id} course={course} />, {
|
|
580
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await screen.findByRole('heading', { level: 3, name: product.title });
|
|
584
|
+
const user = userEvent.setup();
|
|
585
|
+
const buyButton = screen.getByRole('button', { name: product.call_to_action });
|
|
586
|
+
await user.click(buyButton);
|
|
587
|
+
await screen.findByTestId('generic-sale-tunnel-payment-step');
|
|
588
|
+
|
|
589
|
+
// Select group buy form
|
|
590
|
+
const formTypeSelect = screen.getByRole('combobox', { name: 'Purchase type' });
|
|
591
|
+
await user.click(formTypeSelect);
|
|
592
|
+
await user.click(screen.getByText('I am purchasing on behalf of an organization'));
|
|
593
|
+
|
|
594
|
+
// Company step
|
|
595
|
+
const $companyName = await screen.findByRole('textbox', { name: 'Company name' });
|
|
596
|
+
const $idNumber = screen.getByRole('textbox', { name: /Registration number/ });
|
|
597
|
+
const $address = screen.getByRole('textbox', { name: 'Address' });
|
|
598
|
+
const $postCode = screen.getByRole('textbox', { name: 'Postal code' });
|
|
599
|
+
const $city = screen.getByRole('textbox', { name: 'City' });
|
|
600
|
+
const $country = screen.getByRole('combobox', { name: 'Country' });
|
|
601
|
+
|
|
602
|
+
await user.type($companyName, 'Test Company');
|
|
603
|
+
await user.type($idNumber, '123456789');
|
|
604
|
+
await user.type($address, '123 Test Street');
|
|
605
|
+
await user.type($postCode, '12345');
|
|
606
|
+
await user.type($city, 'Test City');
|
|
607
|
+
await user.click($country);
|
|
608
|
+
await user.click(screen.getByText('France'));
|
|
609
|
+
|
|
610
|
+
// Fill and clear billing address optional fields
|
|
611
|
+
const $billingCheckbox = screen.getByLabelText('Use different billing information');
|
|
612
|
+
await user.click($billingCheckbox);
|
|
613
|
+
|
|
614
|
+
const $billingContactName = await screen.findByRole('textbox', {
|
|
615
|
+
name: 'Contact name',
|
|
616
|
+
});
|
|
617
|
+
const $billingContactEmail = screen.getByRole('textbox', { name: 'Contact email' });
|
|
618
|
+
const billingCompanyInputs = screen.getAllByRole('textbox', { name: 'Company name' });
|
|
619
|
+
const $billingCompanyName = billingCompanyInputs[billingCompanyInputs.length - 1];
|
|
620
|
+
const billingIdInputs = screen.getAllByRole('textbox', { name: /Registration number/ });
|
|
621
|
+
const $billingIdNumber = billingIdInputs[billingIdInputs.length - 1];
|
|
622
|
+
const billingAddressInputs = screen.getAllByRole('textbox', { name: 'Address' });
|
|
623
|
+
const $billingAddress = billingAddressInputs[billingAddressInputs.length - 1];
|
|
624
|
+
const billingPostCodeInputs = screen.getAllByRole('textbox', { name: 'Postal code' });
|
|
625
|
+
const $billingPostCode = billingPostCodeInputs[billingPostCodeInputs.length - 1];
|
|
626
|
+
const billingCityInputs = screen.getAllByRole('textbox', { name: 'City' });
|
|
627
|
+
const $billingCity = billingCityInputs[billingCityInputs.length - 1];
|
|
628
|
+
const billingCountrySelects = screen.getAllByRole('combobox', { name: 'Country' });
|
|
629
|
+
const $billingCountry = billingCountrySelects[billingCountrySelects.length - 1];
|
|
630
|
+
|
|
631
|
+
await user.type($billingContactName, 'Billing Contact');
|
|
632
|
+
await user.type($billingContactEmail, 'billing@test.com');
|
|
633
|
+
await user.type($billingCompanyName, 'Billing Company');
|
|
634
|
+
await user.type($billingIdNumber, '987654321');
|
|
635
|
+
await user.type($billingAddress, '456 Billing Street');
|
|
636
|
+
await user.type($billingPostCode, '54321');
|
|
637
|
+
await user.type($billingCity, 'Billing City');
|
|
638
|
+
await user.click($billingCountry);
|
|
639
|
+
await user.click(screen.getAllByText('France')[1]);
|
|
640
|
+
|
|
641
|
+
await user.clear($billingContactName);
|
|
642
|
+
await user.clear($billingContactEmail);
|
|
643
|
+
await user.clear($billingCompanyName);
|
|
644
|
+
await user.clear($billingIdNumber);
|
|
645
|
+
await user.clear($billingAddress);
|
|
646
|
+
await user.clear($billingPostCode);
|
|
647
|
+
await user.clear($billingCity);
|
|
648
|
+
|
|
649
|
+
await user.click($billingCheckbox);
|
|
650
|
+
|
|
651
|
+
// Follow-up step
|
|
652
|
+
await user.click(screen.getByRole('button', { name: 'Next' }));
|
|
653
|
+
const $lastName = await screen.findByRole('textbox', { name: 'Last name' });
|
|
654
|
+
const $firstName = screen.getByRole('textbox', { name: 'First name' });
|
|
655
|
+
const $role = screen.getByRole('textbox', { name: 'Role' });
|
|
656
|
+
const $email = screen.getByRole('textbox', { name: 'Email' });
|
|
657
|
+
const $phone = screen.getByRole('textbox', { name: 'Phone number' });
|
|
658
|
+
|
|
659
|
+
await user.type($lastName, 'Doe');
|
|
660
|
+
await user.type($firstName, 'Jane');
|
|
661
|
+
await user.type($role, 'Manager');
|
|
662
|
+
await user.type($email, 'jane.doe@test.com');
|
|
663
|
+
await user.type($phone, '+33123456789');
|
|
664
|
+
|
|
665
|
+
// Signatory step
|
|
666
|
+
await user.click(screen.getByRole('button', { name: 'Next' }));
|
|
667
|
+
const $signatoryLastName = await screen.findByRole('textbox', { name: 'Last name' });
|
|
668
|
+
const $signatoryFirstName = screen.getByRole('textbox', { name: 'First name' });
|
|
669
|
+
const $signatoryRole = screen.getByRole('textbox', { name: 'Role' });
|
|
670
|
+
const $signatoryEmail = screen.getByRole('textbox', { name: 'Email' });
|
|
671
|
+
const $signatoryPhone = screen.getByRole('textbox', { name: 'Phone number' });
|
|
672
|
+
|
|
673
|
+
await user.type($signatoryLastName, 'Smith');
|
|
674
|
+
await user.type($signatoryFirstName, 'John');
|
|
675
|
+
await user.type($signatoryRole, 'Director');
|
|
676
|
+
await user.type($signatoryEmail, 'john.smith@test.com');
|
|
677
|
+
await user.type($signatoryPhone, '+33987654321');
|
|
678
|
+
|
|
679
|
+
// Participants step
|
|
680
|
+
await user.click(screen.getByRole('button', { name: 'Next' }));
|
|
681
|
+
const $nbParticipants = await screen.findByLabelText('Number of participants to register');
|
|
682
|
+
await user.type($nbParticipants, '10');
|
|
683
|
+
|
|
684
|
+
// Financing step - fill and clear optional fields
|
|
685
|
+
await user.click(screen.getByRole('button', { name: 'Next' }));
|
|
686
|
+
const $purchaseOrderRadio = await screen.findByLabelText('Purchase order');
|
|
687
|
+
await user.click($purchaseOrderRadio);
|
|
688
|
+
|
|
689
|
+
const $fundingEntity = screen.getByLabelText('Entity name');
|
|
690
|
+
await user.type($fundingEntity, 'Test OPCO');
|
|
691
|
+
expect($fundingEntity).toHaveValue('Test OPCO');
|
|
692
|
+
|
|
693
|
+
const $fundingAmount = screen.getByLabelText('Amount covered');
|
|
694
|
+
await user.type($fundingAmount, '1000');
|
|
695
|
+
expect($fundingAmount).toHaveValue(1000);
|
|
696
|
+
|
|
697
|
+
await user.clear($fundingAmount);
|
|
698
|
+
expect($fundingAmount).toHaveValue(null);
|
|
699
|
+
|
|
700
|
+
await user.clear($fundingEntity);
|
|
701
|
+
expect($fundingEntity).toHaveValue('');
|
|
702
|
+
|
|
703
|
+
// Submit the batch order
|
|
704
|
+
const batchOrderRead = BatchOrderReadFactory().one();
|
|
705
|
+
fetchMock.post('https://joanie.endpoint/api/v1.0/batch-orders/', batchOrderRead);
|
|
706
|
+
const $subscribeButton = screen.getByRole('button', {
|
|
707
|
+
name: 'Subscribe',
|
|
708
|
+
}) as HTMLButtonElement;
|
|
709
|
+
|
|
710
|
+
expect($subscribeButton).not.toBeDisabled();
|
|
711
|
+
await user.click($subscribeButton);
|
|
712
|
+
await screen.findByTestId('generic-sale-tunnel-success-step');
|
|
713
|
+
|
|
714
|
+
// Verify the batch order payload does NOT contain the cleared optional fields
|
|
715
|
+
const batchOrderCalls = fetchMock.calls('https://joanie.endpoint/api/v1.0/batch-orders/');
|
|
716
|
+
expect(batchOrderCalls).toHaveLength(1);
|
|
717
|
+
const batchOrderPayload = JSON.parse(batchOrderCalls[0][1]?.body as string);
|
|
718
|
+
|
|
719
|
+
expect(batchOrderPayload.billing_address).toBeUndefined();
|
|
720
|
+
expect(batchOrderPayload.funding_entity).toBeUndefined();
|
|
721
|
+
expect(batchOrderPayload.funding_amount).toBeUndefined();
|
|
722
|
+
expect(batchOrderPayload.organization_id).toBeUndefined();
|
|
723
|
+
|
|
724
|
+
expect(batchOrderPayload).toMatchObject({
|
|
725
|
+
offering_id: offering.id,
|
|
726
|
+
company_name: 'Test Company',
|
|
727
|
+
identification_number: '123456789',
|
|
728
|
+
payment_method: PaymentMethod.PURCHASE_ORDER,
|
|
729
|
+
nb_seats: '10',
|
|
730
|
+
});
|
|
731
|
+
}, 30000);
|
|
576
732
|
});
|
|
@@ -319,6 +319,49 @@ describe.each([
|
|
|
319
319
|
});
|
|
320
320
|
});
|
|
321
321
|
|
|
322
|
+
it('should verify the purchase type selector visibility based on product type', async () => {
|
|
323
|
+
const product = ProductFactory().one();
|
|
324
|
+
const paymentPlan = PaymentPlanFactory().one();
|
|
325
|
+
const user = userEvent.setup({ delay: null });
|
|
326
|
+
|
|
327
|
+
fetchMock
|
|
328
|
+
.get(
|
|
329
|
+
`https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
|
|
330
|
+
[],
|
|
331
|
+
)
|
|
332
|
+
.get(
|
|
333
|
+
`https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
|
|
334
|
+
paymentPlan,
|
|
335
|
+
)
|
|
336
|
+
.get('https://joanie.endpoint/api/v1.0/offerings/get-organizations/', []);
|
|
337
|
+
|
|
338
|
+
render(<Wrapper paymentPlan={paymentPlan} product={product} isWithdrawable={true} />, {
|
|
339
|
+
queryOptions: { client: createTestQueryClient({ user: richieUser }) },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
await screen.findByText(
|
|
343
|
+
formatPrice(paymentPlan.price, product.price_currency).replace(/(\u202F|\u00a0)/g, ' '),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
if (productType === ProductType.CERTIFICATE) {
|
|
347
|
+
expect(screen.queryByRole('combobox', { name: 'Purchase type' })).not.toBeInTheDocument();
|
|
348
|
+
expect(screen.getByText('This information will be used for billing')).toBeInTheDocument();
|
|
349
|
+
} else {
|
|
350
|
+
const purchaseType = screen.getByRole('combobox', { name: 'Purchase type' });
|
|
351
|
+
expect(purchaseType).toBeInTheDocument();
|
|
352
|
+
|
|
353
|
+
expect(screen.getByText('This information will be used for billing')).toBeInTheDocument();
|
|
354
|
+
expect(screen.queryByRole('textbox', { name: 'Company name' })).not.toBeInTheDocument();
|
|
355
|
+
|
|
356
|
+
await user.click(purchaseType);
|
|
357
|
+
await user.click(
|
|
358
|
+
screen.getByRole('option', { name: 'I am purchasing on behalf of an organization' }),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
expect(screen.getByRole('textbox', { name: 'Company name' })).toBeInTheDocument();
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
|
|
322
365
|
it('should start at the save payment method step if order is in state to_save_payment_method', async () => {
|
|
323
366
|
const product = ProductFactory().one();
|
|
324
367
|
const billingAddress = AddressFactory({
|