diff --git a/src/v4/Endpoint/Booking/BookingProduct.php b/src/v4/Endpoint/Booking/BookingProduct.php index 6e38752..581ac04 100644 --- a/src/v4/Endpoint/Booking/BookingProduct.php +++ b/src/v4/Endpoint/Booking/BookingProduct.php @@ -24,7 +24,11 @@ public function __construct( /** @return array */ public function toArray(): array { - $a = ['id' => $this->id->value]; + // Booking API wants the numeric service code in product.id, not the + // string enum value the Shipping Guide uses (see Product::bookingProductId + // for the why). Falls back to the string value for products without a + // known numeric mapping. + $a = ['id' => $this->id->bookingProductId()]; if ($this->additionalServices !== []) { $a['additionalServices'] = array_map( static fn (AdditionalService $s): array => ['id' => $s->value], diff --git a/src/v4/Enum/Product.php b/src/v4/Enum/Product.php index 2459b2d..d89990a 100644 --- a/src/v4/Enum/Product.php +++ b/src/v4/Enum/Product.php @@ -56,4 +56,47 @@ public function legacyNumericCode(): ?int default => null, }; } + + /** + * Product identifier as Bring's Booking API expects it in product.id. + * + * The Booking API takes numeric service codes (e.g. "5000" for + * BUSINESS_PARCEL, "9000" for BUSINESS_PARCEL_RETURN) — the same + * codes the v3 SDK used. Sending the v2 Shipping-Guide string name + * (BUSINESS_PARCEL, …) makes Bring's gateway look it up in the + * international catalog and reject Norway→Norway routes with + * BOOK-INPUT-025 / BOOK_VALIDATION-014 ("product not available + * between the given countries" / "customs declarations required for + * exporting from Norway"), even though the parties are both NO. + * + * The mapping below covers the products with codes confirmed against + * the v3 SDK catalog Bring still accepts in v4. Products NOT in this + * match (e.g. HOME_DELIVERY_PARCEL, BUSINESS_PALLET, the home/express + * return variants) fall back to the enum string value — Bring + * accepts strings for some products and the fallback keeps unknown + * cases at least as functional as today. If you hit BOOK-INPUT-025 + * on a product handled by the fallback, look up its numeric code in + * Mybring (Customer numbers → service product line) and add it + * here. Do not guess: a wrong numeric code silently books the wrong + * product, while the string fallback at worst surfaces a 4xx. + */ + public function bookingProductId(): string + { + $numeric = match ($this) { + self::PICKUP_PARCEL => 5800, + self::PICKUP_PARCEL_BULK => 5802, + self::BUSINESS_PARCEL => 5000, + self::BUSINESS_PARCEL_BULK => 5100, + self::EXPRESS_NORDIC_0900 => 4850, + self::MAILBOX_PARCEL => 3584, + self::MAILBOX_PARCEL_TRACKED => 3570, + self::CARGO_GROUPAGE => 5300, + self::RETURN_PICKUP_PARCEL => 9300, + self::RETURN_BUSINESS_PARCEL => 9000, + self::RETURN_BUSINESS_PALLET => 9100, + default => null, + }; + + return $numeric === null ? $this->value : (string) $numeric; + } } diff --git a/tests/v4/Endpoint/Booking/BookingApiTest.php b/tests/v4/Endpoint/Booking/BookingApiTest.php index 5c74c72..935c21e 100644 --- a/tests/v4/Endpoint/Booking/BookingApiTest.php +++ b/tests/v4/Endpoint/Booking/BookingApiTest.php @@ -47,7 +47,7 @@ public function testBookSerialisesAndPostsToCorrectUrl(): void $request = BookingRequest::single( schemaVersion: '1', customerNumber: 'PARCELS_NORWAY-10001234567', - product: Product::HOME_DELIVERY_PARCEL, + product: Product::BUSINESS_PARCEL, sender: $sender, recipient: $recipient, packages: [new Package(weightInKg: 2)], @@ -67,7 +67,9 @@ public function testBookSerialisesAndPostsToCorrectUrl(): void self::assertArrayNotHasKey('customerNumber', $body, 'customerNumber must not be at request root — Bring expects it under consignments[].product'); self::assertSame('PARCELS_NORWAY-10001234567', $body['consignments'][0]['product']['customerNumber']); self::assertTrue($body['testIndicator']); - self::assertSame(Product::HOME_DELIVERY_PARCEL->value, $body['consignments'][0]['product']['id']); + // Booking API expects the numeric service code, not the string enum + // value (which the Shipping Guide uses). BUSINESS_PARCEL → "5000". + self::assertSame('5000', $body['consignments'][0]['product']['id']); self::assertSame('EVARSLING', $body['consignments'][0]['product']['additionalServices'][0]['id']); self::assertSame('Sender Co', $body['consignments'][0]['parties']['sender']['name']); self::assertSame('NO', $body['consignments'][0]['parties']['recipient']['countryCode']);