diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..853312d --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,68 @@ +name: Code Quality + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + rector: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - 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 + 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 diff --git a/composer.json b/composer.json index 41781e3..6359732 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ }, "autoload-dev": { "psr-4": { - "Test\\AlexisPPLIN\\SendcloudV3\\": "test/" + "Test\\AlexisPPLIN\\SendcloudV3\\": "tests/" } }, "authors": [ @@ -31,17 +31,29 @@ ], "require": { "php": ">=8.2.0", - "psr/http-message": "^2.0", - "psr/http-factory": "^1.1" + "php-http/discovery": "^1.20", + "psr/http-client-implementation": "^1.0", + "php-http/client-common": "^2.7", + "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" + "phpunit/php-code-coverage": "^11.0", + "php-http/mock-client": "^1.6", + "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 10c97f0..137ce84 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": "75a53616a0b15f92d2a19835b3323757", "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", @@ -113,52 +555,100 @@ "source": "https://github.com/php-fig/http-message/tree/2.0" }, "time": "2023-04-04T09:54:51+00:00" - } - ], - "packages-dev": [ + }, { - "name": "guzzlehttp/psr7", - "version": "2.8.0", + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", "source": { "type": "git", - "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0", - "psr/http-factory": "^1.0", - "psr/http-message": "^1.1 || ^2.0", - "ralouphie/getallheaders": "^3.0" + "php": ">=8.1" }, - "provide": { - "psr/http-factory-implementation": "1.0", - "psr/http-message-implementation": "1.0" + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } }, - "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" + "autoload": { + "files": [ + "function.php" + ] }, - "suggest": { - "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + "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" }, - "type": "library", - "extra": { - "bamarni-bin": { - "bin-links": true, - "forward-command": false + "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": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "b38026df55197f9e39a44f3215788edf83187b80" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b38026df55197f9e39a44f3215788edf83187b80", + "reference": "b38026df55197f9e39a44f3215788edf83187b80", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" }, + "type": "library", "autoload": { "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - } + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -166,72 +656,130 @@ ], "authors": [ { - "name": "Graham Campbell", - "email": "hello@gjcampbell.co.uk", - "homepage": "https://github.com/GrahamCampbell" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" + "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/v7.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" }, { - "name": "George Mponos", - "email": "gmponos@gmail.com", - "homepage": "https://github.com/gmponos" + "url": "https://github.com/fabpot", + "type": "github" }, { - "name": "Tobias Nyholm", - "email": "tobias.nyholm@gmail.com", - "homepage": "https://github.com/Nyholm" + "url": "https://github.com/nicolas-grekas", + "type": "github" }, { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://github.com/sagikazarmark" + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-11-12T15:39:26+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": [ + "MIT" + ], + "authors": [ { - "name": "Tobias Schultze", - "email": "webmaster@tubo-world.de", - "homepage": "https://github.com/Tobion" + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" }, { - "name": "Márk Sági-Kazár", - "email": "mark.sagikazar@gmail.com", - "homepage": "https://sagikazarmark.hu" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" } ], - "description": "PSR-7 message implementation that also provides common utility methods", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", "keywords": [ - "http", - "message", - "psr-7", - "request", - "response", - "stream", - "uri", - "url" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" }, "funding": [ { - "url": "https://github.com/GrahamCampbell", + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", "type": "github" }, { - "url": "https://github.com/Nyholm", + "url": "https://github.com/nicolas-grekas", "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-08-23T21:21:41+00:00" - }, + "time": "2025-01-02T08:10:11+00:00" + } + ], + "packages-dev": [ { "name": "myclabs/deep-copy", "version": "1.13.4", @@ -350,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", @@ -466,7 +1092,69 @@ "issues": "https://github.com/phar-io/version/issues", "source": "https://github.com/phar-io/version/tree/3.2.1" }, - "time": "2022-02-21T01:04:05+00:00" + "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", @@ -870,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": { @@ -952,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": [ { @@ -976,34 +1664,88 @@ "type": "tidelift" } ], - "time": "2026-02-05T07:59:30+00:00" + "time": "2026-02-08T07:05:14+00:00" }, { - "name": "ralouphie/getallheaders", - "version": "3.0.3", + "name": "psr/container", + "version": "2.0.2", "source": { "type": "git", - "url": "https://github.com/ralouphie/getallheaders.git", - "reference": "120b605dfeb996808c31b6477290a714d356e822" + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", - "reference": "120b605dfeb996808c31b6477290a714d356e822", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", "shasum": "" }, "require": { - "php": ">=5.6" + "php": ">=7.4.0" }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.1", - "phpunit/phpunit": "^5 || ^6.5" + "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": { - "files": [ - "src/getallheaders.php" - ] + "psr-4": { + "Psr\\Log\\": "src" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1011,34 +1753,39 @@ ], "authors": [ { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" } ], - "description": "A polyfill for getallheaders.", + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" + "source": "https://github.com/php-fig/log/tree/3.0.2" }, - "time": "2019-03-08T08:55:37+00:00" + "time": "2024-09-11T13:17:53+00:00" }, { "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": "*", @@ -1072,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": [ { @@ -1080,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", @@ -2120,6 +2867,352 @@ ], "time": "2024-10-20T05:08:20+00:00" }, + { + "name": "symfony/http-client", + "version": "v7.4.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/84bb634857a893cc146cceb467e31b3f02c5fe9f", + "reference": "84bb634857a893cc146cceb467e31b3f02c5fe9f", + "shasum": "" + }, + "require": { + "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": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "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": "^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/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": { + "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/v7.4.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:16:02+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/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", + "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", @@ -2180,5 +3273,8 @@ "php": ">=8.2.0" }, "platform-dev": {}, + "platform-overrides": { + "php": "8.2" + }, "plugin-api-version": "2.9.0" } 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/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 eb27d61..bf34f08 100644 --- a/src/Client.php +++ b/src/Client.php @@ -4,7 +4,43 @@ 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\Psr17FactoryDiscovery; +use InvalidArgumentException; + class Client { - + protected const API_BASE_URL = 'https://panel.sendcloud.sc/api/v3/'; + + protected HttpMethodsClient $client; + + /** + * @throws NotFoundException + * @throws InvalidArgumentException + */ + public function __construct( + protected string $publicKey, + protected string $secretKey, + protected ?string $partnerId = null, + string $apiBaseUrl = self::API_BASE_URL, + ?HttpClient $client = null + ) { + $client = ClientFactory::create( + $apiBaseUrl, + $publicKey, + $secretKey, + $partnerId, + [], + $client + ); + + $this->client = new HttpMethodsClient( + $client, + Psr17FactoryDiscovery::findRequestFactory(), + Psr17FactoryDiscovery::findStreamFactory() + ); + } } diff --git a/src/Endpoints/Orders.php b/src/Endpoints/Orders.php new file mode 100644 index 0000000..acfeafa --- /dev/null +++ b/src/Endpoints/Orders.php @@ -0,0 +1,260 @@ +client->get('/orders/' . $id); + + SendcloudRequestException::fromResponse($response); + + $body = $response->getBody()->getContents(); + + return Order::fromData(json_decode($body, true)['data']); + } catch (Throwable $throwable) { + SendcloudRequestException::fromException($throwable); + } + } + + /** + * 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 + * + * @throws SendcloudRequestException + */ + 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); + + try { + // Send request + + $response = $this->client->get($uri); + SendcloudRequestException::fromResponse($response); + + // Parse response + + $body = $response->getBody()->getContents(); + $response = json_decode($body, true); + + $orders = []; + foreach ($response['data'] as $order) { + $orders[] = Order::fromData($order); + } + + return $orders; + } catch (Throwable $throwable) { + SendcloudRequestException::fromException($throwable); + } + } + + /** + * Update an order + * Partially update some fields of an order. + * + * @see https://sendcloud.dev/api/v3/orders/update-an-order + * + * @return int Sendcloud order ID + * @throws SendcloudRequestException + * @throws InvalidArgumentException + */ + public function updateOrder( + Order $order + ): int { + if (!isset($order->id)) { + throw new InvalidArgumentException('Order id is null'); + } + + try { + $body = json_encode($order); + $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 $throwable) { + SendcloudRequestException::fromException($throwable); + } + } + + /** + * 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 $throwable) { + SendcloudRequestException::fromException($throwable); + } + } + + /** + * 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 $throwable) { + SendcloudRequestException::fromException($throwable); + } + } +} diff --git a/src/Exceptions/DateParsingException.php b/src/Exceptions/DateParsingException.php new file mode 100644 index 0000000..1b31184 --- /dev/null +++ b/src/Exceptions/DateParsingException.php @@ -0,0 +1,12 @@ + $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 + { + 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; + } + + 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); + } +} diff --git a/src/Factory/ClientFactory.php b/src/Factory/ClientFactory.php new file mode 100644 index 0000000..537e400 --- /dev/null +++ b/src/Factory/ClientFactory.php @@ -0,0 +1,63 @@ + $plugins + * @throws NotFoundException + * @throws InvalidArgumentException + */ + public static function create( + string $base_uri, + string $user, + string $pass, + ?string $partnerId = null, + array $plugins = [], + ?HttpClient $client = null + ): PluginClient { + if ($client === null) { + $client = Psr18ClientDiscovery::find(); + } + + // 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); + } +} diff --git a/src/Models/Address.php b/src/Models/Address.php new file mode 100644 index 0000000..8888c01 --- /dev/null +++ b/src/Models/Address.php @@ -0,0 +1,85 @@ + $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; + } +} diff --git a/src/Models/Customer/CustomerDetails.php b/src/Models/Customer/CustomerDetails.php new file mode 100644 index 0000000..1a4d74b --- /dev/null +++ b/src/Models/Customer/CustomerDetails.php @@ -0,0 +1,54 @@ + $this->name + ]; + + if (isset($this->phone_number)) { + $json['phone_number'] = $this->phone_number; + } + + if (isset($this->email)) { + $json['email'] = $this->email; + } + + return $json; + } +} diff --git a/src/Models/DangerousGoods.php b/src/Models/DangerousGoods.php new file mode 100644 index 0000000..c8bb07f --- /dev/null +++ b/src/Models/DangerousGoods.php @@ -0,0 +1,148 @@ + $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_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 + * @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_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, + 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_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, + 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 + ); + } + + 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_letter' => $this->adr_packing_group_letter + ]; + + 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); + 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; + } +} diff --git a/src/Models/Delivery/DeliveryDates.php b/src/Models/Delivery/DeliveryDates.php new file mode 100644 index 0000000..ee4f131 --- /dev/null +++ b/src/Models/Delivery/DeliveryDates.php @@ -0,0 +1,62 @@ +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; + } +} diff --git a/src/Models/Measurement/Measurement.php b/src/Models/Measurement/Measurement.php new file mode 100644 index 0000000..87c79b4 --- /dev/null +++ b/src/Models/Measurement/Measurement.php @@ -0,0 +1,67 @@ +dimension)) { + $json['dimension'] = $this->dimension; + } + + if (isset($this->weight)) { + $json['weight'] = $this->weight; + } + + if (isset($this->volume)) { + $json['volume'] = $this->volume; + } + + return $json; + } +} diff --git a/src/Models/Measurement/MeasurementDimension.php b/src/Models/Measurement/MeasurementDimension.php new file mode 100644 index 0000000..2214250 --- /dev/null +++ b/src/Models/Measurement/MeasurementDimension.php @@ -0,0 +1,61 @@ + $unit + */ + public function __construct( + public readonly float $length, + public readonly float $width, + public readonly float $height, + public readonly string $unit + ) { + + } + + public static function fromData(array $data) : self + { + return new self( + length: (float) $data['length'], + width: (float) $data['width'], + height: (float) $data['height'], + unit: (string) $data['unit'] + ); + } + + public function jsonSerialize() : array + { + $json = [ + 'length' => $this->length, + 'width' => $this->width, + 'height' => $this->height, + 'unit' => $this->unit + ]; + + return $json; + } +} diff --git a/src/Models/Measurement/MeasurementVolume.php b/src/Models/Measurement/MeasurementVolume.php new file mode 100644 index 0000000..573a222 --- /dev/null +++ b/src/Models/Measurement/MeasurementVolume.php @@ -0,0 +1,53 @@ + $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'] + ); + } + + public function jsonSerialize() : array + { + $json = [ + 'value' => $this->value, + 'unit' => $this->unit + ]; + + return $json; + } +} diff --git a/src/Models/Measurement/MeasurementWeight.php b/src/Models/Measurement/MeasurementWeight.php new file mode 100644 index 0000000..1f2f2b8 --- /dev/null +++ b/src/Models/Measurement/MeasurementWeight.php @@ -0,0 +1,52 @@ + $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'] + ); + } + + public function jsonSerialize() : array + { + $json = [ + 'value' => $this->value, + 'unit' => $this->unit + ]; + + return $json; + } +} diff --git a/src/Models/ModelInterface.php b/src/Models/ModelInterface.php new file mode 100644 index 0000000..8cbbcee --- /dev/null +++ b/src/Models/ModelInterface.php @@ -0,0 +1,25 @@ + $data + * @throws ModelFromDataException + * @throws DateParsingException + */ + public static function fromData(array $data) : self; + + /** + * @return array + * @throws DateParsingException + */ + public function jsonSerialize() : array; +} diff --git a/src/Models/Order/CustomsDetails.php b/src/Models/Order/CustomsDetails.php new file mode 100644 index 0000000..a87934b --- /dev/null +++ b/src/Models/Order/CustomsDetails.php @@ -0,0 +1,70 @@ + $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 + ); + } + + 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; + } +} diff --git a/src/Models/Order/Order.php b/src/Models/Order/Order.php new file mode 100644 index 0000000..a077c75 --- /dev/null +++ b/src/Models/Order/Order.php @@ -0,0 +1,96 @@ + $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 $json; + } +} diff --git a/src/Models/Order/OrderDetails.php b/src/Models/Order/OrderDetails.php new file mode 100644 index 0000000..c35ac34 --- /dev/null +++ b/src/Models/Order/OrderDetails.php @@ -0,0 +1,74 @@ + $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 Status $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 = null + ) { + + } + + 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: 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']), + notes: (string) $data['notes'], + 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; + } +} diff --git a/src/Models/Order/OrderDetailsIntegration.php b/src/Models/Order/OrderDetailsIntegration.php new file mode 100644 index 0000000..21a2366 --- /dev/null +++ b/src/Models/Order/OrderDetailsIntegration.php @@ -0,0 +1,32 @@ + $this->id + ]; + + return $json; + } +} diff --git a/src/Models/Order/OrderItems.php b/src/Models/Order/OrderItems.php new file mode 100644 index 0000000..3da7f98 --- /dev/null +++ b/src/Models/Order/OrderItems.php @@ -0,0 +1,124 @@ + $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, + + ) { + + } + + 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 + ); + } + + 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; + } +} diff --git a/src/Models/Order/OrderResponse.php b/src/Models/Order/OrderResponse.php new file mode 100644 index 0000000..e69de29 diff --git a/src/Models/Order/ShipWith.php b/src/Models/Order/ShipWith.php new file mode 100644 index 0000000..4dba632 --- /dev/null +++ b/src/Models/Order/ShipWith.php @@ -0,0 +1,45 @@ + $this->type, + 'properties' => $this->properties + ]; + + return $json; + } +} diff --git a/src/Models/Order/ShippingDetails.php b/src/Models/Order/ShippingDetails.php new file mode 100644 index 0000000..2a37b4a --- /dev/null +++ b/src/Models/Order/ShippingDetails.php @@ -0,0 +1,57 @@ +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; + } +} diff --git a/src/Models/Order/ShippingOptionProperties.php b/src/Models/Order/ShippingOptionProperties.php new file mode 100644 index 0000000..67d0bd4 --- /dev/null +++ b/src/Models/Order/ShippingOptionProperties.php @@ -0,0 +1,45 @@ +shipping_option_code); + JsonUtils::addIfNotNull($json, 'contract_id', $this->contract_id); + + return $json; + } +} diff --git a/src/Models/PaymentDetails.php b/src/Models/PaymentDetails.php new file mode 100644 index 0000000..8ace94e --- /dev/null +++ b/src/Models/PaymentDetails.php @@ -0,0 +1,81 @@ + $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; + } +} diff --git a/src/Models/Price.php b/src/Models/Price.php new file mode 100644 index 0000000..5e32fc9 --- /dev/null +++ b/src/Models/Price.php @@ -0,0 +1,42 @@ + $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'] + ); + } + + public function jsonSerialize() : array + { + $json = [ + 'value' => $this->value, + 'currency' => $this->currency + ]; + + return $json; + } +} diff --git a/src/Models/ServicePoint/ServicePoint.php b/src/Models/ServicePoint/ServicePoint.php new file mode 100644 index 0000000..cedd6b4 --- /dev/null +++ b/src/Models/ServicePoint/ServicePoint.php @@ -0,0 +1,48 @@ + $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; + } +} diff --git a/src/Models/Status.php b/src/Models/Status.php new file mode 100644 index 0000000..c4d497f --- /dev/null +++ b/src/Models/Status.php @@ -0,0 +1,38 @@ + $this->code + ]; + + if (isset($this->message)) { + $json['message'] = $this->message; + } + + return $json; + } +} diff --git a/src/Models/Tax/TaxNumber.php b/src/Models/Tax/TaxNumber.php new file mode 100644 index 0000000..3724d46 --- /dev/null +++ b/src/Models/Tax/TaxNumber.php @@ -0,0 +1,39 @@ +name); + JsonUtils::addIfNotNull($json, 'country_code', $this->country_code); + JsonUtils::addIfNotNull($json, 'value', $this->value); + + return $json; + } +} diff --git a/src/Models/Tax/TaxNumbers.php b/src/Models/Tax/TaxNumbers.php new file mode 100644 index 0000000..06b9b83 --- /dev/null +++ b/src/Models/Tax/TaxNumbers.php @@ -0,0 +1,64 @@ + $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 + ); + } + + public function jsonSerialize() : array + { + $json = [ + 'sender' => $this->sender, + 'receiver' => $this->receiver, + 'importer_of_record' => $this->importer_of_record, + ]; + + return $json; + } +} diff --git a/src/Utils/DateUtils.php b/src/Utils/DateUtils.php new file mode 100644 index 0000000..2a32feb --- /dev/null +++ b/src/Utils/DateUtils.php @@ -0,0 +1,42 @@ +format(self::DATE_FORMAT); + } +} diff --git a/src/Utils/JsonUtils.php b/src/Utils/JsonUtils.php new file mode 100644 index 0000000..33024f9 --- /dev/null +++ b/src/Utils/JsonUtils.php @@ -0,0 +1,18 @@ + $json + */ + public static function addIfNotNull(array &$json, string $key, mixed $value) : void + { + if (isset($value)) { + $json[$key] = $value; + } + } +} diff --git a/tests/ClientTest.php b/tests/ClientTest.php index ca28542..d7b8f3a 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -5,14 +5,66 @@ namespace Test\AlexisPPLIN\SendcloudV3; use AlexisPPLIN\SendcloudV3\Client; +use AlexisPPLIN\SendcloudV3\Factory\ClientFactory; +use Http\Client\Common\HttpMethodsClient; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use Http\Mock\Client As MockClient; + +use Test\AlexisPPLIN\SendcloudV3\ClientTestInstance; + #[CoversClass(Client::class)] +#[CoversClass(ClientFactory::class)] class ClientTest extends TestCase { - public function testEmpty(): void + public function testConstruct(): void { - $this->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..7850244 --- /dev/null +++ b/tests/ClientTestInstance.php @@ -0,0 +1,15 @@ +client; + } +} diff --git a/tests/Endpoints/OrdersTest.php b/tests/Endpoints/OrdersTest.php new file mode 100644 index 0000000..3090b39 --- /dev/null +++ b/tests/Endpoints/OrdersTest.php @@ -0,0 +1,588 @@ +addResponse(new Response(status: $status, body: $body)); + + $publicKey = '123456'; + $secretKey = 'abcdef'; + $partnerId = '1'; + $apiBaseUrl = 'https://api.example.com/v3'; + + return new Orders( + $publicKey, + $secretKey, + $partnerId, + $apiBaseUrl, + $client + ); + } + + private function generateOrder(bool $with_id = true) : Order + { + 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'), + modified_at: DateUtils::iso8601ToDateTime('2026-02-09T16:00:17.111586+00:00'), + order_details: new OrderDetails( + integration: new OrderDetailsIntegration( + id: 1 + ), + status: new Status( + code: 'fulfilled', + message: 'Order has been fulfilled' + ), + 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', + 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: 14.5, + unit: 'kg' + ), + volume: new MeasurementVolume( + 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: new Price( + value: 3.5, + currency: 'EUR' + ), + unit_price: new Price( + value: 3.5, + currency: 'EUR' + ), + delivery_dates: new DeliveryDates( + 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: '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: 'Call this number before delivery: 063 874 6473', + tags: [ + 'fragile', + 'countryside warehouse' + ] + ), + payment_details: new PaymentDetails( + 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: 3.5, + currency: 'EUR' + ), + status: new Status( + code: 'paid', + message: 'Paid' + ), + invoice_date: '2018-07-14', + discount_granted: new Price( + value: 3.5, + currency: 'EUR' + ), + insurance_costs: new Price( + value: 3.5, + currency: 'EUR' + ), + freight_costs: new Price( + value: 3.5, + currency: 'EUR' + ), + other_costs: new Price( + value: 3.5, + currency: 'EUR' + ), + is_cash_on_delivery: true + ), + customs_details: new CustomsDetails( + commercial_invoice_number: '1002404102022', + shipment_type: 'commercial_goods', + export_type: 'private', + tax_numbers: new TaxNumbers( + sender: [ + new TaxNumber( + name: 'VAT', + country_code: 'NL', + value: 'NL987654321B02' + ) + ], + receiver: [ + new TaxNumber( + name: 'VAT', + country_code: 'NL', + value: 'NL987654321B02' + ) + ], + importer_of_record: [ + new TaxNumber( + name: 'VAT', + country_code: 'NL', + 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: '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_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' + ] + ) + ); + } + + /** + * @throws DateParsingException + */ + protected function setUp(): void + { + $this->order = $this->generateOrder(); + } + + /* getOrder */ + + public function testGetOrder() : void + { + // -- Arrange + + $order_id = 1; + $expected = $this->order; + + $json = $this->getJson(true); + $endpoint = $this->getEndpoint($json, 200); + + // -- Act + + $actual = $endpoint->getOrder($order_id); + + // -- Assert + + $this->assertInstanceOf(Order::class, $actual); + $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 + + $json = $this->getJson(true); + + // -- Act + + $actual = json_encode(['data' => $this->order]); + + // -- Assert + + $this->assertJsonStringEqualsJsonString($json, $actual); + } + + /* getOrders */ + + 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(); + } + + /* 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 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 new file mode 100644 index 0000000..d3d6f4c --- /dev/null +++ b/tests/Endpoints/orders.json @@ -0,0 +1,223 @@ +{ + "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" + }, + "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" + ] + }, + "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 + }, + "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" + } + ] + } + }, + "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" + } + }, + "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 diff --git a/tests/Exceptions/SendcloudRequestExceptionTest.php b/tests/Exceptions/SendcloudRequestExceptionTest.php new file mode 100644 index 0000000..7dab90c --- /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(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..88836fe --- /dev/null +++ b/tests/Utils/JsonUtilsTest.php @@ -0,0 +1,42 @@ + '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