@@ -378,6 +378,265 @@ public function test_validate_handles_product_not_found_for_signature_check(): v
378378 $ this ->assertNull ( $ result );
379379 }
380380
381+ /**
382+ * Given a cart containing a shippable product and no shipping address
383+ * When validate() is called
384+ * Then a ValidationIssue for the 'shipping_address' field is returned
385+ * And the issue asks the customer to provide a shipping address
386+ * And a PROVIDE_MISSING_FIELD resolution option is included
387+ */
388+ public function test_validate_returns_missing_address_issue_when_cart_needs_shipping (): void {
389+ $ product = \Mockery::mock ( 'WC_Product ' );
390+ $ product ->shouldReceive ( 'needs_shipping ' )->andReturn ( true );
391+ $ this ->product_manager ->shouldReceive ( 'find_product ' )
392+ ->andReturn ( $ product );
393+
394+ $ cart = PayPalCart::from_array (
395+ array (
396+ 'items ' => array (
397+ array (
398+ 'item_id ' => '1 ' ,
399+ 'quantity ' => 1 ,
400+ 'name ' => 'Shippable Product ' ,
401+ ),
402+ ),
403+ 'payment_method ' => 'paypal ' ,
404+ )
405+ );
406+
407+ $ result = $ this ->validator ->validate ( $ cart );
408+
409+ $ this ->assertIsArray ( $ result );
410+ $ this ->assertCount ( 1 , $ result );
411+ $ this ->assertInstanceOf ( ValidationIssue::class, $ result [0 ] );
412+
413+ $ issue_data = $ result [0 ]->to_array ();
414+ $ this ->assertStringContainsString ( 'Shipping address is required ' , $ issue_data ['message ' ] );
415+ $ this ->assertStringContainsString ( 'Please provide a shipping address ' , $ issue_data ['user_message ' ] );
416+ $ this ->assertSame ( 'shipping_address ' , $ issue_data ['field ' ] );
417+
418+ $ this ->assertArrayHasKey ( 'resolution_options ' , $ issue_data );
419+ $ actions = array_column ( $ issue_data ['resolution_options ' ], 'action ' );
420+ $ this ->assertContains ( 'PROVIDE_MISSING_FIELD ' , $ actions );
421+ }
422+
423+ /**
424+ * Given a cart shipping to a US address with a postal code containing invalid characters
425+ * When validate() is called
426+ * Then a ValidationIssue for 'shipping_address.postal_code' is returned
427+ * And the issue message mentions an invalid postal code format
428+ */
429+ public function test_validate_detects_invalid_postal_code_format (): void {
430+ $ this ->mock_wc_countries (
431+ array ( 'US ' => 'United States ' ),
432+ array ( 'US ' => 'United States ' )
433+ );
434+
435+ $ cart = $ this ->create_cart_with_shipping (
436+ array (
437+ 'country_code ' => 'US ' ,
438+ 'address_line_1 ' => '123 Main St ' ,
439+ 'admin_area_2 ' => 'New York ' ,
440+ 'postal_code ' => '100!01 ' ,
441+ )
442+ );
443+
444+ $ result = $ this ->validator ->validate ( $ cart );
445+
446+ $ this ->assertIsArray ( $ result );
447+ $ this ->assertCount ( 1 , $ result );
448+ $ this ->assertInstanceOf ( ValidationIssue::class, $ result [0 ] );
449+
450+ $ issue_data = $ result [0 ]->to_array ();
451+ $ this ->assertStringContainsString ( 'Invalid postal code format ' , $ issue_data ['message ' ] );
452+ $ this ->assertSame ( 'shipping_address.postal_code ' , $ issue_data ['field ' ] );
453+ }
454+
455+ // Gap A3: Fallback from empty shipping countries to allowed countries
456+
457+ /**
458+ * Given WooCommerce has no shipping-specific countries configured (empty list)
459+ * And the allowed-countries list contains US
460+ * And the cart ships to US
461+ * When validate() is called
462+ * Then no country issue is returned (US is permitted via the allowed-countries fallback)
463+ */
464+ public function test_validate_falls_back_to_allowed_countries_when_shipping_countries_is_empty (): void {
465+ $ this ->mock_wc_countries (
466+ array ( 'US ' => 'United States ' ),
467+ array () // empty shipping countries → fallback to allowed countries
468+ );
469+
470+ $ cart = $ this ->create_cart_with_shipping (
471+ array (
472+ 'country_code ' => 'US ' ,
473+ 'address_line_1 ' => '123 Main St ' ,
474+ 'admin_area_2 ' => 'New York ' ,
475+ 'postal_code ' => '10001 ' ,
476+ )
477+ );
478+
479+ $ result = $ this ->validator ->validate ( $ cart );
480+
481+ $ this ->assertNull ( $ result );
482+ }
483+
484+ // Group B: PayPal-level US-only restriction (failing tests — feature not yet implemented)
485+
486+ /**
487+ * Given PayPal's supported-country allowlist only includes the United States
488+ * And WooCommerce also allows US
489+ * And the cart ships to US
490+ * When validate() is called
491+ * Then no PayPal-restriction issue is returned
492+ */
493+ public function test_validate_accepts_us_shipping_address_for_paypal_restriction (): void {
494+ $ this ->mock_wc_countries (
495+ array ( 'US ' => 'United States ' ),
496+ array ( 'US ' => 'United States ' )
497+ );
498+
499+ $ cart = $ this ->create_cart_with_shipping (
500+ array (
501+ 'country_code ' => 'US ' ,
502+ 'address_line_1 ' => '123 Main St ' ,
503+ 'admin_area_2 ' => 'New York ' ,
504+ 'postal_code ' => '10001 ' ,
505+ )
506+ );
507+
508+ $ result = $ this ->validator ->validate ( $ cart );
509+
510+ $ this ->assertNull ( $ result );
511+ }
512+
513+ /**
514+ * Given PayPal's supported-country allowlist only includes the United States
515+ * And WooCommerce allows both US and CA
516+ * And the cart ships to CA (Canada)
517+ * When validate() is called
518+ * Then a ValidationIssue is returned indicating CA is not supported by PayPal
519+ * And the issue targets 'shipping_address.country_code'
520+ * And the user message mentions that only specific countries are currently supported
521+ */
522+ public function test_validate_rejects_canada_shipping_address_due_to_paypal_restriction (): void {
523+ $ this ->mock_wc_countries (
524+ array ( 'US ' => 'United States ' , 'CA ' => 'Canada ' ),
525+ array ( 'US ' => 'United States ' , 'CA ' => 'Canada ' )
526+ );
527+
528+ $ cart = $ this ->create_cart_with_shipping (
529+ array (
530+ 'country_code ' => 'CA ' ,
531+ 'address_line_1 ' => '456 Maple Ave ' ,
532+ 'admin_area_2 ' => 'Toronto ' ,
533+ 'postal_code ' => 'M5V 3A8 ' ,
534+ )
535+ );
536+
537+ $ result = $ this ->validator ->validate ( $ cart );
538+
539+ $ this ->assertIsArray ( $ result );
540+ $ this ->assertCount ( 1 , $ result );
541+ $ this ->assertInstanceOf ( ValidationIssue::class, $ result [0 ] );
542+
543+ $ issue_data = $ result [0 ]->to_array ();
544+ $ this ->assertSame ( 'shipping_address.country_code ' , $ issue_data ['field ' ] );
545+
546+ $ message_lower = strtolower ( $ issue_data ['message ' ] );
547+ $ this ->assertTrue (
548+ strpos ( $ message_lower , 'ca ' ) !== false
549+ || strpos ( $ message_lower , 'paypal ' ) !== false
550+ || strpos ( $ message_lower , 'not supported ' ) !== false ,
551+ 'Expected issue message to reference CA, PayPal, or "not supported", got: ' . $ issue_data ['message ' ]
552+ );
553+
554+ $ user_message_lower = strtolower ( $ issue_data ['user_message ' ] );
555+ $ this ->assertTrue (
556+ strpos ( $ user_message_lower , 'united states ' ) !== false
557+ || strpos ( $ user_message_lower , 'supported ' ) !== false
558+ || strpos ( $ user_message_lower , 'country ' ) !== false ,
559+ 'Expected user_message to mention supported countries, got: ' . $ issue_data ['user_message ' ]
560+ );
561+ }
562+
563+ /**
564+ * Given PayPal's supported-country allowlist only includes the United States
565+ * And WooCommerce allows both US and DE
566+ * And the cart ships to DE (Germany)
567+ * When validate() is called
568+ * Then a ValidationIssue is returned indicating DE is not supported by PayPal
569+ * And the issue targets 'shipping_address.country_code'
570+ */
571+ public function test_validate_rejects_germany_shipping_address_due_to_paypal_restriction (): void {
572+ $ this ->mock_wc_countries (
573+ array ( 'US ' => 'United States ' , 'DE ' => 'Germany ' ),
574+ array ( 'US ' => 'United States ' , 'DE ' => 'Germany ' )
575+ );
576+
577+ $ cart = $ this ->create_cart_with_shipping (
578+ array (
579+ 'country_code ' => 'DE ' ,
580+ 'address_line_1 ' => '1 Unter den Linden ' ,
581+ 'admin_area_2 ' => 'Berlin ' ,
582+ 'postal_code ' => '10117 ' ,
583+ )
584+ );
585+
586+ $ result = $ this ->validator ->validate ( $ cart );
587+
588+ $ this ->assertIsArray ( $ result );
589+ $ this ->assertCount ( 1 , $ result );
590+ $ this ->assertInstanceOf ( ValidationIssue::class, $ result [0 ] );
591+
592+ $ issue_data = $ result [0 ]->to_array ();
593+ $ this ->assertSame ( 'shipping_address.country_code ' , $ issue_data ['field ' ] );
594+
595+ $ message_lower = strtolower ( $ issue_data ['message ' ] );
596+ $ this ->assertTrue (
597+ strpos ( $ message_lower , 'de ' ) !== false
598+ || strpos ( $ message_lower , 'paypal ' ) !== false
599+ || strpos ( $ message_lower , 'not supported ' ) !== false ,
600+ 'Expected issue message to reference DE, PayPal, or "not supported", got: ' . $ issue_data ['message ' ]
601+ );
602+ }
603+
604+ /**
605+ * Given a country is disallowed by both WooCommerce settings and the PayPal country restriction
606+ * And the cart ships to FR (France), which WooCommerce does not allow and PayPal does not support
607+ * When validate() is called
608+ * Then exactly one country-level ValidationIssue is returned (checks do not stack)
609+ */
610+ public function test_validate_produces_single_country_issue_when_disallowed_by_both_woocommerce_and_paypal (): void {
611+ $ this ->mock_wc_countries (
612+ array ( 'US ' => 'United States ' , 'FR ' => 'France ' ),
613+ array ( 'US ' => 'United States ' ) // FR not in shipping countries
614+ );
615+
616+ $ cart = $ this ->create_cart_with_shipping (
617+ array (
618+ 'country_code ' => 'FR ' ,
619+ 'address_line_1 ' => '123 Rue de la Paix ' ,
620+ 'admin_area_2 ' => 'Paris ' ,
621+ 'postal_code ' => '75001 ' ,
622+ )
623+ );
624+
625+ $ result = $ this ->validator ->validate ( $ cart );
626+
627+ $ this ->assertIsArray ( $ result );
628+
629+ $ country_issues = array_filter (
630+ $ result ,
631+ static function ( ValidationIssue $ issue ): bool {
632+ $ data = $ issue ->to_array ();
633+ return isset ( $ data ['field ' ] ) && $ data ['field ' ] === 'shipping_address.country_code ' ;
634+ }
635+ );
636+
637+ $ this ->assertCount ( 1 , $ country_issues );
638+ }
639+
381640 private function create_cart_with_shipping ( array $ address_data ): PayPalCart {
382641 return PayPalCart::from_array (
383642 array (
0 commit comments