diff --git a/CHANGELOG.md b/CHANGELOG.md index 40a7617..f56bcdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 authenticated UI via ``. ### Fixed +- `PriceRequest::toQuery()` now gives every package its own indexed query + parameters (`weightInGrams0`, `length0`, …, `weightInGrams1`, …) when a + request carries more than one package. The previous shape repeated the bare + key (`weightInGrams=1200&weightInGrams=800`), which Shipping Guide v2 + collapses to a single value — so a 2-, 3- or 20-package request was priced + as one package and every quote came back identical regardless of package + count. A single-package request keeps the unsuffixed names it always used. - `PriceRequest::toQuery()` now serialises the `product` query parameter as Bring's numeric service code (via the new `Product::shippingGuideCode()`) instead of the v2 string name. Shipping Guide v2 prices by numeric code, diff --git a/src/v4/Endpoint/Shipping/PriceRequest.php b/src/v4/Endpoint/Shipping/PriceRequest.php index d2c82bf..61e6a08 100644 --- a/src/v4/Endpoint/Shipping/PriceRequest.php +++ b/src/v4/Endpoint/Shipping/PriceRequest.php @@ -67,16 +67,25 @@ public function toQuery(): array 'toPostalCode' => $this->toPostalCode, ]; - foreach ($this->packages as $i => $pkg) { - $q['weightInGrams'][$i] = (int) $pkg['weightInGrams']; + // Shipping Guide v2 addresses each package by an index *suffix* on the + // parameter name (weightInGrams0, length0, ..., weightInGrams1, ...) — + // not by a repeated bare key. A repeated `weightInGrams` is collapsed to + // a single value server-side, so every multi-package request was priced + // as one package. A lone package keeps the unsuffixed names, which is + // what the API documents for the single-package case. + $packages = array_values($this->packages); + $multiple = \count($packages) > 1; + foreach ($packages as $i => $pkg) { + $suffix = $multiple ? (string) $i : ''; + $q['weightInGrams'.$suffix] = (int) $pkg['weightInGrams']; if (isset($pkg['length'])) { - $q['length'][$i] = (int) $pkg['length']; + $q['length'.$suffix] = (int) $pkg['length']; } if (isset($pkg['width'])) { - $q['width'][$i] = (int) $pkg['width']; + $q['width'.$suffix] = (int) $pkg['width']; } if (isset($pkg['height'])) { - $q['height'][$i] = (int) $pkg['height']; + $q['height'.$suffix] = (int) $pkg['height']; } } diff --git a/tests/v4/Endpoint/Shipping/PriceRequestTest.php b/tests/v4/Endpoint/Shipping/PriceRequestTest.php index 67b3894..abcdd60 100644 --- a/tests/v4/Endpoint/Shipping/PriceRequestTest.php +++ b/tests/v4/Endpoint/Shipping/PriceRequestTest.php @@ -55,4 +55,47 @@ public function testNoProductParamWhenProductsEmpty(): void { self::assertArrayNotHasKey('product', $this->request()->toQuery()); } + + public function testSinglePackageUsesUnsuffixedParameterNames(): void + { + $q = (new PriceRequest( + fromCountry: Country::NO, + fromPostalCode: '1712', + toCountry: Country::NO, + toPostalCode: '0150', + packages: [['weightInGrams' => 1000, 'length' => 30, 'width' => 20, 'height' => 15]], + ))->toQuery(); + + self::assertSame(1000, $q['weightInGrams']); + self::assertSame(30, $q['length']); + self::assertSame(20, $q['width']); + self::assertSame(15, $q['height']); + } + + public function testEachPackageGetsItsOwnIndexedParameters(): void + { + // Bring indexes multi-package requests by suffixing the parameter name. + // Repeating a bare `weightInGrams` loses every package but one, which is + // what made the price identical for 1, 2 and 20 packages. + $q = (new PriceRequest( + fromCountry: Country::NO, + fromPostalCode: '1712', + toCountry: Country::NO, + toPostalCode: '0150', + packages: [ + ['weightInGrams' => 1000, 'length' => 30, 'width' => 20, 'height' => 15], + ['weightInGrams' => 2500], + ['weightInGrams' => 400], + ], + ))->toQuery(); + + self::assertSame(1000, $q['weightInGrams0']); + self::assertSame(2500, $q['weightInGrams1']); + self::assertSame(400, $q['weightInGrams2']); + self::assertSame(30, $q['length0']); + self::assertSame(20, $q['width0']); + self::assertSame(15, $q['height0']); + self::assertArrayNotHasKey('weightInGrams', $q); + self::assertArrayNotHasKey('length1', $q); + } } diff --git a/tests/v4/Http/TransportTest.php b/tests/v4/Http/TransportTest.php index e52af47..0d63a5a 100644 --- a/tests/v4/Http/TransportTest.php +++ b/tests/v4/Http/TransportTest.php @@ -178,7 +178,7 @@ public function testGuzzleBadResponseExceptionSurfacesAsBringApiException(): voi } } - public function testShippingPriceQueryBuildsBareRepeatedKeys(): void + public function testShippingPriceQueryIndexesEachPackage(): void { $client = new RecordingClient([new Response(200, ['Content-Type' => 'application/json'], '{"Product":[]}')]); $transport = new Transport($client, $this->factory, $this->factory, $this->factory, new NullAuthorization()); @@ -193,11 +193,32 @@ public function testShippingPriceQueryBuildsBareRepeatedKeys(): void $transport->send(new PriceEndpoint($request)); $uri = (string) $client->lastRequest()->getUri(); - // Repeated weightInGrams without [0]/[1] subscripts (Bring's expected format). - self::assertStringContainsString('weightInGrams=1200', $uri); - self::assertStringContainsString('weightInGrams=800', $uri); + // Shipping Guide v2 needs one suffixed parameter per package; a repeated + // bare `weightInGrams` is collapsed server-side and prices one package. + self::assertStringContainsString('weightInGrams0=1200', $uri); + self::assertStringContainsString('weightInGrams1=800', $uri); + self::assertStringNotContainsString('weightInGrams=', $uri); self::assertStringNotContainsString('weightInGrams%5B0%5D=', $uri); - self::assertStringNotContainsString('weightInGrams[0]=', $uri); + } + + public function testShippingPriceQueryKeepsBareKeysForASinglePackage(): void + { + $client = new RecordingClient([new Response(200, ['Content-Type' => 'application/json'], '{"Product":[]}')]); + $transport = new Transport($client, $this->factory, $this->factory, $this->factory, new NullAuthorization()); + + $request = new PriceRequest( + fromCountry: Country::NO, + fromPostalCode: '0150', + toCountry: Country::NO, + toPostalCode: '5003', + packages: [['weightInGrams' => 1200, 'length' => 30, 'width' => 20, 'height' => 15]], + ); + $transport->send(new PriceEndpoint($request)); + + $uri = (string) $client->lastRequest()->getUri(); + self::assertStringContainsString('weightInGrams=1200', $uri); + self::assertStringContainsString('length=30', $uri); + self::assertStringNotContainsString('weightInGrams0=', $uri); } public function testPostalCodeRequestUrl(): void