richie-education 3.2.2-dev52 → 3.2.2-dev58

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.
@@ -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
- fetchMock.get(
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
- fetchMock.get(
308
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/`,
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
- fetchMock.get(
427
- `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/`,
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
- fetchMock.get(`https://joanie.endpoint/api/v1.0/enrollments/`, []);
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({