Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
without performing an HTTP request, for embedding in an already-
authenticated UI via `<img src="…">`.

### Fixed
- `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,
so sending `EXPRESS_NORDIC_0900` (and the other string names) returned an
empty product list — i.e. "no price". Products without a confirmed numeric
code fall back to the string value, unchanged. Note: Express Nordic 09:00
(`0335`) is being decommissioned by Bring on 2026-09-01 in favour of Bring
Courier & Express (`3620` + VAS `1171`).

### Added
- `Product::shippingGuideCode(): ?string` — the numeric service code the
Shipping Guide v2 endpoints expect, string-typed so leading zeros survive
(`0335`). Distinct from `legacyNumericCode()`, which carries the historical
in-app codes (Express Nordic 09:00 is `4850` there, `0335` here).

### Changed
- `TrackingApi::signature(string)` and `SignatureEndpoint::__construct`
now expect the **signature-link path** (the value Bring returns on
Expand Down
9 changes: 8 additions & 1 deletion src/v4/Endpoint/Shipping/PriceRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,14 @@ public function toQuery(): array
}

if ($this->products !== []) {
$q['product'] = array_map(static fn (Product $p): string => $p->value, $this->products);
// Shipping Guide v2 prices by Bring's numeric service code, not the
// v2 string name (sending EXPRESS_NORDIC_0900 et al. yields an empty
// product list / "no price"). Fall back to the string value only for
// products without a confirmed numeric code.
$q['product'] = array_map(
static fn (Product $p): string => $p->shippingGuideCode() ?? $p->value,
$this->products,
);
}
if ($this->additional !== []) {
$q['additional'] = array_map(static fn (AdditionalService $a): string => $a->value, $this->additional);
Expand Down
42 changes: 41 additions & 1 deletion src/v4/Enum/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ enum Product: string
case RETURN_BUSINESS_PARCEL = 'BUSINESS_PARCEL_RETURN';
case RETURN_BUSINESS_PALLET = 'BUSINESS_PALLET_RETURN';

/** Numeric legacy codes still accepted by some endpoints. */
/**
* In-app/legacy numeric codes some callers still send (e.g. the price
* calculator). These are the historical service numbers carried over from
* the v3 era — NOT necessarily the codes the current Shipping Guide v2
* endpoint prices by. Use {@see shippingGuideCode()} when building a
* Shipping Guide request; the two diverge for Express Nordic 09:00
* (legacy 4850 vs Shipping Guide 0335).
*/
public function legacyNumericCode(): ?int
{
return match ($this) {
Expand All @@ -57,6 +64,39 @@ public function legacyNumericCode(): ?int
};
}

/**
* Product identifier as the Shipping Guide v2 endpoints
* (/shippingguide/v2/products[/price]) expect it in the `product` query
* parameter.
*
* Shipping Guide v2 identifies products by Bring's numeric service codes
* (e.g. "5800" Pickup Parcel, "5600" Home Delivery, "0335" Express Nordic
* 09:00). It does NOT price the v2 string names (EXPRESS_NORDIC_0900, …) —
* sending those returns an empty product list ("no price"). Codes are
* returned as strings so leading zeros survive ("0335" must not become 335).
*
* Returns null for products whose Shipping Guide code is not yet confirmed;
* callers should fall back to the enum string value for those so unknown
* products are no worse off than before.
*
* NOTE: Express Nordic 09:00 (0335) is being decommissioned by Bring on
* 2026-09-01. The successor is Bring Courier & Express (3620) with VAS 1171
* — that migration needs both a product and an additional-service change,
* so it is intentionally NOT silently mapped here.
*/
public function shippingGuideCode(): ?string
{
return match ($this) {
self::PICKUP_PARCEL => '5800',
self::HOME_DELIVERY_PARCEL => '5600',
self::BUSINESS_PARCEL => '5000',
self::EXPRESS_NORDIC_0900 => '0335',
self::MAILBOX_PARCEL => '3584',
self::MAILBOX_PARCEL_TRACKED => '3570',
default => null,
};
}

/**
* Product identifier as Bring's Booking API expects it in product.id.
*
Expand Down
57 changes: 57 additions & 0 deletions tests/v4/Endpoint/Shipping/PriceRequestTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

namespace Bring\Api\Tests\Endpoint\Shipping;

use Bring\Api\Endpoint\Shipping\PriceRequest;
use Bring\Api\Enum\Country;
use Bring\Api\Enum\Product;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(PriceRequest::class)]
final class PriceRequestTest extends TestCase
{
private function request(Product ...$products): PriceRequest
{
return new PriceRequest(
fromCountry: Country::NO,
fromPostalCode: '1712',
toCountry: Country::NO,
toPostalCode: '0150',
packages: [['weightInGrams' => 1000]],
products: array_values($products),
);
}

public function testProductsSerialiseToBringNumericServiceCodes(): void
{
$q = $this->request(Product::BUSINESS_PARCEL, Product::PICKUP_PARCEL)->toQuery();

// Shipping Guide v2 prices by numeric code, not the v2 string name.
self::assertSame(['5000', '5800'], $q['product']);
}

public function testExpressNordicKeepsLeadingZeroAndUsesShippingGuideCode(): void
{
$q = $this->request(Product::EXPRESS_NORDIC_0900)->toQuery();

// 0335 (Shipping Guide v2), NOT the string name and NOT the legacy
// in-app code 4850 — and the leading zero must survive.
self::assertSame(['0335'], $q['product']);
}

public function testUnmappedProductFallsBackToStringValue(): void
{
// No confirmed Shipping Guide numeric code: keep prior behaviour.
$q = $this->request(Product::EXPRESS_INTERNATIONAL)->toQuery();

self::assertSame(['EXPRESS_INTERNATIONAL'], $q['product']);
}

public function testNoProductParamWhenProductsEmpty(): void
{
self::assertArrayNotHasKey('product', $this->request()->toQuery());
}
}