diff --git a/packages/table-rate-shipping/config/shipping-tables.php b/packages/table-rate-shipping/config/shipping-tables.php index fe08da85b3..fa50996221 100644 --- a/packages/table-rate-shipping/config/shipping-tables.php +++ b/packages/table-rate-shipping/config/shipping-tables.php @@ -10,4 +10,5 @@ * or add a callable to use your own logic (eg => [MyTaxRateCalculator::class, 'calculate']) */ 'shipping_rate_tax_calculation' => 'default', + ]; diff --git a/packages/table-rate-shipping/src/DataTransferObjects/PostcodeLookup.php b/packages/table-rate-shipping/src/DataTransferObjects/PostcodeLookup.php index cbd8021d6c..6de2e4f7e3 100644 --- a/packages/table-rate-shipping/src/DataTransferObjects/PostcodeLookup.php +++ b/packages/table-rate-shipping/src/DataTransferObjects/PostcodeLookup.php @@ -2,17 +2,26 @@ namespace Lunar\Shipping\DataTransferObjects; +use Illuminate\Support\Collection; use Lunar\Models\Contracts\Country as CountryContract; +use Lunar\Shipping\Facades\Postcode; class PostcodeLookup { - /** - * Initialise the postcode lookup class. - */ public function __construct( public CountryContract $country, public string $postcode ) { // } + + /** + * Return the postcode parts for this lookup, delegating to the country-matched resolver. + * + * @return Collection + */ + public function getParts(): Collection + { + return Postcode::country($this->country)->getParts($this->postcode, $this->country); + } } diff --git a/packages/table-rate-shipping/src/Exceptions/NoPostcodeResolverException.php b/packages/table-rate-shipping/src/Exceptions/NoPostcodeResolverException.php new file mode 100644 index 0000000000..4af6596f92 --- /dev/null +++ b/packages/table-rate-shipping/src/Exceptions/NoPostcodeResolverException.php @@ -0,0 +1,16 @@ + + */ + public function getParts(string $postcode, CountryContract $country): Collection; +} diff --git a/packages/table-rate-shipping/src/Managers/PostcodeManager.php b/packages/table-rate-shipping/src/Managers/PostcodeManager.php new file mode 100644 index 0000000000..053fc7ba60 --- /dev/null +++ b/packages/table-rate-shipping/src/Managers/PostcodeManager.php @@ -0,0 +1,88 @@ + + */ + protected Collection $resolvers; + + public function __construct() + { + $this->resolvers = collect(); + } + + /** + * Register a resolver. Class strings are resolved lazily via the container. + */ + public function addResolver(string|PostcodeResolverInterface $resolver): self + { + $this->resolvers->push($resolver); + + return $this; + } + + /** + * Return the matching resolver for the given country. Iterates in reverse registration + * order — the last-registered resolver that supports the country wins. + */ + public function country(CountryContract $country): PostcodeResolverInterface + { + // Collection::reverse() preserves keys, so $index is the original slot in + // $this->resolvers — which resolveInstance() relies on for in-place caching. + foreach ($this->resolvers->reverse() as $index => $resolver) { + $instance = $this->resolveInstance($index, $resolver); + + if ($instance->supportsCountry($country)) { + return $instance; + } + } + + throw NoPostcodeResolverException::forCountry($country->iso2); + } + + /** + * Access the raw resolver collection — mostly for diagnostic use. + * + * @return Collection + */ + public function getResolvers(): Collection + { + return $this->resolvers; + } + + /** + * Resolve a collection entry to a concrete interface instance, caching in place so + * subsequent calls reuse the same instance for the rest of the request. + */ + protected function resolveInstance(int $index, string|PostcodeResolverInterface $resolver): PostcodeResolverInterface + { + if ($resolver instanceof PostcodeResolverInterface) { + return $resolver; + } + + $instance = app()->make($resolver); + + if (! $instance instanceof PostcodeResolverInterface) { + throw new \InvalidArgumentException(sprintf( + 'Postcode resolver [%s] must implement %s.', + $resolver, + PostcodeResolverInterface::class + )); + } + + $this->resolvers->put($index, $instance); + + return $instance; + } +} diff --git a/packages/table-rate-shipping/src/Resolvers/PostcodeResolver.php b/packages/table-rate-shipping/src/Resolvers/PostcodeResolver.php index 20de5ec55c..e47e7fcbeb 100644 --- a/packages/table-rate-shipping/src/Resolvers/PostcodeResolver.php +++ b/packages/table-rate-shipping/src/Resolvers/PostcodeResolver.php @@ -3,10 +3,26 @@ namespace Lunar\Shipping\Resolvers; use Illuminate\Support\Collection; +use Lunar\Models\Contracts\Country as CountryContract; +use Lunar\Shipping\Interfaces\PostcodeResolverInterface; -class PostcodeResolver +class PostcodeResolver implements PostcodeResolverInterface { - public function getParts($postcode): Collection + /** + * ISO-2 country codes this resolver handles. An empty array matches every country, + * making this resolver a safe catch-all when registered first. + * + * @var array + */ + protected array $countries = []; + + public function supportsCountry(CountryContract $country): bool + { + return empty($this->countries) + || in_array($country->iso2, $this->countries, true); + } + + public function getParts(string $postcode, CountryContract $country): Collection { $postcode = str_replace(' ', '', strtoupper($postcode)); diff --git a/packages/table-rate-shipping/src/Resolvers/ShippingZoneResolver.php b/packages/table-rate-shipping/src/Resolvers/ShippingZoneResolver.php index fb19de7704..4a374fc5a4 100644 --- a/packages/table-rate-shipping/src/Resolvers/ShippingZoneResolver.php +++ b/packages/table-rate-shipping/src/Resolvers/ShippingZoneResolver.php @@ -100,10 +100,7 @@ public function get(): Collection if ($this->postcodeLookup) { $builder->orWhere(function ($qb) { $qb->whereHas('postcodes', function ($query) { - $postcodeParts = (new PostcodeResolver)->getParts( - $this->postcodeLookup->postcode - ); - $query->whereIn('postcode', $postcodeParts); + $query->whereIn('postcode', $this->postcodeLookup->getParts()); })->where(function ($qb) { $qb->whereHas('countries', function ($query) { $query->where('country_id', $this->postcodeLookup->country->id); diff --git a/packages/table-rate-shipping/src/ShippingServiceProvider.php b/packages/table-rate-shipping/src/ShippingServiceProvider.php index b0116b5072..7ed93f7309 100644 --- a/packages/table-rate-shipping/src/ShippingServiceProvider.php +++ b/packages/table-rate-shipping/src/ShippingServiceProvider.php @@ -17,6 +17,7 @@ use Lunar\Shipping\Database\State\MigrateCutoffToSchedule; use Lunar\Shipping\DiscountTypes\ShippingDiscount; use Lunar\Shipping\Interfaces\ShippingMethodManagerInterface; +use Lunar\Shipping\Managers\PostcodeManager; use Lunar\Shipping\Managers\ShippingManager; use Lunar\Shipping\Models\ShippingExclusion; use Lunar\Shipping\Models\ShippingExclusionList; @@ -25,12 +26,20 @@ use Lunar\Shipping\Models\ShippingZone; use Lunar\Shipping\Models\ShippingZonePostcode; use Lunar\Shipping\Observers\OrderObserver; +use Lunar\Shipping\Resolvers\PostcodeResolver; class ShippingServiceProvider extends ServiceProvider { public function register() { $this->mergeConfigFrom(__DIR__.'/../config/shipping-tables.php', 'lunar.shipping-tables'); + + $this->app->singleton(PostcodeManager::class, function () { + $manager = new PostcodeManager; + $manager->addResolver(PostcodeResolver::class); + + return $manager; + }); } public function boot(ShippingModifiers $shippingModifiers) diff --git a/tests/shipping/Stubs/Resolvers/TestCustomPostcodeResolver.php b/tests/shipping/Stubs/Resolvers/TestCustomPostcodeResolver.php new file mode 100644 index 0000000000..3c586d0dd8 --- /dev/null +++ b/tests/shipping/Stubs/Resolvers/TestCustomPostcodeResolver.php @@ -0,0 +1,36 @@ + + */ + protected array $countries = []; + + public function supportsCountry(CountryContract $country): bool + { + return empty($this->countries) + || in_array($country->iso2, $this->countries, true); + } + + public function getParts(string $postcode, CountryContract $country): Collection + { + $postcode = str_replace(' ', '', strtoupper($postcode)); + + return collect([ + $postcode, + substr($postcode, 0, 1).'*', + substr($postcode, 0, 2).'*', + substr($postcode, 0, 3).'*', + substr($postcode, 0, 4).'*', + ])->filter()->unique()->values(); + } +} diff --git a/tests/shipping/Unit/DataTransferObjects/PostcodeLookupTest.php b/tests/shipping/Unit/DataTransferObjects/PostcodeLookupTest.php new file mode 100644 index 0000000000..5987e31883 --- /dev/null +++ b/tests/shipping/Unit/DataTransferObjects/PostcodeLookupTest.php @@ -0,0 +1,38 @@ +group('shipping', 'shipping-postcode'); + +uses(RefreshDatabase::class); + +test('getParts delegates to the resolver matched for the lookup country', function () { + $country = Country::factory()->create(['iso2' => 'GB']); + + $stubbed = new class implements PostcodeResolverInterface + { + public function supportsCountry(CountryContract $country): bool + { + return $country->iso2 === 'GB'; + } + + public function getParts(string $postcode, CountryContract $country): Collection + { + return collect([sprintf('STUB:%s:%s', $country->iso2, $postcode)]); + } + }; + + Postcode::addResolver($stubbed); + + $lookup = new PostcodeLookup($country, 'SW1A 1AA'); + + expect($lookup->getParts()->all())->toBe(['STUB:GB:SW1A 1AA']); +}); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/CollectionTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/CollectionTest.php index 049ba78ec0..f90ef07765 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/CollectionTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/CollectionTest.php @@ -12,7 +12,7 @@ use Lunar\Tests\Shipping\TestCase; use Lunar\Tests\Shipping\TestUtils; -uses(TestCase::class); +uses(TestCase::class)->group('shipping', 'shipping-driver', 'shipping-driver-collection'); uses(RefreshDatabase::class); uses(TestUtils::class); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php index eb9dd722cd..087c43c392 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/FlatRateTest.php @@ -12,7 +12,7 @@ use Lunar\Tests\Shipping\TestCase; use Lunar\Tests\Shipping\TestUtils; -uses(TestCase::class); +uses(TestCase::class)->group('shipping', 'shipping-driver', 'shipping-driver-flatrate'); uses(RefreshDatabase::class); uses(TestUtils::class); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/FreeShippingTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/FreeShippingTest.php index 03b1992603..d3f44bae91 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/FreeShippingTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/FreeShippingTest.php @@ -12,7 +12,7 @@ use Lunar\Tests\Shipping\TestCase; use Lunar\Tests\Shipping\TestUtils; -uses(TestCase::class); +uses(TestCase::class)->group('shipping', 'shipping-driver', 'shipping-driver-freeshiping'); uses(RefreshDatabase::class); uses(TestUtils::class); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php index 462d9c9631..f9eeeeed8a 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php @@ -13,7 +13,7 @@ use Lunar\Tests\Shipping\TestCase; use Lunar\Tests\Shipping\TestUtils; -uses(TestCase::class); +uses(TestCase::class)->group('shipping', 'shipping-driver', 'shipping-driver-shipby'); uses(RefreshDatabase::class); uses(TestUtils::class); diff --git a/tests/shipping/Unit/Managers/PostcodeManagerTest.php b/tests/shipping/Unit/Managers/PostcodeManagerTest.php new file mode 100644 index 0000000000..5e1c4376f3 --- /dev/null +++ b/tests/shipping/Unit/Managers/PostcodeManagerTest.php @@ -0,0 +1,124 @@ +group('shipping', 'shipping-postcode'); +uses(RefreshDatabase::class); + +test('addResolver pushes the resolver onto the collection', function () { + $manager = new PostcodeManager; + + $manager->addResolver(PostcodeResolver::class); + + expect($manager->getResolvers())->toBeInstanceOf(Collection::class); + expect($manager->getResolvers())->toHaveCount(1); +}); + +test('addResolver returns the manager for fluent chaining', function () { + $manager = new PostcodeManager; + + expect($manager->addResolver(PostcodeResolver::class))->toBe($manager); +}); + +test('country returns the last-registered matching resolver', function () { + $gb = Country::factory()->create(['iso2' => 'GB']); + + $resolverA = new class implements PostcodeResolverInterface + { + public string $label = 'A'; + + public function supportsCountry(CountryContract $country): bool + { + return $country->iso2 === 'GB'; + } + + public function getParts(string $postcode, CountryContract $country): Collection + { + return collect([$postcode]); + } + }; + + $resolverB = new class implements PostcodeResolverInterface + { + public string $label = 'B'; + + public function supportsCountry(CountryContract $country): bool + { + return $country->iso2 === 'GB'; + } + + public function getParts(string $postcode, CountryContract $country): Collection + { + return collect([$postcode]); + } + }; + + $manager = new PostcodeManager; + $manager->addResolver($resolverA); + $manager->addResolver($resolverB); + + expect($manager->country($gb)->label)->toBe('B'); +}); + +test('country falls through to an earlier resolver when later ones do not support it', function () { + $us = Country::factory()->create(['iso2' => 'US']); + + $gbOnly = new class implements PostcodeResolverInterface + { + public function supportsCountry(CountryContract $country): bool + { + return $country->iso2 === 'GB'; + } + + public function getParts(string $postcode, CountryContract $country): Collection + { + return collect([$postcode]); + } + }; + + $manager = new PostcodeManager; + $manager->addResolver(PostcodeResolver::class); // catch-all, registered first + $manager->addResolver($gbOnly); // GB-only, registered last + + expect($manager->country($us))->toBeInstanceOf(PostcodeResolver::class); +}); + +test('country throws when no resolver claims the country', function () { + $fr = Country::factory()->create(['iso2' => 'FR']); + + $manager = new PostcodeManager; // no resolvers registered + + $manager->country($fr); +})->throws(NoPostcodeResolverException::class, 'FR'); + +test('country resolves class-string registrations via the container on first use', function () { + $gb = Country::factory()->create(['iso2' => 'GB']); + + $manager = new PostcodeManager; + $manager->addResolver(PostcodeResolver::class); + + $resolved = $manager->country($gb); + + expect($resolved)->toBeInstanceOf(PostcodeResolver::class); +}); + +test('country rejects a registered class that does not implement the interface', function () { + $gb = Country::factory()->create(['iso2' => 'GB']); + + $manager = new PostcodeManager; + $manager->addResolver(stdClass::class); + + $manager->country($gb); +})->throws( + InvalidArgumentException::class, + PostcodeResolverInterface::class +); diff --git a/tests/shipping/Unit/Managers/ShippingManagerTest.php b/tests/shipping/Unit/Managers/ShippingManagerTest.php index 30dee78b15..2cb4f72cbd 100644 --- a/tests/shipping/Unit/Managers/ShippingManagerTest.php +++ b/tests/shipping/Unit/Managers/ShippingManagerTest.php @@ -14,7 +14,7 @@ use Lunar\Tests\Shipping\TestCase; use Lunar\Tests\Shipping\TestUtils; -uses(TestCase::class); +uses(TestCase::class)->group('shipping', 'shipping-manager'); uses(RefreshDatabase::class); uses(TestUtils::class); diff --git a/tests/shipping/Unit/Models/ShippingZonePostcodeTest.php b/tests/shipping/Unit/Models/ShippingZonePostcodeTest.php index fa54a2c727..cb21fd81c8 100644 --- a/tests/shipping/Unit/Models/ShippingZonePostcodeTest.php +++ b/tests/shipping/Unit/Models/ShippingZonePostcodeTest.php @@ -5,7 +5,7 @@ use Lunar\Shipping\Models\ShippingZonePostcode; use Lunar\Tests\Shipping\TestCase; -uses(TestCase::class); +uses(TestCase::class)->group('shipping', 'shipping-zone-postcode'); uses(RefreshDatabase::class); diff --git a/tests/shipping/Unit/Observers/OrderObserverTest.php b/tests/shipping/Unit/Observers/OrderObserverTest.php index 191c6d0479..7f89cce8bf 100644 --- a/tests/shipping/Unit/Observers/OrderObserverTest.php +++ b/tests/shipping/Unit/Observers/OrderObserverTest.php @@ -15,7 +15,8 @@ use Lunar\Tests\Shipping\TestCase; use Lunar\Tests\Shipping\TestUtils; -uses(TestCase::class); +uses(TestCase::class) + ->group('shipping', 'shipping-order'); uses(RefreshDatabase::class); uses(TestUtils::class); diff --git a/tests/shipping/Unit/Resolvers/PostcodeResolverTest.php b/tests/shipping/Unit/Resolvers/PostcodeResolverTest.php index 0f173bbacb..7846e5f888 100644 --- a/tests/shipping/Unit/Resolvers/PostcodeResolverTest.php +++ b/tests/shipping/Unit/Resolvers/PostcodeResolverTest.php @@ -1,30 +1,44 @@ group('shipping', 'shipping-postcode'); +uses(RefreshDatabase::class); -test('can get postcode query parts', function () { - $postcode = 'ABC 123'; +test('splits a UK postcode into queryable parts', function () { + $country = Country::factory()->create(['iso2' => 'GB']); - $parts = (new PostcodeResolver)->getParts($postcode); + $parts = (new PostcodeResolver)->getParts('SW1A 1AA', $country); - expect($parts)->toContain('ABC123'); - expect($parts)->toContain('ABC'); - expect($parts)->toContain('AB'); + expect($parts)->toContain('SW1A1AA'); + expect($parts)->toContain('SW1'); + expect($parts)->toContain('SW'); + expect($parts)->toContain('S'); +}); + +test('supportsCountry returns true for every country when the countries array is empty', function () { + $gb = Country::factory()->create(['iso2' => 'GB']); + $us = Country::factory()->create(['iso2' => 'US']); - $postcode = 'NW1 1TX'; + $resolver = new PostcodeResolver; - $parts = (new PostcodeResolver)->getParts($postcode); + expect($resolver->supportsCountry($gb))->toBeTrue(); + expect($resolver->supportsCountry($us))->toBeTrue(); +}); - expect($parts)->toContain('NW11TX'); - expect($parts)->toContain('NW1'); - expect($parts)->toContain('NW'); +test('supportsCountry restricts to listed iso2 codes when the property is set on a subclass', function () { + $gb = Country::factory()->create(['iso2' => 'GB']); + $us = Country::factory()->create(['iso2' => 'US']); - $postcode = 90210; + $resolver = new class extends PostcodeResolver + { + protected array $countries = ['GB']; + }; - $parts = (new PostcodeResolver)->getParts($postcode); - expect($parts)->toContain('90210'); - expect($parts)->toContain('90'); + expect($resolver->supportsCountry($gb))->toBeTrue(); + expect($resolver->supportsCountry($us))->toBeFalse(); }); diff --git a/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php b/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php index 3558116558..a7fc6f43a4 100644 --- a/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php +++ b/tests/shipping/Unit/Resolvers/ShippingOptionResolverTest.php @@ -17,7 +17,8 @@ use Lunar\Tests\Shipping\TestCase; use Lunar\Tests\Shipping\TestUtils; -uses(TestCase::class); +uses(TestCase::class) + ->group('shipping', 'shipping-option'); uses(RefreshDatabase::class); uses(TestUtils::class); diff --git a/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php b/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php index 2648a20e87..ea034e5151 100644 --- a/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php +++ b/tests/shipping/Unit/Resolvers/ShippingRateResolverTest.php @@ -16,7 +16,8 @@ use Lunar\Tests\Shipping\TestCase; use Lunar\Tests\Shipping\TestUtils; -uses(TestCase::class); +uses(TestCase::class) + ->group('shipping', 'shipping-rate'); uses(RefreshDatabase::class); uses(TestUtils::class); diff --git a/tests/shipping/Unit/Resolvers/ShippingZoneResolverTest.php b/tests/shipping/Unit/Resolvers/ShippingZoneResolverTest.php index 694ed3971f..bc241241cd 100644 --- a/tests/shipping/Unit/Resolvers/ShippingZoneResolverTest.php +++ b/tests/shipping/Unit/Resolvers/ShippingZoneResolverTest.php @@ -1,17 +1,30 @@ group('shipping', 'shipping-zone'); uses(RefreshDatabase::class); +// Reset the PostcodeManager singleton between tests so facade-registered resolvers from one +// test don't leak into the next. The service provider's register() closure re-runs on next +// resolve, rebuilding the manager with only the default PostcodeResolver. +beforeEach(function () { + $this->app->forgetInstance(PostcodeManager::class); +}); + test('can fetch shipping zones by country', function () { $countryA = Country::factory()->create(); $countryB = Country::factory()->create(); @@ -111,3 +124,116 @@ expect($zones->first()->id)->toEqual($shippingZone->id); }); + +test('a resolver registered via the Postcode facade takes precedence over the default', function () { + $country = Country::factory()->create(); + + $shippingZone = ShippingZone::factory()->create([ + 'type' => 'postcodes', + ]); + + $shippingZone->countries()->attach($country); + + $shippingZone->postcodes()->createMany([ + ['postcode' => '390*'], + ['postcode' => '391*'], + ]); + + $unmatchedZone = ShippingZone::factory()->create([ + 'type' => 'postcodes', + ]); + + $unmatchedZone->countries()->attach($country); + + $unmatchedZone->postcodes()->create([ + 'postcode' => '393*', + ]); + + Postcode::addResolver(TestCustomPostcodeResolver::class); + + $zones = (new ShippingZoneResolver) + ->postcode(new PostcodeLookup($country, '39100')) + ->get(); + + expect($zones)->toHaveCount(1); + expect($zones->first()->id)->toEqual($shippingZone->id); +})->group('shipping-postcode'); + +test('the last-registered matching resolver wins over earlier custom resolvers', function () { + $country = Country::factory()->create(); + + $shippingZone = ShippingZone::factory()->create([ + 'type' => 'postcodes', + ]); + $shippingZone->countries()->attach($country); + $shippingZone->postcodes()->create(['postcode' => 'LAST-WON']); + + $firstCustom = new class implements PostcodeResolverInterface + { + public function supportsCountry(CountryContract $country): bool + { + return true; + } + + public function getParts(string $postcode, CountryContract $country): Collection + { + return collect(['FIRST-WON']); + } + }; + + $secondCustom = new class implements PostcodeResolverInterface + { + public function supportsCountry(CountryContract $country): bool + { + return true; + } + + public function getParts(string $postcode, CountryContract $country): Collection + { + return collect(['LAST-WON']); + } + }; + + Postcode::addResolver($firstCustom); + Postcode::addResolver($secondCustom); + + $zones = (new ShippingZoneResolver) + ->postcode(new PostcodeLookup($country, 'irrelevant')) + ->get(); + + expect($zones)->toHaveCount(1); + expect($zones->first()->id)->toEqual($shippingZone->id); +})->group('shipping-postcode'); + +test('a resolver whose supportsCountry returns false is skipped and the default handles the lookup', function () { + $country = Country::factory()->create(['iso2' => 'GB']); + + $shippingZone = ShippingZone::factory()->create([ + 'type' => 'postcodes', + ]); + $shippingZone->countries()->attach($country); + // Default resolver's UK parsing will produce 'SW1A', 'SW', 'S' etc. Match on 'SW'. + $shippingZone->postcodes()->create(['postcode' => 'SW']); + + $usOnly = new class implements PostcodeResolverInterface + { + public function supportsCountry(CountryContract $country): bool + { + return $country->iso2 === 'US'; + } + + public function getParts(string $postcode, CountryContract $country): Collection + { + return collect(['NEVER-CALLED']); + } + }; + + Postcode::addResolver($usOnly); + + $zones = (new ShippingZoneResolver) + ->postcode(new PostcodeLookup($country, 'SW1A 1AA')) + ->get(); + + expect($zones)->toHaveCount(1); + expect($zones->first()->id)->toEqual($shippingZone->id); +})->group('shipping-postcode'); diff --git a/tests/shipping/Unit/ShippingModifierTest.php b/tests/shipping/Unit/ShippingModifierTest.php index ca3ecd0da5..0e92fe85f4 100644 --- a/tests/shipping/Unit/ShippingModifierTest.php +++ b/tests/shipping/Unit/ShippingModifierTest.php @@ -12,7 +12,7 @@ use Lunar\Tests\Shipping\TestCase; use Lunar\Tests\Shipping\TestUtils; -uses(TestCase::class); +uses(TestCase::class)->group('shipping', 'shipping-modifier'); uses(RefreshDatabase::class); uses(TestUtils::class); @@ -82,4 +82,4 @@ $option = $cart->refresh()->getShippingOption(); expect($option->price->value)->toBe(0); -})->group('shipping-modifier'); +}); diff --git a/tests/shipping/Unit/ShippingServiceProviderTest.php b/tests/shipping/Unit/ShippingServiceProviderTest.php new file mode 100644 index 0000000000..8d62c3f7b1 --- /dev/null +++ b/tests/shipping/Unit/ShippingServiceProviderTest.php @@ -0,0 +1,19 @@ +group('shipping', 'shipping-postcode'); + +test('the service provider binds PostcodeManager as a singleton with the default resolver pre-registered', function () { + $manager = app(PostcodeManager::class); + + expect($manager)->toBeInstanceOf(PostcodeManager::class); + expect(app(PostcodeManager::class))->toBe($manager); // singleton — same instance + + $resolvers = $manager->getResolvers(); + expect($resolvers)->toHaveCount(1); + expect($resolvers->first())->toBe(PostcodeResolver::class); +});