From e970580868615d9f3d23e5eb13fe8a97ff6db8e8 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH 01/17] Added plugins --- composer.json | 15 +- composer.lock | 1058 ++++++++++++++++++++++++++++++++- src/Client.php | 25 +- src/Factory/ClientFactory.php | 59 ++ 4 files changed, 1138 insertions(+), 19 deletions(-) create mode 100644 src/Factory/ClientFactory.php diff --git a/composer.json b/composer.json index 41781e3..0a706b0 100644 --- a/composer.json +++ b/composer.json @@ -31,17 +31,26 @@ ], "require": { "php": ">=8.2.0", - "psr/http-message": "^2.0", - "psr/http-factory": "^1.1" + "php-http/discovery": "^1.20", + "psr/http-factory-implementation": "*", + "psr/http-client-implementation": "*", + "php-http/client-common": "^2.7", + "php-http/message": "^1.16" }, "require-dev": { "guzzlehttp/psr7": "^2.8", "phpstan/phpstan": "^2.1", "rector/rector": "^2.3", "phpunit/phpunit": "^11.5", - "phpunit/php-code-coverage": "^11.0" + "phpunit/php-code-coverage": "^11.0", + "symfony/http-client": "^8.0" }, "scripts": { "test": "vendor/bin/phpunit" + }, + "config": { + "allow-plugins": { + "php-http/discovery": false + } } } diff --git a/composer.lock b/composer.lock index 10c97f0..dc0f5b0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,450 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ee44db8b6bed25754895e3cb1384d838", + "content-hash": "b2eca4ba96f5f1bc17587fa6fe92347a", "packages": [ + { + "name": "clue/stream-filter", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.7.3", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "dcc6de29c90dd74faab55f71b79d89409c4bf0c1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/dcc6de29c90dd74faab55f71b79d89409c4bf0c1", + "reference": "dcc6de29c90dd74faab55f71b79d89409c4bf0c1", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0 || ^8.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.3" + }, + "time": "2025-11-29T19:12:34+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.1" + }, + "time": "2024-09-23T11:39:58+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.2" + }, + "time": "2024-10-02T11:34:13+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, { "name": "psr/http-factory", "version": "1.1.0", @@ -86,8 +528,206 @@ }, "autoload": { "psr-4": { - "Psr\\Http\\Message\\": "src/" - } + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v8.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:55:31+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -95,24 +735,48 @@ ], "authors": [ { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "Common interface for HTTP messages", - "homepage": "https://github.com/php-fig/http-message", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "http", - "http-message", - "psr", - "psr-7", - "request", - "response" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, - "time": "2023-04-04T09:54:51+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" } ], "packages-dev": [ @@ -978,6 +1642,109 @@ ], "time": "2026-02-05T07:59:30+00:00" }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -2120,6 +2887,267 @@ ], "time": "2024-10-20T05:08:20+00:00" }, + { + "name": "symfony/http-client", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/f9fdd372473e66469c6d32a4ed12efcffdea38c4", + "reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "psr/log": "^1|^2|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<3", + "php-http/discovery": "<1.15" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^5.3.2", + "amphp/http-tunnel": "^2.0", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/cache": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T16:18:07+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-29T11:18:49+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, { "name": "theseer/tokenizer", "version": "1.3.1", diff --git a/src/Client.php b/src/Client.php index eb27d61..308eee3 100644 --- a/src/Client.php +++ b/src/Client.php @@ -4,7 +4,30 @@ namespace AlexisPPLIN\SendcloudV3; +use AlexisPPLIN\SendcloudV3\Factory\ClientFactory; +use Http\Client\HttpClient; +use Http\Discovery\HttpClientDiscovery; + class Client { - + protected const API_BASE_URL = 'https://panel.sendcloud.sc/api/v3/'; + + protected HttpClient $client; + + public function __construct( + protected string $publicKey, + protected string $secretKey, + protected ?string $partnerId = null, + string $apiBaseUrl = self::API_BASE_URL + ) { + $client = HttpClientDiscovery::find(); + $this->client = ClientFactory::create( + $apiBaseUrl, + $publicKey, + $secretKey, + $partnerId, + [], + $client + ); + } } diff --git a/src/Factory/ClientFactory.php b/src/Factory/ClientFactory.php new file mode 100644 index 0000000..73f4683 --- /dev/null +++ b/src/Factory/ClientFactory.php @@ -0,0 +1,59 @@ + $plugins + */ + public static function create( + string $base_uri, + string $user, + string $pass, + ?string $partnerId = null, + array $plugins = [], + ?HttpClient $client = null + ): PluginClient { + if (!$client) { + $client = HttpClientDiscovery::find(); + } + $plugins[] = new ErrorPlugin(); + + // Basic auth + + $plugins[] = new AuthenticationPlugin( + new BasicAuth($user, $pass) + ); + + // Base Uri + + $uri_factory = Psr17FactoryDiscovery::findUriFactory()->createUri($base_uri); + $plugins[] = new BaseUriPlugin( + $uri_factory, + ['replace' => true], + ); + + // Headers + + $headers = []; + if (isset($partnerId)) { + $headers['Sendcloud-Partner-Id'] = $partnerId; + } + + $plugins[] = new HeaderSetPlugin($headers); + + return new PluginClient($client, $plugins); + } +} \ No newline at end of file From 2ccf93891317d5f22a3a21d88b4df12ffcc1d987 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH 02/17] WIP : Created models for orders endpoints --- composer.json | 10 +- composer.lock | 302 ++++++++---------- src/Client.php | 15 +- src/Endpoints/Orders.php | 23 ++ src/Exceptions/DateParsingException.php | 10 + src/Exceptions/ModelFromDataException.php | 10 + src/Factory/ClientFactory.php | 4 +- src/Models/DangerousGoods.php | 108 +++++++ src/Models/Delivery/DeliveryDates.php | 48 +++ src/Models/Measurement/Measurement.php | 46 +++ .../Measurement/MeasurementDimension.php | 47 +++ src/Models/Measurement/MeasurementVolume.php | 41 +++ src/Models/Measurement/MeasurementWeight.php | 42 +++ src/Models/ModelInterface.php | 14 + src/Models/Order/Order.php | 27 ++ src/Models/Order/OrderDetails.php | 40 +++ src/Models/Order/OrderDetailsIntegration.php | 14 + src/Models/Order/OrderDetailsStatus.php | 15 + src/Models/Order/OrderItems.php | 66 ++++ src/Models/Price.php | 30 ++ src/Utils/DateUtils.php | 24 ++ tests/ClientTest.php | 56 +++- tests/ClientTestInstance.php | 13 + tests/Endpoints/OrdersTest.php | 176 ++++++++++ 24 files changed, 1007 insertions(+), 174 deletions(-) create mode 100644 src/Endpoints/Orders.php create mode 100644 src/Exceptions/DateParsingException.php create mode 100644 src/Exceptions/ModelFromDataException.php create mode 100644 src/Models/DangerousGoods.php create mode 100644 src/Models/Delivery/DeliveryDates.php create mode 100644 src/Models/Measurement/Measurement.php create mode 100644 src/Models/Measurement/MeasurementDimension.php create mode 100644 src/Models/Measurement/MeasurementVolume.php create mode 100644 src/Models/Measurement/MeasurementWeight.php create mode 100644 src/Models/ModelInterface.php create mode 100644 src/Models/Order/Order.php create mode 100644 src/Models/Order/OrderDetails.php create mode 100644 src/Models/Order/OrderDetailsIntegration.php create mode 100644 src/Models/Order/OrderDetailsStatus.php create mode 100644 src/Models/Order/OrderItems.php create mode 100644 src/Models/Price.php create mode 100644 src/Utils/DateUtils.php create mode 100644 tests/ClientTestInstance.php create mode 100644 tests/Endpoints/OrdersTest.php diff --git a/composer.json b/composer.json index 0a706b0..3094a03 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ }, "autoload-dev": { "psr-4": { - "Test\\AlexisPPLIN\\SendcloudV3\\": "test/" + "Test\\AlexisPPLIN\\SendcloudV3\\": "tests/" } }, "authors": [ @@ -32,18 +32,18 @@ "require": { "php": ">=8.2.0", "php-http/discovery": "^1.20", - "psr/http-factory-implementation": "*", "psr/http-client-implementation": "*", "php-http/client-common": "^2.7", - "php-http/message": "^1.16" + "php-http/httplug": "^2.4" }, "require-dev": { - "guzzlehttp/psr7": "^2.8", "phpstan/phpstan": "^2.1", "rector/rector": "^2.3", "phpunit/phpunit": "^11.5", "phpunit/php-code-coverage": "^11.0", - "symfony/http-client": "^8.0" + "php-http/mock-client": "^1.6", + "symfony/http-client": "^8.0", + "nyholm/psr7": "^1.8" }, "scripts": { "test": "vendor/bin/phpunit" diff --git a/composer.lock b/composer.lock index dc0f5b0..6b39ae2 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b2eca4ba96f5f1bc17587fa6fe92347a", + "content-hash": "3cae9a7cc870c53fbc641d208f5c2141", "packages": [ { "name": "clue/stream-filter", @@ -780,122 +780,6 @@ } ], "packages-dev": [ - { - "name": "guzzlehttp/psr7", - "version": "2.8.0", - "source": { - "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", - "shasum": "" - }, - "require": { - "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" - }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" - }, - "require-dev": { - "bamarni/composer-bin-plugin": "^1.8.2", - "http-interop/http-factory-tests": "0.9.0", - "phpunit/phpunit": "^8.5.44 || ^9.6.25" - }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" - }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false - } - }, - "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" - }, - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" - }, - { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" - }, - { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" - }, - { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" - } - ], - "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" - ], - "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" - }, - "funding": [ - { - "url": "https://github.com/GrahamCampbell", - "type": "github" - }, - { - "url": "https://github.com/Nyholm", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", - "type": "tidelift" - } - ], - "time": "2025-08-23T21:21:41+00:00" - }, { "name": "myclabs/deep-copy", "version": "1.13.4", @@ -1014,6 +898,84 @@ }, "time": "2025-12-06T11:56:16+00:00" }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, { "name": "phar-io/manifest", "version": "2.0.4", @@ -1132,6 +1094,68 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "php-http/mock-client", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/mock-client.git", + "reference": "81f558234421f7da58ed015604a03808996017d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/mock-client/zipball/81f558234421f7da58ed015604a03808996017d0", + "reference": "81f558234421f7da58ed015604a03808996017d0", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/client-common": "^2.0", + "php-http/discovery": "^1.16", + "php-http/httplug": "^2.0", + "psr/http-client": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/polyfill-php80": "^1.17" + }, + "provide": { + "php-http/async-client-implementation": "1.0", + "php-http/client-implementation": "1.0", + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Mock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David de Boer", + "email": "david@ddeboer.nl" + } + ], + "description": "Mock HTTP client", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http", + "mock", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/mock-client/issues", + "source": "https://github.com/php-http/mock-client/tree/1.6.1" + }, + "time": "2024-10-31T10:30:18+00:00" + }, { "name": "phpstan/phpstan", "version": "2.1.38", @@ -1745,50 +1769,6 @@ }, "time": "2024-09-11T13:17:53+00:00" }, - { - "name": "ralouphie/getallheaders", - "version": "3.0.3", - "source": { - "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", - "shasum": "" - }, - "require": { - "php": ">=5.6" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, { "name": "rector/rector", "version": "2.3.5", diff --git a/src/Client.php b/src/Client.php index 308eee3..086f30b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -5,23 +5,25 @@ namespace AlexisPPLIN\SendcloudV3; use AlexisPPLIN\SendcloudV3\Factory\ClientFactory; +use Http\Client\Common\HttpMethodsClient; use Http\Client\HttpClient; use Http\Discovery\HttpClientDiscovery; +use Http\Discovery\Psr17FactoryDiscovery; class Client { protected const API_BASE_URL = 'https://panel.sendcloud.sc/api/v3/'; - protected HttpClient $client; + protected HttpMethodsClient $client; public function __construct( protected string $publicKey, protected string $secretKey, protected ?string $partnerId = null, - string $apiBaseUrl = self::API_BASE_URL + string $apiBaseUrl = self::API_BASE_URL, + ?HttpClient $client = null ) { - $client = HttpClientDiscovery::find(); - $this->client = ClientFactory::create( + $client = ClientFactory::create( $apiBaseUrl, $publicKey, $secretKey, @@ -29,5 +31,10 @@ public function __construct( [], $client ); + + $this->client = new HttpMethodsClient( + $client, + Psr17FactoryDiscovery::findRequestFactory() + ); } } diff --git a/src/Endpoints/Orders.php b/src/Endpoints/Orders.php new file mode 100644 index 0000000..e05a216 --- /dev/null +++ b/src/Endpoints/Orders.php @@ -0,0 +1,23 @@ +client->get('/orders/' . $id); + $body = $response->getBody()->getContents(); + + return Order::fromData(json_decode($body, true)['data']); + } +} \ No newline at end of file diff --git a/src/Exceptions/DateParsingException.php b/src/Exceptions/DateParsingException.php new file mode 100644 index 0000000..ba65e7c --- /dev/null +++ b/src/Exceptions/DateParsingException.php @@ -0,0 +1,10 @@ + $regulation_set Regulation set governing the dangerous goods + * @param ?string $packaging_type_quantity Quantity of packaging type + * @param ?string $packaging_type Type of packaging used + * @param ?string $packaging_instruction_code Packaging instruction code + * @param ?string $id_number UN identification number + * @param ?string $class_division_number Hazard class and division number + * @param ?string $quantity Quantity of dangerous goods + * @param value-of $unit_of_measurement Unit of measurement for dangerous goods quantity + * @param ?string $proper_shipping_name Proper shipping name as defined by regulations + * @param value-of $commodity_regulated_level_code Commodity regulated level code + * @param value-of $transportation_mode Mode of transportation + * @param ?string $emergency_contact_name Name of emergency contact person + * @param ?string $emergency_contact_phone Phone number of emergency contact + * @param value-of $adr_packing_group_classification ADR packing group classification + * @param ?string $local_proper_shipping_name Local proper shipping name + * @param ?string $transport_category Transport category for ADR regulations + * @param ?string $tunnel_restriction_code Tunnel restriction code + * @param value-of $weight_type Type of weight measurement + */ + public function __construct( + public readonly string $regulation_set, + public readonly string $unit_of_measurement, + public readonly string $commodity_regulated_level_code, + public readonly string $transportation_mode, + public readonly string $weight_type, + public readonly string $adr_packing_group_classification, + public readonly ?string $value = null, + public readonly ?string $packaging_type_quantity = null, + public readonly ?string $packaging_type = null, + public readonly ?string $packaging_instruction_code = null, + public readonly ?string $id_number = null, + public readonly ?string $class_division_number = null, + public readonly ?string $quantity = null, + public readonly ?string $proper_shipping_name = null, + public readonly ?string $emergency_contact_name = null, + public readonly ?string $emergency_contact_phone = null, + public readonly ?string $local_proper_shipping_name = null, + public readonly ?string $transport_category = null, + public readonly ?string $tunnel_restriction_code = null, + ) { + + } + + public static function fromData(array $data) : self + { + return new self( + regulation_set: (string) $data['regulation_set'], + unit_of_measurement: (string) $data['unit_of_measurement'], + commodity_regulated_level_code: (string) $data['commodity_regulated_level_code'], + transportation_mode: (string) $data['transportation_mode'], + weight_type: (string) $data['weight_type'], + adr_packing_group_classification: (string) $data['adr_packing_group_classification'], + value: isset($data['value']) ? (string) $data['value'] : null, + packaging_type_quantity: isset($data['packaging_type_quantity']) ? (string) $data['packaging_type_quantity'] : null, + packaging_type: isset($data['packaging_type']) ? (string) $data['packaging_type'] : null, + packaging_instruction_code: isset($data['packaging_instruction_code']) ? (string) $data['packaging_instruction_code'] : null, + id_number: isset($data['id_number']) ? (string) $data['id_number'] : null, + class_division_number: isset($data['class_division_number']) ? (string) $data['class_division_number'] : null, + // TODO : finish this + ); + } +} \ No newline at end of file diff --git a/src/Models/Delivery/DeliveryDates.php b/src/Models/Delivery/DeliveryDates.php new file mode 100644 index 0000000..d1971f0 --- /dev/null +++ b/src/Models/Delivery/DeliveryDates.php @@ -0,0 +1,48 @@ + $unit + */ + public function __construct( + public readonly float $lenght, + public readonly float $width, + public readonly float $height, + public readonly string $unit + ) { + + } + + public static function fromData(array $data) : self + { + return new self( + lenght: (float) $data['lenght'], + width: (float) $data['width'], + height: (float) $data['height'], + unit: (string) $data['unit'] + ); + } +} \ No newline at end of file diff --git a/src/Models/Measurement/MeasurementVolume.php b/src/Models/Measurement/MeasurementVolume.php new file mode 100644 index 0000000..526ca93 --- /dev/null +++ b/src/Models/Measurement/MeasurementVolume.php @@ -0,0 +1,41 @@ + $unit + */ + public function __construct( + public readonly float $value, + public readonly string $unit + ) { + + } + + public static function fromData(array $data) : self + { + return new self( + value: (float) $data['value'], + unit: (string) $data['unit'] + ); + } +} \ No newline at end of file diff --git a/src/Models/Measurement/MeasurementWeight.php b/src/Models/Measurement/MeasurementWeight.php new file mode 100644 index 0000000..bb165a0 --- /dev/null +++ b/src/Models/Measurement/MeasurementWeight.php @@ -0,0 +1,42 @@ + $unit + */ + public function __construct( + public readonly float $value, + public readonly string $unit + ) { + + } + + public static function fromData(array $data) : self + { + return new self( + value: (float) $data['value'], + unit: (string) $data['unit'] + ); + } +} \ No newline at end of file diff --git a/src/Models/ModelInterface.php b/src/Models/ModelInterface.php new file mode 100644 index 0000000..f028552 --- /dev/null +++ b/src/Models/ModelInterface.php @@ -0,0 +1,14 @@ + $data + * @throws ModelFromDataException + */ + public static function fromData(array $data) : self; +} \ No newline at end of file diff --git a/src/Models/Order/Order.php b/src/Models/Order/Order.php new file mode 100644 index 0000000..46aad0e --- /dev/null +++ b/src/Models/Order/Order.php @@ -0,0 +1,27 @@ + $order_items The list of items that an order contains + * @param $order_updated_at The date and time that the order was last updated in the respective shop system + * @param $notes Internal notes or comments placed by consumer on the order + * @param array $tags Tags assigned to the order + */ + public function __construct( + public readonly OrderDetailsIntegration $integration, + public readonly OrderDetailsStatus $status, + public readonly DateTimeImmutable $order_created_at, + public readonly array $order_items, + public readonly DateTimeImmutable $order_updated_at, + public readonly string $notes, + public readonly array $tags + ) { + + } + + public static function fromJson(array $data) : self + { + + } +} \ No newline at end of file diff --git a/src/Models/Order/OrderDetailsIntegration.php b/src/Models/Order/OrderDetailsIntegration.php new file mode 100644 index 0000000..d6d00d7 --- /dev/null +++ b/src/Models/Order/OrderDetailsIntegration.php @@ -0,0 +1,14 @@ + $properties Any custom user-defined properties of order item or product + * @param $unit_price The price of a single item in the shop’s currency before discounts have been applied. + * - [Sendcloud platform mapping] This value is shown directly in the Unit value field in the Sendcloud platform + * @param $measurement This object provides essential information for accurate packing, shipping, and inventory management + * @param $ean European standardised number for an article, EAN-13 + * @param $delivery_dates Defined delivery dates + * @param $mid_code MID code is short for Manufacturer's Identification code and must be shown on the commercial invoice. It's used as an alternative to the full name and address of a manufacturer, shipper or exporter and is always required for U.S. formal customs entries. + * @param $material_content A description of materials of the order content. + * @param $intended_use Intended use of the order contents. The intended use may be personal or commercial. + * @param $dangerous_goods Hazardous materials information for items. + */ + public function __construct( + public readonly string $name, + public readonly int $quantity, + public readonly Price $total_price, + public readonly ?string $item_id = null, + public readonly ?string $product_id = null, + public readonly ?string $variant_id = null, + public readonly ?string $image_url = null, + public readonly ?string $description = null, + public readonly ?string $sku = null, + public readonly ?string $hs_code = null, + public readonly ?string $country_of_origin = null, + public readonly ?array $properties = null, + public readonly ?Price $unit_price = null, + public readonly ?Measurement $measurement = null, + public readonly ?string $ean = null, + public readonly ?DeliveryDates $delivery_dates = null, + public readonly ?string $mid_code = null, + public readonly ?string $material_content = null, + public readonly ?string $intended_use = null, + public readonly ?DangerousGoods $dangerous_goods = null, + + ) { + + } +} \ No newline at end of file diff --git a/src/Models/Price.php b/src/Models/Price.php new file mode 100644 index 0000000..c56f66d --- /dev/null +++ b/src/Models/Price.php @@ -0,0 +1,30 @@ + $currency + */ + public function __construct( + public readonly float $value, + public readonly string $currency, + ) { + + } + + public static function fromData(array $data) : self + { + return new self( + value: (float) $data['value'], + currency: (string) $data['currency'] + ); + } +} \ No newline at end of file diff --git a/src/Utils/DateUtils.php b/src/Utils/DateUtils.php new file mode 100644 index 0000000..079deb3 --- /dev/null +++ b/src/Utils/DateUtils.php @@ -0,0 +1,24 @@ +assertTrue(true); + // -- Arrange + + $publicKey = '123456'; + $secretKey = 'abcdef'; + $partnerId = '1'; + $apiBaseUrl = 'https://api.example.com/v3'; + $mockClient = new MockClient(); + + // -- Act + + $client = new ClientTestInstance( + $publicKey, + $secretKey, + $partnerId, + $apiBaseUrl, + $mockClient + ); + + // -- Assert + + $this->assertInstanceOf(HttpMethodsClient::class, $client->getClient()); + } + + public function testConstructWithoutClient(): void + { + // -- Arrange + + $publicKey = '123456'; + $secretKey = 'abcdef'; + $partnerId = '1'; + $apiBaseUrl = 'https://api.example.com/v3'; + $mockClient = null; + + // -- Act + + $client = new ClientTestInstance( + $publicKey, + $secretKey, + $partnerId, + $apiBaseUrl, + $mockClient + ); + + // -- Assert + + $this->assertInstanceOf(HttpMethodsClient::class, $client->getClient()); } } diff --git a/tests/ClientTestInstance.php b/tests/ClientTestInstance.php new file mode 100644 index 0000000..ef02669 --- /dev/null +++ b/tests/ClientTestInstance.php @@ -0,0 +1,13 @@ +client; + } +}; \ No newline at end of file diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php new file mode 100644 index 0000000..1a01eba --- /dev/null +++ b/tests/Endpoints/OrdersTest.php @@ -0,0 +1,176 @@ +addResponse(new Response(body: <<endpoint = new Orders( + $publicKey, + $secretKey, + $partnerId, + $apiBaseUrl, + $client + ); + } + + public function testGetOrder() : void + { + // -- Arrange + + $order_id = 1; + $expected = new Order( + order_id: '555413', + order_number: 'OXSDFGHTD-12' + ); + + // -- Act + + $order = $this->endpoint->getOrder($order_id); + + // -- Assert + + $this->assertInstanceOf(Order::class, $order); + $this->assertEquals($expected, $order); + } +} \ No newline at end of file From 2ece525dde72d16fe99d3e319b4d42227bf44612 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH 03/17] Added OrderDetails --- src/Endpoints/Orders.php | 2 +- src/Models/DangerousGoods.php | 32 ++++++---- src/Models/Measurement/MeasurementWeight.php | 10 ++-- src/Models/Order/Order.php | 13 ++-- src/Models/Order/OrderDetails.php | 20 ++++++- src/Models/Order/OrderDetailsIntegration.php | 11 +++- src/Models/Order/OrderDetailsStatus.php | 12 +++- src/Models/Order/OrderItems.php | 30 +++++++++- src/Utils/DateUtils.php | 2 +- tests/Endpoints/OrdersTest.php | 62 +++++++++++++++++++- 10 files changed, 157 insertions(+), 37 deletions(-) diff --git a/src/Endpoints/Orders.php b/src/Endpoints/Orders.php index e05a216..a81f53b 100644 --- a/src/Endpoints/Orders.php +++ b/src/Endpoints/Orders.php @@ -3,7 +3,7 @@ namespace AlexisPPLIN\SendcloudV3\Endpoints; use AlexisPPLIN\SendcloudV3\Client; -use AlexisPPLIN\SendcloudV3\Models\Order; +use AlexisPPLIN\SendcloudV3\Models\Order\Order; class Orders extends Client { diff --git a/src/Models/DangerousGoods.php b/src/Models/DangerousGoods.php index 9b0c24c..aacb3f1 100644 --- a/src/Models/DangerousGoods.php +++ b/src/Models/DangerousGoods.php @@ -90,19 +90,25 @@ public function __construct( public static function fromData(array $data) : self { return new self( - regulation_set: (string) $data['regulation_set'], - unit_of_measurement: (string) $data['unit_of_measurement'], - commodity_regulated_level_code: (string) $data['commodity_regulated_level_code'], - transportation_mode: (string) $data['transportation_mode'], - weight_type: (string) $data['weight_type'], - adr_packing_group_classification: (string) $data['adr_packing_group_classification'], - value: isset($data['value']) ? (string) $data['value'] : null, - packaging_type_quantity: isset($data['packaging_type_quantity']) ? (string) $data['packaging_type_quantity'] : null, - packaging_type: isset($data['packaging_type']) ? (string) $data['packaging_type'] : null, - packaging_instruction_code: isset($data['packaging_instruction_code']) ? (string) $data['packaging_instruction_code'] : null, - id_number: isset($data['id_number']) ? (string) $data['id_number'] : null, - class_division_number: isset($data['class_division_number']) ? (string) $data['class_division_number'] : null, - // TODO : finish this + regulation_set: (string) $data['regulation_set'], + unit_of_measurement: (string) $data['unit_of_measurement'], + commodity_regulated_level_code: (string) $data['commodity_regulated_level_code'], + transportation_mode: (string) $data['transportation_mode'], + weight_type: (string) $data['weight_type'], + adr_packing_group_classification: (string) $data['adr_packing_group_classification'], + value: isset($data['value']) ? (string) $data['value'] : null, + packaging_type_quantity: isset($data['packaging_type_quantity']) ? (string) $data['packaging_type_quantity'] : null, + packaging_type: isset($data['packaging_type']) ? (string) $data['packaging_type'] : null, + packaging_instruction_code: isset($data['packaging_instruction_code']) ? (string) $data['packaging_instruction_code'] : null, + id_number: isset($data['id_number']) ? (string) $data['id_number'] : null, + class_division_number: isset($data['class_division_number']) ? (string) $data['class_division_number'] : null, + quantity: isset($data['quantity']) ? (string) $data['quantity'] : null, + proper_shipping_name: isset($data['proper_shipping_name']) ? (string) $data['proper_shipping_name'] : null, + emergency_contact_name: isset($data['emergency_contact_name']) ? (string) $data['emergency_contact_name'] : null, + emergency_contact_phone: isset($data['emergency_contact_phone']) ? (string) $data['emergency_contact_phone'] : null, + local_proper_shipping_name: isset($data['local_proper_shipping_name']) ? (string) $data['local_proper_shipping_name'] : null, + transport_category: isset($data['transport_category']) ? (string) $data['transport_category'] : null, + tunnel_restriction_code: isset($data['tunnel_restriction_code']) ? (string) $data['tunnel_restriction_code'] : null ); } } \ No newline at end of file diff --git a/src/Models/Measurement/MeasurementWeight.php b/src/Models/Measurement/MeasurementWeight.php index bb165a0..25dd16c 100644 --- a/src/Models/Measurement/MeasurementWeight.php +++ b/src/Models/Measurement/MeasurementWeight.php @@ -13,12 +13,10 @@ class MeasurementWeight implements ModelInterface { public const UNITS = [ - 'cm', - 'mm', - 'm', - 'yd', - 'ft', - 'in' + 'kg', + 'g', + 'lbs', + 'oz' ]; /** diff --git a/src/Models/Order/Order.php b/src/Models/Order/Order.php index 46aad0e..5a4ba70 100644 --- a/src/Models/Order/Order.php +++ b/src/Models/Order/Order.php @@ -7,12 +7,14 @@ class Order implements ModelInterface { /** - * @param string $order_id External order ID assigned by shop system - * @param string $order_number Unique order number generated manually or by shop system + * @param $order_id External order ID assigned by shop system + * @param $order_number Unique order number generated manually or by shop system + * @param $order_details Node for general order information */ public function __construct( public readonly string $order_id, - public readonly string $order_number + public readonly string $order_number, + public readonly OrderDetails $order_details, ) { } @@ -20,8 +22,9 @@ public function __construct( public static function fromData(array $data) : self { return new self( - (string) $data['order_id'], - (string) $data['order_number'], + order_id: (string) $data['order_id'], + order_number: (string) $data['order_number'], + order_details: OrderDetails::fromData($data['order_details']) ); } } \ No newline at end of file diff --git a/src/Models/Order/OrderDetails.php b/src/Models/Order/OrderDetails.php index ab01ab3..d0bccdc 100644 --- a/src/Models/Order/OrderDetails.php +++ b/src/Models/Order/OrderDetails.php @@ -3,6 +3,7 @@ namespace AlexisPPLIN\SendcloudV3\Models\Order; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; +use AlexisPPLIN\SendcloudV3\Utils\DateUtils; use DateTimeImmutable; /** @@ -28,13 +29,26 @@ public function __construct( public readonly array $order_items, public readonly DateTimeImmutable $order_updated_at, public readonly string $notes, - public readonly array $tags + public readonly ?array $tags = null ) { } - public static function fromJson(array $data) : self + public static function fromData(array $data) : self { - + $order_items = []; + foreach ($data['order_items'] as $item) { + $order_items[] = OrderItems::fromData($item); + } + + return new self( + integration: OrderDetailsIntegration::fromData($data['integration']), + status: OrderDetailsStatus::fromData($data['status']), + order_created_at: DateUtils::iso8601ToDateTime($data['order_created_at']), + order_items: $order_items, + order_updated_at: DateUtils::iso8601ToDateTime($data['order_updated_at']), + notes: (string) $data['notes'], + tags: isset($data['tags']) ? $data['tags'] : null + ); } } \ No newline at end of file diff --git a/src/Models/Order/OrderDetailsIntegration.php b/src/Models/Order/OrderDetailsIntegration.php index d6d00d7..c1b31a0 100644 --- a/src/Models/Order/OrderDetailsIntegration.php +++ b/src/Models/Order/OrderDetailsIntegration.php @@ -2,13 +2,20 @@ namespace AlexisPPLIN\SendcloudV3\Models\Order; -use DateTimeImmutable; +use AlexisPPLIN\SendcloudV3\Models\ModelInterface; -class OrderDetailsIntegration +class OrderDetailsIntegration implements ModelInterface { public function __construct( public readonly int $id ) { } + + public static function fromData(array $data) : self + { + return new self( + id: (int) $data['id'] + ); + } } \ No newline at end of file diff --git a/src/Models/Order/OrderDetailsStatus.php b/src/Models/Order/OrderDetailsStatus.php index 74aeada..c356a2d 100644 --- a/src/Models/Order/OrderDetailsStatus.php +++ b/src/Models/Order/OrderDetailsStatus.php @@ -2,9 +2,9 @@ namespace AlexisPPLIN\SendcloudV3\Models\Order; -use DateTimeImmutable; +use AlexisPPLIN\SendcloudV3\Models\ModelInterface; -class OrderDetailsStatus +class OrderDetailsStatus implements ModelInterface { public function __construct( public readonly string $code, @@ -12,4 +12,12 @@ public function __construct( ) { } + + public static function fromData(array $data) : self + { + return new self( + code: (string) $data['code'], + message: isset($data['message']) ? (string) $data['message'] : null + ); + } } \ No newline at end of file diff --git a/src/Models/Order/OrderItems.php b/src/Models/Order/OrderItems.php index ef6faf6..2ed3872 100644 --- a/src/Models/Order/OrderItems.php +++ b/src/Models/Order/OrderItems.php @@ -5,13 +5,13 @@ use AlexisPPLIN\SendcloudV3\Models\DangerousGoods; use AlexisPPLIN\SendcloudV3\Models\Delivery\DeliveryDates; use AlexisPPLIN\SendcloudV3\Models\Measurement\Measurement; +use AlexisPPLIN\SendcloudV3\Models\ModelInterface; use AlexisPPLIN\SendcloudV3\Models\Price; -use DateTimeImmutable; /** * @see https://sendcloud.dev/api/v3/orders/retrieve-an-order#response-data-order-details-order-items */ -class OrderItems +class OrderItems implements ModelInterface { /** * @param $name The name of ordered product @@ -63,4 +63,30 @@ public function __construct( ) { } + + public static function fromData(array $data) : self + { + return new self( + name: (string) $data['name'], + quantity: (int) $data['quantity'], + total_price: isset($data['total_price']) ? Price::fromData($data['total_price']) : null, + item_id: isset($data['item_id']) ? (string) $data['item_id'] : null, + product_id: isset($data['product_id']) ? (string) $data['product_id'] : null, + variant_id: isset($data['variant_id']) ? (string) $data['variant_id'] : null, + image_url: isset($data['image_url']) ? (string) $data['image_url'] : null, + description: isset($data['description']) ? (string) $data['description'] : null, + sku: isset($data['sku']) ? (string) $data['sku'] : null, + hs_code: isset($data['hs_code']) ? (string) $data['hs_code'] : null, + country_of_origin: isset($data['country_of_origin']) ? (string) $data['country_of_origin'] : null, + properties: isset($data['properties']) ? $data['properties'] : null, + unit_price: isset($data['unit_price']) ? Price::fromData($data['unit_price']) : null, + measurement: isset($data['measurement']) ? Measurement::fromData($data['measurement']) : null, + ean: isset($data['ean']) ? (string) $data['ean'] : null, + delivery_dates: isset($data['delivery_dates']) ? DeliveryDates::fromData($data['delivery_dates']) : null, + mid_code: isset($data['mid_code']) ? (string) $data['mid_code'] : null, + material_content: isset($data['material_content']) ? (string) $data['material_content'] : null, + intended_use: isset($data['intended_use']) ? (string) $data['intended_use'] : null, + dangerous_goods: isset($data['dangerous_goods']) ? DangerousGoods::fromData($data['dangerous_goods']) : null + ); + } } \ No newline at end of file diff --git a/src/Utils/DateUtils.php b/src/Utils/DateUtils.php index 079deb3..bccbd3f 100644 --- a/src/Utils/DateUtils.php +++ b/src/Utils/DateUtils.php @@ -14,7 +14,7 @@ class DateUtils */ public static function iso8601ToDateTime(string $iso8601) : DateTimeImmutable { - $date = DateTimeImmutable::createFromFormat(DateTimeInterface::ISO8601, $iso8601); + $date = DateTimeImmutable::createFromFormat("Y-m-d\TH:i:s.up", $iso8601); if (!$date) { throw new DateParsingException("Error when parsing ISO 8601 date ({$iso8601})"); } diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php index 1a01eba..d98cb65 100644 --- a/tests/Endpoints/OrdersTest.php +++ b/tests/Endpoints/OrdersTest.php @@ -6,7 +6,17 @@ use AlexisPPLIN\SendcloudV3\Endpoints\Orders; use AlexisPPLIN\SendcloudV3\Factory\ClientFactory; -use AlexisPPLIN\SendcloudV3\Models\Order; +use AlexisPPLIN\SendcloudV3\Models\Delivery\DeliveryDates; +use AlexisPPLIN\SendcloudV3\Models\Measurement\Measurement; +use AlexisPPLIN\SendcloudV3\Models\Measurement\MeasurementWeight; +use AlexisPPLIN\SendcloudV3\Models\Order\Order; +use AlexisPPLIN\SendcloudV3\Models\Order\OrderDetails; +use AlexisPPLIN\SendcloudV3\Models\Order\OrderDetailsIntegration; +use AlexisPPLIN\SendcloudV3\Models\Order\OrderDetailsStatus; +use AlexisPPLIN\SendcloudV3\Models\Order\OrderItems; +use AlexisPPLIN\SendcloudV3\Models\Price; +use AlexisPPLIN\SendcloudV3\Utils\DateUtils; +use DateTimeImmutable; use Nyholm\Psr7\Response; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; @@ -18,6 +28,15 @@ #[CoversClass(Orders::class)] #[CoversClass(Order::class)] +#[CoversClass(DeliveryDates::class)] +#[CoversClass(Measurement::class)] +#[CoversClass(MeasurementWeight::class)] +#[CoversClass(OrderDetails::class)] +#[CoversClass(OrderDetailsIntegration::class)] +#[CoversClass(OrderItems::class)] +#[CoversClass(Price::class)] +#[CoversClass(DateUtils::class)] +#[CoversClass(OrderDetailsStatus::class)] #[UsesClass(Client::class)] #[UsesClass(ClientFactory::class)] class OrdersTest extends TestCase @@ -161,7 +180,46 @@ public function testGetOrder() : void $order_id = 1; $expected = new Order( order_id: '555413', - order_number: 'OXSDFGHTD-12' + order_number: 'OXSDFGHTD-12', + order_details: new OrderDetails( + integration: new OrderDetailsIntegration( + id: 739283 + ), + status: new OrderDetailsStatus( + code: 'fulfilled', + message: 'Fulfilled' + ), + order_created_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555Z'), + order_updated_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555Z'), + order_items: [ + new OrderItems( + name: 'Cylinder candle', + quantity: 1, + measurement: new Measurement( + weight: new MeasurementWeight( + value: 1, + unit: 'kg' + ) + ), + unit_price: new Price( + value: 3.5, + currency: 'EUR' + ), + total_price: new Price( + value: 3.5, + currency: 'EUR' + ), + delivery_dates: new DeliveryDates( + handover_at: DateUtils::iso8601ToDateTime('2022-02-27T10:00:00.555309+00:00'), + deliver_at: DateUtils::iso8601ToDateTime('2022-03-02T11:50:00.555309+00:00'), + ), + mid_code: 'US1234567', + material_content: '100% Cotton', + intended_use: 'Personal use', + ) + ], + notes: '' + ) ); // -- Act From cd0bf6be7af7dc28eda012e9b534fe20601d6311 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH 04/17] Added remaining models --- src/Models/Address.php | 60 ++++++++++++ src/Models/Customer/CustomerDetails.php | 35 +++++++ src/Models/Order/CustomsDetails.php | 55 +++++++++++ src/Models/Order/Order.php | 40 +++++++- src/Models/Order/OrderDetails.php | 5 +- src/Models/Order/ShipWith.php | 33 +++++++ src/Models/Order/ShippingDetails.php | 43 ++++++++ src/Models/Order/ShippingOptionProperties.php | 32 ++++++ src/Models/PaymentDetails.php | 57 +++++++++++ src/Models/ServicePoint/ServicePoint.php | 30 ++++++ .../OrderDetailsStatus.php => Status.php} | 4 +- src/Models/Tax/TaxNumber.php | 25 +++++ src/Models/Tax/TaxNumbers.php | 51 ++++++++++ tests/Endpoints/OrdersTest.php | 97 ++++++++++++++++--- 14 files changed, 548 insertions(+), 19 deletions(-) create mode 100644 src/Models/Address.php create mode 100644 src/Models/Customer/CustomerDetails.php create mode 100644 src/Models/Order/CustomsDetails.php create mode 100644 src/Models/Order/ShipWith.php create mode 100644 src/Models/Order/ShippingDetails.php create mode 100644 src/Models/Order/ShippingOptionProperties.php create mode 100644 src/Models/PaymentDetails.php create mode 100644 src/Models/ServicePoint/ServicePoint.php rename src/Models/{Order/OrderDetailsStatus.php => Status.php} (81%) create mode 100644 src/Models/Tax/TaxNumber.php create mode 100644 src/Models/Tax/TaxNumbers.php diff --git a/src/Models/Address.php b/src/Models/Address.php new file mode 100644 index 0000000..c4eb227 --- /dev/null +++ b/src/Models/Address.php @@ -0,0 +1,60 @@ + $shipment_type Indicates the purpose or reason behind exporting the items. This classification helps customs authorities determine the applicable regulations, taxes, and duties. + * @param value-of $export_type Export type documentation serves to categorize international shipments into three primary classifications: + * - Private exports, intended for personal use + * - Commercial B2C exports, directed towards individual consumers + * - Commercial B2B exports, involving business-to-business transactions These distinctions facilitate adherence to regulatory requirements and ensure the orderly movement of goods across international boundaries. + * @param $tax_numbers Identification numbers and codes related to sender, receiver and importer of record provider. + */ + public function __construct( + public readonly ?string $commercial_invoice_number = null, + public readonly ?string $shipment_type = null, + public readonly ?string $export_type = null, + public readonly ?TaxNumbers $tax_numbers = null + ) { + + } + + public static function fromData(array $data) : self + { + return new self( + commercial_invoice_number: isset($data['commercial_invoice_number']) ? (string) $data['commercial_invoice_number'] : null, + shipment_type: isset($data['shipment_type']) ? (string) $data['shipment_type'] : null, + export_type: isset($data['export_type']) ? (string) $data['export_type'] : null, + tax_numbers: isset($data['tax_numbers']) ? TaxNumbers::fromData($data['tax_numbers']) : null + ); + } +} \ No newline at end of file diff --git a/src/Models/Order/Order.php b/src/Models/Order/Order.php index 5a4ba70..f0e4970 100644 --- a/src/Models/Order/Order.php +++ b/src/Models/Order/Order.php @@ -2,7 +2,13 @@ namespace AlexisPPLIN\SendcloudV3\Models\Order; +use AlexisPPLIN\SendcloudV3\Models\Address; +use AlexisPPLIN\SendcloudV3\Models\Customer\CustomerDetails; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; +use AlexisPPLIN\SendcloudV3\Models\PaymentDetails; +use AlexisPPLIN\SendcloudV3\Models\ServicePoint\ServicePoint; +use AlexisPPLIN\SendcloudV3\Utils\DateUtils; +use DateTimeImmutable; class Order implements ModelInterface { @@ -10,11 +16,29 @@ class Order implements ModelInterface * @param $order_id External order ID assigned by shop system * @param $order_number Unique order number generated manually or by shop system * @param $order_details Node for general order information + * @param $payment_details Node for everything about payments and money + * @param $id Autogenerated Sendcloud's internal ID + * @param $created_at The date when an order was created at Sendcloud in ISO 8601 + * @param $modified_at The date when an order was last modified at Sendcloud in ISO 8601 + * @param $customs_details Customs information required for international shipments. + * @param $customer_details Node for an information about customer. + * @param $shipping_details Shipping information + * @param $service_point_details Node for service point information. The service point information can be retrieved from the Service points API {@see https://sendcloud.dev/api/v2/service-points} */ public function __construct( public readonly string $order_id, public readonly string $order_number, public readonly OrderDetails $order_details, + public readonly PaymentDetails $payment_details, + public readonly ?string $id = null, + public readonly ?DateTimeImmutable $created_at = null, + public readonly ?DateTimeImmutable $modified_at = null, + public readonly ?CustomsDetails $customs_details = null, + public readonly ?CustomerDetails $customer_details = null, + public readonly ?Address $billing_address = null, + public readonly ?Address $shipping_address = null, + public readonly ?ShippingDetails $shipping_details = null, + public readonly ?ServicePoint $service_point_details = null ) { } @@ -22,9 +46,19 @@ public function __construct( public static function fromData(array $data) : self { return new self( - order_id: (string) $data['order_id'], - order_number: (string) $data['order_number'], - order_details: OrderDetails::fromData($data['order_details']) + order_id: (string) $data['order_id'], + order_number: (string) $data['order_number'], + order_details: OrderDetails::fromData($data['order_details']), + payment_details: PaymentDetails::fromData($data['payment_details']), + id: isset($data['id']) ? (string) $data['id'] : null, + created_at: isset($data['created_at']) ? DateUtils::iso8601ToDateTime($data['created_at']) : null, + modified_at: isset($data['modified_at']) ? DateUtils::iso8601ToDateTime($data['modified_at']) : null, + customs_details: isset($data['customs_details']) ? CustomsDetails::fromData($data['customs_details']) : null, + customer_details: isset($data['customer_details']) ? CustomerDetails::fromData($data['customer_details']) : null, + billing_address: isset($data['billing_address']) ? Address::fromData($data['billing_address']) : null, + shipping_address: isset($data['shipping_address']) ? Address::fromData($data['shipping_address']) : null, + shipping_details: isset($data['shipping_details']) ? ShippingDetails::fromData($data['shipping_details']) : null, + service_point_details: isset($data['service_point_details']) ? ServicePoint::fromData($data['service_point_details']) : null, ); } } \ No newline at end of file diff --git a/src/Models/Order/OrderDetails.php b/src/Models/Order/OrderDetails.php index d0bccdc..20aa9c5 100644 --- a/src/Models/Order/OrderDetails.php +++ b/src/Models/Order/OrderDetails.php @@ -3,6 +3,7 @@ namespace AlexisPPLIN\SendcloudV3\Models\Order; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; +use AlexisPPLIN\SendcloudV3\Models\Status; use AlexisPPLIN\SendcloudV3\Utils\DateUtils; use DateTimeImmutable; @@ -24,7 +25,7 @@ class OrderDetails implements ModelInterface */ public function __construct( public readonly OrderDetailsIntegration $integration, - public readonly OrderDetailsStatus $status, + public readonly Status $status, public readonly DateTimeImmutable $order_created_at, public readonly array $order_items, public readonly DateTimeImmutable $order_updated_at, @@ -43,7 +44,7 @@ public static function fromData(array $data) : self return new self( integration: OrderDetailsIntegration::fromData($data['integration']), - status: OrderDetailsStatus::fromData($data['status']), + status: Status::fromData($data['status']), order_created_at: DateUtils::iso8601ToDateTime($data['order_created_at']), order_items: $order_items, order_updated_at: DateUtils::iso8601ToDateTime($data['order_updated_at']), diff --git a/src/Models/Order/ShipWith.php b/src/Models/Order/ShipWith.php new file mode 100644 index 0000000..766c863 --- /dev/null +++ b/src/Models/Order/ShipWith.php @@ -0,0 +1,33 @@ + $sender + * @param array $receiver + * @param array $importer_of_record + */ + public function __construct( + public readonly array $sender, + public readonly array $receiver, + public readonly array $importer_of_record, + ) { + + } + + public static function fromData(array $data) : self + { + $sender = []; + $receiver = []; + $importer_of_record = []; + + foreach ($data['sender'] as $s) { + $sender[] = TaxNumber::fromData($s); + } + + foreach ($data['receiver'] as $r) { + $receiver[] = TaxNumber::fromData($r); + } + + foreach ($data['importer_of_record'] as $i) { + $importer_of_record[] = TaxNumber::fromData($i); + } + + return new self( + sender: $sender, + receiver: $receiver, + importer_of_record: $importer_of_record + ); + } +} \ No newline at end of file diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php index d98cb65..2bd350d 100644 --- a/tests/Endpoints/OrdersTest.php +++ b/tests/Endpoints/OrdersTest.php @@ -6,21 +6,23 @@ use AlexisPPLIN\SendcloudV3\Endpoints\Orders; use AlexisPPLIN\SendcloudV3\Factory\ClientFactory; +use AlexisPPLIN\SendcloudV3\Models\Address; use AlexisPPLIN\SendcloudV3\Models\Delivery\DeliveryDates; use AlexisPPLIN\SendcloudV3\Models\Measurement\Measurement; use AlexisPPLIN\SendcloudV3\Models\Measurement\MeasurementWeight; +use AlexisPPLIN\SendcloudV3\Models\Order\CustomsDetails; use AlexisPPLIN\SendcloudV3\Models\Order\Order; use AlexisPPLIN\SendcloudV3\Models\Order\OrderDetails; use AlexisPPLIN\SendcloudV3\Models\Order\OrderDetailsIntegration; -use AlexisPPLIN\SendcloudV3\Models\Order\OrderDetailsStatus; use AlexisPPLIN\SendcloudV3\Models\Order\OrderItems; +use AlexisPPLIN\SendcloudV3\Models\PaymentDetails; use AlexisPPLIN\SendcloudV3\Models\Price; +use AlexisPPLIN\SendcloudV3\Models\Status; +use AlexisPPLIN\SendcloudV3\Models\Tax\TaxNumber; +use AlexisPPLIN\SendcloudV3\Models\Tax\TaxNumbers; use AlexisPPLIN\SendcloudV3\Utils\DateUtils; -use DateTimeImmutable; use Nyholm\Psr7\Response; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\CoversMethod; -use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\Attributes\UsesClass; use PHPUnit\Framework\TestCase; @@ -36,7 +38,12 @@ #[CoversClass(OrderItems::class)] #[CoversClass(Price::class)] #[CoversClass(DateUtils::class)] -#[CoversClass(OrderDetailsStatus::class)] +#[CoversClass(Status::class)] +#[CoversClass(PaymentDetails::class)] +#[CoversClass(CustomsDetails::class)] +#[CoversClass(TaxNumber::class)] +#[CoversClass(TaxNumbers::class)] +#[CoversClass(Address::class)] #[UsesClass(Client::class)] #[UsesClass(ClientFactory::class)] class OrdersTest extends TestCase @@ -153,12 +160,12 @@ public function setUp(): void } }, "shipping_address": { - "name": "John Doe", - "address_line_1": "Stadhuisplein", - "house_number": "15", - "postal_code": "5341TW", - "city": "Oss", - "country_code": "NL" + "name": "John Doe", + "address_line_1": "Stadhuisplein", + "house_number": "15", + "postal_code": "5341TW", + "city": "Oss", + "country_code": "NL" } } } @@ -179,13 +186,16 @@ public function testGetOrder() : void $order_id = 1; $expected = new Order( + id: '669', order_id: '555413', + created_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555Z'), + modified_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555Z'), order_number: 'OXSDFGHTD-12', order_details: new OrderDetails( integration: new OrderDetailsIntegration( id: 739283 ), - status: new OrderDetailsStatus( + status: new Status( code: 'fulfilled', message: 'Fulfilled' ), @@ -219,6 +229,69 @@ public function testGetOrder() : void ) ], notes: '' + ), + payment_details: new PaymentDetails( + is_cash_on_delivery: true, + total_price: new Price( + value: 7, + currency: 'EUR' + ), + status: new Status( + code: 'paid', + message: 'Order has been paid' + ), + discount_granted: new Price( + value: 3.99, + currency: 'EUR' + ), + insurance_costs: new Price( + value: 9.99, + currency: 'EUR' + ), + freight_costs: new Price( + value: 5.99, + currency: 'EUR' + ), + other_costs: new Price( + value: 2.99, + currency: 'EUR' + ), + ), + customs_details: new CustomsDetails( + commercial_invoice_number: '0124-03102022', + shipment_type: 'commercial_goods', + export_type: 'commercial_b2c', + tax_numbers: new TaxNumbers( + sender: [ + new TaxNumber( + name: 'VAT', + country_code: 'NL', + value: 'NL987654321B02' + ) + ], + receiver: [ + new TaxNumber( + name: 'VAT', + country_code: 'DE', + value: 'DE123456789B03' + ) + ], + importer_of_record: [ + new TaxNumber( + name: 'VAT', + country_code: 'NL', + value: 'NL975318642B01' + ) + ] + ) + ), + shipping_address: new Address( + name: 'John Doe', + address_line_1: 'Stadhuisplein', + house_number: '15', + postal_code: '5341TW', + city: 'Oss', + country_code: 'NL' ) ); From 69639ed8e3311feeb4576af53fa89ce46314b06d Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH 05/17] Added jsonSerialize --- src/Endpoints/Orders.php | 149 ++++++++++++++++ src/Models/Address.php | 23 +++ src/Models/Customer/CustomerDetails.php | 18 ++ src/Models/DangerousGoods.php | 30 ++++ src/Models/Delivery/DeliveryDates.php | 16 ++ src/Models/Measurement/Measurement.php | 19 ++ .../Measurement/MeasurementDimension.php | 12 ++ src/Models/Measurement/MeasurementVolume.php | 10 ++ src/Models/Measurement/MeasurementWeight.php | 10 ++ src/Models/ModelInterface.php | 8 +- src/Models/Order/CustomsDetails.php | 13 ++ src/Models/Order/Order.php | 30 ++++ src/Models/Order/OrderDetails.php | 17 ++ src/Models/Order/OrderDetailsIntegration.php | 9 + src/Models/Order/OrderItems.php | 30 ++++ src/Models/Order/ShipWith.php | 10 ++ src/Models/Order/ShippingDetails.php | 13 ++ src/Models/Order/ShippingOptionProperties.php | 11 ++ src/Models/PaymentDetails.php | 22 +++ src/Models/Price.php | 10 ++ src/Models/ServicePoint/ServicePoint.php | 16 ++ src/Models/Status.php | 13 ++ src/Models/Tax/TaxNumber.php | 12 ++ src/Models/Tax/TaxNumbers.php | 11 ++ src/Utils/DateUtils.php | 13 +- src/Utils/JsonUtils.php | 16 ++ tests/Endpoints/OrdersTest.php | 163 ++++-------------- tests/Endpoints/orders.json | 110 ++++++++++++ 28 files changed, 686 insertions(+), 128 deletions(-) create mode 100644 src/Utils/JsonUtils.php create mode 100644 tests/Endpoints/orders.json diff --git a/src/Endpoints/Orders.php b/src/Endpoints/Orders.php index a81f53b..8eb4cc8 100644 --- a/src/Endpoints/Orders.php +++ b/src/Endpoints/Orders.php @@ -4,6 +4,7 @@ use AlexisPPLIN\SendcloudV3\Client; use AlexisPPLIN\SendcloudV3\Models\Order\Order; +use Http\Discovery\Psr17FactoryDiscovery; class Orders extends Client { @@ -20,4 +21,152 @@ public function getOrder(int $id): Order return Order::fromData(json_decode($body, true)['data']); } + + /** + * Retrieve a list of orders + * Get a list of orders filtered by integration, order number, order ID, order status, creation date, and update date. You can also optionally sort the results and pass a cursor value. + * + * @see https://sendcloud.dev/api/v3/orders/retrieve-a-list-of-orders + * + * @param ?array $integration Filter orders by one or more integration IDs. + * @param $order_number Filter orders by a specific order number. The filtering is case insensitive. + * @param $order_id Filter orders by a specific order id. The filtering is case insensitive. + * @param $status Filter orders based on their status=. The filtering is case insensitive. + * @param $order_created_at Find orders that were created on a specific date. + * @param $order_created_at_min Find orders that were created at or after a specific date. + * @param $order_created_at_max Find orders that were created at or after a specific date. + * @param $order_updated_at Find orders that were created at or after a specific date. + * @param $order_updated_at_min Find orders that were created at or after a specific date. + * @param $order_updated_at_max Find orders that were created at or after a specific date. + * @param $sort Sort the orders in the response by using one or more of the following values: + * - integration: Sort by the integration ID of the retrieved orders; to sort in descending order, use -integration + * - order_number: Sort by the order number of the retrieved orders; to sort in descending order, use -order_number. + * - order_created_at: Sort by the date of an order creation of the retrieved orders; to sort in descending order, use -order_created_at + * - order_updated_at: Sort by the date of an order update of the retrieved orders; to sort in descending order, use -order_updated_at + * - pk: Sort by the ID (autogenerated internal ID) of the retrieved orders; to sort in descending order, use -pk + * + * Additional information about this query: + * - Any valid combination of the above values is supported, e.g. sort=integration,-order_created_at. + * - In case of conflicting sort values, e.g. /?sort=integration,-integration, the conflicting value will be ignored. + * - If the sort query parameter is not set, the sorting of the retrieved orders will default to -pk. + * - If an unsupported sort value is provided, the results will be sorted by the default (-pk). + * Example: + * "-order_number" + * @param $page_size The maximum number of results to be returned per page + * @param $cursor The cursor query string is used as the pivot value to filter results. If no value is provided, the first page of results will be returned. To get this value, you must encode the offset, reverse and position into a base64 string. + * There are 3 possible parameters to encode: + * - o: Offset + * - r: Reverse + * - p: Position + * For example, r=1&p=300 encoded as a base64 string would be cj0xJnA9MzAw. The query string would then be cursor=cj0xJnA9MzAw. + * + * Example: + * "cj0xJnA9MzAw" + * @return array + */ + public function getOrders( + ?array $integration = null, + ?string $order_number = null, + ?string $order_id = null, + ?string $status = null, + ?string $order_created_at = null, + ?string $order_created_at_min = null, + ?string $order_created_at_max = null, + ?string $order_updated_at = null, + ?string $order_updated_at_min = null, + ?string $order_updated_at_max = null, + ?string $sort = null, + ?int $page_size = null, + ?string $cursor = null, + ) : array { + // Build query parameters + + $query = []; + + if (isset($integration) && !empty($integration)) { + $query['integration'] = $integration; + } + + if (isset($order_number)) { + $query['order_number'] = $order_number; + } + + if (isset($order_id)) { + $query['order_id'] = $order_id; + } + + if (isset($status)) { + $query['status'] = $status; + } + + if (isset($order_created_at)) { + $query['order_created_at'] = $order_created_at; + } + + if (isset($order_created_at_min)) { + $query['order_created_at_min'] = $order_created_at_min; + } + + if (isset($order_created_at_max)) { + $query['order_created_at_max'] = $order_created_at_max; + } + + if (isset($order_updated_at)) { + $query['order_updated_at'] = $order_updated_at; + } + + if (isset($order_updated_at_min)) { + $query['order_updated_at_min'] = $order_updated_at_min; + } + + if (isset($order_updated_at_max)) { + $query['order_updated_at_max'] = $order_updated_at_max; + } + + if (isset($sort)) { + $query['sort'] = $sort; + } + + if (isset($page_size)) { + $query['page_size'] = $page_size; + } + + if (isset($cursor)) { + $query['cursor'] = $cursor; + } + + $uri = '/orders?' . http_build_query($query); + + // Send request + + $response = $this->client->get($uri); + + // Parse response + + $body = $response->getBody()->getContents(); + $response = json_decode($body, true); + + $orders = []; + foreach ($response['data'] as $order) { + $orders[] = Order::fromData($order); + } + + return $orders; + } + + /** + * Update an order + * Partially update some fields of an order. + * + * @see https://sendcloud.dev/api/v3/orders/update-an-order + */ + public function updateOrder( + int $id, + Order $order + ): void { + $body = json_encode($order); + $response = $this->client->patch('/orders/' . $id, [], $body); + + $body = $response->getBody()->getContents(); + } } \ No newline at end of file diff --git a/src/Models/Address.php b/src/Models/Address.php index c4eb227..907eec3 100644 --- a/src/Models/Address.php +++ b/src/Models/Address.php @@ -2,6 +2,8 @@ namespace AlexisPPLIN\SendcloudV3\Models; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; + /** * Sendcloud Address object * @@ -57,4 +59,25 @@ public static function fromData(array $data) : self phone_number: isset($data['phone_number']) ? (string) $data['phone_number'] : null, ); } + + public function jsonSerialize() : array + { + $json = [ + 'name' => $this->name, + 'address_line_1' => $this->address_line_1, + 'postal_code' => $this->postal_code, + 'city' => $this->city, + 'country_code' => $this->country_code, + ]; + + JsonUtils::addIfNotNull($json, 'company_name', $this->company_name); + JsonUtils::addIfNotNull($json, 'house_number', $this->house_number); + JsonUtils::addIfNotNull($json, 'address_line_2', $this->address_line_2); + JsonUtils::addIfNotNull($json, 'po_box', $this->po_box); + JsonUtils::addIfNotNull($json, 'state_province_code', $this->state_province_code); + JsonUtils::addIfNotNull($json, 'email', $this->email); + JsonUtils::addIfNotNull($json, 'phone_number', $this->phone_number); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Customer/CustomerDetails.php b/src/Models/Customer/CustomerDetails.php index c57b4bc..d0be1cf 100644 --- a/src/Models/Customer/CustomerDetails.php +++ b/src/Models/Customer/CustomerDetails.php @@ -3,6 +3,7 @@ namespace AlexisPPLIN\SendcloudV3\Models\Customer; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; +use JsonSerializable; /** * Node for an information about customer @@ -32,4 +33,21 @@ public static function fromData(array $data) : self email: isset($data['email']) ? (string) $data['email'] : null, ); } + + public function jsonSerialize() : array + { + $json = [ + 'name' => $this->name + ]; + + if (isset($this->phone_number)) { + $json['name'] = $this->phone_number; + } + + if (isset($this->email)) { + $json['email'] = $this->email; + } + + return $json; + } } \ No newline at end of file diff --git a/src/Models/DangerousGoods.php b/src/Models/DangerousGoods.php index aacb3f1..7ed4343 100644 --- a/src/Models/DangerousGoods.php +++ b/src/Models/DangerousGoods.php @@ -2,6 +2,8 @@ namespace AlexisPPLIN\SendcloudV3\Models; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; + /** * Hazardous materials information for items. * @@ -111,4 +113,32 @@ class_division_number: isset($data['class_division_number']) tunnel_restriction_code: isset($data['tunnel_restriction_code']) ? (string) $data['tunnel_restriction_code'] : null ); } + + public function jsonSerialize() : array + { + $json = [ + 'regulation_set' => $this->regulation_set, + 'unit_of_measurement' => $this->unit_of_measurement, + 'commodity_regulated_level_code' => $this->commodity_regulated_level_code, + 'transportation_mode' => $this->transportation_mode, + 'weight_type' => $this->weight_type, + 'adr_packing_group_classification' => $this->adr_packing_group_classification + ]; + + JsonUtils::addIfNotNull($json, 'value', $this->value); + JsonUtils::addIfNotNull($json, 'packaging_type_quantity', $this->packaging_type_quantity); + JsonUtils::addIfNotNull($json, 'packaging_type', $this->packaging_type); + JsonUtils::addIfNotNull($json, 'packaging_instruction_code', $this->packaging_instruction_code); + JsonUtils::addIfNotNull($json, 'id_number', $this->id_number); + JsonUtils::addIfNotNull($json, 'class_division_number', $this->class_division_number); + JsonUtils::addIfNotNull($json, 'quantity', $this->quantity); + JsonUtils::addIfNotNull($json, 'proper_shipping_name', $this->proper_shipping_name); + JsonUtils::addIfNotNull($json, 'emergency_contact_name', $this->emergency_contact_name); + JsonUtils::addIfNotNull($json, 'emergency_contact_phone', $this->emergency_contact_phone); + JsonUtils::addIfNotNull($json, 'local_proper_shipping_name', $this->local_proper_shipping_name); + JsonUtils::addIfNotNull($json, 'transport_category', $this->transport_category); + JsonUtils::addIfNotNull($json, 'tunnel_restriction_code', $this->tunnel_restriction_code); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Delivery/DeliveryDates.php b/src/Models/Delivery/DeliveryDates.php index d1971f0..3138b10 100644 --- a/src/Models/Delivery/DeliveryDates.php +++ b/src/Models/Delivery/DeliveryDates.php @@ -8,6 +8,7 @@ use AlexisPPLIN\SendcloudV3\Utils\DateUtils; use DateTimeImmutable; use DateTimeInterface; +use JsonSerializable; /** * Defined delivery dates @@ -45,4 +46,19 @@ public static function fromData(array $data) : self $deliver_at ); } + + public function jsonSerialize() : array + { + $json = []; + + if (isset($this->handover_at)) { + $json['handover_at'] = DateUtils::dateTimeToIso8601($this->handover_at); + } + + if (isset($this->deliver_at)) { + $json['deliver_at'] = DateUtils::dateTimeToIso8601($this->deliver_at); + } + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Measurement/Measurement.php b/src/Models/Measurement/Measurement.php index 1bb08c4..79315e6 100644 --- a/src/Models/Measurement/Measurement.php +++ b/src/Models/Measurement/Measurement.php @@ -43,4 +43,23 @@ public static function fromData(array $data) : self volume: $volume ); } + + public function jsonSerialize() : array + { + $json = []; + + if (isset($this->dimension)) { + $json['dimension'] = $this->dimension; + } + + if (isset($this->weight)) { + $json['weight'] = $this->weight; + } + + if (isset($this->volume)) { + $json['volume'] = $this->volume; + } + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Measurement/MeasurementDimension.php b/src/Models/Measurement/MeasurementDimension.php index 48e8872..ee345d4 100644 --- a/src/Models/Measurement/MeasurementDimension.php +++ b/src/Models/Measurement/MeasurementDimension.php @@ -44,4 +44,16 @@ public static function fromData(array $data) : self unit: (string) $data['unit'] ); } + + public function jsonSerialize() : array + { + $json = [ + 'lenght' => $this->lenght, + 'width' => $this->width, + 'height' => $this->height, + 'unit' => $this->unit + ]; + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Measurement/MeasurementVolume.php b/src/Models/Measurement/MeasurementVolume.php index 526ca93..d076a01 100644 --- a/src/Models/Measurement/MeasurementVolume.php +++ b/src/Models/Measurement/MeasurementVolume.php @@ -38,4 +38,14 @@ public static function fromData(array $data) : self unit: (string) $data['unit'] ); } + + public function jsonSerialize() : array + { + $json = [ + 'value' => $this->value, + 'unit' => $this->unit + ]; + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Measurement/MeasurementWeight.php b/src/Models/Measurement/MeasurementWeight.php index 25dd16c..8290867 100644 --- a/src/Models/Measurement/MeasurementWeight.php +++ b/src/Models/Measurement/MeasurementWeight.php @@ -37,4 +37,14 @@ public static function fromData(array $data) : self unit: (string) $data['unit'] ); } + + public function jsonSerialize() : array + { + $json = [ + 'value' => $this->value, + 'unit' => $this->unit + ]; + + return $json; + } } \ No newline at end of file diff --git a/src/Models/ModelInterface.php b/src/Models/ModelInterface.php index f028552..e2d0243 100644 --- a/src/Models/ModelInterface.php +++ b/src/Models/ModelInterface.php @@ -3,12 +3,18 @@ namespace AlexisPPLIN\SendcloudV3\Models; use AlexisPPLIN\SendcloudV3\Exceptions\ModelFromDataException; +use JsonSerializable; -interface ModelInterface +interface ModelInterface extends JsonSerializable { /** * @param array $data * @throws ModelFromDataException */ public static function fromData(array $data) : self; + + /** + * @return array + */ + public function jsonSerialize() : array; } \ No newline at end of file diff --git a/src/Models/Order/CustomsDetails.php b/src/Models/Order/CustomsDetails.php index edee64c..c918978 100644 --- a/src/Models/Order/CustomsDetails.php +++ b/src/Models/Order/CustomsDetails.php @@ -4,6 +4,7 @@ use AlexisPPLIN\SendcloudV3\Models\ModelInterface; use AlexisPPLIN\SendcloudV3\Models\Tax\TaxNumbers; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; /** * Customs information required for international shipments. @@ -52,4 +53,16 @@ public static function fromData(array $data) : self tax_numbers: isset($data['tax_numbers']) ? TaxNumbers::fromData($data['tax_numbers']) : null ); } + + public function jsonSerialize() : array + { + $json = []; + + JsonUtils::addIfNotNull($json, 'commercial_invoice_number', $this->commercial_invoice_number); + JsonUtils::addIfNotNull($json, 'shipment_type', $this->shipment_type); + JsonUtils::addIfNotNull($json, 'export_type', $this->export_type); + JsonUtils::addIfNotNull($json, 'tax_numbers', $this->tax_numbers); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Order/Order.php b/src/Models/Order/Order.php index f0e4970..850657e 100644 --- a/src/Models/Order/Order.php +++ b/src/Models/Order/Order.php @@ -8,6 +8,7 @@ use AlexisPPLIN\SendcloudV3\Models\PaymentDetails; use AlexisPPLIN\SendcloudV3\Models\ServicePoint\ServicePoint; use AlexisPPLIN\SendcloudV3\Utils\DateUtils; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; use DateTimeImmutable; class Order implements ModelInterface @@ -61,4 +62,33 @@ public static function fromData(array $data) : self service_point_details: isset($data['service_point_details']) ? ServicePoint::fromData($data['service_point_details']) : null, ); } + + public function jsonSerialize() : array + { + $json = [ + 'order_id' => $this->order_id, + 'order_number' => $this->order_number, + 'order_details' => $this->order_details, + 'payment_details' => $this->payment_details + ]; + + JsonUtils::addIfNotNull($json, 'id', $this->id); + + if (isset($this->created_at)) { + $json['created_at'] = DateUtils::dateTimeToIso8601($this->created_at); + } + + if (isset($this->modified_at)) { + $json['modified_at'] = DateUtils::dateTimeToIso8601($this->modified_at); + } + + JsonUtils::addIfNotNull($json, 'customs_details', $this->customs_details); + JsonUtils::addIfNotNull($json, 'customer_details', $this->customer_details); + JsonUtils::addIfNotNull($json, 'billing_address', $this->billing_address); + JsonUtils::addIfNotNull($json, 'shipping_address', $this->shipping_address); + JsonUtils::addIfNotNull($json, 'shipping_details', $this->shipping_details); + JsonUtils::addIfNotNull($json, 'service_point_details', $this->service_point_details); + + return ['data' => $json]; + } } \ No newline at end of file diff --git a/src/Models/Order/OrderDetails.php b/src/Models/Order/OrderDetails.php index 20aa9c5..cc883f0 100644 --- a/src/Models/Order/OrderDetails.php +++ b/src/Models/Order/OrderDetails.php @@ -5,6 +5,7 @@ use AlexisPPLIN\SendcloudV3\Models\ModelInterface; use AlexisPPLIN\SendcloudV3\Models\Status; use AlexisPPLIN\SendcloudV3\Utils\DateUtils; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; use DateTimeImmutable; /** @@ -52,4 +53,20 @@ public static function fromData(array $data) : self tags: isset($data['tags']) ? $data['tags'] : null ); } + + public function jsonSerialize() : array + { + $json = [ + 'integration' => $this->integration, + 'status' => $this->status, + 'order_created_at' => DateUtils::dateTimeToIso8601($this->order_created_at), + 'order_items' => $this->order_items, + 'order_updated_at' => DateUtils::dateTimeToIso8601($this->order_updated_at), + 'notes' => $this->notes + ]; + + JsonUtils::addIfNotNull($json, 'tags', $this->tags); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Order/OrderDetailsIntegration.php b/src/Models/Order/OrderDetailsIntegration.php index c1b31a0..d0f49ae 100644 --- a/src/Models/Order/OrderDetailsIntegration.php +++ b/src/Models/Order/OrderDetailsIntegration.php @@ -18,4 +18,13 @@ public static function fromData(array $data) : self id: (int) $data['id'] ); } + + public function jsonSerialize() : array + { + $json = [ + 'id' => $this->id + ]; + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Order/OrderItems.php b/src/Models/Order/OrderItems.php index 2ed3872..86580ca 100644 --- a/src/Models/Order/OrderItems.php +++ b/src/Models/Order/OrderItems.php @@ -7,6 +7,7 @@ use AlexisPPLIN\SendcloudV3\Models\Measurement\Measurement; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; use AlexisPPLIN\SendcloudV3\Models\Price; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; /** * @see https://sendcloud.dev/api/v3/orders/retrieve-an-order#response-data-order-details-order-items @@ -89,4 +90,33 @@ public static function fromData(array $data) : self dangerous_goods: isset($data['dangerous_goods']) ? DangerousGoods::fromData($data['dangerous_goods']) : null ); } + + public function jsonSerialize() : array + { + $json = [ + 'name' => $this->name, + 'quantity' => $this->quantity + ]; + + JsonUtils::addIfNotNull($json, 'total_price', $this->total_price); + JsonUtils::addIfNotNull($json, 'item_id', $this->item_id); + JsonUtils::addIfNotNull($json, 'product_id', $this->product_id); + JsonUtils::addIfNotNull($json, 'variant_id', $this->variant_id); + JsonUtils::addIfNotNull($json, 'image_url', $this->image_url); + JsonUtils::addIfNotNull($json, 'description', $this->description); + JsonUtils::addIfNotNull($json, 'sku', $this->sku); + JsonUtils::addIfNotNull($json, 'hs_code', $this->hs_code); + JsonUtils::addIfNotNull($json, 'country_of_origin', $this->country_of_origin); + JsonUtils::addIfNotNull($json, 'properties', $this->properties); + JsonUtils::addIfNotNull($json, 'unit_price', $this->unit_price); + JsonUtils::addIfNotNull($json, 'measurement', $this->measurement); + JsonUtils::addIfNotNull($json, 'ean', $this->ean); + JsonUtils::addIfNotNull($json, 'delivery_dates', $this->delivery_dates); + JsonUtils::addIfNotNull($json, 'mid_code', $this->mid_code); + JsonUtils::addIfNotNull($json, 'material_content', $this->material_content); + JsonUtils::addIfNotNull($json, 'intended_use', $this->intended_use); + JsonUtils::addIfNotNull($json, 'dangerous_goods', $this->dangerous_goods); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Order/ShipWith.php b/src/Models/Order/ShipWith.php index 766c863..3b2e65a 100644 --- a/src/Models/Order/ShipWith.php +++ b/src/Models/Order/ShipWith.php @@ -30,4 +30,14 @@ public static function fromData(array $data) : self properties: ShippingOptionProperties::fromData($data['properties']) ); } + + public function jsonSerialize() : array + { + $json = [ + 'type' => $this->type, + 'properties' => $this->properties + ]; + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Order/ShippingDetails.php b/src/Models/Order/ShippingDetails.php index f74ea96..686c6b1 100644 --- a/src/Models/Order/ShippingDetails.php +++ b/src/Models/Order/ShippingDetails.php @@ -5,6 +5,7 @@ use AlexisPPLIN\SendcloudV3\Models\Measurement\Measurement; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; use AlexisPPLIN\SendcloudV3\Models\ServicePoint\ServicePoint; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; /** * Shipping information @@ -40,4 +41,16 @@ public static function fromData(array $data) : self ship_with: isset($data['ship_with']) ? ShipWith::fromData($data['ship_with']) : null ); } + + public function jsonSerialize() : array + { + $json = []; + + JsonUtils::addIfNotNull($json, 'is_local_pickup', $this->is_local_pickup); + JsonUtils::addIfNotNull($json, 'delivery_indicator', $this->delivery_indicator); + JsonUtils::addIfNotNull($json, 'measurement', $this->measurement); + JsonUtils::addIfNotNull($json, 'ship_with', $this->ship_with); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Order/ShippingOptionProperties.php b/src/Models/Order/ShippingOptionProperties.php index ba6c3fe..8a86cbc 100644 --- a/src/Models/Order/ShippingOptionProperties.php +++ b/src/Models/Order/ShippingOptionProperties.php @@ -3,6 +3,7 @@ namespace AlexisPPLIN\SendcloudV3\Models\Order; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; /** * Contains the required properties to be sent when API client informs the shipping method and carrier to be used @@ -29,4 +30,14 @@ public static function fromData(array $data) : self contract_id: isset($data['contract_id']) ? (int) $data['contract_id'] : null ); } + + public function jsonSerialize() : array + { + $json = []; + + JsonUtils::addIfNotNull($json, 'shipping_option_code', $this->shipping_option_code); + JsonUtils::addIfNotNull($json, 'contract_id', $this->contract_id); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/PaymentDetails.php b/src/Models/PaymentDetails.php index 156d7cf..e5f4b9d 100644 --- a/src/Models/PaymentDetails.php +++ b/src/Models/PaymentDetails.php @@ -2,6 +2,8 @@ namespace AlexisPPLIN\SendcloudV3\Models; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; + /** * Node for everything about payments and money * @@ -54,4 +56,24 @@ public static function fromData(array $data) : self other_costs: isset($data['other_costs']) ? Price::fromData($data['other_costs']) : null ); } + + public function jsonSerialize() : array + { + $json = [ + 'total_price' => $this->total_price, + 'status' => $this->status + ]; + + JsonUtils::addIfNotNull($json, 'is_cash_on_delivery', $this->is_cash_on_delivery); + JsonUtils::addIfNotNull($json, 'subtotal_price', $this->subtotal_price); + JsonUtils::addIfNotNull($json, 'estimated_shipping_price', $this->estimated_shipping_price); + JsonUtils::addIfNotNull($json, 'estimated_tax_price', $this->estimated_tax_price); + JsonUtils::addIfNotNull($json, 'invoice_date', $this->invoice_date); + JsonUtils::addIfNotNull($json, 'discount_granted', $this->discount_granted); + JsonUtils::addIfNotNull($json, 'insurance_costs', $this->insurance_costs); + JsonUtils::addIfNotNull($json, 'freight_costs', $this->freight_costs); + JsonUtils::addIfNotNull($json, 'other_costs', $this->other_costs); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Price.php b/src/Models/Price.php index c56f66d..527a9cb 100644 --- a/src/Models/Price.php +++ b/src/Models/Price.php @@ -27,4 +27,14 @@ public static function fromData(array $data) : self currency: (string) $data['currency'] ); } + + public function jsonSerialize() : array + { + $json = [ + 'value' => $this->value, + 'currency' => $this->currency + ]; + + return $json; + } } \ No newline at end of file diff --git a/src/Models/ServicePoint/ServicePoint.php b/src/Models/ServicePoint/ServicePoint.php index 3876723..75f6e5a 100644 --- a/src/Models/ServicePoint/ServicePoint.php +++ b/src/Models/ServicePoint/ServicePoint.php @@ -3,6 +3,7 @@ namespace AlexisPPLIN\SendcloudV3\Models\ServicePoint; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; class ServicePoint implements ModelInterface{ public function __construct( @@ -27,4 +28,19 @@ public static function fromData(array $data) : self extra_data: isset($data['extra_data']) ? (object) $data['extra_data'] : null, ); } + + public function jsonSerialize() : array + { + $json = [ + 'id' => $this->id + ]; + + JsonUtils::addIfNotNull($json, 'post_number', $this->post_number); + JsonUtils::addIfNotNull($json, 'latitude', $this->latitude); + JsonUtils::addIfNotNull($json, 'longitude', $this->longitude); + JsonUtils::addIfNotNull($json, 'type', $this->type); + JsonUtils::addIfNotNull($json, 'extra_data', $this->extra_data); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Status.php b/src/Models/Status.php index cb3f035..2ae48a8 100644 --- a/src/Models/Status.php +++ b/src/Models/Status.php @@ -20,4 +20,17 @@ public static function fromData(array $data) : self message: isset($data['message']) ? (string) $data['message'] : null ); } + + public function jsonSerialize() : array + { + $json = [ + 'code' => $this->code + ]; + + if (isset($this->message)) { + $json['message'] = $this->message; + } + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Tax/TaxNumber.php b/src/Models/Tax/TaxNumber.php index f27c24e..f0abd95 100644 --- a/src/Models/Tax/TaxNumber.php +++ b/src/Models/Tax/TaxNumber.php @@ -3,6 +3,7 @@ namespace AlexisPPLIN\SendcloudV3\Models\Tax; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; class TaxNumber implements ModelInterface { @@ -22,4 +23,15 @@ public static function fromData(array $data) : self value: isset($data['value']) ? (string) $data['value'] : null ); } + + public function jsonSerialize() : array + { + $json = []; + + JsonUtils::addIfNotNull($json, 'name', $this->name); + JsonUtils::addIfNotNull($json, 'country_code', $this->country_code); + JsonUtils::addIfNotNull($json, 'value', $this->value); + + return $json; + } } \ No newline at end of file diff --git a/src/Models/Tax/TaxNumbers.php b/src/Models/Tax/TaxNumbers.php index b3d801c..c4c8204 100644 --- a/src/Models/Tax/TaxNumbers.php +++ b/src/Models/Tax/TaxNumbers.php @@ -48,4 +48,15 @@ public static function fromData(array $data) : self importer_of_record: $importer_of_record ); } + + public function jsonSerialize() : array + { + $json = [ + 'sender' => $this->sender, + 'receiver' => $this->receiver, + 'importer_of_record' => $this->importer_of_record, + ]; + + return $json; + } } \ No newline at end of file diff --git a/src/Utils/DateUtils.php b/src/Utils/DateUtils.php index bccbd3f..a649666 100644 --- a/src/Utils/DateUtils.php +++ b/src/Utils/DateUtils.php @@ -8,17 +8,28 @@ class DateUtils { + public const DATE_FORMAT = "Y-m-d\TH:i:s.uP"; + /** * Parse ISO 8601 date into an DateTimeImmutable object * @throws DateParsingException */ public static function iso8601ToDateTime(string $iso8601) : DateTimeImmutable { - $date = DateTimeImmutable::createFromFormat("Y-m-d\TH:i:s.up", $iso8601); + $date = DateTimeImmutable::createFromFormat(self::DATE_FORMAT, $iso8601); if (!$date) { throw new DateParsingException("Error when parsing ISO 8601 date ({$iso8601})"); } return $date; } + + /** + * Convert DateTimeImmutable object into an ISO 8601 date + * @throws DateParsingException + */ + public static function dateTimeToIso8601(DateTimeImmutable $date) : string + { + return $date->format(self::DATE_FORMAT); + } } \ No newline at end of file diff --git a/src/Utils/JsonUtils.php b/src/Utils/JsonUtils.php new file mode 100644 index 0000000..c865771 --- /dev/null +++ b/src/Utils/JsonUtils.php @@ -0,0 +1,16 @@ + $json + */ + public static function addIfNotNull(array &$json, string $key, mixed $value) : void + { + if (isset($value)) { + $json[$key] = $value; + } + } +} \ No newline at end of file diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php index 2bd350d..aca43b0 100644 --- a/tests/Endpoints/OrdersTest.php +++ b/tests/Endpoints/OrdersTest.php @@ -50,6 +50,9 @@ class OrdersTest extends TestCase { private Orders $endpoint; + private string $json; + private Order $order; + public function setUp(): void { $publicKey = '123456'; @@ -57,119 +60,10 @@ public function setUp(): void $partnerId = '1'; $apiBaseUrl = 'https://api.example.com/v3'; + $this->json = file_get_contents(__DIR__ . '/orders.json'); + $client = new Client(); - $client->addResponse(new Response(body: <<addResponse(new Response(body: $this->json)); $this->endpoint = new Orders( $publicKey, @@ -178,18 +72,12 @@ public function setUp(): void $apiBaseUrl, $client ); - } - public function testGetOrder() : void - { - // -- Arrange - - $order_id = 1; - $expected = new Order( + $this->order = new Order( id: '669', order_id: '555413', - created_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555Z'), - modified_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555Z'), + created_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555309+00:00'), + modified_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555309+00:00'), order_number: 'OXSDFGHTD-12', order_details: new OrderDetails( integration: new OrderDetailsIntegration( @@ -199,8 +87,8 @@ public function testGetOrder() : void code: 'fulfilled', message: 'Fulfilled' ), - order_created_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555Z'), - order_updated_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555Z'), + order_created_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555309+00:00'), + order_updated_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555309+00:00'), order_items: [ new OrderItems( name: 'Cylinder candle', @@ -294,14 +182,37 @@ public function testGetOrder() : void country_code: 'NL' ) ); + } + + public function testGetOrder() : void + { + // -- Arrange + + $order_id = 1; + $expected = $this->order; + + // -- Act + + $actual = $this->endpoint->getOrder($order_id); + + // -- Assert + + $this->assertInstanceOf(Order::class, $actual); + $this->assertEquals($expected, $actual); + } + + public function testOrderJson() : void + { + // -- Arrange + + $expected = $this->json; // -- Act - $order = $this->endpoint->getOrder($order_id); + $actual = json_encode($this->order); // -- Assert - $this->assertInstanceOf(Order::class, $order); - $this->assertEquals($expected, $order); + $this->assertJsonStringEqualsJsonString($expected, $actual); } } \ No newline at end of file diff --git a/tests/Endpoints/orders.json b/tests/Endpoints/orders.json new file mode 100644 index 0000000..dad2718 --- /dev/null +++ b/tests/Endpoints/orders.json @@ -0,0 +1,110 @@ +{ + "data": { + "id": "669", + "order_id": "555413", + "created_at": "2018-02-27T10:00:00.555309+00:00", + "modified_at": "2018-02-27T10:00:00.555309+00:00", + "order_number": "OXSDFGHTD-12", + "order_details": { + "integration": { + "id": 739283 + }, + "status": { + "code": "fulfilled", + "message": "Fulfilled" + }, + "order_created_at": "2018-02-27T10:00:00.555309+00:00", + "order_updated_at": "2018-02-27T10:00:00.555309+00:00", + "order_items": [ + { + "name": "Cylinder candle", + "measurement": { + "weight": { + "value": 1, + "unit": "kg" + } + }, + "quantity": 1, + "unit_price": { + "value": 3.5, + "currency": "EUR" + }, + "total_price": { + "value": 3.5, + "currency": "EUR" + }, + "delivery_dates": { + "handover_at": "2022-02-27T10:00:00.555309+00:00", + "deliver_at": "2022-03-02T11:50:00.555309+00:00" + }, + "mid_code": "US1234567", + "material_content": "100% Cotton", + "intended_use": "Personal use" + } + ] + }, + "payment_details": { + "is_cash_on_delivery": true, + "total_price": { + "value": 7, + "currency": "EUR" + }, + "status": { + "code": "paid", + "message": "Order has been paid" + }, + "discount_granted": { + "value": "3.99", + "currency": "EUR" + }, + "insurance_costs": { + "value": "9.99", + "currency": "EUR" + }, + "freight_costs": { + "value": "5.99", + "currency": "EUR" + }, + "other_costs": { + "value": "2.99", + "currency": "EUR" + } + }, + "customs_details": { + "commercial_invoice_number": "0124-03102022", + "shipment_type": "commercial_goods", + "export_type": "commercial_b2c", + "tax_numbers": { + "sender": [ + { + "name": "VAT", + "country_code": "NL", + "value": "NL987654321B02" + } + ], + "receiver": [ + { + "name": "VAT", + "country_code": "DE", + "value": "DE123456789B03" + } + ], + "importer_of_record": [ + { + "name": "VAT", + "country_code": "NL", + "value": "NL975318642B01" + } + ] + } + }, + "shipping_address": { + "name": "John Doe", + "address_line_1": "Stadhuisplein", + "house_number": "15", + "postal_code": "5341TW", + "city": "Oss", + "country_code": "NL" + } + } +} \ No newline at end of file From 3fbe4aa27cfda2844f73b9a4f01f7bf64a5ad8f3 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH 06/17] Added full coverage for Orders GET --- src/Models/Customer/CustomerDetails.php | 2 +- src/Models/DangerousGoods.php | 33 +-- .../Measurement/MeasurementDimension.php | 8 +- tests/Endpoints/OrdersTest.php | 196 +++++++++++++++--- tests/Endpoints/orders.json | 177 +++++++++++++--- 5 files changed, 333 insertions(+), 83 deletions(-) diff --git a/src/Models/Customer/CustomerDetails.php b/src/Models/Customer/CustomerDetails.php index d0be1cf..ceb0593 100644 --- a/src/Models/Customer/CustomerDetails.php +++ b/src/Models/Customer/CustomerDetails.php @@ -41,7 +41,7 @@ public function jsonSerialize() : array ]; if (isset($this->phone_number)) { - $json['name'] = $this->phone_number; + $json['phone_number'] = $this->phone_number; } if (isset($this->email)) { diff --git a/src/Models/DangerousGoods.php b/src/Models/DangerousGoods.php index 7ed4343..59d5aac 100644 --- a/src/Models/DangerousGoods.php +++ b/src/Models/DangerousGoods.php @@ -17,10 +17,10 @@ class DangerousGoods implements ModelInterface ]; public const UNITS_OF_MESUREMENT = [ - 'KG', - 'G', - 'L', - 'ML' + 'kg', + 'g', + 'l', + 'ml' ]; public const COMMODITY_REGULATED_LEVEL_CODES = [ @@ -29,8 +29,10 @@ class DangerousGoods implements ModelInterface ]; public const TRANSPORTATION_MODES = [ - 'LQ', - 'EQ' + 'Highway', + 'Ground', + 'PAX', + 'CAO' ]; public const ADR_PACKING_GROUPS = [ @@ -40,11 +42,12 @@ class DangerousGoods implements ModelInterface ]; public const WEIGHT_TYPES = [ - 'Net', - 'Gross' + 'net', + 'gross' ]; /** + * @param ?string $chemical_record_identifier Chemical record identifier for the dangerous goods * @param ?string $value Chemical record identifier for the dangerous goods * @param value-of $regulation_set Regulation set governing the dangerous goods * @param ?string $packaging_type_quantity Quantity of packaging type @@ -59,7 +62,7 @@ class DangerousGoods implements ModelInterface * @param value-of $transportation_mode Mode of transportation * @param ?string $emergency_contact_name Name of emergency contact person * @param ?string $emergency_contact_phone Phone number of emergency contact - * @param value-of $adr_packing_group_classification ADR packing group classification + * @param value-of $adr_packing_group_letter ADR packing group classification * @param ?string $local_proper_shipping_name Local proper shipping name * @param ?string $transport_category Transport category for ADR regulations * @param ?string $tunnel_restriction_code Tunnel restriction code @@ -71,8 +74,8 @@ public function __construct( public readonly string $commodity_regulated_level_code, public readonly string $transportation_mode, public readonly string $weight_type, - public readonly string $adr_packing_group_classification, - public readonly ?string $value = null, + public readonly string $adr_packing_group_letter, + public readonly ?string $chemical_record_identifier = null, public readonly ?string $packaging_type_quantity = null, public readonly ?string $packaging_type = null, public readonly ?string $packaging_instruction_code = null, @@ -97,8 +100,8 @@ public static function fromData(array $data) : self commodity_regulated_level_code: (string) $data['commodity_regulated_level_code'], transportation_mode: (string) $data['transportation_mode'], weight_type: (string) $data['weight_type'], - adr_packing_group_classification: (string) $data['adr_packing_group_classification'], - value: isset($data['value']) ? (string) $data['value'] : null, + adr_packing_group_letter: (string) $data['adr_packing_group_letter'], + chemical_record_identifier: isset($data['chemical_record_identifier']) ? (string) $data['chemical_record_identifier'] : null, packaging_type_quantity: isset($data['packaging_type_quantity']) ? (string) $data['packaging_type_quantity'] : null, packaging_type: isset($data['packaging_type']) ? (string) $data['packaging_type'] : null, packaging_instruction_code: isset($data['packaging_instruction_code']) ? (string) $data['packaging_instruction_code'] : null, @@ -122,10 +125,10 @@ public function jsonSerialize() : array 'commodity_regulated_level_code' => $this->commodity_regulated_level_code, 'transportation_mode' => $this->transportation_mode, 'weight_type' => $this->weight_type, - 'adr_packing_group_classification' => $this->adr_packing_group_classification + 'adr_packing_group_letter' => $this->adr_packing_group_letter ]; - JsonUtils::addIfNotNull($json, 'value', $this->value); + JsonUtils::addIfNotNull($json, 'chemical_record_identifier', $this->chemical_record_identifier); JsonUtils::addIfNotNull($json, 'packaging_type_quantity', $this->packaging_type_quantity); JsonUtils::addIfNotNull($json, 'packaging_type', $this->packaging_type); JsonUtils::addIfNotNull($json, 'packaging_instruction_code', $this->packaging_instruction_code); diff --git a/src/Models/Measurement/MeasurementDimension.php b/src/Models/Measurement/MeasurementDimension.php index ee345d4..8b02c35 100644 --- a/src/Models/Measurement/MeasurementDimension.php +++ b/src/Models/Measurement/MeasurementDimension.php @@ -21,13 +21,13 @@ class MeasurementDimension implements ModelInterface ]; /** - * @param float $lenght length in specified unit + * @param float $length length in specified unit * @param float $width width in specified unit * @param float $height height in specified unit * @param value-of $unit */ public function __construct( - public readonly float $lenght, + public readonly float $length, public readonly float $width, public readonly float $height, public readonly string $unit @@ -38,7 +38,7 @@ public function __construct( public static function fromData(array $data) : self { return new self( - lenght: (float) $data['lenght'], + length: (float) $data['length'], width: (float) $data['width'], height: (float) $data['height'], unit: (string) $data['unit'] @@ -48,7 +48,7 @@ public static function fromData(array $data) : self public function jsonSerialize() : array { $json = [ - 'lenght' => $this->lenght, + 'length' => $this->length, 'width' => $this->width, 'height' => $this->height, 'unit' => $this->unit diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php index aca43b0..dfbfd48 100644 --- a/tests/Endpoints/OrdersTest.php +++ b/tests/Endpoints/OrdersTest.php @@ -7,20 +7,29 @@ use AlexisPPLIN\SendcloudV3\Endpoints\Orders; use AlexisPPLIN\SendcloudV3\Factory\ClientFactory; use AlexisPPLIN\SendcloudV3\Models\Address; +use AlexisPPLIN\SendcloudV3\Models\Customer\CustomerDetails; +use AlexisPPLIN\SendcloudV3\Models\DangerousGoods; use AlexisPPLIN\SendcloudV3\Models\Delivery\DeliveryDates; use AlexisPPLIN\SendcloudV3\Models\Measurement\Measurement; +use AlexisPPLIN\SendcloudV3\Models\Measurement\MeasurementDimension; +use AlexisPPLIN\SendcloudV3\Models\Measurement\MeasurementVolume; use AlexisPPLIN\SendcloudV3\Models\Measurement\MeasurementWeight; use AlexisPPLIN\SendcloudV3\Models\Order\CustomsDetails; use AlexisPPLIN\SendcloudV3\Models\Order\Order; use AlexisPPLIN\SendcloudV3\Models\Order\OrderDetails; use AlexisPPLIN\SendcloudV3\Models\Order\OrderDetailsIntegration; use AlexisPPLIN\SendcloudV3\Models\Order\OrderItems; +use AlexisPPLIN\SendcloudV3\Models\Order\ShippingDetails; +use AlexisPPLIN\SendcloudV3\Models\Order\ShippingOptionProperties; +use AlexisPPLIN\SendcloudV3\Models\Order\ShipWith; use AlexisPPLIN\SendcloudV3\Models\PaymentDetails; use AlexisPPLIN\SendcloudV3\Models\Price; +use AlexisPPLIN\SendcloudV3\Models\ServicePoint\ServicePoint; use AlexisPPLIN\SendcloudV3\Models\Status; use AlexisPPLIN\SendcloudV3\Models\Tax\TaxNumber; use AlexisPPLIN\SendcloudV3\Models\Tax\TaxNumbers; use AlexisPPLIN\SendcloudV3\Utils\DateUtils; +use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; use Nyholm\Psr7\Response; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; @@ -33,19 +42,28 @@ #[CoversClass(DeliveryDates::class)] #[CoversClass(Measurement::class)] #[CoversClass(MeasurementWeight::class)] +#[CoversClass(MeasurementDimension::class)] +#[CoversClass(MeasurementVolume::class)] #[CoversClass(OrderDetails::class)] #[CoversClass(OrderDetailsIntegration::class)] #[CoversClass(OrderItems::class)] #[CoversClass(Price::class)] -#[CoversClass(DateUtils::class)] #[CoversClass(Status::class)] #[CoversClass(PaymentDetails::class)] #[CoversClass(CustomsDetails::class)] #[CoversClass(TaxNumber::class)] #[CoversClass(TaxNumbers::class)] #[CoversClass(Address::class)] +#[CoversClass(CustomerDetails::class)] +#[CoversClass(ShipWith::class)] +#[CoversClass(ShippingDetails::class)] +#[CoversClass(ShippingOptionProperties::class)] +#[CoversClass(ServicePoint::class)] +#[CoversClass(DangerousGoods::class)] #[UsesClass(Client::class)] #[UsesClass(ClientFactory::class)] +#[UsesClass(JsonUtils::class)] +#[UsesClass(DateUtils::class)] class OrdersTest extends TestCase { private Orders $endpoint; @@ -74,81 +92,140 @@ public function setUp(): void ); $this->order = new Order( - id: '669', - order_id: '555413', - created_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555309+00:00'), - modified_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555309+00:00'), - order_number: 'OXSDFGHTD-12', + id: '752417284', + order_id: '7bdd5bfd-76bc-4654-9d40-5d5d49f1cd6c', + order_number: '101170081', + created_at: DateUtils::iso8601ToDateTime('2026-02-09T16:00:17.040454+00:00'), + modified_at: DateUtils::iso8601ToDateTime('2026-02-09T16:00:17.111586+00:00'), order_details: new OrderDetails( integration: new OrderDetailsIntegration( - id: 739283 + id: 1 ), status: new Status( code: 'fulfilled', - message: 'Fulfilled' + message: 'Order has been fulfilled' ), - order_created_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555309+00:00'), + order_created_at: DateUtils::iso8601ToDateTime('2026-02-09T10:00:00.556000+00:00'), order_updated_at: DateUtils::iso8601ToDateTime('2018-02-27T10:00:00.555309+00:00'), order_items: [ new OrderItems( + item_id: '5552', name: 'Cylinder candle', - quantity: 1, + description: 'Pebble green - 12x8 cm', + product_id: '1458734634', + variant_id: '15346645', + image_url: 'https://i.ibb.co/6tLZ2Jz/orange.jpeg', measurement: new Measurement( + dimension: new MeasurementDimension( + length: 15.0, + width: 20.5, + height: 37.0, + unit: 'cm' + ), weight: new MeasurementWeight( - value: 1, + value: 14.5, unit: 'kg' + ), + volume: new MeasurementVolume( + value: 5, + unit: 'l' ) ), - unit_price: new Price( + quantity: 1, + sku: 'WW-DR-GR-XS-001', + hs_code: '6205.20', + ean: '0799439112766', + properties: [ + 'size' => 'red', + 'color' => 'green', + ], + country_of_origin: 'NL', + total_price: new Price( value: 3.5, currency: 'EUR' ), - total_price: new Price( + unit_price: new Price( value: 3.5, currency: 'EUR' ), delivery_dates: new DeliveryDates( - handover_at: DateUtils::iso8601ToDateTime('2022-02-27T10:00:00.555309+00:00'), - deliver_at: DateUtils::iso8601ToDateTime('2022-03-02T11:50:00.555309+00:00'), + handover_at: DateUtils::iso8601ToDateTime('2025-02-27T10:00:00.555309+00:00'), + deliver_at: DateUtils::iso8601ToDateTime('2025-03-15T10:00:00.555309+00:00'), ), - mid_code: 'US1234567', + mid_code: 'NLOZR92MEL', material_content: '100% Cotton', intended_use: 'Personal use', + dangerous_goods: new DangerousGoods( + chemical_record_identifier: '1', + regulation_set: 'IATA', + packaging_type_quantity: 'type', + id_number: '123456', + class_division_number: '42', + quantity: '1', + unit_of_measurement: 'kg', + proper_shipping_name: 'product', + commodity_regulated_level_code: 'LQ', + transportation_mode: 'Highway', + emergency_contact_name: 'John Doe', + emergency_contact_phone: '+319881729999', + adr_packing_group_letter: 'I', + local_proper_shipping_name: 'shipping', + transport_category: 'transport', + tunnel_restriction_code: 'tunnel', + weight_type: 'net', + ) ) ], - notes: '' + notes: 'Call this number before delivery: 063 874 6473', + tags: [ + 'fragile', + 'countryside warehouse' + ] ), payment_details: new PaymentDetails( - is_cash_on_delivery: true, + subtotal_price: new Price( + value: 3.5, + currency: 'EUR' + ), + estimated_shipping_price: new Price( + value: 3.5, + currency: 'EUR' + ), + estimated_tax_price: new Price( + value: 3.5, + currency: 'EUR' + ), total_price: new Price( - value: 7, + value: 3.5, currency: 'EUR' ), status: new Status( code: 'paid', - message: 'Order has been paid' + message: 'Paid' ), + invoice_date: '2018-07-14', discount_granted: new Price( - value: 3.99, + value: 3.5, currency: 'EUR' ), insurance_costs: new Price( - value: 9.99, + value: 3.5, currency: 'EUR' ), freight_costs: new Price( - value: 5.99, + value: 3.5, currency: 'EUR' ), other_costs: new Price( - value: 2.99, + value: 3.5, currency: 'EUR' ), + is_cash_on_delivery: true ), customs_details: new CustomsDetails( - commercial_invoice_number: '0124-03102022', + commercial_invoice_number: '1002404102022', shipment_type: 'commercial_goods', - export_type: 'commercial_b2c', + export_type: 'private', tax_numbers: new TaxNumbers( sender: [ new TaxNumber( @@ -160,26 +237,81 @@ public function setUp(): void receiver: [ new TaxNumber( name: 'VAT', - country_code: 'DE', - value: 'DE123456789B03' + country_code: 'NL', + value: 'NL987654321B02' ) ], importer_of_record: [ new TaxNumber( name: 'VAT', country_code: 'NL', - value: 'NL975318642B01' + value: 'NL987654321B02' ) ] ) ), + customer_details: new CustomerDetails( + name: 'John Doe', + phone_number: '+319881729999', + email: 'john@doe.com' + ), + billing_address: new Address( + name: 'John Doe', + address_line_1: 'Lansdown Glade', + address_line_2: 'a', + house_number: '15', + postal_code: '5341XT', + city: 'Oss', + country_code: 'NL', + email: 'johndoe@gmail.com', + phone_number: '+319881729999' + ), shipping_address: new Address( name: 'John Doe', - address_line_1: 'Stadhuisplein', + address_line_1: 'Lansdown Glade', + address_line_2: 'a', house_number: '15', - postal_code: '5341TW', + postal_code: '5341XT', city: 'Oss', - country_code: 'NL' + country_code: 'NL', + email: 'johndoe@gmail.com', + phone_number: '+319881729999' + ), + shipping_details: new ShippingDetails( + is_local_pickup: true, + delivery_indicator: 'DHL Home Delivery', + measurement: new Measurement( + dimension: new MeasurementDimension( + length: 15.0, + width: 20.5, + height: 37.0, + unit: 'cm' + ), + weight: new MeasurementWeight( + value: 14.5, + unit: 'kg' + ), + volume: new MeasurementVolume( + value: 5, + unit: 'l' + ) + ), + ship_with: new ShipWith( + type: 'shipping_option_code', + properties: new ShippingOptionProperties( + shipping_option_code: 'postnl:standard' + ) + ) + ), + service_point_details: new ServicePoint( + id: '123', + post_number: 'some-post-number', + latitude: '51.427063', + longitude: '5.486414', + type: 'packstation', + extra_data: (object) [ + 'test' => 'test' + ] ) ); } diff --git a/tests/Endpoints/orders.json b/tests/Endpoints/orders.json index dad2718..585cfe8 100644 --- a/tests/Endpoints/orders.json +++ b/tests/Endpoints/orders.json @@ -1,79 +1,139 @@ { "data": { - "id": "669", - "order_id": "555413", - "created_at": "2018-02-27T10:00:00.555309+00:00", - "modified_at": "2018-02-27T10:00:00.555309+00:00", - "order_number": "OXSDFGHTD-12", + "id": "752417284", + "order_id": "7bdd5bfd-76bc-4654-9d40-5d5d49f1cd6c", + "order_number": "101170081", + "created_at": "2026-02-09T16:00:17.040454+00:00", + "modified_at": "2026-02-09T16:00:17.111586+00:00", "order_details": { "integration": { - "id": 739283 + "id": 1 }, "status": { "code": "fulfilled", - "message": "Fulfilled" + "message": "Order has been fulfilled" }, - "order_created_at": "2018-02-27T10:00:00.555309+00:00", + "order_created_at": "2026-02-09T10:00:00.556000+00:00", "order_updated_at": "2018-02-27T10:00:00.555309+00:00", "order_items": [ { + "item_id": "5552", "name": "Cylinder candle", + "description": "Pebble green - 12x8 cm", + "product_id": "1458734634", + "variant_id": "15346645", + "image_url": "https://i.ibb.co/6tLZ2Jz/orange.jpeg", "measurement": { + "dimension": { + "length": 15.0, + "width": 20.5, + "height": 37.0, + "unit": "cm" + }, "weight": { - "value": 1, + "value": 14.5, "unit": "kg" + }, + "volume": { + "value": 5, + "unit": "l" } }, "quantity": 1, - "unit_price": { + "sku": "WW-DR-GR-XS-001", + "hs_code": "6205.20", + "ean": "0799439112766", + "properties": { + "size": "red", + "color": "green" + }, + "country_of_origin": "NL", + "total_price": { "value": 3.5, "currency": "EUR" }, - "total_price": { + "unit_price": { "value": 3.5, "currency": "EUR" }, "delivery_dates": { - "handover_at": "2022-02-27T10:00:00.555309+00:00", - "deliver_at": "2022-03-02T11:50:00.555309+00:00" + "handover_at": "2025-02-27T10:00:00.555309+00:00", + "deliver_at": "2025-03-15T10:00:00.555309+00:00" }, - "mid_code": "US1234567", + "mid_code": "NLOZR92MEL", "material_content": "100% Cotton", - "intended_use": "Personal use" + "intended_use": "Personal use", + "dangerous_goods": { + "chemical_record_identifier": "1", + "regulation_set": "IATA", + "packaging_type_quantity": "type", + "id_number": "123456", + "class_division_number": "42", + "quantity": "1", + "unit_of_measurement": "kg", + "proper_shipping_name": "product", + "commodity_regulated_level_code": "LQ", + "transportation_mode": "Highway", + "emergency_contact_name": "John Doe", + "emergency_contact_phone": "+319881729999", + "adr_packing_group_letter": "I", + "local_proper_shipping_name": "shipping", + "transport_category": "transport", + "tunnel_restriction_code": "tunnel", + "weight_type": "net" + } } + ], + "notes": "Call this number before delivery: 063 874 6473", + "tags": [ + "fragile", + "countryside warehouse" ] }, "payment_details": { - "is_cash_on_delivery": true, + "subtotal_price": { + "value": 3.5, + "currency": "EUR" + }, + "estimated_shipping_price": { + "value": 3.5, + "currency": "EUR" + }, + "estimated_tax_price": { + "value": 3.5, + "currency": "EUR" + }, "total_price": { - "value": 7, + "value": 3.5, "currency": "EUR" }, "status": { "code": "paid", - "message": "Order has been paid" + "message": "Paid" }, + "invoice_date": "2018-07-14", "discount_granted": { - "value": "3.99", + "value": 3.5, "currency": "EUR" }, "insurance_costs": { - "value": "9.99", + "value": 3.5, "currency": "EUR" }, "freight_costs": { - "value": "5.99", + "value": 3.5, "currency": "EUR" }, "other_costs": { - "value": "2.99", + "value": 3.5, "currency": "EUR" - } + }, + "is_cash_on_delivery": true }, "customs_details": { - "commercial_invoice_number": "0124-03102022", + "commercial_invoice_number": "1002404102022", "shipment_type": "commercial_goods", - "export_type": "commercial_b2c", + "export_type": "private", "tax_numbers": { "sender": [ { @@ -85,26 +145,81 @@ "receiver": [ { "name": "VAT", - "country_code": "DE", - "value": "DE123456789B03" + "country_code": "NL", + "value": "NL987654321B02" } ], "importer_of_record": [ { "name": "VAT", "country_code": "NL", - "value": "NL975318642B01" + "value": "NL987654321B02" } ] } }, + "customer_details": { + "name": "John Doe", + "phone_number": "+319881729999", + "email": "john@doe.com" + }, + "billing_address": { + "name": "John Doe", + "address_line_1": "Lansdown Glade", + "house_number": "15", + "address_line_2": "a", + "postal_code": "5341XT", + "city": "Oss", + "country_code": "NL", + "email": "johndoe@gmail.com", + "phone_number": "+319881729999" + }, "shipping_address": { "name": "John Doe", - "address_line_1": "Stadhuisplein", + "address_line_1": "Lansdown Glade", "house_number": "15", - "postal_code": "5341TW", + "address_line_2": "a", + "postal_code": "5341XT", "city": "Oss", - "country_code": "NL" + "country_code": "NL", + "email": "johndoe@gmail.com", + "phone_number": "+319881729999" + }, + "shipping_details": { + "is_local_pickup": true, + "delivery_indicator": "DHL Home Delivery", + "measurement": { + "dimension": { + "length": 15.0, + "width": 20.5, + "height": 37.0, + "unit": "cm" + }, + "weight": { + "value": 14.5, + "unit": "kg" + }, + "volume": { + "value": 5, + "unit": "l" + } + }, + "ship_with": { + "type": "shipping_option_code", + "properties": { + "shipping_option_code": "postnl:standard" + } + } + }, + "service_point_details": { + "id": "123", + "post_number": "some-post-number", + "latitude": "51.427063", + "longitude": "5.486414", + "type": "packstation", + "extra_data": { + "test": "test" + } } } } \ No newline at end of file From e665301a894a88be1d9e2aa57c96c1e6501f2a7c Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH 07/17] Updated composer --- composer.json | 2 +- composer.lock | 28 ++++++++++++++-------------- src/Models/DangerousGoods.php | 1 - 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/composer.json b/composer.json index 3094a03..72e1708 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "require": { "php": ">=8.2.0", "php-http/discovery": "^1.20", - "psr/http-client-implementation": "*", + "psr/http-client-implementation": "1.0", "php-http/client-common": "^2.7", "php-http/httplug": "^2.4" }, diff --git a/composer.lock b/composer.lock index 6b39ae2..c0b203e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "3cae9a7cc870c53fbc641d208f5c2141", + "content-hash": "2a05939372331b36ed1fcdfe3b0e6f19", "packages": [ { "name": "clue/stream-filter", @@ -1558,16 +1558,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.51", + "version": "11.5.52", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "ad14159f92910b0f0e3928c13e9b2077529de091" + "reference": "b287d32c26f78768e391843c5a59395f24b62605" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad14159f92910b0f0e3928c13e9b2077529de091", - "reference": "ad14159f92910b0f0e3928c13e9b2077529de091", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b287d32c26f78768e391843c5a59395f24b62605", + "reference": "b287d32c26f78768e391843c5a59395f24b62605", "shasum": "" }, "require": { @@ -1640,7 +1640,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.51" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.52" }, "funding": [ { @@ -1664,7 +1664,7 @@ "type": "tidelift" } ], - "time": "2026-02-05T07:59:30+00:00" + "time": "2026-02-08T07:05:14+00:00" }, { "name": "psr/container", @@ -1771,21 +1771,21 @@ }, { "name": "rector/rector", - "version": "2.3.5", + "version": "2.3.6", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070" + "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/9442f4037de6a5347ae157fe8e6c7cda9d909070", - "reference": "9442f4037de6a5347ae157fe8e6c7cda9d909070", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/ca9ebb81d280cd362ea39474dabd42679e32ca6b", + "reference": "ca9ebb81d280cd362ea39474dabd42679e32ca6b", "shasum": "" }, "require": { "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.36" + "phpstan/phpstan": "^2.1.38" }, "conflict": { "rector/rector-doctrine": "*", @@ -1819,7 +1819,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.3.5" + "source": "https://github.com/rectorphp/rector/tree/2.3.6" }, "funding": [ { @@ -1827,7 +1827,7 @@ "type": "github" } ], - "time": "2026-01-28T15:22:48+00:00" + "time": "2026-02-06T14:25:06+00:00" }, { "name": "sebastian/cli-parser", diff --git a/src/Models/DangerousGoods.php b/src/Models/DangerousGoods.php index 59d5aac..bc60ecf 100644 --- a/src/Models/DangerousGoods.php +++ b/src/Models/DangerousGoods.php @@ -48,7 +48,6 @@ class DangerousGoods implements ModelInterface /** * @param ?string $chemical_record_identifier Chemical record identifier for the dangerous goods - * @param ?string $value Chemical record identifier for the dangerous goods * @param value-of $regulation_set Regulation set governing the dangerous goods * @param ?string $packaging_type_quantity Quantity of packaging type * @param ?string $packaging_type Type of packaging used From 2ccdb206c17b8d9e32a276b9837ffdb9fc8f382f Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH 08/17] Fixed versionning --- composer.json | 2 +- composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 72e1708..1100996 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "require": { "php": ">=8.2.0", "php-http/discovery": "^1.20", - "psr/http-client-implementation": "1.0", + "psr/http-client-implementation": "^1.0", "php-http/client-common": "^2.7", "php-http/httplug": "^2.4" }, diff --git a/composer.lock b/composer.lock index c0b203e..dc5a67d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2a05939372331b36ed1fcdfe3b0e6f19", + "content-hash": "c8f867e51e04ce9235e7e36c3704628e", "packages": [ { "name": "clue/stream-filter", From 58a3673ba52da47846b567d864f0a6c2c9ec4261 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:21 +0100 Subject: [PATCH 09/17] Fixed composer for php 8.2 --- composer.json | 7 ++- composer.lock | 140 ++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 119 insertions(+), 28 deletions(-) diff --git a/composer.json b/composer.json index 1100996..6359732 100644 --- a/composer.json +++ b/composer.json @@ -42,13 +42,16 @@ "phpunit/phpunit": "^11.5", "phpunit/php-code-coverage": "^11.0", "php-http/mock-client": "^1.6", - "symfony/http-client": "^8.0", - "nyholm/psr7": "^1.8" + "nyholm/psr7": "^1.8", + "symfony/http-client": "^7.4" }, "scripts": { "test": "vendor/bin/phpunit" }, "config": { + "platform": { + "php": "8.2" + }, "allow-plugins": { "php-http/discovery": false } diff --git a/composer.lock b/composer.lock index dc5a67d..137ce84 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c8f867e51e04ce9235e7e36c3704628e", + "content-hash": "75a53616a0b15f92d2a19835b3323757", "packages": [ { "name": "clue/stream-filter", @@ -625,20 +625,20 @@ }, { "name": "symfony/options-resolver", - "version": "v8.0.0", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" + "reference": "b38026df55197f9e39a44f3215788edf83187b80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", - "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80", + "reference": "b38026df55197f9e39a44f3215788edf83187b80", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -672,7 +672,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.4.0" }, "funding": [ { @@ -692,7 +692,7 @@ "type": "tidelift" } ], - "time": "2025-11-12T15:55:31+00:00" + "time": "2025-11-12T15:39:26+00:00" }, { "name": "symfony/polyfill-php80", @@ -2869,27 +2869,31 @@ }, { "name": "symfony/http-client", - "version": "v8.0.5", + "version": "v7.4.5", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4" + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/f9fdd372473e66469c6d32a4ed12efcffdea38c4", - "reference": "f9fdd372473e66469c6d32a4ed12efcffdea38c4", + "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", "shasum": "" }, "require": { - "php": ">=8.4", + "php": ">=8.2", "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/polyfill-php83": "^1.29", "symfony/service-contracts": "^2.5|^3" }, "conflict": { - "amphp/amp": "<3", - "php-http/discovery": "<1.15" + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" }, "provide": { "php-http/async-client-implementation": "*", @@ -2898,19 +2902,20 @@ "symfony/http-client-implementation": "3.0" }, "require-dev": { - "amphp/http-client": "^5.3.2", - "amphp/http-tunnel": "^2.0", + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", "guzzlehttp/promises": "^1.4|^2.0", "nyholm/psr7": "^1.0", "php-http/httplug": "^1.0|^2.0", "psr/http-client": "^1.0", - "symfony/cache": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/messenger": "^7.4|^8.0", - "symfony/process": "^7.4|^8.0", - "symfony/rate-limiter": "^7.4|^8.0", - "symfony/stopwatch": "^7.4|^8.0" + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -2941,7 +2946,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v8.0.5" + "source": "https://github.com/symfony/http-client/tree/v7.4.5" }, "funding": [ { @@ -2961,7 +2966,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T16:18:07+00:00" + "time": "2026-01-27T16:16:02+00:00" }, { "name": "symfony/http-client-contracts", @@ -3041,6 +3046,86 @@ ], "time": "2025-04-29T11:18:49+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.1", @@ -3188,5 +3273,8 @@ "php": ">=8.2.0" }, "platform-dev": {}, + "platform-overrides": { + "php": "8.2" + }, "plugin-api-version": "2.9.0" } From eb81787a52335b15e34ffafb2cfdb6fd60098d67 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:22 +0100 Subject: [PATCH 10/17] Added error handling and parsing --- phpstan.neon | 8 +- src/Client.php | 5 + src/Endpoints/Orders.php | 59 ++- src/Exceptions/SendcloudRequestException.php | 85 ++++ src/Factory/ClientFactory.php | 4 +- src/Models/ModelInterface.php | 3 + src/Utils/DateUtils.php | 9 +- tests/Endpoints/OrdersTest.php | 118 +++++- tests/Endpoints/errors/400.json | 12 + tests/Endpoints/errors/401.json | 12 + tests/Endpoints/orders.json | 410 +++++++++---------- 11 files changed, 483 insertions(+), 242 deletions(-) create mode 100644 src/Exceptions/SendcloudRequestException.php create mode 100644 tests/Endpoints/errors/400.json create mode 100644 tests/Endpoints/errors/401.json diff --git a/phpstan.neon b/phpstan.neon index e05e39d..4ba9d80 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,5 @@ parameters: - level: 6 - paths: - - src - - tests \ No newline at end of file + level: 6 + paths: + - src + - tests \ No newline at end of file diff --git a/src/Client.php b/src/Client.php index 086f30b..32610fc 100644 --- a/src/Client.php +++ b/src/Client.php @@ -9,6 +9,7 @@ use Http\Client\HttpClient; use Http\Discovery\HttpClientDiscovery; use Http\Discovery\Psr17FactoryDiscovery; +use InvalidArgumentException; class Client { @@ -16,6 +17,10 @@ class Client protected HttpMethodsClient $client; + /** + * @throws \Http\Discovery\Exception\NotFoundException + * @throws InvalidArgumentException + */ public function __construct( protected string $publicKey, protected string $secretKey, diff --git a/src/Endpoints/Orders.php b/src/Endpoints/Orders.php index 8eb4cc8..2b90016 100644 --- a/src/Endpoints/Orders.php +++ b/src/Endpoints/Orders.php @@ -3,8 +3,11 @@ namespace AlexisPPLIN\SendcloudV3\Endpoints; use AlexisPPLIN\SendcloudV3\Client; +use AlexisPPLIN\SendcloudV3\Exceptions\SendcloudRequestException; use AlexisPPLIN\SendcloudV3\Models\Order\Order; +use Exception; use Http\Discovery\Psr17FactoryDiscovery; +use Throwable; class Orders extends Client { @@ -13,13 +16,22 @@ class Orders extends Client * Find a specific order by its order ID. * * @see https://sendcloud.dev/api/v3/orders/retrieve-an-order + * + * @throws SendcloudRequestException */ public function getOrder(int $id): Order { - $response = $this->client->get('/orders/' . $id); - $body = $response->getBody()->getContents(); + try { + $response = $this->client->get('/orders/' . $id); + + SendcloudRequestException::fromResponse($response); + + $body = $response->getBody()->getContents(); - return Order::fromData(json_decode($body, true)['data']); + return Order::fromData(json_decode($body, true)['data']); + } catch (Throwable $e) { + SendcloudRequestException::fromException($e); + } } /** @@ -63,6 +75,8 @@ public function getOrder(int $id): Order * Example: * "cj0xJnA9MzAw" * @return array + * + * @throws SendcloudRequestException */ public function getOrders( ?array $integration = null, @@ -137,21 +151,26 @@ public function getOrders( $uri = '/orders?' . http_build_query($query); - // Send request + try { + // Send request - $response = $this->client->get($uri); + $response = $this->client->get($uri); + SendcloudRequestException::fromResponse($response); - // Parse response + // Parse response - $body = $response->getBody()->getContents(); - $response = json_decode($body, true); + $body = $response->getBody()->getContents(); + $response = json_decode($body, true); - $orders = []; - foreach ($response['data'] as $order) { - $orders[] = Order::fromData($order); - } + $orders = []; + foreach ($response['data'] as $order) { + $orders[] = Order::fromData($order); + } - return $orders; + return $orders; + } catch (Throwable $e) { + SendcloudRequestException::fromException($e); + } } /** @@ -159,14 +178,22 @@ public function getOrders( * Partially update some fields of an order. * * @see https://sendcloud.dev/api/v3/orders/update-an-order + * + * @throws SendcloudRequestException */ public function updateOrder( int $id, Order $order ): void { - $body = json_encode($order); - $response = $this->client->patch('/orders/' . $id, [], $body); + try { + $body = json_encode($order); + $response = $this->client->patch('/orders/' . $id, [], $body); - $body = $response->getBody()->getContents(); + SendcloudRequestException::fromResponse($response); + + $body = $response->getBody()->getContents(); + } catch (Throwable $e) { + SendcloudRequestException::fromException($e); + } } } \ No newline at end of file diff --git a/src/Exceptions/SendcloudRequestException.php b/src/Exceptions/SendcloudRequestException.php new file mode 100644 index 0000000..8296237 --- /dev/null +++ b/src/Exceptions/SendcloudRequestException.php @@ -0,0 +1,85 @@ + $sendcloudSource + */ + public function __construct( + ?string $message = null, + ?int $code = null, + ?Throwable $previous = null, + protected ?string $sendcloudCode = null, + protected ?array $sendcloudSource = null, + ) { + $message = $message ?? ''; + $code = $code ?? SendcloudRequestException::CODE_UNKNOWN; + + parent::__construct($message, $code, $previous); + } + + /** + * Checks response for errors code and throws exception if needed + * + * @throws SendcloudRequestException + */ + public static function fromResponse(ResponseInterface $response) : void + { + $code = null; + if ($response->getStatusCode() === 401) { + $code = self::CODE_AUTHENTIFICATION_FAILED; + } else if ($response->getStatusCode() === 400) { + $code = self::CODE_INVALID; + } else { + return; + } + + try { + $result = (string) $response->getBody()->getContents(); + } catch (HttpException|RuntimeException $e) { + self::fromException($e); + } + + $responseData = json_decode($result, true); + $sc_code = $responseData['errors'][0]['code'] ?? null; + $detail = $responseData['errors'][0]['detail'] ?? null; + $source = $responseData['errors'][0]['source'] ?? null; + + throw new self($detail, $code, null, $sc_code, $source); + } + + /** + * Build custom exception from others exceptions + * + * @throws SendcloudRequestException + */ + public static function fromException(Throwable $exception) : never + { + $code = null; + $message = null; + + if ($exception instanceof self) { + throw $exception; + } + + if ($exception instanceof \Http\Client\Exception) { + $message = 'Could not contact Sendcloud API.'; + $code = self::CODE_CONNECTION_FAILED; + } + + throw new self($message, $code, $exception); + } +} \ No newline at end of file diff --git a/src/Factory/ClientFactory.php b/src/Factory/ClientFactory.php index fdbaf50..14669d7 100644 --- a/src/Factory/ClientFactory.php +++ b/src/Factory/ClientFactory.php @@ -12,11 +12,14 @@ use Http\Discovery\Psr17FactoryDiscovery; use Http\Discovery\Psr18ClientDiscovery; use Http\Message\Authentication\BasicAuth; +use InvalidArgumentException; class ClientFactory { /** * @param array $plugins + * @throws \Http\Discovery\Exception\NotFoundException + * @throws InvalidArgumentException */ public static function create( string $base_uri, @@ -29,7 +32,6 @@ public static function create( if (!$client) { $client = Psr18ClientDiscovery::find(); } - $plugins[] = new ErrorPlugin(); // Basic auth diff --git a/src/Models/ModelInterface.php b/src/Models/ModelInterface.php index e2d0243..449d0bc 100644 --- a/src/Models/ModelInterface.php +++ b/src/Models/ModelInterface.php @@ -2,6 +2,7 @@ namespace AlexisPPLIN\SendcloudV3\Models; +use AlexisPPLIN\SendcloudV3\Exceptions\DateParsingException; use AlexisPPLIN\SendcloudV3\Exceptions\ModelFromDataException; use JsonSerializable; @@ -10,11 +11,13 @@ interface ModelInterface extends JsonSerializable /** * @param array $data * @throws ModelFromDataException + * @throws DateParsingException */ public static function fromData(array $data) : self; /** * @return array + * @throws DateParsingException */ public function jsonSerialize() : array; } \ No newline at end of file diff --git a/src/Utils/DateUtils.php b/src/Utils/DateUtils.php index a649666..05a14c6 100644 --- a/src/Utils/DateUtils.php +++ b/src/Utils/DateUtils.php @@ -4,7 +4,7 @@ use AlexisPPLIN\SendcloudV3\Exceptions\DateParsingException; use DateTimeImmutable; -use DateTimeInterface; +use ValueError; class DateUtils { @@ -16,7 +16,12 @@ class DateUtils */ public static function iso8601ToDateTime(string $iso8601) : DateTimeImmutable { - $date = DateTimeImmutable::createFromFormat(self::DATE_FORMAT, $iso8601); + try { + $date = DateTimeImmutable::createFromFormat(self::DATE_FORMAT, $iso8601); + } catch (ValueError $e) { + throw new DateParsingException("Error when parsing ISO 8601 date ({$iso8601})", 0, $e); + } + if (!$date) { throw new DateParsingException("Error when parsing ISO 8601 date ({$iso8601})"); } diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php index dfbfd48..3eda794 100644 --- a/tests/Endpoints/OrdersTest.php +++ b/tests/Endpoints/OrdersTest.php @@ -5,6 +5,8 @@ namespace Test\AlexisPPLIN\SendcloudV3; use AlexisPPLIN\SendcloudV3\Endpoints\Orders; +use AlexisPPLIN\SendcloudV3\Exceptions\DateParsingException; +use AlexisPPLIN\SendcloudV3\Exceptions\SendcloudRequestException; use AlexisPPLIN\SendcloudV3\Factory\ClientFactory; use AlexisPPLIN\SendcloudV3\Models\Address; use AlexisPPLIN\SendcloudV3\Models\Customer\CustomerDetails; @@ -30,6 +32,7 @@ use AlexisPPLIN\SendcloudV3\Models\Tax\TaxNumbers; use AlexisPPLIN\SendcloudV3\Utils\DateUtils; use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; +use InvalidArgumentException; use Nyholm\Psr7\Response; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; @@ -64,33 +67,60 @@ #[UsesClass(ClientFactory::class)] #[UsesClass(JsonUtils::class)] #[UsesClass(DateUtils::class)] +#[UsesClass(SendcloudRequestException::class)] class OrdersTest extends TestCase { - private Orders $endpoint; - - private string $json; private Order $order; - public function setUp(): void + /** + * @throws \Http\Discovery\Exception\NotFoundException + * @throws InvalidArgumentException + */ + private function getJson(bool $one_order) : string + { + $json = file_get_contents(__DIR__ . '/orders.json'); + + if ($one_order) { + $json = <<addResponse(new Response(status: $status, body: $body)); + $publicKey = '123456'; $secretKey = 'abcdef'; $partnerId = '1'; $apiBaseUrl = 'https://api.example.com/v3'; - $this->json = file_get_contents(__DIR__ . '/orders.json'); - - $client = new Client(); - $client->addResponse(new Response(body: $this->json)); - - $this->endpoint = new Orders( + return new Orders( $publicKey, $secretKey, $partnerId, $apiBaseUrl, $client ); + } + /** + * @throws DateParsingException + */ + public function setUp(): void + { $this->order = new Order( id: '752417284', order_id: '7bdd5bfd-76bc-4654-9d40-5d5d49f1cd6c', @@ -323,9 +353,12 @@ public function testGetOrder() : void $order_id = 1; $expected = $this->order; + $json = $this->getJson(true); + $endpoint = $this->getEndpoint($json, 200); + // -- Act - $actual = $this->endpoint->getOrder($order_id); + $actual = $endpoint->getOrder($order_id); // -- Assert @@ -333,11 +366,25 @@ public function testGetOrder() : void $this->assertEquals($expected, $actual); } + public function testGetOrderException() : void + { + // -- Arrange + + $json = file_get_contents(__DIR__ . '/errors/400.json'); + $endpoint = $this->getEndpoint($json, 400); + + // -- Act & Assert + + $this->expectException(SendcloudRequestException::class); + + $endpoint->getOrder(1); + } + public function testOrderJson() : void { // -- Arrange - $expected = $this->json; + $json = $this->getJson(true); // -- Act @@ -345,6 +392,51 @@ public function testOrderJson() : void // -- Assert - $this->assertJsonStringEqualsJsonString($expected, $actual); + $this->assertJsonStringEqualsJsonString($json, $actual); + } + + public function testGetOrders() : void + { + // -- Arrange + + $json = $this->getJson(false); + $endpoint = $this->getEndpoint($json, 200); + $expected = [$this->order]; + + // -- Act + + $actual = $endpoint->getOrders( + integration: [1], + order_number: '1', + order_id: '1', + status: '1', + order_created_at: '1', + order_created_at_min: '1', + order_created_at_max: '1', + order_updated_at: '1', + order_updated_at_min: '1', + order_updated_at_max: '1', + sort: '1', + page_size: 1, + cursor: '1', + ); + + // -- Assert + + $this->assertEquals($expected, $actual); + } + + public function testGetOrdersException() : void + { + // -- Arrange + + $json = file_get_contents(__DIR__ . '/errors/400.json'); + $endpoint = $this->getEndpoint($json, 400); + + // -- Act & Assert + + $this->expectException(SendcloudRequestException::class); + + $endpoint->getOrders(); } } \ No newline at end of file diff --git a/tests/Endpoints/errors/400.json b/tests/Endpoints/errors/400.json new file mode 100644 index 0000000..13f889e --- /dev/null +++ b/tests/Endpoints/errors/400.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "detail": "String should have at least 3 characters", + "status": "400", + "source": { + "pointer": "/data/attributes/[0]/order_details/order_items/0/total_price/currency" + }, + "code": "invalid" + } + ] +} \ No newline at end of file diff --git a/tests/Endpoints/errors/401.json b/tests/Endpoints/errors/401.json new file mode 100644 index 0000000..e8a2553 --- /dev/null +++ b/tests/Endpoints/errors/401.json @@ -0,0 +1,12 @@ +{ + "errors": [ + { + "detail": "Invalid username/password.", + "status": "401", + "source": { + "pointer": "/data" + }, + "code": "authentication_failed" + } + ] +} \ No newline at end of file diff --git a/tests/Endpoints/orders.json b/tests/Endpoints/orders.json index 585cfe8..d3d6f4c 100644 --- a/tests/Endpoints/orders.json +++ b/tests/Endpoints/orders.json @@ -1,225 +1,223 @@ { - "data": { - "id": "752417284", - "order_id": "7bdd5bfd-76bc-4654-9d40-5d5d49f1cd6c", - "order_number": "101170081", - "created_at": "2026-02-09T16:00:17.040454+00:00", - "modified_at": "2026-02-09T16:00:17.111586+00:00", - "order_details": { - "integration": { - "id": 1 - }, - "status": { - "code": "fulfilled", - "message": "Order has been fulfilled" - }, - "order_created_at": "2026-02-09T10:00:00.556000+00:00", - "order_updated_at": "2018-02-27T10:00:00.555309+00:00", - "order_items": [ - { - "item_id": "5552", - "name": "Cylinder candle", - "description": "Pebble green - 12x8 cm", - "product_id": "1458734634", - "variant_id": "15346645", - "image_url": "https://i.ibb.co/6tLZ2Jz/orange.jpeg", - "measurement": { - "dimension": { - "length": 15.0, - "width": 20.5, - "height": 37.0, - "unit": "cm" - }, - "weight": { - "value": 14.5, - "unit": "kg" - }, - "volume": { - "value": 5, - "unit": "l" - } - }, - "quantity": 1, - "sku": "WW-DR-GR-XS-001", - "hs_code": "6205.20", - "ean": "0799439112766", - "properties": { - "size": "red", - "color": "green" - }, - "country_of_origin": "NL", - "total_price": { - "value": 3.5, - "currency": "EUR" - }, - "unit_price": { - "value": 3.5, - "currency": "EUR" + "id": "752417284", + "order_id": "7bdd5bfd-76bc-4654-9d40-5d5d49f1cd6c", + "order_number": "101170081", + "created_at": "2026-02-09T16:00:17.040454+00:00", + "modified_at": "2026-02-09T16:00:17.111586+00:00", + "order_details": { + "integration": { + "id": 1 + }, + "status": { + "code": "fulfilled", + "message": "Order has been fulfilled" + }, + "order_created_at": "2026-02-09T10:00:00.556000+00:00", + "order_updated_at": "2018-02-27T10:00:00.555309+00:00", + "order_items": [ + { + "item_id": "5552", + "name": "Cylinder candle", + "description": "Pebble green - 12x8 cm", + "product_id": "1458734634", + "variant_id": "15346645", + "image_url": "https://i.ibb.co/6tLZ2Jz/orange.jpeg", + "measurement": { + "dimension": { + "length": 15.0, + "width": 20.5, + "height": 37.0, + "unit": "cm" }, - "delivery_dates": { - "handover_at": "2025-02-27T10:00:00.555309+00:00", - "deliver_at": "2025-03-15T10:00:00.555309+00:00" + "weight": { + "value": 14.5, + "unit": "kg" }, - "mid_code": "NLOZR92MEL", - "material_content": "100% Cotton", - "intended_use": "Personal use", - "dangerous_goods": { - "chemical_record_identifier": "1", - "regulation_set": "IATA", - "packaging_type_quantity": "type", - "id_number": "123456", - "class_division_number": "42", - "quantity": "1", - "unit_of_measurement": "kg", - "proper_shipping_name": "product", - "commodity_regulated_level_code": "LQ", - "transportation_mode": "Highway", - "emergency_contact_name": "John Doe", - "emergency_contact_phone": "+319881729999", - "adr_packing_group_letter": "I", - "local_proper_shipping_name": "shipping", - "transport_category": "transport", - "tunnel_restriction_code": "tunnel", - "weight_type": "net" + "volume": { + "value": 5, + "unit": "l" } + }, + "quantity": 1, + "sku": "WW-DR-GR-XS-001", + "hs_code": "6205.20", + "ean": "0799439112766", + "properties": { + "size": "red", + "color": "green" + }, + "country_of_origin": "NL", + "total_price": { + "value": 3.5, + "currency": "EUR" + }, + "unit_price": { + "value": 3.5, + "currency": "EUR" + }, + "delivery_dates": { + "handover_at": "2025-02-27T10:00:00.555309+00:00", + "deliver_at": "2025-03-15T10:00:00.555309+00:00" + }, + "mid_code": "NLOZR92MEL", + "material_content": "100% Cotton", + "intended_use": "Personal use", + "dangerous_goods": { + "chemical_record_identifier": "1", + "regulation_set": "IATA", + "packaging_type_quantity": "type", + "id_number": "123456", + "class_division_number": "42", + "quantity": "1", + "unit_of_measurement": "kg", + "proper_shipping_name": "product", + "commodity_regulated_level_code": "LQ", + "transportation_mode": "Highway", + "emergency_contact_name": "John Doe", + "emergency_contact_phone": "+319881729999", + "adr_packing_group_letter": "I", + "local_proper_shipping_name": "shipping", + "transport_category": "transport", + "tunnel_restriction_code": "tunnel", + "weight_type": "net" } - ], - "notes": "Call this number before delivery: 063 874 6473", - "tags": [ - "fragile", - "countryside warehouse" - ] + } + ], + "notes": "Call this number before delivery: 063 874 6473", + "tags": [ + "fragile", + "countryside warehouse" + ] + }, + "payment_details": { + "subtotal_price": { + "value": 3.5, + "currency": "EUR" }, - "payment_details": { - "subtotal_price": { - "value": 3.5, - "currency": "EUR" - }, - "estimated_shipping_price": { - "value": 3.5, - "currency": "EUR" - }, - "estimated_tax_price": { - "value": 3.5, - "currency": "EUR" - }, - "total_price": { - "value": 3.5, - "currency": "EUR" - }, - "status": { - "code": "paid", - "message": "Paid" - }, - "invoice_date": "2018-07-14", - "discount_granted": { - "value": 3.5, - "currency": "EUR" - }, - "insurance_costs": { - "value": 3.5, - "currency": "EUR" - }, - "freight_costs": { - "value": 3.5, - "currency": "EUR" - }, - "other_costs": { - "value": 3.5, - "currency": "EUR" - }, - "is_cash_on_delivery": true + "estimated_shipping_price": { + "value": 3.5, + "currency": "EUR" }, - "customs_details": { - "commercial_invoice_number": "1002404102022", - "shipment_type": "commercial_goods", - "export_type": "private", - "tax_numbers": { - "sender": [ - { - "name": "VAT", - "country_code": "NL", - "value": "NL987654321B02" - } - ], - "receiver": [ - { - "name": "VAT", - "country_code": "NL", - "value": "NL987654321B02" - } - ], - "importer_of_record": [ - { - "name": "VAT", - "country_code": "NL", - "value": "NL987654321B02" - } - ] - } + "estimated_tax_price": { + "value": 3.5, + "currency": "EUR" }, - "customer_details": { - "name": "John Doe", - "phone_number": "+319881729999", - "email": "john@doe.com" + "total_price": { + "value": 3.5, + "currency": "EUR" }, - "billing_address": { - "name": "John Doe", - "address_line_1": "Lansdown Glade", - "house_number": "15", - "address_line_2": "a", - "postal_code": "5341XT", - "city": "Oss", - "country_code": "NL", - "email": "johndoe@gmail.com", - "phone_number": "+319881729999" + "status": { + "code": "paid", + "message": "Paid" }, - "shipping_address": { - "name": "John Doe", - "address_line_1": "Lansdown Glade", - "house_number": "15", - "address_line_2": "a", - "postal_code": "5341XT", - "city": "Oss", - "country_code": "NL", - "email": "johndoe@gmail.com", - "phone_number": "+319881729999" + "invoice_date": "2018-07-14", + "discount_granted": { + "value": 3.5, + "currency": "EUR" }, - "shipping_details": { - "is_local_pickup": true, - "delivery_indicator": "DHL Home Delivery", - "measurement": { - "dimension": { - "length": 15.0, - "width": 20.5, - "height": 37.0, - "unit": "cm" - }, - "weight": { - "value": 14.5, - "unit": "kg" - }, - "volume": { - "value": 5, - "unit": "l" + "insurance_costs": { + "value": 3.5, + "currency": "EUR" + }, + "freight_costs": { + "value": 3.5, + "currency": "EUR" + }, + "other_costs": { + "value": 3.5, + "currency": "EUR" + }, + "is_cash_on_delivery": true + }, + "customs_details": { + "commercial_invoice_number": "1002404102022", + "shipment_type": "commercial_goods", + "export_type": "private", + "tax_numbers": { + "sender": [ + { + "name": "VAT", + "country_code": "NL", + "value": "NL987654321B02" } - }, - "ship_with": { - "type": "shipping_option_code", - "properties": { - "shipping_option_code": "postnl:standard" + ], + "receiver": [ + { + "name": "VAT", + "country_code": "NL", + "value": "NL987654321B02" + } + ], + "importer_of_record": [ + { + "name": "VAT", + "country_code": "NL", + "value": "NL987654321B02" } + ] + } + }, + "customer_details": { + "name": "John Doe", + "phone_number": "+319881729999", + "email": "john@doe.com" + }, + "billing_address": { + "name": "John Doe", + "address_line_1": "Lansdown Glade", + "house_number": "15", + "address_line_2": "a", + "postal_code": "5341XT", + "city": "Oss", + "country_code": "NL", + "email": "johndoe@gmail.com", + "phone_number": "+319881729999" + }, + "shipping_address": { + "name": "John Doe", + "address_line_1": "Lansdown Glade", + "house_number": "15", + "address_line_2": "a", + "postal_code": "5341XT", + "city": "Oss", + "country_code": "NL", + "email": "johndoe@gmail.com", + "phone_number": "+319881729999" + }, + "shipping_details": { + "is_local_pickup": true, + "delivery_indicator": "DHL Home Delivery", + "measurement": { + "dimension": { + "length": 15.0, + "width": 20.5, + "height": 37.0, + "unit": "cm" + }, + "weight": { + "value": 14.5, + "unit": "kg" + }, + "volume": { + "value": 5, + "unit": "l" } }, - "service_point_details": { - "id": "123", - "post_number": "some-post-number", - "latitude": "51.427063", - "longitude": "5.486414", - "type": "packstation", - "extra_data": { - "test": "test" + "ship_with": { + "type": "shipping_option_code", + "properties": { + "shipping_option_code": "postnl:standard" } } + }, + "service_point_details": { + "id": "123", + "post_number": "some-post-number", + "latitude": "51.427063", + "longitude": "5.486414", + "type": "packstation", + "extra_data": { + "test": "test" + } } } \ No newline at end of file From d57e74463fe05baac73ed518eaee38db6f210875 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:22 +0100 Subject: [PATCH 11/17] Added testing for all methods of Orders client --- src/Client.php | 3 +- src/Endpoints/Orders.php | 67 +++++++++++- src/Models/Order/Order.php | 2 +- src/Models/Order/OrderResponse.php | 0 tests/Endpoints/OrdersTest.php | 159 +++++++++++++++++++++++++++-- 5 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 src/Models/Order/OrderResponse.php diff --git a/src/Client.php b/src/Client.php index 32610fc..b6df1bc 100644 --- a/src/Client.php +++ b/src/Client.php @@ -39,7 +39,8 @@ public function __construct( $this->client = new HttpMethodsClient( $client, - Psr17FactoryDiscovery::findRequestFactory() + Psr17FactoryDiscovery::findRequestFactory(), + Psr17FactoryDiscovery::findStreamFactory() ); } } diff --git a/src/Endpoints/Orders.php b/src/Endpoints/Orders.php index 2b90016..4b316cb 100644 --- a/src/Endpoints/Orders.php +++ b/src/Endpoints/Orders.php @@ -7,6 +7,7 @@ use AlexisPPLIN\SendcloudV3\Models\Order\Order; use Exception; use Http\Discovery\Psr17FactoryDiscovery; +use InvalidArgumentException; use Throwable; class Orders extends Client @@ -179,19 +180,79 @@ public function getOrders( * * @see https://sendcloud.dev/api/v3/orders/update-an-order * + * @return int Sendcloud order ID * @throws SendcloudRequestException + * @throws InvalidArgumentException */ public function updateOrder( - int $id, Order $order - ): void { + ): int { + if (!isset($order->id)) { + throw new InvalidArgumentException('Order id is null'); + } + try { $body = json_encode($order); - $response = $this->client->patch('/orders/' . $id, [], $body); + $response = $this->client->patch('/orders/' . $order->id, [], $body); SendcloudRequestException::fromResponse($response); $body = $response->getBody()->getContents(); + $json = json_decode($body, true); + + return (int) $json['data']['id']; + } catch (Throwable $e) { + SendcloudRequestException::fromException($e); + } + } + + /** + * Create/Update orders in batch + * Use this endpoint to insert orders into a Sendcloud API integration. + * + * @see https://sendcloud.dev/api/v3/orders/create-update-orders-in-batch + * + * @param array $orders + * @return array Sendcloud orders IDs + * + * @throws SendcloudRequestException + */ + public function createOrder( + array $orders + ) : array { + try { + $body = json_encode($orders); + $response = $this->client->post('/orders', [], $body); + + SendcloudRequestException::fromResponse($response); + + $body = $response->getBody()->getContents(); + $json = json_decode($body, true); + + $ids = array_column($json['data'], 'id'); + $ids = array_map('intval', $ids); + + return $ids; + } catch (Throwable $e) { + SendcloudRequestException::fromException($e); + } + } + + /** + * Delete an order + * Delete an order by its unique id. + * + * @see https://sendcloud.dev/api/v3/orders/delete-an-order + * + * @throws SendcloudRequestException + */ + public function deleteOrder( + int $id + ): void { + try { + $response = $this->client->post('/orders/' . $id); + + SendcloudRequestException::fromResponse($response); } catch (Throwable $e) { SendcloudRequestException::fromException($e); } diff --git a/src/Models/Order/Order.php b/src/Models/Order/Order.php index 850657e..684b91a 100644 --- a/src/Models/Order/Order.php +++ b/src/Models/Order/Order.php @@ -89,6 +89,6 @@ public function jsonSerialize() : array JsonUtils::addIfNotNull($json, 'shipping_details', $this->shipping_details); JsonUtils::addIfNotNull($json, 'service_point_details', $this->service_point_details); - return ['data' => $json]; + return $json; } } \ No newline at end of file diff --git a/src/Models/Order/OrderResponse.php b/src/Models/Order/OrderResponse.php new file mode 100644 index 0000000..e69de29 diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php index 3eda794..39fd04c 100644 --- a/tests/Endpoints/OrdersTest.php +++ b/tests/Endpoints/OrdersTest.php @@ -116,13 +116,10 @@ public function getEndpoint(string $body, int $status = 200) : Orders ); } - /** - * @throws DateParsingException - */ - public function setUp(): void + private function generateOrder(bool $with_id = true) : Order { - $this->order = new Order( - id: '752417284', + return new Order( + id: $with_id ? '752417284' : null, order_id: '7bdd5bfd-76bc-4654-9d40-5d5d49f1cd6c', order_number: '101170081', created_at: DateUtils::iso8601ToDateTime('2026-02-09T16:00:17.040454+00:00'), @@ -346,6 +343,16 @@ class_division_number: '42', ); } + /** + * @throws DateParsingException + */ + public function setUp(): void + { + $this->order = $this->generateOrder(); + } + + /* getOrder */ + public function testGetOrder() : void { // -- Arrange @@ -388,13 +395,15 @@ public function testOrderJson() : void // -- Act - $actual = json_encode($this->order); + $actual = json_encode(['data' => $this->order]); // -- Assert $this->assertJsonStringEqualsJsonString($json, $actual); } + /* getOrders */ + public function testGetOrders() : void { // -- Arrange @@ -439,4 +448,140 @@ public function testGetOrdersException() : void $endpoint->getOrders(); } + + /* updateOrder */ + + public function testUpdateOrder() : void + { + // -- Arrange + + $expected = 669; + $json = <<getEndpoint($json, 200); + + // -- Act + + $result = $endpoint->updateOrder($this->order); + + // -- Assert + + $this->assertEquals($expected, $result); + } + + public function testUpdateOrderException() : void + { + // -- Arrange + + $json = file_get_contents(__DIR__ . '/errors/400.json'); + $endpoint = $this->getEndpoint($json, 400); + + // -- Act & Assert + + $this->expectException(SendcloudRequestException::class); + + $endpoint->updateOrder($this->order); + } + + public function testUpdateOrderWithNullId() : void + { + // -- Arrange + + $json = file_get_contents(__DIR__ . '/errors/400.json'); + $endpoint = $this->getEndpoint($json, 400); + + $order = $this->generateOrder(false); + + // -- Act & Assert + + $this->expectException(InvalidArgumentException::class); + + $endpoint->updateOrder($order); + } + + /* createOrder */ + + public function testCreateOrder() : void + { + // -- Arrange + + $expected = [669]; + $json = <<getEndpoint($json, 200); + + // -- Act + + $result = $endpoint->createOrder([ + $this->order + ]); + + // -- Assert + + $this->assertEquals($expected, $result); + } + + public function testCreateOrderException() : void + { + // -- Arrange + + $json = file_get_contents(__DIR__ . '/errors/400.json'); + $endpoint = $this->getEndpoint($json, 400); + + // -- Act & Assert + + $this->expectException(SendcloudRequestException::class); + + $endpoint->createOrder([ + $this->order + ]); + } + + /* deleteOrder */ + + public function testDeleteOrder() : void + { + // -- Arrange + + $order_id = 1; + $endpoint = $this->getEndpoint('', 200); + + // -- Act & Assert + + $endpoint->deleteOrder($order_id); + + $this->expectNotToPerformAssertions(); + } + + public function testDeleteOrderException() : void + { + // -- Arrange + + $order_id = 1; + + $json = file_get_contents(__DIR__ . '/errors/400.json'); + $endpoint = $this->getEndpoint($json, 400); + + // -- Act & Assert + + $this->expectException(SendcloudRequestException::class); + + $endpoint->deleteOrder($order_id); + } } \ No newline at end of file From 3a0f26bc14611b447cb454d088627ce1c445a778 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:22 +0100 Subject: [PATCH 12/17] Finished tests for full coverage --- src/Exceptions/SendcloudRequestException.php | 9 +- .../SendcloudRequestExceptionTest.php | 170 ++++++++++++++++++ tests/Utils/DateUtilsTest.php | 76 ++++++++ tests/Utils/JsonUtilsTest.php | 46 +++++ 4 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 tests/Exceptions/SendcloudRequestExceptionTest.php create mode 100644 tests/Utils/DateUtilsTest.php create mode 100644 tests/Utils/JsonUtilsTest.php diff --git a/src/Exceptions/SendcloudRequestException.php b/src/Exceptions/SendcloudRequestException.php index 8296237..2d827c8 100644 --- a/src/Exceptions/SendcloudRequestException.php +++ b/src/Exceptions/SendcloudRequestException.php @@ -38,13 +38,16 @@ public function __construct( */ public static function fromResponse(ResponseInterface $response) : void { - $code = null; + if ($response->getStatusCode() === 200) { + return; + } + + $code = self::CODE_UNKNOWN; + if ($response->getStatusCode() === 401) { $code = self::CODE_AUTHENTIFICATION_FAILED; } else if ($response->getStatusCode() === 400) { $code = self::CODE_INVALID; - } else { - return; } try { diff --git a/tests/Exceptions/SendcloudRequestExceptionTest.php b/tests/Exceptions/SendcloudRequestExceptionTest.php new file mode 100644 index 0000000..7300f0a --- /dev/null +++ b/tests/Exceptions/SendcloudRequestExceptionTest.php @@ -0,0 +1,170 @@ +createMock(ResponseInterface::class); + $response->method('getStatusCode') + ->willReturn(200); + + // -- Act + + $this->expectNotToPerformAssertions(); + SendcloudRequestException::fromResponse($response); + } + + public function testFromResponse401() : void + { + // -- Arrange + + $json = <<createMock(ResponseInterface::class); + $response->method('getStatusCode') + ->willReturn(401); + + $stream = $this->createMock(StreamInterface::class); + $stream->method('getContents') + ->willReturn($json); + + $response->method('getBody') + ->willReturn($stream); + + $expected = new SendcloudRequestException( + message: 'Invalid username/password.', + code: SendcloudRequestException::CODE_AUTHENTIFICATION_FAILED, + sendcloudCode: 'authentication_failed', + sendcloudSource: ['pointer' => '/data'] + ); + + // -- Act & Assert + + $this->expectExceptionObject($expected); + SendcloudRequestException::fromResponse($response); + } + + public function testFromResponse400() : void + { + // -- Arrange + + $json = <<createMock(ResponseInterface::class); + $response->method('getStatusCode') + ->willReturn(400); + + $stream = $this->createMock(StreamInterface::class); + $stream->method('getContents') + ->willReturn($json); + + $response->method('getBody') + ->willReturn($stream); + + $expected = new SendcloudRequestException( + message: 'String should have at least 3 characters', + code: SendcloudRequestException::CODE_INVALID, + sendcloudCode: 'invalid', + sendcloudSource: ['pointer' => '/data/attributes/[0]/order_details/order_items/0/total_price/currency'] + ); + + // -- Act & Assert + + $this->expectExceptionObject($expected); + SendcloudRequestException::fromResponse($response); + } + + public function testFromResponseHttpOrRuntimeException() : void + { + // -- Arrange + + $response = $this->createMock(ResponseInterface::class); + $response->method('getStatusCode') + ->willReturn(400); + + $response->method('getBody') + ->willThrowException(new RuntimeException()); + + $expected = new SendcloudRequestException( + code: SendcloudRequestException::CODE_UNKNOWN + ); + + // -- Act & Assert + + $this->expectExceptionObject($expected); + SendcloudRequestException::fromResponse($response); + } + + /* fromException */ + + public function testFromExceptionSelf() : void + { + // -- Arrange + + $exception = new SendcloudRequestException(); + + // -- Act & Assert + + $this->expectExceptionObject($exception); + SendcloudRequestException::fromException($exception); + } + + public function testFromExceptionHttpClient() : void + { + // -- Arrange + + $exception = $this->createMock(\Http\Client\Exception::class); + $expected = new SendcloudRequestException( + message: 'Could not contact Sendcloud API.', + code: SendcloudRequestException::CODE_CONNECTION_FAILED + ); + + // -- Act & Assert + + $this->expectExceptionObject($expected); + SendcloudRequestException::fromException($exception); + } +} \ No newline at end of file diff --git a/tests/Utils/DateUtilsTest.php b/tests/Utils/DateUtilsTest.php new file mode 100644 index 0000000..1b7514f --- /dev/null +++ b/tests/Utils/DateUtilsTest.php @@ -0,0 +1,76 @@ +assertEquals($expected, $actual); + } + + public function testIso8601ToDateTimeDateParsingExceptionNullBytes() : void + { + // -- Arrange + + $iso8601 = "\0"; + + // -- Act + + $this->expectException(DateParsingException::class); + DateUtils::iso8601ToDateTime($iso8601); + } + + public function testIso8601ToDateTimeDateParsingExceptionFormatError() : void + { + // -- Arrange + + $iso8601 = "Hello world"; + + // -- Act + + $this->expectException(DateParsingException::class); + DateUtils::iso8601ToDateTime($iso8601); + } + + /* dateTimeToIso8601 */ + + public function testDateTimeToIso8601() : void + { + // -- Arrange + + $date = new DateTimeImmutable('2026-01-01 00:00:00', new DateTimeZone('+0000')); + $expected = '2026-01-01T00:00:00.000000+00:00'; + + // -- Act + + $actual = DateUtils::dateTimeToIso8601($date); + + // -- Assert + + $this->assertEquals($expected, $actual); + } +} \ No newline at end of file diff --git a/tests/Utils/JsonUtilsTest.php b/tests/Utils/JsonUtilsTest.php new file mode 100644 index 0000000..65c159d --- /dev/null +++ b/tests/Utils/JsonUtilsTest.php @@ -0,0 +1,46 @@ + 'test']; + + $key_1 = 'id'; + $value_1 = 1; + + $key_2 = 'name'; + $value_2 = null; + + $expected = [ + 'data' => 'test', + 'id' => 1 + ]; + + // -- Act + + JsonUtils::addIfNotNull($json, $key_1, $value_1); + JsonUtils::addIfNotNull($json, $key_2, $value_2); + + // -- Assert + + $this->assertEquals($expected, $json); + } +} \ No newline at end of file From 0076493ba36ae1e81b42c04d7f5cf36dc5477a11 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:22 +0100 Subject: [PATCH 13/17] Rector --- src/Endpoints/Orders.php | 24 ++++++++++--------- src/Exceptions/DateParsingException.php | 4 +++- src/Exceptions/ModelFromDataException.php | 4 +++- src/Exceptions/SendcloudRequestException.php | 9 +++++-- src/Factory/ClientFactory.php | 6 +++-- src/Models/Address.php | 4 +++- src/Models/Customer/CustomerDetails.php | 4 +++- src/Models/DangerousGoods.php | 4 +++- src/Models/Delivery/DeliveryDates.php | 4 +++- src/Models/Measurement/Measurement.php | 4 +++- .../Measurement/MeasurementDimension.php | 4 +++- src/Models/Measurement/MeasurementVolume.php | 4 +++- src/Models/Measurement/MeasurementWeight.php | 4 +++- src/Models/ModelInterface.php | 4 +++- src/Models/Order/CustomsDetails.php | 4 +++- src/Models/Order/Order.php | 4 +++- src/Models/Order/OrderDetails.php | 4 +++- src/Models/Order/OrderDetailsIntegration.php | 4 +++- src/Models/Order/OrderItems.php | 4 +++- src/Models/Order/ShipWith.php | 4 +++- src/Models/Order/ShippingDetails.php | 4 +++- src/Models/Order/ShippingOptionProperties.php | 4 +++- src/Models/PaymentDetails.php | 4 +++- src/Models/Price.php | 4 +++- src/Models/ServicePoint/ServicePoint.php | 4 +++- src/Models/Status.php | 4 +++- src/Models/Tax/TaxNumber.php | 4 +++- src/Models/Tax/TaxNumbers.php | 4 +++- src/Utils/DateUtils.php | 10 ++++---- src/Utils/JsonUtils.php | 4 +++- tests/ClientTestInstance.php | 4 +++- tests/Endpoints/OrdersTest.php | 2 +- 32 files changed, 112 insertions(+), 47 deletions(-) diff --git a/src/Endpoints/Orders.php b/src/Endpoints/Orders.php index 4b316cb..0459565 100644 --- a/src/Endpoints/Orders.php +++ b/src/Endpoints/Orders.php @@ -1,5 +1,7 @@ getBody()->getContents(); return Order::fromData(json_decode($body, true)['data']); - } catch (Throwable $e) { - SendcloudRequestException::fromException($e); + } catch (Throwable $throwable) { + SendcloudRequestException::fromException($throwable); } } @@ -169,8 +171,8 @@ public function getOrders( } return $orders; - } catch (Throwable $e) { - SendcloudRequestException::fromException($e); + } catch (Throwable $throwable) { + SendcloudRequestException::fromException($throwable); } } @@ -201,8 +203,8 @@ public function updateOrder( $json = json_decode($body, true); return (int) $json['data']['id']; - } catch (Throwable $e) { - SendcloudRequestException::fromException($e); + } catch (Throwable $throwable) { + SendcloudRequestException::fromException($throwable); } } @@ -233,8 +235,8 @@ public function createOrder( $ids = array_map('intval', $ids); return $ids; - } catch (Throwable $e) { - SendcloudRequestException::fromException($e); + } catch (Throwable $throwable) { + SendcloudRequestException::fromException($throwable); } } @@ -253,8 +255,8 @@ public function deleteOrder( $response = $this->client->post('/orders/' . $id); SendcloudRequestException::fromResponse($response); - } catch (Throwable $e) { - SendcloudRequestException::fromException($e); + } catch (Throwable $throwable) { + SendcloudRequestException::fromException($throwable); } } -} \ No newline at end of file +} diff --git a/src/Exceptions/DateParsingException.php b/src/Exceptions/DateParsingException.php index ba65e7c..1b31184 100644 --- a/src/Exceptions/DateParsingException.php +++ b/src/Exceptions/DateParsingException.php @@ -1,5 +1,7 @@ format(self::DATE_FORMAT); } -} \ No newline at end of file +} diff --git a/src/Utils/JsonUtils.php b/src/Utils/JsonUtils.php index c865771..33024f9 100644 --- a/src/Utils/JsonUtils.php +++ b/src/Utils/JsonUtils.php @@ -1,5 +1,7 @@ client; } -}; \ No newline at end of file +} diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php index 39fd04c..c595dfd 100644 --- a/tests/Endpoints/OrdersTest.php +++ b/tests/Endpoints/OrdersTest.php @@ -346,7 +346,7 @@ class_division_number: '42', /** * @throws DateParsingException */ - public function setUp(): void + protected function setUp(): void { $this->order = $this->generateOrder(); } From 52f10d271af2a2f4e0cc3ef0eca2f5587587af3b Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 15:56:22 +0100 Subject: [PATCH 14/17] Fixed imports with rector --- rector.php | 5 ++++- src/Client.php | 4 ++-- src/Endpoints/Orders.php | 2 -- src/Factory/ClientFactory.php | 4 ++-- src/Models/Customer/CustomerDetails.php | 1 - src/Models/Delivery/DeliveryDates.php | 4 ---- src/Models/Order/ShippingDetails.php | 1 - tests/Endpoints/OrdersTest.php | 3 ++- tests/Exceptions/SendcloudRequestExceptionTest.php | 4 ++-- tests/Utils/JsonUtilsTest.php | 4 ---- 10 files changed, 12 insertions(+), 20 deletions(-) diff --git a/rector.php b/rector.php index fa44f26..8c9ece4 100644 --- a/rector.php +++ b/rector.php @@ -16,4 +16,7 @@ ->withRules([ DeclareStrictTypesRector::class ]) - ->withPreparedSets(codingStyle: true); + ->withPreparedSets(codingStyle: true) + ->withImportNames( + removeUnusedImports: true + ); diff --git a/src/Client.php b/src/Client.php index b6df1bc..bf34f08 100644 --- a/src/Client.php +++ b/src/Client.php @@ -4,10 +4,10 @@ namespace AlexisPPLIN\SendcloudV3; +use Http\Discovery\Exception\NotFoundException; use AlexisPPLIN\SendcloudV3\Factory\ClientFactory; use Http\Client\Common\HttpMethodsClient; use Http\Client\HttpClient; -use Http\Discovery\HttpClientDiscovery; use Http\Discovery\Psr17FactoryDiscovery; use InvalidArgumentException; @@ -18,7 +18,7 @@ class Client protected HttpMethodsClient $client; /** - * @throws \Http\Discovery\Exception\NotFoundException + * @throws NotFoundException * @throws InvalidArgumentException */ public function __construct( diff --git a/src/Endpoints/Orders.php b/src/Endpoints/Orders.php index 0459565..acfeafa 100644 --- a/src/Endpoints/Orders.php +++ b/src/Endpoints/Orders.php @@ -7,8 +7,6 @@ use AlexisPPLIN\SendcloudV3\Client; use AlexisPPLIN\SendcloudV3\Exceptions\SendcloudRequestException; use AlexisPPLIN\SendcloudV3\Models\Order\Order; -use Exception; -use Http\Discovery\Psr17FactoryDiscovery; use InvalidArgumentException; use Throwable; diff --git a/src/Factory/ClientFactory.php b/src/Factory/ClientFactory.php index 6aac06b..537e400 100644 --- a/src/Factory/ClientFactory.php +++ b/src/Factory/ClientFactory.php @@ -4,13 +4,13 @@ namespace AlexisPPLIN\SendcloudV3\Factory; +use Http\Discovery\Exception\NotFoundException; use Http\Client\Common\Plugin\BaseUriPlugin; use Http\Client\Common\Plugin\HeaderSetPlugin; use Http\Client\Common\PluginClient; use Http\Client\HttpClient; use Http\Client\Common\Plugin; use Http\Client\Common\Plugin\AuthenticationPlugin; -use Http\Client\Common\Plugin\ErrorPlugin; use Http\Discovery\Psr17FactoryDiscovery; use Http\Discovery\Psr18ClientDiscovery; use Http\Message\Authentication\BasicAuth; @@ -20,7 +20,7 @@ class ClientFactory { /** * @param array $plugins - * @throws \Http\Discovery\Exception\NotFoundException + * @throws NotFoundException * @throws InvalidArgumentException */ public static function create( diff --git a/src/Models/Customer/CustomerDetails.php b/src/Models/Customer/CustomerDetails.php index 2ac2d6f..1a4d74b 100644 --- a/src/Models/Customer/CustomerDetails.php +++ b/src/Models/Customer/CustomerDetails.php @@ -5,7 +5,6 @@ namespace AlexisPPLIN\SendcloudV3\Models\Customer; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; -use JsonSerializable; /** * Node for an information about customer diff --git a/src/Models/Delivery/DeliveryDates.php b/src/Models/Delivery/DeliveryDates.php index 400f771..ee4f131 100644 --- a/src/Models/Delivery/DeliveryDates.php +++ b/src/Models/Delivery/DeliveryDates.php @@ -4,13 +4,9 @@ namespace AlexisPPLIN\SendcloudV3\Models\Delivery; -use AlexisPPLIN\SendcloudV3\Exceptions\ModelFromDataException; -use AlexisPPLIN\SendcloudV3\Models\AbstractModel; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; use AlexisPPLIN\SendcloudV3\Utils\DateUtils; use DateTimeImmutable; -use DateTimeInterface; -use JsonSerializable; /** * Defined delivery dates diff --git a/src/Models/Order/ShippingDetails.php b/src/Models/Order/ShippingDetails.php index 882ef24..2a37b4a 100644 --- a/src/Models/Order/ShippingDetails.php +++ b/src/Models/Order/ShippingDetails.php @@ -6,7 +6,6 @@ use AlexisPPLIN\SendcloudV3\Models\Measurement\Measurement; use AlexisPPLIN\SendcloudV3\Models\ModelInterface; -use AlexisPPLIN\SendcloudV3\Models\ServicePoint\ServicePoint; use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; /** diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php index c595dfd..3090b39 100644 --- a/tests/Endpoints/OrdersTest.php +++ b/tests/Endpoints/OrdersTest.php @@ -4,6 +4,7 @@ namespace Test\AlexisPPLIN\SendcloudV3; +use Http\Discovery\Exception\NotFoundException; use AlexisPPLIN\SendcloudV3\Endpoints\Orders; use AlexisPPLIN\SendcloudV3\Exceptions\DateParsingException; use AlexisPPLIN\SendcloudV3\Exceptions\SendcloudRequestException; @@ -73,7 +74,7 @@ class OrdersTest extends TestCase private Order $order; /** - * @throws \Http\Discovery\Exception\NotFoundException + * @throws NotFoundException * @throws InvalidArgumentException */ private function getJson(bool $one_order) : string diff --git a/tests/Exceptions/SendcloudRequestExceptionTest.php b/tests/Exceptions/SendcloudRequestExceptionTest.php index 7300f0a..7dab90c 100644 --- a/tests/Exceptions/SendcloudRequestExceptionTest.php +++ b/tests/Exceptions/SendcloudRequestExceptionTest.php @@ -4,8 +4,8 @@ namespace Test\AlexisPPLIN\SendcloudV3; +use Http\Client\Exception; use AlexisPPLIN\SendcloudV3\Exceptions\SendcloudRequestException; -use Http\Client\Exception\HttpException; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; @@ -156,7 +156,7 @@ public function testFromExceptionHttpClient() : void { // -- Arrange - $exception = $this->createMock(\Http\Client\Exception::class); + $exception = $this->createMock(Exception::class); $expected = new SendcloudRequestException( message: 'Could not contact Sendcloud API.', code: SendcloudRequestException::CODE_CONNECTION_FAILED diff --git a/tests/Utils/JsonUtilsTest.php b/tests/Utils/JsonUtilsTest.php index 65c159d..88836fe 100644 --- a/tests/Utils/JsonUtilsTest.php +++ b/tests/Utils/JsonUtilsTest.php @@ -4,11 +4,7 @@ namespace Test\AlexisPPLIN\SendcloudV3; -use AlexisPPLIN\SendcloudV3\Exceptions\DateParsingException; -use AlexisPPLIN\SendcloudV3\Utils\DateUtils; use AlexisPPLIN\SendcloudV3\Utils\JsonUtils; -use DateTimeImmutable; -use DateTimeZone; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; From 63b12e0530f1a740c70badc6522d08d01786f458 Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 16:01:51 +0100 Subject: [PATCH 15/17] Added phpstan code quality checks --- .github/workflows/code-quality.yml | 33 ++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/code-quality.yml diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..3432f2b --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,33 @@ +name: Code Quality + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Validate composer.json and composer.lock + run: composer validate --strict + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v5 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - uses: php-actions/phpstan@v3 + with: + php_version: 8.2 \ No newline at end of file From e665608b442c6ac0942a8cbfea4c1d7dbdc26afe Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 16:07:03 +0100 Subject: [PATCH 16/17] Added rector action --- .github/workflows/code-quality.yml | 40 +++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 3432f2b..4eed29d 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -7,14 +7,46 @@ on: branches: [ "main" ] jobs: - build-test: + rector: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v6 - - name: Validate composer.json and composer.lock - run: composer validate --strict + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + coverage: xdebug + + - name: Cache Composer packages + id: composer-cache + uses: actions/cache@v5 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress + + - name: Rector Cache + uses: actions/cache@v4 + with: + path: /tmp/rector + key: ${{ runner.os }}-rector-${{ github.run_id }} + restore-keys: ${{ runner.os }}-rector- + + - run: mkdir -p /tmp/rector + + - name: Rector Dry Run + run: php vendor/bin/rector process --dry-run --config=rector.php + + phpstan: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 - name: Cache Composer packages id: composer-cache From 64c8d10ab7e64d0ffb4dc88d95317462362a6c7a Mon Sep 17 00:00:00 2001 From: AlexisPPLIN Date: Tue, 10 Feb 2026 16:08:30 +0100 Subject: [PATCH 17/17] Added permission to code-quality action --- .github/workflows/code-quality.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 4eed29d..853312d 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ "main" ] +permissions: + contents: read + jobs: rector: runs-on: ubuntu-latest