diff --git a/docs/postnl-v4-migration/approach-2/architecture.md b/docs/postnl-v4-migration/approach-2/architecture.md new file mode 100644 index 00000000..6aab65f4 --- /dev/null +++ b/docs/postnl-v4-migration/approach-2/architecture.md @@ -0,0 +1,198 @@ +# Approach 2 — Architecture & Folder Structure + +## Pattern + +**Per-flow service interfaces with two implementations: `Legacy` (current V1 HTTP) and `V4` (new SDK).** A `Service_Factory` selects the implementation at runtime based on V4-key presence + per-flow `Router` filter. Callers depend only on the interfaces. + +## Folder structure + +``` +src/Rest_API/ +├── Contracts/ (new — interface contracts) +│ ├── Barcode_Service_Interface.php +│ ├── Timeframe_Service_Interface.php +│ ├── Pickup_Location_Service_Interface.php +│ ├── Label_Service_Interface.php +│ ├── Return_Label_Service_Interface.php +│ ├── Postcode_Check_Service_Interface.php +│ └── Smart_Returns_Service_Interface.php +├── Legacy/ (existing — moved + implements interfaces) +│ ├── Barcode/ +│ │ ├── Client.php (existing class, now implements Barcode_Service_Interface) +│ │ └── Item_Info.php (unchanged) +│ ├── Checkout/ (existing combined timeframe+pickup; split at the interface layer) +│ ├── Shipping/ +│ ├── Return_Label/ +│ ├── Letterbox/ +│ ├── Shipment_and_Return/ +│ ├── Postcode_Check/ +│ ├── Smart_Returns/ +│ ├── Base.php (existing HTTP base; unchanged) +│ └── Base_Info.php (existing payload base; unchanged) +├── V4/ (new — SDK-backed implementations) +│ ├── Barcode/ +│ │ ├── Service.php (implements Barcode_Service_Interface) +│ │ └── Request_Builder.php (maps plugin args → BarcodeRequest DTO) +│ ├── Timeframe/ +│ ├── Pickup_Location/ +│ ├── Label/ +│ ├── Return_Label/ +│ ├── Postcode_Check/ (uses V1 PostalCodeCheckExtension — SDK exposes nothing newer) +│ └── Smart_Returns/ +├── SDK/ (new — SDK wiring) +│ ├── Client_Factory.php (builds PostnlClientInterface from settings) +│ ├── Logger_Adapter.php (WC_Logger → PSR-3) +│ ├── Cache_Adapter.php (WP transients → PSR-16) +│ └── Exception_Converter.php (SDK exceptions → plugin error shape) +├── Service_Factory.php (new — chooses Legacy vs V4 per flow) +└── Router.php (new — per-flow filter gating) + +src/Helper/ +└── Product_Mapper/ (new — Phase 0.1) + ├── V1_Mapper.php (extracted from Mapping.php; same legacy codes) + └── V4_Mapper.php (legacy options → ShipmentType + Services) +``` + +## Interface contracts + +One example shown; all seven follow the same pattern (small surface, return-shape contract). + +```php +namespace PostNLWooCommerce\Rest_API\Contracts; + +interface Barcode_Service_Interface { + + /** + * Generate a single barcode for a shipment. + * + * @param array $args { + * @type string $type Barcode type, e.g. '3S', 'UE', 'LA', 'CD'. + * @type string $range Barcode range derived from type. + * @type string $serie Serie range, e.g. '000000000-999999999'. + * @type string $customer_code 4-char customer code. + * @type string $customer_number Numeric customer number. + * } + * + * @return array { 'Barcode': string } + * + * @throws \PostNLWooCommerce\Exception\Service_Exception + */ + public function generate( array $args ): array; +} +``` + +Each interface defines: +- **Parameter shape** — what callers pass in (matches existing `Item_Info` output). +- **Return shape** — same array shape both Legacy and V4 produce, so callers (`Order/Base.php` etc.) don't branch. +- **Exception type** — both implementations throw the same `Service_Exception` (Legacy uses existing error handling; V4 uses the `Exception_Converter`). + +## Class diagram + +```mermaid +classDiagram + class Barcode_Service_Interface { + <> + +generate(array args) array + } + + class Legacy_Barcode_Client { + +generate(array args) array + -send_request() array + } + + class V4_Barcode_Service { + -PostnlClientInterface client + +generate(array args) array + -build_request(array args) BarcodeRequest + -map_response(BarcodeResponseInterface) array + } + + class Service_Factory { + -Settings settings + -SDK_Client_Factory sdk_factory + +barcode_service() Barcode_Service_Interface + +label_service() Label_Service_Interface + +timeframe_service() Timeframe_Service_Interface + } + + class Router { + <> + +sdk_enabled_for(string flow) bool + } + + class SDK_Client_Factory { + +build(string v4_key, bool sandbox) PostnlClientInterface + } + + Barcode_Service_Interface <|.. Legacy_Barcode_Client + Barcode_Service_Interface <|.. V4_Barcode_Service + Service_Factory --> Barcode_Service_Interface : returns + Service_Factory --> Router : checks + Service_Factory --> SDK_Client_Factory : uses + V4_Barcode_Service --> SDK_Client_Factory : built from + + class Order_Base { + +create_barcode() + } + Order_Base --> Service_Factory : depends on + Order_Base ..> Barcode_Service_Interface : calls +``` + +## Request flow (sequence) + +```mermaid +sequenceDiagram + participant Caller as Order/Base.php + participant Factory as Service_Factory + participant Router + participant Settings + participant Service as Barcode_Service
(Legacy or V4) + participant Transport as HTTP / SDK + + Caller->>Factory: barcode_service() + Factory->>Settings: get_v4_api_key() + Factory->>Router: sdk_enabled_for('barcode') + alt V4 key set AND filter enabled + Factory-->>Caller: V4_Barcode_Service + else + Factory-->>Caller: Legacy_Barcode_Client + end + Caller->>Service: generate($args) + Service->>Transport: HTTP request / SDK call + Transport-->>Service: response + Service-->>Caller: ['Barcode' => 'xxx'] (same shape either way) +``` + +## File-by-file change summary + +| Existing file | Change | Phase | +|---|---|---| +| `src/Helper/Mapping.php` | Extract into `Helper/Product_Mapper/V1_Mapper.php`; add unit tests for all 72 combinations | 0.1 | +| `src/Rest_API/Barcode/Client.php` | Move to `Rest_API/Legacy/Barcode/Client.php`; `implements Barcode_Service_Interface` | 1.2 | +| `src/Rest_API/Checkout/Client.php` | Move to `Rest_API/Legacy/Checkout/Client.php`; split into two interface methods (timeframe + pickup) | 1.2 | +| `src/Rest_API/Shipping/Client.php` | Move to `Rest_API/Legacy/Shipping/Client.php`; `implements Label_Service_Interface` | 1.2 | +| `src/Rest_API/Return_Label/Client.php` | Move to `Rest_API/Legacy/Return_Label/Client.php`; `implements Return_Label_Service_Interface` | 1.2 | +| `src/Rest_API/Smart_Returns/Client.php` | Move to `Rest_API/Legacy/Smart_Returns/Client.php`; `implements Smart_Returns_Service_Interface` | 1.2 | +| `src/Rest_API/Postcode_Check/Client.php` | Move to `Rest_API/Legacy/Postcode_Check/Client.php`; `implements Postcode_Check_Service_Interface` | 1.2 | +| `src/Rest_API/Letterbox/Client.php` | Move to `Rest_API/Legacy/Letterbox/Client.php` (still extends Shipping) | 1.2 | +| `src/Rest_API/Shipment_and_Return/Client.php` | Move to `Rest_API/Legacy/Shipment_and_Return/Client.php` (still extends Shipping) | 1.2 | +| `src/Rest_API/Base.php` | Move to `Rest_API/Legacy/Base.php`; no behavioral change | 1.2 | +| `src/Rest_API/Base_Info.php` | Move to `Rest_API/Legacy/Base_Info.php`; no behavioral change | 1.2 | +| `src/Order/Base.php::create_barcode()` | Switch to `Service_Factory->barcode_service()->generate(...)` | 1.2 | +| `src/Order/Base.php::create_shipping_label()` | Switch to `Service_Factory->label_service()->create(...)` | 1.2 | +| `src/Order/Single.php` (AJAX handlers) | Inject `Service_Factory`; call interfaces instead of `new Client()` | 1.2 | +| `src/Checkout_Blocks/Extend_Block_Core.php` | Wire to timeframe + pickup services via factory (was single Checkout/Client call) | 2.4 | +| `src/Frontend/Container.php` | Same — call two services and compose | 2.4 | +| `src/Logger.php` | Unchanged — legacy paths still use `check_pdf_content()`. SDK path uses `Logger_Adapter` + SDK redaction. | — | +| `src/Shipping_Method/Settings.php` | V4-key fields added in separate PR (already in flight) | (other PR) | +| `composer.json` | `postnl/api-client-sdk` added (done) | — | + +## What does NOT change + +- Order metadata structure (`_postnl_order_metadata` shape, key names, barcode/label storage) +- Filters and actions (`postnl_shipment_addresses`, `postnl_order_weight`, etc.) — fired from both transports at the same callsite-equivalent point +- Frontend React components (`client/checkout/postnl-container/block.js` etc.) — they consume the same shape from REST endpoints +- Tracking URL generation +- Email templates and notifications +- Settings UI (other than the V4-key field added in the separate PR) +- Database schema diff --git a/docs/postnl-v4-migration/approach-2/implementation-approach.md b/docs/postnl-v4-migration/approach-2/implementation-approach.md new file mode 100644 index 00000000..e4f4f8c4 --- /dev/null +++ b/docs/postnl-v4-migration/approach-2/implementation-approach.md @@ -0,0 +1,223 @@ +# Approach 2 — Implementation Approach + +## TL;DR + +Build a thin interface layer above the existing REST clients. Two implementations per flow: `Legacy` (existing HTTP code, unchanged) and `V4` (new SDK-backed). A `Service_Factory` picks one based on V4-key presence plus a per-flow filter gate. Callers stop knowing about transports. + +Migration is **opt-in per site** (driven by whether the merchant has filled in the V4 API key field) and **opt-in per flow** (driven by per-flow filters that the dev team controls). Legacy code path is permanent — not transitional. + +## Design rationale + +The previous plan grafted SDK branches *inside* existing client classes (`if ( Router::use_sdk_for() ) { ... }`). That approach entangles two transports and two payload shapes in the same file, makes testing harder, and makes future deletion impossible. + +The chosen alternative — interfaces with two implementations — has these properties: + +- **Compile-time contract.** Both implementations satisfy the same interface; return-shape drift becomes an interface violation, not a runtime surprise. +- **Legacy code untouched.** No behavioral changes to current V1 paths. Sites without a V4 key are bit-identical to today. +- **Independent testability.** V4 services can be unit-tested with mocked SDK clients. Legacy clients keep their existing test coverage (or lack thereof) unchanged. +- **Modern PHP for new code.** V4 services can use readonly DTOs, typed properties, enums — without retrofitting legacy code to PHP 8.2 idioms. +- **Clean deletion path.** When V4 reaches 100% adoption, removing the `Legacy/` directory is mechanical. + +The cost is one wrapping pass over existing client classes plus seven small interface files. That's Phase 1.2 (~8–12h). + +## Phasing + +Phases are sequential. Phase 0 must complete before Phase 2 starts; Phase 1 can overlap Phase 0.1 once the interfaces are settled. + +### Phase 0 — Mapper extraction (the contract) + +Extract the 72 product-code combinations in `src/Helper/Mapping.php` into a `Helper/Product_Mapper/V1_Mapper.php` class with unit tests covering every combination. This becomes the **acceptance criteria for the V4 mapper**: every legacy code must map to a V4 `ShipmentType` + `Services` combination that PostNL confirms produces the same label. + +This is the longest-lead-time piece (because PostNL confirmation is part of it) and must finish before Phase 2.6 (Shipping label) can start. + +### Phase 1 — Foundations + +In order: + +1. Define seven interfaces in `src/Rest_API/Contracts/`. +2. Move existing `Client` classes into `src/Rest_API/Legacy/` and add `implements _Service_Interface`. +3. Build `Service_Factory`, `Router`, `SDK\Client_Factory`, `SDK\Logger_Adapter`, `SDK\Cache_Adapter`, `SDK\Exception_Converter`. +4. Cut callers (`Order/Base.php`, `Order/Single.php`, AJAX handlers) over to the factory. **At end of Phase 1, all flows still go through the legacy clients** — V4 implementations don't exist yet. This is a refactor PR with zero behavior change. +5. Establish PHPUnit harness if not already in place; integration tests covering each Legacy implementation against the interface contract. + +End-of-phase test: turn off legacy entirely and the plugin should error cleanly in every flow. Turn it back on and everything works as today. + +### Phase 2 — Per-flow V4 implementations + +Each flow is its own PR. Order from lowest to highest risk: + +1. **2.1 Barcode** — simplest; pure utility. +2. **2.2 Timeframes** — checkout-critical; caching wired. +3. **2.3 Pickup Locations** — same risk profile as Timeframes. +4. **2.4 Checkout aggregation** — consumer-level refactor; V1 returned both flows in one call, V4 needs two. Affects `Checkout_Blocks/Extend_Block_Core.php` and `Frontend/Container.php`. +5. **2.5 Postcode check** — uses SDK's V1 `PostalCodeCheckExtension`; no real V4 equivalent exists. +6. **2.6 Shipping label** — the big one; depends on Phase 0.1 + PostNL mapping sign-off. +7. **2.7 Return label** — depends on 2.6. +8. **2.8 Smart Returns** — start after PostNL confirms the V4 activation flow. +9. **2.9 Letterbox + Shipment_and_Return** — variants of 2.6; should fall out cleanly. + +Each PR adds the V4 service, enables it behind a filter (default off), and ships. Once staging parity is confirmed, the team flips the filter to default on in a separate PR or by updating the default in `Router::sdk_enabled_for()`. + +### Phase 3 — Cross-cutting + +QA, documentation, filter audit, bug fixes from staging discoveries. Spread across all phases but bucketed for budget. + +## Routing model + +Two independent gates must both pass for a request to go through the SDK: + +```php +public function barcode_service(): Barcode_Service_Interface { + $v4_key = $this->settings->get_v4_api_key(); + if ( '' !== $v4_key && Router::sdk_enabled_for( 'barcode' ) ) { + return new V4\Barcode\Service( $this->sdk_client_factory->build( $v4_key, $this->settings->is_sandbox() ) ); + } + return new Legacy\Barcode\Client( $this->settings ); +} +``` + +### Gate 1 — V4 key presence + +The merchant has entered a V4 API key. Until they do, **nothing changes** — every flow still uses the legacy client and the legacy API key. The new key field (added in a separate PR already in flight) exists alongside the legacy key field, not in place of it. Both keys coexist; the legacy key is still used for any flow that isn't routed to V4. + +### Gate 2 — Per-flow filter + +```php +apply_filters( "postnl_sdk_enable_{$flow}", false ); +``` + +Filter is `false` by default for every flow. The plugin's own bootstrap can flip individual filters to `true` once a flow has reached staging parity: + +```php +add_filter( 'postnl_sdk_enable_barcode', '__return_true' ); +``` + +This allows: + +- **Staged rollout.** Enable barcode for one site; observe; expand. +- **Per-site override.** A site operator can disable a specific flow by returning `false` from the filter in `functions.php`. +- **Emergency revert.** Set every filter to `false` via a single bootstrap edit — V4 turns off everywhere without a code revert. + +### Sandbox vs production + +`SDK\Client_Factory::build()` reads the `environment_mode` setting (which already exists for legacy) and passes the matching V4 key. Sandbox/production toggle is orthogonal to V4 routing. + +## Backward compatibility + +### Legacy-key sites: zero behavioral change + +No V4 key → V4 services never instantiated → no SDK code runs. The legacy code paths are exactly the same as before this migration. Risk envelope for these sites is the same as a no-op refactor (the file moves into `Legacy/` and gains an interface declaration; no logic changes). + +### V4-key sites: opt-in per flow + +The merchant has the V4 key but the per-flow filter is still off → still uses the legacy client with the legacy key (which their new V4 key also works for, since new keys work with both APIs). They're paying nothing for entering the V4 key until the dev team flips a filter. + +### Filters and actions + +The existing plugin exposes several public filters that third-party extensions depend on: + +- `postnl_shipment_addresses` (in `Shipping/Client.php`) +- `postnl_order_weight` (multiple files) +- `postnl_order_meta_box_fields` +- `postnl_logger_write_message` + +Each V4 service implementation must fire the equivalent filter at the equivalent point in request construction with the same parameter shape. This is Phase 3.1 work and must be audited per-flow before that flow's V4 implementation is enabled. + +### Order metadata + +`_postnl_order_metadata` shape stays identical. V4 services map their response back to the existing structure: `barcodes[]`, `labels[]`, `backend{}`, `frontend{}`. Existing orders processed under V1 remain readable; new orders processed under V4 store data in the same keys. + +### In-flight orders + +An order whose barcode was generated by V1 but whose label generation happens after V4 is enabled: the stored barcode (V1 shape) goes into the V4 `items[].barcode` field. **Confirmation needed from PostNL** (see open-questions doc) that V4 accepts pre-existing V1-format barcodes. + +## Test strategy + +### Phase 0 — mapper + +Unit tests for all 72 product-code combinations against the extracted `V1_Mapper`. These tests establish the contract that the V4 mapper must satisfy. + +### Phase 1 — interfaces + +Contract tests: a generic suite that, given any implementation of a service interface, verifies it accepts the expected input shape and produces the expected output shape. Both Legacy adapters and V4 services run this suite. + +### Phase 2 — per-flow + +For each V4 service: + +- **Unit tests** against a mocked `PostnlClientInterface`. Verifies request DTO construction is correct and response mapping is correct. +- **Integration tests** against the PostNL sandbox (gated to CI environments with credentials). +- **Staging parity tests.** With V4 enabled, run the same operation (create order → barcode → label) on staging and compare order meta to a baseline produced by V1. Differences must be intentional and documented. + +Each flow's filter does not flip to default-on until staging parity is confirmed. + +### Phase 3 — manual QA + +Classic checkout + Blocks checkout, both. Multicollo, customs declaration, insured shipping, ID check, signature on delivery, letterbox auto-detection, pickup-point selection, smart returns activation. + +## Rollout & rollback + +### Rollout + +1. Ship Phase 1 (refactor only). Zero behavioral change. +2. Ship Phase 2.1 (Barcode V4). Filter default `false`. +3. Enable barcode filter on internal/staging sites with V4 keys. +4. Validate for one week; check support tickets. +5. Flip barcode default to `true` in `Router::sdk_enabled_for()` for next minor release. +6. Repeat for next flow. + +### Rollback levels (in order of preference) + +1. **Per-site, per-flow:** site operator adds `add_filter( 'postnl_sdk_enable_barcode', '__return_false' )` in `functions.php` — instant. +2. **Per-site, all flows:** site operator clears the V4 API key field in settings — instant. +3. **Per-flow, globally:** flip the default in the plugin's `Router::sdk_enabled_for()` from `true` back to `false`; ship a patch release — minutes to deploy. +4. **All flows, globally:** revert the PR that enabled defaults — minutes. + +No rollback requires data migration, schema change, or order re-processing. The Legacy code path stays installed and functional throughout. + +## Cross-cutting concerns + +### Logging & redaction + +- **Legacy path** continues to use `Logger.php::check_pdf_content()` — V1-shaped, scans `Shipments[].Labels[].Content`. Unchanged. +- **V4 path** uses the SDK's built-in `RedactionRegistry::forProduction()` (on by default), which redacts label binary, PII, and credentials before messages reach the PSR-3 adapter. The plugin's `WC_Logger`-backed adapter just writes whatever the SDK gives it. + +Both paths log to the same WC log channel with a tag distinguishing them (`[postnl-legacy]` vs `[postnl-v4]`) so support can filter on it. + +### Caching + +V4 timeframes and pickup-location calls are cached via SDK `CachingPlugin`: + +- Backend: WP transients via `SDK\Cache_Adapter` (PSR-16). +- TTL: 600s (configurable via filter). +- Allowlist: only `/timeframe/` and `/locations/` endpoints. +- Key includes the V4 API key prefix to avoid cross-tenant collisions. + +Legacy path uses whatever caching exists today (or none); not changed in this migration. + +### Error mapping + +`SDK\Exception_Converter` translates SDK exception hierarchy → plugin's existing error shape: + +- `HttpSdkException::getCode()` carries the HTTP status code — preserved. +- PostNL error details from `ProblemDetails` (including `traceId`) are extracted and included in the converted exception message for support correlation. +- `AuthenticationException` → "Invalid PostNL API credentials" (user-facing). +- `ValidationException` → bubble the field-level errors from `ProblemDetails`. +- `RateLimitException`, `TimeoutException` → "PostNL temporarily unavailable, please try again." + +Legacy error handling unchanged. + +### Order metadata API-version tagging + +Each label/barcode written by a V4 service tags itself in order meta: + +```php +$metadata['labels'][0]['api_version'] = 'v4'; +$metadata['barcodes'][0]['api_version'] = 'v4'; +``` + +Absent for V1-generated entries (or backfilled as `'v1'` lazily). Allows support to determine, six months from now, which transport produced any given barcode. + +## Open items handed off to PostNL + +See `postnl-open-questions.md` for the full list. The single most important one is the product-code mapping confirmation — without it, Phase 2.6 cannot start. diff --git a/docs/postnl-v4-migration/approach-2/postnl-open-questions.md b/docs/postnl-v4-migration/approach-2/postnl-open-questions.md new file mode 100644 index 00000000..7587da10 --- /dev/null +++ b/docs/postnl-v4-migration/approach-2/postnl-open-questions.md @@ -0,0 +1,157 @@ +# Open Questions for PostNL + +This document collects questions about the V4 API that benefit from PostNL input. Items are grouped by which migration phases they affect. + +--- + +## Critical (block the migration) + +### 1. Product-code → `ShipmentType` + `Services` mapping confirmation + +**The single most important item.** The plugin today maps merchant-configured shipping options to 72 distinct legacy product codes (3085, 3189, 3438, 3533, 4946, 6440, 2928, 3285, etc.) across NL → NL/BE/EU/ROW zones plus pickup-point variants. + +The V4 SDK does not take product codes. It takes: + +- `ShipmentType` enum: `parcel`, `parcelnonstandard`, `letter`, `letterbox`, `pallet`, `packet` +- `Services` object with discrete boolean flags: `registered`, `returnWhenNotHome`, `statedAddressOnly`, `minimalAgeCheck`, `deliveryConfirmation`, `deliveryWindow`, `insuredValue` +- `DeliveryLocation` for pickup-point selection +- `InternationalShipmentData` for non-NL destinations + +For each combination, the resolution is either a confirmed 1:1 V4 mapping, the equivalent V4 configuration if the legacy option no longer exists, or a decision that the flow should remain on V1. + +**Format:** the full mapping table is maintained as a shared spreadsheet — see "Product mapping confirmation" below. + +### 2. Smart Returns on V4 — does the flow change? + +Today the plugin calls `POST /shipment/v2_2/label/` (V2.2, not V1) with a hardcoded `Characteristic: 152, Option: 025`. The merchant-facing flow is: admin activates a return → PostNL issues a barcode + activation link → customer receives an email with a QR/link → customer activates within a return window. + +The V4 SDK has `ReturnsInterface::generateReturn(ReturnShipmentRequest)`. Questions: + +1. Does the V4 endpoint replace V2.2 entirely, or do both coexist? +2. Is the customer-facing activation flow (email + activation link + return window) the same in V4? +3. The V2.2 hardcoded `Characteristic: 152, Option: 025` — what does that translate to in V4 (`ReturnPeriod` enum? a `Services` flag?) +4. Is there a separate API call for "activate return" vs "generate return label," or does `generateReturn()` cover both? + +Phase 2.8 (Smart Returns) implementation begins once these are resolved. + +### 3. Acceptance of V1-format barcodes in V4 label calls + +In-flight orders: an order's barcode was generated by V1 (via `GET /shipment/v1_1/barcode`, returning a `3SXXXXXXXXX` string). The label generation happens later, after the merchant has enabled V4 labelling. The plugin passes the existing V1 barcode into the V4 `ShipmentDeliveryRequest::items[].barcode` field. + +**Question:** Will V4 accept a barcode that was issued by V1, or must barcodes used in V4 label calls also be generated through V4? + +If the answer is "must be V4," the plugin needs to enforce that barcode and label generation always use the same transport for a given order. If the answer is "either works," no change needed. + +--- + +## Important (block specific flows) + +### 4. `location_code = '123456'` — what is this supposed to be? + +`src/Shipping_Method/Settings.php:835` hardcodes `'123456'` as `CollectionLocation` in shipping requests. The comment says "Temporarily hardcoded." + +**Question:** Is this a per-customer value PostNL assigns, a constant for all customers, or a placeholder? V1 appears not to validate it; should V4 callers expect validation against the merchant's account? + +Depending on the answer, the plugin will either expose it as a per-merchant setting or use a confirmed value. + +### 5. `MessageID` UUID reuse + +Every V1 request from this plugin sends `MessageID: '36209c3d-14d2-478f-85de-abccd84fa790'` — the same UUID for every order, every customer, every site. + +**Question:** Does V4 enforce uniqueness on `MessageID` (e.g., for idempotency / replay protection)? The plugin will switch to `wp_generate_uuid4()` per request regardless; this question is just to understand whether V4 will reject the duplicate UUIDs that V1 accepted, so any in-flight requests during the cut-over are handled correctly. + +### 6. Postcode check on V4 + +The SDK exposes postal-code validation only as a V1 Extension (`PostalCodeCheckExtension`), hitting `/shipment/checkout/v1/postalcodecheck`. No V4 equivalent exists in the SDK. + +**Question:** Is V4 postal-code validation on the PostNL roadmap, or is V1 the permanent home for this functionality? If permanent, the plugin will stay on V1 for this flow indefinitely. + +### 7. Customs declaration mapping (NL → ROW orders) + +V1 plugin sends a `Customs` block with hardcoded structure. V4 SDK uses `InternationalShipmentData`. + +**Question:** Field-by-field mapping confirmation. Specifically: HS code, country of origin, currency, declared value, invoice number, weight per item. Are V4 field names and required fields the same as the legacy `Customs` block? + +### 8. Barcode types per zone + +V1 plugin selects barcode `Type` based on destination: `3S` for NL/BE, `UE` for EU, `LA` for Latin America, `CD` for GlobalPack ROW. Different `Range` parameters apply. + +**Question:** Does V4's `BarcodeRequest` infer type from customer code + destination, or does the caller still specify? If the caller specifies, what are the V4-valid type values? + +### 9. `ReturnPeriod` defaults + +The SDK enum supports `IN_20_DAYS`, `IN_35_DAYS`, `IN_100_DAYS`, `IN_200_DAYS`, `IN_365_DAYS`. + +**Question:** What's the V4 default if not specified? Today the plugin sends no period and relies on whatever PostNL defaults to. Is that still 20 days under V4? + +### 10. Printer-type and label-format mapping + +V1 plugin sends strings like `'GraphicFile|PDF'`, `'Zebra|Generic ZPL II 600 dpi'`, `'JPG'`, `'GIF'`. + +**Question:** V4 SDK exposes `LabelSettings`. What are the supported values for printer type, output format (A4 vs A6), and resolution? Direct enum or string? + +--- + +## Clarifying (don't block, but resolve before staging) + +### 11. `SourceSystem` header value + +V1 plugin sends `SourceSystem: 35` in every request. The V4 SDK has `ClientBuilder::withSourceSystem(string)`. + +**Question:** Is `35` meaningful (e.g., a PostNL-assigned ID for this plugin)? Should we keep `35`, or assign a new value for V4 traffic so PostNL can distinguish? + +### 12. Rate limits + +V1 plugin retries up to 5 times on `WP_Error`. V4 SDK retries up to 3 times with exponential backoff on 429, 500, 502, 503, 504. + +**Question:** What are the actual rate limits PostNL enforces on V4 endpoints (especially `/timeframe/` and `/locations/` which are hit on every checkout page load)? + +### 13. Pickup-point count and delivery-day count + +Plugin currently hardcodes 3 pickup points and 10 delivery days per request. The V4 SDK's request DTOs accept these as parameters. + +**Question:** What are the actual upper bounds PostNL enforces for these parameters? + +### 14. Reply-number vs home-address return mapping + +V1 plugin uses `ProductCodeDelivery: 3285` for return-to-home, `2285` for reply-number (freepost). V4 mapping needed. + +**Question:** Are both delivery types still supported in V4? What's the V4 representation — `ShipmentType` variant, `DeliveryLocation` field, or a `Services` flag? + +### 15. Tracking URL format + +V1 plugin generates customer-facing tracking URLs from the barcode. V4 may use a different URL pattern or hostname. + +**Question:** What is the canonical V4 customer tracking URL format? Does it differ by destination country? + +--- + +## How this document is organized + +Questions are grouped by the migration phase they affect: + +- **Critical (#1–#3)** — prerequisites for the corresponding migration phases. Implementation of those phases begins once these are resolved. +- **Important (#4–#10)** — prerequisites for specific flows. +- **Clarifying (#11–#15)** — can be addressed alongside sandbox testing. + +Responses can be added inline under each question — prefix with `**Answered :**` to keep history. Question #1 has its own shared spreadsheet (see below) since it covers many combinations. + +--- + +## Product mapping confirmation + +The full mapping table is a shared spreadsheet: + +> **Spreadsheet:** https://docs.google.com/spreadsheets/d/19NpZkc8Ul-cgvYheotCU9qRC73I42KiLOr9hnfSNGtU/edit?gid=993839082#gid=993839082 + +Each row represents one combination of legacy product code + options as currently supported by the plugin. The proposed V4 `ShipmentType`, `Services` flags, and `DeliveryLocation` columns are pre-filled where they can be inferred directly from the V4 SDK source code. Each row also has a Confidence column (High / Medium / Low) indicating how much SDK-derived inference was involved. + +The Notes column is the primary place for PostNL responses on each row. The 28 High-confidence rows can be reviewed quickly as confirmations; the Medium and Low-confidence rows are where PostNL input is most valuable. + +### Possible per-row outcomes + +Where a legacy combination does not have a clean V4 equivalent, the resolution per row is one of: + +- **Direct V4 mapping** — the proposed `ShipmentType` + `Services` is correct, or PostNL provides a corrected mapping. +- **Configuration change** — the legacy option no longer exists in V4; PostNL recommends an alternative merchant-facing configuration. +- **Stays on V1** — the option remains on the V1 transport in the plugin. This is supported by the migration design: the `Service_Factory` continues routing this flow to the Legacy implementation regardless of V4 key status. diff --git a/docs/postnl-v4-migration/approach-2/product-mapping-for-postnl.csv b/docs/postnl-v4-migration/approach-2/product-mapping-for-postnl.csv new file mode 100644 index 00000000..e7f4c5d6 --- /dev/null +++ b/docs/postnl-v4-migration/approach-2/product-mapping-for-postnl.csv @@ -0,0 +1,96 @@ +ID,Sender,Receiver,Flow,Legacy Combination,Legacy Product Code,Legacy Characteristics,V4 ShipmentType (proposed),V4 Services (proposed),V4 DeliveryLocation,V4 InternationalShipmentData,Confidence,Notes / PostNL to confirm +1,NL,NL,delivery_day,(default),3085,,parcel,(none),,no,High,Default parcel; round-trip should return productData=3085 +2,NL,NL,delivery_day,delivery_code_at_door + insured_shipping,3085,char=004 opt=020,parcel,"deliveryConfirmation=DELIVERY_CODE, insuredValue=",,no,High,DELIVERY_CODE enum matches char 004/020 by name +3,NL,NL,delivery_day,only_home_address,3385,,parcel,statedAddressOnly=true,,no,High, +4,NL,NL,delivery_day,return_no_answer,3090,,parcel,returnWhenNotHome=true,,no,High,Confirm semantics: legacy 'return if no answer' vs V4 'return when not home' +5,NL,NL,delivery_day,signature_on_delivery,3189,,parcel,deliveryConfirmation=SIGNATURE,,no,High, +6,NL,NL,delivery_day,return_no_answer + only_home_address,3390,,parcel,"returnWhenNotHome=true, statedAddressOnly=true",,no,High, +7,NL,NL,delivery_day,signature_on_delivery + insured_shipping + return_no_answer,3094,,parcel,"deliveryConfirmation=SIGNATURE, insuredValue=, returnWhenNotHome=true",,no,High, +8,NL,NL,delivery_day,signature_on_delivery + only_home_address,3089,,parcel,"deliveryConfirmation=SIGNATURE, statedAddressOnly=true",,no,High, +9,NL,NL,delivery_day,insured_shipping + signature_on_delivery,3087,,parcel,"deliveryConfirmation=SIGNATURE, insuredValue=",,no,High, +10,NL,NL,delivery_day,signature_on_delivery + return_no_answer,3389,,parcel,"deliveryConfirmation=SIGNATURE, returnWhenNotHome=true",,no,High, +11,NL,NL,delivery_day,signature_on_delivery + only_home_address + return_no_answer,3096,,parcel,"deliveryConfirmation=SIGNATURE, statedAddressOnly=true, returnWhenNotHome=true",,no,High, +12,NL,NL,delivery_day,letterbox,2928,,letterbox,(none),,no,High, +13,NL,NL,delivery_day,id_check,3438,char=002 opt=014,parcel,minimalAgeCheck=AGE_18_PLUS,,no,Medium,Confirm: is char 002/014 = 18+ or 16+? SDK enum supports both +14,NL,NL,delivery_day,id_check + signature_on_delivery,3438,char=002 opt=014,parcel,"minimalAgeCheck=AGE_18_PLUS, deliveryConfirmation=SIGNATURE",,no,Medium,Same product code as #13 — id_check alone implies signature? +15,NL,NL,delivery_day,id_check + only_home_address,3438,char=002 opt=014,parcel,"minimalAgeCheck=AGE_18_PLUS, statedAddressOnly=true",,no,Medium, +16,NL,NL,delivery_day,id_check + only_home_address + signature_on_delivery,3438,char=002 opt=014,parcel,"minimalAgeCheck=AGE_18_PLUS, statedAddressOnly=true, deliveryConfirmation=SIGNATURE",,no,Medium, +17,NL,NL,delivery_day,id_check + insured_shipping,3443,char=002 opt=014,parcel,"minimalAgeCheck=AGE_18_PLUS, insuredValue=",,no,Medium, +18,NL,NL,delivery_day,id_check + insured_shipping + signature_on_delivery,3443,char=002 opt=014,parcel,"minimalAgeCheck=AGE_18_PLUS, insuredValue=, deliveryConfirmation=SIGNATURE",,no,Medium, +19,NL,NL,delivery_day,id_check + insured_shipping + only_home_address,3443,char=002 opt=014,parcel,"minimalAgeCheck=AGE_18_PLUS, insuredValue=, statedAddressOnly=true",,no,Medium, +20,NL,NL,delivery_day,id_check + insured_shipping + only_home_address + signature_on_delivery,3443,char=002 opt=014,parcel,"minimalAgeCheck=AGE_18_PLUS, insuredValue=, statedAddressOnly=true, deliveryConfirmation=SIGNATURE",,no,Medium, +21,NL,NL,pickup_points,(default),3533,,parcel,(none),pickupLocationId=,no,High, +22,NL,NL,pickup_points,insured_shipping,3534,,parcel,insuredValue=,pickupLocationId=,no,High, +23,NL,NL,pickup_points,id_check,3571,char=002 opt=014,parcel,minimalAgeCheck=AGE_18_PLUS,pickupLocationId=,no,Medium, +24,NL,NL,pickup_points,id_check + insured_shipping,3581,char=002 opt=014,parcel,"minimalAgeCheck=AGE_18_PLUS, insuredValue=",pickupLocationId=,no,Medium, +25,NL,BE,delivery_day,(default),4946,,parcel,(none),,no,High,Receiver country=BE; no InternationalShipmentData needed +26,NL,BE,delivery_day,only_home_address,4941,,parcel,statedAddressOnly=true,,no,High, +27,NL,BE,delivery_day,signature_on_delivery,4912,,parcel,deliveryConfirmation=SIGNATURE,,no,High, +28,NL,BE,delivery_day,insured_shipping,4914,,parcel,insuredValue=,,no,High, +29,NL,BE,delivery_day,insured_shipping + track_and_trace,4914,,parcel,"insuredValue=, registered=true?",,no,Low,Same code as #28 — is track_and_trace already implicit for insured BE? Or maps to Services.registered? +30,NL,BE,delivery_day,insured_shipping + signature_on_delivery,4914,,parcel,"insuredValue=, deliveryConfirmation=SIGNATURE",,no,Medium,Same product code as just insured — confirm signature gets recorded +31,NL,BE,delivery_day,insured_shipping + only_home_address,4914,,parcel,"insuredValue=, statedAddressOnly=true",,no,Medium, +32,NL,BE,delivery_day,insured_shipping + signature_on_delivery + only_home_address,4914,,parcel,"insuredValue=, deliveryConfirmation=SIGNATURE, statedAddressOnly=true",,no,Medium, +33,NL,BE,delivery_day,insured_shipping + track_and_trace + signature_on_delivery,4914,,parcel,"insuredValue=, registered=true?, deliveryConfirmation=SIGNATURE",,no,Low,Same as #29 with signature +34,NL,BE,delivery_day,insured_shipping + track_and_trace + only_home_address,4914,,parcel,"insuredValue=, registered=true?, statedAddressOnly=true",,no,Low, +35,NL,BE,delivery_day,insured_shipping + track_and_trace + signature_on_delivery + only_home_address,4914,,parcel,"insuredValue=, registered=true?, deliveryConfirmation=SIGNATURE, statedAddressOnly=true",,no,Low, +36,NL,BE,delivery_day,mailboxpacket,6440,,letterbox OR packet,(none),,no,Low,Is 'mailboxpacket' V4 = ShipmentType::LetterBox or ShipmentType::Packet? +37,NL,BE,delivery_day,mailboxpacket + track_and_trace,6972,,letterbox OR packet,registered=true?,,no,Low,Tracked variant of #36 +38,NL,BE,delivery_day,packets,6405,,packet,(none),,no,Medium, +39,NL,BE,delivery_day,packets + track_and_trace,6350,,packet,registered=true?,,no,Low,Confirm registered=true is the right V4 mapping for tracked packets +40,NL,BE,delivery_day,packets + track_and_trace + insured_shipping,6906,,packet,"registered=true?, insuredValue=",,no,Low, +41,NL,BE,pickup_points,(default),4936,,parcel,(none),pickupLocationId=,no,High, +42,NL,EU,delivery_day,(default),4907,"char=005 opt=025, char=101 opt=012",parcel,(none),,yes,Medium,What do chars 005/025 and 101/012 represent in V4? Possibly track_and_trace + customs flag +43,NL,EU,delivery_day,track_and_trace,4907,"char=005 opt=025, char=101 opt=012",parcel,registered=true?,,yes,Low,Same code+chars as default — is 005/025 already a 'tracked' indicator? +44,NL,EU,delivery_day,track_and_trace + insured_shipping,4907,"char=004 opt=015, char=101 opt=012",parcel,"registered=true?, insuredValue=",,yes,Low,Char 004/015: standard insured shipping? +45,NL,EU,delivery_day,track_and_trace + insured_plus,4907,"char=004 opt=016, char=101 opt=012",parcel,"registered=true?, insuredValue=",,yes,Low,Char 004/016: 'insured plus' = higher tier? What threshold? +46,NL,EU,delivery_day,mailboxpacket,6440,,letterbox OR packet,(none),,yes,Low,Same as #36; international variant +47,NL,EU,delivery_day,track_and_trace + mailboxpacket,6972,,letterbox OR packet,registered=true?,,yes,Low, +48,NL,EU,delivery_day,packets,6405,,packet,(none),,yes,Low, +49,NL,EU,delivery_day,track_and_trace + packets,6350,,packet,registered=true?,,yes,Low, +50,NL,EU,delivery_day,track_and_trace + packets + insured_shipping,6906,,packet,"registered=true?, insuredValue=",,yes,Low, +51,NL,EU,pickup_points,(default),4907,"char=005 opt=025, char=101 opt=012",parcel,(none),pickupLocationId=,yes,Low,Confirm pickup-point flow works for EU at all +52,NL,ROW,delivery_day,(default),4909,char=004 opt=015,parcel,(none),,yes,Low,Char 004/015 in ROW context — different meaning vs EU? +53,NL,ROW,delivery_day,track_and_trace,4909,char=005 opt=025,parcel,registered=true?,,yes,Low, +54,NL,ROW,delivery_day,track_and_trace + insured_plus,4909,char=004 opt=016,parcel,"registered=true?, insuredValue=",,yes,Low, +55,NL,ROW,delivery_day,mailboxpacket,6440,,letterbox OR packet,(none),,yes,Low, +56,NL,ROW,delivery_day,track_and_trace + mailboxpacket,6972,,letterbox OR packet,registered=true?,,yes,Low, +57,NL,ROW,delivery_day,packets,6405,,packet,(none),,yes,Low, +58,NL,ROW,delivery_day,track_and_trace + packets,6350,,packet,registered=true?,,yes,Low, +59,NL,ROW,delivery_day,track_and_trace + packets + insured_shipping,6906,,packet,"registered=true?, insuredValue=",,yes,Low, +60,NL,ROW,pickup_points,(default),4909,char=005 opt=025,parcel,(none),pickupLocationId=,yes,Low,Confirm pickup-point flow works for ROW at all +61,BE,BE,delivery_day,(default),4961,,parcel,(none),,no,High, +62,BE,BE,delivery_day,only_home_address,4960,,parcel,statedAddressOnly=true,,no,High, +63,BE,BE,delivery_day,signature_on_delivery,4963,,parcel,deliveryConfirmation=SIGNATURE,,no,High, +64,BE,BE,delivery_day,signature_on_delivery + only_home_address,4962,,parcel,"deliveryConfirmation=SIGNATURE, statedAddressOnly=true",,no,High, +65,BE,BE,delivery_day,insured_shipping + only_home_address,4965,,parcel,"insuredValue=, statedAddressOnly=true",,no,High, +66,BE,BE,pickup_points,(default),4880,,parcel,(none),pickupLocationId=,no,High, +67,BE,BE,pickup_points,insured_shipping,4878,,parcel,insuredValue=,pickupLocationId=,no,High, +68,BE,NL,delivery_day,(default),4890,,parcel,(none),,no,High, +69,BE,NL,delivery_day,signature_on_delivery,4891,,parcel,deliveryConfirmation=SIGNATURE,,no,High, +70,BE,NL,delivery_day,only_home_address,4893,,parcel,statedAddressOnly=true,,no,High, +71,BE,NL,delivery_day,signature_on_delivery + only_home_address,4894,,parcel,"deliveryConfirmation=SIGNATURE, statedAddressOnly=true",,no,High, +72,BE,NL,delivery_day,id_check + signature_on_delivery + only_home_address,4895,char=002 opt=014,parcel,"minimalAgeCheck=AGE_18_PLUS, deliveryConfirmation=SIGNATURE, statedAddressOnly=true",,no,Medium, +73,BE,NL,delivery_day,signature_on_delivery + only_home_address + return_no_answer,4896,,parcel,"deliveryConfirmation=SIGNATURE, statedAddressOnly=true, returnWhenNotHome=true",,no,High, +74,BE,NL,delivery_day,signature_on_delivery + only_home_address + insured_shipping,4897,,parcel,"deliveryConfirmation=SIGNATURE, statedAddressOnly=true, insuredValue=",,no,High, +75,BE,NL,pickup_points,signature_on_delivery,4898,,parcel,deliveryConfirmation=SIGNATURE,pickupLocationId=,no,High,Same code as #76 — signature flag still recorded? +76,BE,NL,pickup_points,(default),4898,,parcel,(none),pickupLocationId=,no,High, +77,BE,EU,delivery_day,(default),4907,"char=005 opt=025, char=101 opt=012",parcel,(none),,yes,Low,Same as #42 (sender=NL); confirm BE sender works the same +78,BE,EU,delivery_day,track_and_trace,4907,"char=005 opt=025, char=101 opt=012",parcel,registered=true?,,yes,Low, +79,BE,EU,delivery_day,track_and_trace + insured_shipping,4907,"char=004 opt=015, char=101 opt=012",parcel,"registered=true?, insuredValue=",,yes,Low, +80,BE,EU,delivery_day,track_and_trace + insured_plus,4907,"char=004 opt=016, char=101 opt=012",parcel,"registered=true?, insuredValue=",,yes,Low, +81,BE,EU,delivery_day,mailboxpacket,6440,,letterbox OR packet,(none),,yes,Low, +82,BE,EU,delivery_day,track_and_trace + mailboxpacket,6972,,letterbox OR packet,registered=true?,,yes,Low, +83,BE,EU,delivery_day,packets,6405,,packet,(none),,yes,Low, +84,BE,EU,delivery_day,track_and_trace + packets,6350,,packet,registered=true?,,yes,Low, +85,BE,EU,delivery_day,track_and_trace + packets + insured_shipping,6906,,packet,"registered=true?, insuredValue=",,yes,Low, +86,BE,ROW,delivery_day,(default),4909,char=004 opt=015,parcel,(none),,yes,Low, +87,BE,ROW,delivery_day,track_and_trace,4909,char=005 opt=025,parcel,registered=true?,,yes,Low, +88,BE,ROW,delivery_day,track_and_trace + insured_plus,4909,char=004 opt=016,parcel,"registered=true?, insuredValue=",,yes,Low, +89,any,NL,delivery_day,(slot) Evening (delivery_day_type),(varies),char=118 opt=006,,deliveryWindow.service=Evening,,no,High,DeliveryWindowService::Evening matches by name; applies on top of base product +90,any,NL,delivery_day,(slot) 08:00-12:00 (delivery_day_type),(varies),char=118 opt=008,,deliveryWindow.duration=WITHIN_24_HOURS,,no,High,Likely the morning window. Confirm: is char 118/008 = 24hr window or guaranteedBefore=10:00 or 12:00? +91,any,NL,delivery_day,(slot) Daytime (default),(varies),(none),,(no deliveryWindow),,no,High,Daytime is default — no DeliveryWindow needed +92,any,any,return,Smart Return — in_box,(varies),char=152 opt=028,,(set in ReturnOptions),,N/A,Medium,Configure via ReturnOptions DTO — what's the V4 field name? +93,any,any,return,Smart Return — shipping_return,3285 OR 2285,char=152 opt=026,,(set in ReturnOptions),,N/A,Medium,Configure via ReturnOptions DTO. 3285=return-to-home; 2285=reply-number/freepost. Confirm V4 mechanism for both +94,any,any,return,Smart Return — return_all_labels_not_active,(varies),"char=152 opt=026, char=191 opt=004",,(set in ReturnOptions — don't auto-activate),,N/A,Low,Don't activate return until customer triggers. V4 equivalent? +95,any,any,return,Smart Return — default (V2.2 currently),3285 OR 2285,char=152 opt=025,,(set in ReturnOptions),,N/A,Low,Plugin currently calls V2.2 endpoint. What V4 mechanism replaces this — generateReturn() or something else? diff --git a/docs/postnl-v4-migration/postnl-v4-migration-implementation-plan.md b/docs/postnl-v4-migration/postnl-v4-migration-implementation-plan.md new file mode 100644 index 00000000..a6d3aac3 --- /dev/null +++ b/docs/postnl-v4-migration/postnl-v4-migration-implementation-plan.md @@ -0,0 +1,1022 @@ +# PostNL V4 API Migration — WooCommerce Plugin Implementation Plan + +**Version:** 1.0 (preliminary / internal) +**Date:** 2026-05-12 +**API reference:** `postnl-v4-sdk-api-reference.md` +**SDK source:** `postnl-sdk-audit/vendor/postnl/api-client-sdk` +**Plugin source:** `postnl-for-woocommerce-org/` + +--- + +## Context + +The PostNL SDK (`postnl/api-client-sdk`) covers V4 flows for ShipmentDelivery, ReturnShipment, Barcode, Locations, and TimeFrame. The migration uses a **hybrid approach**: SDK for confirmed V4 flows, existing old REST clients retained as fallback until staging parity is validated per flow. Smart Returns and activatereturn remain on old clients until PostNL confirms V4 equivalence. + +All time estimates in this plan are **preliminary and internal only**. +- **Track A** = with SDK (recommended) +- **Track B** = without SDK (custom HTTP client, auth, retry, error parsing written per area) + +--- + +## PHP Version Note + +The SDK requires PHP ≥ 8.2. The plugin currently declares PHP ≥ 7.4. This is a **release/build decision**, not a blocker for starting development work. + +Options to resolve before any SDK code ships to production: +- Bump plugin PHP minimum to 8.2 in plugin header and `readme.txt`. +- Implement conditional SDK loading that skips SDK calls on PHP < 8.2 with an admin notice. + +Agree on this decision before Task 1 merges to production. + +--- + +## SDK Composer Dependency Note + +The SDK is installed in `postnl-sdk-audit/` for audit purposes only. Before any SDK code ships: +- Add `postnl/api-client-sdk` to `postnl-for-woocommerce-org/composer.json` with a **pinned version** (not `dev-main`). +- Validate that the resulting `vendor/` tree builds correctly in the plugin's WordPress context. +- Verify `vendor/autoload.php` loads without conflicts with existing Composer dependencies (`clegginabox/pdf-merger` etc.). +- This validation is part of Task 1. + +--- + +## SDK Docs Warning + +The SDK docs under `docs/postnl-v4-migration/sources/sdk-docs/` are the primary reference for method names, namespaces, and request/response shapes. Several discrepancies exist between these docs and prior code inspection of the installed SDK — see `postnl-v4-sdk-api-reference.md §11` for the full list. Where SDK docs and code conflict, the discrepancy is noted in §11; verify against the installed SDK before use. + +--- + +## Proposed ClickUp Task Structure + +``` +Epic: PostNL V4 API Migration +│ +├── [Overview] Routing table + required inputs reference only, not a PR +├── Task 1 SDK Client Factory + Router Ready — no deps +├── Task 2 Barcode (SDK POC) Ready — dep: Task 1 +├── Task 3 TimeFrame / Delivery Dates (SDK POC) Ready — dep: Task 1 +├── Task 4 Pickup Locations (SDK POC) Ready — dep: Task 1 +├── Task 5 Checkout Aggregation Ready — dep: Task 3 + 4 staging validated +├── Task 6 Shipping + Letterbox Labels BLOCKED: product mapping input +├── Task 7 Return Labels (SDK POC) Ready — dep: Task 1 +├── Task 8 Smart Returns BLOCKED: PostNL confirmation +└── Task 9 activatereturn BLOCKED: PostNL decision +``` + +--- + +## Routing Table + +| Flow | File | Decision | Status | +|---|---|---|---| +| Barcode | `src/Rest_API/Barcode/Client.php` | SDK (POC first) | Ready | +| Checkout delivery days | `src/Rest_API/Checkout/Client.php` | SDK (POC first) | Ready | +| Checkout pickup points | `src/Rest_API/Checkout/Client.php` | SDK (POC first) | Ready | +| Shipping labels | `src/Rest_API/Shipping/Client.php` | SDK | Blocked: product mapping | +| Letterbox labels | `src/Rest_API/Letterbox/Client.php` | SDK | Blocked: product mapping | +| Return labels | `src/Rest_API/Return_Label/Client.php` | SDK (POC first) | Ready | +| Smart Returns | `src/Rest_API/Smart_Returns/Client.php` | Old client | Blocked: PostNL confirmation | +| activatereturn | `src/Rest_API/Shipment_and_Return/Client.php` | Old client | Blocked: PostNL decision | +| Fill In With PostNL | `src/Frontend/Fill_In_With_Postnl.php` | Old client (permanent) | — | +| Postal code check | `src/Rest_API/Postcode_Check/Client.php` | Old client (permanent) | — | + +--- + +## Required Inputs + +| Input | Blocks | Status | +|---|---|---| +| Product code / ProductOptions → V4 `shipmentType` + `services` mapping table | Task 6 | Needed | +| Smart Returns V4 equivalence confirmed (`return/generate` vs old `v2_2/label`) | Task 8 | Needed | +| activatereturn decision: SDK extension / old client / drop | Task 9 | Needed | +| `services.adrLq` vs `services.adrlq` API casing confirmed | Task 6 | Needed | +| Guaranteed-before `12:00` bug status (AITS-382) | Task 6 | Needed | +| PHP ≥ 8.2 hosting / release decision | All tasks | Release decision | + +--- + +--- + +## Task 1 — SDK Client Factory + Router + +**Status:** Ready | **Depends on:** Nothing +**Estimate:** Track A 6–8 h | Track B 16–20 h + +### Goal +Create two foundational classes in `src/SDK/`: +- `ClientFactory.php` — builds the PostNL SDK client from plugin settings. +- `Router.php` — decides per-flow whether to use the SDK path or the old client; SDK paths are disabled by default and enabled only after staging validation. + +All subsequent SDK tasks call the factory through the router. No existing flow is changed. + +### Current behavior +- Each `Rest_API/*/Client.php` builds API calls directly using `Rest_API\Base` → `wp_remote_request()`. +- API key and sandbox flag come from `Settings::get_instance()`. +- Error handling via `\Exception` thrown in `Base::check_response_error()`. + +### Target behavior +- New `PostNLWooCommerce\SDK\ClientFactory` class at `src/SDK/ClientFactory.php`. +- Provides `get_client(): PostnlClientInterface`. +- Reads API key and sandbox flag from `Settings::get_instance()`. +- **Does not catch runtime SDK/API exceptions** — exception handling belongs at each SDK service wrapper (per-flow client classes, Tasks 2–9), not at the factory. +- New `PostNLWooCommerce\SDK\Router` class at `src/SDK/Router.php`. +- Provides per-flow `use_sdk_for(string $flow): bool` method. +- All SDK paths disabled by default; enabled per flow only after staging validation passes. +- All other plugin behavior unchanged — no runtime behavior change expected. + +### Scope +- `src/SDK/ClientFactory.php` — new file, new directory. +- `src/SDK/Router.php` — new file; per-flow SDK/old-client switch. +- `src/Logger.php` — add sanitization to strip label binary content from SDK log entries. +- `postnl-for-woocommerce-org/composer.json` — add SDK dependency, pin version. +- Composer build validation in WordPress plugin context. + +### Out of scope +- No changes to any `Rest_API/*/Client.php`. +- No SDK calls made from this task — factory only. +- No changes to frontend, admin, checkout, or label flows. + +### Files/classes likely touched +- `src/SDK/ClientFactory.php` — new +- `src/SDK/Router.php` — new +- `src/Logger.php` — minor addition +- `postnl-for-woocommerce-org/composer.json` + +### Implementation details + +**ClientFactory:** +- Namespace: `PostNLWooCommerce\SDK` +- `src/SDK/` is a new top-level sub-directory under `src/`, consistent with `src/Helper/`, `src/Library/`. +- Use `Postnl::client(Auth::apiKey($apiKey))` for production. +- Use `Postnl::sandboxClient(Auth::apiKey($apiKey))` for sandbox. +- Read settings via `Settings::get_instance()->get_api_key()` and `->is_sandbox()`. +- Factory only builds the client — it does not make API calls and does not catch runtime API exceptions. +- Include `ABSPATH` guard at top of file (WordPress convention — see `agents.md`). + +**Router:** +- Class: `PostNLWooCommerce\SDK\Router` +- Method: `use_sdk_for(string $flow): bool` — returns false by default for every flow. +- Flow keys match the routing table (e.g., `barcode`, `timeframe`, `locations`, `return_labels`, `shipping_labels`). +- Enabling a flow in the Router is how a developer activates the SDK path after staging validation; reverting it re-enables the old client immediately. +- Each per-flow client (Tasks 2–9) checks `Router::use_sdk_for($flow)` before deciding which path to take. +- Runtime SDK/API exceptions are caught inside each per-flow client's SDK branch and converted to `\Exception` matching `Base::check_response_error()` behavior. +- Include `ABSPATH` guard at top of file. + +### SDK methods/classes involved +- `Postnl\Sdk\Client\Postnl::client()` +- `Postnl\Sdk\Client\Postnl::sandboxClient()` +- `Postnl\Sdk\Auth\Auth::apiKey()` +- `Postnl\Sdk\Client\PostnlClientInterface` +- `Postnl\Sdk\Exception\PostnlExceptionInterface` — caught at each per-flow SDK service wrapper (Tasks 2–9), not at the factory + +### Data mapping notes +- None. This task does not map any API fields. + +### Fallback/old-client behavior +- All existing `Rest_API/*/Client.php` clients remain fully operational and unchanged. +- Factory is purely additive; it does not replace anything in this task. + +### Unit tests +- `ClientFactory::get_client()` returns a `PostnlClientInterface` instance when API key is set. +- `ClientFactory::get_client()` returns a sandbox client when `is_sandbox()` returns true. +- `Router::use_sdk_for('barcode')` returns false by default (SDK paths off unless enabled). +- `Router::use_sdk_for('barcode')` returns true after the barcode flow is enabled. +- Logger does not output API key. +- Logger does not output label binary data. + +### Integration/staging tests +- Instantiate `ClientFactory` in staging environment; verify client builds without PHP error. +- Toggle sandbox mode; verify `is_sandbox()` routes to sandbox base URL (check debug log). + +### Manual QA checks +- Toggle sandbox mode in plugin settings; verify no PHP error. +- Verify WooCommerce logs after factory call contain no API key. + +### Acceptance criteria +- `ClientFactory::get_client()` returns a working SDK client against PostNL sandbox. +- `Router::use_sdk_for()` returns false for all flows by default; SDK paths are off until explicitly enabled. +- No existing plugin functionality changes — old clients are not used only when an SDK path is enabled and validated; they remain available as fallback at all times. +- No API key or label binary in log output. +- Composer build succeeds; no autoload conflicts with existing dependencies. +- No runtime behavior change expected in any existing flow. + +### Dependencies/blockers +- PHP ≥ 8.2 required in development environment. +- Composer build must be validated in WordPress plugin context before merging. + +### Notes for reviewer +- Confirm `vendor/autoload.php` loads correctly in plugin bootstrap (`postnl-for-woocommerce.php`). +- Confirm no namespace collision between SDK `Postnl\Sdk\` and plugin `PostNLWooCommerce\`. +- SDK version must be pinned; reject `dev-main` or `*` in `composer.json`. +- Verify `src/SDK/` directory is autoloaded by plugin PSR-4 config. +- **Standing rule for all SDK tasks:** Any task enabling an SDK path must either prove fallback behavior in the PR description, or explicitly explain why fallback is intentionally removed. Task 5 (Checkout Aggregation) is the only task currently approved to remove the old checkout monolith, and only after Tasks 3 + 4 are individually staging-validated. + +--- + +## Task 2 — Barcode (SDK POC) + +**Status:** Ready | **Depends on:** Task 1 +**Estimate:** Track A 4–6 h | Track B 10–14 h + +### Goal +Migrate barcode generation to the SDK as a proof-of-concept. Validate the end-to-end SDK integration pattern before applying it to higher-risk flows. Old HTTP client remains available as fallback until staging parity is confirmed. + +### Current behavior +- `Rest_API\Barcode\Client` calls `GET /shipment/v1_1/barcode` via `wp_remote_request()`. +- Request fields: `CustomerCode`, `CustomerNumber`, `Type`, `Serie` (range string). +- Returns barcode string(s) used in downstream label generation. +- Called from `Order\Base` before label generation. + +### Target behavior +- `Rest_API\Barcode\Client` calls `ClientFactory::get_client()->barcode()->generateBarcode(BarcodeRequest)`. +- Old HTTP call preserved in class as fallback, not removed. +- Response barcode string returned to callers with no shape change. +- Old client restored via single-line change if SDK parity fails on staging. + +### Scope +- `src/Rest_API/Barcode/Client.php` — replace active HTTP call with SDK call; keep old call as fallback. + +### Out of scope +- Label generation, checkout, returns — not touched. +- `src/Rest_API/Barcode/Item_Info.php` — not changed. +- `src/Order/Base.php` caller — not changed. + +### Files/classes likely touched +- `src/Rest_API/Barcode/Client.php` + +### Implementation details +- Check `Router::use_sdk_for('barcode')` — if false, fall through to old `send_request()` call (default until staging validated). +- When SDK path is active: inject `ClientFactory`, build `BarcodeRequest` from mapped fields, call `$factory->get_client()->barcode()->generateBarcode($request)`. +- Catch `PostnlExceptionInterface` inside the SDK branch; re-throw as `\Exception` matching current `Base::check_response_error()` behavior. +- Old `send_request()` call remains in place as the else branch — not removed, not commented out. + +### SDK methods/classes involved +- `Client::barcode()->generateBarcode(BarcodeRequest)` → `GenerateBarcodeResponse` +- `Postnl\Sdk\RequestData\V4\Barcode\BarcodeRequest` +- `Postnl\Sdk\ResponseData\V4\Barcode\GenerateBarcodeResponse` + +### Data mapping notes + +| Old field | SDK field | Note | +|---|---|---| +| `CustomerCode` | `customerCode` | Direct map | +| `CustomerNumber` | `customerNumber` | Direct map | +| `Type` | Barcode type prefix | Confirm V4 equivalent with PostNL | +| `Serie` (range start) | `serieStart` | Split from old range string | +| `Serie` (range end) | `serieEnd` | Split from old range string | +| Implied count | `numberOfBarcodes` | Derive from request context | + +### Fallback/old-client behavior +- Old `GET /shipment/v1_1/barcode` call remains in the class as the else branch of the Router check. +- Old client is not used when the SDK path is enabled and validated; it remains available as fallback at all times. +- Disable the SDK path in `Router` to restore old behavior immediately — no rollback PR needed. +- Old client remains available as fallback until staging parity is confirmed for all barcode types. + +### Unit tests +- Mock `ClientFactory`; verify `BarcodeRequest` fields match expected mapped values from `Item_Info`. +- Verify response barcode string returned unchanged to callers. +- `HttpSdkException` caught; surfaces as `\Exception` matching existing error behavior. +- `BarcodeRequest` with missing required fields triggers caught `ValidationException`. + +### Integration/staging tests +- Generate a barcode for a test NL domestic order on sandbox. +- Verify barcode format matches expected PostNL pattern (e.g., `3S...` for NL domestic). +- Compare SDK-generated barcode format to old-client-generated barcode for same inputs. + +### Manual QA checks +- Generate a barcode for a test order in staging admin. +- Verify barcode is stored correctly in `_postnl_order_metadata`. +- Verify no barcode value or API key appears in WC logs. + +### Acceptance criteria +- Barcode generates successfully via SDK on sandbox. +- Barcode format matches PostNL expected pattern. +- Old client remains available as fallback until staging parity confirmed. +- No change to barcode storage or downstream label use. +- Old client is not used when the SDK path is enabled and validated; it remains available as fallback at all times. + +### Dependencies/blockers +- Task 1 merged and Composer build validated. +- Confirm V4 barcode `Type` field equivalent with PostNL (can proceed with known mapping first). + +### Notes for reviewer +- This is a POC task. If SDK barcode output format differs unexpectedly from old API output, stop and consult PostNL before proceeding with other tasks. +- Verify `numberOfBarcodes` behavior for multicollo orders (multiple barcodes per label request). + +--- + +## Task 3 — TimeFrame / Delivery Dates (SDK POC) + +**Status:** Ready | **Depends on:** Task 1 +**Estimate:** Track A 6–8 h | Track B 14–18 h + +### Goal +Migrate checkout delivery-day options to the SDK TimeFrame V4 services as a POC. Validate the response shape against what `Frontend\Delivery_Day` expects. Old checkout client preserved as fallback. Prerequisite for Task 5. + +### Current behavior +- `Rest_API\Checkout\Client` calls `POST /shipment/v1/checkout`. +- Single response contains both delivery-day options and pickup-point options. +- Delivery-day portion consumed by `Frontend\Delivery_Day` (classic) and blocks delivery-day component. + +### Target behavior +- Delivery-day portion: call `ClientFactory::get_client()->singleTimeframe()->getTimeframe()` or `->multipleTimeframes()->getTimeframes()` based on configured services. +- Response mapped to the same shape expected by `Frontend\Delivery_Day`; adapter added if shapes differ. +- Old checkout call preserved as fallback; pickup-point portion untouched in this PR. + +### Scope +- Delivery-date portion of `src/Rest_API/Checkout/Client.php`. +- `src/Frontend/Delivery_Day.php` — read-only check; response adapter added if needed. + +### Out of scope +- Pickup locations (Task 4). +- Checkout aggregation (Task 5). +- Classic vs. blocks checkout rendering — no template changes. +- `src/Frontend/Container.php` selection logic and fee calculation — not changed. +- `src/Checkout_Blocks/` — not changed. + +### Files/classes likely touched +- `src/Rest_API/Checkout/Client.php` +- `src/Frontend/Delivery_Day.php` (response adapter only if shapes differ) + +### Implementation details + +- Check `Router::use_sdk_for('timeframe')` — if false, fall through to old `POST /shipment/v1/checkout` call (default until staging validated). +- When SDK path is active: build request from mapped fields, call SDK TimeFrame service, map response, catch `PostnlExceptionInterface` inside the SDK branch. +- Old checkout call remains in the class as the else branch — not removed. + +**SDK method names — per SDK docs:** +- `$client->checkout()->getSingleServiceTimeframe(SingleServiceTimeframeRequest)` → `TimeFrameSingleServiceResponse` +- `$client->checkout()->getMultipleServicesTimeframe(MultipleServicesTimeframeRequest)` → `TimeframesMultipleServicesResponse` +- SDK docs namespace: `Postnl\Sdk\Service\Checkout\V4\Request\SingleServiceTimeframeRequest` +- SDK docs namespace: `Postnl\Sdk\Service\Checkout\V4\Request\MultipleServicesTimeframeRequest` +- **Verify:** SDK docs consistently use `$postnl->checkout()` but prior code inspection found `singleTimeframe()`/`multipleTimeframes()` on `Client`. Verify `src/Client/Client.php` in installed SDK before use. SDK docs also show `$postnl->checkout()->multipleTimeframes()` in one complete example — inconsistent with `getMultipleServicesTimeframe()` used elsewhere in SDK docs. + +Choose single vs. multiple services based on plugin settings for daytime/evening options. + +### SDK methods/classes involved +- `Client::checkout()->getSingleServiceTimeframe(SingleServiceTimeframeRequest)` (per SDK docs; verify against installed SDK) +- `Client::checkout()->getMultipleServicesTimeframe(MultipleServicesTimeframeRequest)` (per SDK docs; verify against installed SDK) +- `Postnl\Sdk\ResponseData\V4\TimeFrame\TimeFrameSingleServiceResponse` +- `Postnl\Sdk\ResponseData\V4\TimeFrame\TimeframesMultipleServicesResponse` + +### Data mapping notes + +| Plugin / order data | SDK field | Source | +|---|---|---| +| Customer address | `receiverAddress` | Session / `Item_Info` | +| Cut-off / handover date | `handoverDate` | Plugin settings | +| Days to show (delivery) | `deliveryDays` / `numberOfDays` | Plugin settings | +| Service (daytime / evening) | `service` / `services[]` | Plugin settings | +| Shipment type | `shipmentType` | `parcel` as default | +| Customer code | `customerCode` | Settings | +| Customer number | `customerNumber` | Settings | + +### Fallback/old-client behavior +- Old `POST /shipment/v1/checkout` call remains in the class as the else branch of the Router check. +- Pickup-point portion of old checkout call remains active and unmodified in this PR. +- Old client is not used when the SDK path is enabled and validated; it remains available as fallback at all times. +- Disable `Router::use_sdk_for('timeframe')` to restore old behavior immediately — no rollback PR needed. +- Old client remains available as fallback until staging parity is confirmed. + +### Unit tests +- Mock `ClientFactory`; verify `SingleServiceTimeframeRequest` fields populated from plugin/order data. +- Verify `MultipleServicesTimeframeRequest` sends correct `services[]` from plugin settings. +- Verify `TimeFrameSingleServiceResponse` maps to existing delivery-day format (compare shapes explicitly). +- SDK error does not crash checkout; gracefully returns empty slots. + +### Integration/staging tests +- Trigger delivery-day load for a NL address on staging. +- Verify daytime and evening slots appear where configured. +- Compare slot list to old API output for same address and settings. + +### Manual QA checks +- Classic checkout: enter NL address, verify delivery-day slots load. +- Blocks checkout: enter NL address, verify delivery-day slots load. +- Select a slot, place order; verify `_postnl_order_metadata` saved correctly. + +### Acceptance criteria +- Delivery-day options display via SDK on staging. +- Slot content matches old API output for same inputs (parity check). +- Classic and blocks checkout both work. +- Old client remains available as fallback until staging parity is confirmed. +- Old client is not used when the SDK path is enabled and validated. + +### Dependencies/blockers +- Task 1 merged. +- Always test both classic and blocks checkout modes (see `agents.md`). + +### Notes for reviewer +- `Frontend\Delivery_Day` expects a specific response shape. Any V4 shape difference must be handled by a response adapter — do not change `Delivery_Day` itself. +- Re-confirm `Container.php` tax/fee back-calculation is unaffected (see `agents.md` tax display architecture note — `taxRatio` logic must not change). + +--- + +## Task 4 — Pickup Locations (SDK POC) + +**Status:** Ready | **Depends on:** Task 1 +**Estimate:** Track A 5–7 h | Track B 12–16 h + +### Goal +Migrate checkout pickup-point options to the SDK Locations V4 services as a POC. Validate response shape against `Frontend\Dropoff_Points`. Old checkout client preserved as fallback. Prerequisite for Task 5. + +### Current behavior +- `Rest_API\Checkout\Client` calls `POST /shipment/v1/checkout`. +- Pickup-point portion of response consumed by `Frontend\Dropoff_Points` (classic) and blocks dropoff-points component. + +### Target behavior +- Pickup-point portion: check `Router::use_sdk_for('locations')` — if true, call SDK Locations service; otherwise fall through to old `POST /shipment/v1/checkout` call. +- Response mapped to the shape expected by `Frontend\Dropoff_Points`; adapter added if shapes differ. +- Old checkout call remains in the class as the else branch; delivery-day portion handled by Task 3. + +### Scope +- Pickup-point portion of `src/Rest_API/Checkout/Client.php`. +- `src/Frontend/Dropoff_Points.php` — read-only check; adapter if shapes differ. + +### Out of scope +- Delivery-day options (Task 3). +- Checkout aggregation (Task 5). +- `Container.php` selection logic — not changed. +- `client/checkout/postnl-dropoff-points/` JS — not changed. + +### Files/classes likely touched +- `src/Rest_API/Checkout/Client.php` +- `src/Frontend/Dropoff_Points.php` (response adapter only if shapes differ) + +### Implementation details +- Check `Router::use_sdk_for('locations')` — if false, fall through to old `POST /shipment/v1/checkout` call (default until staging validated). +- When SDK path is active: build request from mapped fields, call SDK Locations service, map response, catch `PostnlExceptionInterface` inside the SDK branch. +- Old checkout call remains in the class as the else branch — not removed. +- By address (primary): `$client->locations()->getPickupLocationsByAddress(PickUpNearAddressRequest)` → `PickUpLocationsResponse` +- By coordinates (secondary, if lat/long available): `$client->locations()->getNearPickupLocationsByCoordinates(PickUpNearCoordinatesRequest)` → `PickUpLocationsResponse` +- **Verify:** SDK docs show `$postnl->locations()` but prior code inspection found `addressLocations()`/`coordinateLocations()` on `Client`. Verify `src/Client/Client.php` in installed SDK before use. + +### SDK methods/classes involved +- `Client::locations()->getPickupLocationsByAddress(PickUpNearAddressRequest)` (per SDK docs; verify against installed SDK) +- `Client::locations()->getNearPickupLocationsByCoordinates(PickUpNearCoordinatesRequest)` (per SDK docs; verify against installed SDK) +- `Postnl\Sdk\RequestData\V4\Locations\PickUpNearAddressRequest` +- `Postnl\Sdk\RequestData\V4\Locations\PickUpNearCoordinatesRequest` +- `Postnl\Sdk\ResponseData\V4\Locations\PickUpLocationsResponse` + +### Data mapping notes + +| Plugin / order data | SDK field | Note | +|---|---|---| +| Customer address | `receiverAddress` | Map from session / `Item_Info` | +| Number of locations | `numberOfLocations` | From settings; min 1, max 10. **Currently hardcoded in settings** — map as-is, do not change behavior | +| Location type | `locationType` | `Retail` or `ParcelLocker` from settings | +| Pickup date | `pickUpDate` | Delivery date at pickup location | +| Customer country | `receiverCountryIso` | For coordinates call | +| Customer code / number | `customerCode`, `customerNumber` | Settings | + +### Fallback/old-client behavior +- Old `POST /shipment/v1/checkout` call remains in the class as the else branch of the Router check. +- Delivery-day portion of old call handled separately in Task 3. +- Old client is not used when the SDK path is enabled and validated; it remains available as fallback at all times. +- Disable `Router::use_sdk_for('locations')` to restore old behavior immediately — no rollback PR needed. +- Old client remains available as fallback until staging parity confirmed. + +### Unit tests +- Mock `ClientFactory`; verify `PickUpNearAddressRequest` built from customer address fields. +- Verify `PickUpLocationsResponse` maps to existing pickup-point frontend format. +- Coordinates-based call falls back to address-based when lat/long unavailable. +- SDK error does not crash checkout; returns empty pickup list gracefully. + +### Integration/staging tests +- Trigger pickup-point load for a NL address on staging. +- Verify pickup points appear with correct location data. +- Compare pickup-point list to old API output for same address. + +### Manual QA checks +- Classic checkout: enter NL address, verify pickup-point tab/list loads. +- Blocks checkout: verify pickup-point list loads. +- Select a pickup point, place order; verify saved to `_postnl_order_metadata`. + +### Acceptance criteria +- Pickup points display via SDK on staging. +- Location list matches old API output for same inputs (parity check). +- Classic and blocks checkout both work. +- Old client remains available as fallback until staging parity confirmed. +- Old client is not used when the SDK path is enabled and validated. + +### Dependencies/blockers +- Task 1 merged. +- Always test both classic and blocks checkout modes. + +### Notes for reviewer +- `numberOfLocations` is currently hardcoded in `Settings` (see `agents.md` known tech debt note). Do not change this; just map it as-is. +- `Frontend\Dropoff_Points` may expect specific field names in the response. Adapter must preserve those exactly. + +--- + +## Task 5 — Checkout Aggregation + +**Status:** Ready after Tasks 3 + 4 staging parity confirmed | **Depends on:** Task 3 + Task 4 +**Estimate:** Track A 3–5 h | Track B 3–5 h + +### Goal +Remove the last reference to the old `POST /shipment/v1/checkout` call and replace it with an aggregation of the SDK TimeFrame (Task 3) and Locations (Task 4) calls. Merge only after both Tasks 3 and 4 pass staging parity checks independently. + +### Current behavior +- `Rest_API\Checkout\Client` makes one `POST /shipment/v1/checkout` call that returns delivery days + pickup points in a single combined response. + +### Target behavior +- `Checkout\Client` aggregates: one TimeFrame call + one Locations call → merged response. +- Response shape seen by `Frontend\Container`, `Frontend\Delivery_Day`, `Frontend\Dropoff_Points` is unchanged. +- Old checkout call is removed (Tasks 3 + 4 are the individually validated replacements). + +### Scope +- `src/Rest_API/Checkout/Client.php` — remove old call, add aggregation method. +- `src/Frontend/Container.php` — read-only verification only; no changes expected. + +### Out of scope +- `src/Rest_API/Postcode_Check/Client.php` — permanent old client; not touched here. +- Delivery-day or pickup-point response adapters — already handled in Tasks 3 + 4. +- Template changes, JS changes. + +### Files/classes likely touched +- `src/Rest_API/Checkout/Client.php` + +### Implementation details +- Call TimeFrame and Locations sequentially (or with isolated error handling). +- If TimeFrame fails: return empty delivery days; still return pickup points. +- If Locations fails: return empty pickup points; still return delivery days. +- **Verify:** SDK docs show `$postnl->checkout()` for TimeFrame calls, but prior code inspection did not find this method on `Client`. Verify `src/Client/Client.php` in the installed SDK before using `checkout()`. +- **There is no standalone `DeliveryDate` V4 SDK service.** Do not look for one. +- Postal-code check (`Postcode_Check/Client.php`) stays on old client; do not call from here. + +### SDK methods/classes involved +- Same as Task 3 (TimeFrame) and Task 4 (Locations) — no new SDK methods. + +### Data mapping notes +- No new mapping. All mapping handled in Tasks 3 + 4. + +### Fallback/old-client behavior +- Old checkout call removed in this PR (individual replacements already validated). +- If a full rollback is needed, revert this PR entirely to restore old call. + +### Unit tests +- Aggregated result combines TimeFrame + Locations into expected combined shape. +- TimeFrame failure is isolated: Locations still returns, no fatal error. +- Locations failure is isolated: TimeFrame still returns, no fatal error. +- Both fail: empty response returned gracefully, no PHP fatal. + +### Integration/staging tests +- Full checkout end-to-end: address entry → delivery-day options → pickup-point options → select → place order → verify `_postnl_order_metadata`. +- Toggle delivery-day setting off: only pickup points appear. +- Toggle pickup-point setting off: only delivery days appear. + +### Manual QA checks +- Classic checkout: complete a full checkout from address entry to order placed. +- Blocks checkout: same. +- Verify delivery date and pickup point saved correctly to order. +- Verify postal-code validation still works (old client — confirm no regression). +- Verify Fill In With PostNL still works (old client — confirm no regression). + +### Acceptance criteria +- No call to `POST /shipment/v1/checkout` anywhere in the codebase after this PR. +- Full checkout flow works end-to-end on staging (classic + blocks). +- Postal-code check behavior unchanged. +- No regression in fee calculation or delivery-option display. +- Delivery-day fees and tax display match pre-migration behavior (verify `taxRatio` logic). + +### Dependencies/blockers +- Task 3 staging parity confirmed. +- Task 4 staging parity confirmed. +- Both classic and blocks checkout tested. + +### Notes for reviewer +- This is a small, clean PR. The aggregation logic is the only real change. If Tasks 3 + 4 are stable, this should be low risk. +- Re-confirm `Container.php` tax/fee back-calculation is unaffected (see `agents.md` tax display architecture — `taxRatio` invariant must hold). + +--- + +## Task 6 — Shipping + Letterbox Labels ⛔ BLOCKED + +**Status:** Blocked — requires product/options → V4 field mapping table from PostNL/Joris +**Depends on:** Task 1 + mapping input confirmed in writing +**Estimate:** Track A 16–24 h | Track B 28–40 h + +### Goal +Migrate all shipping and letterbox label generation to the SDK `shipmentDelivery()->labelConfirm()`. Covers all product types: NL domestic, BE, EU, ROW, multicollo, insured, signature, age check, ADR LQ, pickup label, guaranteed delivery, evening delivery, letterbox. Old clients remain as fallback per product type until staging parity confirmed. + +### Current behavior +- `Rest_API\Shipping\Client` calls `POST /v1/shipment?confirm=true`. +- `Rest_API\Letterbox\Client` extends `Shipping\Client`, same endpoint. +- Product type determined by `Helper\Mapping::products_data()` based on origin, destination, and selected options. +- Request body: old `ProductCodeDelivery`, `ProductOptions`, `Dimension`, `Addresses` structure. + +### Target behavior +- Both clients call `ClientFactory::get_client()->shipmentDelivery()->labelConfirm(ShipmentDeliveryRequest)`. +- Old clients preserved as fallback until each product type is individually validated on staging. +- `Helper\Mapping` extended with V4 mapping methods alongside existing old methods (old methods not removed). + +### Scope +- `src/Rest_API/Shipping/Client.php`. +- `src/Rest_API/Letterbox/Client.php`. +- `src/Helper/Mapping.php` — new V4 mapping methods added. + +### Out of scope +- Return labels (Task 7). +- Smart Returns (Task 8). +- `src/Order/Single.php` and `src/Order/Bulk.php` callers — interface unchanged. +- `_postnl_order_metadata` structure — not changed. + +### Files/classes likely touched +- `src/Rest_API/Shipping/Client.php` +- `src/Rest_API/Letterbox/Client.php` +- `src/Helper/Mapping.php` + +### Implementation details +- SDK call: `$factory->get_client()->shipmentDelivery()->labelConfirm(ShipmentDeliveryRequest)` → `LabelConfirmResponse` +- V4 request uses `receiver`, `sender`, `items[]`, `labelSettings`, `shipmentType`, `services`, `deliveryWindow`, `returnOptions`. +- Old `Mapping::products_data()` must remain intact — do not remove. +- Add V4 mapping as a new method in `Mapping.php`, following the same structure as existing methods. +- Migrate one product type at a time; validate each on staging before switching next. + +### SDK methods/classes involved +- `Client::shipmentDelivery()->labelConfirm(ShipmentDeliveryRequest)` → `LabelConfirmResponse` +- `Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest` +- Supporting V4 request objects: `ShipmentParty`, `Contact`, `Address`, `ShippingItem`, `Dimensions`, `Services`, `LabelSettings`, `ReturnOptions`, `DeliveryWindow` + +### Data mapping notes (to be confirmed with PostNL before starting) + +| Old option | V4 field | Note | +|---|---|---| +| `ProductCodeDelivery` (domestic parcel) | `shipmentType = parcel` | | +| `ProductCodeDelivery` (letterbox) | `shipmentType = letterbox` | | +| Insured + amount | `services.insuredValue` (float) | | +| Return when not home | `services.returnWhenNotHome` (bool) | | +| Stated address only | `services.statedAddressOnly` (bool) | | +| Signature | `services.deliveryConfirmation = signature` | | +| Delivery code | `services.deliveryConfirmation = deliverycode` | | +| Age check 16+ | `services.minimalAgeCheck = 16+` | | +| Age check 18+ | `services.minimalAgeCheck = 18+` | | +| ADR LQ | `services.adrLq` | **Casing must be confirmed with PostNL** | +| Evening | `services.deliveryWindow.service = evening` | | +| Guaranteed 10:00 / 17:00 | `services.deliveryWindow.guaranteedBefore` | `12:00` has open SDK bug AITS-382 | +| LiB | `returnOptions.labelType = labelinthebox` | | +| `Dimension.Weight` | `items[0].dimensions.weight` (grams) | SDK property: `weightGr`, API key: `weight` | +| Old `Addresses` array | `sender` + `receiver` objects | Restructured in V4 | +| Multicollo | Multiple entries in `items[]` | One `ShippingItem` per parcel | + +### Fallback/old-client behavior +- Old `Shipping\Client` and `Letterbox\Client` HTTP calls preserved as fallback. +- Old `Mapping::products_data()` method not removed. +- Each product type validated individually on staging before switching; revert per type if parity fails. +- No full rollback PR needed — fallback available per product type. + +### Unit tests +- One test per product type: NL parcel, insured, signature, age check 16+, age check 18+, ADR LQ, evening, guaranteed 10:00, guaranteed 17:00, letterbox, NL-BE, EU, ROW, multicollo (2 items). +- Mock SDK client; verify `ShipmentDeliveryRequest` built correctly per variant. +- Label binary content not logged (sanitize check against `Logger`). +- Multicollo: `items[]` count matches expected number; each item has correct barcode + dimensions. + +### Integration/staging tests +- Download label for each product type on PostNL sandbox. +- Compare label content and barcode format to old client output for same order data. +- Multicollo: verify correct number of barcodes generated. +- ADR LQ: verify option reflected on label (after casing confirmed). + +### Manual QA checks +- NL domestic parcel: print label, barcode scannable. +- Insured parcel: verify insured option on label. +- Letterbox parcel: verify letterbox label format. +- NL-BE parcel: verify label format. +- Multicollo (2 items): verify two separate barcodes generated. +- Age check 18+: verify option reflected. + +### Acceptance criteria +- All product types generate correct labels on PostNL sandbox (confirmed per type, not globally). +- Old HTTP clients preserved as fallback for each product type until per-type parity confirmed. +- Old `Mapping::products_data()` intact. +- Label binary not in log output. + +### Dependencies/blockers +- Task 1 merged and Composer build validated. +- **Product code / ProductOptions → V4 `shipmentType` + `services` mapping table received from PostNL/Joris and agreed in writing before implementation starts.** +- `services.adrLq` casing confirmed by PostNL. +- AITS-382 status confirmed (guaranteed-before `12:00`). + +### Notes for reviewer +- Highest-risk task in the migration. Do not start without the written mapping table. +- `Mapping.php` is the authoritative product code source per `agents.md`. New V4 methods must follow the same structure as existing methods. +- Per-type staging sign-off is required before any old client code is removed. + +--- + +## Task 7 — Return Labels (SDK POC) + +**Status:** Ready (standard NL/BE return + LiB) | **Depends on:** Task 1 +**Estimate:** Track A 8–12 h | Track B 18–24 h + +### Goal +Migrate standard return label generation to the SDK `returnShipment()->generateReturn()`. Covers NL-NL, NL-BE, BE-NL, BE-BE standard returns and Label-in-Box. Old client remains available as fallback until staging parity is confirmed. Smart Returns client is not touched. + +### Current behavior +- `Rest_API\Return_Label\Client` calls `POST /v1/shipment?confirm=true` with return-specific product codes. +- Shares the base shipping endpoint; return behavior determined by product code. +- Return label PDF stored in `wp-content/uploads/postnl/`. + +### Target behavior +- `Return_Label\Client` checks `Router::use_sdk_for('return_labels')` — if true, calls SDK; otherwise falls through to old HTTP call. +- Standard NL/BE returns and LiB covered in this task. +- Old client remains available as fallback until staging parity confirmed. +- `Smart_Returns/Client.php` entirely unchanged. + +### Scope +- `src/Rest_API/Return_Label/Client.php`. + +### Out of scope +- `src/Rest_API/Smart_Returns/Client.php` — not touched (Task 8, blocked). +- `src/Rest_API/Shipment_and_Return/Client.php` — not touched (Task 9, blocked). +- Return label PDF storage path and `_postnl_order_metadata` — not changed. +- `WC_Email_Smart_Return` — not touched. + +### Files/classes likely touched +- `src/Rest_API/Return_Label/Client.php` + +### Implementation details +- Check `Router::use_sdk_for('return_labels')` — if false, fall through to old HTTP call (default until staging validated). +- When SDK path is active: build `ReturnShipmentRequest` from mapped fields, call `$factory->get_client()->returnShipment()->generateReturn($request)`, catch `PostnlExceptionInterface` inside the SDK branch. +- LiB: set `returnOptions.labelType = labelinthebox`, include `returnOptions.returnBarcode`. +- Print method: map from plugin settings to `returnOptions.printMethod` (`consumerPrint` / `retailPrint`). +- Old HTTP call remains in the class as the else branch — not removed. +- Preserve existing label PDF output path and download behavior. + +### SDK methods/classes involved +- `Client::returnShipment()->generateReturn(ReturnShipmentRequest)` → `GenerateReturnResponse` +- `Postnl\Sdk\RequestData\V4\ReturnShipment\ReturnShipmentRequest` +- `Postnl\Sdk\ResponseData\V4\ReturnShipment\GenerateReturnResponse` + +### Data mapping notes + +| Old field | SDK field | Note | +|---|---|---| +| Return address (merchant) | `receiver` | Merchant address for returns | +| Sender (customer) | `sender` | Consumer contact + address | +| Product code (return) | `returnOptions.domestic.returnPeriod` | Map to 20 or 35 days per SDK `ReturnPeriod` enum; values 100, 200, 365 not confirmed in SDK docs | +| Valuable return flag | `returnOptions.domestic.valuableReturn` (bool) | | +| LiB product code | `returnOptions.labelType = labelinthebox` | | +| LiB return barcode | `returnOptions.returnBarcode` | String | +| Print method setting | `returnOptions.printMethod` | `consumerPrint` / `retailPrint` | +| Label output type | `labelSettings.outputType` | Match existing settings (PDF, ZPL, etc.) | +| Label resolution | `labelSettings.resolution` | Match settings | +| Page orientation | `labelSettings.pageOrientation` | Match settings | + +### Fallback/old-client behavior +- Old return HTTP call remains in the class as the else branch of the Router check. +- Old client is not used when the SDK path is enabled and validated; it remains available as fallback at all times. +- `Smart_Returns/Client.php` entirely unaffected. +- Disable `Router::use_sdk_for('return_labels')` to restore old behavior per type immediately — no rollback PR needed. +- Old client remains available as fallback until staging parity confirmed. + +### Unit tests +- NL-NL standard return: `ReturnShipmentRequest` built correctly. +- NL-BE and BE-NL combinations. +- LiB: `returnOptions.labelType` and `returnOptions.returnBarcode` present. +- Valuable return: `returnOptions.domestic.valuableReturn = true`. +- Missing `receiver` address triggers caught `ValidationException`. + +### Integration/staging tests +- Generate NL domestic return label on sandbox; verify download and barcode valid. +- Generate LiB return label; verify return barcode present. +- Compare return label output to old client output for same order data. + +### Manual QA checks +- Generate return label for a NL domestic order in staging admin; verify PDF downloads. +- Generate LiB return label; verify QR/barcode for customer use. +- Verify return label stored at expected path in `wp-content/uploads/postnl/`. + +### Acceptance criteria +- Standard NL/BE return labels generate via SDK on staging. +- LiB return label includes return barcode. +- Old client remains available as fallback until staging parity confirmed. +- Old client is not used when the SDK path is enabled and validated. +- Smart Returns behavior completely unchanged. + +### Dependencies/blockers +- Task 1 merged. +- Confirm return period mapping from old product codes to V4 `returnPeriod` values. + +### Notes for reviewer +- Verify `Smart_Returns/Client.php` is **not** touched — check diff carefully before approving. +- `WC_Email_Smart_Return` is only for Smart Returns, not standard returns — confirm no side effects. +- Return label PDF path behavior must remain unchanged; verify `_postnl_order_metadata` saves identically. + +--- + +## Task 8 — Smart Returns ⛔ BLOCKED + +**Status:** Blocked — PostNL must confirm V4 `return/generate` replaces old `POST /shipment/v2_2/label/` +**Depends on:** Task 1 + PostNL written confirmation +**Estimate:** Track A 4–6 h | Track B 10–14 h + +### Goal +Migrate Smart Returns barcode generation to the SDK `returnShipment()->generateReturn()`, replacing the old V2.2 label API call. Only start after PostNL confirms full behavioral equivalence in writing. + +### Current behavior +- `Rest_API\Smart_Returns\Client` calls `POST /shipment/v2_2/label/` with Smart Returns product code. +- Returns a barcode (no PDF); barcode is sent to customer via `WC_Email_Smart_Return`. +- Triggered from admin via `postnl_send_smart_return_email` AJAX action. + +### Target behavior +- `Smart_Returns\Client` calls `ClientFactory::get_client()->returnShipment()->generateReturn(ReturnShipmentRequest)`. +- Barcode returned from response; customer email behavior preserved. +- Old call preserved as fallback until staging parity confirmed. + +### Scope +- `src/Rest_API/Smart_Returns/Client.php`. + +### Out of scope +- `src/Emails/WC_Email_Smart_Return.php` — not changed. +- `templates/emails/` — not changed. +- Return labels (Task 7) — separate flow. + +### Files/classes likely touched +- `src/Rest_API/Smart_Returns/Client.php` + +### Implementation details +- Same SDK call as Task 7: `$factory->get_client()->returnShipment()->generateReturn(ReturnShipmentRequest)` → `GenerateReturnResponse`. +- Smart Returns-specific field mapping to be defined after PostNL confirmation. +- `Router::use_sdk_for('smart_returns')` remains false until PostNL confirmation is received; old call remains active as fallback. + +### SDK methods/classes involved +- `Client::returnShipment()->generateReturn(ReturnShipmentRequest)` → `GenerateReturnResponse` + +### Data mapping notes +- To be defined after PostNL confirmation. +- Old body: product-code-based Smart Returns request to `v2_2/label`. +- New: `ReturnShipmentRequest` with confirmed Smart Returns fields (print method, return period, etc.). + +### Fallback/old-client behavior +- Old `POST /shipment/v2_2/label/` call preserved as active call and remains so until this task is unblocked and staging parity confirmed. +- No change to this client until written PostNL confirmation is received. + +### Unit tests (after confirmation) +- Mock SDK `generateReturn()`; verify Smart Returns request fields match confirmed mapping. +- Verify customer barcode extracted correctly from `GenerateReturnResponse`. + +### Integration/staging tests +- Generate Smart Returns barcode on PostNL sandbox; verify format. +- Trigger Smart Returns email flow; verify customer receives email with valid barcode. + +### Manual QA checks +- Trigger Smart Returns email for a test order on staging. +- Verify customer receives email with valid barcode. +- Verify admin flow (AJAX trigger) works end-to-end. + +### Acceptance criteria +- PostNL confirms V4 equivalence in writing before this task starts. +- Smart Returns barcode generates via SDK on staging. +- Customer email flow unchanged. +- Old client preserved as fallback until staging parity confirmed. + +### Dependencies/blockers +- Task 1 merged. +- **PostNL must confirm in writing: V4 `POST /shipment/delivery/v4/return/generate` fully replaces `POST /shipment/v2_2/label/` for Smart Returns** — including label format, return period behavior, and customer notification side effects. + +### Notes for reviewer +- Do not start implementation until written PostNL confirmation is in hand. +- Verify `WC_Email_Smart_Return` is not changed — only the API call providing the barcode changes. + +--- + +## Task 9 — activatereturn ⛔ BLOCKED + +**Status:** Blocked — SDK has no service for V4 activate; PostNL decision required +**Depends on:** Task 1 (Option A only) + PostNL/Joris decision +**Estimate:** Option A (SDK extension) 4–8 h | Option B (old client retained) 0 h | Track B delta for Option A +4–6 h + +### Goal +Update the activatereturn flow to V4 (via SDK extension) or explicitly confirm old-client retention. No implementation until the decision is received from PostNL/Joris. + +### Current behavior +- `Rest_API\Shipment_and_Return\Client` calls `POST /parcels/v1/shipment/activatereturn`. +- Triggered from `Order\Single` via `postnl_activate_return_function` AJAX action. +- Sets `_postnl_return_activated` order meta on success. + +### Target behavior +- **Option A:** New SDK extension class at `src/SDK/Extension/ActivateReturnExtension.php` implementing `ConfigurableAction`; calls `POST /shipment/delivery/v4/return/activate`. +- **Option B:** Old client retained as confirmed permanent fallback; decision documented in code comment. + +### Scope (Option A — SDK extension) +- `src/SDK/Extension/ActivateReturnExtension.php` — new file. +- `src/Rest_API/Shipment_and_Return/Client.php` — swap active call. + +### Scope (Option B — old client retained) +- `src/Rest_API/Shipment_and_Return/Client.php` — add code comment documenting retention decision. +- No functional change. + +### Out of scope +- `src/Order/Single.php` AJAX handler — not changed in either option. +- Return labels (Task 7), Smart Returns (Task 8) — separate. + +### Files/classes likely touched (Option A) +- `src/SDK/Extension/ActivateReturnExtension.php` — new +- `src/Rest_API/Shipment_and_Return/Client.php` + +### Implementation details (Option A) +- Implement `ConfigurableAction` interface from SDK Extension system. +- Register via `$client->extensions()->register(new ActivateReturnExtension())`. +- Execute via `$client->extensions()->getAs(ActivateReturnExtension::class)->execute($request)`. +- Request body from Postman examples: `barcode`, `sender.customerNumber`, `source`, `label`. +- Reference working SDK extension: `postnl-sdk-audit/vendor/postnl/api-client-sdk/src/Service/Checkout/V1/Extension/PostalCodeCheckExtension.php`. + +### SDK methods/classes involved (Option A) +- `Client::extensions()->register()` +- `Client::extensions()->getAs(ActivateReturnExtension::class)->execute()` +- `Postnl\Sdk\Service\Extension\ConfigurableAction` (SDK extension contract) + +### Data mapping notes + +| Old field | V4 field | Note | +|---|---|---| +| Barcode | `barcode` | Direct map | +| Customer number | `sender.customerNumber` | From settings | +| Source | `source` | Confirm value with PostNL | +| Label flag | `label` | Confirm behavior with PostNL | + +### Fallback/old-client behavior +- Old `POST /parcels/v1/shipment/activatereturn` call remains active until decision is made. +- Option B explicitly retains old client as permanent fallback with no functional change. + +### Unit tests (Option A) +- Mock SDK extensions; verify `ActivateReturnExtension` executes with correct request fields. +- Verify V4 activate endpoint called, not old parcels endpoint. +- `_postnl_return_activated` order meta set correctly on success. + +### Integration/staging tests (Option A) +- Trigger activate return for a test order on PostNL sandbox. +- Verify response behavior (see Postman: `ActivateReturn denied no label`, `ActivateReturn warning Label`). + +### Manual QA checks (Option A) +- Activate return for a test order in staging admin. +- Verify activation confirmed in order admin; verify `_postnl_return_activated` meta set. + +### Acceptance criteria +- Decision documented and agreed with PostNL/Joris. +- Option A: activatereturn calls V4 endpoint on staging successfully. +- Option B: old client retention explicitly documented in code comment; behavior unchanged. + +### Dependencies/blockers +- Task 1 merged (Option A only). +- **PostNL/Joris must answer:** Is `POST /shipment/delivery/v4/return/activate` equivalent to old `POST /parcels/v1/shipment/activatereturn`? Build SDK extension, keep old client, or drop? + +### Notes for reviewer +- SDK Extension docs show `$context->cache` as a valid `ServiceContext` property. Whether this property exists in the installed SDK's `ServiceContext.php` requires verification — check the installed source before using it. If absent, `PostalCodeCheckExtension.php` is the reference implementation. +- Postman examples for `return/activate` show `ActivateReturn denied no label` and `ActivateReturn warning Label` responses — review these response shapes before Option A implementation. + +--- + +## Full Staging QA Checklist + +Run after Tasks 1–7 are merged. Run again after Task 6 labels are considered stable. + +- [ ] Barcode: generate for NL domestic order; verify format +- [ ] Classic checkout: NL address → delivery-day slots appear (daytime + evening) +- [ ] Blocks checkout: NL address → delivery-day slots appear +- [ ] Classic checkout: NL address → pickup-point list appears +- [ ] Blocks checkout: NL address → pickup-point list appears +- [ ] Classic + blocks: select delivery day, place order; verify `_postnl_order_metadata` saved correctly +- [ ] Classic + blocks: select pickup point, place order; verify saved correctly +- [ ] Label — NL domestic parcel: downloads, barcode scannable +- [ ] Label — insured parcel: insurance option reflected +- [ ] Label — signature parcel: signature option reflected +- [ ] Label — letterbox: shipment type correct +- [ ] Label — NL-BE parcel: label format correct +- [ ] Label — multicollo (2+ items): all barcodes generated +- [ ] Return — NL domestic: label downloads, barcode valid +- [ ] Return — LiB: return barcode present in label +- [ ] Error: invalid API key → admin error message, no raw SDK exception output +- [ ] Sandbox toggle: requests route to `api-sandbox.postnl.nl` +- [ ] Logs: no API key, label binary, or PII visible in WC log output +- [ ] Postal-code check: still works (old client — confirm no regression) +- [ ] Fill In With PostNL: still works (old client — confirm no regression) + +--- + +## Overall Acceptance Criteria + +1. All flows in the routing table marked SDK are using the SDK. Old-client flows use the old HTTP client. +2. Old clients preserved as fallback for each SDK flow until staging parity confirmed per flow. +3. PHP ≥ 8.2 release/hosting decision resolved; plugin PHP minimum updated if needed. +4. SDK Composer build validated in WordPress plugin context; no autoload conflicts. +5. No API key, label binary, or customer PII in WC log output. +6. Full staging QA checklist passes. +7. Classic and blocks checkout both work end-to-end. +8. SDK requests and old-client requests are distinguishable in log entries. + +--- + +## Risks + +| Risk | Impact | Mitigation | +|---|---|---| +| Hosting / plugin PHP < 8.2 | Blocks SDK in production | Resolve PHP decision before Task 1 ships; develop on PHP 8.2 | +| SDK docs / code mismatches | Wrong method names fail at runtime | SDK docs are primary reference; discrepancies with installed code noted in reference §11; verify against installed SDK before use | +| Product mapping incomplete or wrong | PR6 labels wrong or API rejects | PostNL signs off on mapping table in writing before Task 6 starts | +| Checkout aggregation shape change | Breaks checkout display or fee calculation | Response adapters in Tasks 3+4; verify `taxRatio` logic in `Container.php` | +| Smart Returns V4 differs from V2.2 | Wrong customer return flow | Keep old client; Task 8 blocked until PostNL confirms | +| `adrLq` casing mismatch | ADR LQ option silently ignored | PostNL to confirm casing before Task 6 | +| SDK version unpinned | SDK update breaks plugin at deploy | Pin version in `composer.json`; review SDK changelog on any update | +| Composer dependency conflict | Plugin fails to load on activation | Validate full Composer build in WordPress context as part of Task 1 | diff --git a/docs/postnl-v4-migration/postnl-v4-sdk-api-reference.md b/docs/postnl-v4-migration/postnl-v4-sdk-api-reference.md new file mode 100644 index 00000000..754fcc8d --- /dev/null +++ b/docs/postnl-v4-migration/postnl-v4-sdk-api-reference.md @@ -0,0 +1,485 @@ +# PostNL V4 SDK/API Developer Reference + +## 1. Reference Scope + +- SDK package: `postnl/api-client-sdk`, local source at `postnl-sdk-audit/vendor/postnl/api-client-sdk`. +- SDK reference/version: `composer.json` branch alias `dev-main: 1.x-dev`; no installed lock version found in inspected SDK folder. +- Postman collection: `postnl-docs/PostNL Future Proof V4 API's.postman_collection.json`. +- Plugin inspected: `postnl-for-woocommerce-org`. +- Purpose: implementation and review reference for PostNL V4 SDK integration in the WooCommerce plugin. +- Evidence paths are local repository paths from the inspected folders only. + +## 2. SDK Summary + +| Item | Value | Evidence | +|---|---|---| +| package name | `postnl/api-client-sdk` | `postnl-sdk-audit/vendor/postnl/api-client-sdk/composer.json` | +| version/reference | `dev-main: 1.x-dev`; no concrete Composer installed version in SDK source folder | `postnl-sdk-audit/vendor/postnl/api-client-sdk/composer.json` | +| PHP requirement | `>=8.2 <8.5`; requires `ext-mbstring` | `postnl-sdk-audit/vendor/postnl/api-client-sdk/composer.json` | +| namespace | `Postnl\Sdk\` | `composer.json` PSR-4 autoload | +| autoload path | `vendor/autoload.php` when installed by Composer; package maps `Postnl\Sdk\` to `src/` | `composer.json` | +| main facade/client classes | `Postnl\Sdk\Client\Postnl`, `Postnl\Sdk\Client\Client`, `Postnl\Sdk\Client\PostnlClientInterface` | `src/Client/Postnl.php`, `src/Client/Client.php`, `src/Client/PostnlClientInterface.php` | +| builder/factory classes | `Postnl\Sdk\Client\ClientBuilder`, `Postnl\Sdk\Service\ServiceFactory`, `Postnl\Sdk\Transport\TransportFactory` | `src/Client/ClientBuilder.php`, `src/Service/ServiceFactory.php`, `src/Transport/TransportFactory.php` | +| auth methods | API key via `Auth::apiKey()` / `apikey` header; OAuth client credentials via `Auth::oauthClientCredentials()` / `Authorization: Bearer ...`; `Auth::fromEnv()` | `src/Auth/Auth.php`, `src/Auth/ApiKeyRequestAuthenticator.php`, `src/Auth/Oauth/OauthRequestAuthenticator.php` | +| sandbox/production handling | `ClientBuilder::SANDBOX_BASE_URI = https://api-sandbox.postnl.nl/`; production base URI `https://api.postnl.nl/`; `withSandbox(bool)` | `src/Client/ClientBuilder.php` | +| transport layer | PSR-18 client, PSR-17 factories, Guzzle helper builder, retry/log/trace/plugin transport decorators | `src/Client/ClientBuilder.php`, `src/Transport/*` | +| exception system | `PostnlSdkException`, `HttpSdkException`, client/server/transport/auth exceptions, parsers/normalizers | `src/Exception/*`, `docs/ErrorHandling/README.md` | +| docs folder | Service docs under `docs/Barcode`, `docs/ShipmentDelivery`, `docs/Labelling`, `docs/Confirming`, `docs/ReturnShipment`, `docs/Locations`, `docs/TimeFrame`, `docs/Extension` | `postnl-sdk-audit/vendor/postnl/api-client-sdk/docs` | +| versioning | `Version::V1` is deprecated with `#[DeprecatedVersion]`; `Version::V4` is the default (no explicit `withApiVersion()` call required); `SDK_POSTNL_API_VERSION` accepts `1`, `4`, or `5` but no V5 service implementations appear in SDK docs | `docs/Versioning.md`, `docs/Configuration/README.md` | +| package distribution | Distributed via Private Packagist at `https://repo.packagist.com/postnl/`; customers add the repo + an `auth.json` with a read-only token; internal runbook is in `docs/Distribution/README.md` (not customer-facing) | `docs/Distribution/README.md` | + +## 3. API Area Coverage Matrix + +| API area | Postman endpoint(s) | SDK service | SDK method | Request class | Response class | Existing plugin flow | Status | +|---|---|---|---|---|---|---|---| +| Shipment Delivery V4 | `POST /shipment/delivery/v4/labelconfirm` | `ShipmentDeliveryInterface` | `labelConfirm()` | `Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest` | `LabelConfirmResponse` | `src/Rest_API/Shipping/Client.php` old `POST /v1/shipment?confirm=true` | Covered | +| Return Shipment V4 | `POST /shipment/delivery/v4/return/generate` | `ReturnShipmentInterface` | `generateReturn()` | `ReturnShipmentRequest` | `GenerateReturnResponse` | `src/Rest_API/Return_Label/Client.php`, `src/Rest_API/Shipping/Client.php` | Covered | +| Labelling V4 | Not present | `LabellingInterface` | `requestLabel()` | `ShipmentDeliveryRequest` | `LabellingResponse` | Shipping label flow currently old client | Covered | +| Confirming / pre-announce V4 | Not present | `ConfirmingInterface` | `preAnnounceShipment()` | `ShipmentDeliveryRequest` | `ConfirmingResponse` | No distinct old pre-announce-only flow found | Covered | +| Barcode | Not present | `BarcodeInterface` | `generateBarcode()` | `BarcodeRequest` | `GenerateBarcodeResponse` | `src/Rest_API/Barcode/Client.php` old `GET /shipment/v1_1/barcode` | Covered | +| Locations / pickup locations | Not present | `Client::locations()` → `NearAddressPickupLocationsInterface`, `NearCoordinatesPickupLocationsInterface` | `getPickupLocationsByAddress()`, `getNearPickupLocationsByCoordinates()` | `PickUpNearAddressRequest`, `PickUpNearCoordinatesRequest` | `PickUpLocationsResponse` | Checkout pickup points through `src/Rest_API/Checkout/Client.php` old `POST /shipment/v1/checkout` | Covered | +| TimeFrame / delivery dates | Not present | `Client::checkout()` → `SingleServiceTimeframeInterface`, `MultipleServicesTimeframeInterface` | `getSingleServiceTimeframe()`, `getMultipleServicesTimeframe()` | `SingleServiceTimeframeRequest`, `MultipleServicesTimeframeRequest` | `TimeFrameSingleServiceResponse`, `TimeframesMultipleServicesResponse` | Checkout delivery days through `src/Rest_API/Checkout/Client.php` | Covered | +| Checkout coverage | Not present | No standalone `checkout()` service on `Client` | N/A | N/A | N/A | `src/Rest_API/Checkout/Client.php` old `POST /shipment/v1/checkout` | Needs mapping | +| Postal code check / address validation | Not present | Extension only: `PostalCodeCheckExtension` | `ConfigurableAction::execute()` | `PostalCodeCheckRequest` | `PostalCodeAddressResponse` | `src/Rest_API/Postcode_Check/Client.php` old `POST /shipment/checkout/v1/postalcodecheck` | Partially covered | +| Smart Returns | `POST /shipment/delivery/v4/return/generate` examples named Smart Returns | `ReturnShipmentInterface` | `generateReturn()` | `ReturnShipmentRequest` | `GenerateReturnResponse` | `src/Rest_API/Smart_Returns/Client.php` old `POST /shipment/v2_2/label/` | Needs mapping | +| Shipment & Return activation / activatereturn | `POST /shipment/delivery/v4/return/activate` | Not found | Not found | Not found | Not found | `src/Rest_API/Shipment_and_Return/Client.php` old `POST /parcels/v1/shipment/activatereturn` | Needs mapping | +| Fill In With PostNL OAuth | Not present | SDK OAuth client credentials only | `Postnl::oauthClient()`, `Auth::oauthClientCredentials()` | N/A | N/A | `src/Frontend/Fill_In_With_Postnl.php`, `src/Frontend/Fill_In_With_Postnl_Handler.php` browser PKCE flow | Keep old client | + +## 4. Endpoint Reference + +Endpoint: +`POST /shipment/delivery/v4/labelconfirm` + +| Item | Value | +|---|---| +| API area | Shipment Delivery V4 | +| endpoint names | 50 Postman examples under `PostNL Shipment API` | +| auth/header | `apikey: {{APIkey-Sandbox}}`; SDK adds `Content-Type: application/json` by default | +| common request sections | `receiver`, `sender`, `itemCount`, `items[]`, `labelSettings`, `shipmentType`; optional `services`, `returnOptions`, `deliveryLocation`, `handOverDate`, `internationalShipmentData` | +| matching SDK service/method | `Client::shipmentDelivery()->labelConfirm(ShipmentDeliveryRequest)` | +| matching plugin flow | `src/Rest_API/Shipping/Client.php` old `POST /v1/shipment?confirm=true`; `src/Rest_API/Letterbox/Client.php` extends shipping client | + +Variations found: +- Parcel NL; Parcel NL LiB; Parcel NL With Dimensions; Parcel NL Multicollo. +- Insured; ReturnWhenNotHome; StatedAddressOnly; Signature on Delivery; deliveryCode on Delivery. +- ADR Low Quantity; AgeCheck 16+; AgeCheck 18+. +- Guaranteed Before `10:00`, `12:00`, `17:00`; Evening Delivery. +- Pickup at PostNL Location; APL; Pickup + Insured; Pickup + Insured + AgeCheck 18+. +- Letterbox Parcel NL; Letterbox Parcel NL 48hours. +- Parcel BE to NL; NL to BE; BE to BE. +- EU parcel/packet/letterbox Track and Trace, Insured, Insured Plus, Untracked. +- ROW parcel/letterbox Track and Trace, Insured, Insured Plus, Untracked. + +Endpoint: +`POST /shipment/delivery/v4/return/generate` + +| Item | Value | +|---|---| +| API area | Return Shipment V4 / Smart Returns examples | +| endpoint names | `NL-NL Single Label`, `NL-BE Single Label`, `BE-NL Single Label`, `BE-BE Single Label`, `NL-NL Smart Returns`, `NL-BE Smart Returns`, `NL-NL Antwoordnummer`, `BE-NL Antwoordnummer`, `NL-NL Single Label Valuable Return` | +| auth/header | `Content-Type: application/json`, `Accept: application/json`, `apikey: {{APIkey-Sandbox}}` | +| common request sections | `receiver`, `sender`, `labelSettings`, `returnOptions`, `shipmentType`, `items[]` | +| unique variations/options | NL/BE sender receiver combinations; Smart Returns names; Antwoordnummer return address; `returnOptions.domestic.valuableReturn` | +| matching SDK service/method | `Client::returnShipment()->generateReturn(ReturnShipmentRequest)` | +| matching plugin flow | `src/Rest_API/Return_Label/Client.php`; `src/Rest_API/Smart_Returns/Client.php` needs mapping from old label V2.2 body | + +Endpoint: +`POST /shipment/delivery/v4/return/activate` + +| Item | Value | +|---|---| +| API area | Shipment & Return activation / activatereturn | +| endpoint names | `ActivateReturn denied no label`, `ActivateReturn warning Label` | +| auth/header | `Content-Type: application/json`, `apikey: {{APIkey-Sandbox}}` | +| common request sections | `barcode`, `sender.customerNumber`, `source`, `label` | +| unique variations/options | Postman examples use identical visible body shape; names indicate denied/warning responses | +| matching SDK service/method | Not found | +| matching plugin flow | `src/Rest_API/Shipment_and_Return/Client.php` old `POST /parcels/v1/shipment/activatereturn` | + +SDK endpoints not present in Postman collection: + +| API area | Method | Path | SDK service/method | Request | +|---|---|---|---|---| +| Barcode | `POST` | `/shipment/delivery/v4/barcode` | `Client::barcode()->generateBarcode()` | `BarcodeRequest` | +| Labelling | `POST` | `/shipment/delivery/v4/label` | `Client::labelling()->requestLabel()` | `ShipmentDeliveryRequest` | +| Confirming | `POST` | `/shipment/delivery/v4/confirm` | `Client::confirming()->preAnnounceShipment()` | `ShipmentDeliveryRequest` | +| Locations by address | `POST` | `/shipment/delivery/v4/locations/near-address` | `Client::locations()->getPickupLocationsByAddress()` | `PickUpNearAddressRequest` | +| Locations by coordinates | `POST` | `/shipment/delivery/v4/locations/near-coordinates` | `Client::locations()->getNearPickupLocationsByCoordinates()` | `PickUpNearCoordinatesRequest` | +| Single TimeFrame | `POST` | `/shipment/delivery/v4/timeframe/singleservice` | `Client::checkout()->getSingleServiceTimeframe()` | `SingleServiceTimeframeRequest` | +| Multiple TimeFrames | `POST` | `/shipment/delivery/v4/timeframe/multipleservices` | `Client::checkout()->getMultipleServicesTimeframe()` | `MultipleServicesTimeframeRequest` | +| Postal code check extension | `GET` | `/shipment/checkout/v1/postalcodecheck` | `PostalCodeCheckExtension` via `extensions()` | `PostalCodeCheckRequest` | + +## 5. Payload Field Reference + +### receiver + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `receiver.customerNumber` | string | Return V4 receiver | `1122334455` | Merchant receiver for returns; SDK `ShipmentParty::$customerNumber` | +| `receiver.customerCode` | string | Return V4 receiver | `DEVC` | SDK `ShipmentParty::$customerCode` | +| `receiver.type` | enum/string | Shipment V4 receiver | `consumer` | SDK field is `receiverType` mapped to API key `type` | +| `receiver.contact.firstName` | string | Shipment/Return | `Test` | SDK `Contact::$firstName` | +| `receiver.contact.lastName` | string | Shipment/Return | `Persoon` | SDK `Contact::$lastName` | +| `receiver.contact.email` | string | Shipment/Return | `test.persoon@postnl.nl` | SDK `Contact::$email` | +| `receiver.contact.language` | enum/string | Shipment/Return | `NL` | SDK `Language` enum | +| `receiver.contact.mobileNumber` | string | Shipment/Return | `0612345678` | Max length noted in SDK docblock: 16 | +| `receiver.address.countryIso` | enum/string | Shipment/Return | `NL` | SDK `Country` enum; ISO2 | +| `receiver.address.city` | string | Shipment/Return | `Den Haag` | SDK `Address::$city` | +| `receiver.address.companyName` | string | Shipment/Return | `TestBedrijf` | SDK `Address::$companyName` | +| `receiver.address.departmentName` | string | ROW shipment | `Afdeling` | International examples | +| `receiver.address.houseNumber` | string | Shipment/Return | `3` | SDK `Address::$houseNumber` | +| `receiver.address.houseNumberAddition` | string/null | Shipment/Return | `bis` | SDK `Address::$houseNumberAddition` | +| `receiver.address.postalCode` | string | Shipment/Return | `2521CA` | SDK `Address::$postalCode` | +| `receiver.address.street` | string | Shipment/Return | `Waldorpstraat` | SDK `Address::$street` | +| `receiver.address.addressLine` | string/null | Shipment | Unclear | Present in SDK; Postman examples mostly null/omitted | +| `receiver.address.internationalAddressData.*` | object | ROW shipment | `area`, `region`, `buildingName`, `floor`, `doorcode` | SDK `InternationalAddressData` | + +### sender + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `sender.customerNumber` | string | Shipment sender / activation sender | `11223344` | SDK credential strategy can merge into sender | +| `sender.customerCode` | string | Shipment sender | `DEVC` | SDK credential strategy can merge into sender | +| `sender.contact.firstName` | string | Return sender | `Test` | Consumer return sender | +| `sender.contact.lastName` | string | Return sender | `Verzender` | Consumer return sender | +| `sender.contact.email` | string | Return sender | `verzendemail@postnl.nl` | SDK `Contact` | +| `sender.contact.language` | enum/string | Return sender | `NL` | SDK `Language` enum | +| `sender.contact.mobileNumber` | string | Return sender | `+31687654321` | SDK `Contact` | +| `sender.address.countryIso` | enum/string | Shipment/Return | `NL` | SDK `Country` enum | +| `sender.address.city` | string | Shipment/Return | `Den Haag` | SDK `Address` | +| `sender.address.companyName` | string | Shipment/Return | `TestBedrijf` | SDK `Address` | +| `sender.address.houseNumber` | string | Shipment/Return | `2` | SDK `Address` | +| `sender.address.houseNumberAddition` | string/null | Shipment/Return | `bis` | SDK `Address` | +| `sender.address.postalCode` | string | Shipment/Return | `2521CA` | SDK `Address` | +| `sender.address.street` | string | Shipment/Return | `Teststraat` | SDK `Address` | +| `sender.undeliverableReturnAddress` | object | SDK model | Unclear | Present in SDK; not visible in extracted Postman field list | + +### items[] + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `itemCount` | int | Shipment V4 | `1` | SDK derives from item collection when `items[]` present | +| `items[].barcode` | string | Shipment/Return | `{{barcode}}` | SDK `ShippingItem::$barcode` | +| `items[].customerReferences.shipmentReference` | string | Shipment | `Reference` | SDK `CustomerReferences` | +| `items[].customerReferences.costCenter` | string | Shipment | `Factuurnummer` | SDK `CustomerReferences` | +| `items[].customerReferences.returnReference` | string | Return | `returnReference` | SDK `CustomerReferences` | +| `items[].dimensions` | object | Shipment/Return | See `dimensions` | SDK `Dimensions` | + +### dimensions + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `items[].dimensions.length` | int | Shipment with dimensions | `1000` | SDK constructor property `lengthCm`, API key `length` | +| `items[].dimensions.width` | int | Shipment with dimensions | `1000` | SDK constructor property `widthCm`, API key `width` | +| `items[].dimensions.height` | int | Shipment with dimensions | `1000` | SDK constructor property `heightCm`, API key `height` | +| `items[].dimensions.weight` | int | Shipment/Return | `1000` | SDK constructor property `weightGr`, API key `weight` | +| `items[].dimensions.volume` | int | Shipment with dimensions | `1000` | SDK constructor property `volumeCm3`, API key `volume` | + +### customerReferences + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `customerReferences.shipmentReference` | string | Shipment item | `Reference` | Old plugin maps order number to old `Reference` | +| `customerReferences.costCenter` | string | Shipment item | `Factuurnummer` | Optional reference | +| `customerReferences.returnReference` | string | Return item | `returnReference` | Return reference/RMA candidate | + +### labelSettings + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `labelSettings.outputType` | enum/string | Shipment/Return | `PDF` in Postman, SDK enum values are lowercase `pdf`, `zpl`, `jpg`, `gif`, `png` | Docs/code casing must be verified against API | +| `labelSettings.resolution` | enum/int | Shipment/Return | `200` | SDK enum values `200`, `300`, `600` | +| `labelSettings.pageOrientation` | enum/string | Shipment/Return | `portrait` | SDK enum `portrait`, `landscape` | +| `labelSettings.printMethod` | enum/string | Labelling, ShipmentDelivery, Return | `consumerPrint` | SDK enum `consumerPrint` (BE, PDF recommended), `retailPrint` (NL, PNG/JPG recommended); present in all label endpoints, not return-only | +| `labelSettings.mergeType` | enum/string | SDK model | Unclear | SDK enum `singlepdf`, `pdfa6toa4` | +| `labelSettings.positioning` | enum/string | SDK model | Unclear | SDK enum `topleft`, `topright`, `bottomleft`, `bottomright` | + +### services + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `services.insuredValue` | float | Insured shipment | `250` | Replaces old insurance amount/product option when mapped | +| `services.returnWhenNotHome` | bool | ReturnWhenNotHome | `true` | SDK `Services::$returnWhenNotHome` | +| `services.statedAddressOnly` | bool | StatedAddressOnly | `true` | SDK `Services::$statedAddressOnly` | +| `services.deliveryConfirmation` | enum/string | Signature/delivery code | `signature` | SDK enum `signature`, `deliverycode` | +| `services.minimalAgeCheck` | enum/string | AgeCheck | `16+`, `18+` | SDK enum values `16+`, `18+` | +| `services.adrLq` | bool | ADR Low Quantity | `true` | SDK field/API key is `adrLq`; one extracted Postman field appeared as `adrlq`, verify casing before implementation | +| `services.registered` | bool | SDK model | Unclear | Present in SDK; not found in extracted Postman examples | +| `services.deliveryWindow.service` | enum/string | Evening | `evening` | SDK enum also contains `daytime` | +| `services.deliveryWindow.guaranteedBefore` | enum/string | Guaranteed delivery | `10:00`, `12:00`, `17:00` | SDK comments note `12:00` validation issue `AITS-382` | +| `services.deliveryWindow.duration` | enum/string | Letterbox 48hours | `non24hours` | SDK enum also `24hours` | + +### returnOptions + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `returnOptions.labelType` | enum/string | LiB / return labels | `labelinthebox` | SDK enum also `Label`, `shipmentandreturnlabel`, `retourLabel`, `CN23`, `CommercialInvoice` | +| `returnOptions.returnBarcode` | string | LiB | `{{returnBarcode}}` | SDK `ReturnOptions::$returnBarcode` | +| `returnOptions.returnAddress` | object | LiB / EU return | `returnOptions.returnAddress.*` | SDK `Address` | +| `returnOptions.domestic.returnPeriod` | enum/int | Return generate | `20` | SDK `ReturnPeriod` enum confirms `IN_20_DAYS` (20) and `IN_35_DAYS` (35) only; values `100`, `200`, `365` not found in provided SDK docs | +| `returnOptions.domestic.valuableReturn` | bool | Valuable return | `false` | SDK `DomesticReturnOptions::$valuableReturn` | +| `returnOptions.returnBlock` | bool | SDK model | Unclear | Present in SDK; likely relates activation, but not found in extracted Postman return/generate examples | + +### TimeFrame request fields + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `handoverDate` | string/date | Single/multiple TimeFrame | Unclear | SDK `SingleServiceTimeframeRequest`, `MultipleServicesTimeframeRequest` | +| `deliveryDays` | int | Single TimeFrame | Unclear | Single service only | +| `numberOfDays` | int | Multiple TimeFrames | Unclear | Multiple services only | +| `receiverAddress` | object | TimeFrame | Unclear | SDK `Address` | +| `service` | enum/string | Single TimeFrame | `daytime` / `evening` | SDK `DeliveryWindowService` | +| `services[]` | enum/string[] | Multiple TimeFrames | `daytime`, `evening` | SDK validates enum items | +| `shipmentType` | enum/string | TimeFrame | `parcel` | SDK `ShipmentType` | +| `customerCode` | string | TimeFrame | Unclear | SDK field | +| `customerNumber` | string | TimeFrame | Unclear | SDK field | + +### Locations request fields + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `numberOfLocations` | int | Locations | Unclear | SDK docblock min 1, max 10 | +| `receiverAddress` | object | Near-address locations | Unclear | SDK `Address` | +| `coordinates.latitude` | float/string | Near-coordinates locations | Unclear | SDK `Coordinates` | +| `coordinates.longitude` | float/string | Near-coordinates locations | Unclear | SDK `Coordinates` | +| `locationType` | enum/string | Locations | `Retail`, `ParcelLocker` | SDK `PickUpLocationType` | +| `pickUpDate` | string/date | Locations | Unclear | Delivery date at pickup location | +| `receiverCountryIso` | enum/string | Near-coordinates locations | Unclear | SDK `Country` | +| `customerCode` | string | Locations | Unclear | SDK field | +| `customerNumber` | string | Locations | Unclear | SDK field | + +### Barcode request fields + +| Field | Type if inferable | Used in | Example value | Notes | +|---|---|---|---|---| +| `customerNumber` | string | Barcode V4 | Unclear | SDK `BarcodeRequest` | +| `customerCode` | string | Barcode V4 | Unclear | SDK `BarcodeRequest` | +| `serieStart` | string | Barcode V4 | Unclear | SDK constructor parameter is `serieStart`/`serieEnd`; `fromArray()` key is `seriesStart`/`seriesEnd` — inconsistency in SDK docs; verify against installed SDK version | +| `serieEnd` | string | Barcode V4 | Unclear | SDK field; see note on `serieStart` | +| `numberOfBarcodes` | int | Barcode V4 | Unclear | SDK field | + +## 6. Services / Options Reference + +| Option | Field path | Example value | Example request names | Notes | +|---|---|---|---|---| +| insuredValue | `services.insuredValue` | `250` | Parcel NL Insured; EU/ROW Insured | Maps old insured product option/amount after product mapping | +| returnWhenNotHome | `services.returnWhenNotHome` | `true` | Return When Not Home combinations | Replaces old `return_no_answer` when mapped | +| statedAddressOnly | `services.statedAddressOnly` | `true` | Stated Address Only combinations | Replaces old `only_home_address` when mapped | +| deliveryConfirmation | `services.deliveryConfirmation` | `signature`, `deliverycode` | Signature on Delivery; deliveryCode on Delivery | Replaces old signature/delivery code options | +| minimalAgeCheck | `services.minimalAgeCheck` | `16+`, `18+` | AgeCheck 16+; AgeCheck 18+ | Replaces old adult/id check option when mapped | +| adrlq | `services.adrLq` | `true` | ADR Low Quantity | SDK casing `adrLq`; extracted Postman casing looked `adrlq`; verify request casing | +| deliveryWindow.guaranteedBefore | `services.deliveryWindow.guaranteedBefore` | `10:00`, `12:00`, `17:00` | Guaranteed Before 10/12/17 | SDK comment flags `12:00` validation issue | +| deliveryWindow.service | `services.deliveryWindow.service` | `evening` | Evening Delivery | TimeFrame services use `daytime`/`evening` | +| deliveryWindow.duration | `services.deliveryWindow.duration` | `non24hours` | Letterbox Parcel NL 48hours | SDK enum also `24hours` | +| labelType | `returnOptions.labelType` | `labelinthebox` | Parcel NL LiB | SDK docs confirm 3 `LabelType` values: `Label`, `labelinthebox`, `shipmentandreturnlabel`; values `retourLabel`, `CN23`, `CommercialInvoice` not found in provided SDK docs | +| returnBarcode | `returnOptions.returnBarcode` | `{{returnBarcode}}` | Parcel NL LiB | Used with label-in-the-box | +| returnAddress | `returnOptions.returnAddress.*` | `Teststraat`, `Den Haag` | Parcel NL LiB; EU return options | SDK `Address` | +| shipmentType | `shipmentType` | `parcel`, `letterbox`, `packet` | Parcel, Letterbox, Packet examples | SDK enum also `parcelnonstandard`, `letter`, `pallet` | +| dimensions | `items[].dimensions.*` | `length: 1000`, `weight: 1000` | With Dimensions; EU/ROW examples | Old client used `Dimension.Weight`; V4 item dimensions object | +| customerReferences | `items[].customerReferences.*` | `Reference`, `Factuurnummer` | Shipment/Return examples | Map order number/cost center/RMA references here | + +## 7. SDK Service Reference + +| Area | service/facade method | interface method | request class | response class | docs path | matching endpoint from Postman | short purpose | +|---|---|---|---|---|---|---|---| +| ShipmentDelivery | `Client::shipmentDelivery()` | `ShipmentDeliveryInterface::labelConfirm()` | `ShipmentDeliveryRequest` | `LabelConfirmResponse` | `docs/ShipmentDelivery/README.md` | `POST /shipment/delivery/v4/labelconfirm` | Create shipment and label/confirm response | +| ReturnShipment | `Client::returnShipment()` | `ReturnShipmentInterface::generateReturn()` | `ReturnShipmentRequest` | `GenerateReturnResponse` | `docs/ReturnShipment/README.md` | `POST /shipment/delivery/v4/return/generate` | Generate return shipment/label | +| Labelling | `Client::labelling()` | `LabellingInterface::requestLabel()` | `ShipmentDeliveryRequest` | `LabellingResponse` | `docs/Labelling/README.md` | Not present | Request label endpoint without confirm path | +| Confirming | `Client::confirming()` | `confirmShipmentPreAnnouncement()` / `preAnnounceShipment()` (SDK docs inconsistent; prior code inspection found `preAnnounceShipment()` on interface — use that) | `ShipmentDeliveryRequest` | `ConfirmingResponse` | `docs/Confirming/README.md` | Not present | Pre-announce/confirm shipment without label | +| Barcode | `Client::barcode()` | `BarcodeInterface::generateBarcode()` | `BarcodeRequest` | `GenerateBarcodeResponse` | `docs/Barcode/README.md` | Not present | Generate one or more barcodes | +| Locations by address | `Client::locations()` | `getPickupLocationsByAddress()` | `PickUpNearAddressRequest` | `PickUpLocationsResponse` | `docs/Locations/README.md` | Not present | Pickup locations near postal address | +| Locations by coordinates | `Client::locations()` | `getNearPickupLocationsByCoordinates()` | `PickUpNearCoordinatesRequest` | `PickUpLocationsResponse` | `docs/Locations/README.md` | Not present | Pickup locations near lat/long | +| Single TimeFrame | `Client::checkout()` | `getSingleServiceTimeframe()` | `SingleServiceTimeframeRequest` | `TimeFrameSingleServiceResponse` | `docs/TimeFrame/README.md` | Not present | Delivery timeframes for one service | +| Multiple TimeFrames | `Client::checkout()` | `getMultipleServicesTimeframe()` (also `multipleTimeframes()` in SDK complete example — SDK docs are inconsistent) | `MultipleServicesTimeframeRequest` | `TimeframesMultipleServicesResponse` | `docs/TimeFrame/README.md` | Not present | Delivery timeframes for multiple services | +| PostalCodeCheck extension | `Client::extensions()` | `ConfigurableAction::execute()` | `PostalCodeCheckRequest` | `PostalCodeAddressResponse` | `docs/Extension/README.md` | Not present | V1 checkout postal-code/address lookup extension | +| Auth | `Postnl::client()`, `Postnl::sandboxClient()`, `Postnl::oauthClient()`, `Postnl::sandboxOauthClient()`, `Auth::*` | N/A | N/A | N/A | `docs/Configuration/README.md` | API key headers in Postman | Request authentication | +| Extensions | `Client::extensions()` | `ClientExtensionsInterface::register()`, `getAs()` | Extension-defined | Extension-defined | `docs/Extension/README.md` | Not present | Add unsupported endpoints such as postal-code check or activatereturn | + +## 8. Current Plugin Flow Mapping + +| Plugin flow | Current file/class | Old endpoint | V4 endpoint | SDK method | Recommended path | Notes | +|---|---|---|---|---|---|---| +| Barcode | `src/Rest_API/Barcode/Client.php` | `GET /shipment/v1_1/barcode` | `/shipment/delivery/v4/barcode` | `barcode()->generateBarcode()` | SDK after mapping | Old query fields `Type`, `Serie`, `Range` do not directly match SDK `serieStart`, `serieEnd`, `numberOfBarcodes` | +| Shipping labels | `src/Rest_API/Shipping/Client.php` | `POST /v1/shipment?confirm=true` | `/shipment/delivery/v4/labelconfirm` | `shipmentDelivery()->labelConfirm()` | SDK after mapping | Must map old `ProductCodeDelivery`/`ProductOptions` to V4 `shipmentType` and `services` | +| Return labels | `src/Rest_API/Return_Label/Client.php` | `POST /v1/shipment?confirm=true` via shipping client | `/shipment/delivery/v4/return/generate` or `/labelconfirm` with `returnOptions` | `returnShipment()->generateReturn()` or `shipmentDelivery()->labelConfirm()` | SDK after mapping | Preserve existing label output and return address behavior | +| Letterbox labels | `src/Rest_API/Letterbox/Client.php` | `POST /v1/shipment?confirm=true` | `/shipment/delivery/v4/labelconfirm` | `shipmentDelivery()->labelConfirm()` | SDK after mapping | Map to `shipmentType=letterbox` and applicable dimensions/services | +| Checkout delivery options | `src/Rest_API/Checkout/Client.php`, `src/Frontend/Container.php`, `src/Frontend/Delivery_Day.php` | `POST /shipment/v1/checkout` | `/shipment/delivery/v4/timeframe/singleservice`, `/shipment/delivery/v4/timeframe/multipleservices` | `checkout()->getSingleServiceTimeframe()`, `checkout()->getMultipleServicesTimeframe()` | Hybrid | Checkout V1 combines delivery dates and locations; V4 split requires aggregation | +| Pickup points | `src/Rest_API/Checkout/Client.php`, `src/Frontend/Dropoff_Points.php` | `POST /shipment/v1/checkout` | `/shipment/delivery/v4/locations/near-address`, `/shipment/delivery/v4/locations/near-coordinates` | `locations()->getPickupLocationsByAddress()`, `locations()->getNearPickupLocationsByCoordinates()` | SDK after mapping | Preserve UI response shape expected by checkout frontend | +| TimeFrame / delivery dates | `src/Rest_API/Checkout/Client.php`, `src/Frontend/Container.php` | `POST /shipment/v1/checkout` | `/shipment/delivery/v4/timeframe/*` | `checkout()->getSingleServiceTimeframe()`, `checkout()->getMultipleServicesTimeframe()` | SDK after mapping | DeliveryDate is represented by TimeFrame V4, not standalone DeliveryDate service | +| Postcode check | `src/Rest_API/Postcode_Check/Client.php` | `POST /shipment/checkout/v1/postalcodecheck` | `/shipment/checkout/v1/postalcodecheck` | `PostalCodeCheckExtension` | Old client | SDK extension uses `GET` while plugin uses `POST`; validation remains outside V4 | +| Smart Returns | `src/Rest_API/Smart_Returns/Client.php` | `POST /shipment/v2_2/label/` | `/shipment/delivery/v4/return/generate` possibly | `returnShipment()->generateReturn()` | Needs confirmation | Postman names Smart Returns under return/generate, but old body/product behavior requires mapping confirmation | +| Shipment & Return activation | `src/Rest_API/Shipment_and_Return/Client.php` | `POST /parcels/v1/shipment/activatereturn` | `/shipment/delivery/v4/return/activate` | Not found | Needs confirmation | Postman has V4 endpoint; SDK service absent | +| Fill In With PostNL OAuth | `src/Frontend/Fill_In_With_Postnl.php`, `src/Frontend/Fill_In_With_Postnl_Handler.php` | `https://dil-login.postnl.nl/oauth2/token/`, `https://dil-login.postnl.nl/api/user_info/` | Not in V4 shipment API | SDK OAuth client credentials not equivalent | Old client | Existing browser authorization-code PKCE flow differs from SDK machine-to-machine OAuth | + +## 9. Checkout / DeliveryDate Reference + +- DeliveryDate is represented through TimeFrame V4 SDK services: + - `POST /shipment/delivery/v4/timeframe/singleservice` + - `POST /shipment/delivery/v4/timeframe/multipleservices` +- SDK docs show `$postnl->checkout()->getSingleServiceTimeframe()` and `$postnl->checkout()->getMultipleServicesTimeframe()` for TimeFrame calls; prior code inspection found `singleTimeframe()` / `multipleTimeframes()` on `Client` — verify before use. +- Checkout behavior is covered through TimeFrame + Locations: + - delivery-day options: TimeFrame services. + - pickup-point options: Locations services (`$postnl->locations()` per SDK docs). +- No standalone Checkout V4 service is exposed; no standalone DeliveryDate V4 service is exposed. +- Postal-code/address validation remains outside V4; SDK includes a V1 checkout postal-code extension. + +## 10. Gaps / Unknowns + +| Area | What is known | What is missing | Impact | +|---|---|---|---| +| product/options -> V4 productData/services mapping | V4 examples use `shipmentType`, `services`, `returnOptions`, `deliveryWindow`; plugin uses old `ProductCodeDelivery` and `ProductOptions` from `src/Helper/Mapping.php` | Exact mapping for every old product code/characteristic/option to V4 fields | Cannot safely migrate all labels without a mapping table and test cases | +| Smart Returns replacement or keep-old decision | Postman has Smart Returns examples on `POST /shipment/delivery/v4/return/generate`; SDK covers return generate | Whether old `POST /shipment/v2_2/label/` Smart Returns behavior is fully replaced by V4 return generate | Keep old client or hybrid until equivalence confirmed | +| activatereturn replacement or keep-old decision | Postman has `POST /shipment/delivery/v4/return/activate`; SDK has no service | Request/response model and SDK extension decision | Requires custom extension or old client fallback | +| OAuth requirement per endpoint | Postman V4 shipment/return/activate examples show `apikey`; SDK supports API key and OAuth client credentials | Which V4 endpoints require OAuth instead of API key is not explicit in inspected sources | Mark endpoint auth as API-key evidenced; OAuth requirement unclear | +| SDK docs/code mismatches that affect method names | SDK docs and prior code inspection disagree on facade accessor names for Locations and TimeFrame services (see Section 11 for detail); Confirming method name is inconsistent within the SDK docs themselves | If wrong method is called the call fails at runtime | Verify `src/Client/Client.php` and interfaces before any implementation | +| Checkout/DeliveryDate standalone absence | SDK docs show `$postnl->checkout()` for TimeFrame calls; no standalone `deliveryDate()` accessor documented | No direct V4 deliveryDate or single checkout service; TimeFrame + Locations must be aggregated | Checkout migration must call TimeFrame and Locations separately | +| `services.adrLq` casing | SDK `PayloadKey::adrLq = 'adrLq'`; extracted Postman field appeared as `services.adrlq` | Exact API casing accepted by V4 | Verify before sending ADR LQ requests | +| `labelSettings.outputType` casing | SDK enum values lowercase; Postman examples show `PDF` | Whether API accepts both or only one casing | Normalize through SDK enum unless PostNL confirms otherwise | +| Barcode request field naming | SDK constructor uses `serieStart`/`serieEnd`; SDK `fromArray()` uses `seriesStart`/`seriesEnd` (with trailing `s`) | Whether the API key (serialized form) is `serieStart` or `seriesStart` | Verify against installed SDK `src/Service/Barcode/V4/Request/BarcodeRequest.php` | +| `LabelType` enum completeness | SDK docs confirm `Label`, `labelinthebox`, `shipmentandreturnlabel` | Values `retourLabel`, `CN23`, `CommercialInvoice` listed in prior reference but not found in provided SDK docs | Check SDK `src/Enums/Payload/LabelType.php` before mapping | +| Locations and TimeFrame `Client` facade | SDK docs consistently show `$postnl->locations()` and `$postnl->checkout()` | Prior code inspection found `addressLocations()`, `coordinateLocations()`, `singleTimeframe()`, `multipleTimeframes()` on `Client` — if those are absent, SDK doc examples will fail | Verify `src/Client/Client.php` before implementation | + +## 11. Docs / Code Mismatches + +| Area | Docs say | Code exposes | Impact | Evidence | +|---|---|---|---|---| +| TimeFrame namespaces | Docs import `Postnl\Sdk\Service\Checkout\V4\Request\SingleServiceTimeframeRequest` and `MultipleServicesTimeframeRequest` | Code classes are under `Postnl\Sdk\Service\SingleServiceTimeframe\V4\Request` and `Postnl\Sdk\Service\MultipleServicesTimeframe\V4\Request` | Wrong imports fail | `docs/TimeFrame/README.md`, `src/Service/SingleServiceTimeframe/V4/Request/SingleServiceTimeframeRequest.php` | +| TimeFrame facade | SDK docs consistently use `$postnl->checkout()->getSingleServiceTimeframe()` and `$postnl->checkout()->getMultipleServicesTimeframe()`; one complete example uses `$postnl->checkout()->multipleTimeframes()` | Prior code inspection found `$postnl->singleTimeframe()->getTimeframe()` and `$postnl->multipleTimeframes()->getTimeframes()` on `Client` — if `checkout()` is absent from `Client`, all SDK doc examples fail | Verify `src/Client/Client.php` before writing integration code | `docs/TimeFrame/README.md` | +| Locations facade | SDK docs use `$postnl->locations()->getPickupLocationsByAddress()` and `$postnl->locations()->getNearPickupLocationsByCoordinates()` | Prior code inspection found `$postnl->addressLocations()->getNearestByAddress()` and `$postnl->coordinateLocations()->getNearestByCoordinates()` on `Client` — if `locations()` is absent, SDK doc examples fail | Verify `src/Client/Client.php` before writing integration code | `docs/Locations/README.md` | +| Confirming method name | SDK docs examples use `confirmShipmentPreAnnouncement()` in most code blocks but the complete example uses `preAnnounceShipment()` — SDK docs are internally inconsistent | Prior code inspection found interface exposes `preAnnounceShipment()` | Use `preAnnounceShipment()` per prior code inspection; confirm against installed SDK | `docs/Confirming/README.md` | +| retry builder method | SDK docs show `withRetryPolicy(new ExponentialBackoffRetryPolicy(maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10000))` | Prior code inspection found `ClientBuilder::withRetry(RetryConfig $config)` — different method name and signature | Verify `src/Client/ClientBuilder.php` before calling | `docs/Configuration/README.md` | +| enum namespaces | Docs examples may imply checkout-specific request namespace | Code payload enums are under `Postnl\Sdk\Enums\Payload` | Use code namespaces for imports | `src/Enums/Payload/*` | +| Checkout references | TimeFrame SDK docs use `$postnl->checkout()` as the facade accessor for timeframe calls; Extension docs reference V1 checkout postal-code extension only | Prior code inspection of `Client` found no `checkout()` method — if absent, TimeFrame calls via `$postnl->checkout()` will fail | Verify `src/Client/Client.php`; the postal-code V1 extension is accessed via `Client::extensions()` regardless | `docs/TimeFrame/README.md`, `docs/Extension/README.md`, `src/Client/Client.php` | +| Extension cache context | SDK Extension docs explicitly show `$context->cache` as valid and document `ServiceContext` as exposing `transport`, `apiVersion`, `identity`, `logger`, `cache`, `payloadMapper` | Prior code inspection of `ServiceContext.php` did not find a `cache` property | Verify `src/Service/ServiceContext.php` against installed SDK | `docs/Extension/README.md` | + +## 12. Error Handling Reference + +- Base/catch-all SDK interfaces/classes: + - `Postnl\Sdk\Exception\PostnlExceptionInterface` + - `Postnl\Sdk\Exception\PostnlSdkException` + - `Postnl\Sdk\Exception\HttpSdkException` +- Client/auth/validation/rate-limit/timeout/server exceptions: + - `Exception\Client\AuthenticationException` + - `Exception\Client\ClientException` + - `Exception\Client\ValidationException` + - `Exception\Client\RateLimitException` + - `Exception\Client\TimeoutException` + - `Exception\Server\ServerException` + - `Exception\Auth\AuthException` + - `Exception\Transport\TransportException` + - `Exception\Retry\RetryExhaustedException` +- Normalizers/parsers: + - `Exception\ExceptionNormalizer` + - `Exception\Data\Parser\ProblemDetailsParser` + - `Exception\Data\Parser\Rfc9457JsonParser` + - `Exception\Data\Parser\LegacyFaultListParser` + - `Exception\Data\Parser\PlainTextParser` + - `Exception\Data\Parser\FaultsNormalizer` + - `Exception\Data\Parser\FieldErrorsNormalizer` +- Additional exception: `SchemaMismatchException` — thrown when PostNL returns a response that does not conform to the SDK schema (required field absent or wrong type); implements `ServerErrorExceptionInterface`; exposes `$targetClass` and `$field` for structured logging. +- Horizontal marker interfaces (catch-by-intent, all extend `PostnlExceptionInterface`): + - `AuthExceptionInterface` — any auth failure (pre-request or HTTP 401/403) + - `TransportExceptionInterface` — pre-response network failure; exposes `getFailureReason(): TransportFailureReason` + - `ClientErrorExceptionInterface` — any HTTP 4xx + - `ServerErrorExceptionInterface` — server-side failure or API schema break +- Log redaction: SDK ships `RedactionRegistry` with default rules — sensitive fields (email, name, postal code, street, house number, label binary) are auto-redacted from all log output; customize via `withRedactionRegistry()` on `ClientBuilder`; disable with `NullRedactionRegistry`. +- Integration boundary rule: + - Catch `PostnlExceptionInterface` at the plugin SDK wrapper boundary. + - Convert SDK exceptions to existing plugin admin/customer error surfaces. + - Log sanitized request/response metadata only; preserve old plugin masking of label binary data in `src/Logger.php`. + +## 13. Auth / Environment Reference + +| Item | Value | Evidence | +|---|---|---| +| API key header | `apikey` | `src/Auth/ApiKeyRequestAuthenticator.php`, `src/Enums/HttpHeader.php`, Postman examples | +| OAuth client credentials support | `Auth::oauthClientCredentials()`, `Postnl::oauthClient()`, `Postnl::sandboxOauthClient()` | `src/Auth/Auth.php`, `src/Client/Postnl.php` | +| OAuth browser PKCE support | Not provided by SDK; plugin implements Fill In With PostNL manually | `postnl-for-woocommerce-org/src/Frontend/Fill_In_With_Postnl_Handler.php` | +| sandbox base URL | `https://api-sandbox.postnl.nl/` | `ClientBuilder::SANDBOX_BASE_URI` | +| production base URL | `https://api.postnl.nl/` | `ClientBuilder::PRODUCTION_BASE_URI` | +| SDK env vars (auth) | `SDK_POSTNL_API_KEY`, `SDK_POSTNL_CLIENT_ID`, `SDK_POSTNL_CLIENT_SECRET`, `SDK_POSTNL_OAUTH_TOKEN_URL`, `SDK_POSTNL_IS_SANDBOX` | `docs/Configuration/README.md` | +| SDK env vars (operational) | `SDK_POSTNL_API_VERSION` (1/4/5, default 4), `SDK_POSTNL_MAX_RETRIES` (default 3; 0 to disable), `SDK_POSTNL_RETRY_DELAY_MS` (default 1000), `SDK_POSTNL_MAX_RETRY_DELAY_MS` (default 10000), `SDK_POSTNL_SOURCE_SYSTEM`, `SDK_POSTNL_CUSTOMER_NUMBER`, `SDK_POSTNL_CUSTOMER_CODE`, `SDK_POSTNL_MIN_LOG_LEVEL`, `SDK_POSTNL_LOGGER_CLASS_PATH` | `docs/Configuration/README.md` | +| SDK env vars (cache) | `SDK_POSTNL_CACHE_STORE_TYPE` (auto/redis/memcached/file/array), `SDK_POSTNL_CACHE_TTL` (default 3600), `SDK_POSTNL_CACHE_PREFIX` (default `sdk_postnl_`), `SDK_POSTNL_REDIS_HOST`/`_PORT`/`_PASSWORD`/`_DATABASE`, `SDK_POSTNL_MEMCACHED_HOST`/`_PORT`, `SDK_POSTNL_FILE_CACHE_DIR` | `docs/Configuration/README.md` | +| where configured | `Auth::fromEnv()`, `Environment::readFactorySecrets()`, `ClientBuilder::withAuth()`, `ClientBuilder::withSandbox()` | `src/Auth/Auth.php`, `src/Config/Environment.php`, `src/Client/ClientBuilder.php` | +| Postman apikey endpoints | All three Postman V4 paths show `apikey`: `/labelconfirm`, `/return/generate`, `/return/activate` | `postnl-docs/PostNL Future Proof V4 API's.postman_collection.json` | +| OAuth requirement per endpoint | Unclear | Sources show apikey examples and SDK OAuth support, but no endpoint-level OAuth requirement matrix | +| HTTP-layer response caching | `CachingPlugin::create(cache, ttl, allowedEndpoints, logger)` registered via `withPlugin()` on `ClientBuilder`; auth headers excluded from cache key; supports per-tenant `keyPrefix`; useful for `/timeframe/` and `/locations/` | `docs/Configuration/README.md` | + +## 14. Developer Checklist + +### SDK integration task checklist + +- Uses a plugin-owned SDK wrapper/boundary, not direct SDK calls scattered through UI/admin classes. +- Builds SDK client with API key from existing plugin settings and correct sandbox flag. +- Catches `PostnlExceptionInterface` at the boundary. +- Logs sanitized request/response data; does not expose API keys, OAuth tokens, label binary content, or PII unnecessarily. +- Preserves existing old-client fallback for flows marked `Old client`, `Hybrid`, `Needs confirmation`, or `Needs mapping`. +- Uses code-exposed SDK method names, not mismatched docs examples. + +### Barcode task checklist + +- Uses `Client::barcode()->generateBarcode(BarcodeRequest)` for V4 barcode calls. +- Maps old `Type`, `Serie`, `Range`, `CustomerCode`, `CustomerNumber` deliberately to V4 `serieStart`, `serieEnd`, `numberOfBarcodes`, `customerCode`, `customerNumber`. +- Keeps old barcode client if barcode range/type mapping is not confirmed. +- Does not log generated barcode batches with credentials. + +### Locations task checklist + +- Verifies the correct facade method name against `src/Client/Client.php` before calling (`locations()` per SDK docs vs. `addressLocations()`/`coordinateLocations()` per prior code inspection). +- Uses `getPickupLocationsByAddress()` for address searches and `getNearPickupLocationsByCoordinates()` for coordinate searches (per SDK docs). +- Maps checkout pickup UI data to `PickUpLocationsResponse` without changing frontend expected fields. +- Preserves `numberOfLocations`, `locationType`, and `pickUpDate` behavior from plugin settings. + +### TimeFrame task checklist + +- Verifies the correct facade method name against `src/Client/Client.php` before calling (`checkout()` per SDK docs vs. `singleTimeframe()`/`multipleTimeframes()` per prior code inspection). +- Uses `getSingleServiceTimeframe()` and `getMultipleServicesTimeframe()` (per SDK docs; `multipleTimeframes()` also appears in one SDK doc example). +- Represents delivery dates through TimeFrame V4 responses. +- Maps old checkout options `Daytime`, `Evening`, `08:00-12:00` only where V4 service fields support them. +- Keeps checkout fee/selection behavior compatible with `src/Frontend/Container.php`. + +### Shipment/Labelling task checklist + +- Uses `shipmentDelivery()->labelConfirm()` for combined label+confirm flow. +- Uses `labelling()->requestLabel()` only when label-only behavior is intended. +- Maps old `ProductCodeDelivery` and `ProductOptions` to V4 `shipmentType`, `services`, `deliveryWindow`, and `returnOptions`. +- Preserves multicollo, insured, signature, delivery code, age check, ADR LQ, pickup, letterbox, EU, and ROW behavior. +- Preserves label output format behavior or documents deliberate SDK enum normalization. + +### Return task checklist + +- Uses `returnShipment()->generateReturn()` for confirmed V4 return-generate behavior. +- Maps return address, return barcode, return period, valuable return, and print method. +- Does not migrate Smart Returns unless replacement behavior is confirmed. +- Keeps old return label behavior where V4 mapping is incomplete. + +### Checkout replacement task checklist + +- Treats checkout replacement as TimeFrame + Locations aggregation. +- Does not assume a standalone `checkout()` SDK service exists. +- Does not assume a standalone DeliveryDate V4 SDK service exists. +- Preserves frontend response shape consumed by delivery-day and pickup-point UI. +- Keeps postal-code validation outside V4. + +### Fallback/old-client task checklist + +- Keeps old client for Fill In With PostNL OAuth browser PKCE flow. +- Keeps or extends old client for activatereturn until V4 SDK extension/request model is confirmed. +- Keeps old client for postal-code check unless SDK extension `GET` behavior is verified against current plugin `POST` behavior. +- Keeps old client for Smart Returns until `return/generate` equivalence is confirmed. +- Provides explicit logging that identifies whether SDK or old client handled the request, without secrets. + +## 15. Source Index + +| Source type | Important files | +|---|---| +| Postman collection file | `postnl-docs/PostNL Future Proof V4 API's.postman_collection.json` | +| SDK composer.json | `postnl-sdk-audit/vendor/postnl/api-client-sdk/composer.json` | +| SDK main client/factory files | `src/Client/Postnl.php`; `src/Client/Client.php`; `src/Client/PostnlClientInterface.php`; `src/Client/ClientBuilder.php`; `src/Service/ServiceFactory.php`; `src/Service/ServiceContext.php` | +| SDK service files | `src/Service/ShipmentDelivery/ShipmentDelivery.php`; `src/Service/ReturnShipment/ReturnShipment.php`; `src/Service/Labelling/Labelling.php`; `src/Service/Confirming/Confirming.php`; `src/Service/Barcode/Barcode.php`; `src/Service/NearAddressPickupLocations/NearAddressPickupLocations.php`; `src/Service/NearCoordinatesPickupLocations/NearCoordinatesPickupLocations.php`; `src/Service/SingleServiceTimeframe/SingleServiceTimeframe.php`; `src/Service/MultipleServicesTimeframe/MultipleServicesTimeframe.php`; `src/Service/Checkout/V1/Extension/PostalCodeCheckExtension.php` | +| SDK request/response folders | `src/RequestData/V4`; `src/Service/*/V4/Request`; `src/Service/*/V4/Response`; `src/ResponseData/V4`; `src/ResponseData/V1` | +| SDK docs folders | `docs/Barcode`; `docs/ShipmentDelivery`; `docs/Labelling`; `docs/Confirming`; `docs/ReturnShipment`; `docs/Locations`; `docs/TimeFrame`; `docs/Extension`; `docs/ErrorHandling`; `docs/Configuration` | +| plugin current API client files | `postnl-for-woocommerce-org/src/Rest_API/Base.php`; `src/Rest_API/Barcode/Client.php`; `src/Rest_API/Shipping/Client.php`; `src/Rest_API/Return_Label/Client.php`; `src/Rest_API/Letterbox/Client.php`; `src/Rest_API/Checkout/Client.php`; `src/Rest_API/Postcode_Check/Client.php`; `src/Rest_API/Smart_Returns/Client.php`; `src/Rest_API/Shipment_and_Return/Client.php` | +| plugin checkout/frontend files | `src/Frontend/Container.php`; `src/Frontend/Delivery_Day.php`; `src/Frontend/Dropoff_Points.php`; `src/Checkout_Blocks/Extend_Store_Endpoint.php`; `src/Checkout_Blocks/Extend_Block_Core.php` | +| plugin Fill In With PostNL files | `src/Frontend/Fill_In_With_Postnl.php`; `src/Frontend/Fill_In_With_Postnl_Handler.php`; `src/Shipping_Method/Fill_In_With_PostNL_Settings.php` | +| plugin mapping/settings files | `src/Helper/Mapping.php`; `src/Shipping_Method/Settings.php`; `src/Utils.php`; `src/Logger.php` | diff --git a/docs/postnl-v4-migration/sources/PostNL Future Proof V4 API's.postman_collection.json b/docs/postnl-v4-migration/sources/PostNL Future Proof V4 API's.postman_collection.json new file mode 100644 index 00000000..5ef7c848 --- /dev/null +++ b/docs/postnl-v4-migration/sources/PostNL Future Proof V4 API's.postman_collection.json @@ -0,0 +1,3253 @@ +{ + "info": { + "_postman_id": "1a5e0d26-8316-4e7d-b2d0-ca616ac410e5", + "name": "PostNL Future Proof V4 API's", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "10568351" + }, + "item": [ + { + "name": "PostNL Shipment API", + "item": [ + { + "name": "Shipment to NL", + "item": [ + { + "name": "Parcel NL", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"returnBarcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": \"bis\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\",\r\n \"houseNumberAddition\": \"bis\",\r\n \"addressLine\": null\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL LiB", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"returnBarcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": \"bis\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\",\r\n \"houseNumberAddition\": \"bis\",\r\n \"addressLine\": null\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"returnOptions\": {\r\n \"labelType\": \"labelinthebox\",\r\n \"returnBarcode\": \"{{returnBarcode}}\",\r\n \"returnAddress\": { \r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\"\r\n }\r\n },\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\"\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL With Dimensions", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": \"bis\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"1122334455\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\",\r\n \"houseNumberAddition\": \"bis\",\r\n \"addressLine\": null\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n \"dimensions\": {\r\n\t\t\t\t\"height\": 1000,\r\n\t\t\t\t\"length\": 1000,\r\n\t\t\t\t\"volume\": 1000,\r\n\t\t\t\t\"weight\": 1000,\r\n\t\t\t\t\"width\": 1000\r\n\t\t\t},\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Multicollo", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcode2\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": null,\r\n \"addressLine\": null\r\n },\r\n \"type\": \"consumer\"\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\",\r\n \"houseNumberAddition\": \"bis\",\r\n \"addressLine\": null\r\n }\r\n },\r\n \"itemCount\": 2,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer1\",\r\n\t\t\t\t\"shipmentReference\": \"Reference1\"\r\n\t\t\t}\r\n },\r\n {\r\n \"barcode\": \"{{barcode2}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer2\",\r\n\t\t\t\t\"shipmentReference\": \"Reference2\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Insured", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": \"bis\",\r\n \"addressLine\": null\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\",\r\n\t\t\t\"addressLine\": null\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"insuredValue\": 250\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Insured/ReturnWhenNotHome", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\",\r\n\t\t\t\"addressLine\": null\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"insuredValue\": 250,\r\n \"returnWhenNotHome\":true\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Stated Address Only", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": \"bis\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\",\r\n\t\t\t\"addressLine\": null\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"statedAddressOnly\": true\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Signature on Delivery", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"deliveryConfirmation\":\"signature\"\r\n }\r\n \r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Signature on Delivery/Stated Address Only", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": null,\r\n \"addressLine\": null\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\",\r\n \"houseNumberAddition\": \"bis\",\r\n \"addressLine\": null\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"deliveryConfirmation\":\"signature\"\r\n }\r\n \r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Signature on Delivery/ReturnWhenNotHome", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\",\r\n \"houseNumberAddition\": \"bis\",\r\n \"addressLine\": null\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"deliveryConfirmation\":\"signature\",\r\n \"returnWhenNotHome\":true\r\n }\r\n \r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Signature On Delivery/ReturnWhenNotHome/StatedAddressOnly", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"returnWhenNotHome\":true,\r\n \"statedAddressOnly\": true,\r\n \"deliveryConfirmation\": \"signature\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL deliveryCode on Delivery", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"mobileNumber\": \"0612345678\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"insuredValue\": 250,\r\n \"deliveryConfirmation\":\"deliveryCode\"\r\n }\r\n \r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Return When Not Home", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": \"bis\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"returnWhenNotHome\": true\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Return When Not Home/Stated Address Only", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": \"bis\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"returnWhenNotHome\": true,\r\n \"statedAddressOnly\": true\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL ADR Low Quantity", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"adrlq\": true\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL AgeCheck 16+", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": \"bis\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"minimalAgeCheck\": \"16+\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL AgeCheck 18+", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\",\r\n \"houseNumberAddition\": \"bis\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"minimalAgeCheck\": \"18+\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL AgeCheck 18+ / Insured", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"insuredValue\": 250,\r\n \"minimalAgeCheck\": \"18+\"\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL AgeCheck 18+ / Insured / ReturnWhenNotHome", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"insuredValue\": 250,\r\n \"minimalAgeCheck\": \"18+\",\r\n \"returnWhenNotHome\":true\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL AgeCheck 18+ / ReturnWhenNotHome", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"minimalAgeCheck\": \"18+\",\r\n \"returnWhenNotHome\":true\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Guaranteed Before 10:00", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\",\r\n\t\t\t\"addressLine\": null\r\n\t\t}\r\n },\r\n \"handOverDate\": \"{{currentdate}}\",\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"deliveryWindow\": {\r\n \"guaranteedBefore\": \"10:00\"\r\n }\r\n }\r\n}\r\n\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Guaranteed Before 12:00", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"handOverDate\": \"{{currentdate}}\",\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"deliveryWindow\": {\r\n \"guaranteedBefore\": \"12:00\"\r\n }\r\n }\r\n}\r\n\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Guaranteed Before 17:00", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"handOverDate\": \"{{currentdate}}\",\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"deliveryWindow\": {\r\n \"guaranteedBefore\": \"17:00\"\r\n }\r\n }\r\n}\r\n\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Evening Delivery", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"handOverDate\": \"{{currentdate}}\",\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"deliveryWindow\": {\r\n \"service\": \"evening\"\r\n }\r\n }\r\n}\r\n\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Pickup at PostNL Location", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"deliveryLocation\": {\r\n \"pickupLocationId\": \"402753\"\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Pickup at PostNL Location / APL", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"deliveryLocation\": {\r\n \"pickUpLocationId\": \"219346\"\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Pickup at PostNL Location / Insured", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"deliveryLocation\": {\r\n \"pickupLocationId\": \"402753\"\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"insuredValue\": 250\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL Pickup at PostNL Location / Insured / AgeCheck 18+", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"deliveryLocation\": {\r\n \"pickupLocationId\": \"402753\"\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"insuredValue\": 250,\r\n \"minimalAgeCheck\": \"18+\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Letterbox Parcel NL", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n \"customerReferences\": {\r\n \"costCenter\": \"Factuurnummer\",\r\n \"shipmentReference\": \"Reference\"\r\n }\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"letterbox\",\r\n \"services\": {\r\n\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Letterbox Parcel NL 48hours", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n \"customerReferences\": {\r\n \"costCenter\": \"Factuurnummer\",\r\n \"shipmentReference\": \"Reference\"\r\n }\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"letterbox\",\r\n \"services\": {\r\n \"deliveryWindow\": {\r\n \"duration\": \"non24hours\"\r\n }\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel BE to NL", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n \"address\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"2\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Teststraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n\r\n }\r\n}\r\n", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Shipment NL to BE", + "item": [ + { + "name": "Parcel NL to BE", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Brussel\",\r\n \"countryIso\": \"BE\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"5\",\r\n \"postalCode\": \"0612\",\r\n \"street\": \"Dorpsstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL to BE Stated Address Only", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Brussel\",\r\n \"countryIso\": \"BE\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"5\",\r\n \"postalCode\": \"0612\",\r\n \"street\": \"Dorpsstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"statedAddressOnly\": true\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL to BE Signature on Delivery", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Brussel\",\r\n \"countryIso\": \"BE\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"5\",\r\n \"postalCode\": \"0612\",\r\n \"street\": \"Dorpsstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"deliveryConfirmation\": \"signature\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel NL to BE Extra Cover", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Brussel\",\r\n \"countryIso\": \"BE\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"5\",\r\n \"postalCode\": \"0612\",\r\n \"street\": \"Dorpsstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"insuredValue\": 250\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Letterbox NL to BE", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Brussel\",\r\n \"countryIso\": \"BE\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"5\",\r\n \"postalCode\": \"0612\",\r\n \"street\": \"Dorpsstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"letterbox\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Shipment BE to BE", + "item": [ + { + "name": "Parcel BE to BE", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Brussel\",\r\n \"countryIso\": \"BE\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"5\",\r\n \"postalCode\": \"0612\",\r\n \"street\": \"Dorpsstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Brussel\",\r\n \"countryIso\": \"BE\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"5\",\r\n \"postalCode\": \"0612\",\r\n \"street\": \"Dorpsstraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel BE to BE Stated Address Only", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Brussel\",\r\n \"countryIso\": \"BE\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"5\",\r\n \"postalCode\": \"0612\",\r\n \"street\": \"Dorpsstraat\"\r\n }\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Brussel\",\r\n \"countryIso\": \"BE\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"5\",\r\n \"postalCode\": \"0612\",\r\n \"street\": \"Dorpsstraat\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcode}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\",\r\n \"pageOrientation\": \"portrait\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"services\": {\r\n \"statedAddressOnly\": true\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Shipment to EU", + "item": [ + { + "name": "Parcel EU Track And Trace", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(100000000,999999999)+\"NL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Wenen\",\r\n \"countryIso\": \"AT\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"1100\",\r\n \"street\": \"WaldorfStrasse\"\r\n },\r\n \"type\": \"consumer\"\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\",\r\n\t\t\t\"addressLine\": null\r\n\t\t}\r\n },\r\n \"returnOptions\": {\r\n \"returnAddress\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcodeEU}}\",\r\n \"dimensions\": {\r\n\t\t\t \"weight\": 100\r\n\t\t },\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"internationalShipmentData\": {\r\n \"bundle\": \"track_trace\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel EU Insured", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(100000000,999999999)+\"NL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Wenen\",\r\n \"countryIso\": \"AT\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"1100\",\r\n \"street\": \"WaldorfStrasse\"\r\n },\r\n \"type\": \"consumer\"\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\",\r\n\t\t\t\"addressLine\": null\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcodeEU}}\",\r\n \"dimensions\": {\r\n\t\t\t \"weight\": 100\r\n\t\t },\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"internationalShipmentData\": {\r\n \"bundle\": \"insured\"\r\n },\r\n \"services\": {}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel EU Insured Plus", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(100000000,999999999)+\"NL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Wenen\",\r\n \"countryIso\": \"AT\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"1100\",\r\n \"street\": \"WaldorfStrasse\"\r\n },\r\n \"type\": \"consumer\"\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\"\r\n\t\t}\r\n },\r\n \"returnOptions\": {\r\n \"returnAddress\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcodeEU}}\",\r\n \"dimensions\": {\r\n\t\t\t \"weight\": 100\r\n\t\t },\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\"\r\n },\r\n \"shipmentType\": \"parcel\",\r\n \"internationalShipmentData\": {\r\n \"bundle\": \"insured_plus\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Packet EU Untracked", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(100000000,999999999)+\"NL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Paris\",\r\n \"countryIso\": \"FR\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"75001\",\r\n \"street\": \"Rue Napoleon\"\r\n },\r\n \"type\": \"consumer\"\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n },\r\n \"returnOptions\": {\r\n \"returnAddress\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"LA92645068XNL\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\"\r\n },\r\n \"shipmentType\": \"packet\",\r\n \"services\": {\r\n \r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Packet EU Track and Trace", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(100000000,999999999)+\"NL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Paris\",\r\n \"countryIso\": \"FR\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"75001\",\r\n \"street\": \"Rue Napoleon\"\r\n },\r\n \"type\": \"consumer\"\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"LA22645068XNL\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\"\r\n },\r\n \"shipmentType\": \"packet\",\r\n \"internationalShipmentData\": {\r\n \"bundle\": \"track_trace\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Letterbox EU Untracked", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(10000000,99999999)+\"XNL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Paris\",\r\n \"countryIso\": \"FR\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"75001\",\r\n \"street\": \"Rue Napoleon\"\r\n },\r\n \"type\": \"consumer\"\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n },\r\n \"returnOptions\": {\r\n \"returnAddress\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcodeROW}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\"\r\n },\r\n \"shipmentType\": \"letterbox\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Letterbox EU Track and Trace", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(10000000,99999999)+\"XNL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"receiver\": {\r\n \"contact\": {\r\n \"firstName\": \"Test\",\r\n \"lastName\": \"Persoon\",\r\n \"email\": \"test.persoon@postnl.nl\",\r\n \"language\": \"NL\",\r\n \"mobileNumber\": \"0612345678\"\r\n },\r\n \"address\": {\r\n \"city\": \"Paris\",\r\n \"countryIso\": \"FR\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"75001\",\r\n \"street\": \"Rue Napoleon\"\r\n },\r\n \"type\": \"consumer\"\r\n },\r\n \"sender\": {\r\n \"customerNumber\": \"11223344\",\r\n \"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n },\r\n \"returnOptions\": {\r\n \"returnAddress\": {\r\n \"city\": \"Den Haag\",\r\n \"countryIso\": \"NL\",\r\n \"companyName\": \"TestBedrijf\",\r\n \"houseNumber\": \"3\",\r\n \"postalCode\": \"2521CA\",\r\n \"street\": \"Waldorpstraat\"\r\n }\r\n },\r\n \"itemCount\": 1,\r\n \"items\": [\r\n {\r\n \"barcode\": \"{{barcodeROW}}\",\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n }\r\n ],\r\n \"labelSettings\": {\r\n \"outputType\": \"PDF\"\r\n },\r\n \"shipmentType\": \"letterbox\",\r\n \"internationalShipmentData\": {\r\n \"bundle\": \"track_trace\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "Shipment to ROW", + "item": [ + { + "name": "Parcel ROW Track And Trace", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(10000000,99999999)+\"XNL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"receiver\": {\r\n\t\t\"contact\": {\r\n\t\t\t\"firstName\": \"Internationaal\",\r\n\t\t\t\"lastName\": \"Persoon\",\r\n\t\t\t\"email\": \"internationaal.persoon@postnl.nl\",\r\n\t\t\t\"language\": \"NL\",\r\n\t\t\t\"mobileNumber\": \"+18175606892\"\r\n\t\t},\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Gonderange\",\r\n\t\t\t\"countryIso\": \"CN\",\r\n\t\t\t\"companyName\": \"Bedrijfsnaam\",\r\n\t\t\t\"houseNumber\": \"50\",\r\n\t\t\t\"postalCode\": \"254863\",\r\n\t\t\t\"street\": \"Op der Tonn\",\r\n \"departmentName\": \"Afdeling\",\r\n\t\t\t\"internationalAddressData\": {\r\n\t\t\t\t\"area\": \"AREA\",\r\n\t\t\t\t\"buildingName\": \"Buildingname\",\r\n\t\t\t\t\"doorcode\": \"Doorcode\",\r\n\t\t\t\t\"floor\": \"Floor\",\r\n\t\t\t\t\"region\": \"Regio\"\r\n\t\t\t}\r\n\t\t},\r\n\t\t\"type\": \"consumer\"\r\n\t},\r\n\t\"sender\": {\r\n\t\t\"customerNumber\": \"11223344\",\r\n\t\t\"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n\t},\r\n\t\"itemCount\": 1,\r\n\t\"items\": [\r\n\t\t{\r\n\t\t\t\"barcode\": \"{{barcodeROW}}\",\r\n\t\t\t\"dimensions\": {\r\n\t\t\t\t\"weight\": 1000\r\n\t\t\t},\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n\t\t}\r\n\t],\r\n\t\"labelSettings\": {\r\n\t\t\"outputType\": \"PDF\",\r\n \"resolution\": 200\r\n\t},\r\n\t\"shipmentType\": \"parcel\",\r\n\t\"internationalShipmentData\": {\r\n\t\t\"pudo\": false,\r\n\t\t\"customs\": {\r\n\t\t\t\"content\": [\r\n\t\t\t\t{\r\n\t\t\t\t\t\"description\": \"Omschrijving 1\",\r\n\t\t\t\t\t\"quantity\": 1,\r\n\t\t\t\t\t\"weight\": 100,\r\n\t\t\t\t\t\"value\": 100,\r\n\t\t\t\t\t\"countryOfOrigin\": \"FR\",\r\n\t\t\t\t\t\"hsTariffNumber\": \"11134823\"\r\n\t\t\t\t}\r\n\t\t\t],\r\n\t\t\t\"transactionCode\": \"11\",\r\n\t\t\t\"currency\": \"EUR\",\r\n\t\t\t\"handleAsNonDeliverable\": false,\r\n\t\t\t\"associatedDocument\": {\r\n\t\t\t\t\"type\": \"certificate\",\r\n\t\t\t\t\"number\": \"1234567\"\r\n\t\t\t},\r\n\t\t\t\"senderIdentification\": \"Ik ben trustedShipperID\",\r\n\t\t\t\"receiverIdentification\": \"Ik ben importerReferenceCd\"\r\n\t\t},\r\n\t\t\"bundle\": \"track_trace\"\r\n\t}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel ROW Insured", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(10000000,99999999)+\"XNL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"receiver\": {\r\n\t\t\"contact\": {\r\n\t\t\t\"firstName\": \"Internationaal\",\r\n\t\t\t\"lastName\": \"Persoon\",\r\n\t\t\t\"mobileNumber\": \"0612345678\",\r\n\t\t\t\"email\": \"internationaal.persoon@postnl.nl\",\r\n\t\t\t\"language\": \"NL\"\r\n\t\t},\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Gonderange\",\r\n\t\t\t\"countryIso\": \"CN\",\r\n\t\t\t\"companyName\": \"Bedrijfsnaam\",\r\n\t\t\t\"houseNumber\": \"50\",\r\n\t\t\t\"postalCode\": \"254863\",\r\n\t\t\t\"street\": \"Op der Tonn\",\r\n \"departmentName\": \"Afdeling\",\r\n\t\t\t\"internationalAddressData\": {\r\n\t\t\t\t\"area\": \"AREA\",\r\n\t\t\t\t\"buildingName\": \"Buildingname\",\r\n\t\t\t\t\"doorcode\": \"Doorcode\",\r\n\t\t\t\t\"floor\": \"Floor\",\r\n\t\t\t\t\"region\": \"Regio\"\r\n\t\t\t}\r\n\t\t},\r\n\t\t\"type\": \"consumer\"\r\n\t},\r\n\t\"sender\": {\r\n\t\t\"customerNumber\": \"11223344\",\r\n\t\t\"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n\t},\r\n\t\"itemCount\": 1,\r\n\t\"items\": [\r\n\t\t{\r\n\t\t\t\"barcode\": \"{{barcodeROW}}\",\r\n\t\t\t\"dimensions\": {\r\n\t\t\t\t\"weight\": 1000\r\n\t\t\t},\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n\t\t}\r\n\t],\r\n\t\"labelSettings\": {\r\n\t\t\"outputType\": \"PDF\",\r\n \"resolution\": 200\r\n\t},\r\n\t\"shipmentType\": \"parcel\",\r\n\t\"internationalShipmentData\": {\r\n\t\t\"pudo\": false,\r\n\t\t\"customs\": {\r\n\t\t\t\"content\": [\r\n\t\t\t\t{\r\n\t\t\t\t\t\"description\": \"Omschrijving 1\",\r\n\t\t\t\t\t\"quantity\": 1,\r\n\t\t\t\t\t\"weight\": 100,\r\n\t\t\t\t\t\"value\": 100,\r\n\t\t\t\t\t\"countryOfOrigin\": \"FR\",\r\n\t\t\t\t\t\"hsTariffNumber\": \"11134823\"\r\n\t\t\t\t}\r\n\t\t\t],\r\n\t\t\t\"transactionCode\": \"11\",\r\n\t\t\t\"currency\": \"EUR\",\r\n\t\t\t\"handleAsNonDeliverable\": false,\r\n\t\t\t\"associatedDocument\": {\r\n\t\t\t\t\"type\": \"certificate\",\r\n\t\t\t\t\"number\": \"1234567\"\r\n\t\t\t},\r\n\t\t\t\"senderIdentification\": \"Ik ben trustedShipperID\",\r\n\t\t\t\"receiverIdentification\": \"Ik ben importerReferenceCd\"\r\n\t\t},\r\n\t\t\"bundle\": \"insured\"\r\n\t}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Parcel ROW Insured Plus", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(10000000,99999999)+\"XNL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"receiver\": {\r\n\t\t\"contact\": {\r\n\t\t\t\"firstName\": \"Internationaal\",\r\n\t\t\t\"lastName\": \"Persoon\",\r\n\t\t\t\"mobileNumber\": \"0612345678\",\r\n\t\t\t\"email\": \"internationaal.persoon@postnl.nl\",\r\n\t\t\t\"language\": \"NL\"\r\n\t\t},\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Gonderange\",\r\n\t\t\t\"countryIso\": \"CN\",\r\n\t\t\t\"companyName\": \"Bedrijfsnaam\",\r\n\t\t\t\"houseNumber\": \"50\",\r\n\t\t\t\"postalCode\": \"254863\",\r\n\t\t\t\"street\": \"Op der Tonn\",\r\n \"departmentName\": \"Afdeling\",\r\n\t\t\t\"internationalAddressData\": {\r\n\t\t\t\t\"area\": \"AREA\",\r\n\t\t\t\t\"buildingName\": \"Buildingname\",\r\n\t\t\t\t\"doorcode\": \"Doorcode\",\r\n\t\t\t\t\"floor\": \"Floor\",\r\n\t\t\t\t\"region\": \"Regio\"\r\n\t\t\t}\r\n\t\t},\r\n\t\t\"type\": \"consumer\"\r\n\t},\r\n\t\"sender\": {\r\n\t\t\"customerNumber\": \"11223344\",\r\n\t\t\"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n\t},\r\n\t\"itemCount\": 1,\r\n\t\"items\": [\r\n\t\t{\r\n\t\t\t\"barcode\": \"{{barcodeROW}}\",\r\n\t\t\t\"dimensions\": {\r\n\t\t\t\t\"weight\": 1000\r\n\t\t\t},\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n\t\t}\r\n\t],\r\n\t\"labelSettings\": {\r\n\t\t\"outputType\": \"PDF\",\r\n \"resolution\": 200\r\n\t},\r\n\t\"shipmentType\": \"parcel\",\r\n\t\"internationalShipmentData\": {\r\n\t\t\"pudo\": false,\r\n\t\t\"customs\": {\r\n\t\t\t\"content\": [\r\n\t\t\t\t{\r\n\t\t\t\t\t\"description\": \"Omschrijving 1\",\r\n\t\t\t\t\t\"quantity\": 1,\r\n\t\t\t\t\t\"weight\": 100,\r\n\t\t\t\t\t\"value\": 100,\r\n\t\t\t\t\t\"countryOfOrigin\": \"FR\",\r\n\t\t\t\t\t\"hsTariffNumber\": \"11134823\"\r\n\t\t\t\t}\r\n\t\t\t],\r\n\t\t\t\"transactionCode\": \"11\",\r\n\t\t\t\"currency\": \"EUR\",\r\n\t\t\t\"handleAsNonDeliverable\": false,\r\n\t\t\t\"associatedDocument\": {\r\n\t\t\t\t\"type\": \"certificate\",\r\n\t\t\t\t\"number\": \"1234567\"\r\n\t\t\t},\r\n\t\t\t\"senderIdentification\": \"Ik ben trustedShipperID\",\r\n\t\t\t\"receiverIdentification\": \"Ik ben importerReferenceCd\"\r\n\t\t},\r\n\t\t\"bundle\": \"insured_plus\"\r\n\t}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Letterbox ROW Untracked", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(10000000,99999999)+\"XNL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"receiver\": {\r\n\t\t\"contact\": {\r\n\t\t\t\"firstName\": \"Internationaal\",\r\n\t\t\t\"lastName\": \"Persoon\",\r\n\t\t\t\"mobileNumber\": \"0612345678\",\r\n\t\t\t\"email\": \"internationaal.persoon@postnl.nl\",\r\n\t\t\t\"language\": \"NL\",\r\n\t\t\t\"mobileNumber\": \"+18175606892\"\r\n\t\t},\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Gonderange\",\r\n\t\t\t\"countryIso\": \"CN\",\r\n\t\t\t\"companyName\": \"Bedrijfsnaam\",\r\n\t\t\t\"houseNumber\": \"50\",\r\n\t\t\t\"postalCode\": \"254863\",\r\n\t\t\t\"street\": \"Op der Tonn\",\r\n \"departmentName\": \"Afdeling\",\r\n\t\t\t\"internationalAddressData\": {\r\n\t\t\t\t\"area\": \"AREA\",\r\n\t\t\t\t\"buildingName\": \"Buildingname\",\r\n\t\t\t\t\"doorcode\": \"Doorcode\",\r\n\t\t\t\t\"floor\": \"Floor\",\r\n\t\t\t\t\"region\": \"Regio\"\r\n\t\t\t}\r\n\t\t}\r\n\t},\r\n\t\"sender\": {\r\n\t\t\"customerNumber\": \"11223344\",\r\n\t\t\"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n\t},\r\n\t\"itemCount\": 1,\r\n\t\"items\": [\r\n\t\t{\r\n\t\t\t\"barcode\": \"{{barcodeROW}}\",\r\n\t\t\t\"dimensions\": {\r\n\t\t\t\t\"weight\": 100\r\n\t\t\t},\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n\t\t}\r\n\t],\r\n\t\"labelSettings\": {\r\n\t\t\"outputType\": \"PDF\",\r\n \"resolution\": 200\r\n\t},\r\n\t\"shipmentType\": \"letterbox\",\r\n\t\"internationalShipmentData\": {\r\n\t\t\"pudo\": false,\r\n\t\t\"customs\": {\r\n\t\t\t\"content\": [\r\n\t\t\t\t{\r\n\t\t\t\t\t\"description\": \"Omschrijving 1\",\r\n\t\t\t\t\t\"quantity\": 1,\r\n\t\t\t\t\t\"weight\": 100,\r\n\t\t\t\t\t\"value\": 100,\r\n\t\t\t\t\t\"countryOfOrigin\": \"FR\",\r\n\t\t\t\t\t\"hsTariffNumber\": \"11134823\"\r\n\t\t\t\t}\r\n\t\t\t],\r\n\t\t\t\"transactionCode\": \"11\",\r\n\t\t\t\"currency\": \"EUR\",\r\n\t\t\t\"handleAsNonDeliverable\": false,\r\n\t\t\t\"associatedDocument\": {\r\n\t\t\t\t\"type\": \"certificate\",\r\n\t\t\t\t\"number\": \"1234567\"\r\n\t\t\t},\r\n\t\t\t\"senderIdentification\": \"Ik ben trustedShipperID\",\r\n\t\t\t\"receiverIdentification\": \"Ik ben importerReferenceCd\"\r\n\t\t}\r\n\t}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + }, + { + "name": "Letterbox Track and Trace", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "const lodash = require('lodash');\r", + "pm.collectionVariables.set(\"barcode\", \"3SDEVC\"+lodash.random(100000000,999999999));\r", + "pm.collectionVariables.set(\"barcodeEU\", \"3SDEVC\"+lodash.random(1000000,9999999));\r", + "pm.collectionVariables.set(\"barcodeROW\", \"LA\"+lodash.random(10000000,99999999)+\"XNL\");" + ], + "type": "text/javascript", + "packages": {}, + "requests": {} + } + } + ], + "request": { + "method": "POST", + "header": [ + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"receiver\": {\r\n\t\t\"contact\": {\r\n\t\t\t\"firstName\": \"Internationaal\",\r\n\t\t\t\"lastName\": \"Persoon\",\r\n\t\t\t\"mobileNumber\": \"0612345678\",\r\n\t\t\t\"email\": \"internationaal.persoon@postnl.nl\",\r\n\t\t\t\"language\": \"NL\",\r\n\t\t\t\"mobileNumber\": \"+18175606892\"\r\n\t\t},\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Gonderange\",\r\n\t\t\t\"countryIso\": \"CN\",\r\n\t\t\t\"companyName\": \"Bedrijfsnaam\",\r\n\t\t\t\"houseNumber\": \"50\",\r\n\t\t\t\"postalCode\": \"254863\",\r\n\t\t\t\"street\": \"Op der Tonn\",\r\n \"departmentName\": \"Afdeling\",\r\n\t\t\t\"internationalAddressData\": {\r\n\t\t\t\t\"area\": \"AREA\",\r\n\t\t\t\t\"buildingName\": \"Buildingname\",\r\n\t\t\t\t\"doorcode\": \"Doorcode\",\r\n\t\t\t\t\"floor\": \"Floor\",\r\n\t\t\t\t\"region\": \"Regio\"\r\n\t\t\t}\r\n\t\t}\r\n\t},\r\n\t\"sender\": {\r\n\t\t\"customerNumber\": \"11223344\",\r\n\t\t\"customerCode\": \"DEVC\",\r\n\t\t\"address\": {\r\n\t\t\t\"city\": \"Den Haag\",\r\n\t\t\t\"countryIso\": \"NL\",\r\n\t\t\t\"houseNumber\": \"2\",\r\n\t\t\t\"postalCode\": \"2521CA\",\r\n\t\t\t\"street\": \"Teststraat\",\r\n\t\t\t\"houseNumberAddition\": \"bis\"\r\n\t\t}\r\n\t},\r\n\t\"itemCount\": 1,\r\n\t\"items\": [\r\n\t\t{\r\n\t\t\t\"barcode\": \"{{barcodeROW}}\",\r\n\t\t\t\"dimensions\": {\r\n\t\t\t\t\"weight\": 100\r\n\t\t\t},\r\n\t\t\t\"customerReferences\": {\r\n\t\t\t\t\"costCenter\": \"Factuurnummer\",\r\n\t\t\t\t\"shipmentReference\": \"Reference\"\r\n\t\t\t}\r\n\t\t}\r\n\t],\r\n\t\"labelSettings\": {\r\n\t\t\"outputType\": \"PDF\",\r\n \"resolution\": 200\r\n\t},\r\n\t\"shipmentType\": \"letterbox\",\r\n\t\"internationalShipmentData\": {\r\n\t\t\"pudo\": false,\r\n\t\t\"customs\": {\r\n\t\t\t\"content\": [\r\n\t\t\t\t{\r\n\t\t\t\t\t\"description\": \"Omschrijving 1\",\r\n\t\t\t\t\t\"quantity\": 1,\r\n\t\t\t\t\t\"weight\": 100,\r\n\t\t\t\t\t\"value\": 100,\r\n\t\t\t\t\t\"countryOfOrigin\": \"FR\",\r\n\t\t\t\t\t\"hsTariffNumber\": \"11134823\"\r\n\t\t\t\t}\r\n\t\t\t],\r\n\t\t\t\"transactionCode\": \"11\",\r\n\t\t\t\"currency\": \"EUR\",\r\n\t\t\t\"handleAsNonDeliverable\": false,\r\n\t\t\t\"associatedDocument\": {\r\n\t\t\t\t\"type\": \"certificate\",\r\n\t\t\t\t\"number\": \"1234567\"\r\n\t\t\t},\r\n\t\t\t\"senderIdentification\": \"Ik ben trustedShipperID\",\r\n\t\t\t\"receiverIdentification\": \"Ik ben importerReferenceCd\"\r\n\t\t},\r\n \"bundle\": \"track_trace\"\r\n\t}\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/labelconfirm", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "labelconfirm" + ] + } + }, + "response": [] + } + ] + } + ] + }, + { + "name": "PostNL Return API", + "item": [ + { + "name": "NL-NL Single Label", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"receiver\": {\n \"customerNumber\": \"1122334455\",\n \"customerCode\": \"DEVC\",\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"companyName\": \"PostNL\",\n \"houseNumber\": \"3\",\n \"postalCode\": \"2532 CA\",\n \"street\": \"Waldorpstraat\"\n }\n },\n \"sender\": {\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2352 CA\",\n \"street\": \"Waldorpstraat\"\n },\n \"contact\": {\n \"email\": \"verzendemail@postnl.nl\",\n \"firstName\": \"Test\",\n \"lastName\": \"Verzender\",\n \"language\": \"NL\",\n \"mobileNumber\": \"+31687654321\"\n }\n },\n \"labelSettings\": {\n \"outputType\": \"pdf\",\n \"printMethod\": \"consumerPrint\"\n },\n \"returnOptions\": {\n \"domestic\": {\n \"returnPeriod\": 20,\n \"valuableReturn\": false\n }\n },\n \"items\": [\n {\n \"barcode\": \"3SDEVC123456789\",\n \"customerReferences\": {\n \"shipmentReference\": \"shipmentReference\",\n \"costCenter\": \"costCenter\",\n \"returnReference\": \"returnReference\"\n }\n }\n ]\n}", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/generate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "generate" + ] + }, + "description": "Create a return shipment with the provided details, it will return generated labels" + }, + "response": [] + }, + { + "name": "NL-BE Single Label", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"receiver\": {\n \"customerNumber\": \"1122334455\",\n \"customerCode\": \"DEVC\",\n \"address\": {\n \"countryIso\": \"BE\",\n \"city\": \"Brussel\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2050\",\n \"street\": \"Brusselstraat\"\n }\n },\n \"sender\": {\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2352 CA\",\n \"street\": \"Waldorpstraat\"\n },\n \"contact\": {\n \"email\": \"verzendemail@postnl.nl\",\n \"firstName\": \"Test\",\n \"lastName\": \"Verzender\",\n \"language\": \"NL\",\n \"mobileNumber\": \"+31687654321\"\n }\n },\n \"labelSettings\": {\n \"outputType\": \"pdf\",\n \"printMethod\": \"consumerPrint\"\n },\n \"returnOptions\": {\n \"domestic\": {\n \"returnPeriod\": 20,\n \"valuableReturn\": false\n }\n },\n \"items\": [\n {\n \"barcode\": \"3SDEVC987654321\",\n \"customerReferences\": {\n \"shipmentReference\": \"shipmentReference\",\n \"costCenter\": \"costCenter\",\n \"returnReference\": \"returnReference\"\n }\n }\n ]\n}", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/generate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "generate" + ] + }, + "description": "Create a return shipment with the provided details, it will return generated labels" + }, + "response": [] + }, + { + "name": "BE-NL Single Label", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"receiver\": {\n \"customerNumber\": \"1122334455\",\n \"customerCode\": \"DEVC\",\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"companyName\": \"PostNL\",\n \"houseNumber\": \"3\",\n \"postalCode\": \"2532 CA\",\n \"street\": \"Waldorpstraat\"\n }\n },\n \"sender\": {\n \"address\": {\n \"countryIso\": \"BE\",\n \"city\": \"Brussel\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"20500\",\n \"street\": \"Brusselstraat\"\n },\n \"contact\": {\n \"email\": \"verzendemail@postnl.nl\",\n \"firstName\": \"Test\",\n \"lastName\": \"Verzender\",\n \"language\": \"NL\",\n \"mobileNumber\": \"+31687654321\"\n }\n },\n \"labelSettings\": {\n \"outputType\": \"pdf\",\n \"printMethod\": \"consumerPrint\"\n },\n \"returnOptions\": {\n \"domestic\": {\n \"returnPeriod\": 20,\n \"valuableReturn\": false\n }\n },\n \"items\": [\n {\n \"barcode\": \"3SDEVC654123789\",\n \"customerReferences\": {\n \"shipmentReference\": \"shipmentReference\",\n \"costCenter\": \"costCenter\",\n \"returnReference\": \"returnReference\"\n }\n }\n ]\n}", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/generate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "generate" + ] + }, + "description": "Create a return shipment with the provided details, it will return generated labels" + }, + "response": [] + }, + { + "name": "BE-BE Single Label", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"receiver\": {\n \"customerNumber\": \"1122334455\",\n \"customerCode\": \"DEVC\",\n \"address\": {\n \"countryIso\": \"BE\",\n \"city\": \"Brussel\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2050\",\n \"street\": \"Brusselstraat\"\n }\n },\n \"sender\": {\n \"address\": {\n \"countryIso\": \"BE\",\n \"city\": \"Brussel\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2050\",\n \"street\": \"Brusselstraat\"\n },\n \"contact\": {\n \"email\": \"verzendemail@postnl.nl\",\n \"firstName\": \"Test\",\n \"lastName\": \"Verzender\",\n \"language\": \"NL\",\n \"mobileNumber\": \"+31687654321\"\n }\n },\n \"labelSettings\": {\n \"outputType\": \"pdf\",\n \"printMethod\": \"consumerPrint\"\n },\n \"returnOptions\": {\n \"domestic\": {\n \"returnPeriod\": 20,\n \"valuableReturn\": false\n }\n },\n \"items\": [\n {\n \"barcode\": \"3SDEVC987123654\",\n \"customerReferences\": {\n \"shipmentReference\": \"shipmentReference\",\n \"costCenter\": \"costCenter\",\n \"returnReference\": \"returnReference\"\n }\n }\n ]\n}", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/generate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "generate" + ] + }, + "description": "Create a return shipment with the provided details, it will return generated labels" + }, + "response": [] + }, + { + "name": "NL-NL Smart Returns", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"receiver\": {\n \"customerNumber\": \"1122334455\",\n \"customerCode\": \"DEVC\",\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"companyName\": \"PostNL\",\n \"houseNumber\": \"3\",\n \"postalCode\": \"2532 CA\",\n \"street\": \"Waldorpstraat\"\n }\n },\n \"sender\": {\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2352 CA\",\n \"street\": \"Waldorpstraat\"\n },\n \"contact\": {\n \"email\": \"verzendemail@postnl.nl\",\n \"firstName\": \"Test\",\n \"lastName\": \"Verzender\",\n \"language\": \"NL\",\n \"mobileNumber\": \"+31687654321\"\n }\n },\n \"labelSettings\": {\n \"outputType\": \"pdf\",\n \"printMethod\": \"retailPrint\"\n },\n \"returnOptions\": {\n \"domestic\": {\n \"returnPeriod\": 20,\n \"valuableReturn\": false\n }\n },\n \"items\": [\n {\n \"barcode\": \"3SDEVC112233445\",\n \"customerReferences\": {\n \"shipmentReference\": \"shipmentReference\",\n \"costCenter\": \"costCenter\",\n \"returnReference\": \"returnReference\"\n }\n }\n ]\n}", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/generate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "generate" + ] + }, + "description": "Create a return shipment with the provided details, it will return generated labels" + }, + "response": [] + }, + { + "name": "NL-BE Smart Returns", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"receiver\": {\n \"customerNumber\": \"1122334455\",\n \"customerCode\": \"DEVC\",\n \"address\": {\n \"countryIso\": \"BE\",\n \"city\": \"Brussel\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2050\",\n \"street\": \"Brusselstraat\"\n }\n },\n \"sender\": {\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2352 CA\",\n \"street\": \"Waldorpstraat\"\n },\n \"contact\": {\n \"email\": \"verzendemail@postnl.nl\",\n \"firstName\": \"Test\",\n \"lastName\": \"Verzender\",\n \"language\": \"NL\",\n \"mobileNumber\": \"+31687654321\"\n }\n },\n \"labelSettings\": {\n \"outputType\": \"pdf\",\n \"printMethod\": \"retailPrint\"\n },\n \"returnOptions\": {\n \"domestic\": {\n \"returnPeriod\": 20,\n \"valuableReturn\": false\n }\n },\n \"items\": [\n {\n \"barcode\": \"3SDEVC556677889\",\n \"customerReferences\": {\n \"shipmentReference\": \"shipmentReference\",\n \"costCenter\": \"costCenter\",\n \"returnReference\": \"returnReference\"\n }\n }\n ]\n}", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/generate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "generate" + ] + }, + "description": "Create a return shipment with the provided details, it will return generated labels" + }, + "response": [] + }, + { + "name": "NL-NL Antwoordnummer", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"receiver\": {\n \"customerNumber\": \"1122334455\",\n \"customerCode\": \"DEVC\",\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Utrecht\",\n \"companyName\": \"PostNL\",\n \"houseNumber\": \"9\",\n \"postalCode\": \"3532VA\",\n \"street\": \"Antwoordnummer\"\n }\n },\n \"sender\": {\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2352 CA\",\n \"street\": \"Waldorpstraat\"\n },\n \"contact\": {\n \"email\": \"verzendemail@postnl.nl\",\n \"firstName\": \"Test\",\n \"lastName\": \"Verzender\",\n \"language\": \"NL\",\n \"mobileNumber\": \"+31687654321\"\n }\n },\n \"labelSettings\": {\n \"outputType\": \"pdf\",\n \"printMethod\": \"consumerPrint\"\n },\n \"returnOptions\": {\n \"domestic\": {\n \"returnPeriod\": 20,\n \"valuableReturn\": false\n }\n },\n \"items\": [\n {\n \"barcode\": \"3SDEVC995511224\",\n \"customerReferences\": {\n \"shipmentReference\": \"shipmentReference\",\n \"costCenter\": \"costCenter\",\n \"returnReference\": \"returnReference\"\n }\n }\n ]\n}", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/generate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "generate" + ] + }, + "description": "Create a return shipment with the provided details, it will return generated labels" + }, + "response": [] + }, + { + "name": "BE-NL Antwoordnummer", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"receiver\": {\n \"customerNumber\": \"1122334455\",\n \"customerCode\": \"DEVC\",\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Utrecht\",\n \"companyName\": \"PostNL\",\n \"houseNumber\": \"9\",\n \"postalCode\": \"3532VA\",\n \"street\": \"Antwoordnummer\"\n }\n },\n \"sender\": {\n \"address\": {\n \"countryIso\": \"BE\",\n \"city\": \"Brussel\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"20500\",\n \"street\": \"Brusselstraat\"\n },\n \"contact\": {\n \"email\": \"verzendemail@postnl.nl\",\n \"firstName\": \"Test\",\n \"lastName\": \"Verzender\",\n \"language\": \"NL\",\n \"mobileNumber\": \"+31687654321\"\n }\n },\n \"labelSettings\": {\n \"outputType\": \"pdf\",\n \"printMethod\": \"consumerPrint\"\n },\n \"returnOptions\": {\n \"domestic\": {\n \"returnPeriod\": 20,\n \"valuableReturn\": false\n }\n },\n \"items\": [\n {\n \"barcode\": \"3SDEVC147258369\",\n \"customerReferences\": {\n \"shipmentReference\": \"shipmentReference\",\n \"costCenter\": \"costCenter\",\n \"returnReference\": \"returnReference\"\n }\n }\n ]\n}", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/generate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "generate" + ] + }, + "description": "Create a return shipment with the provided details, it will return generated labels" + }, + "response": [] + }, + { + "name": "NL-NL Single Label Valuable Return", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"receiver\": {\n \"customerNumber\": \"11223344\",\n \"customerCode\": \"DEVC\",\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"companyName\": \"PostNL\",\n \"houseNumber\": \"3\",\n \"postalCode\": \"2532 CA\",\n \"street\": \"Waldorpstraat\"\n }\n },\n \"sender\": {\n \"address\": {\n \"countryIso\": \"NL\",\n \"city\": \"Den Haag\",\n \"houseNumber\": \"2\",\n \"houseNumberAddition\": \"test\",\n \"postalCode\": \"2352 CA\",\n \"street\": \"Waldorpstraat\"\n },\n \"contact\": {\n \"email\": \"verzendemail@postnl.nl\",\n \"firstName\": \"Test\",\n \"lastName\": \"Verzender\",\n \"language\": \"NL\",\n \"mobileNumber\": \"+31687654321\"\n }\n },\n \"labelSettings\": {\n \"outputType\": \"pdf\",\n \"printMethod\": \"consumerPrint\"\n },\n \"returnOptions\": {\n \"domestic\": {\n \"returnPeriod\": 20,\n \"valuableReturn\": true\n }\n },\n \"items\": [\n {\n \"barcode\": \"3SDEVC176348952\",\n \"customerReferences\": {\n \"shipmentReference\": \"shipmentReference\",\n \"costCenter\": \"costCenter\",\n \"returnReference\": \"returnReference\"\n }\n }\n ]\n}", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + } + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/generate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "generate" + ] + }, + "description": "Create a return shipment with the provided details, it will return generated labels" + }, + "response": [] + } + ] + }, + { + "name": "PostNL Activate Return API", + "item": [ + { + "name": "ActivateReturn denied no label", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"barcode\": \"3SCJRG47139095\",\r\n\t\"sender\": {\r\n\t\t\"customerNumber\": \"11223344\"\r\n\t},\r\n \"source\": \"79\",\r\n\t\"label\": true\r\n}" + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/activate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "activate" + ] + } + }, + "response": [] + }, + { + "name": "ActivateReturn warning Label", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "default" + }, + { + "key": "apikey", + "value": "{{APIkey-Sandbox}}", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n\t\"barcode\": \"3SCJRG47139095\",\r\n\t\"sender\": {\r\n\t\t\"customerNumber\": \"11223344\"\r\n\t},\r\n \"source\": \"79\",\r\n\t\"label\": true\r\n}\r\n" + }, + "url": { + "raw": "https://api-sandbox.postnl.nl/shipment/delivery/v4/return/activate", + "protocol": "https", + "host": [ + "api-sandbox", + "postnl", + "nl" + ], + "path": [ + "shipment", + "delivery", + "v4", + "return", + "activate" + ] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "// Setting date & datetime variables", + "var moment = require('moment')", + "pm.collectionVariables.set('currentdate', moment().format((\"YYYY-MM-DD\")))", + "pm.collectionVariables.set('currentdatetime', moment().format((\"YYYY-MM-DDTHH:mm:ss\")))", + "pm.collectionVariables.set('currentdate+1', moment().clone().add(1, 'day').format((\"YYYY-MM-DD\")))", + "pm.collectionVariables.set('currentdatetime+1', moment().clone().add(1, 'day').format((\"YYYY-MM-DDTHH:mm:ss\")))", + "", + "", + "//Generates random barcode series for testing purposes", + "const lodash = require('lodash');", + "pm.collectionVariables.set(\"klantcode\", \"DEVC\")", + "pm.collectionVariables.set(\"klantnr\", \"11223344\")", + "pm.collectionVariables.set(\"barcode15\", \"3SDEVC\"+lodash.random(100000000,999999999));", + "pm.collectionVariables.set(\"barcode14\", \"3SDEVC\"+lodash.random(10000000,99999999));" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "BOIServiceAddressTest", + "value": "internal-boi.bas.t16.cldsvc.net" + }, + { + "key": "currentdate", + "value": "" + }, + { + "key": "currentdatetime", + "value": "" + }, + { + "key": "currentdate+1", + "value": "" + }, + { + "key": "currentdatetime+1", + "value": "" + }, + { + "key": "klantcode", + "value": "" + }, + { + "key": "barcode15", + "value": "" + }, + { + "key": "barcode14", + "value": "" + }, + { + "key": "klantnummer", + "value": "" + }, + { + "key": "BOIServiceAddressACC", + "value": "" + }, + { + "key": "BOIServiceAddressPROD", + "value": "" + }, + { + "key": "barcode", + "value": "" + }, + { + "key": "barcodeROW", + "value": "" + }, + { + "key": "barcodeEU", + "value": "" + }, + { + "key": "klantnr", + "value": "" + }, + { + "key": "returnBarcode", + "value": "" + }, + { + "key": "date", + "value": "" + }, + { + "key": "barcode2", + "value": "" + } + ] +} \ No newline at end of file diff --git a/docs/postnl-v4-migration/sources/sdk-docs/Barcode/README.md b/docs/postnl-v4-migration/sources/sdk-docs/Barcode/README.md new file mode 100644 index 00000000..d633b750 --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/Barcode/README.md @@ -0,0 +1,374 @@ +# Barcode API Documentation + +The Barcode functionality allows you to generate barcodes for PostNL shipments. Barcodes are unique identifiers used to track parcels throughout the shipping process. + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Generate Barcode](#generate-barcode) +- [Data Models](#data-models) +- [Collection Methods](#collection-methods) +- [Error Handling](#error-handling) +- [Complete Example](#complete-example) + +--- + +## Prerequisites + +### SDK Setup + +See the main SDK setup guide in the root documentation: +➡️ [SDK Root Documentation](../../README.md) + +--- + +### Required Credentials + +All Barcode requests require: +- `customerCode` - Your PostNL customer code +- `customerNumber` - Your PostNL customer number + +--- + +## Generate Barcode + +Generate one or more barcodes for shipments using a specified series range. + +### Endpoint + +``` +POST /shipment/delivery/v4/barcode +``` + +### Request Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `customerNumber` | string | No | Your PostNL customer number | +| `customerCode` | string | No | Your PostNL customer code | +| `seriesStart` | string | No | Start of barcode series range (e.g., `000000000`) | +| `seriesEnd` | string | No | End of barcode series range (e.g., `999999999`) | +| `numberOfBarcodes` | int | No | Number of barcodes to generate | + +### Code Examples + +#### Using Constructor + +```php +use Postnl\Sdk\Service\Barcode\V4\Request\BarcodeRequest; + +$request = new BarcodeRequest( + customerNumber: 'YOUR_CUSTOMER_NUMBER', + customerCode: 'YOUR_CUSTOMER_CODE', + serieStart: '000000000', + serieEnd: '999999999', + numberOfBarcodes: 5, +); + +$response = $postnl->barcode()->generateBarcode($request); +``` + +#### Using fromArray Factory + +```php +use Postnl\Sdk\Service\Barcode\V4\Request\BarcodeRequest; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = BarcodeRequest::fromArray([ + 'customerNumber' => 'YOUR_CUSTOMER_NUMBER', + 'customerCode' => 'YOUR_CUSTOMER_CODE', + 'seriesStart' => '000000000', + 'seriesEnd' => '999999999', + 'numberOfBarcodes' => 5, +], $mapper); + +$response = $postnl->barcode()->generateBarcode($request); +``` + +### API Response Structure + +```json +{ + "barcodes": [ + "3SDEVC123456789", + "3SDEVC123456790", + "3SDEVC123456791", + "3SDEVC123456792", + "3SDEVC123456793" + ] +} +``` + +### Working with the Response + +```php +$response = $postnl->barcode()->generateBarcode($request); + +// Get the collection of barcodes +$collection = $response->barcodes(); + +// Check response status +if ($response->isSuccess()) { + echo "Status: " . $response->meta()->statusCode; // 200 +} + +// Get total count +echo "Generated " . $collection->count() . " barcodes\n"; + +// Get first barcode +$firstBarcode = $collection->first(); +echo $firstBarcode->get(); // "3SDEVC123456789" +echo (string) $firstBarcode; // "3SDEVC123456789" + +// Check if barcode is international +echo $firstBarcode->isInternational(); // false + +// Iterate over all barcodes +foreach ($collection as $barcode) { + echo $barcode->get() . "\n"; +} + +// Get raw array response +$rawData = $response->meta()->toArray(); +``` + +--- + +## Data Models + +### BarcodeRequest + +Represents the request payload for generating barcodes. + +```php +readonly class BarcodeRequest +{ + public ?string $customerNumber; // Your PostNL customer number + public ?string $customerCode; // Your PostNL customer code + public ?string $seriesStart; // Start of barcode series range + public ?string $seriesEnd; // End of barcode series range + public ?int $numberOfBarcodes; // Number of barcodes to generate +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `fromArray(array $data, PayloadMapperInterface $mapper)` | self | Create instance from associative array | +| `toArray(PayloadMapperInterface $mapper)` | array | Convert to array (null values filtered) | + +### Barcode + +Represents a single barcode value object. + +```php +final readonly class Barcode +{ + public string $barcode; // The barcode string +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `get()` | string | Returns the barcode string | +| `__toString()` | string | String conversion (same as `get()`) | +| `isInternational()` | bool | Returns `true` if barcode starts with 'LA' and ends with 'XNL' | + +**Example Usage:** + +```php +$barcode = $collection->first(); + +// Get barcode value +echo $barcode->get(); // "3SDEVC123456789" +echo (string) $barcode; // "3SDEVC123456789" + +// Check if international shipment +if ($barcode->isInternational()) { + echo "This is an international barcode"; +} +``` + +--- + +## Collection Methods + +`BarcodesCollection` provides methods for working with barcode results: + +### Base Methods + +| Method | Return | Description | +|--------|--------|-------------| +| `count()` | int | Number of barcodes in collection | +| `isEmpty()` | bool | Returns `true` if collection is empty | +| `all()` | array | Returns all Barcode objects as array | +| `first()` | ?Barcode | Returns first barcode or null | +| `last()` | ?Barcode | Returns last barcode or null | +| `get(int $index)` | ?Barcode | Returns barcode at index or null | + +### Filtering Methods + +All filter methods return a new collection (immutable). + +```php +// Filter to only international barcodes +$international = $collection->filter(function ($barcode) { + return $barcode->isInternational(); +}); + +// Filter barcodes starting with specific prefix +$filtered = $collection->filter(function ($barcode) { + return str_starts_with($barcode->get(), '3SDEVC'); +}); +``` + +### Helper Methods + +```php +// Find first matching barcode +$found = $collection->find( + fn ($barcode) => $barcode->isInternational() +); + +// Check if any barcodes are international +$hasInternational = $collection->some( + fn ($barcode) => $barcode->isInternational() +); + +// Check if all barcodes are domestic +$allDomestic = $collection->every( + fn ($barcode) => !$barcode->isInternational() +); + +// Map to array of strings +$barcodeStrings = $collection->map( + fn ($barcode) => $barcode->get() +); +``` + +### Iteration + +```php +// Iterate using foreach +foreach ($collection as $barcode) { + echo $barcode->get() . "\n"; +} + +// Using iterator +$iterator = $collection->getIterator(); +``` + +--- + +## Error Handling + +The SDK throws semantic exceptions for validation and request errors. + +> For the complete exception hierarchy, ProblemDetails, and retry behavior, see the [Error Handling guide](../ErrorHandling/README.md). + +### Common Exception Types + +The most common concrete exception types (non-exhaustive) are: + +| Exception | Description | +|-----------|-------------| +| `ValidationException` | Invalid request parameters (400, 422) | +| `AuthenticationException` | Invalid or insufficient credentials (401, 403) | +| `ClientException` | Generic client error for other 4xx responses | +| `TimeoutException` | Request timeout (408, retry behavior depends on idempotency) | +| `RateLimitException` | Too many requests (429, retryable) | +| `ServerException` | Server error (5xx, retryable) | +| `HttpSdkException` | Base class for all HTTP errors | + +### Common Validation Errors + +| Error Scenario | Description | +|----------------|-------------| +| Invalid credentials | `customerCode` or `customerNumber` not valid | +| Invalid series range | `seriesStart` greater than `seriesEnd` | +| Excessive count | `numberOfBarcodes` exceeds allowed maximum | + +### Example Error Handling + +```php +use Postnl\Sdk\Exception\Client\AuthenticationException; +use Postnl\Sdk\Exception\Client\ValidationException; + +try { + $response = $postnl->barcode()->generateBarcode($request); + $collection = $response->barcodes(); + + foreach ($collection as $barcode) { + echo $barcode->get() . "\n"; + } + +} catch (ValidationException $e) { + // Handle validation errors (400, 422) + echo "Validation failed: " . $e->getMessage() . "\n"; + foreach ($e->fieldErrors as $error) { + echo " Field '{$error->field}': {$error->message}\n"; + } + +} catch (AuthenticationException $e) { + // Handle authentication/authorization errors (401, 403) + echo "Authentication failed: " . $e->getMessage() . "\n"; +} +``` + +--- + +## Complete Example + +```php +barcode()->generateBarcode($request); + $collection = $response->barcodes(); + + // Check if successful + if ($response->isSuccess()) { + echo "Successfully generated {$collection->count()} barcodes\n\n"; + } + + // Process barcodes + foreach ($collection as $index => $barcode) { + $type = $barcode->isInternational() ? 'International' : 'Domestic'; + echo sprintf("[%d] %s (%s)\n", $index + 1, $barcode->get(), $type); + } + + // Get specific barcode by index + $thirdBarcode = $collection->get(2); + if ($thirdBarcode !== null) { + echo "\nThird barcode: " . $thirdBarcode->get() . "\n"; + } + + // Map to plain array of strings + $barcodeStrings = $collection->map(fn ($b) => $b->get()); + echo "\nAll barcodes as array:\n"; + print_r($barcodeStrings); + +} catch (ValidationException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +``` diff --git a/docs/postnl-v4-migration/sources/sdk-docs/Configuration/README.md b/docs/postnl-v4-migration/sources/sdk-docs/Configuration/README.md new file mode 100644 index 00000000..ca189e50 --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/Configuration/README.md @@ -0,0 +1,355 @@ +# Configuration + +## Environment Variables + +`Auth::fromEnv()` loads **authentication** from environment variables. All other settings (retry policy, API version, cache, etc.) are configured via fluent builder methods on `ClientBuilder`. + +### Authentication (required) + +| Variable | Required | Description | +|----------|----------|-------------| +| `SDK_POSTNL_API_KEY` | Yes (if using API key) | PostNL API key | +| `SDK_POSTNL_CLIENT_ID` | Yes (if using OAuth) | OAuth client ID | +| `SDK_POSTNL_CLIENT_SECRET` | Yes (if using OAuth) | OAuth client secret | +| `SDK_POSTNL_OAUTH_TOKEN_URL` | Yes (if using OAuth) | OAuth token endpoint URL | +| `SDK_POSTNL_IS_SANDBOX` | No | Set to `true` for sandbox mode (default: `false`) | + +### Cache (optional) + +| Variable | Description | +|----------|-------------| +| `SDK_POSTNL_CACHE_STORE_TYPE` | Backend: `array`, `redis`, `memcached`, `file` (default: `auto`) | +| `SDK_POSTNL_CACHE_TTL` | Default TTL in seconds (default: `3600`) | +| `SDK_POSTNL_CACHE_PREFIX` | Key prefix (default: `sdk_postnl_`) | +| `SDK_POSTNL_REDIS_HOST` | Redis host (enables Redis when set) | +| `SDK_POSTNL_REDIS_PORT` | Redis port (default: `6379`) | +| `SDK_POSTNL_REDIS_PASSWORD` | Redis password | +| `SDK_POSTNL_REDIS_DATABASE` | Redis database index (default: `0`) | +| `SDK_POSTNL_MEMCACHED_HOST` | Memcached host (enables Memcached when set) | +| `SDK_POSTNL_MEMCACHED_PORT` | Memcached port (default: `11211`) | +| `SDK_POSTNL_FILE_CACHE_DIR` | Directory for file-based cache | +| `SDK_POSTNL_LOGGER_CLASS_PATH` | FQCN of a PSR-3 logger to use for cache logging | + +### Operational (optional) + +| Variable | Description | +|----------|-------------| +| `SDK_POSTNL_MAX_RETRIES` | Maximum retry attempts (default: `3`, set `0` to disable) | +| `SDK_POSTNL_RETRY_DELAY_MS` | Base retry delay in milliseconds (default: `1000`) | +| `SDK_POSTNL_MAX_RETRY_DELAY_MS` | Maximum retry delay cap in milliseconds (default: `10000`) | +| `SDK_POSTNL_SOURCE_SYSTEM` | Source system identifier header | +| `SDK_POSTNL_API_VERSION` | API version (`1`, `4`, or `5`; default: `4`) | +| `SDK_POSTNL_CUSTOMER_NUMBER` | Customer number | +| `SDK_POSTNL_CUSTOMER_CODE` | Customer code | +| `SDK_POSTNL_MIN_LOG_LEVEL` | Minimum log level (`debug`, `info`, `warning`, `error`, etc.) | +| `SDK_POSTNL_LOGGER_CLASS_PATH` | FQCN of a PSR-3 logger class to instantiate | + +All settings can be configured via fluent methods on the `ClientBuilder` returned by `Postnl::factory()`. + +## HTTP Client + +The SDK uses [PSR-18](https://www.php-fig.org/psr/psr-18/) for HTTP. No specific client is bundled — `php-http/discovery` auto-detects whichever PSR-18 client your project already has installed. + +**Precedence:** If you call both `withHttpClient()` and `withGuzzleOptions()`, the client passed to `withHttpClient()` is used and Guzzle options are ignored. + +### Zero-config (auto-discovery) + +Install any PSR-18 client and the SDK will find it automatically: + +```bash +# Guzzle (most common) +composer require guzzlehttp/guzzle + +# Or Symfony HTTP Client +composer require symfony/http-client nyholm/psr7 +``` + +Then build the client normally — no extra configuration needed: + +```php +use Postnl\Sdk\Postnl; +use Postnl\Sdk\Auth\Auth; + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->make(); +``` + +### Guzzle options (most common case) + +When Guzzle is installed, use `withGuzzleOptions()` to configure timeouts, SSL verification, and debug output without instantiating Guzzle yourself. The SDK creates the Guzzle client and applies its own retry logic. + +```php +use Postnl\Sdk\Postnl; +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Config\GuzzleClientOptions; + +$options = new GuzzleClientOptions( + timeout: 30.0, + connectTimeout: 10.0, + verify: true, + debug: false, +); + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withGuzzleOptions($options) + ->make(); +``` + +Or use defaults: + +```php +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withGuzzleOptions(GuzzleClientOptions::defaults()) + ->make(); +``` + +If you call `withGuzzleOptions()` but Guzzle is not installed, `make()` throws `SdkLogicException`. Install Guzzle (`composer require guzzlehttp/guzzle`) or use `withHttpClient()` with another PSR-18 client. + +### Custom PSR-18 client + +Pass your own pre-configured PSR-18 client via `withHttpClient()`. This bypasses auto-discovery entirely and gives you full control (timeouts, SSL verification, proxy, etc.): + +```php +use Postnl\Sdk\Postnl; +use Postnl\Sdk\Auth\Auth; +use GuzzleHttp\Client as GuzzleClient; +use GuzzleHttp\RequestOptions; + +$httpClient = new GuzzleClient([ + RequestOptions::TIMEOUT => 30, + RequestOptions::CONNECT_TIMEOUT => 10, + RequestOptions::VERIFY => false, // disable SSL for internal networks +]); + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withHttpClient($httpClient) + ->make(); +``` + +When you pass a custom HTTP client, the SDK **does not** wrap the transport with its own retry logic (to avoid double retries). If you need retries, configure them on your client or implement retry in your application. + +### HTTP Plugins (PSR-18 agnostic) + +Use `withPlugin()` to intercept requests without any Guzzle dependency. Plugins implement `HttpPluginInterface` and work with any PSR-18 client: + +```php +use Postnl\Sdk\Transport\HttpPluginInterface; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +class CustomHeaderPlugin implements HttpPluginInterface +{ + public function handleRequest(RequestInterface $request, callable $next): ResponseInterface + { + return $next($request->withHeader('X-Custom', 'value')); + } +} + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withPlugin(new CustomHeaderPlugin()) + ->make(); +``` + +### HTTP-layer response caching + +Use `CachingPlugin::create()` together with `withPlugin()` to enable response caching for selected endpoint URI patterns. The named constructor performs PSR-17 factory discovery automatically: + +```php +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Transport\Cache\CachingPlugin; + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withLogger($logger) + ->withPlugin(CachingPlugin::create( + cache: $yourPsr16Cache, + ttl: 3600, + allowedEndpoints: ['/timeframe/', '/locations/'], + logger: $logger, + )) + ->make(); +``` + +If you want cache read/write failures to be logged, pass `logger: $logger` explicitly to `CachingPlugin::create()`, typically using the same logger instance you passed to `withLogger()`. The caching plugin does not inherit the client logger automatically. + +Auth headers are excluded from the cache key, so OAuth token rotation never causes unnecessary cache misses. + +**Multi-tenant deployments:** When multiple API keys share the same cache backend, pass a unique `$keyPrefix` per tenant to prevent cross-tenant cache collisions: + +```php +->withPlugin(CachingPlugin::create( + cache: $sharedCache, + allowedEndpoints: ['/locations/'], + keyPrefix: 'sdk_postnl_http_tenant-a_', +)) +``` + +Pass `withPlugin()` multiple times to register independent caching plugins for different endpoint groups: + +```php +->withPlugin(CachingPlugin::create(cache: $cache, allowedEndpoints: ['/locations/'])) +->withPlugin(CachingPlugin::create(cache: $cache, allowedEndpoints: ['/timeframe/'])) +``` + +## Log Redaction + +The SDK automatically redacts sensitive data from all log output. No configuration is required — the default rules cover the most common cases. + +### How it works + +`RedactionRegistry` maintains a flat, case-insensitive key→strategy map built from a hardcoded `DEFAULT_MAP`. `LogSanitizer` recursively walks decoded request/response arrays and redacts values whose key matches a registered strategy. + +### Redaction strategies + +| Strategy | Result | Use case | +|----------|--------|----------| +| `RedactionStrategy::FullMask` | `***REDACTED***` | Addresses, house numbers, secrets, tokens | +| `RedactionStrategy::PartialMask` | `foo********com` (fixed-width hidden block) | Email addresses, names, postal codes | +| `RedactionStrategy::BinaryContentOmit` | `[CONTENT OMITTED]` | Label PDFs, binary signatures | + +### Default sensitive fields + +The following fields are redacted automatically (case-insensitive key matching, applied at any nesting depth): + +**Contact / address** — `email`, `firstname`, `lastname`, `mobilenumber`, `phonenumber` → `PartialMask`; `housenumber`, `postalcode`, `street`, `addressline`, `doorcode`, `insuredvalue`, `description` → `FullMask`; `label`, `mergedlabel`, `labelsignature` → `BinaryContentOmit` + +**Credential defaults** — `authorization`, `cookie`, `set-cookie`, `password`, `token`, `secret` → `FullMask`; `apikey`, `clientid`, `clientsecret` → `PartialMask` + +### Customising the registry + +```php +use Postnl\Sdk\Logger\Redaction\RedactionRegistry; +use Postnl\Sdk\Logger\Redaction\RedactionStrategy; + +$registry = new RedactionRegistry(); // starts with all defaults +$registry->addStrategy('myField', RedactionStrategy::FullMask); +$registry->removeStrategy('description'); // remove a default rule + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withRedactionRegistry($registry) + ->make(); +``` + +Start from a blank slate with `RedactionRegistry::empty()`: + +```php +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Logger\Redaction\RedactionRegistry; +use Postnl\Sdk\Logger\Redaction\RedactionStrategy; + +$registry = RedactionRegistry::empty(); +$registry->addStrategy('apiKey', RedactionStrategy::PartialMask); + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withRedactionRegistry($registry) + ->make(); +``` + +### Disabling redaction entirely + +Pass a `NullRedactionRegistry` to emit raw data to logs without any masking: + +```php +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Logger\Redaction\NullRedactionRegistry; + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withRedactionRegistry(new NullRedactionRegistry()) + ->make(); +``` + +## Custom Payload Mapper + +The SDK ships with a default `PayloadMapper` that handles hydration and normalisation of all request and response objects. To replace it — for example, to register domain-specific type casters or alternative reflection strategies — implement `PayloadMapperInterface` and pass it to `withPayloadMapper()`: + +```php +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Support\Contracts\PayloadMapperInterface; + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withPayloadMapper(new MyCustomPayloadMapper()) + ->make(); +``` + +When `withPayloadMapper()` is not called, the SDK automatically creates a `PayloadMapper::create()` instance per client. The mapper is stored in `ServiceContext::$payloadMapper` and threaded through every `fromArray()` / `toArray()` call in the request and response pipeline. + +When building request payloads with `fromArray()` outside the SDK pipeline (for example in tests or CLI scripts), create a mapper instance directly: + +```php +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = SomeRequest::fromArray($data, $mapper); +``` + +--- + +## Recommended Usage + +### API Key Authentication + +```php +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Enums\Version; +use Postnl\Sdk\Transport\Retry\ExponentialBackoffRetryPolicy; + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withRetryPolicy(new ExponentialBackoffRetryPolicy(maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10000)) + ->withApiVersion(Version::V4) + ->make(); +``` + +### OAuth Authentication + +```php +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Transport\Retry\ExponentialBackoffRetryPolicy; + +$client = Postnl::factory() + ->withAuth(Auth::fromEnv()) + ->withRetryPolicy(new ExponentialBackoffRetryPolicy(maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10000)) + ->make(); +``` + +### Fully Programmatic (No Environment Variables) + +```php +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\ClientBuilder; +use Postnl\Sdk\Enums\Version; +use Postnl\Sdk\Transport\Retry\ExponentialBackoffRetryPolicy; + +$client = (new ClientBuilder()) + ->withAuth(Auth::apiKey('your-api-key')) + ->withSandbox(true) + ->withRetryPolicy(new ExponentialBackoffRetryPolicy(maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10000)) + ->withApiVersion(Version::V4) + ->make(); +``` + +## Error Handling + +`make()` fails fast with clear error messages: + +- **No auth configured**: Throws when `make()` is called without a prior `withAuth()` call. +- **Auth already configured**: Throws if `withAuth()` is called twice on the same builder. +- **Both auth types in env**: `Auth::fromEnv()` throws if both `SDK_POSTNL_API_KEY` and OAuth env vars are set. +- **Partial OAuth in env**: `Auth::fromEnv()` throws if only some OAuth env vars are set, listing the missing ones. +- **No PSR-18 client found**: Throws `SdkLogicException` if `make()` is called and no PSR-18 client is installed. Fix: `composer require guzzlehttp/guzzle`. +- **Guzzle options set but Guzzle not installed**: Throws `SdkLogicException` if `withGuzzleOptions()` was used but `guzzlehttp/guzzle` is not installed. Fix: `composer require guzzlehttp/guzzle` or use `withHttpClient()` with another PSR-18 client. diff --git a/docs/postnl-v4-migration/sources/sdk-docs/Confirming/README.md b/docs/postnl-v4-migration/sources/sdk-docs/Confirming/README.md new file mode 100644 index 00000000..99076f1b --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/Confirming/README.md @@ -0,0 +1,606 @@ +# Confirming API Documentation + +The Confirming service allows you to confirm shipment pre-announcements with PostNL. This service registers shipments in the PostNL system without generating shipping labels. Use this when you need to pre-announce shipments but will print labels separately, or when using external label printing systems. + +For generating shipping labels along with confirmation, see the [Labelling service](../Labelling/README.md). + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Confirm Shipment](#confirm-shipment) +- [Data Models](#data-models) +- [Collection Methods](#collection-methods) +- [Error Handling](#error-handling) +- [Enums Reference](#enums-reference) +- [Complete Example](#complete-example) + +--- + +## Prerequisites + +### SDK Setup + +See the main SDK setup guide in the root documentation: +[SDK Root Documentation](../../README.md) + +--- + +### Required Credentials + +All Confirming requests require: +- `customerCode` - Your PostNL customer code +- `customerNumber` - Your PostNL customer number + +These are automatically injected into the sender object by the SDK. + +--- + +## Confirm Shipment + +Confirm a shipment pre-announcement with PostNL. + +### Endpoint + +``` +POST /shipment/delivery/v4/confirm +``` + +### Request Parameters (ShipmentDeliveryRequest) + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `sender` | ShipmentParty | Yes | Sender details including customer credentials and address | +| `receiver` | ShipmentParty | Yes | Receiver contact and address information | +| `labelSettings` | LabelSettings | No | Label format and output configuration | +| `returnOptions` | ReturnOptions | No | Return shipment options | +| `shipmentType` | ShipmentType | No | Type of shipment (default: `parcel`) | +| `handOverDate` | string | No | Date when shipment is handed to PostNL (`YYYY-MM-DD`) | +| `deliveryLocation` | DeliveryLocation | No | Alternative delivery location | +| `services` | Services | No | Additional shipment services | +| `internationalShipmentData` | InternationalShipmentData | No | Data for international shipments | +| `itemCount` | int | No | Number of items in shipment (max 999, default: 1) | +| `items` | ShippingItem[] | No | Individual item details with barcodes | + +### Code Examples + +#### Using Constructor + +```php +use Postnl\Sdk\Enums\Payload\Country; +use Postnl\Sdk\Enums\Payload\ShipmentType; +use Postnl\Sdk\RequestData\V4\Address; +use Postnl\Sdk\RequestData\V4\Contact; +use Postnl\Sdk\RequestData\V4\ShipmentParty; +use Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest; + +$request = new ShipmentDeliveryRequest( + sender: ShipmentParty::asSender( + customerNumber: 'YOUR_CUSTOMER_NUMBER', + customerCode: 'YOUR_CUSTOMER_CODE', + address: new Address( + companyName: 'Your Company', + street: 'Siriusdreef', + houseNumber: '42', + postalCode: '2132WT', + city: 'Hoofddorp', + countryIso: Country::NL, + ), + ), + receiver: ShipmentParty::asReceiver( + address: new Address( + street: 'Waldorpstraat', + houseNumber: '3', + postalCode: '2521CA', + city: 'Den Haag', + countryIso: Country::NL, + ), + contact: new Contact( + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + ), + ), + shipmentType: ShipmentType::Parcel, + handOverDate: date('Y-m-d', strtotime('+1 day')), +); + +$response = $postnl->confirming()->confirmShipmentPreAnnouncement($request); +``` + +#### Using fromArray Factory + +```php +use Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = ShipmentDeliveryRequest::fromArray([ + 'sender' => [ + 'customerNumber' => 'YOUR_CUSTOMER_NUMBER', + 'customerCode' => 'YOUR_CUSTOMER_CODE', + 'address' => [ + 'companyName' => 'Your Company', + 'street' => 'Siriusdreef', + 'houseNumber' => '42', + 'postalCode' => '2132WT', + 'city' => 'Hoofddorp', + 'countryIso' => 'NL', + ], + ], + 'receiver' => [ + 'contact' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'email' => 'john.doe@example.com', + ], + 'address' => [ + 'street' => 'Waldorpstraat', + 'houseNumber' => '3', + 'postalCode' => '2521CA', + 'city' => 'Den Haag', + 'countryIso' => 'NL', + ], + ], + 'type' => 'parcel', + 'handOverDate' => '2024-01-15', +], $mapper); + +$response = $postnl->confirming()->confirmShipmentPreAnnouncement($request); +``` + +#### Using Fluent Interface + +```php +use Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest; + +$request = (new ShipmentDeliveryRequest()) + ->sender($sender) + ->receiver($receiver) + ->shipmentType(ShipmentType::Parcel) + ->handOverDate('2024-01-15') + ->services($services) + ->itemsCount(1); + +$response = $postnl->confirming()->confirmShipmentPreAnnouncement($request); +``` + +### API Response Structure + +```json +{ + "items": [ + { + "shipmentReference": "REF-2024-001", + "barcode": "3SDEVC123456789", + "codingText": "D2132WT+42+0000000", + "productService": { + "productData": "3085", + "services": ["002", "003"] + } + } + ] +} +``` + +### Working with the Response + +```php +$response = $postnl->confirming()->confirmShipmentPreAnnouncement($request); + +// Check response status +if ($response->isSuccess()) { + echo "Status: " . $response->meta()->statusCode; // 200 +} + +// Get the collection of shipping items +$collection = $response->shippingItems(); + +// Get total count +echo "Confirmed " . $collection->count() . " shipments\n"; + +// Iterate over all items +foreach ($collection as $item) { + echo "Reference: " . $item->shipmentReference . "\n"; + echo "Barcode: " . $item->barcode . "\n"; + echo "Coding Text: " . $item->codingText . "\n"; + + // Access product service info + if ($item->productService !== null) { + echo "Product: " . $item->productService->productData . "\n"; + } +} + +// Get first item directly +$firstItem = $collection->first(); + +// Get raw array response +$rawData = $response->meta()->toArray(); + +// Get request correlation ID +$requestId = $response->meta()->requestId; + +// On HTTP 429, catch RateLimitException (see docs/ErrorHandling/README.md). +``` + +--- + +## Data Models + +### ShipmentParty + +Represents a party (sender or receiver) in a shipment or return shipment. Use the static constructors to create parties for the correct role: + +| Static Method | Use Case | Parameters | +|---------------|----------|-------------| +| `asSender()` | Direct shipment sender (merchant dispatching) | customerNumber, customerCode, address, undeliverableReturnAddress? | +| `asReceiver()` | Direct shipment receiver (consumer receiving) | address, contact?, receiverType? | +| `asReturnSender()` | Return shipment sender (consumer returning) | address, contact? | +| `asReturnReceiver()` | Return shipment receiver (merchant receiving) | customerNumber, customerCode, address, contact? | + +```php +final readonly class ShipmentParty +{ + public ?string $customerNumber; // Customer number (max 10 chars) + public ?string $customerCode; // Customer code (max 6 chars) + public ?Address $address; // Postal address + public ?Address $undeliverableReturnAddress; // Return address for undeliverable items + public ?Contact $contact; // Contact information (name, email, phone) + public ?ReceiverType $receiverType; // business or consumer +} +``` + +### ProcessedShippingItem + +Represents a confirmed shipment item in the response. + +```php +readonly class ProcessedShippingItem +{ + public ?string $shipmentReference; // Your reference for the shipment + public ?string $returnReference; // Return shipment reference + public ?Label $label; // Generated label (if requested) + public ?string $barcode; // PostNL barcode for tracking + public ?string $returnBarcode; // Return parcel barcode + public ?string $partnerId; // Carrier-id of commercial network partner + public ?string $partnerBarcode; // Partner barcode at commercial network partner + public ?string $codingText; // Sorting/routing code + public ?ProductService $productService; // Product and service details +} +``` + +### Label + +Represents label data returned from the API. + +```php +readonly class Label +{ + public ?string $content; // Base64 encoded label content + public ?string $partnerId; // Partner identifier + public ?string $partnerBarcode; // Partner's barcode reference + public ?string $mergedLabelContent; // Merged labels (base64) + public ?LabelOutputType $outputType; // Format: pdf, zpl, jpg, gif, png + public ?LabelType $labelType; // Type: Label, labelinthebox, shipmentandreturnlabel, retourLabel, CN23, CommercialInvoice +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `isEmpty()` | bool | Returns `true` if label has no content or output type | +| `saveLabelAsFile(string $filepath)` | void | Decode and save label to file | + +**Example - Saving a Label:** + +```php +$item = $collection->first(); + +if ($item->label !== null && !$item->label->isEmpty()) { + $item->label->saveLabelAsFile('/path/to/label.pdf'); +} +``` + +### ProductService + +Product and service information for the shipment. + +```php +readonly class ProductService +{ + public ?string $productData; // Product code identifier + public ?array $services; // Array of service codes + public ?array $bundles; // Bundle information +} +``` + +### ShippingItem (Request) + +Individual item details for multi-item shipments. + +```php +final readonly class ShippingItem +{ + public ?string $barcode; // Pre-generated barcode + public ?CustomerReferences $customerReferences; // Custom references + public ?Dimensions $dimensions; // Item dimensions +} +``` + +--- + +## Collection Methods + +`ShippingItemsCollection` provides methods for working with confirmed shipment items: + +### Base Methods + +| Method | Return | Description | +|--------|--------|-------------| +| `count()` | int | Number of items in collection | +| `isEmpty()` | bool | Returns `true` if collection is empty | +| `all()` | array | Returns all ProcessedShippingItem objects | +| `first()` | ?ProcessedShippingItem | Returns first item or null | +| `last()` | ?ProcessedShippingItem | Returns last item or null | +| `get(int $index)` | ?ProcessedShippingItem | Returns item at index or null | + +### Filtering Methods + +All filter methods return a new collection (immutable). + +```php +// Filter items with barcodes +$withBarcodes = $collection->filter(function ($item) { + return $item->barcode !== null; +}); + +// Filter items with labels +$withLabels = $collection->filter(function ($item) { + return $item->label !== null && !$item->label->isEmpty(); +}); + +// Filter by specific reference prefix +$filtered = $collection->filter(function ($item) { + return str_starts_with($item->shipmentReference ?? '', 'REF-2024'); +}); +``` + +### Helper Methods + +```php +// Find first item with a specific barcode +$found = $collection->find( + fn ($item) => $item->barcode === '3SDEVC123456789' +); + +// Check if any items have labels +$hasLabels = $collection->some( + fn ($item) => $item->label !== null +); + +// Check if all items have barcodes +$allHaveBarcodes = $collection->every( + fn ($item) => $item->barcode !== null +); + +// Extract all barcodes as array +$barcodes = $collection->map( + fn ($item) => $item->barcode +); +``` + +### Iteration + +```php +// Iterate using foreach +foreach ($collection as $item) { + echo $item->barcode . "\n"; +} + +// Using iterator +$iterator = $collection->getIterator(); +``` + +--- + +## Error Handling + +The SDK throws semantic exceptions for validation and request errors. + +> For the complete exception hierarchy, ProblemDetails, and retry behavior, see the [Error Handling guide](../ErrorHandling/README.md). + +### Exception Types + +| Exception | Description | +|-----------|-------------| +| `ValidationException` | Invalid request parameters (400, 422) | +| `AuthenticationException` | Invalid or insufficient credentials (401, 403) | +| `RateLimitException` | Too many requests (429, retryable) | +| `TimeoutException` | Request timeout (408, usually retryable) | +| `ClientException` | Other client-side errors (remaining 4xx) | +| `ServerException` | Server error (5xx, retryable) | +| `HttpSdkException` | Base class for all HTTP errors | + +### Common Validation Errors + +| Error Scenario | Description | +|----------------|-------------| +| Missing sender | Sender information is required | +| Missing receiver | Receiver information is required | +| Invalid postal code | Postal code format is invalid | +| Invalid country | Country code not supported | +| Invalid shipment type | Must be valid ShipmentType value | +| Missing credentials | customerCode and customerNumber required | + +### Example Error Handling + +```php +use Postnl\Sdk\Exception\Client\AuthenticationException; +use Postnl\Sdk\Exception\Client\ValidationException; +use Postnl\Sdk\Exception\HttpSdkException; + +try { + $response = $postnl->confirming()->confirmShipmentPreAnnouncement($request); + $collection = $response->shippingItems(); + + foreach ($collection as $item) { + echo "Confirmed: " . $item->barcode . "\n"; + } + +} catch (ValidationException $e) { + // Handle validation errors (400, 422) + echo "Validation failed: " . $e->getMessage() . "\n"; + foreach ($e->fieldErrors as $error) { + echo " Field '{$error->field}': {$error->message}\n"; + } + +} catch (AuthenticationException $e) { + // Handle authentication/authorization errors (401, 403) + echo "Authentication failed: " . $e->getMessage() . "\n"; + +} catch (HttpSdkException $e) { + // Handle all other HTTP errors + echo "Request failed [{$e->statusCode}]: " . $e->getMessage() . "\n"; + if ($e->problemDetails->traceId !== null) { + echo "Trace ID: " . $e->problemDetails->traceId . "\n"; + } +} +``` + +--- + +## Enums Reference + +### ShipmentType + +```php +use Postnl\Sdk\Enums\Payload\ShipmentType; + +ShipmentType::Parcel->value; // 'parcel' +ShipmentType::NonStandardParcel->value; // 'parcelnonstandard' +ShipmentType::Letter->value; // 'letter' +ShipmentType::LetterBox->value; // 'letterbox' +ShipmentType::Pallet->value; // 'pallet' +ShipmentType::Packet->value; // 'packet' +``` + +### ReceiverType + +```php +use Postnl\Sdk\Enums\Payload\ReceiverType; + +ReceiverType::Business->value; // 'business' +ReceiverType::Consumer->value; // 'consumer' +``` + +### LabelOutputType + +```php +use Postnl\Sdk\Enums\Payload\LabelOutputType; + +LabelOutputType::PDF->value; // 'pdf' +LabelOutputType::ZPL->value; // 'zpl' +LabelOutputType::JPG->value; // 'jpg' +LabelOutputType::GIF->value; // 'gif' +LabelOutputType::PNG->value; // 'png' +``` + +### LabelType + +```php +use Postnl\Sdk\Enums\Payload\LabelType; + +LabelType::Label->value; // 'Label' +LabelType::LabelInTheBox->value; // 'labelinthebox' +LabelType::ShipmentAndReturnLabel->value; // 'shipmentandreturnlabel' +``` + +--- + +## Complete Example + +```php +confirming()->preAnnounceShipment($request); + $collection = $response->shippingItems(); + + // Check if successful + if ($response->isSuccess()) { + echo "Successfully confirmed {$collection->count()} shipment(s)\n\n"; + } + + // Process confirmed items + foreach ($collection as $index => $item) { + echo sprintf("[%d] Shipment Confirmed\n", $index + 1); + echo " Reference: " . ($item->shipmentReference ?? 'N/A') . "\n"; + echo " Barcode: " . ($item->barcode ?? 'N/A') . "\n"; + echo " Coding Text: " . ($item->codingText ?? 'N/A') . "\n"; + + if ($item->productService !== null) { + echo " Product: " . $item->productService->productData . "\n"; + echo " Services: " . implode(', ', $item->productService->services ?? []) . "\n"; + } + echo "\n"; + } + + // Extract all barcodes for tracking + $barcodes = $collection->map(fn ($item) => $item->barcode); + echo "All barcodes: " . implode(', ', array_filter($barcodes)) . "\n"; + +} catch (ValidationException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +``` diff --git a/docs/postnl-v4-migration/sources/sdk-docs/Distribution/README.md b/docs/postnl-v4-migration/sources/sdk-docs/Distribution/README.md new file mode 100644 index 00000000..dad92b1b --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/Distribution/README.md @@ -0,0 +1,186 @@ +# SDK Distribution — Internal Runbook + +This document describes how the `postnl/api-client-sdk` package is distributed to authorized customers via [Private Packagist](https://packagist.com) and how to manage ongoing operations. + +--- + +## Architecture overview + +``` +Private GitHub Repo (Postnl-Production/api-client-sdk) + ↓ GitHub App webhook (on tag push) +Private Packagist (repo.packagist.com/postnl/) + ↓ HTTP basic auth (customer token) +Customer's Composer install +``` + +--- + +## Initial setup (one-time) + +### 1. Create a Private Packagist account + +Sign up at https://packagist.com with a paid organization plan. Create an organization named `postnl`. + +### 2. PoC — add the repository manually + +Before enabling the GitHub App, validate the distribution flow with a manual repository connection: + +#### Option A — Deploy Key (SSH, no stored credential needed) + +1. In the Private Packagist dashboard go to **Settings → Credentials** (`/orgs/postnl/credentials`). +2. Scroll to the **SSH Access** section and copy the Packagist-generated SSH public key. +3. In the GitHub repository go to **Settings → Deploy keys → Add deploy key**. + - Title: `Private Packagist` + - Key: paste the SSH public key from step 2 + - Leave **Allow write access** unchecked +4. In the Private Packagist dashboard go to **Packages → Add Package**. +5. Set the repository URL to the **SSH form** (not HTTPS): + ``` + git@github.com:Postnl-Production/api-client-sdk.git + ``` + Leave credentials as **No Credentials** — the deploy key handles authentication. +6. Click **Create**. + +> **Note:** The SSH deploy key approach updates packages only sporadically (no webhook). This is acceptable for a PoC. For production, use the GitHub App (step 3) or a stored credential (Option B below) which enables webhooks and near-instant updates. + +#### Option B — Fine-Grained PAT (enables webhooks) + +1. Create a GitHub Fine-Grained PAT scoped to the `api-client-sdk` repository with `Contents: Read` permission. +2. In the Private Packagist dashboard go to **Settings → Credentials → Add Credential**. + - Type: HTTP Basic / Token + - Enter the PAT as the password +3. In the Private Packagist dashboard go to **Packages → Add Package**. +4. Enter the HTTPS URL: `https://github.com/Postnl-Production/api-client-sdk` +5. Select the stored credential from step 2. +6. Click **Create**. + +--- + +After either option, confirm that: + +- `postnl/api-client-sdk` is discovered and its tags/branches are indexed. +- The `dev-main → 1.x-dev` alias resolves (defined in `composer.json`). + +Once the PoC passes, replace the PAT/deploy-key credential with the GitHub App integration below. + +### 3. Security-compliant integration — GitHub App + +Private Packagist provides a native GitHub App integration that avoids personal tokens: + +1. In the Private Packagist dashboard go to **Integration → GitHub App**. +2. Install the Private Packagist GitHub App on the `Postnl-Production` GitHub organization. +3. During installation, grant access to **only** the `api-client-sdk` repository. +4. Private Packagist automatically installs a webhook on the repository. +5. From this point, every new tag pushed to GitHub triggers an automatic package index update within ~30 seconds. + +--- + +## Releasing a new version + +1. Ensure `composer.json` version constraints are updated and release notes are prepared. +2. Push a semver tag: + ```bash + git tag v1.2.0 + git push origin v1.2.0 + ``` +3. The GitHub App webhook notifies Private Packagist, which indexes the new version automatically. No manual action required. +4. Verify the new version is visible in the Private Packagist dashboard under **Packages → postnl/api-client-sdk**. + +--- + +## Customer access management + +### Issuing a customer token + +1. In the Private Packagist dashboard go to **Authentication Tokens → Create Token**. +2. Set scope to **Read** (package download only). +3. Optionally associate the token with a specific customer or team in the dashboard. +4. Share the token with the customer over a secure channel (not email plain text). +5. Provide the customer with the installation instructions from `README.md`. + +### Revoking a customer token + +1. In the Private Packagist dashboard go to **Authentication Tokens**. +2. Find the token by customer name or token prefix. +3. Click **Revoke**. The token is immediately invalidated. +4. Inform the customer that their token has been revoked and issue a replacement if needed. + +### Rotating a token + +Follow the revoke steps above, then issue a new token and deliver it to the customer. There is no in-place rotation — revoke and re-issue. + +--- + +## Customer installation reference + +Customers add the following to their `composer.json`: + +```json +{ + "repositories": [ + { + "type": "composer", + "url": "https://repo.packagist.com/postnl/" + }, + { + "packagist.org": false + } + ] +} +``` + +And authenticate via `auth.json` (project root or `~/.composer/`): + +```json +{ + "http-basic": { + "repo.packagist.com": { + "username": "token", + "password": "CUSTOMER_TOKEN" + } + } +} +``` + +> **Note:** `token` is a literal string required by Private Packagist — do not replace it with a username. + +Or via environment variable (CI/CD): + +```bash +export COMPOSER_AUTH='{"http-basic":{"repo.packagist.com":{"username":"token","password":"CUSTOMER_TOKEN"}}}' +``` + +Install: + +```bash +composer require postnl/api-client-sdk +``` + +> **Tip:** If the project previously pulled the SDK via a local `path` repository (symlink), delete `composer.lock` before running `composer install` to force Composer to re-resolve dependencies from Private Packagist: +> ```bash +> rm composer.lock && composer install +> ``` + +--- + +## Where to find things + +| Resource | Location | +|----------|----------| +| Private Packagist dashboard | https://packagist.com/orgs/postnl | +| Credentials (SSH key / PAT) | https://packagist.com/orgs/postnl/credentials | +| Package page | https://packagist.com/packages/postnl/api-client-sdk | +| GitHub deploy keys | GitHub repo → Settings → Deploy keys | +| GitHub App settings | GitHub org → Settings → GitHub Apps | +| Repository webhook | GitHub repo → Settings → Webhooks | + +--- + +## Security notes + +- The GitHub App grants **read-only** access to the single repository. Do not expand its permissions. +- Customer tokens are **read-only** and scoped to package download. They do not grant repository access. +- `auth.json` must never be committed to source control (enforced by `.gitignore`). +- For CI/CD pipelines, use the `COMPOSER_AUTH` environment variable injected from a secrets manager. +- Rotate tokens immediately if a credential leak is suspected. diff --git a/docs/postnl-v4-migration/sources/sdk-docs/ErrorHandling/README.md b/docs/postnl-v4-migration/sources/sdk-docs/ErrorHandling/README.md new file mode 100644 index 00000000..c39b04c8 --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/ErrorHandling/README.md @@ -0,0 +1,534 @@ +# Error Handling + +The SDK uses structured exceptions aligned with [RFC 9457 — Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457). Every HTTP error response is parsed into a `ProblemDetails` DTO and wrapped in a semantic exception class, giving you typed access to status codes, validation errors, trace IDs, and retry information. + +[SDK Root Documentation](../../README.md) + +--- + +## Table of Contents + +- [Exception Hierarchy](#exception-hierarchy) +- [Horizontal Marker Interfaces](#horizontal-marker-interfaces) +- [HTTP Exceptions](#http-exceptions) +- [ProblemDetails (RFC 9457)](#problemdetails-rfc-9457) +- [Validation Errors](#validation-errors) +- [Retry Behavior](#retry-behavior) +- [Transport Exceptions](#transport-exceptions) +- [Schema Mismatch Exceptions](#schema-mismatch-exceptions) +- [SDK Exceptions](#sdk-exceptions) +- [Exception Hierarchy Reference](#exception-hierarchy-reference) +- [Catch Patterns](#catch-patterns) + +--- + +## Exception Hierarchy + +``` +PostnlExceptionInterface (marker — catch-all for any SDK exception) +├── AuthExceptionInterface (horizontal marker — any auth failure) +├── TransportExceptionInterface (horizontal capability — pre-response transport failure; exposes getFailureReason()) +├── ClientErrorExceptionInterface (horizontal marker — any HTTP 4xx) +├── ServerErrorExceptionInterface (horizontal marker — server-side failure or schema break) +│ +├── PostnlSdkException (extends RuntimeException) +│ ├── HttpSdkException (abstract — all HTTP error responses) +│ │ ├── Client\ValidationException (400, 422) [ClientErrorExceptionInterface] +│ │ ├── Client\AuthenticationException (401, 403) [AuthExceptionInterface, ClientErrorExceptionInterface] +│ │ ├── Client\ClientException (other 4xx) [ClientErrorExceptionInterface] +│ │ └── RetryableHttpSdkException (abstract — retryable HTTP errors) +│ │ ├── Client\RateLimitException (429) [ClientErrorExceptionInterface] +│ │ ├── Client\TimeoutException (408) [ClientErrorExceptionInterface] +│ │ └── Server\ServerException (5xx, Cloudflare 521-524) [ServerErrorExceptionInterface] +│ ├── Transport\TransportException (network failures, always retryable) [TransportExceptionInterface] +│ ├── SchemaMismatchException (API contract break) [ServerErrorExceptionInterface] +│ ├── Retry\RetryExhaustedException (all retries exhausted) +│ └── SdkRuntimeException (internal SDK errors) +├── SdkLogicException (extends LogicException — programmer errors) +└── SdkInvalidArgumentException (extends InvalidArgumentException) +``` + +All exceptions implement `PostnlExceptionInterface`, so `catch (PostnlExceptionInterface $e)` covers every exception the SDK can throw. + +### Auth\AuthException + +`AuthException` also implements `AuthExceptionInterface`. + +--- + +## Horizontal Marker Interfaces + +Three empty marker interfaces and one capability interface allow `catch`-by-intent without listing every concrete class. +`TransportExceptionInterface` is a **capability interface** (declares `getFailureReason(): TransportFailureReason`); the other three are pure markers with no methods. + +| Interface | Implemented by | Use when | +|-----------|---------------|----------| +| `AuthExceptionInterface` | `AuthException`, `AuthenticationException` | Any auth failure (pre-request or HTTP 401/403) | +| `TransportExceptionInterface` | `TransportException` | Pre-response network failure (DNS, TLS, etc.) | +| `ClientErrorExceptionInterface` | `ClientException`, `AuthenticationException`, `ValidationException`, `RateLimitException`, `TimeoutException` | Any HTTP 4xx | +| `ServerErrorExceptionInterface` | `ServerException`, `SchemaMismatchException` | Server-side failure or API schema break | + +All four extend `PostnlExceptionInterface`, so they remain within the SDK exception boundary. + +```php +use Postnl\Sdk\Exception\AuthExceptionInterface; +use Postnl\Sdk\Exception\ClientErrorExceptionInterface; +use Postnl\Sdk\Exception\ServerErrorExceptionInterface; +use Postnl\Sdk\Exception\TransportExceptionInterface; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); +} catch (AuthExceptionInterface $e) { + // Refresh credentials, page auth oncall +} catch (TransportExceptionInterface $e) { + // Check $e->getFailureReason() for DNS/TLS/timeout details +} catch (ServerErrorExceptionInterface $e) { + // PostNL is down or returned unrecognisable data +} catch (ClientErrorExceptionInterface $e) { + // Caller-side error; inspect and fix the request +} +``` + +--- + +## HTTP Exceptions + +Every HTTP error response is mapped to a concrete `HttpSdkException` subclass based on the status code: + +| Status Code | Exception Class | Retryable | +|-------------|-----------------|-----------| +| 400 Bad Request | `ValidationException` | No | +| 401 Unauthorized | `AuthenticationException` | No | +| 403 Forbidden | `AuthenticationException` | No | +| 408 Request Timeout | `TimeoutException` | Yes | +| 422 Unprocessable Entity | `ValidationException` | No | +| 429 Too Many Requests | `RateLimitException` | Yes | +| Other 4xx | `ClientException` | No | +| 500, 502, 503, 504 | `ServerException` | Yes | +| 501 Not Implemented | `ServerException` | No | +| 521-524 (Cloudflare) | `ServerException` | Yes | + +### Properties + +All `HttpSdkException` subclasses expose: + +| Property / Method | Type | Description | +|-------------------|------|-------------| +| `$status` | `?HttpStatus` | Typed enum value, or `null` for non-standard codes | +| `$statusCode` | `int` | Raw HTTP status code (always present) | +| `$problemDetails` | `ProblemDetails` | Parsed response body (see below) | +| `getRequest()` | `RequestInterface` | The PSR-7 request that failed | +| `getResponse()` | `ResponseInterface` | The PSR-7 response received | +| `getCode()` | `int` | Same as `$statusCode` | +| `getMessage()` | `string` | Human-readable error message | + +### String Representation + +``` +[HTTP 400] POST https://api.postnl.nl/v4/shipment - Invalid postal code (traceId: abc-123) +``` + +--- + +## ProblemDetails (RFC 9457) + +Every `HttpSdkException` carries a `ProblemDetails` instance that normalizes the API error response, regardless of format. + +### Properties + +| Property | Type | Description | +|----------|------|-------------| +| `$type` | `?string` | RFC 9457 problem type URI | +| `$title` | `?string` | Short human-readable summary | +| `$status` | `?int` | HTTP status code (echoed from body) | +| `$detail` | `?string` | Occurrence-specific explanation | +| `$instance` | `?string` | URI identifying this specific occurrence | +| `$traceId` | `?string` | PostNL correlation ID for server-side tracing | +| `$fieldErrors` | `list` | Normalized validation errors | +| `$faults` | `list` | Legacy fault entries | +| `$extensions` | `array` | Any extra response body fields | +| `$rawBody` | `string` | Original response body | + +### Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `getMessage()` | `string` | Best-effort message (priority: detail → title → first fault → "Unknown error") | +| `hasFieldErrors()` | `bool` | Whether any field-level validation errors exist | +| `getFieldError(string $field)` | `?FieldError` | Get the first error for a specific field | +| `hasFaults()` | `bool` | Whether any legacy fault entries exist | + +### Supported API Error Formats + +`ProblemDetails` automatically normalizes all PostNL API error formats: + +- **RFC 9457**: `{ "type": "...", "title": "...", "status": 400, "detail": "...", "errors": { "field": ["msg"] } }` +- **Legacy fault**: `{ "fault": { "faultstring": "...", "detail": {} } }` +- **Legacy faults array**: `{ "faults": [{ "faultstring": "..." }] }` +- **Barcode errors**: `{ "errors": [{ "code": "x", "description": "y" }] }` +- **Checkout V1 errors**: `{ "errors": [{ "status": 400, "title": "Bad Request", "detail": "..." }] }` +- **Generic message**: `{ "message": "..." }` +- **Plain text body**: Non-JSON responses are stored in `$detail` + +### Example + +```php +use Postnl\Sdk\Exception\HttpSdkException; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); +} catch (HttpSdkException $e) { + $details = $e->problemDetails; + + echo $details->getMessage(); // Human-readable error + echo $details->traceId; // PostNL correlation ID + echo $details->type; // RFC 9457 problem type URI + + // Access raw body for debugging + echo $details->rawBody; +} +``` + +--- + +## Validation Errors + +`ValidationException` is thrown for **400 Bad Request** and **422 Unprocessable Entity** responses. It exposes a convenience `$fieldErrors` property containing structured validation errors. + +### FieldError + +Each `FieldError` has: + +| Property | Type | Description | +|----------|------|-------------| +| `$field` | `string` | The field name that failed validation | +| `$message` | `string` | Human-readable error message | +| `$code` | `?string` | Optional error code | + +### Example + +```php +use Postnl\Sdk\Exception\Client\ValidationException; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); +} catch (ValidationException $e) { + foreach ($e->fieldErrors as $error) { + echo "Field '{$error->field}': {$error->message}\n"; + } + + // Or look up a specific field + $postalCodeError = $e->problemDetails->getFieldError('postalCode'); + if ($postalCodeError !== null) { + echo "Postal code error: {$postalCodeError->message}\n"; + } +} +``` + +--- + +## Retry Behavior + +The SDK automatically retries requests that fail with retryable exceptions when retry is configured. Retryable exceptions implement `RetryableExceptionInterface`: + +| Exception | Retryable | Retry-After Header | +|-----------|-----------|--------------------| +| `RateLimitException` (429) | Always | Yes, parsed from response | +| `TimeoutException` (408) | Always | Yes, parsed from response | +| `ServerException` (5xx) | Depends on status code | Yes, parsed from response | +| `TransportException` (network) | Always | No (no response received) | + +### RetryableExceptionInterface + +```php +interface RetryableExceptionInterface +{ + public function isRetryable(): bool; + public function retryAfterSeconds(): ?int; +} +``` + +### RetryExhaustedException + +When all retry attempts are exhausted, `RetryExhaustedException` is thrown. The last failure is available via `getPrevious()`. Every failure (including the final attempt) is recorded in `$failedAttempts`, so you can inspect the full chain (not only the final exception). + +| Property / Method | Type | Description | +|-------------------|------|-------------| +| `$attempts` | `int` | Total number of attempts made | +| `$lastResponse` | `?ResponseInterface` | The last response received (if any) | +| `$failedAttempts` | `list` | Each failed attempt: one-based attempt index and the exception thrown | +| `getPrevious()` | `?Throwable` | The exception from the final failed attempt (same as the last entry in `$failedAttempts` when non-empty) | + +### Example + +```php +use Postnl\Sdk\Exception\Retry\RetryExhaustedException; +use Postnl\Sdk\Exception\HttpSdkException; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); +} catch (RetryExhaustedException $e) { + echo "Failed after {$e->attempts} attempts\n"; + + foreach ($e->failedAttempts as $entry) { + echo " Attempt {$entry['attempt']}: " . $entry['exception']->getMessage() . "\n"; + } + + // Access the final failure (same as the last failedAttempts entry) + $original = $e->getPrevious(); + if ($original instanceof HttpSdkException) { + echo "Last error: " . $original->getMessage() . "\n"; + } +} +``` + +--- + +## Transport Exceptions + +`TransportException` represents non-HTTP transport failures: DNS resolution, TLS handshake, connection refused, network timeouts, etc. + +- Always retryable (implements `RetryableExceptionInterface`) +- No `Retry-After` delay available (no HTTP response was received) +- Implements PSR-18 `NetworkExceptionInterface` +- Implements `TransportExceptionInterface` (horizontal capability interface — exposes `getFailureReason()` for catch-by-intent without downcasting) +- Access the failed request via `getRequest()` +- `$failureReason` provides a best-effort classification of the failure (see `TransportFailureReason`) + +### TransportFailureReason + +`TransportException::$failureReason` is a `TransportFailureReason` enum (default: `Unknown`) populated by `ExceptionNormalizer` based on the underlying PSR-18 exception: + +| Case | Value | Description | +|------|-------|-------------| +| `ConnectionRefused` | `connection_refused` | TCP refused / network unreachable | +| `DnsFailure` | `dns_failure` | Hostname resolution failed | +| `TlsHandshake` | `tls_handshake` | SSL/TLS negotiation failed | +| `SocketTimeout` | `socket_timeout` | Connect or read timeout expired | +| `Unknown` | `unknown` | Transport gave insufficient detail to classify | + +Classification is best-effort. `Unknown` is a first-class valid value when the transport layer does not expose classifiable details. + +### String Representation + +``` +[Transport] GET https://api.postnl.nl/v4/shipment - Connection refused +``` + +### Example + +```php +use Postnl\Sdk\Enums\TransportFailureReason; +use Postnl\Sdk\Exception\Transport\TransportException; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); +} catch (TransportException $e) { + echo "Transport error: " . $e->getMessage() . "\n"; + echo "Request: " . $e->getRequest()->getUri() . "\n"; + + // Inspect the failure reason for targeted alerting + match ($e->failureReason) { + TransportFailureReason::DnsFailure => log("DNS lookup failed — check network config"), + TransportFailureReason::TlsHandshake => log("TLS error — check certificate chain"), + TransportFailureReason::SocketTimeout => log("Connect timeout — PostNL may be slow"), + TransportFailureReason::ConnectionRefused => log("Connection refused — check firewall"), + TransportFailureReason::Unknown => log("Unknown transport error"), + }; +} +``` + +--- + +## Schema Mismatch Exceptions + +`SchemaMismatchException` is thrown when PostNL returns a response that does not conform to the SDK's expected schema — a required field is absent or a field value cannot be cast to the required type. This is a PostNL API contract break, not a programmer error. + +- Extends `PostnlSdkException` (runtime exception root) +- Implements `ServerErrorExceptionInterface` (PostNL returned something the SDK cannot parse) +- Carries `$targetClass` and `$field` public readonly properties for structured logging + +| Property | Type | Description | +|----------|------|-------------| +| `$targetClass` | `string` | The payload class that could not be hydrated | +| `$field` | `string` | The payload key of the offending field | + +### Named Constructors + +| Method | Thrown when | +|--------|-------------| +| `SchemaMismatchException::missingField($class, $field)` | A required (non-nullable, no default) field is absent from the response | +| `SchemaMismatchException::typeMismatch($class, $field, $expected, $actual)` | A field value cannot be coerced to the required type | + +### Example + +```php +use Postnl\Sdk\Exception\SchemaMismatchException; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); +} catch (SchemaMismatchException $e) { + // PostNL returned an incompatible response — page SDK maintainers + log("Schema mismatch in {$e->targetClass} field {$e->field}: " . $e->getMessage()); +} +``` + +--- + +## SDK Exceptions + +These exceptions are thrown for programming errors and SDK internal failures, not HTTP responses: + +| Exception | Base Class | When Thrown | +|-----------|------------|------------| +| `SdkRuntimeException` | `RuntimeException` | Internal SDK errors | +| `SdkLogicException` | `LogicException` | Programmer errors (e.g., invalid builder usage) | +| `SdkInvalidArgumentException` | `InvalidArgumentException` | Invalid arguments passed to SDK methods | + +These are documented in their respective service guides: +- Builder validation: [Configuration](../Configuration/README.md) +- Extension errors: [Extension](../Extension/README.md) + +--- + +## Catch Patterns + +### Simple — Catch All SDK Exceptions + +```php +use Postnl\Sdk\Exception\PostnlExceptionInterface; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); +} catch (PostnlExceptionInterface $e) { + echo "SDK error: " . $e->getMessage() . "\n"; +} +``` + +### Capability-based — Horizontal Markers + +```php +use Postnl\Sdk\Exception\AuthExceptionInterface; +use Postnl\Sdk\Exception\ClientErrorExceptionInterface; +use Postnl\Sdk\Exception\SchemaMismatchException; +use Postnl\Sdk\Exception\ServerErrorExceptionInterface; +use Postnl\Sdk\Exception\TransportExceptionInterface; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); +} catch (AuthExceptionInterface $e) { + // Refresh creds, page auth oncall +} catch (TransportExceptionInterface $e) { + // Check $e->getFailureReason() for DNS/TLS/timeout +} catch (SchemaMismatchException $e) { + // API contract break — page SDK maintainers +} catch (ServerErrorExceptionInterface $e) { + // PostNL is down or returned unrecognisable data +} catch (ClientErrorExceptionInterface $e) { + // Caller-side error; typically don't retry +} +``` + +### Standard — HTTP-Aware + +```php +use Postnl\Sdk\Exception\Client\AuthenticationException; +use Postnl\Sdk\Exception\Client\ValidationException; +use Postnl\Sdk\Exception\HttpSdkException; +use Postnl\Sdk\Exception\Transport\TransportException; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); +} catch (ValidationException $e) { + // 400, 422 — inspect field errors + foreach ($e->fieldErrors as $error) { + echo " {$error->field}: {$error->message}\n"; + } + +} catch (AuthenticationException $e) { + // 401, 403 — check credentials + echo "Authentication failed: " . $e->getMessage() . "\n"; + +} catch (HttpSdkException $e) { + // All other HTTP errors (4xx, 5xx) + echo "[{$e->statusCode}] " . $e->getMessage() . "\n"; + +} catch (TransportException $e) { + // Network failure — no HTTP response + echo "Network error: " . $e->getMessage() . "\n"; +} +``` + +### Production — Full Coverage + +```php +use Postnl\Sdk\Exception\Client\AuthenticationException; +use Postnl\Sdk\Exception\Client\RateLimitException; +use Postnl\Sdk\Exception\Client\ValidationException; +use Postnl\Sdk\Exception\HttpSdkException; +use Postnl\Sdk\Exception\Retry\RetryExhaustedException; +use Postnl\Sdk\Exception\SchemaMismatchException; +use Postnl\Sdk\Exception\Transport\TransportException; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); + +} catch (ValidationException $e) { + foreach ($e->fieldErrors as $error) { + log("Validation: {$error->field} — {$error->message}"); + } + +} catch (AuthenticationException $e) { + log("Auth failed: " . $e->getMessage()); + +} catch (RateLimitException $e) { + $retryAfter = $e->retryAfterSeconds(); + log("Rate limited, retry after {$retryAfter}s"); + +} catch (RetryExhaustedException $e) { + log("Gave up after {$e->attempts} attempts: " . $e->getPrevious()?->getMessage()); + foreach ($e->failedAttempts as $entry) { + log("Attempt {$entry['attempt']}: " . $entry['exception']->getMessage()); + } + +} catch (HttpSdkException $e) { + log("[HTTP {$e->statusCode}] " . $e->getMessage()); + if ($e->problemDetails->traceId !== null) { + log("Trace ID: " . $e->problemDetails->traceId); + } + +} catch (TransportException $e) { + log("Network error ({$e->failureReason->value}): " . $e->getMessage()); + +} catch (SchemaMismatchException $e) { + log("API schema break — {$e->targetClass}::{$e->field}: " . $e->getMessage()); +} +``` + +--- + +## Exception Hierarchy Reference + +``` +PostnlExceptionInterface (marker — catch-all for any SDK exception) +├── PostnlSdkException (extends RuntimeException) +│ ├── HttpSdkException (abstract — all HTTP error responses) +│ │ ├── Client\ValidationException (400, 422) +│ │ ├── Client\AuthenticationException (401, 403) +│ │ ├── Client\ClientException (other 4xx) +│ │ └── RetryableHttpSdkException (abstract — retryable HTTP errors) +│ │ ├── Client\RateLimitException (429) +│ │ ├── Client\TimeoutException (408) +│ │ └── Server\ServerException (5xx, Cloudflare 521-524) +│ ├── Transport\TransportException (network failures, always retryable) +│ ├── SchemaMismatchException (API contract break) [ServerErrorExceptionInterface] +│ ├── Retry\RetryExhaustedException (all retries exhausted) +│ └── SdkRuntimeException (internal SDK errors) +├── SdkLogicException (extends LogicException — programmer errors) +└── SdkInvalidArgumentException (extends InvalidArgumentException) +``` + +All exceptions implement `PostnlExceptionInterface`, so `catch (PostnlExceptionInterface $e)` covers every exception the SDK can throw. + +--- + diff --git a/docs/postnl-v4-migration/sources/sdk-docs/Extension/README.md b/docs/postnl-v4-migration/sources/sdk-docs/Extension/README.md new file mode 100644 index 00000000..0df13675 --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/Extension/README.md @@ -0,0 +1,819 @@ +# SDK Extension System Documentation + +The Extension system allows developers to add custom API endpoints to the PostNL SDK while leveraging the SDK's built-in infrastructure including HTTP transport (with full middleware stack), authentication, logging, retries, and optional caching. + +Extensions are ideal for: +- Integrating with PostNL APIs not yet covered by the SDK +- Creating reusable custom service wrappers +- Prototyping new API integrations +- Adding organization-specific API functionality + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Quick Start](#quick-start) +- [Closure-Based Extensions](#closure-based-extensions) +- [Class-Based Extensions](#class-based-extensions) +- [Using ConfigurableAction](#using-configurableaction) +- [Credential Strategies](#credential-strategies) +- [Using Cache Adapters](#using-cache-adapters) +- [Error Handling](#error-handling) +- [Validation and Extension IDs](#validation-and-extension-ids) +- [Best Practices](#best-practices) +- [Complete Examples](#complete-examples) + +--- + +## Prerequisites + +### SDK Setup + +See the main SDK setup guide in the root documentation: +[SDK Root Documentation](../../README.md) + +### Requirements + +- PHP 8.2+ +- An initialized `Postnl` client instance + +--- + +## Quick Start + +The SDK supports two extension approaches: **closure-based** (quick, inline) and **class-based** (reusable, testable). + +### Closure-Based (Inline) + +```php +use Postnl\Sdk\Action\ConfigurableAction; +use Postnl\Sdk\Service\Checkout\V1\Response\PostalCodeAddressResponse; +use Postnl\Sdk\Enums\HttpMethod; +use Postnl\Sdk\Enums\CredentialStrategy; +use Postnl\Sdk\Service\ServiceContext; +use Psr\Http\Message\ResponseInterface; + +// Register an inline extension +$postnl->extensions()->register('postal-code-check', fn(ServiceContext $context) => new ConfigurableAction( + context: $context, + endpoint: '/shipment/checkout/v1/postalcodecheck', + httpMethod: HttpMethod::GET, + credentialStrategy: CredentialStrategy::NONE, + responseFactory: static fn(ResponseInterface $r) => new PostalCodeAddressResponse($r), +)); + +// Resolve the extension explicitly +$action = $postnl->extensions()->getAs('postal-code-check', ConfigurableAction::class); +$response = $action->execute($payload); +``` + +### Class-Based (Reusable) + +```php +use Postnl\Sdk\Action\CacheableConfigurableAction; +use Postnl\Sdk\Service\Checkout\V1\Extension\PostalCodeCheckExtension; + +// Register a class-based extension +$postnl->extensions()->register('postal-code-check', PostalCodeCheckExtension::class); + +// Resolve the typed extension explicitly +$action = $postnl->extensions()->getAs('postal-code-check', CacheableConfigurableAction::class); +$response = $action->execute($payload); +``` + +--- + +## Closure-Based Extensions + +Closure-based extensions are ideal for quick prototypes, simple integrations, and development/testing scenarios. + +### Closure Signature + +```php +fn(ServiceContext $context): object +``` + +### Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `$context` | `ServiceContext` | SDK context exposing `transport`, `apiVersion`, `identity`, `logger`, `cache`, and `payloadMapper` | + +Closures should accept the current `ServiceContext` and return the extension service object. + +### Code Example + +```php +use Postnl\Sdk\Action\ConfigurableAction; +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Enums\CredentialStrategy; +use Postnl\Sdk\Enums\HttpMethod; +use Postnl\Sdk\Service\Checkout\V1\Response\PostalCodeAddressResponse; +use Postnl\Sdk\Service\ServiceContext; +use Psr\Http\Message\ResponseInterface; + +$postnl = Postnl::factory()->withAuth(Auth::fromEnv())->make(); + +// Register a closure-based extension +$postnl->extensions()->register('postal-code-check', fn(ServiceContext $context) => new ConfigurableAction( + context: $context, + endpoint: '/shipment/checkout/v1/postalcodecheck', + httpMethod: HttpMethod::GET, + credentialStrategy: CredentialStrategy::NONE, + responseFactory: static fn(ResponseInterface $r) => new PostalCodeAddressResponse($r), +)); + +// Use the extension +$action = $postnl->extensions()->getAs('postal-code-check', ConfigurableAction::class); +$response = $action->execute($payload); +``` + +--- + +## Class-Based Extensions + +Class-based extensions are recommended for production code, reusable components, and scenarios requiring unit testing. + +### ExtensionInterface Contract + +```php +use Postnl\Sdk\Extension\ExtensionInterface; +use Postnl\Sdk\Service\ServiceContext; + +interface ExtensionInterface +{ + /** + * Create the extension service instance. + * + * @param ServiceContext $context SDK context (transport, apiVersion, identity, cache, logger) + * @return object The service instance (user-defined type) + */ + public function create(ServiceContext $context): object; +} +``` + +### Implementation Example + +```php +use Postnl\Sdk\Action\CacheableConfigurableAction; +use Postnl\Sdk\Action\ConfigurableAction; +use Postnl\Sdk\Enums\CredentialStrategy; +use Postnl\Sdk\Enums\HttpMethod; +use Postnl\Sdk\Extension\ExtensionInterface; +use Postnl\Sdk\Service\Checkout\V1\Response\PostalCodeAddressResponse; +use Postnl\Sdk\Service\ServiceContext; +use Psr\Http\Message\ResponseInterface; + +class PostalCodeCheckExtension implements ExtensionInterface +{ + public function create(ServiceContext $context): object + { + $action = new ConfigurableAction( + context: $context, + endpoint: '/shipment/checkout/v1/postalcodecheck', + httpMethod: HttpMethod::GET, + credentialStrategy: CredentialStrategy::NONE, + responseFactory: static fn(ResponseInterface $r) => new PostalCodeAddressResponse($r), + ); + + // Wrap with CacheableConfigurableAction for opt-in caching support + return new CacheableConfigurableAction($action, $context->cache); + } +} +``` + +> **Note:** Wrapping with `CacheableConfigurableAction` enables opt-in caching via the `cache($ttl)` method. Without calling `cache()`, requests execute normally without caching. + +### Registration and Usage + +```php +use Postnl\Sdk\Action\CacheableConfigurableAction; +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; + +$postnl = Postnl::factory()->withAuth(Auth::fromEnv())->make(); + +// Register the extension +$postnl->extensions()->register('postal-code-check', PostalCodeCheckExtension::class); + +$action = $postnl->extensions()->getAs('postal-code-check', CacheableConfigurableAction::class); + +// Use the extension without caching +$response = $action->execute($payload); + +// Use the extension with caching (TTL in seconds) +$response = $action->cache(3600)->execute($payload); + +// Use indefinite caching (TTL = 0) +$response = $action->cache(0)->execute($payload); +``` + +### Returning Custom Service Classes + +Extensions can return any object type, not just `ConfigurableAction`. For complex APIs with multiple endpoints, return a custom service class: + +```php +use Postnl\Sdk\Extension\ExtensionInterface; +use Postnl\Sdk\Service\ServiceContext; + +class TrackingExtension implements ExtensionInterface +{ + public function create(ServiceContext $context): object + { + return new TrackingService($context); + } +} + +class TrackingService +{ + public function __construct( + private ServiceContext $context + ) {} + + public function getStatus(string $barcode): TrackingResponse + { + // Implementation using $this->context->transport + } + + public function getHistory(string $barcode): TrackingHistoryResponse + { + // Implementation using $this->context->transport + } +} +``` + +--- + +## Using ConfigurableAction + +`ConfigurableAction` is a utility class that simplifies creating single API endpoint calls without writing a dedicated action class. + +### Constructor Parameters + +| Parameter | Type | Required | Default | Description | +|-----------|------|----------|---------|-------------| +| `context` | `ServiceContext` | Yes | - | SDK infrastructure context (transport, identity, cache, etc.) | +| `endpoint` | `string` | Yes | - | API endpoint path (e.g., `/shipment/delivery/v4/track`) | +| `httpMethod` | `HttpMethod` | No | `POST` | HTTP method enum (`GET`, `POST`, `PUT`, `DELETE`) | +| `credentialStrategy` | `CredentialStrategy` | No | `ROOT` | How credentials are merged into payload | +| `responseFactory` | `Closure(ResponseInterface): ApiResponseInterface` | Yes | - | Factory to wrap PSR-7 response into API response | + +### The execute() Method + +```php +public function execute(RequestPayloadInterface $payload): ApiResponseInterface +``` + +The `execute()` method sends the request and returns the response produced by the configured `responseFactory`. + +### Code Examples + +#### GET Request + +```php +use Postnl\Sdk\Support\PayloadMapper; +use Psr\Http\Message\ResponseInterface; + +$action = new ConfigurableAction( + context: $context, + endpoint: '/shipment/checkout/v1/postalcodecheck', + httpMethod: HttpMethod::GET, + credentialStrategy: CredentialStrategy::NONE, + responseFactory: static fn(ResponseInterface $r) => new CheckoutResponse($r), +); + +$mapper = PayloadMapper::create(); +$payload = PostalCodeCheckRequest::fromArray([ + 'postalcode' => '2521CA', + 'housenumber' => '3', +], $mapper); + +$response = $action->execute($payload); +``` + +#### POST Request + +```php +$action = new ConfigurableAction( + context: $context, + endpoint: '/shipment/v4/label', + httpMethod: HttpMethod::POST, + credentialStrategy: CredentialStrategy::ROOT, + responseFactory: static fn(ResponseInterface $r) => new LabelResponse($r), +); + +$response = $action->execute($labelPayload); +``` + +--- + +## Credential Strategies + +The `CredentialStrategy` enum controls how `customerCode` and `customerNumber` are automatically merged into request payloads. + +### Available Strategies + +| Strategy | Enum Value | Description | +|----------|------------|-------------| +| `ROOT` | `CredentialStrategy::ROOT` | Merge credentials at payload root level | +| `INTO_SENDER` | `CredentialStrategy::INTO_SENDER` | Merge into the `sender` nested object | +| `INTO_RECEIVER` | `CredentialStrategy::INTO_RECEIVER` | Merge into the `receiver` nested object | +| `NONE` | `CredentialStrategy::NONE` | Do not merge credentials (caller must provide) | + +### Usage + +```php +use Postnl\Sdk\Enums\CredentialStrategy; + +// Credentials merged at root: { "customerCode": "...", "customerNumber": "...", ... } +$action = new ConfigurableAction( + // ... + credentialStrategy: CredentialStrategy::ROOT, +); + +// Credentials merged into sender: { "sender": { "customerCode": "...", ... } } +$action = new ConfigurableAction( + // ... + credentialStrategy: CredentialStrategy::INTO_SENDER, +); + +// No credential merging (use for APIs that don't require credentials in payload) +$action = new ConfigurableAction( + // ... + credentialStrategy: CredentialStrategy::NONE, +); +``` + +### When to Use Each Strategy + +| Strategy | Use Case | +|----------|----------| +| `ROOT` | Most common. APIs expecting flat credentials at payload root. | +| `INTO_SENDER` | Labelling/Confirming APIs with sender object structure. | +| `INTO_RECEIVER` | Return shipment APIs with receiver object structure. | +| `NONE` | APIs that don't require credentials in payload, or when handling credentials manually. | + +--- + +## Using Cache Adapters + +Extensions automatically receive the configured cache adapter from the `Postnl` client. This allows extensions to cache API responses for improved performance. + +### Configuring Cache on the Client + +#### Using CacheConfig Object + +Create a `CacheConfig`, resolve it to a concrete adapter via `CacheFactory::create()`, and pass it to the builder with `withCache()`: + +```php +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Cache\CacheConfig; +use Postnl\Sdk\Cache\CacheFactory; +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Enums\CacheStoreType; + +$cache = CacheFactory::create(new CacheConfig( + cacheStoreType: CacheStoreType::REDIS, + redisHost: '127.0.0.1', + redisPort: 6379, + defaultTtl: 3600, + prefix: 'sdk_postnl_', +)); + +$postnl = Postnl::factory() + ->withAuth(Auth::apiKey('your-api-key')) + ->withCache($cache) + ->make(); +``` + +#### Using fromArray Configuration + +```php +$config = ClientConfig::fromArray([ + 'authenticationType' => 'apiKey', + 'apiKey' => 'your-api-key', + 'cache' => [ + 'cacheStoreType' => 'redis', // 'auto', 'redis', 'memcached', 'file', 'array' + 'redisHost' => '127.0.0.1', + 'redisPort' => 6379, + 'defaultTtl' => 3600, + ], +]); + +$postnl = PostnlFacade::sandboxClient('your-api-key'); +``` + +#### Using Environment Variables + +```bash +# Cache configuration +SDK_POSTNL_CACHE_STORE_TYPE=redis # auto, redis, memcached, file, array +SDK_POSTNL_CACHE_TTL=3600 +SDK_POSTNL_CACHE_PREFIX=sdk_postnl_ + +# Redis configuration +SDK_POSTNL_REDIS_HOST=127.0.0.1 +SDK_POSTNL_REDIS_PORT=6379 +SDK_POSTNL_REDIS_PASSWORD= +SDK_POSTNL_REDIS_DATABASE=0 +``` + +```php +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; + +$postnl = Postnl::factory()->withAuth(Auth::fromEnv())->make(); +``` + +### Available Cache Store Types + +| Type | Description | +|------|-------------| +| `auto` | Auto-detect best available backend (Redis > Memcached > File > Array) | +| `redis` | Redis backend (requires `phpredis` extension) | +| `memcached` | Memcached backend (requires `memcached` extension) | +| `file` | File-based persistent cache | +| `array` | In-memory cache (request-scoped, useful for testing) | + +### Using Cache in Extensions + +The cache adapter is available on the `ServiceContext` passed to extensions (`$context->cache`). + +#### Closure-Based Extension with Cache + +```php +use Postnl\Sdk\Action\CacheableConfigurableAction; +use Postnl\Sdk\Action\ConfigurableAction; +use Postnl\Sdk\Enums\CredentialStrategy; +use Postnl\Sdk\Enums\HttpMethod; +use Postnl\Sdk\Service\ServiceContext; +use Psr\Http\Message\ResponseInterface; + +// Access cache via $context->cache; wrap with CacheableConfigurableAction for opt-in caching +$postnl->extensions()->register('postal-code-check', fn(ServiceContext $context) => new CacheableConfigurableAction( + new ConfigurableAction( + context: $context, + endpoint: '/shipment/checkout/v1/postalcodecheck', + httpMethod: HttpMethod::GET, + credentialStrategy: CredentialStrategy::NONE, + responseFactory: static fn(ResponseInterface $r) => new CheckoutResponse($r), + ), + $context->cache +)); +``` + +#### Class-Based Extension with Cache + +```php +use Postnl\Sdk\Action\CacheableConfigurableAction; +use Postnl\Sdk\Action\ConfigurableAction; +use Postnl\Sdk\Enums\CredentialStrategy; +use Postnl\Sdk\Enums\HttpMethod; +use Postnl\Sdk\Extension\ExtensionInterface; +use Postnl\Sdk\Service\Checkout\V1\Response\PostalCodeAddressResponse; +use Postnl\Sdk\Service\ServiceContext; +use Psr\Http\Message\ResponseInterface; + +class PostalCodeCheckExtension implements ExtensionInterface +{ + public function create(ServiceContext $context): object + { + $action = new ConfigurableAction( + context: $context, + endpoint: '/shipment/checkout/v1/postalcodecheck', + httpMethod: HttpMethod::GET, + credentialStrategy: CredentialStrategy::NONE, + responseFactory: static fn(ResponseInterface $r) => new PostalCodeAddressResponse($r), + ); + + return new CacheableConfigurableAction($action, $context->cache); + } +} + +// Usage with caching +$action = $postnl->extensions()->getAs('postal-code-check', CacheableConfigurableAction::class); +$response = $action->cache(3600)->execute($payload); +``` + +### Custom Caching in Services + +For custom service classes, you can implement your own caching logic using the cache adapter from the context: + +```php +use Postnl\Sdk\Service\ServiceContext; + +class CachedTrackingService +{ + private const CACHE_PREFIX = 'tracking_'; + + public function __construct( + private ServiceContext $context + ) {} + + public function getStatus(string $barcode): array + { + $cacheKey = self::CACHE_PREFIX . $barcode; + $cache = $this->context->cache; + + // Try to get from cache + if ($cache !== null) { + $cached = $cache->get($cacheKey); + if ($cached !== null) { + return $cached; + } + } + + // Cache miss - fetch from API + $result = $this->fetchFromApi($barcode); + + // Store in cache (TTL: 5 minutes) + if ($cache !== null) { + $cache->set($cacheKey, $result, 300); + } + + return $result; + } +} +``` + +### Cache Adapter Methods (PSR-16) + +| Method | Description | +|--------|-------------| +| `get(string $key, mixed $default = null)` | Retrieve value or return default | +| `set(string $key, mixed $value, int\|null $ttl = null)` | Store value with optional TTL | +| `has(string $key)` | Check if key exists | +| `delete(string $key)` | Delete a single key | +| `clear()` | Clear all cached entries | + +--- + +## Error Handling + +### Exception Types + +| Exception | When Thrown | +|-----------|-------------| +| `SdkInvalidArgumentException` | Empty extension ID, invalid extension ID, invalid factory class, or `getAs()` type mismatch / missing expected class | +| `UnknownExtensionException` | Resolving an unregistered extension ID | +| `SdkRuntimeException` | Factory execution failure or factory returns non-object | + +### Common Validation Errors + +| Error Scenario | Message | +|----------------|---------| +| Empty ID | `Extension ID cannot be empty.` | +| Invalid ID | `Extension ID "Tracking" must contain only lowercase letters, digits, ".", "_" or "-".` | +| Non-existent class | `Extension class "Foo\Bar" does not exist.` | +| Missing interface | `Extension class "stdClass" must implement ExtensionInterface.` | +| Type mismatch | `Extension "tracking" resolved to stdClass, expected App\TrackingService.` | + +### Example Error Handling + +```php +use Postnl\Sdk\Exception\InvalidArgumentSdkException; +use Postnl\Sdk\Exception\UnknownExtensionException; +use Postnl\Sdk\Exception\RuntimeSdkException; +use Postnl\Sdk\Exception\PostnlSdkException; + +try { + $postnl->extensions()->register('tracking', TrackingExtension::class); + $tracking = $postnl->extensions()->getAs('tracking', TrackingService::class); + $response = $tracking->getStatus($request); + +} catch (InvalidArgumentSdkException $e) { + // Registration or type validation failed + echo "Argument error: " . $e->getMessage(); + +} catch (UnknownExtensionException $e) { + // Extension not registered + echo "Extension not found: " . $e->getMessage(); + +} catch (RuntimeSdkException $e) { + // Factory execution failed + echo "Factory error: " . $e->getMessage(); + +} catch (PostnlSdkException $e) { + // API request failed + echo "API error: " . $e->getMessage(); +} +``` + +--- + +## Validation and Extension IDs + +### Extension ID Rules + +- ID cannot be empty +- ID must contain only lowercase letters, digits, `.`, `_`, or `-` +- IDs do not share the main client method namespace, so built-in service methods never collide with extension IDs + +### Factory Validation + +- **Closures**: Expected shape is `fn(ServiceContext $context): object` +- **Class strings**: Must exist and implement `ExtensionInterface` + +--- + +## Best Practices + +### Naming Conventions + +- Use lowercase IDs such as `tracking`, `address-validation`, or `postal-code-check` +- Be descriptive but concise (for example `bulk-shipment` over `bs`) +- Avoid generic names like `api`, `service`, or `custom` + +### Performance + +- Extensions are **lazily resolved** (created only when first called) +- Extensions are **cached as singletons** per `Postnl` instance +- Re-registering an extension clears the cached instance +- Use the cache adapter for repetitive API calls + +### Registration Lifecycle + +- Register extensions during application bootstrap, service provider setup, or container wiring +- Avoid registering extensions inside request handlers when the client is shared as a singleton +- Laravel: register extensions in a service provider after constructing the shared client instance +- Symfony: register extensions in the service definition or bundle extension that wires the client + +### Testing + +```php +use Postnl\Sdk\Service\ServiceContext; + +// Unit testing class-based extensions +public function testExtensionCreatesConfigurableAction(): void +{ + $context = $this->createMock(ServiceContext::class); + + $extension = new PostalCodeCheckExtension(); + $result = $extension->create($context); + + $this->assertInstanceOf(ConfigurableAction::class, $result); +} +``` + +### Retry Opt-Out for Non-Idempotent Endpoints + +POST requests are retried by default because most PostNL POST endpoints are effectively idempotent +(e.g. label generation returns the same label for the same payload within a session). If your +endpoint has side effects that must not be duplicated (e.g. creating a new return shipment record +on every call), pass `retryable: false`: + +```php +use Postnl\Sdk\Action\ConfigurableAction; +use Postnl\Sdk\Enums\CredentialStrategy; +use Postnl\Sdk\Enums\HttpMethod; +use Postnl\Sdk\Service\ReturnShipment\V4\Response\GenerateReturnResponse; +use Postnl\Sdk\Service\ServiceContext; +use Psr\Http\Message\ResponseInterface; + +$postnl->extensions()->register('generate-return', fn(ServiceContext $context) => new ConfigurableAction( + context: $context, + endpoint: '/shipment/delivery/v4/return/generate', + httpMethod: HttpMethod::POST, + credentialStrategy: CredentialStrategy::ROOT, + retryable: false, // non-idempotent — each call creates a new return shipment record + responseFactory: static fn(ResponseInterface $r) => new GenerateReturnResponse($r), +)); +``` + +When `retryable: false`, the transport calls the endpoint exactly once and propagates any exception +directly without entering the retry loop. + +### Security + +- Use `CredentialStrategy::NONE` when credentials should not be automatically merged +- Validate user input before passing to extension methods +- Do not expose `TransportInterface` instances publicly + +--- + +## Complete Examples + +### Example 1: Postal Code Check (From SDK) + +This example shows the built-in `PostalCodeCheckExtension`: + +```php +use Postnl\Sdk\Action\ConfigurableAction; +use Postnl\Sdk\Action\CacheableConfigurableAction; +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Enums\CredentialStrategy; +use Postnl\Sdk\Enums\HttpMethod; +use Postnl\Sdk\Exception\PostnlSdkException; +use Postnl\Sdk\RequestData\V1\PostalCodeCheckRequest; +use Postnl\Sdk\Service\Checkout\V1\Extension\PostalCodeCheckExtension; +use Postnl\Sdk\Service\Checkout\V1\Response\PostalCodeAddressResponse; +use Postnl\Sdk\Service\ServiceContext; +use Postnl\Sdk\Support\PayloadMapper; +use Psr\Http\Message\ResponseInterface; + +$postnl = Postnl::factory()->withAuth(Auth::fromEnv())->make(); + +try { + // Create request payload + $mapper = PayloadMapper::create(); + $payload = PostalCodeCheckRequest::fromArray([ + 'postalcode' => '2521CA', + 'housenumber' => '3', + 'housenumberaddition' => 'bis', + ], $mapper); + + // Option 1: Class-based extension + $postnl->extensions()->register('postal-code-check', PostalCodeCheckExtension::class); + $action = $postnl->extensions()->getAs('postal-code-check', CacheableConfigurableAction::class); + + $response = $action->execute($payload); + + // Option 2: Closure-based extension (inline) + $postnl->extensions()->register('postal-code-check', fn(ServiceContext $context) => new ConfigurableAction( + context: $context, + endpoint: '/shipment/checkout/v1/postalcodecheck', + httpMethod: HttpMethod::GET, + credentialStrategy: CredentialStrategy::NONE, + responseFactory: static fn(ResponseInterface $r) => new PostalCodeAddressResponse($r), + )); + + $action = $postnl->extensions()->getAs('postal-code-check', ConfigurableAction::class); + $response = $action->execute($payload); + + // Process response + if ($response->isSuccess()) { + echo json_encode($response->meta()->toArray(), JSON_PRETTY_PRINT); + } + +} catch (PostnlSdkException $e) { + echo "Error: " . $e->getMessage(); +} +``` + +### Example 2: Custom Tracking Service (Multi-Action) + +This example shows a class-based extension returning a custom service with multiple methods: + +```php +use Postnl\Sdk\Extension\ExtensionInterface; +use Postnl\Sdk\Service\ServiceContext; + +// Extension class +class TrackingExtension implements ExtensionInterface +{ + public function create(ServiceContext $context): object + { + return new TrackingService($context); + } +} + +// Custom service class +class TrackingService +{ + public function __construct( + private ServiceContext $context + ) {} + + public function getStatus(string $barcode): array + { + // Use $this->context->transport to make API calls + // ... + } + + public function getFullHistory(string $barcode): array + { + // Use $this->context->transport to make API calls + // ... + } +} + +// Usage +$postnl->extensions()->register('tracking', TrackingExtension::class); + +$trackingService = $postnl->extensions()->getAs('tracking', TrackingService::class); +$status = $trackingService->getStatus('3SDEVC123456789'); +$history = $trackingService->getFullHistory('3SDEVC123456789'); +``` + +### Example 3: Bootstrap Registration + +Register extensions once during application bootstrap, then resolve them explicitly where needed: + +```php +// Bootstrap time +$postnl + ->extensions() + ->register('postal-code-check', PostalCodeCheckExtension::class) + ->register('tracking', TrackingExtension::class) + ->register('address-validation', AddressValidationExtension::class); + +// Later in request handling / application services +$postalCodeCheck = $postnl->extensions()->getAs('postal-code-check', CacheableConfigurableAction::class); +$tracking = $postnl->extensions()->getAs('tracking', TrackingService::class); +$validation = $postnl->extensions()->getAs('address-validation', AddressValidationService::class); + +$postalCodeResult = $postalCodeCheck->execute($payload); +$trackingResult = $tracking->getStatus($barcode); +$validationResult = $validation->validate($address); +``` diff --git a/docs/postnl-v4-migration/sources/sdk-docs/Labelling/README.md b/docs/postnl-v4-migration/sources/sdk-docs/Labelling/README.md new file mode 100644 index 00000000..66bfdfc8 --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/Labelling/README.md @@ -0,0 +1,779 @@ +# Labelling API Documentation + +The Labelling service allows you to generate shipping labels for your PostNL shipments. This service creates labels that can be printed and attached to packages for delivery. Labels include barcodes for tracking and all necessary shipping information. + +For confirming shipments without generating labels, see the [Confirming service](../Confirming/README.md). + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Generate Shipping Label](#generate-shipping-label) +- [Label Settings](#label-settings) +- [Data Models](#data-models) +- [Collection Methods](#collection-methods) +- [Error Handling](#error-handling) +- [Enums Reference](#enums-reference) +- [Complete Example](#complete-example) + +--- + +## Prerequisites + +### SDK Setup + +See the main SDK setup guide in the root documentation: +[SDK Root Documentation](../../README.md) + +--- + +### Required Credentials + +All Labelling requests require: +- `customerCode` - Your PostNL customer code +- `customerNumber` - Your PostNL customer number + +These are automatically injected into the sender object by the SDK. + +--- + +## Generate Shipping Label + +Generate a shipping label for a shipment. + +### Endpoint + +``` +POST /shipment/delivery/v4/label +``` + +### Request Parameters (ShipmentDeliveryRequest) + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `sender` | ShipmentParty | Yes | Sender details including customer credentials and address | +| `receiver` | ShipmentParty | Yes | Receiver contact and address information | +| `labelSettings` | LabelSettings | No | Label format and output configuration | +| `returnOptions` | ReturnOptions | No | Return shipment options | +| `shipmentType` | ShipmentType | No | Type of shipment (default: `parcel`) | +| `handOverDate` | string | No | Date when shipment is handed to PostNL (`YYYY-MM-DD`) | +| `deliveryLocation` | DeliveryLocation | No | Alternative delivery location | +| `services` | Services | No | Additional shipment services | +| `internationalShipmentData` | InternationalShipmentData | No | Data for international shipments | +| `itemCount` | int | No | Number of items in shipment (max 999, default: 1) | +| `items` | ShippingItem[] | No | Individual item details with barcodes | + +### Code Examples + +#### Using Constructor + +```php +use Postnl\Sdk\Enums\Payload\Country; +use Postnl\Sdk\Enums\Payload\LabelOutputType; +use Postnl\Sdk\Enums\Payload\ShipmentType; +use Postnl\Sdk\RequestData\V4\Address; +use Postnl\Sdk\RequestData\V4\Contact; +use Postnl\Sdk\RequestData\V4\LabelSettings; +use Postnl\Sdk\RequestData\V4\ShipmentParty; +use Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest; + +$request = new ShipmentDeliveryRequest( + sender: ShipmentParty::asSender( + customerNumber: 'YOUR_CUSTOMER_NUMBER', + customerCode: 'YOUR_CUSTOMER_CODE', + address: new Address( + companyName: 'Your Company', + street: 'Siriusdreef', + houseNumber: '42', + postalCode: '2132WT', + city: 'Hoofddorp', + countryIso: Country::NL, + ), + ), + receiver: ShipmentParty::asReceiver( + address: new Address( + street: 'Waldorpstraat', + houseNumber: '3', + postalCode: '2521CA', + city: 'Den Haag', + countryIso: Country::NL, + ), + contact: new Contact( + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + ), + ), + labelSettings: new LabelSettings( + outputType: LabelOutputType::PDF, + ), + shipmentType: ShipmentType::Parcel, + handOverDate: date('Y-m-d', strtotime('+1 day')), +); + +$response = $postnl->labelling()->requestLabel($request); +``` + +#### Using fromArray Factory + +```php +use Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = ShipmentDeliveryRequest::fromArray([ + 'sender' => [ + 'customerNumber' => 'YOUR_CUSTOMER_NUMBER', + 'customerCode' => 'YOUR_CUSTOMER_CODE', + 'address' => [ + 'companyName' => 'Your Company', + 'street' => 'Siriusdreef', + 'houseNumber' => '42', + 'postalCode' => '2132WT', + 'city' => 'Hoofddorp', + 'countryIso' => 'NL', + ], + ], + 'receiver' => [ + 'contact' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'email' => 'john.doe@example.com', + ], + 'address' => [ + 'street' => 'Waldorpstraat', + 'houseNumber' => '3', + 'postalCode' => '2521CA', + 'city' => 'Den Haag', + 'countryIso' => 'NL', + ], + ], + 'labelSettings' => [ + 'outputType' => 'pdf', + ], + 'type' => 'parcel', + 'handOverDate' => '2024-01-15', +], $mapper); + +$response = $postnl->labelling()->requestLabel($request); +``` + +#### Using Fluent Interface + +```php +use Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest; + +$request = (new ShipmentDeliveryRequest()) + ->sender($sender) + ->receiver($receiver) + ->labelSettings($labelSettings) + ->shipmentType(ShipmentType::Parcel) + ->handOverDate('2024-01-15') + ->services($services) + ->itemsCount(1); + +$response = $postnl->labelling()->requestLabel($request); +``` + +### API Response Structure + +```json +{ + "items": [ + { + "shipmentReference": "REF-2024-001", + "barcode": "3SDEVC123456789", + "codingText": "D2132WT+42+0000000", + "labels": [ + { + "label": "JVBERi0xLjQK... (base64 encoded)", + "outputType": "pdf", + "labelType": "Label" + } + ], + "productService": { + "productData": "3085", + "services": ["002", "003"] + } + } + ] +} +``` + +### Working with the Response + +```php +$response = $postnl->labelling()->requestLabel($request); + +// Check response status +if ($response->isSuccess()) { + echo "Status: " . $response->meta()->statusCode; // 200 +} + +// Get the collection of shipping items +$collection = $response->shippingItems(); + +// Get total count +echo "Generated " . $collection->count() . " label(s)\n"; + +// Iterate over all items +foreach ($collection as $item) { + echo "Reference: " . $item->shipmentReference . "\n"; + echo "Barcode: " . $item->barcode . "\n"; + echo "Coding Text: " . $item->codingText . "\n"; + + // Save the label to a file + $label = $item->labels?->first(); + if ($label !== null && !$label->isEmpty()) { + $label->saveLabelAsFile('/path/to/labels/' . $item->barcode . '.pdf'); + echo "Label saved!\n"; + } + + // Access product service info + if ($item->productService !== null) { + echo "Product: " . $item->productService->productData . "\n"; + } +} + +// Get first item directly +$firstItem = $collection->first(); + +// Get raw array response +$rawData = $response->meta()->toArray(); + +// Get request correlation ID +$requestId = $response->meta()->requestId; + +// On HTTP 429, catch RateLimitException (see docs/ErrorHandling/README.md). +``` + +--- + +## Label Settings + +Configure label generation options using the `LabelSettings` class. + +### LabelSettings Properties + +| Property | Type | Description | +|----------|------|-------------| +| `outputType` | LabelOutputType | The file format of the label (pdf, zpl, gif, jpg, png) | +| `resolution` | LabelResolution | The resolution in DPI (200, 300, 600) | +| `pageOrientation` | LabelPageOrientation | The orientation of the page (portrait, landscape) | +| `mergeType` | LabelMergeType | Merge multiple labels into one document (singlepdf, pdfa6toa4) | +| `positioning` | LabelPositioning | Position of the label on the page (topleft, topright, bottomleft, bottomright) | +| `printMethod` | LabelPrintMethod | Specifies which party will print the label (consumerPrint, retailPrint) | + +### LabelSettings Example + +```php +use Postnl\Sdk\Enums\Payload\LabelMergeType; +use Postnl\Sdk\Enums\Payload\LabelOutputType; +use Postnl\Sdk\Enums\Payload\LabelPageOrientation; +use Postnl\Sdk\Enums\Payload\LabelPositioning; +use Postnl\Sdk\Enums\Payload\LabelPrintMethod; +use Postnl\Sdk\Enums\Payload\LabelResolution; +use Postnl\Sdk\RequestData\V4\LabelSettings; + +// PDF label for consumer printing +$labelSettings = new LabelSettings( + outputType: LabelOutputType::PDF, + resolution: LabelResolution::DPI_300, + pageOrientation: LabelPageOrientation::Portrait, + printMethod: LabelPrintMethod::ConsumerPrint, +); + +// ZPL label for thermal printers +$thermalLabelSettings = new LabelSettings( + outputType: LabelOutputType::ZPL, + resolution: LabelResolution::DPI_200, +); + +// Merged labels (A6 to A4) +$mergedLabelSettings = new LabelSettings( + outputType: LabelOutputType::PDF, + mergeType: LabelMergeType::PDFA6TOA4, + positioning: LabelPositioning::TopLeft, +); +``` + +### Print Method Recommendations + +| Print Method | Recommended Output Type | Use Case | +|--------------|------------------------|----------| +| `consumerPrint` | PDF | End consumers printing at home | +| `retailPrint` | PNG or JPG | Retail/store printing with image-based printers | + +--- + +## Data Models + +### ShipmentParty + +Represents a party (sender or receiver) in a shipment or return shipment. Use the static constructors to create parties for the correct role: + +| Static Method | Use Case | Parameters | +|---------------|----------|-------------| +| `asSender()` | Direct shipment sender (merchant dispatching) | customerNumber, customerCode, address, undeliverableReturnAddress? | +| `asReceiver()` | Direct shipment receiver (consumer receiving) | address, contact?, receiverType? | +| `asReturnSender()` | Return shipment sender (consumer returning) | address, contact? | +| `asReturnReceiver()` | Return shipment receiver (merchant receiving) | customerNumber, customerCode, address, contact? | + +```php +final readonly class ShipmentParty +{ + public ?string $customerNumber; // Customer number (max 10 chars) + public ?string $customerCode; // Customer code (max 6 chars) + public ?Address $address; // Postal address + public ?Address $undeliverableReturnAddress; // Return address for undeliverable items + public ?Contact $contact; // Contact information (name, email, phone) + public ?ReceiverType $receiverType; // business or consumer +} +``` + +### ProcessedShippingItem + +Represents a processed shipment item with its generated label. + +```php +readonly class ProcessedShippingItem +{ + public ?string $shipmentReference; // Your reference for the shipment + public ?string $returnReference; // Return shipment reference (e.g. label-in-the-box) + public ?LabelsCollection $labels; // Collection of generated labels + public ?string $barcode; // PostNL barcode for tracking + public ?string $returnBarcode; // Return parcel barcode + public ?string $partnerId; // Carrier-id of commercial network partner (last mile) + public ?string $partnerBarcode; // Partner barcode at commercial network partner + public ?string $codingText; // Sorting/routing code (e.g. letterbox NL) + public ?ProductService $productService; // Product and service details +} +``` + +### Label + +Represents label data returned from the API. + +```php +readonly class Label +{ + public ?string $label; // Base64 encoded label content + public ?string $partnerId; // Partner identifier + public ?string $partnerBarcode; // Partner's barcode reference + public ?string $mergedLabelContent; // Merged labels (base64) + public ?LabelOutputType $outputType; // Format: pdf, zpl, jpg, gif, png + public ?LabelType $labelType; // Type: Label, labelinthebox, shipmentandreturnlabel, retourLabel, CN23, CommercialInvoice +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `isEmpty()` | bool | Returns `true` if label has no content or output type | +| `saveLabelAsFile(string $filepath)` | void | Decode and save label to file | + +**Example - Saving a Label:** + +```php +$item = $collection->first(); +$label = $item->labels?->first(); + +if ($label !== null && !$label->isEmpty()) { + // Save as PDF + $label->saveLabelAsFile('/path/to/label.pdf'); + + // Or save with barcode as filename + $filename = $item->barcode . '.' . $label->outputType->value; + $label->saveLabelAsFile('/path/to/labels/' . $filename); +} +``` + +### ProductService + +Product and service information for the shipment. + +```php +readonly class ProductService +{ + public ?string $productData; // Product code identifier + public ?array $services; // Array of service codes + public ?array $bundles; // Bundle information +} +``` + +### ShippingItem (Request) + +Individual item details for multi-item shipments. + +```php +final readonly class ShippingItem +{ + public ?string $barcode; // Pre-generated barcode + public ?CustomerReferences $customerReferences; // Custom references + public ?Dimensions $dimensions; // Item dimensions +} +``` + +--- + +## Collection Methods + +`ShippingItemsCollection` provides methods for working with generated label items: + +### Base Methods + +| Method | Return | Description | +|--------|--------|-------------| +| `count()` | int | Number of items in collection | +| `isEmpty()` | bool | Returns `true` if collection is empty | +| `all()` | array | Returns all ProcessedShippingItem objects | +| `first()` | ?ProcessedShippingItem | Returns first item or null | +| `last()` | ?ProcessedShippingItem | Returns last item or null | +| `get(int $index)` | ?ProcessedShippingItem | Returns item at index or null | + +### Filtering Methods + +All filter methods return a new collection (immutable). + +```php +// Filter items with labels +$withLabels = $collection->filter(function ($item) { + return $item->labels !== null && !$item->labels->isEmpty(); +}); + +// Filter items with barcodes +$withBarcodes = $collection->filter(function ($item) { + return $item->barcode !== null; +}); + +// Filter by specific reference prefix +$filtered = $collection->filter(function ($item) { + return str_starts_with($item->shipmentReference ?? '', 'REF-2024'); +}); +``` + +### Helper Methods + +```php +// Find first item with a specific barcode +$found = $collection->find( + fn ($item) => $item->barcode === '3SDEVC123456789' +); + +// Check if any items have labels +$hasLabels = $collection->some( + fn ($item) => $item->labels !== null +); + +// Check if all items have barcodes +$allHaveBarcodes = $collection->every( + fn ($item) => $item->barcode !== null +); + +// Extract all barcodes as array +$barcodes = $collection->map( + fn ($item) => $item->barcode +); +``` + +### Iteration + +```php +// Iterate using foreach +foreach ($collection as $item) { + echo $item->barcode . "\n"; +} + +// Using iterator +$iterator = $collection->getIterator(); +``` + +### Saving All Labels + +```php +$collection = $response->shippingItems(); +$outputDir = '/path/to/labels/'; + +foreach ($collection as $item) { + $label = $item->labels?->first(); + if ($label !== null && !$label->isEmpty()) { + $filename = $item->barcode . '.' . $label->outputType->value; + $label->saveLabelAsFile($outputDir . $filename); + } +} +``` + +--- + +## Error Handling + +The SDK throws semantic exceptions for validation and request errors. + +> For the complete exception hierarchy, ProblemDetails, and retry behavior, see the [Error Handling guide](../ErrorHandling/README.md). + +### Exception Types + +| Exception | Description | +|-----------|-------------| +| `ValidationException` | Invalid request parameters (400, 422) | +| `AuthenticationException` | Invalid or insufficient credentials (401, 403) | +| `RateLimitException` | Too many requests (429, retryable) | +| `TimeoutException` | Request timed out (408, typically retryable) | +| `ClientException` | Other client errors (remaining 4xx) | +| `ServerException` | Server error (5xx, retryable) | +| `HttpSdkException` | Base class for all HTTP errors | + +### Common Validation Errors + +| Error Scenario | Description | +|----------------|-------------| +| Missing sender | Sender information is required | +| Missing receiver | Receiver information is required | +| Invalid postal code | Postal code format is invalid | +| Invalid country | Country code not supported | +| Invalid shipment type | Must be valid ShipmentType value | +| Missing credentials | customerCode and customerNumber required | +| Invalid label output type | Must be valid LabelOutputType value | + +### Example Error Handling + +```php +use Postnl\Sdk\Exception\Client\AuthenticationException; +use Postnl\Sdk\Exception\Client\ValidationException; +use Postnl\Sdk\Exception\HttpSdkException; + +try { + $response = $postnl->labelling()->requestLabel($request); + $collection = $response->shippingItems(); + + foreach ($collection as $item) { + $label = $item->labels?->first(); + if ($label !== null && !$label->isEmpty()) { + $label->saveLabelAsFile('/path/to/label.pdf'); + } + echo "Generated label for: " . $item->barcode . "\n"; + } + +} catch (ValidationException $e) { + // Handle validation errors (400, 422) + echo "Validation failed: " . $e->getMessage() . "\n"; + foreach ($e->fieldErrors as $error) { + echo " Field '{$error->field}': {$error->message}\n"; + } + +} catch (AuthenticationException $e) { + // Handle authentication/authorization errors (401, 403) + echo "Authentication failed: " . $e->getMessage() . "\n"; + +} catch (HttpSdkException $e) { + // Handle all other HTTP errors + echo "Request failed [{$e->statusCode}]: " . $e->getMessage() . "\n"; + if ($e->problemDetails->traceId !== null) { + echo "Trace ID: " . $e->problemDetails->traceId . "\n"; + } +} +``` + +--- + +## Enums Reference + +### LabelOutputType + +```php +use Postnl\Sdk\Enums\Payload\LabelOutputType; + +LabelOutputType::PDF->value; // 'pdf' +LabelOutputType::ZPL->value; // 'zpl' +LabelOutputType::JPG->value; // 'jpg' +LabelOutputType::GIF->value; // 'gif' +LabelOutputType::PNG->value; // 'png' +``` + +### LabelType + +```php +use Postnl\Sdk\Enums\Payload\LabelType; + +LabelType::Label->value; // 'Label' +LabelType::LabelInTheBox->value; // 'labelinthebox' +LabelType::ShipmentAndReturnLabel->value; // 'shipmentandreturnlabel' +``` + +### LabelResolution + +```php +use Postnl\Sdk\Enums\Payload\LabelResolution; + +LabelResolution::DPI_200->value; // 200 +LabelResolution::DPI_300->value; // 300 +LabelResolution::DPI_600->value; // 600 +``` + +### LabelPageOrientation + +```php +use Postnl\Sdk\Enums\Payload\LabelPageOrientation; + +LabelPageOrientation::Portrait->value; // 'portrait' +LabelPageOrientation::Landscape->value; // 'landscape' +``` + +### LabelMergeType + +```php +use Postnl\Sdk\Enums\Payload\LabelMergeType; + +LabelMergeType::SinglePDF->value; // 'singlepdf' +LabelMergeType::PDFA6TOA4->value; // 'pdfa6toa4' +``` + +### LabelPositioning + +```php +use Postnl\Sdk\Enums\Payload\LabelPositioning; + +LabelPositioning::TopLeft->value; // 'topleft' +LabelPositioning::TopRight->value; // 'topright' +LabelPositioning::BottomLeft->value; // 'bottomleft' +LabelPositioning::BottomRight->value; // 'bottomright' +``` + +### LabelPrintMethod + +```php +use Postnl\Sdk\Enums\Payload\LabelPrintMethod; + +LabelPrintMethod::ConsumerPrint->value; // 'consumerPrint' +LabelPrintMethod::RetailPrint->value; // 'retailPrint' +``` + +### ShipmentType + +```php +use Postnl\Sdk\Enums\Payload\ShipmentType; + +ShipmentType::Parcel->value; // 'parcel' +ShipmentType::NonStandardParcel->value; // 'parcelnonstandard' +ShipmentType::Letter->value; // 'letter' +ShipmentType::LetterBox->value; // 'letterbox' +ShipmentType::Pallet->value; // 'pallet' +ShipmentType::Packet->value; // 'packet' +``` + +### ReceiverType + +```php +use Postnl\Sdk\Enums\Payload\ReceiverType; + +ReceiverType::Business->value; // 'business' +ReceiverType::Consumer->value; // 'consumer' +``` + +--- + +## Complete Example + +```php +labelling()->requestLabel($request); + $collection = $response->shippingItems(); + + // Check if successful + if ($response->isSuccess()) { + echo "Successfully generated {$collection->count()} label(s)\n\n"; + } + + // Process generated items + foreach ($collection as $index => $item) { + echo sprintf("[%d] Label Generated\n", $index + 1); + echo " Reference: " . ($item->shipmentReference ?? 'N/A') . "\n"; + echo " Barcode: " . ($item->barcode ?? 'N/A') . "\n"; + echo " Coding Text: " . ($item->codingText ?? 'N/A') . "\n"; + + // Save the label + $label = $item->labels?->first(); + if ($label !== null && !$label->isEmpty()) { + $filename = '/path/to/labels/' . $item->barcode . '.pdf'; + $label->saveLabelAsFile($filename); + echo " Label saved to: " . $filename . "\n"; + } + + if ($item->productService !== null) { + echo " Product: " . $item->productService->productData . "\n"; + echo " Services: " . implode(', ', $item->productService->services ?? []) . "\n"; + } + echo "\n"; + } + + // Extract all barcodes for tracking + $barcodes = $collection->map(fn ($item) => $item->barcode); + echo "All barcodes: " . implode(', ', array_filter($barcodes)) . "\n"; + +} catch (ValidationException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +``` diff --git a/docs/postnl-v4-migration/sources/sdk-docs/Locations/README.md b/docs/postnl-v4-migration/sources/sdk-docs/Locations/README.md new file mode 100644 index 00000000..a7685b03 --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/Locations/README.md @@ -0,0 +1,710 @@ +# Locations API Documentation + +The Locations functionality allows you to search for PostNL pickup locations where customers can collect their parcels. It supports two search methods: by address (postal code) and by geographic coordinates (latitude/longitude). + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Pickup Locations by Address](#pickup-locations-by-address) +- [Pickup Locations by Coordinates](#pickup-locations-by-coordinates) +- [Data Models](#data-models) +- [Collection Methods](#collection-methods) +- [Error Handling](#error-handling) +- [Enums Reference](#enums-reference) +- [Complete Example](#complete-example) + +--- + +## Prerequisites + +### SDK Setup + +See the main SDK setup guide in the root documentation: +[SDK Root Documentation](../../README.md) + +--- + +### Required Credentials + +All Locations requests require: +- `customerCode` - Your PostNL customer code +- `customerNumber` - Your PostNL customer number + +--- + +## Pickup Locations by Address + +Search for pickup locations near a postal address. + +### Endpoint + +``` +POST /shipment/delivery/v4/locations/near-address +``` + +### Request Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `customerNumber` | string | Yes | Your PostNL customer number | +| `customerCode` | string | Yes | Your PostNL customer code | +| `numberOfLocations` | int | No | Number of locations to return (1-10, default: 10) | +| `receiverAddress` | Address | Yes | Address with `postalCode` and `countryIso` | +| `locationType` | string | Yes | Location type: `Retail` or `ParcelLocker` | +| `pickUpDate` | string | Yes | Pickup date (ISO 8601: `YYYY-MM-DD`) | +| `receiverCountryIso` | string | No | Country ISO code override | + +### Code Examples + +#### Using Constructor + +```php +use Postnl\Sdk\Enums\Country; +use Postnl\Sdk\Enums\PickUpLocationType; +use Postnl\Sdk\RequestData\V4\Address; +use Postnl\Sdk\Service\Locations\V4\Request\PickUpNearAddressRequest; + +$request = new PickUpNearAddressRequest( + customerNumber: 'YOUR_CUSTOMER_NUMBER', + customerCode: 'YOUR_CUSTOMER_CODE', + numberOfLocations: 5, + receiverAddress: new Address( + postalCode: '2521CA', + countryIso: Country::NL->value, + ), + locationType: PickUpLocationType::Retail->value, + pickUpDate: date('Y-m-d'), +); + +$response = $postnl->locations()->getPickupLocationsByAddress($request); +``` + +#### Using fromArray Factory + +```php +use Postnl\Sdk\Service\Locations\V4\Request\PickUpNearAddressRequest; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = PickUpNearAddressRequest::fromArray([ + 'customerNumber' => 'YOUR_CUSTOMER_NUMBER', + 'customerCode' => 'YOUR_CUSTOMER_CODE', + 'numberOfLocations' => 5, + 'receiverAddress' => [ + 'postalCode' => '2521CA', + 'countryIso' => 'NL', + ], + 'locationType' => 'Retail', + 'pickUpDate' => '2024-01-15', +], $mapper); + +$response = $postnl->locations()->getPickupLocationsByAddress($request); +``` + +### API Response Structure + +```json +{ + "locations": [ + { + "pickUpLocationId": "176227", + "locationType": "Retail", + "name": "Jumbo Den Haag", + "distance": 523, + "address": { + "street": "Weimarstraat", + "houseNumber": "70", + "postalCode": "2521CA", + "city": "Den Haag", + "countryIso": "NL" + }, + "coordinates": { + "latitude": 52.07004808, + "longitude": 4.32501423 + }, + "openingTimes": { + "openingTimes": [ + { + "day": "Monday", + "times": [ + { "from": "08:00", "until": "21:00" } + ] + }, + { + "day": "Tuesday", + "times": [ + { "from": "08:00", "until": "21:00" } + ] + } + ] + }, + "sustainability": { + "code": "02", + "description": "Sustainable" + } + } + ] +} +``` + +### Working with the Response + +```php +$response = $postnl->locations()->getPickupLocationsByAddress($request); + +// Get the collection of locations +$collection = $response->locationsCollection(); + +// Check response status +if ($response->isSuccess()) { + echo "Status: " . $response->meta()->statusCode; // 200 +} + +// Iterate over all locations +foreach ($collection as $location) { + echo $location->pickUpLocationId; // "176227" + echo $location->locationType; // "Retail" + echo $location->name; // "Jumbo Den Haag" + echo $location->distance; // 523 (meters) + echo $location->getDistanceInKilometers(); // 0.52 (km) + echo $location->is24Hour() ? '24/7' : 'Limited'; + echo $location->isSustainable(); // true/false + echo $location->address->getFullAddress(); // Full address string + echo $location->coordinates->latitude; // 52.07004808 + echo $location->coordinates->longitude; // 4.32501423 +} + +// Get raw array response +$rawData = $response->meta()->toArray(); +``` + +--- + +## Pickup Locations by Coordinates + +Search for pickup locations near geographic coordinates. + +### Endpoint + +``` +POST /shipment/delivery/v4/locations/near-coordinates +``` + +### Request Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `customerNumber` | string | Yes | Your PostNL customer number | +| `customerCode` | string | Yes | Your PostNL customer code | +| `numberOfLocations` | int | No | Number of locations to return (1-10, default: 10) | +| `coordinates` | Coordinates | Yes | Geographic coordinates with `latitude` and `longitude` | +| `locationType` | string | Yes | Location type: `Retail` or `ParcelLocker` | +| `pickUpDate` | string | Yes | Pickup date (ISO 8601: `YYYY-MM-DD`) | +| `receiverCountryIso` | string | Yes | Country ISO code (e.g., `NL`) | + +### Code Examples + +#### Using Constructor + +```php +use Postnl\Sdk\Enums\Country; +use Postnl\Sdk\Enums\PickUpLocationType; +use Postnl\Sdk\RequestData\V4\Coordinates; +use Postnl\Sdk\Service\Locations\V4\Request\PickUpNearCoordinatesRequest; + +$request = new PickUpNearCoordinatesRequest( + customerNumber: 'YOUR_CUSTOMER_NUMBER', + customerCode: 'YOUR_CUSTOMER_CODE', + numberOfLocations: 5, + coordinates: new Coordinates( + latitude: 52.07004808, + longitude: 4.32501423, + ), + locationType: PickUpLocationType::Retail->value, + pickUpDate: date('Y-m-d'), + receiverCountryIso: Country::NL->value, +); + +$response = $postnl->locations()->getNearPickupLocationsByCoordinates($request); +``` + +#### Using fromArray Factory + +```php +use Postnl\Sdk\Service\Locations\V4\Request\PickUpNearCoordinatesRequest; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = PickUpNearCoordinatesRequest::fromArray([ + 'customerNumber' => 'YOUR_CUSTOMER_NUMBER', + 'customerCode' => 'YOUR_CUSTOMER_CODE', + 'numberOfLocations' => 5, + 'coordinates' => [ + 'latitude' => 52.07004808, + 'longitude' => 4.32501423, + ], + 'locationType' => 'Retail', + 'pickUpDate' => '2024-01-15', + 'receiverCountryIso' => 'NL', +], $mapper); + +$response = $postnl->locations()->getNearPickupLocationsByCoordinates($request); +``` + +### API Response Structure + +```json +{ + "locations": [ + { + "pickUpLocationId": "176227", + "locationType": "Retail", + "name": "Jumbo Den Haag", + "distance": 150, + "address": { + "street": "Weimarstraat", + "houseNumber": "70", + "postalCode": "2521CA", + "city": "Den Haag", + "countryIso": "NL" + }, + "coordinates": { + "latitude": 52.07004808, + "longitude": 4.32501423 + }, + "openingTimes": { + "openingTimes": [ + { + "day": "Monday", + "times": [ + { "from": "00:00", "until": "23:59" } + ] + } + ] + }, + "sustainability": { + "code": "02", + "description": "Sustainable" + } + } + ] +} +``` + +### Working with the Response + +```php +$response = $postnl->locations()->getNearPickupLocationsByCoordinates($request); + +// Get the collection of locations +$collection = $response->locationsCollection(); + +// Filter and sort locations +$nearestRetail = $collection + ->filterByLocationType('Retail') + ->sortByDistance() + ->first(); + +if ($nearestRetail) { + echo "Nearest retail location: " . $nearestRetail->name; + echo "Distance: " . $nearestRetail->getDistanceInKilometers() . " km"; +} + +// Get only 24/7 locations +$always247 = $collection->filter24Hour(); + +// Filter by maximum distance (in meters) +$within1km = $collection->filterByMaxDistance(1000); +``` + +--- + +## Data Models + +### PickupLocation + +Represents a single pickup location. + +```php +readonly class PickupLocation +{ + public ?string $pickUpLocationId; // "176227" + public ?string $locationType; // "Retail" or "ParcelLocker" + public ?string $name; // "Jumbo Den Haag" + public ?int $distance; // Distance in meters + public ?Address $address; // Address object + public ?Coordinates $coordinates; // Geographic coordinates + public ?LocationOpeningHours $openingTimes; // Opening hours + public ?Sustainability $sustainability; // Sustainability info +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `is24Hour()` | bool | Returns `true` if location is open 24/7 | +| `isSustainable()` | bool | Returns `true` if location has sustainability features | +| `getDistanceInKilometers()` | float | Returns distance in km (rounded to 2 decimals) | + +### LocationOpeningHours + +Represents the opening hours for a location. + +```php +readonly class LocationOpeningHours +{ + public ?array $openingTimes; // array +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `is24Hour()` | bool | Returns `true` if open 24/7 all days | +| `isOpenOn(string $day)` | bool | Returns `true` if open on specific day | +| `getTimesForDay(string $day)` | ?DayOpeningTimes | Returns opening times for specific day | + +### DayOpeningTimes + +Represents opening times for a specific day. + +```php +readonly class DayOpeningTimes +{ + public string $day; // "Monday", "Tuesday", etc. + public array $times; // array +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `is24Hour()` | bool | Returns `true` if 24-hour opening | +| `isClosed()` | bool | Returns `true` if location is closed | + +### TimeSlot + +Represents a time window. + +```php +readonly class TimeSlot +{ + public ?string $from; // "08:00" + public ?string $until; // "21:00" +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `getFormattedRange()` | string | Returns `"08:00 - 21:00"` | +| `isMorning()` | bool | Returns `true` if until before 12:00 | +| `isEvening()` | bool | Returns `true` if from 17:00+ | +| `is24Hour()` | bool | Returns `true` if covers full 24-hour period | + +### Address + +Represents a postal address. + +```php +readonly class Address +{ + public ?string $houseNumber; + public ?string $postalCode; + public ?string $countryIso; + public ?string $companyName; + public ?string $street; + public ?string $houseNumberAddition; + public ?string $city; +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `getFullAddress()` | string | Returns single-line address | +| `getFormattedAddress()` | string | Returns multi-line formatted address | + +### Coordinates + +Represents geographic coordinates. + +```php +readonly class Coordinates +{ + public ?float $latitude; // -90 to 90 + public ?float $longitude; // -180 to 180 +} +``` + +**Notes:** +- Constructor validates latitude range (-90 to 90) and longitude range (-180 to 180) +- `fromArray()` accepts alternative keys: `lat`/`latitude`, `lng`/`lon`/`longitude` + +### Sustainability + +Represents sustainability information for a location. + +```php +readonly class Sustainability +{ + public ?string $code; // "00", "01", "02", "03" + public ?string $description; // "Not available", "Sustainable", etc. +} +``` + +**Sustainability Codes:** + +| Code | Description | Sustainable | +|------|-------------|-------------| +| `00` | Not available | No | +| `01` | Carbon neutral | Yes | +| `02` | Sustainable | Yes | +| `03` | Sustainable Plus | Yes | + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `isSustainable()` | bool | Returns `true` if code is not `'00'` | +| `isCarbonNeutral()` | bool | Returns `true` if code is `'01'` or `'03'` | + +--- + +## Collection Methods + +`PickUpLocationsCollection` provides these methods: + +### Base Methods + +| Method | Return | Description | +|--------|--------|-------------| +| `count()` | int | Number of items in collection | +| `isEmpty()` | bool | Returns `true` if collection is empty | +| `all()` | array | Returns all items as array | +| `first()` | ?PickupLocation | Returns first item or null | +| `last()` | ?PickupLocation | Returns last item or null | +| `get(int $index)` | ?PickupLocation | Returns item at index or null | + +### Filtering Methods + +All filter methods return a new collection (immutable). + +```php +// Filter by location type +$retailOnly = $collection->filterByLocationType('Retail'); +$lockersOnly = $collection->filterByLocationType('ParcelLocker'); + +// Filter by sustainability code +$sustainable = $collection->filterBySustainability('02'); + +// Filter by maximum distance (in meters) +$within500m = $collection->filterByMaxDistance(500); +$within1km = $collection->filterByMaxDistance(1000); + +// Filter to 24/7 locations only +$always247 = $collection->filter24Hour(); + +// Custom filtering with callback +$custom = $collection->filter( + fn (PickupLocation $loc) => $loc->distance < 1000 && $loc->isSustainable() +); + +// Chain multiple filters +$result = $collection + ->filterByLocationType('Retail') + ->filter24Hour() + ->filterByMaxDistance(2000); +``` + +### Sorting Methods + +```php +// Sort by distance (ascending - nearest first) +$sorted = $collection->sortByDistance(); +``` + +### Helper Methods + +```php +// Get the nearest location +$nearest = $collection->sortByDistance()->first(); + +// Find first matching item +$found = $collection->find( + fn (PickupLocation $loc) => $loc->name === 'Jumbo Den Haag' +); + +// Check if any match condition +$has24Hour = $collection->some( + fn (PickupLocation $loc) => $loc->is24Hour() +); + +// Check if all match condition +$allSustainable = $collection->every( + fn (PickupLocation $loc) => $loc->isSustainable() +); + +// Map to custom array +$names = $collection->map( + fn (PickupLocation $loc) => $loc->name +); +``` + +### Iteration + +```php +// Iterate using foreach +foreach ($collection as $location) { + // Process each PickupLocation +} + +// Using iterator +$iterator = $collection->getIterator(); +``` + +--- + +## Error Handling + +The SDK throws `ValidationException` for validation errors. For the complete error handling guide, see [Error Handling](../ErrorHandling/README.md). + +### Common Validation Errors + +| Error Scenario | Description | +|----------------|-------------| +| `numberOfLocations` > 10 | Value must be between 1 and 10 | +| Invalid `locationType` | Must be `Retail` or `ParcelLocker` | +| Invalid `pickUpDate` | Must be valid ISO 8601 date format (YYYY-MM-DD) | +| Missing `receiverAddress` | Address is required for address search | +| Missing `coordinates` | Coordinates are required for coordinates search | +| Invalid postal code | Postal code must be valid format | +| Missing credentials | `customerCode` and `customerNumber` required | +| Invalid latitude | Must be between -90 and 90 | +| Invalid longitude | Must be between -180 and 180 | + +### Example Error Handling + +```php +use Postnl\Sdk\Exception\Client\ValidationException; + +try { + $response = $postnl->locations()->getPickupLocationsByAddress($request); + $collection = $response->locationsCollection(); + + // Process locations... + +} catch (ValidationException $e) { + // Handle validation errors + echo "Request failed: " . $e->getMessage(); + echo "Status code: " . $e->getCode(); +} +``` + +--- + +## Enums Reference + +### PickUpLocationType + +```php +use Postnl\Sdk\Enums\PickUpLocationType; + +PickUpLocationType::Retail->value; // 'Retail' +PickUpLocationType::ParcelLocker->value; // 'ParcelLocker' +``` + +### Country + +```php +use Postnl\Sdk\Enums\Country; + +Country::NL->value; // 'NL' +Country::BE->value; // 'BE' +Country::DE->value; // 'DE' +// ... other country codes +``` + +### LocationSustainabilityCode + +```php +use Postnl\Sdk\Enums\LocationSustainabilityCode; + +LocationSustainabilityCode::NOT_AVAILABLE->value; // '00' +LocationSustainabilityCode::CARBON_NEUTRAL->value; // '01' +LocationSustainabilityCode::SUSTAINABLE->value; // '02' +LocationSustainabilityCode::SUSTAINABLE_PLUS->value; // '03' + +// Helper methods +LocationSustainabilityCode::SUSTAINABLE->getLevel(); // 2 +LocationSustainabilityCode::isValid('02'); // true +``` + +--- + +## Complete Example + +```php +value, + ), + locationType: PickUpLocationType::Retail->value, + pickUpDate: date('Y-m-d'), +); + +try { + $response = $postnl->locations()->getPickupLocationsByAddress($request); + $collection = $response->locationsCollection(); + + // Get nearest 24/7 sustainable location + $filtered = $collection + ->filter24Hour() + ->filterBySustainability('02') + ->sortByDistance(); + + if (!$filtered->isEmpty()) { + $nearest = $filtered->first(); + echo "Best pickup location: {$nearest->name}\n"; + echo "Distance: {$nearest->getDistanceInKilometers()} km\n"; + echo "Sustainability: {$nearest->sustainability->description}\n"; + } + + // List all retail locations within 1km + $nearby = $collection + ->filterByLocationType('Retail') + ->filterByMaxDistance(1000) + ->sortByDistance(); + + echo "\nNearby retail locations (within 1km):\n"; + foreach ($nearby as $location) { + $hours = $location->is24Hour() ? '24/7' : 'Limited hours'; + echo "- {$location->name}: {$location->getDistanceInKilometers()} km ({$hours})\n"; + } + +} catch (ValidationException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +``` diff --git a/docs/postnl-v4-migration/sources/sdk-docs/ReturnShipment/README.md b/docs/postnl-v4-migration/sources/sdk-docs/ReturnShipment/README.md new file mode 100644 index 00000000..4c740710 --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/ReturnShipment/README.md @@ -0,0 +1,747 @@ +# Return Shipment API Documentation + +The Return Shipment functionality allows you to generate return labels for customers to send parcels back. It creates shipment labels with return-specific options, supporting both domestic and international returns. + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Generate Return Shipment](#generate-return-shipment) +- [Data Models](#data-models) +- [Collection Methods](#collection-methods) +- [Error Handling](#error-handling) +- [Enums Reference](#enums-reference) +- [Complete Example](#complete-example) + +--- + +## Prerequisites + +### SDK Setup + +See the main SDK setup guide in the root documentation: +[SDK Root Documentation](../../README.md) + +--- + +### Required Credentials + +All Return Shipment requests require: +- `customerCode` - Your PostNL customer code +- `customerNumber` - Your PostNL customer number + +These credentials are automatically merged into the `receiver` object via the `CredentialStrategy::INTO_RECEIVER` strategy. + +--- + +## Generate Return Shipment + +Generate a return shipment label for customers to return parcels. + +### Endpoint + +``` +POST /shipment/delivery/v4/return/generate +``` + +### Request Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `sender` | ShipmentParty | Yes | Sender information (customer returning the parcel) | +| `receiver` | ShipmentParty | Yes | Receiver information (your company details) | +| `labelSettings` | LabelSettings | No | Label output configuration (format, resolution, etc.) | +| `returnOptions` | ReturnOptions | No | Return-specific options (return period, valuable return, etc.) | +| `items` | array\ | No | Items to be returned with their barcodes and references | + +### Code Examples + +#### Using Constructor + +```php +use Postnl\Sdk\Enums\Country; +use Postnl\Sdk\Enums\LabelOutputType; +use Postnl\Sdk\Enums\LabelPageOrientation; +use Postnl\Sdk\Enums\LabelResolution; +use Postnl\Sdk\Enums\LabelPrintMethod; +use Postnl\Sdk\Enums\ReturnPeriod; +use Postnl\Sdk\RequestData\V4\Address; +use Postnl\Sdk\RequestData\V4\Contact; +use Postnl\Sdk\RequestData\V4\LabelSettings; +use Postnl\Sdk\RequestData\V4\ReturnOptions\DomesticReturnOptions; +use Postnl\Sdk\RequestData\V4\ReturnOptions\ReturnOptions; +use Postnl\Sdk\RequestData\V4\ShipmentParty; +use Postnl\Sdk\RequestData\V4\ReturnShipment\ReturnShipmentRequest; +use Postnl\Sdk\ResponseData\V4\ShippingItem; + +$request = new ReturnShipmentRequest( + sender: ShipmentParty::asReturnSender( + address: new Address( + street: 'Klantstraat', + houseNumber: '123', + postalCode: '1234AB', + city: 'Amsterdam', + countryIso: Country::NL->value, + ), + contact: new Contact( + firstName: 'Jan', + lastName: 'Klant', + email: 'jan.klant@example.com', + ), + ), + receiver: ShipmentParty::asReturnReceiver( + customerNumber: 'YOUR_CUSTOMER_NUMBER', + customerCode: 'YOUR_CUSTOMER_CODE', + address: new Address( + street: 'Waldorpstraat', + houseNumber: '3', + postalCode: '2521CA', + city: 'Den Haag', + countryIso: Country::NL->value, + ), + ), + labelSettings: new LabelSettings( + outputType: LabelOutputType::PDF->value, + resolution: LabelResolution::DPI_300->value, + pageOrientation: LabelPageOrientation::Portrait->value, + printMethod: LabelPrintMethod::Consumer->value, + ), + returnOptions: new ReturnOptions( + domesticReturnOptions: new DomesticReturnOptions( + returnPeriod: ReturnPeriod::IN_35_DAYS, + valuableReturn: true, + ), + ), +); + +$response = $postnl->returnShipment()->generateReturn($request); +``` + +#### Using fromArray Factory + +```php +use Postnl\Sdk\RequestData\V4\ReturnShipment\ReturnShipmentRequest; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = ReturnShipmentRequest::fromArray([ + 'sender' => [ + 'address' => [ + 'street' => 'Klantstraat', + 'houseNumber' => '123', + 'postalCode' => '1234AB', + 'city' => 'Amsterdam', + 'countryIso' => 'NL', + ], + 'contact' => [ + 'firstName' => 'Jan', + 'lastName' => 'Klant', + 'email' => 'jan.klant@example.com', + ], + ], + 'receiver' => [ + 'customerNumber' => 'YOUR_CUSTOMER_NUMBER', + 'customerCode' => 'YOUR_CUSTOMER_CODE', + 'address' => [ + 'street' => 'Waldorpstraat', + 'houseNumber' => '3', + 'postalCode' => '2521CA', + 'city' => 'Den Haag', + 'countryIso' => 'NL', + ], + ], + 'labelSettings' => [ + 'outputType' => 'pdf', + 'resolution' => 300, + 'pageOrientation' => 'portrait', + 'printMethod' => 'consumerPrint', + ], + 'returnOptions' => [ + 'domestic' => [ + 'returnPeriod' => 35, + 'valuableReturn' => true, + ], + ], +], $mapper); + +$response = $postnl->returnShipment()->generateReturn($request); +``` + +#### Using Fluent Interface + +```php +use Postnl\Sdk\RequestData\V4\Address; +use Postnl\Sdk\RequestData\V4\Contact; +use Postnl\Sdk\RequestData\V4\ReturnShipment\ReturnShipmentRequest; +use Postnl\Sdk\RequestData\V4\ShipmentParty; +use Postnl\Sdk\RequestData\V4\LabelSettings; +use Postnl\Sdk\RequestData\V4\ReturnOptions\ReturnOptions; +use Postnl\Sdk\ResponseData\V4\ShippingItem; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = (new ReturnShipmentRequest()) + ->sender(ShipmentParty::asReturnSender( + address: new Address( + street: 'Klantstraat', + houseNumber: '123', + postalCode: '1234AB', + city: 'Amsterdam', + countryIso: 'NL', + ), + contact: new Contact( + lastName: 'Klant', + email: 'klant@example.com', + ), + )) + ->receiver(ShipmentParty::asReturnReceiver( + customerNumber: 'YOUR_CUSTOMER_NUMBER', + customerCode: 'YOUR_CUSTOMER_CODE', + address: new Address( + street: 'Waldorpstraat', + houseNumber: '3', + postalCode: '2521CA', + city: 'Den Haag', + countryIso: 'NL', + ), + )) + ->labelSettings(LabelSettings::fromArray([ + 'outputType' => 'pdf', + 'resolution' => 300, + ], $mapper)) + ->returnOptions(ReturnOptions::fromArray([ + 'domestic' => [ + 'returnPeriod' => 35, + ], + ], $mapper)) + ->addItem(ShippingItem::fromArray([ + 'barcode' => '3SDEVC123456789', + 'customerReferences' => [ + 'shipmentReference' => 'ORDER-12345', + 'costCenter' => 'RETURNS', + ], + ], $mapper)); + +$response = $postnl->returnShipment()->generateReturn($request); +``` + +### API Response Structure + +```json +{ + "items": [ + { + "shipmentReference": "ORDER-12345", + "labels": [ + { + "label": "JVBERi0xLjMKJeLjz9...", + "outputType": "PDF", + "labelType": "Return Label" + } + ], + "barcode": "3SDEVC330399651", + "productService": { + "productData": "Returns Homedress", + "services": [ + "Online Label on request" + ] + } + } + ] +} +``` + +### Working with the Response + +```php +$response = $postnl->returnShipment()->generateReturn($request); + +// Get the collection of processed shipping items +$collection = $response->shippingItemsCollection(); + +// Check response status +if ($response->isSuccess()) { + echo "Status: " . $response->meta()->statusCode; // 200 +} + +// Iterate over all processed items +foreach ($collection as $item) { + echo $item->shipmentReference; // "ORDER-12345" + echo $item->barcode; // "3SDEVC330399651" + echo $item->codingText; // Routing code (if present) + $label = $item->labels?->first(); + echo $label->outputType->value; // "pdf" + echo $label->labelType->value; // "labelinthebox" + echo $item->productService->productData; // "Returns Homedress" + + // Save the label to a file + if ($label !== null && !$label->isEmpty()) { + $label->saveLabelAsFile('/path/to/return-label.pdf'); + } +} + +// Get the first item +$firstItem = $collection->first(); + +// Get raw array response +$rawData = $response->meta()->toArray(); +``` + +--- + +## Data Models + +### ReturnShipmentRequest + +The main request object for generating return shipments. + +```php +class ReturnShipmentRequest +{ + public function __construct( + private ?ShipmentParty $sender = null, + private ?ShipmentParty $receiver = null, + private ?LabelSettings $labelSettings = null, + private ?ReturnOptions $returnOptions = null, + array $items = [], + ); +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `sender(ShipmentParty $sender)` | static | Set the sender (fluent) | +| `receiver(ShipmentParty $receiver)` | static | Set the receiver (fluent) | +| `labelSettings(LabelSettings $labelSettings)` | static | Set label settings (fluent) | +| `returnOptions(ReturnOptions $returnOptions)` | static | Set return options (fluent) | +| `addItem(ShippingItem $item)` | static | Add a shipping item (fluent) | +| `withItems(array $items)` | static | Replace all items (fluent) | +| `clearItems()` | static | Remove all items (fluent) | +| `toArray(PayloadMapperInterface $mapper)` | array | Convert to array | +| `fromArray(array $data, PayloadMapperInterface $mapper)` | self | Create from array (static) | + +### ShipmentParty + +Represents a party (sender or receiver) in a shipment or return shipment. Use the static constructors to create parties for the correct role: + +| Static Method | Use Case | Parameters | +|---------------|----------|-------------| +| `asSender()` | Direct shipment sender (merchant dispatching) | customerNumber, customerCode, address, undeliverableReturnAddress? | +| `asReceiver()` | Direct shipment receiver (consumer receiving) | address, contact?, receiverType? | +| `asReturnSender()` | Return shipment sender (consumer returning) | address, contact? | +| `asReturnReceiver()` | Return shipment receiver (merchant receiving) | customerNumber, customerCode, address, contact? | + +```php +final readonly class ShipmentParty +{ + public ?string $customerNumber; // Customer number (max 10 chars) + public ?string $customerCode; // Customer code (max 6 chars) + public ?Address $address; // Postal address + public ?Address $undeliverableReturnAddress; // Return address for undeliverable items + public ?Contact $contact; // Contact information (name, email, phone) + public ?ReceiverType $receiverType; // business or consumer +} +``` + +### Address + +Represents a postal address. + +```php +readonly class Address +{ + public ?string $houseNumber; + public ?string $postalCode; + public ?string $countryIso; + public ?string $companyName; + public ?string $departmentName; + public ?string $street; + public ?string $houseNumberAddition; + public ?string $city; + public ?string $addressLine; + public ?InternationalAddressData $internationalAddressData; +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `getFullAddress()` | string | Returns single-line address | +| `getFormattedAddress()` | string | Returns multi-line formatted address | + +### Contact + +Represents contact information for the sender. + +```php +readonly class Contact +{ + public ?string $companyName; + public ?string $lastName; + public ?string $mobileNumber; + public ?string $smsNumber; + public ?string $firstName; + public ?string $email; + public ?string $phoneNumber; + public ?string $language; +} +``` + +### LabelSettings + +Configures the output format and appearance of the label. + +```php +readonly class LabelSettings +{ + public ?string $outputType; // "pdf", "jpg", "png", "gif" + public ?int $resolution; // 200, 300, or 600 DPI + public ?string $pageOrientation; // "portrait" or "landscape" + public ?string $mergeType; // Merge multiple labels + public ?string $positioning; // Label positioning on page + public ?string $printMethod; // "retailPrint" or "consumerPrint" +} +``` + +### ReturnOptions + +Configures return-specific options. + +```php +readonly class ReturnOptions +{ + public ?string $labelType; // Label type + public ?string $returnBarcode; // Pre-assigned barcode + public ?Address $returnAddress; // Alternative return address + public ?DomesticReturnOptions $domesticReturnOptions; // Domestic return settings + public ?bool $returnBlock; // Block return option +} +``` + +### DomesticReturnOptions + +Configures domestic return settings. + +```php +readonly class DomesticReturnOptions +{ + public ?ReturnPeriod $returnPeriod; // 20 or 35 days + public ?bool $valuableReturn; // Valuable item handling +} +``` + +### ProcessedShippingItem + +Represents a processed return shipment in the response. + +```php +readonly class ProcessedShippingItem +{ + public ?string $shipmentReference; // Your reference + public ?LabelsCollection $labels; // Collection of generated labels + public ?string $barcode; // Assigned barcode + public ?string $codingText; // Routing code + public ?ProductService $productService; // Product/service info +} +``` + +### Label + +Represents a generated shipping label. + +```php +readonly class Label +{ + public ?string $label; // Base64 encoded label data + public ?string $partnerId; // Partner ID + public ?string $partnerBarcode; // Partner barcode + public ?string $mergedLabelContent; // Merged label data + public ?LabelOutputType $outputType; // PDF, ZPL, JPG, etc. + public ?LabelType $labelType; // Label type +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `isEmpty()` | bool | Returns `true` if label has no content | +| `saveLabelAsFile(string $filepath)` | void | Save label to file | + +--- + +## Collection Methods + +`ShippingItemsCollection` provides these methods: + +### Base Methods + +| Method | Return | Description | +|--------|--------|-------------| +| `count()` | int | Number of items in collection | +| `isEmpty()` | bool | Returns `true` if collection is empty | +| `all()` | array | Returns all items as array | +| `first()` | ?ProcessedShippingItem | Returns first item or null | +| `last()` | ?ProcessedShippingItem | Returns last item or null | +| `get(int $index)` | ?ProcessedShippingItem | Returns item at index or null | + +### Filtering Methods + +All filter methods return a new collection (immutable). + +```php +// Custom filtering with callback +$pdfLabels = $collection->filter( + fn (ProcessedShippingItem $item) => $item->labels?->first()?->outputType === LabelOutputType::PDF +); + +// Filter to items with barcodes +$withBarcodes = $collection->filter( + fn (ProcessedShippingItem $item) => $item->barcode !== null +); +``` + +### Helper Methods + +```php +// Find first matching item +$found = $collection->find( + fn (ProcessedShippingItem $item) => $item->barcode === '3SDEVC330399651' +); + +// Check if any match condition +$hasLabels = $collection->some( + fn (ProcessedShippingItem $item) => $item->labels !== null && !$item->labels->isEmpty() +); + +// Check if all match condition +$allHaveBarcodes = $collection->every( + fn (ProcessedShippingItem $item) => $item->barcode !== null +); + +// Map to custom array +$barcodes = $collection->map( + fn (ProcessedShippingItem $item) => $item->barcode +); +``` + +### Iteration + +```php +// Iterate using foreach +foreach ($collection as $item) { + // Process each ProcessedShippingItem +} + +// Using iterator +$iterator = $collection->getIterator(); +``` + +--- + +## Error Handling + +The SDK throws `ValidationException` for validation errors. For the complete error handling guide, see [Error Handling](../ErrorHandling/README.md). + +### Common Validation Errors + +| Error Scenario | Description | +|----------------|-------------| +| Missing `sender` | Sender information is required | +| Missing `receiver` | Receiver information is required | +| Invalid `customerNumber` | Customer number must be valid | +| Invalid `customerCode` | Customer code must be valid | +| Invalid `outputType` | Must be `pdf`, `jpg`, `png`, or `gif` | +| Invalid `printMethod` | Must be `retailPrint` or `consumerPrint` | +| Invalid `returnPeriod` | Must be 20 or 35 | +| Missing address fields | Required address fields must be provided | + +### Example Error Handling + +```php +use Postnl\Sdk\Exception\Client\ValidationException; + +try { + $response = $postnl->returnShipment()->generateReturn($request); + $collection = $response->shippingItemsCollection(); + + // Process items... + +} catch (ValidationException $e) { + // Handle validation errors + echo "Request failed: " . $e->getMessage(); + echo "Status code: " . $e->getCode(); +} +``` + +--- + +## Enums Reference + +### LabelOutputType + +```php +use Postnl\Sdk\Enums\LabelOutputType; + +LabelOutputType::PDF->value; // 'pdf' +LabelOutputType::JPG->value; // 'jpg' +LabelOutputType::PNG->value; // 'png' +LabelOutputType::GIF->value; // 'gif' +// Note: ZPL is not applicable to return endpoints +``` + +### LabelOrientation + +```php +use Postnl\Sdk\Enums\LabelPageOrientation; + +LabelPageOrientation::Portrait->value; // 'portrait' +LabelPageOrientation::Landscape->value; // 'landscape' +``` + +### LabelResolution + +```php +use Postnl\Sdk\Enums\LabelResolution; + +LabelResolution::DPI_200->value; // 200 +LabelResolution::DPI_300->value; // 300 +LabelResolution::DPI_600->value; // 600 +``` + +### PrintMethod + +```php +use Postnl\Sdk\Enums\LabelPrintMethod; + +LabelPrintMethod::Retail->value; // 'retailPrint' (NL only, use PNG/JPG) +LabelPrintMethod::Consumer->value; // 'consumerPrint' (BE only, use PDF) +``` + +### ReturnPeriod + +```php +use Postnl\Sdk\Enums\ReturnPeriod; + +ReturnPeriod::IN_20_DAYS->value; // 20 +ReturnPeriod::IN_35_DAYS->value; // 35 +``` + +### Country + +```php +use Postnl\Sdk\Enums\Country; + +Country::NL->value; // 'NL' +Country::BE->value; // 'BE' +Country::DE->value; // 'DE' +// ... other country codes +``` + +--- + +## Complete Example + +```php +value, + ), + contact: new Contact( + firstName: 'Jan', + lastName: 'Klant', + email: 'jan.klant@example.com', + mobileNumber: '+31612345678', + ), + ), + receiver: ShipmentParty::asReturnReceiver( + customerNumber: 'YOUR_CUSTOMER_NUMBER', + customerCode: 'YOUR_CUSTOMER_CODE', + address: new Address( + companyName: 'Your Company B.V.', + street: 'Waldorpstraat', + houseNumber: '3', + postalCode: '2521CA', + city: 'Den Haag', + countryIso: Country::NL->value, + ), + ), + labelSettings: new LabelSettings( + outputType: LabelOutputType::PDF->value, + resolution: LabelResolution::DPI_300->value, + printMethod: LabelPrintMethod::Consumer->value, + ), + returnOptions: new ReturnOptions( + domesticReturnOptions: new DomesticReturnOptions( + returnPeriod: ReturnPeriod::IN_35_DAYS, + valuableReturn: false, + ), + ), +); + +try { + $response = $postnl->returnShipment()->generateReturn($request); + $collection = $response->shippingItems(); + + if ($collection->isEmpty()) { + echo "No return labels generated.\n"; + return; + } + + // Process each generated return label + foreach ($collection as $index => $item) { + echo "Return #{$index}:\n"; + echo " Barcode: {$item->barcode}\n"; + echo " Reference: {$item->shipmentReference}\n"; + + if ($item->productService !== null) { + echo " Product: {$item->productService->productData}\n"; + } + + // Save the label + $label = $item->labels?->first(); + if ($label !== null && !$label->isEmpty()) { + $filename = "return-label-{$item->barcode}.pdf"; + $label->saveLabelAsFile("/tmp/{$filename}"); + echo " Label saved: {$filename}\n"; + } + + echo "\n"; + } + + // Get all barcodes for tracking + $barcodes = $collection->map(fn($item) => $item->barcode); + echo "All return barcodes: " . implode(', ', array_filter($barcodes)) . "\n"; + +} catch (ValidationException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +``` diff --git a/docs/postnl-v4-migration/sources/sdk-docs/ShipmentDelivery/README.md b/docs/postnl-v4-migration/sources/sdk-docs/ShipmentDelivery/README.md new file mode 100644 index 00000000..e7b41cae --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/ShipmentDelivery/README.md @@ -0,0 +1,794 @@ +# ShipmentDelivery API Documentation + +The ShipmentDelivery service combines label generation AND shipment confirmation in a single API call. This is the recommended approach when you need both a shipping label and want to pre-announce the shipment to PostNL simultaneously. + +This service differs from the individual services: +- **[Confirming service](../Confirming/README.md)** - Pre-announces shipments WITHOUT generating labels +- **[Labelling service](../Labelling/README.md)** - Generates labels only, without confirmation + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Label Confirm](#label-confirm) +- [Label Settings](#label-settings) +- [Data Models](#data-models) +- [Collection Methods](#collection-methods) +- [Error Handling](#error-handling) +- [Enums Reference](#enums-reference) +- [Complete Example](#complete-example) + +--- + +## Prerequisites + +### SDK Setup + +See the main SDK setup guide in the root documentation: +[SDK Root Documentation](../../README.md) + +--- + +### Required Credentials + +All ShipmentDelivery requests require: +- `customerCode` - Your PostNL customer code +- `customerNumber` - Your PostNL customer number + +These are automatically injected into the sender object by the SDK. + +--- + +## Label Confirm + +Generate a shipping label and confirm the shipment in a single API call. + +### Endpoint + +``` +POST /shipment/delivery/v4/labelconfirm +``` + +### Request Parameters (ShipmentDeliveryRequest) + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `sender` | ShipmentParty | Yes | Sender details including customer credentials and address | +| `receiver` | ShipmentParty | Yes | Receiver contact and address information | +| `labelSettings` | LabelSettings | No | Label format and output configuration | +| `returnOptions` | ReturnOptions | No | Return shipment options | +| `shipmentType` | ShipmentType | No | Type of shipment (default: `parcel`) | +| `handOverDate` | string | No | Date when shipment is handed to PostNL (`YYYY-MM-DD`) | +| `deliveryLocation` | DeliveryLocation | No | Alternative delivery location | +| `services` | Services | No | Additional shipment services | +| `internationalShipmentData` | InternationalShipmentData | No | Data for international shipments | +| `itemCount` | int | No | Number of items in shipment (max 999, default: 1) | +| `items` | ShippingItem[] | No | Individual item details with barcodes | + +### Code Examples + +#### Using Constructor + +```php +use Postnl\Sdk\Enums\Payload\Country; +use Postnl\Sdk\Enums\Payload\LabelOutputType; +use Postnl\Sdk\Enums\Payload\ShipmentType; +use Postnl\Sdk\RequestData\V4\Address; +use Postnl\Sdk\RequestData\V4\Contact; +use Postnl\Sdk\RequestData\V4\LabelSettings; +use Postnl\Sdk\RequestData\V4\ShipmentParty; +use Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest; + +$request = new ShipmentDeliveryRequest( + sender: ShipmentParty::asSender( + customerNumber: 'YOUR_CUSTOMER_NUMBER', + customerCode: 'YOUR_CUSTOMER_CODE', + address: new Address( + companyName: 'Your Company', + street: 'Siriusdreef', + houseNumber: '42', + postalCode: '2132WT', + city: 'Hoofddorp', + countryIso: Country::NL, + ), + ), + receiver: ShipmentParty::asReceiver( + address: new Address( + street: 'Waldorpstraat', + houseNumber: '3', + postalCode: '2521CA', + city: 'Den Haag', + countryIso: Country::NL, + ), + contact: new Contact( + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + ), + ), + labelSettings: new LabelSettings( + outputType: LabelOutputType::PDF, + ), + shipmentType: ShipmentType::Parcel, + handOverDate: date('Y-m-d', strtotime('+1 day')), +); + +$response = $postnl->shipmentDelivery()->labelConfirm($request); +``` + +#### Using fromArray Factory + +```php +use Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = ShipmentDeliveryRequest::fromArray([ + 'sender' => [ + 'customerNumber' => 'YOUR_CUSTOMER_NUMBER', + 'customerCode' => 'YOUR_CUSTOMER_CODE', + 'address' => [ + 'companyName' => 'Your Company', + 'street' => 'Siriusdreef', + 'houseNumber' => '42', + 'postalCode' => '2132WT', + 'city' => 'Hoofddorp', + 'countryIso' => 'NL', + ], + ], + 'receiver' => [ + 'contact' => [ + 'firstName' => 'John', + 'lastName' => 'Doe', + 'email' => 'john.doe@example.com', + ], + 'address' => [ + 'street' => 'Waldorpstraat', + 'houseNumber' => '3', + 'postalCode' => '2521CA', + 'city' => 'Den Haag', + 'countryIso' => 'NL', + ], + ], + 'labelSettings' => [ + 'outputType' => 'pdf', + ], + 'type' => 'parcel', + 'handOverDate' => '2024-01-15', +], $mapper); + +$response = $postnl->shipmentDelivery()->labelConfirm($request); +``` + +#### Using Fluent Interface + +```php +use Postnl\Sdk\RequestData\V4\ShipmentDelivery\ShipmentDeliveryRequest; + +$request = (new ShipmentDeliveryRequest()) + ->sender($sender) + ->receiver($receiver) + ->labelSettings($labelSettings) + ->shipmentType(ShipmentType::Parcel) + ->handOverDate('2024-01-15') + ->services($services) + ->itemsCount(1); + +$response = $postnl->shipmentDelivery()->labelConfirm($request); +``` + +### API Response Structure + +```json +{ + "items": [ + { + "shipmentReference": "REF-2024-001", + "barcode": "3SDEVC123456789", + "codingText": "D2132WT+42+0000000", + "labels": [ + { + "label": "JVBERi0xLjQK... (base64 encoded)", + "outputType": "pdf", + "labelType": "Label" + } + ], + "productService": { + "productData": "3085", + "services": ["002", "003"] + } + } + ] +} +``` + +### Working with the Response + +```php +$response = $postnl->shipmentDelivery()->labelConfirm($request); + +// Check response status +if ($response->isSuccess()) { + echo "Status: " . $response->meta()->statusCode; // 200 +} + +// Get the collection of shipping items +$collection = $response->shippingItems(); + +// Get total count +echo "Processed " . $collection->count() . " shipment(s)\n"; + +// Iterate over all items +foreach ($collection as $item) { + echo "Reference: " . $item->shipmentReference . "\n"; + echo "Barcode: " . $item->barcode . "\n"; + echo "Coding Text: " . $item->codingText . "\n"; + + // Save the label to a file + $label = $item->labels?->first(); + if ($label !== null && !$label->isEmpty()) { + $label->saveLabelAsFile('/path/to/labels/' . $item->barcode . '.pdf'); + echo "Label saved!\n"; + } + + // Access product service info + if ($item->productService !== null) { + echo "Product: " . $item->productService->productData . "\n"; + } +} + +// Get first item directly +$firstItem = $collection->first(); + +// Get raw array response +$rawData = $response->meta()->toArray(); + +// Get request correlation ID +$requestId = $response->meta()->requestId; + +// On HTTP 429, catch RateLimitException (see docs/ErrorHandling/README.md). +``` + +--- + +## Label Settings + +Configure label generation options using the `LabelSettings` class. + +### LabelSettings Properties + +| Property | Type | Description | +|----------|------|-------------| +| `outputType` | LabelOutputType | The file format of the label (pdf, zpl, gif, jpg, png) | +| `resolution` | LabelResolution | The resolution in DPI (200, 300, 600) | +| `pageOrientation` | LabelPageOrientation | The orientation of the page (portrait, landscape) | +| `mergeType` | LabelMergeType | Merge multiple labels into one document (singlepdf, pdfa6toa4) | +| `positioning` | LabelPositioning | Position of the label on the page (topleft, topright, bottomleft, bottomright) | +| `printMethod` | LabelPrintMethod | Specifies which party will print the label (consumerPrint, retailPrint) | + +### LabelSettings Example + +```php +use Postnl\Sdk\Enums\Payload\LabelMergeType; +use Postnl\Sdk\Enums\Payload\LabelOutputType; +use Postnl\Sdk\Enums\Payload\LabelPageOrientation; +use Postnl\Sdk\Enums\Payload\LabelPositioning; +use Postnl\Sdk\Enums\Payload\LabelPrintMethod; +use Postnl\Sdk\Enums\Payload\LabelResolution; +use Postnl\Sdk\RequestData\V4\LabelSettings; + +// PDF label for consumer printing +$labelSettings = new LabelSettings( + outputType: LabelOutputType::PDF, + resolution: LabelResolution::DPI_300, + pageOrientation: LabelPageOrientation::Portrait, + printMethod: LabelPrintMethod::ConsumerPrint, +); + +// ZPL label for thermal printers +$thermalLabelSettings = new LabelSettings( + outputType: LabelOutputType::ZPL, + resolution: LabelResolution::DPI_200, +); + +// Merged labels (A6 to A4) +$mergedLabelSettings = new LabelSettings( + outputType: LabelOutputType::PDF, + mergeType: LabelMergeType::PDFA6TOA4, + positioning: LabelPositioning::TopLeft, +); +``` + +### Print Method Recommendations + +| Print Method | Recommended Output Type | Use Case | +|--------------|------------------------|----------| +| `consumerPrint` | PDF | End consumers printing at home | +| `retailPrint` | PNG or JPG | Retail/store printing with image-based printers | + +--- + +## Data Models + +### ShipmentParty + +Represents a party (sender or receiver) in a shipment or return shipment. Use the static constructors to create parties for the correct role: + +| Static Method | Use Case | Parameters | +|---------------|----------|-------------| +| `asSender()` | Direct shipment sender (merchant dispatching) | customerNumber, customerCode, address, undeliverableReturnAddress? | +| `asReceiver()` | Direct shipment receiver (consumer receiving) | address, contact?, receiverType? | +| `asReturnSender()` | Return shipment sender (consumer returning) | address, contact? | +| `asReturnReceiver()` | Return shipment receiver (merchant receiving) | customerNumber, customerCode, address, contact? | + +```php +final readonly class ShipmentParty +{ + public ?string $customerNumber; // Customer number (max 10 chars) + public ?string $customerCode; // Customer code (max 6 chars) + public ?Address $address; // Postal address + public ?Address $undeliverableReturnAddress; // Return address for undeliverable items + public ?Contact $contact; // Contact information (name, email, phone) + public ?ReceiverType $receiverType; // business or consumer +} +``` + +### DeliveryLocation + +Represents an alternative delivery location for the shipment. + +```php +final readonly class DeliveryLocation +{ + public ?string $pickupLocationId; // Pickup location id (parcel locker / PostNL location) + public ?Address $address; // Or alternative delivery address (exactly one of the two) +} +``` + +### ProcessedShippingItem + +Represents a processed shipment item with its generated label. + +```php +readonly class ProcessedShippingItem +{ + public ?string $shipmentReference; // Your reference for the shipment + public ?string $returnReference; // Return shipment reference (e.g. label-in-the-box) + public ?LabelsCollection $labels; // Collection of generated labels + public ?string $barcode; // PostNL barcode for tracking + public ?string $returnBarcode; // Return parcel barcode + public ?string $partnerId; // Carrier-id of commercial network partner (last mile) + public ?string $partnerBarcode; // Partner barcode at commercial network partner + public ?string $codingText; // Sorting/routing code (e.g. letterbox NL) + public ?ProductService $productService; // Product and service details +} +``` + +### Label + +Represents label data returned from the API. + +```php +readonly class Label +{ + public ?string $label; // Base64 encoded label content + public ?string $partnerId; // Partner identifier + public ?string $partnerBarcode; // Partner's barcode reference + public ?string $mergedLabelContent; // Merged labels (base64) + public ?LabelOutputType $outputType; // Format: pdf, zpl, jpg, gif, png + public ?LabelType $labelType; // Type: Label, labelinthebox, shipmentandreturnlabel, retourLabel, CN23, CommercialInvoice +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `isEmpty()` | bool | Returns `true` if label has no content or output type | +| `saveLabelAsFile(string $filepath)` | void | Decode and save label to file | + +**Example - Saving a Label:** + +```php +$item = $collection->first(); + +$label = $item->labels?->first(); + +if ($label !== null && !$label->isEmpty()) { + // Save as PDF + $label->saveLabelAsFile('/path/to/label.pdf'); + + // Or save with barcode as filename + $filename = $item->barcode . '.' . $label->outputType->value; + $label->saveLabelAsFile('/path/to/labels/' . $filename); +} +``` + +### ProductService + +Product and service information for the shipment. + +```php +readonly class ProductService +{ + public ?string $productData; // Product code identifier + public ?array $services; // Array of service codes + public ?array $bundles; // Bundle information +} +``` + +### ShippingItem (Request) + +Individual item details for multi-item shipments. + +```php +final readonly class ShippingItem +{ + public ?string $barcode; // Pre-generated barcode + public ?CustomerReferences $customerReferences; // Custom references + public ?Dimensions $dimensions; // Item dimensions +} +``` + +--- + +## Collection Methods + +`ShippingItemsCollection` provides methods for working with processed shipment items: + +### Base Methods + +| Method | Return | Description | +|--------|--------|-------------| +| `count()` | int | Number of items in collection | +| `isEmpty()` | bool | Returns `true` if collection is empty | +| `all()` | array | Returns all ProcessedShippingItem objects | +| `first()` | ?ProcessedShippingItem | Returns first item or null | +| `last()` | ?ProcessedShippingItem | Returns last item or null | +| `get(int $index)` | ?ProcessedShippingItem | Returns item at index or null | + +### Filtering Methods + +All filter methods return a new collection (immutable). + +```php +// Filter items with labels +$withLabels = $collection->filter(function ($item) { + return $item->labels !== null && !$item->labels->isEmpty(); +}); + +// Filter items with barcodes +$withBarcodes = $collection->filter(function ($item) { + return $item->barcode !== null; +}); + +// Filter by specific reference prefix +$filtered = $collection->filter(function ($item) { + return str_starts_with($item->shipmentReference ?? '', 'REF-2024'); +}); +``` + +### Helper Methods + +```php +// Find first item with a specific barcode +$found = $collection->find( + fn ($item) => $item->barcode === '3SDEVC123456789' +); + +// Check if any items have labels +$hasLabels = $collection->some( + fn ($item) => $item->labels !== null +); + +// Check if all items have barcodes +$allHaveBarcodes = $collection->every( + fn ($item) => $item->barcode !== null +); + +// Extract all barcodes as array +$barcodes = $collection->map( + fn ($item) => $item->barcode +); +``` + +### Iteration + +```php +// Iterate using foreach +foreach ($collection as $item) { + echo $item->barcode . "\n"; +} + +// Using iterator +$iterator = $collection->getIterator(); +``` + +### Saving All Labels + +```php +$collection = $response->shippingItems(); +$outputDir = '/path/to/labels/'; + +foreach ($collection as $item) { + $label = $item->labels?->first(); + if ($label !== null && !$label->isEmpty()) { + $filename = $item->barcode . '.' . $label->outputType->value; + $label->saveLabelAsFile($outputDir . $filename); + } +} +``` + +--- + +## Error Handling + +The SDK throws semantic exceptions for validation and request errors. + +> For the complete exception hierarchy, ProblemDetails, and retry behavior, see the [Error Handling guide](../ErrorHandling/README.md). + +### Exception Types + +| Exception | Description | +|-----------|-------------| +| `ValidationException` | Invalid request parameters (400, 422) | +| `AuthenticationException` | Invalid or insufficient credentials (401, 403) | +| `RateLimitException` | Too many requests (429, retryable) | +| `TimeoutException` | Request timed out (408, retryable) | +| `ClientException` | Other client errors (4xx, non-retryable) | +| `ServerException` | Server error (5xx, retryable) | +| `HttpSdkException` | Base class for all HTTP errors | + +### Common Validation Errors + +| Error Scenario | Description | +|----------------|-------------| +| Missing sender | Sender information is required | +| Missing receiver | Receiver information is required | +| Invalid postal code | Postal code format is invalid | +| Invalid country | Country code not supported | +| Invalid shipment type | Must be valid ShipmentType value | +| Missing credentials | customerCode and customerNumber required | +| Invalid label output type | Must be valid LabelOutputType value | + +### Example Error Handling + +```php +use Postnl\Sdk\Exception\Client\AuthenticationException; +use Postnl\Sdk\Exception\Client\ValidationException; +use Postnl\Sdk\Exception\HttpSdkException; + +try { + $response = $postnl->shipmentDelivery()->labelConfirm($request); + $collection = $response->shippingItems(); + + foreach ($collection as $item) { + $label = $item->labels?->first(); + if ($label !== null && !$label->isEmpty()) { + $label->saveLabelAsFile('/path/to/label.pdf'); + } + echo "Processed: " . $item->barcode . "\n"; + } + +} catch (ValidationException $e) { + // Handle validation errors (400, 422) + echo "Validation failed: " . $e->getMessage() . "\n"; + foreach ($e->fieldErrors as $error) { + echo " Field '{$error->field}': {$error->message}\n"; + } + +} catch (AuthenticationException $e) { + // Handle authentication/authorization errors (401, 403) + echo "Authentication failed: " . $e->getMessage() . "\n"; + +} catch (HttpSdkException $e) { + // Handle all other HTTP errors + echo "Request failed [{$e->statusCode}]: " . $e->getMessage() . "\n"; + if ($e->problemDetails->traceId !== null) { + echo "Trace ID: " . $e->problemDetails->traceId . "\n"; + } +} +``` + +--- + +## Enums Reference + +### LabelOutputType + +```php +use Postnl\Sdk\Enums\Payload\LabelOutputType; + +LabelOutputType::PDF->value; // 'pdf' +LabelOutputType::ZPL->value; // 'zpl' +LabelOutputType::JPG->value; // 'jpg' +LabelOutputType::GIF->value; // 'gif' +LabelOutputType::PNG->value; // 'png' +``` + +### LabelType + +```php +use Postnl\Sdk\Enums\Payload\LabelType; + +LabelType::Label->value; // 'Label' +LabelType::LabelInTheBox->value; // 'labelinthebox' +LabelType::ShipmentAndReturnLabel->value; // 'shipmentandreturnlabel' +``` + +### LabelResolution + +```php +use Postnl\Sdk\Enums\Payload\LabelResolution; + +LabelResolution::DPI_200->value; // 200 +LabelResolution::DPI_300->value; // 300 +LabelResolution::DPI_600->value; // 600 +``` + +### LabelPageOrientation + +```php +use Postnl\Sdk\Enums\Payload\LabelPageOrientation; + +LabelPageOrientation::Portrait->value; // 'portrait' +LabelPageOrientation::Landscape->value; // 'landscape' +``` + +### LabelMergeType + +```php +use Postnl\Sdk\Enums\Payload\LabelMergeType; + +LabelMergeType::SinglePDF->value; // 'singlepdf' +LabelMergeType::PDFA6TOA4->value; // 'pdfa6toa4' +``` + +### LabelPositioning + +```php +use Postnl\Sdk\Enums\Payload\LabelPositioning; + +LabelPositioning::TopLeft->value; // 'topleft' +LabelPositioning::TopRight->value; // 'topright' +LabelPositioning::BottomLeft->value; // 'bottomleft' +LabelPositioning::BottomRight->value; // 'bottomright' +``` + +### LabelPrintMethod + +```php +use Postnl\Sdk\Enums\Payload\LabelPrintMethod; + +LabelPrintMethod::ConsumerPrint->value; // 'consumerPrint' +LabelPrintMethod::RetailPrint->value; // 'retailPrint' +``` + +### ShipmentType + +```php +use Postnl\Sdk\Enums\Payload\ShipmentType; + +ShipmentType::Parcel->value; // 'parcel' +ShipmentType::NonStandardParcel->value; // 'parcelnonstandard' +ShipmentType::Letter->value; // 'letter' +ShipmentType::LetterBox->value; // 'letterbox' +ShipmentType::Pallet->value; // 'pallet' +ShipmentType::Packet->value; // 'packet' +``` + +### ReceiverType + +```php +use Postnl\Sdk\Enums\Payload\ReceiverType; + +ReceiverType::Business->value; // 'business' +ReceiverType::Consumer->value; // 'consumer' +``` + +--- + +## Complete Example + +```php +shipmentDelivery()->labelConfirm($request); + $collection = $response->shippingItems(); + + // Check if successful + if ($response->isSuccess()) { + echo "Successfully processed {$collection->count()} shipment(s)\n\n"; + } + + // Process items + foreach ($collection as $index => $item) { + echo sprintf("[%d] Shipment Processed\n", $index + 1); + echo " Reference: " . ($item->shipmentReference ?? 'N/A') . "\n"; + echo " Barcode: " . ($item->barcode ?? 'N/A') . "\n"; + echo " Coding Text: " . ($item->codingText ?? 'N/A') . "\n"; + + // Save the label + $label = $item->labels?->first(); + if ($label !== null && !$label->isEmpty()) { + $filename = '/path/to/labels/' . $item->barcode . '.pdf'; + $label->saveLabelAsFile($filename); + echo " Label saved to: " . $filename . "\n"; + } + + if ($item->productService !== null) { + echo " Product: " . $item->productService->productData . "\n"; + echo " Services: " . implode(', ', $item->productService->services ?? []) . "\n"; + } + echo "\n"; + } + + // Extract all barcodes for tracking + $barcodes = $collection->map(fn ($item) => $item->barcode); + echo "All barcodes: " . implode(', ', array_filter($barcodes)) . "\n"; + +} catch (ValidationException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +``` diff --git a/docs/postnl-v4-migration/sources/sdk-docs/TimeFrame/README.md b/docs/postnl-v4-migration/sources/sdk-docs/TimeFrame/README.md new file mode 100644 index 00000000..edd5f202 --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/TimeFrame/README.md @@ -0,0 +1,635 @@ +# TimeFrame API Documentation + +The TimeFrame functionality allows you to retrieve available delivery timeframes for PostNL shipments. It supports both single service queries (one delivery option) and multiple services queries (comparing different delivery options). + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Single Service Timeframe](#single-service-timeframe) +- [Multiple Services Timeframe](#multiple-services-timeframe) +- [Data Models](#data-models) +- [Collection Methods](#collection-methods) +- [Error Handling](#error-handling) +- [Enums Reference](#enums-reference) + +--- + +## Prerequisites + +### SDK Setup + +See the main SDK setup guide in the root documentation: +➡️ [SDK Root Documentation](../../README.md) + +--- + +### Required Credentials + +All TimeFrame requests require: +- `customerCode` - Your PostNL customer code +- `customerNumber` - Your PostNL customer number + +--- + +## Single Service Timeframe + +Query delivery timeframes for a single service type (e.g., daytime or evening delivery). + +### Endpoint + +``` +POST /shipment/delivery/v4/timeframe/singleservice +``` + +### Request Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `handoverDate` | string | Yes | Date when shipment is handed to PostNL (ISO 8601: `YYYY-MM-DD`) | +| `deliveryDays` | int | No | Number of delivery days to include (1-14, default varies) | +| `receiverAddress` | Address | Yes | Receiver address with `postalCode` and `countryIso` | +| `service` | string | Yes | Service type: `daytime` or `evening` | +| `shipmentType` | string | No | Shipment type: `parcel`, `letterbox`, `pallet`, `packet` | +| `customerCode` | string | Yes | Your PostNL customer code | +| `customerNumber` | string | Yes | Your PostNL customer number | + +### Code Examples + +#### Using Constructor + +```php +use Postnl\Sdk\Enums\ShipmentType; +use Postnl\Sdk\Enums\TimeFrameService; +use Postnl\Sdk\RequestData\V4\Address; +use Postnl\Sdk\Service\Checkout\V4\Request\SingleServiceTimeframeRequest; + +$request = new SingleServiceTimeframeRequest( + handoverDate: date('Y-m-d', strtotime('+1 day')), + deliveryDays: 7, + receiverAddress: new Address( + postalCode: '2595AA', + countryIso: 'NL', + ), + service: TimeFrameService::Daytime->value, + shipmentType: ShipmentType::Parcel->value, + customerCode: 'YOUR_CUSTOMER_CODE', + customerNumber: 'YOUR_CUSTOMER_NUMBER', +); + +$response = $postnl->checkout()->getSingleServiceTimeframe($request); +``` + +#### Using fromArray Factory + +```php +use Postnl\Sdk\Service\Checkout\V4\Request\SingleServiceTimeframeRequest; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = SingleServiceTimeframeRequest::fromArray([ + 'handoverDate' => '2024-01-15', + 'deliveryDays' => 7, + 'receiverAddress' => [ + 'postalCode' => '2595AA', + 'countryIso' => 'NL', + ], + 'service' => 'daytime', + 'shipmentType' => 'parcel', + 'customerCode' => 'YOUR_CUSTOMER_CODE', + 'customerNumber' => 'YOUR_CUSTOMER_NUMBER', +], $mapper); + +$response = $postnl->checkout()->getSingleServiceTimeframe($request); +``` + +### API Response Structure + +```json +{ + "deliveryDates": [ + { + "deliveryDate": "2024-01-16", + "timeFrame": { + "from": "08:00:00", + "until": "10:30:00" + }, + "sustainability": { + "code": "00", + "description": "Not available" + }, + "service": "daytime", + "shipmentType": "parcel" + }, + { + "deliveryDate": "2024-01-17", + "timeFrame": { + "from": "08:30:00", + "until": "21:30:00" + }, + "sustainability": { + "code": "02", + "description": "Sustainable" + }, + "service": "daytime", + "shipmentType": "parcel" + } + ] +} +``` + +### Working with the Response + +```php +$response = $postnl->checkout()->getSingleServiceTimeframe($request); + +// Get the collection of timeframes +$collection = $response->timeframesSingleServiceCollection(); + +// Check response status +if ($response->isSuccess()) { + echo "Status: " . $response->meta()->statusCode; // 200 +} + +// Iterate over all timeframes +foreach ($collection as $timeframe) { + echo $timeframe->deliveryDate; // "2024-01-16" + echo $timeframe->service; // "daytime" + echo $timeframe->shipmentType; // "parcel" + echo $timeframe->timeFrame->from; // "08:00:00" + echo $timeframe->timeFrame->until; // "10:30:00" + echo $timeframe->timeFrame->getFormattedRange(); // "08:00:00 - 10:30:00" + echo $timeframe->sustainability->code; // "00" + echo $timeframe->sustainability->isSustainable(); // false +} + +// Get raw array response +$rawData = $response->meta()->toArray(); +``` + +--- + +## Multiple Services Timeframe + +Query delivery timeframes for multiple service types simultaneously, allowing comparison between options. + +### Endpoint + +``` +POST /shipment/delivery/v4/timeframe/multipleservices +``` + +### Request Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `handoverDate` | string | Yes | Date when shipment is handed to PostNL (ISO 8601: `YYYY-MM-DD`) | +| `numberOfDays` | int | No | Number of days to look ahead (1-14) | +| `receiverAddress` | Address | Yes | Receiver address with `postalCode` and `countryIso` | +| `services` | array | Yes | Array of service types: `['daytime', 'evening']` | +| `shipmentType` | string | No | Shipment type: `parcel`, `letterbox`, `pallet`, `packet` | +| `customerCode` | string | Yes | Your PostNL customer code | +| `customerNumber` | string | Yes | Your PostNL customer number | + +### Code Examples + +#### Using Constructor + +```php +use Postnl\Sdk\Enums\ShipmentType; +use Postnl\Sdk\Enums\TimeFrameService; +use Postnl\Sdk\RequestData\V4\Address; +use Postnl\Sdk\Service\Checkout\V4\Request\MultipleServicesTimeframeRequest; + +$request = new MultipleServicesTimeframeRequest( + handoverDate: date('Y-m-d', strtotime('+1 day')), + receiverAddress: new Address( + postalCode: '2595AA', + countryIso: 'NL', + ), + services: [ + TimeFrameService::Daytime->value, + TimeFrameService::Evening->value, + ], + shipmentType: ShipmentType::Parcel->value, + numberOfDays: 14, + customerCode: 'YOUR_CUSTOMER_CODE', + customerNumber: 'YOUR_CUSTOMER_NUMBER', +); + +$response = $postnl->checkout()->getMultipleServicesTimeframe($request); +``` + +#### Using fromArray Factory + +```php +use Postnl\Sdk\Service\Checkout\V4\Request\MultipleServicesTimeframeRequest; +use Postnl\Sdk\Support\PayloadMapper; + +$mapper = PayloadMapper::create(); +$request = MultipleServicesTimeframeRequest::fromArray([ + 'handoverDate' => '2024-01-15', + 'numberOfDays' => 14, + 'receiverAddress' => [ + 'postalCode' => '2595AA', + 'countryIso' => 'NL', + ], + 'services' => ['daytime', 'evening'], + 'shipmentType' => 'parcel', + 'customerCode' => 'YOUR_CUSTOMER_CODE', + 'customerNumber' => 'YOUR_CUSTOMER_NUMBER', +], $mapper); + +$response = $postnl->checkout()->getMultipleServicesTimeframe($request); +``` + +### API Response Structure + +```json +{ + "deliveryDates": [ + { + "deliveryDate": "2024-01-16", + "services": [ + { + "service": "daytime", + "timeFrame": { + "from": "08:00:00", + "until": "12:00:00" + }, + "sustainability": { + "code": "00", + "description": "Not available" + }, + "shipmentType": "parcel", + "availability": true, + "reason": null + }, + { + "service": "evening", + "timeFrame": { + "from": "18:00:00", + "until": "22:00:00" + }, + "sustainability": { + "code": "02", + "description": "Sustainable" + }, + "shipmentType": "parcel", + "availability": true, + "reason": null + } + ] + } + ] +} +``` + +### Working with the Response + +```php +$response = $postnl->checkout()->getMultipleServicesTimeframe($request); + +// Get the flattened collection of all timeframes +$collection = $response->timeframesMultipleServicesCollection(); + +// The collection flattens the nested structure +// Each item includes deliveryDate from parent level +foreach ($collection as $timeframe) { + echo $timeframe->deliveryDate; // "2024-01-16" + echo $timeframe->service; // "daytime" or "evening" + echo $timeframe->isAvailable(); // true/false + echo $timeframe->reason; // null or reason string if unavailable +} + +// Total count (services * days) +echo $collection->count(); // e.g., 28 for 2 services * 14 days +``` + +--- + +## Data Models + +### SingleTimeFrame + +Represents a single delivery timeframe option. + +```php +readonly class SingleTimeFrame +{ + public ?string $deliveryDate; // "2024-01-16" + public ?TimeSlot $timeFrame; // Time window object + public ?Sustainability $sustainability; // Sustainability info + public ?bool $availability; // true if available (multiple services only) + public ?string $reason; // Reason if unavailable + public ?string $service; // "daytime" or "evening" + public ?string $shipmentType; // "parcel", "letterbox", etc. +} +``` + +**Methods:** + +| Method / Property | Return | Description | +|-------------------|--------|-------------| +| `isAvailable()` | bool | Returns `true` if `availability === true` | +| `sustainability` | `?Sustainability` | Sustainability info; use `$timeframe->sustainability?->isSustainable()` to check if it is sustainable | + +### TimeSlot + +Represents the delivery time window. + +```php +readonly class TimeSlot +{ + public ?string $from; // "08:00:00" + public ?string $until; // "17:30:00" +} +``` + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `getFormattedRange()` | string | Returns `"08:00:00 - 17:30:00"` | +| `isMorning()` | bool | Returns `true` if delivery until 12:00 | +| `isEvening()` | bool | Returns `true` if delivery from 17:00+ | +| `is24Hour()` | bool | Returns `true` if slot covers entire day | + +### Sustainability + +Represents sustainability information for the delivery option. + +```php +readonly class Sustainability +{ + public ?string $code; // "00", "01", "02", "03" + public ?string $description; // "Not available", "Carbon neutral", etc. +} +``` + +**Sustainability Codes:** + +| Code | Description | Sustainable | +|------|-------------|-------------| +| `00` | Not available | No | +| `01` | Carbon neutral | Yes | +| `02` | Sustainable | Yes | +| `03` | Sustainable Plus | Yes | + +**Methods:** + +| Method | Return | Description | +|--------|--------|-------------| +| `isSustainable()` | bool | Returns `true` if code is not `'00'` | +| `isCarbonNeutral()` | bool | Returns `true` if code is `'01'` or `'03'` | + +--- + +## Collection Methods + +Both `TimeframeSingleServiceCollection` and `TimeframeMultipleServicesCollection` provide these methods: + +### Base Methods + +| Method | Return | Description | +|--------|--------|-------------| +| `count()` | int | Number of items in collection | +| `isEmpty()` | bool | Returns `true` if collection is empty | +| `all()` | array | Returns all items as array | +| `first()` | ?SingleTimeFrame | Returns first item or null | +| `last()` | ?SingleTimeFrame | Returns last item or null | +| `get(int $index)` | ?SingleTimeFrame | Returns item at index or null | + +### Filtering Methods + +All filter methods return a new collection (immutable). + +```php +// Filter to only available timeframes +$available = $collection->filterAvailable(); + +// Filter to unavailable timeframes (useful for debugging) +$unavailable = $collection->filter(function ($timeframe) { + return !$timeframe->isAvailable(); +}); + +// Filter by specific service (e.g. "daytime" or "evening") +$daytimeOnly = $collection->filter(function ($timeframe) { + return $timeframe->getService() === 'daytime'; +}); + +$eveningOnly = $collection->filter(function ($timeframe) { + return $timeframe->getService() === 'evening'; +}); + +// Filter to sustainable delivery options only +$sustainable = $collection->filter(function ($timeframe) { + return $timeframe->isSustainable(); +}); + +// Filter by specific delivery date +$specificDate = $collection->filter(function ($timeframe) { + return $timeframe->getDeliveryDate() === '2024-01-17'; +}); + +// Chain multiple filters using successive callbacks +$result = $collection + ->filter(function ($timeframe) { + return $timeframe->isAvailable(); + }) + ->filter(function ($timeframe) { + return $timeframe->getService() === 'daytime'; + }) + ->filter(function ($timeframe) { + return $timeframe->isSustainable(); + }); +``` + +### Sorting Methods + +```php +// Sort by delivery date (ascending) +$sorted = $collection->sortByDate(); +``` + +### Helper Methods + +```php +// Get the earliest available delivery date +$earliestDate = $collection->sortByDate()->first()?->deliveryDate; // "2024-01-16" or null + +// Custom filtering with callback +$custom = $collection->filter( + fn (SingleTimeFrame $tf) => $tf->timeFrame?->isMorning() +); + +// Find first matching item +$found = $collection->find( + fn (SingleTimeFrame $tf) => $tf->deliveryDate === '2024-01-17' +); + +// Check if any match condition +$hasEvening = $collection->some( + fn (SingleTimeFrame $tf) => $tf->service === 'evening' +); + +// Check if all match condition +$allAvailable = $collection->every( + fn (SingleTimeFrame $tf) => $tf->isAvailable() +); + +// Map to custom array +$dates = $collection->map( + fn (SingleTimeFrame $tf) => $tf->deliveryDate +); +``` + +### Iteration + +```php +// Iterate using foreach +foreach ($collection as $timeframe) { + // Process each SingleTimeFrame +} + +// Using iterator +$iterator = $collection->getIterator(); +``` + +--- + +## Error Handling + +The SDK throws `ValidationException` for validation errors. For the complete error handling guide, see [Error Handling](../ErrorHandling/README.md). + +### Common Validation Errors + +| Error Scenario | Description | +|----------------|-------------| +| `deliveryDays` > 14 | Value must be between 1 and 14 | +| `numberOfDays` > 14 | Value must be between 1 and 14 | +| Invalid `handoverDate` | Must be valid ISO 8601 date format | +| Missing `receiverAddress` | Address is required | +| Invalid postal code | Postal code must exist | +| Missing credentials | `customerCode` and `customerNumber` required | +| Invalid `service` | Must be `daytime` or `evening` | +| Invalid `shipmentType` | Must be `parcel`, `letterbox`, `pallet`, or `packet` | + +### Example Error Handling + +```php +use Postnl\Sdk\Exception\Client\ValidationException; + +try { + $response = $postnl->checkout()->getSingleServiceTimeframe($request); + $collection = $response->timeframesSingleServiceCollection(); + + // Process timeframes... + +} catch (ValidationException $e) { + // Handle validation errors + echo "Request failed: " . $e->getMessage(); + echo "Status code: " . $e->getCode(); +} +``` + +--- + +## Enums Reference + +### TimeFrameService + +```php +use Postnl\Sdk\Enums\TimeFrameService; + +TimeFrameService::Daytime->value; // 'daytime' +TimeFrameService::Evening->value; // 'evening' +``` + +### ShipmentType + +```php +use Postnl\Sdk\Enums\ShipmentType; + +ShipmentType::Parcel->value; // 'parcel' +ShipmentType::LetterBox->value; // 'letterbox' +ShipmentType::Pallet->value; // 'pallet' +ShipmentType::Packet->value; // 'packet' +``` + +### LocationSustainabilityCode + +```php +use Postnl\Sdk\Enums\LocationSustainabilityCode; + +LocationSustainabilityCode::NOT_AVAILABLE->value; // '00' +LocationSustainabilityCode::CARBON_NEUTRAL->value; // '01' +LocationSustainabilityCode::SUSTAINABLE->value; // '02' +LocationSustainabilityCode::SUSTAINABLE_PLUS->value; // '03' + +// Helper methods +LocationSustainabilityCode::SUSTAINABLE->getLevel(); // 2 +LocationSustainabilityCode::isValid('02'); // true +``` + +--- + +## Complete Example + +```php +value, + TimeFrameService::Evening->value, + ], + shipmentType: ShipmentType::Parcel->value, + numberOfDays: 7, + customerCode: 'YOUR_CUSTOMER_CODE', + customerNumber: 'YOUR_CUSTOMER_NUMBER', +); + +try { + $response = $postnl->checkout()->multipleTimeframes($request); + $collection = $response->timeframesMultipleServicesCollection(); + + // Get earliest sustainable daytime delivery + $sustainable = $collection + ->filterAvailable(); + + if (!$sustainable->isEmpty()) { + $earliest = $sustainable->first(); + echo "Earliest sustainable delivery: {$earliest->deliveryDate}\n"; + echo "Time window: {$earliest->timeFrame->getFormattedRange()}\n"; + echo "Sustainability: {$earliest->sustainability->description}\n"; + } + + // List all available evening options + $eveningOptions = $collection + ->filterAvailable(); + + echo "\nAvailable evening deliveries:\n"; + foreach ($eveningOptions as $option) { + echo "- {$option->deliveryDate}: {$option->timeFrame->getFormattedRange()}\n"; + } + +} catch (ValidationException $e) { + echo "Error: " . $e->getMessage() . "\n"; +} +``` diff --git a/docs/postnl-v4-migration/sources/sdk-docs/Versioning.md b/docs/postnl-v4-migration/sources/sdk-docs/Versioning.md new file mode 100644 index 00000000..3c3d67b9 --- /dev/null +++ b/docs/postnl-v4-migration/sources/sdk-docs/Versioning.md @@ -0,0 +1,106 @@ +# API Versioning & Deprecation + +The PostNL PHP SDK supports multiple API versions via the `Version` enum (`V1`, `V4`). +All current service implementations target **V4**, which is the recommended version for all integrations. + +## Why V1 is deprecated + +`Version::V1` was an early API version for which no service implementations exist. +It is marked `#[DeprecatedVersion(since: '1.0', migrateToVersion: 'V4')]` on the enum constant. +Any attempt to create a service for V1 will emit a PHP `E_USER_DEPRECATED` error and a PSR-3 warning, pointing you to V4. + +## Migrating to V4 + +Replace any `Version::V1` references in your code by configuring the client to use `Version::V4`. +`Version::V4` is the default, so no explicit `withApiVersion()` call is required in most cases. + +```php +use Postnl\Sdk\Auth\Auth; +use Postnl\Sdk\Client\Postnl; +use Postnl\Sdk\Enums\Version; + +// Before (deprecated): code that attempted to use Version::V1 +// would receive a LogicSdkException (no V1 implementations exist), +// now preceded by an E_USER_DEPRECATED notice pointing to V4. + +// After — Version::V4 is the default; withApiVersion() is shown here +// only to make the migration intent explicit. +$client = Postnl::factory() + ->withAuth(Auth::apiKey('your-api-key')) + ->withApiVersion(Version::V4) + ->make(); + +$client->singleTimeframe()->getSingleServiceTimeframe($payload); +``` + +## Handling `E_USER_DEPRECATED` notices in test suites + +When running your tests you may see output like: + +``` +PostNL SDK: API version "V1" is deprecated since 1.0. Migrate to version "V4". +``` + +To suppress these in PHPUnit you can register a custom error handler in your `setUp()`: + +```php +protected function setUp(): void +{ + set_error_handler(static fn() => true, E_USER_DEPRECATED); +} + +protected function tearDown(): void +{ + restore_error_handler(); +} +``` + +Or, in `phpunit.xml`, configure `` and `failOnDeprecation="false"` as needed. + +## Registering per-service deprecations + +> **Note:** This is an advanced, internal-extension pattern intended for framework integrations +> and SDK forks. `ServiceFactory` is the internal service registry (see [CLAUDE.md](../CLAUDE.md)). +> This surface is **not** covered by the semver-stable public API guarantee. + +SDK integrators can register custom per-service deprecation entries at runtime only when working +with `ServiceFactory` directly. Call `deprecateVersion()` on your own factory instance in your +application bootstrapping layer or other custom integration code. + +This is **not supported via `Postnl::factory()` / `ClientBuilder`**. The builder creates and owns +its internal `ServiceFactory`, and `withService()` is only for registering interface-to-class +mappings, not for injecting a `ServiceFactory` instance or per-service deprecation metadata. + +```php +use Postnl\Sdk\Enums\Version; +use Postnl\Sdk\Service\Deprecation\DeprecationNotifier; +use Postnl\Sdk\Service\ServiceFactory; +use Postnl\Sdk\Service\SingleServiceTimeframe\SingleServiceTimeframeInterface; + +$factory = new ServiceFactory(notifier: new DeprecationNotifier($logger)); + +$factory->deprecateVersion( + interface: SingleServiceTimeframeInterface::class, + version: Version::V1, + since: '2024-01', + migrateToVersion: 'V4', + message: 'V1 timeframe endpoint removed from sandbox. Switch to V4 immediately.', +); +``` + +When `ServiceFactory::create()` is called for that interface+version, the per-service message fires +instead of the global enum attribute message — preventing double notifications and enabling targeted guidance. + +## Introspecting deprecation metadata at runtime + +You can read the `#[DeprecatedVersion]` attribute from any `Version` case: + +```php +$dep = Version::V1->deprecation(); +// $dep->since → '1.0' +// $dep->migrateToVersion → 'V4' +// $dep->message → '' + +$dep = Version::V4->deprecation(); +// null — V4 is not deprecated +``` diff --git a/docs/superpowers/plans/2026-05-13-postnl-v4-sdk-migration.md b/docs/superpowers/plans/2026-05-13-postnl-v4-sdk-migration.md new file mode 100644 index 00000000..20566165 --- /dev/null +++ b/docs/superpowers/plans/2026-05-13-postnl-v4-sdk-migration.md @@ -0,0 +1,1475 @@ +# PostNL V4 SDK Migration — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the PostNL for WooCommerce plugin from hand-rolled `wp_remote_request` API calls to the `postnl/api-client-sdk` V4 SDK, one flow at a time, with old-client fallback preserved until each flow passes staging parity. + +**Architecture:** A `ClientFactory` builds a `PostnlClientInterface` from plugin settings. A static `Router` decides per-flow whether the SDK or old client runs — all flows are off by default. Each `Rest_API/*/Client.php` overrides `send_request()` to check `Router::use_sdk_for()` first; when off, it falls through to `parent::send_request()` (the old HTTP call). Old clients are never removed until individual staging sign-off. Checkout is a special case: the old single-endpoint call is replaced by two SDK calls (TimeFrame + Locations) aggregated by Task 5. + +**Tech Stack:** PHP 8.2+, `postnl/api-client-sdk` via Private Packagist (`https://repo.packagist.com/postnl/`), WordPress/WooCommerce PSR-4 namespace `PostNLWooCommerce\`, PHPCS for linting (`composer check-php`), no PHPUnit — PHP testing is manual staging QA. + +--- + +## ⚠ Scope Note + +Tasks 6 (Shipping + Letterbox Labels), 8 (Smart Returns), and 9 (activatereturn) are **externally blocked**. Their implementation structure is documented here so work begins immediately when blockers clear — but no code is written for them until the blocker is resolved in writing. Tasks 0–2 and 7 have no external blockers and can begin right away. + +--- + +## ⚠ Verify These Before Writing Any Code + +Open the installed SDK source at `postnl-sdk-audit/vendor/postnl/api-client-sdk/src/` and confirm: + +1. **`Client/Client.php`** — Does `checkout()` exist? Or is it `singleTimeframe()` / `multipleTimeframes()`? Does `locations()` exist? Or is it `addressLocations()` / `coordinateLocations()`? +2. **`Service/Barcode/V4/Request/BarcodeRequest.php`** — Is the constructor param `serieStart` or `seriesStart`? +3. **`Enums/Payload/LabelType.php`** — List all enum cases. +4. **`Service/ServiceContext.php`** — Does a `$cache` property exist? +5. **`Service/Checkout/V4/Request/`** — Do `SingleServiceTimeframeRequest` and `MultipleServicesTimeframeRequest` live here, or under `Service/SingleServiceTimeframe/V4/Request/`? +6. **`Client/ClientBuilder.php`** — Is the retry builder method `withRetryPolicy()` or `withRetry()`? + +Record findings in `docs/postnl-v4-migration/postnl-v4-sdk-api-reference.md §11` before starting any task. Every `// VERIFY:` comment in the code below refers to one of these points. + +--- + +## File Structure + +### New files + +| File | Purpose | +|---|---| +| `src/SDK/ClientFactory.php` | Builds `PostnlClientInterface` from plugin settings | +| `src/SDK/Router.php` | Per-flow SDK/old-client switch; all flows off by default | +| `src/SDK/SdkExceptionConverter.php` | Converts `PostnlExceptionInterface` → `\Exception` | +| `src/SDK/Extension/ActivateReturnExtension.php` | Task 9 Option A only | + +### Modified files + +| File | Change | +|---|---| +| `composer.json` | Add SDK dependency + Private Packagist repo | +| `auth.json` (gitignored) | Private Packagist read-only token | +| `postnl-for-woocommerce.php` | PHP 8.2 admin notice guard | +| `src/Logger.php` | Strip raw base64 PDF binary from log output | +| `src/Rest_API/Barcode/Client.php` | Override `send_request()`; SDK path via `barcode()` | +| `src/Rest_API/Checkout/Client.php` | Override `send_request()`; SDK paths for TimeFrame + Locations | +| `src/Rest_API/Return_Label/Client.php` | Override `send_request()`; SDK path via `returnShipment()` | +| `src/Rest_API/Shipping/Client.php` | Task 6 (blocked): SDK path via `shipmentDelivery()` | +| `src/Rest_API/Letterbox/Client.php` | Task 6 (blocked): SDK path via `shipmentDelivery()` | +| `src/Rest_API/Shipment_and_Return/Client.php` | Task 9: extension or retention comment | + +--- + +## Task 0 — Composer Setup + PHP Guard + +**Status:** Required before all other tasks +**Files:** `composer.json`, `auth.json`, `postnl-for-woocommerce.php` + +**Context:** The SDK requires PHP ≥ 8.2. The plugin currently declares PHP ≥ 7.4. This plan adds a runtime admin notice on PHP < 8.2 so the plugin continues to load on older PHP — SDK calls are simply never reached. Bumping the plugin's declared PHP minimum is a separate release decision made outside this plan. + +- [ ] **Step 1: Confirm `auth.json` is gitignored** + +```bash +grep -n "auth.json" .gitignore +``` + +Expected: `auth.json` appears. If it does not, add it before continuing. + +- [ ] **Step 2: Create `auth.json` with read-only Packagist token** + +Create `auth.json` in the plugin root (this file must never be committed): + +```json +{ + "bearer": { + "repo.packagist.com": "REPLACE_WITH_READONLY_TOKEN" + } +} +``` + +Replace `REPLACE_WITH_READONLY_TOKEN` with the token PostNL provides for `repo.packagist.com`. + +- [ ] **Step 3: Add SDK to `composer.json`** + +Read `composer.json`, then add the `repositories` entry and the `require` line. The result must include: + +```json +{ + "repositories": [ + { + "type": "composer", + "url": "https://repo.packagist.com/postnl/" + } + ], + "require": { + "postnl/api-client-sdk": "^1.0" + } +} +``` + +Pin to `^1.0`. Do not use `dev-main` or `*`. Confirm the exact published version with PostNL before installing. + +- [ ] **Step 4: Install and verify no autoload conflicts** + +```bash +composer install +php -r "require 'vendor/autoload.php'; echo 'OK' . PHP_EOL;" +``` + +Expected: `OK` with no errors. If there are namespace conflicts with `clegginabox/pdf-merger`, open `composer.json` and check for conflicting autoload entries. + +- [ ] **Step 5: Add PHP 8.2 admin notice to `postnl-for-woocommerce.php`** + +After the plugin header block and before the bootstrap require, add: + +```php +add_action( + 'admin_notices', + function () { + if ( version_compare( PHP_VERSION, '8.2', '>=' ) ) { + return; + } + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + echo '

'; + printf( + /* translators: %s: current PHP version */ + esc_html__( 'PostNL for WooCommerce: V4 SDK features require PHP 8.2 or higher. Your server runs PHP %s. SDK-based flows are disabled until PHP is upgraded.', 'postnl-for-woocommerce' ), + esc_html( PHP_VERSION ) + ); + echo '

'; + } +); +``` + +- [ ] **Step 6: Run PHPCS** + +```bash +composer check-php +``` + +Expected: no new errors. + +- [ ] **Step 7: Commit** + +```bash +git add composer.json composer.lock postnl-for-woocommerce.php +git commit -m "feat: add postnl/api-client-sdk dependency and PHP 8.2 admin notice guard" +``` + +(Do not commit `auth.json` — it must remain gitignored.) + +--- + +## Task 1 — SDK ClientFactory + Router + Logger + +**Status:** Ready | **Depends on:** Task 0 +**Files:** `src/SDK/ClientFactory.php` (new), `src/SDK/Router.php` (new), `src/SDK/SdkExceptionConverter.php` (new), `src/Logger.php` (modify) + +**Context:** `ClientFactory` builds a `PostnlClientInterface` from plugin settings. `Router` decides per-flow which path runs — all flows off by default. `SdkExceptionConverter` converts SDK exceptions to `\Exception` so existing callers see the same error surface. Logger gets a new guard for raw base64 PDF binary that SDK responses may include. No API calls are made in this task. + +- [ ] **Step 1: Verify PSR-4 autoload covers `src/SDK/`** + +```bash +grep -n "PostNLWooCommerce" composer.json +``` + +Expected: `"PostNLWooCommerce\\\\": "src/"` is present. The new `src/SDK/` directory is covered by this mapping — no change needed. + +- [ ] **Step 2: Create `src/SDK/ClientFactory.php`** + +```php +is_sandbox() + ? $settings->get_api_key_sandbox() + : $settings->get_api_key(); + + $auth = Auth::apiKey( $api_key ); + + return $settings->is_sandbox() + ? PostnlSdk::sandboxClient( $auth ) + : PostnlSdk::client( $auth ); + } +} +``` + +- [ ] **Step 3: Create `src/SDK/Router.php`** + +```php +getMessage(), 0, $e ); + } +} +``` + +- [ ] **Step 5: Add base64 PDF binary guard to `src/Logger.php`** + +Read `src/Logger.php`. In the `write()` method, after the `$message = $this->check_pdf_content( $message );` line, add one more call: + +```php +$message = $this->check_pdf_content( $message ); +$message = $this->strip_binary_content( $message ); // add this line +``` + +Then add this private method to the class after `check_pdf_content()`: + +```php +/** + * Strip raw binary label content that the SDK may surface as a string. + * + * @param mixed $message Log message. + * @return mixed + */ +private function strip_binary_content( $message ) { + if ( ! is_string( $message ) ) { + return $message; + } + // JVBERi0 is base64 for %PDF — SDK label content arrives this way. + if ( 0 === strpos( $message, 'JVBERi0' ) || 0 === strpos( $message, '%PDF' ) ) { + return '[label binary redacted]'; + } + return $message; +} +``` + +- [ ] **Step 6: Run PHPCS** + +```bash +composer check-php +``` + +Expected: no errors in `src/SDK/` or `src/Logger.php`. + +- [ ] **Step 7: Smoke-test that classes load** + +```bash +php -r " +define('ABSPATH', realpath('../../../..') . '/'); +require 'vendor/autoload.php'; +echo class_exists('PostNLWooCommerce\\SDK\\ClientFactory') ? 'ClientFactory OK' : 'FAIL'; +echo PHP_EOL; +echo class_exists('PostNLWooCommerce\\SDK\\Router') ? 'Router OK' : 'FAIL'; +echo PHP_EOL; +" +``` + +Expected: +``` +ClientFactory OK +Router OK +``` + +- [ ] **Step 8: Commit** + +```bash +git add src/SDK/ClientFactory.php src/SDK/Router.php src/SDK/SdkExceptionConverter.php src/Logger.php +git commit -m "feat: add SDK ClientFactory, Router, SdkExceptionConverter; strip binary from logger" +``` + +--- + +## Task 2 — Barcode (SDK POC) + +**Status:** Ready | **Depends on:** Task 1 +**Files:** `src/Rest_API/Barcode/Client.php` (modify) + +**Context:** `Barcode\Client` calls `GET /shipment/v1_1/barcode` via the inherited `Base::send_request()`. This task overrides `send_request()` to check `Router::use_sdk_for('barcode')` first. When on, it calls `$client->barcode()->generateBarcode()`. The SDK path is off by default; flipping `Router::enable('barcode')` activates it. + +**Before starting — verify in installed SDK:** +- `src/Service/Barcode/V4/Request/BarcodeRequest.php` — constructor param name: `serieStart` or `seriesStart`? +- `src/Service/Barcode/V4/Response/GenerateBarcodeResponse.php` — how to extract the barcode string (e.g., `->barcodes()->first()->barcode()`). +- `src/Order/Base.php` — search for the call to `Barcode\Client::send_request()` and the key it reads from the response array (e.g., `$response['Barcode']`). The SDK path must return the same array structure. + +The existing `Item_Info` has `$this->item_info->query_args['serie']` as a range string like `'000000000-999999999'`. Split it to get `serieStart` and `serieEnd`. + +- [ ] **Step 1: Override `send_request()` in `src/Rest_API/Barcode/Client.php`** + +Read the file first. Replace the entire file content with: + +```php +item_info->query_args['barcode_type'], $this->item_info->query_args['globalpack_customer_code'] ); + + return array( + 'Type' => $this->item_info->query_args['barcode_type'], + 'Serie' => $this->item_info->query_args['serie'], + 'CustomerCode' => $this->item_info->query_args['customer_code'], + 'CustomerNumber' => $this->item_info->query_args['customer_num'], + 'Range' => $range, + ); + } + + /** + * Send API request — SDK path when enabled, old client otherwise. + * + * @throws \Exception On API or SDK error. + * @return array + */ + public function send_request() { + if ( ! Router::use_sdk_for( 'barcode' ) ) { + return parent::send_request(); + } + + $this->logger->write( 'Barcode: using SDK path.' ); + + try { + return $this->send_sdk_barcode_request(); + } catch ( PostnlExceptionInterface $e ) { + $this->logger->write( 'Barcode SDK error: ' . $e->getMessage() ); + throw SdkExceptionConverter::convert( $e ); + } + } + + /** + * Call the V4 barcode SDK service and return an array matching the old client response shape. + * + * VERIFY: check src/Order/Base.php to confirm the array key it reads from this response + * (e.g., $response['Barcode']). Adjust the return array below to match exactly. + * + * @throws PostnlExceptionInterface On SDK error. + * @return array + */ + private function send_sdk_barcode_request(): array { + $serie_parts = explode( '-', $this->item_info->query_args['serie'], 2 ); + $serie_start = $serie_parts[0] ?? '000000000'; + $serie_end = $serie_parts[1] ?? '999999999'; + + // VERIFY: confirm constructor param names against BarcodeRequest.php. + // SDK docs show serieStart/serieEnd; fromArray() may use seriesStart/seriesEnd. + $request = new BarcodeRequest( + customerNumber: $this->item_info->query_args['customer_num'], + customerCode: $this->item_info->query_args['customer_code'], + serieStart: $serie_start, + serieEnd: $serie_end, + numberOfBarcodes: 1, + ); + + $client = ( new ClientFactory() )->get_client(); + $response = $client->barcode()->generateBarcode( $request ); + + // VERIFY: confirm the collection accessor on GenerateBarcodeResponse. + // Adjust ->barcodes()->first()->barcode() if the API differs. + $barcode_string = $response->barcodes()->first()->barcode(); + + // Return the same shape that Order\Base reads from the old client response. + // Confirm the key name ('Barcode') against src/Order/Base.php before shipping. + return array( 'Barcode' => $barcode_string ); + } +} +``` + +- [ ] **Step 2: Run PHPCS** + +```bash +composer check-php +``` + +Expected: no errors. + +- [ ] **Step 3: Verify old path still works (SDK off)** + +In a staging WordPress environment with the plugin active (SDK path is off by default): +1. Open any order in WP Admin → PostNL meta box. +2. Generate a label. Confirm a barcode is created. +3. Check WC logs: the line "Barcode: using SDK path." must **not** appear. + +- [ ] **Step 4: Enable SDK path and test parity** + +Temporarily add to `postnl-for-woocommerce.php` (remove before committing): + +```php +add_action( + 'init', + static function () { + \PostNLWooCommerce\SDK\Router::enable( 'barcode' ); + } +); +``` + +1. Reload admin, open an order, generate a label. +2. Check WC log: "Barcode: using SDK path." must appear. +3. Verify the barcode string in `_postnl_order_metadata` matches the format from the old client (e.g., `3SDEVC...` for NL domestic). +4. Test international (EU/ROW) orders if applicable — verify barcode format. + +Expected: identical barcode format between SDK and old client for the same inputs. + +- [ ] **Step 5: Remove the temporary `Router::enable()` line** + +- [ ] **Step 6: Commit** + +```bash +git add src/Rest_API/Barcode/Client.php +git commit -m "feat: add SDK barcode path to Barcode\Client (off by default)" +``` + +--- + +## Task 3 — TimeFrame / Delivery Dates (SDK POC) + +**Status:** Ready | **Depends on:** Task 1 +**Files:** `src/Rest_API/Checkout/Client.php` (modify) + +**Context:** The current `Checkout\Client` calls `POST /shipment/v1/checkout`, which returns delivery days and pickup points combined. This task adds the SDK TimeFrame path (delivery days only). Router key is `'timeframe'`. Pickup points are untouched in this task. + +**Before starting:** +1. **Verify `Client::checkout()` exists** in `src/Client/Client.php`. If the method is `singleTimeframe()` / `multipleTimeframes()` instead, update every call below. +2. **Verify `SingleServiceTimeframeRequest` namespace.** SDK docs show `Postnl\Sdk\Service\Checkout\V4\Request\...`; code may show `Postnl\Sdk\Service\SingleServiceTimeframe\V4\Request\...`. Update the `use` statement. +3. **Read `src/Frontend/Delivery_Day.php`** — run the grep in Step 1 to find the exact array keys it reads from the checkout response. The mapper in Step 2 must return those keys. +4. **Read `src/Rest_API/Checkout/Item_Info.php`** — confirm what fields are available in `$this->item_info->body`, `$this->item_info->receiver`, and `$this->item_info->shipper`. + +- [ ] **Step 1: Find the response keys `Frontend\Delivery_Day` reads** + +```bash +grep -n "DeliveryOptions\|TimeFrames\|ReasonNoTimeframe\|Timeframes\|timeframe\|Date\|From\|To\|Options" src/Frontend/Delivery_Day.php | head -40 +``` + +Write down every array key. You will use them in `map_timeframe_response()` below. + +- [ ] **Step 2: Add SDK TimeFrame path to `src/Rest_API/Checkout/Client.php`** + +Read the file. Keep all existing methods (`compose_body_request()`, `get_cutoff_times()`, `get_checkout_options()`) unchanged. Add the imports and new methods shown below to the existing class: + +```php +// Add these use statements at the top with the existing ones: +use Postnl\Sdk\Exception\PostnlExceptionInterface; +// VERIFY namespace — may be Postnl\Sdk\Service\SingleServiceTimeframe\V4\Request\... in code: +use Postnl\Sdk\Service\Checkout\V4\Request\SingleServiceTimeframeRequest; +use Postnl\Sdk\Service\Checkout\V4\Request\MultipleServicesTimeframeRequest; +use PostNLWooCommerce\SDK\ClientFactory; +use PostNLWooCommerce\SDK\Router; +use PostNLWooCommerce\SDK\SdkExceptionConverter; +use PostNLWooCommerce\Shipping_Method\Settings; +``` + +Add these methods inside the class after `get_checkout_options()`: + +```php +/** + * Send API request — SDK TimeFrame path when enabled, old client otherwise. + * + * @throws \Exception On API or SDK error. + * @return array + */ +public function send_request() { + if ( ! Router::use_sdk_for( 'timeframe' ) ) { + return parent::send_request(); + } + + $this->logger->write( 'Checkout: using SDK TimeFrame path.' ); + + try { + return $this->send_sdk_timeframe_request(); + } catch ( PostnlExceptionInterface $e ) { + $this->logger->write( 'TimeFrame SDK error: ' . $e->getMessage() ); + throw SdkExceptionConverter::convert( $e ); + } +} + +/** + * Call V4 TimeFrame SDK service; return array shaped for Frontend\Delivery_Day. + * + * Pickup options are empty in this response — Task 4 adds them. + * + * @throws PostnlExceptionInterface On SDK error. + * @return array + */ +private function send_sdk_timeframe_request(): array { + $settings = Settings::get_instance(); + $customer_code = $settings->get_field_value( 'customer_code' ); + $customer_number = $settings->get_field_value( 'customer_num' ); + + $receiver_address = array( + 'countryIso' => $this->item_info->receiver['country'], + 'postalCode' => $this->item_info->receiver['postcode'], + 'city' => $this->item_info->receiver['city'], + 'street' => $this->item_info->receiver['address_1'], + 'houseNumber' => $this->item_info->receiver['address_2'], + ); + + $client = ( new ClientFactory() )->get_client(); + $use_multiple = $this->item_info->body['morning_delivery_enabled'] + || $this->item_info->body['evening_delivery_enabled']; + + if ( $use_multiple ) { + $services = array( 'daytime' ); + if ( $this->item_info->body['evening_delivery_enabled'] ) { + $services[] = 'evening'; + } + // VERIFY: method name. SDK docs show checkout()->getMultipleServicesTimeframe(). + // If Client has multipleTimeframes()->getTimeframes() instead, update below. + $request = new MultipleServicesTimeframeRequest( + handoverDate: $this->item_info->body['order_date'], + numberOfDays: (int) $this->item_info->body['days'], + receiverAddress: $receiver_address, + services: $services, + shipmentType: 'parcel', + customerCode: $customer_code, + customerNumber: $customer_number, + ); + $response = $client->checkout()->getMultipleServicesTimeframe( $request ); + } else { + // VERIFY: method name. SDK docs show checkout()->getSingleServiceTimeframe(). + $request = new SingleServiceTimeframeRequest( + handoverDate: $this->item_info->body['order_date'], + deliveryDays: (int) $this->item_info->body['days'], + receiverAddress: $receiver_address, + service: 'daytime', + shipmentType: 'parcel', + customerCode: $customer_code, + customerNumber: $customer_number, + ); + $response = $client->checkout()->getSingleServiceTimeframe( $request ); + } + + return $this->map_timeframe_response( $response ); +} + +/** + * Map SDK TimeFrame response to the shape Frontend\Delivery_Day reads. + * + * IMPORTANT: The keys below are illustrative. Replace them with the actual + * keys found in Step 1. Do not ship this task until keys are confirmed. + * + * VERIFY: the collection accessor on the response object (timeFrames(), getTimeframes(), etc.) + * and the slot accessors (date(), from(), to(), options()) against installed SDK source. + * + * @param mixed $response SDK response object. + * @return array + */ +private function map_timeframe_response( $response ): array { + $delivery_options = array(); + + foreach ( $response->timeFrames() as $slot ) { + $delivery_options[] = array( + // Replace these keys with what Delivery_Day.php actually reads (Step 1 grep). + 'Date' => $slot->date(), + 'Timeframes' => array( + array( + 'TimeframeTimeRange' => array( + 'From' => $slot->from(), + 'To' => $slot->to(), + ), + 'Options' => array( $slot->service() ), + ), + ), + ); + } + + // Pickup options are empty here; Task 4 populates them. + return array( + 'DeliveryOptions' => $delivery_options, + 'PickupOptions' => array(), + ); +} +``` + +- [ ] **Step 3: Run PHPCS** + +```bash +composer check-php +``` + +Expected: no errors. + +- [ ] **Step 4: Verify old path unchanged** + +In staging (SDK off for 'timeframe'): +1. Load classic checkout with a NL address. +2. Confirm delivery-day slots appear as before. +3. Check WC log — "Checkout: using SDK TimeFrame path." must NOT appear. + +- [ ] **Step 5: Enable TimeFrame SDK path and test parity** + +Add temporarily to `postnl-for-woocommerce.php`: + +```php +add_action( + 'init', + static function () { + \PostNLWooCommerce\SDK\Router::enable( 'timeframe' ); + } +); +``` + +1. Classic checkout, NL address: verify delivery-day slots load. +2. Blocks checkout, NL address: verify delivery-day slots load. +3. Select a slot, place order; check `_postnl_order_metadata` saved correctly. +4. Compare slot list to old API output for the same address and settings. +5. Check WC log: "Checkout: using SDK TimeFrame path." must appear. + +Expected: parity with old `/shipment/v1/checkout` delivery-day output. + +- [ ] **Step 6: Remove temporary `Router::enable()` line** + +- [ ] **Step 7: Commit** + +```bash +git add src/Rest_API/Checkout/Client.php +git commit -m "feat: add SDK TimeFrame path to Checkout\Client (off by default)" +``` + +--- + +## Task 4 — Pickup Locations (SDK POC) + +**Status:** Ready | **Depends on:** Task 1 +**Files:** `src/Rest_API/Checkout/Client.php` (modify further) + +**Context:** Add the SDK Locations path alongside the TimeFrame path added in Task 3. Router key is `'locations'` (independent of `'timeframe'`). When only one flag is on, the other half supplements from the old client. When both are on, neither falls back to the old client. + +**Before starting:** +1. **Verify `Client::locations()` exists** in `src/Client/Client.php`. If the method is `addressLocations()` instead, update calls below. +2. **Verify `PickUpNearAddressRequest` namespace** in installed SDK. +3. **Read `src/Frontend/Dropoff_Points.php`** — run the grep in Step 1 to find exact response keys. + +- [ ] **Step 1: Find the response keys `Frontend\Dropoff_Points` reads** + +```bash +grep -n "PickupOptions\|PickupOption\|Company\|Address\|LocationCode\|Distance\|Name\|pickup" src/Frontend/Dropoff_Points.php | head -40 +``` + +Write down every array key. Use them in `map_locations_response()`. + +- [ ] **Step 2: Update `send_request()` to handle both Router flags** + +Read `src/Rest_API/Checkout/Client.php` (modified in Task 3). Replace the `send_request()` method with this version that handles both `'timeframe'` and `'locations'` flags simultaneously: + +```php +// Add to use statements: +use Postnl\Sdk\RequestData\V4\Locations\PickUpNearAddressRequest; +``` + +```php +/** + * Send API request — SDK paths when enabled, old client otherwise. + * Timeframe and Locations flags are independent. + * + * @throws \Exception On API or SDK error. + * @return array + */ +public function send_request() { + $use_timeframe = Router::use_sdk_for( 'timeframe' ); + $use_locations = Router::use_sdk_for( 'locations' ); + + if ( ! $use_timeframe && ! $use_locations ) { + return parent::send_request(); + } + + $result = array( + 'DeliveryOptions' => array(), + 'PickupOptions' => array(), + ); + + if ( $use_timeframe ) { + $this->logger->write( 'Checkout: using SDK TimeFrame path.' ); + try { + $tf = $this->send_sdk_timeframe_request(); + $result['DeliveryOptions'] = $tf['DeliveryOptions']; + } catch ( PostnlExceptionInterface $e ) { + $this->logger->write( 'TimeFrame SDK error: ' . $e->getMessage() ); + } + } + + if ( $use_locations ) { + $this->logger->write( 'Checkout: using SDK Locations path.' ); + try { + $result['PickupOptions'] = $this->send_sdk_locations_request(); + } catch ( PostnlExceptionInterface $e ) { + $this->logger->write( 'Locations SDK error: ' . $e->getMessage() ); + } + } + + // Supplement from old client for whichever flag is still off. + if ( ! $use_timeframe || ! $use_locations ) { + try { + $old = parent::send_request(); + if ( ! $use_timeframe ) { + $result['DeliveryOptions'] = $old['DeliveryOptions'] ?? array(); + } + if ( ! $use_locations ) { + $result['PickupOptions'] = $old['PickupOptions'] ?? array(); + } + } catch ( \Exception $e ) { + $this->logger->write( 'Checkout old-client fallback error: ' . $e->getMessage() ); + } + } + + return $result; +} +``` + +Add these two methods after `map_timeframe_response()`: + +```php +/** + * Call V4 Locations SDK service; return pickup locations shaped for Frontend\Dropoff_Points. + * + * @throws PostnlExceptionInterface On SDK error. + * @return array + */ +private function send_sdk_locations_request(): array { + $settings = Settings::get_instance(); + $customer_code = $settings->get_field_value( 'customer_code' ); + $customer_number = $settings->get_field_value( 'customer_num' ); + + $receiver_address = array( + 'countryIso' => $this->item_info->receiver['country'], + 'postalCode' => $this->item_info->receiver['postcode'], + 'city' => $this->item_info->receiver['city'], + 'street' => $this->item_info->receiver['address_1'], + 'houseNumber' => $this->item_info->receiver['address_2'], + ); + + // VERIFY: method name. SDK docs show locations()->getPickupLocationsByAddress(). + // If Client has addressLocations()->getNearestByAddress() instead, update below. + $request = new PickUpNearAddressRequest( + receiverAddress: $receiver_address, + numberOfLocations: (int) $this->item_info->body['locations'], + locationType: 'Retail', + pickUpDate: $this->item_info->body['order_date'], + customerCode: $customer_code, + customerNumber: $customer_number, + ); + $client = ( new ClientFactory() )->get_client(); + $response = $client->locations()->getPickupLocationsByAddress( $request ); + + return $this->map_locations_response( $response ); +} + +/** + * Map SDK PickUpLocationsResponse to the shape Frontend\Dropoff_Points reads. + * + * IMPORTANT: Replace all keys below with the actual keys from Step 1 grep. + * Do not ship until keys are confirmed against Dropoff_Points.php. + * + * VERIFY: the collection accessor on PickUpLocationsResponse (locationsCollection(), etc.) + * and location object accessors against installed SDK source. + * + * @param mixed $response SDK response object. + * @return array + */ +private function map_locations_response( $response ): array { + $locations = array(); + + foreach ( $response->locationsCollection() as $location ) { + $addr = $location->address(); + $locations[] = array( + // Replace with keys Dropoff_Points.php actually reads. + 'Address' => array( + 'Street' => $addr->street(), + 'HouseNr' => $addr->houseNumber(), + 'City' => $addr->city(), + 'Zipcode' => $addr->postalCode(), + 'Countrycode' => $addr->countryIso(), + ), + 'Name' => $location->name(), + 'Distance' => $location->distance(), + 'LocationCode' => $location->locationCode(), + ); + } + + return $locations; +} +``` + +- [ ] **Step 3: Run PHPCS** + +```bash +composer check-php +``` + +- [ ] **Step 4: Verify old path unchanged** + +With both `'timeframe'` and `'locations'` disabled, load classic and blocks checkout. Pickup points must appear as before. Log must show no SDK lines. + +- [ ] **Step 5: Enable Locations SDK path and test parity** + +Add temporarily: + +```php +add_action( + 'init', + static function () { + \PostNLWooCommerce\SDK\Router::enable( 'locations' ); + } +); +``` + +1. Classic checkout, NL address: pickup-point list loads. +2. Blocks checkout, NL address: pickup-point list loads. +3. Select a pickup point, place order; check `_postnl_order_metadata`. +4. Compare location list to old API output for same address. +5. WC log: "Checkout: using SDK Locations path." must appear. + +- [ ] **Step 6: Remove temporary enable line** + +- [ ] **Step 7: Commit** + +```bash +git add src/Rest_API/Checkout/Client.php +git commit -m "feat: add SDK Locations path to Checkout\Client (off by default)" +``` + +--- + +## Task 5 — Checkout Aggregation + +**Status:** Ready after Tasks 3 + 4 staging parity confirmed | **Depends on:** Tasks 3 + 4 +**Files:** `src/Rest_API/Checkout/Client.php` (modify) + +**Context:** Remove the last reference to `POST /shipment/v1/checkout`. `send_request()` already handles both SDK flags; this task removes the old-client fallback and makes the SDK paths mandatory. Merge only after both Tasks 3 and 4 pass staging parity independently. + +**Do not proceed unless all of these are confirmed:** +- [ ] Task 3 delivery-day parity: NL domestic, NL→BE, BE→NL on staging. +- [ ] Task 4 pickup-location parity: NL domestic on staging. +- [ ] Classic checkout end-to-end tested. +- [ ] Blocks checkout end-to-end tested. + +- [ ] **Step 1: Replace `send_request()` with the aggregation-only version** + +Read `src/Rest_API/Checkout/Client.php`. Replace `send_request()` with: + +```php +/** + * Send checkout request — SDK aggregation of TimeFrame + Locations. + * + * @throws \Exception On SDK error in both sub-calls. + * @return array + */ +public function send_request() { + $this->logger->write( 'Checkout: SDK aggregation (TimeFrame + Locations).' ); + + $result = array( + 'DeliveryOptions' => array(), + 'PickupOptions' => array(), + ); + + if ( $this->item_info->body['delivery_days_enabled'] ) { + try { + $tf = $this->send_sdk_timeframe_request(); + $result['DeliveryOptions'] = $tf['DeliveryOptions']; + } catch ( PostnlExceptionInterface $e ) { + $this->logger->write( 'TimeFrame SDK error: ' . $e->getMessage() ); + } + } + + if ( $this->item_info->body['pickup_points_enabled'] ) { + try { + $result['PickupOptions'] = $this->send_sdk_locations_request(); + } catch ( PostnlExceptionInterface $e ) { + $this->logger->write( 'Locations SDK error: ' . $e->getMessage() ); + } + } + + return $result; +} +``` + +Remove the now-unused `$endpoint` property (`/shipment/v1/checkout`), `compose_body_request()`, `get_cutoff_times()`, and `get_checkout_options()` — but only if they are not called from any other class. Confirm with: + +```bash +grep -rn "get_cutoff_times\|get_checkout_options\|compose_body_request" src/ +``` + +If they are referenced externally, keep them. If not, remove them. + +- [ ] **Step 2: Run PHPCS** + +```bash +composer check-php +``` + +- [ ] **Step 3: Confirm old endpoint is gone** + +```bash +grep -rn "shipment/v1/checkout" src/ +``` + +Expected: no results. + +- [ ] **Step 4: Full end-to-end checkout test** + +Classic checkout: +1. NL address → delivery-day and pickup tabs both appear. +2. Select delivery day → place order → verify `_postnl_order_metadata`. +3. Select pickup point → place order → verify `_postnl_order_metadata`. +4. Disable delivery-day setting → only pickup tab appears. +5. Disable pickup-point setting → only delivery-day tab appears. + +Blocks checkout: repeat steps 1–3. + +Regression checks: +- Postal-code validation still works (old client — check log). +- Fill In With PostNL still works (old client — check log). +- Delivery fee display and tax `taxRatio` back-calculation unchanged (see `agents.md` tax display architecture). + +- [ ] **Step 5: Commit** + +```bash +git add src/Rest_API/Checkout/Client.php +git commit -m "feat: replace old checkout endpoint with SDK TimeFrame+Locations aggregation" +``` + +--- + +## Task 6 — Shipping + Letterbox Labels ⛔ BLOCKED + +**Status:** Blocked — product/options → V4 field mapping table needed from PostNL/Joris +**Files (when unblocked):** `src/Rest_API/Shipping/Client.php`, `src/Rest_API/Letterbox/Client.php`, `src/Helper/Mapping.php` + +**Do not write any code until:** +- Written mapping table from PostNL/Joris confirming every `ProductCodeDelivery` + `ProductOptions` → V4 `shipmentType` + `services` combination. +- `services.adrLq` API field casing confirmed (`adrLq` vs `adrlq`). +- AITS-382 (`guaranteedBefore: '12:00'`) status confirmed. + +**Implementation pattern (for reference when unblocked):** + +In `Shipping\Client`, override `send_request()` and add a `build_label_confirm_request()` method. Key mappings: + +| Old field | V4 SDK field | Note | +|---|---|---| +| `$item_info->shipment['shipping_product']['code']` | `shipmentType` | Use new V4 mapping method in `Mapping.php` | +| `$item_info->shipment['product_options']` | `services` object | Map per confirmed table | +| `$item_info->receiver` | `receiver` (ShipmentParty) | Restructure addresses | +| `$item_info->shipper` | `sender` (ShipmentParty) | Restructure addresses | +| `$item_info->shipment['total_weight']` | `items[0].dimensions.weight` | Already in grams; SDK property `weightGr` | +| `$item_info->backend_data['num_labels'] > 1` | Multiple `items[]` entries | One `ShippingItem` per collo | +| `$item_info->shipment['barcodes'][n]` | `items[n].barcode` | One barcode per item | + +Add V4 mapping as a new method in `Mapping.php` alongside existing `products_data()`. Do not remove or modify `products_data()`. Old clients stay as fallback per product type until each type is staging-validated. + +--- + +## Task 7 — Return Labels (SDK POC) + +**Status:** Ready | **Depends on:** Task 1 +**Files:** `src/Rest_API/Return_Label/Client.php` (modify) + +**Context:** `Return_Label\Client` extends `Shipping\Client` and overrides `get_customer_address()` for return addresses. This task overrides `send_request()` to call `returnShipment()->generateReturn()` when `Router::use_sdk_for('return_labels')` is true. `Smart_Returns\Client` is not touched. + +**Before starting:** +1. Read `src/Rest_API/Return_Label/Item_Info.php` — confirm all return-specific fields available in `$this->item_info` (return address, return period, valuable return, LiB barcode). +2. Read `src/Order/Base.php` — find what array key it reads from the return label response. The `map_return_response()` method must return that shape. +3. Confirm `ReturnShipmentRequest` namespace in installed SDK: `Postnl\Sdk\RequestData\V4\ReturnShipment\ReturnShipmentRequest`. +4. Confirm `returnPeriod` valid values: SDK docs show only `IN_20_DAYS` (20) and `IN_35_DAYS` (35). Values 100, 200, 365 are **not confirmed** — use 20 or 35 only. + +- [ ] **Step 1: Override `send_request()` in `src/Rest_API/Return_Label/Client.php`** + +Read the file. Replace the entire file content with: + +```php + '02', + 'City' => $this->item_info->customer['return_address_city'], + 'CompanyName' => $this->item_info->customer['return_company'], + 'Countrycode' => $this->item_info->shipper['country'], + 'HouseNr' => $this->item_info->customer['return_address_2'], + 'Street' => $this->item_info->customer['return_address_1'], + 'Zipcode' => $this->item_info->customer['return_address_zip'], + ); + } + + /** + * Send API request — SDK return path when enabled, old client otherwise. + * + * @throws \Exception On API or SDK error. + * @return array + */ + public function send_request() { + if ( ! Router::use_sdk_for( 'return_labels' ) ) { + return parent::send_request(); + } + + $this->logger->write( 'ReturnLabel: using SDK path.' ); + + try { + return $this->send_sdk_return_request(); + } catch ( PostnlExceptionInterface $e ) { + $this->logger->write( 'ReturnLabel SDK error: ' . $e->getMessage() ); + throw SdkExceptionConverter::convert( $e ); + } + } + + /** + * Call the V4 return SDK service. + * + * VERIFY: confirm ReturnShipmentRequest constructor parameter names against installed SDK. + * VERIFY: check Order\Base.php for the exact response array keys it reads from this client. + * Adjust map_return_response() to match before shipping. + * + * @throws PostnlExceptionInterface On SDK error. + * @return array + */ + private function send_sdk_return_request(): array { + $receiver = array( + 'customerNumber' => $this->item_info->customer['customer_num'], + 'customerCode' => $this->item_info->customer['customer_code'], + 'address' => array( + 'countryIso' => $this->item_info->shipper['country'], + 'city' => $this->item_info->customer['return_address_city'], + 'companyName' => $this->item_info->customer['return_company'], + 'houseNumber' => $this->item_info->customer['return_address_2'], + 'street' => $this->item_info->customer['return_address_1'], + 'postalCode' => $this->item_info->customer['return_address_zip'], + ), + ); + + $sender = array( + 'contact' => array( + 'firstName' => $this->item_info->receiver['first_name'], + 'lastName' => $this->item_info->receiver['last_name'], + 'email' => $this->item_info->shipment['email'], + 'mobileNumber' => $this->item_info->shipment['phone'], + ), + 'address' => array( + 'countryIso' => $this->item_info->receiver['country'], + 'city' => $this->item_info->receiver['city'], + 'houseNumber' => $this->item_info->receiver['house_number'], + 'street' => $this->item_info->receiver['address_1'], + 'postalCode' => $this->item_info->receiver['postcode'], + ), + ); + + // SDK ReturnPeriod: only IN_20_DAYS (20) or IN_35_DAYS (35) are confirmed. + // Map from old product code → 20 or 35. Default 20 when unmapped. + $return_period = 20; + + $return_options = array( + 'labelType' => 'Label', + 'domestic' => array( + 'returnPeriod' => $return_period, + 'valuableReturn' => ! empty( $this->item_info->shipment['valuable_return'] ), + ), + ); + + if ( ! empty( $this->item_info->shipment['return_barcode'] ) ) { + $return_options['labelType'] = 'labelinthebox'; + $return_options['returnBarcode'] = $this->item_info->shipment['return_barcode']; + } + + $label_settings = array( + 'outputType' => $this->item_info->shipment['printer_type'] ?? 'PDF', + 'resolution' => 200, + 'pageOrientation' => 'portrait', + ); + + $request = new ReturnShipmentRequest( + receiver: $receiver, + sender: $sender, + returnOptions: $return_options, + labelSettings: $label_settings, + items: array( + array( 'barcode' => $this->item_info->shipment['main_barcode'] ), + ), + ); + + $client = ( new ClientFactory() )->get_client(); + $response = $client->returnShipment()->generateReturn( $request ); + + return $this->map_return_response( $response ); + } + + /** + * Map SDK GenerateReturnResponse to the shape Order\Base reads. + * + * IMPORTANT: Open src/Order/Base.php, find the call to this client's send_request(), + * and identify every array key it reads. Replace the keys below with the actual keys. + * + * VERIFY: the collection accessor on GenerateReturnResponse (shippingItemsCollection(), etc.) + * and item accessors against installed SDK source. + * + * @param mixed $response SDK response. + * @return array + */ + private function map_return_response( $response ): array { + $items = array(); + + // VERIFY: collection accessor name on GenerateReturnResponse. + foreach ( $response->shippingItemsCollection() as $item ) { + $items[] = array( + 'Barcode' => $item->barcode(), + 'Labels' => array( + array( + 'Content' => $item->label()->content(), + 'Labeltype' => $item->label()->labelType(), + ), + ), + ); + } + + // Replace 'MergedLabels' with the key Order\Base actually reads. + return array( 'MergedLabels' => $items ); + } +} +``` + +- [ ] **Step 2: Run PHPCS** + +```bash +composer check-php +``` + +- [ ] **Step 3: Verify old path unchanged** + +With `'return_labels'` disabled (default), generate a return label for a test order. Confirm it works. Check log — "ReturnLabel: using SDK path." must NOT appear. Confirm `Smart_Returns\Client` is untouched: + +```bash +git diff src/Rest_API/Smart_Returns/ +``` + +Expected: no changes. + +- [ ] **Step 4: Enable SDK return path and test parity** + +Add temporarily: + +```php +add_action( + 'init', + static function () { + \PostNLWooCommerce\SDK\Router::enable( 'return_labels' ); + } +); +``` + +1. Generate NL domestic return label on staging — verify PDF downloads; barcode scannable. +2. Generate LiB return label — verify return barcode present. +3. Generate NL-BE and BE-NL return labels. +4. Verify return label stored at `wp-content/uploads/postnl/`. +5. Compare output to old client output for same order data. + +- [ ] **Step 5: Remove temporary enable line** + +- [ ] **Step 6: Commit** + +```bash +git add src/Rest_API/Return_Label/Client.php +git commit -m "feat: add SDK return label path to Return_Label\Client (off by default)" +``` + +--- + +## Task 8 — Smart Returns ⛔ BLOCKED + +**Status:** Blocked — PostNL must confirm V4 `return/generate` replaces `POST /shipment/v2_2/label/` +**Files (when unblocked):** `src/Rest_API/Smart_Returns/Client.php` + +**Do not write any code until:** PostNL confirms in writing that `POST /shipment/delivery/v4/return/generate` fully replaces the old `POST /shipment/v2_2/label/` for Smart Returns — including barcode format, return period behavior, and customer notification side-effects. + +**Pattern when unblocked:** Override `send_request()` in `Smart_Returns\Client`. Check `Router::use_sdk_for('smart_returns')`. When on, call `$client->returnShipment()->generateReturn($request)`, extract the barcode string from the response, return it in the shape `Order\Single` reads. Do not change `WC_Email_Smart_Return`. + +--- + +## Task 9 — activatereturn ⛔ BLOCKED + +**Status:** Blocked — PostNL/Joris must decide: SDK extension, old client retention, or drop +**Files (Option A):** `src/SDK/Extension/ActivateReturnExtension.php` (new), `src/Rest_API/Shipment_and_Return/Client.php` +**Files (Option B):** `src/Rest_API/Shipment_and_Return/Client.php` — add retention comment only + +**Do not write any code until:** PostNL/Joris answers whether `POST /shipment/delivery/v4/return/activate` is behaviorally equivalent to old `POST /parcels/v1/shipment/activatereturn`. + +**Option A pattern (when unblocked):** + +Create `src/SDK/Extension/ActivateReturnExtension.php` implementing `ConfigurableAction`. Reference the installed SDK's `PostalCodeCheckExtension.php` (`src/Service/Checkout/V1/Extension/PostalCodeCheckExtension.php`) as the concrete implementation example — it shows the full interface shape. + +```php +// Skeleton only — implement from PostalCodeCheckExtension reference: +namespace PostNLWooCommerce\SDK\Extension; + +use Postnl\Sdk\Service\Extension\ConfigurableAction; + +if ( ! defined( 'ABSPATH' ) ) { exit; } + +class ActivateReturnExtension implements ConfigurableAction { + // implement execute($context, $payload) per the interface + // endpoint: POST /shipment/delivery/v4/return/activate + // fields: barcode, sender.customerNumber, source, label +} +``` + +Then in `Shipment_and_Return\Client::send_request()`: +```php +if ( Router::use_sdk_for( 'activatereturn' ) ) { + $client = ( new ClientFactory() )->get_client(); + $ext = new ActivateReturnExtension(); + $client->extensions()->register( $ext ); + $response = $client->extensions()->getAs( ActivateReturnExtension::class )->execute( $payload ); + // map response; set _postnl_return_activated +} +``` + +Note: SDK docs show `$context->cache` in `ServiceContext`. Verify it exists in the installed `ServiceContext.php` before using it. + +**Option B:** Add a code comment to `Shipment_and_Return\Client.php` documenting the retention decision. No functional change. + +--- + +## Full Staging QA Checklist + +Run after Tasks 1–5 and 7 are all merged. Run again when Task 6 labels are considered stable. + +- [ ] Barcode: generate for NL domestic order; verify format (e.g., `3SDEVC...`) +- [ ] Classic checkout: NL address → delivery-day slots appear (daytime + evening) +- [ ] Blocks checkout: NL address → delivery-day slots appear +- [ ] Classic checkout: NL address → pickup-point list appears +- [ ] Blocks checkout: NL address → pickup-point list appears +- [ ] Classic + blocks: select delivery day → place order → verify `_postnl_order_metadata` +- [ ] Classic + blocks: select pickup point → place order → verify `_postnl_order_metadata` +- [ ] Return label — NL domestic: PDF downloads; barcode scannable +- [ ] Return label — LiB: return barcode present +- [ ] Return label — NL-BE and BE-NL: label format correct +- [ ] Error: invalid API key → admin error shown; no raw SDK exception in browser +- [ ] Sandbox toggle: requests route to `api-sandbox.postnl.nl` (check WC log) +- [ ] WC log: no API key, label binary, or customer PII visible in any log entry +- [ ] Postal-code check: still works (old client — confirm no regression) +- [ ] Fill In With PostNL: still works (old client — confirm no regression) +- [ ] Delivery fees and tax display unchanged (verify `taxRatio` logic in `Container.php`) + +--- + +## Self-Review + +**Spec coverage:** + +| Requirement from migration plan | Task | Covered | +|---|---|---| +| Composer SDK dependency + Private Packagist | 0 | ✓ | +| PHP 8.2 guard / admin notice | 0 | ✓ | +| ClientFactory (API key, sandbox) | 1 | ✓ | +| Router (per-flow, off by default) | 1 | ✓ | +| SdkExceptionConverter | 1 | ✓ | +| Logger binary redaction | 1 | ✓ | +| Barcode SDK POC | 2 | ✓ | +| TimeFrame SDK POC | 3 | ✓ | +| Locations SDK POC | 4 | ✓ | +| Checkout aggregation | 5 | ✓ | +| Shipping + Letterbox labels | 6 | ✓ (blocked) | +| Return labels SDK POC | 7 | ✓ | +| Smart Returns | 8 | ✓ (blocked) | +| activatereturn | 9 | ✓ (blocked, both options) | +| Old client preserved as fallback | 2–5, 7 | ✓ | +| Classic + blocks checkout tested in every checkout task | 3, 4, 5 | ✓ | +| `taxRatio` / fee regression check | 5 | ✓ | +| Staging QA checklist | — | ✓ | + +**Type consistency check:** +- `Router::use_sdk_for(string $flow)` — called consistently in Tasks 2, 3, 4, 7. +- `new ClientFactory()` — instantiated identically in Tasks 2, 3, 4, 7. +- `SdkExceptionConverter::convert($e)` — used identically in Tasks 2, 3, 7. +- `$client->barcode()`, `->checkout()`, `->locations()`, `->returnShipment()` — all annotated with VERIFY comments pointing to the same pre-flight check. +- `send_sdk_timeframe_request()` referenced in both Task 3 (step 2) and Task 5 (step 1) — consistent private method name. ✓ +- `send_sdk_locations_request()` referenced in both Task 4 (step 2) and Task 5 (step 1) — consistent. ✓