-
Notifications
You must be signed in to change notification settings - Fork 473
Feat: configurable postcode resolver #2021
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
wychoong
wants to merge
11
commits into
lunarphp:1.x
Choose a base branch
from
wychoong:feat-configurable-postcode-resolver
base: 1.x
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
30d6fb2
add postcode resolver config
wychoong a14857d
add test
wychoong 8d33afb
update shipping tests group
wychoong d6463df
add postcode resolver interface
wychoong 1a614ea
cleanup
wychoong 56f9cc7
Merge branch '1.x' into feat-configurable-postcode-resolver
wychoong 347d6a8
Merge branch '1.x' into feat-configurable-postcode-resolver
wychoong 9d48c04
Merge branch '1.x' into feat-configurable-postcode-resolver
alecritson eb43fad
Merge branch '1.x' into feat-configurable-postcode-resolver
alecritson 48b5fb2
chore: fix code style
actions-user c3ba1a1
Add postcode resolver logic
alecritson File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
packages/table-rate-shipping/src/Exceptions/NoPostcodeResolverException.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| <?php | ||
|
|
||
| namespace Lunar\Shipping\Exceptions; | ||
|
|
||
| use Lunar\Exceptions\LunarException; | ||
|
|
||
| class NoPostcodeResolverException extends LunarException | ||
| { | ||
| public static function forCountry(string $iso2): self | ||
| { | ||
| return new self(sprintf( | ||
| 'No postcode resolver is registered that supports country [%s]. Register a resolver via Postcode::addResolver() or ensure the default PostcodeResolver remains registered.', | ||
| $iso2 | ||
| )); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| <?php | ||
|
|
||
| namespace Lunar\Shipping\Facades; | ||
|
|
||
| use Illuminate\Support\Collection; | ||
| use Illuminate\Support\Facades\Facade; | ||
| use Lunar\Models\Contracts\Country as CountryContract; | ||
| use Lunar\Shipping\Interfaces\PostcodeResolverInterface; | ||
| use Lunar\Shipping\Managers\PostcodeManager; | ||
|
|
||
| /** | ||
| * @method static \Lunar\Shipping\Managers\PostcodeManager addResolver(string|PostcodeResolverInterface $resolver) | ||
| * @method static PostcodeResolverInterface country(CountryContract $country) | ||
| * @method static Collection getResolvers() | ||
| * | ||
| * @see PostcodeManager | ||
| */ | ||
| class Postcode extends Facade | ||
| { | ||
| public static function getFacadeAccessor(): string | ||
| { | ||
| return PostcodeManager::class; | ||
| } | ||
| } |
21 changes: 21 additions & 0 deletions
21
packages/table-rate-shipping/src/Interfaces/PostcodeResolverInterface.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| <?php | ||
|
|
||
| namespace Lunar\Shipping\Interfaces; | ||
|
|
||
| use Illuminate\Support\Collection; | ||
| use Lunar\Models\Contracts\Country as CountryContract; | ||
|
|
||
| interface PostcodeResolverInterface | ||
| { | ||
| /** | ||
| * Whether this resolver supports the given country. | ||
| */ | ||
| public function supportsCountry(CountryContract $country): bool; | ||
|
|
||
| /** | ||
| * Return the postcode parts the resolver wants to match against zone records. | ||
| * | ||
| * @return Collection<int, string> | ||
| */ | ||
| public function getParts(string $postcode, CountryContract $country): Collection; | ||
| } |
88 changes: 88 additions & 0 deletions
88
packages/table-rate-shipping/src/Managers/PostcodeManager.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| <?php | ||
|
|
||
| namespace Lunar\Shipping\Managers; | ||
|
|
||
| use Illuminate\Support\Collection; | ||
| use Lunar\Models\Contracts\Country as CountryContract; | ||
| use Lunar\Shipping\Exceptions\NoPostcodeResolverException; | ||
| use Lunar\Shipping\Interfaces\PostcodeResolverInterface; | ||
|
|
||
| class PostcodeManager | ||
| { | ||
| /** | ||
| * Registered resolvers, in registration order. Entries are either an already-resolved | ||
| * instance or a class-string that will be resolved through the container on first use. | ||
| * | ||
| * @var Collection<int, string|PostcodeResolverInterface> | ||
| */ | ||
| 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<int, string|PostcodeResolverInterface> | ||
| */ | ||
| 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; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
tests/shipping/Stubs/Resolvers/TestCustomPostcodeResolver.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| <?php | ||
|
|
||
| namespace Lunar\Tests\Shipping\Stubs\Resolvers; | ||
|
|
||
| use Illuminate\Support\Collection; | ||
| use Lunar\Models\Contracts\Country as CountryContract; | ||
| use Lunar\Shipping\Interfaces\PostcodeResolverInterface; | ||
|
|
||
| class TestCustomPostcodeResolver implements PostcodeResolverInterface | ||
| { | ||
| /** | ||
| * ISO-2 codes this test resolver claims. Override via subclass if you need a different set. | ||
| * | ||
| * @var array<int, string> | ||
| */ | ||
| 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(); | ||
| } | ||
| } |
38 changes: 38 additions & 0 deletions
38
tests/shipping/Unit/DataTransferObjects/PostcodeLookupTest.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| <?php | ||
|
|
||
| use Illuminate\Foundation\Testing\RefreshDatabase; | ||
| use Illuminate\Support\Collection; | ||
| use Lunar\Models\Contracts\Country as CountryContract; | ||
| use Lunar\Models\Country; | ||
| use Lunar\Shipping\DataTransferObjects\PostcodeLookup; | ||
| use Lunar\Shipping\Facades\Postcode; | ||
| use Lunar\Shipping\Interfaces\PostcodeResolverInterface; | ||
| use Lunar\Tests\Shipping\TestCase; | ||
|
|
||
| uses(TestCase::class) | ||
| ->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']); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be nice DX if this can accept an array of resolvers