diff --git a/.github/workflows/build-actions.yml b/.github/workflows/build-actions.yml
new file mode 100644
index 0000000..a8b8644
--- /dev/null
+++ b/.github/workflows/build-actions.yml
@@ -0,0 +1,51 @@
+name: Unit tests and checkstyle
+on: [push]
+jobs:
+ phpunit:
+ runs-on: ${{ matrix.operating-system }}
+ strategy:
+ matrix:
+ operating-system: ['ubuntu-latest']
+ php-versions: ['8.2', '8.3']
+ phpunit-versions: ['latest']
+ include:
+ - operating-system: 'ubuntu-latest'
+ php-versions: '8.2'
+ phpunit-versions: 10
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ extensions: mbstring
+ coverage: xdebug
+ tools: composer:v2, php-cs-fixer, phpunit:${{ matrix.phpunit-versions }}
+
+ - name: Get composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
+
+ - name: Cache dependencies
+ uses: actions/cache@v3
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
+ restore-keys: ${{ runner.os }}-composer-
+
+ - name: Install dependencies
+ run: composer install --prefer-dist
+
+ - name: Run PHPCS
+ run: ./vendor/bin/phpcs src tests -v --standard=PSR2
+
+ - name: Run Psalm
+ run: ./vendor/bin/psalm --show-info=true
+
+ - name: Run PHPStan
+ run: ./vendor/bin/phpstan analyse --level=5 src tests
+
+ - name: Run PHPUnit
+ run: ./vendor/bin/phpunit --coverage-text --colors
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..cd23d83
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,11 @@
+# IDE
+/.idea/
+
+# PHP Unit
+.phpunit.cache
+.phpunit.result.cache
+
+# Composer
+composer.lock
+composer.phar
+/vendor/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8b16544
--- /dev/null
+++ b/README.md
@@ -0,0 +1,6 @@
+## Requirements
+[![PHP Version Require](http://poser.pugx.org/inserve/also-cloud-marketplace-api-php/require/php)](https://packagist.org/packages/inserve/also-cloud-marketplace-api-php)
+
+## Installation
+`composer require inserve/also-cloud-marketplace-api-php`
+
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..d143aba
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,37 @@
+{
+ "name": "inserve/also-cloud-marketplace-api-php",
+ "description": "A PHP wrapper ALSO Cloud Marketplace",
+ "license": "MIT",
+ "type": "library",
+ "require": {
+ "php": "^8.2",
+ "guzzlehttp/guzzle": "^7.7",
+ "symfony/serializer": "^6.3|^7",
+ "symfony/property-access": "^6.3|^7",
+ "psr/log": "^3.0",
+ "phpdocumentor/reflection-docblock": "^5.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.2",
+ "squizlabs/php_codesniffer": "^3.7",
+ "phpstan/phpstan": "^1.10",
+ "vimeo/psalm": "^5.22"
+ },
+ "autoload": {
+ "psr-4": {
+ "Inserve\\ALSOCloudMarketplaceAPI\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Inserve\\ALSOCloudMarketplaceAPI\\Tests\\": "tests/"
+ }
+ },
+ "authors": [
+ {
+ "name": "Inserve",
+ "email": "dev@inserve.nl"
+ }
+ ],
+ "minimum-stability": "stable"
+}
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..0fe1e4a
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,25 @@
+
+
+
+
+ tests
+
+
+
+
+
diff --git a/psalm.xml b/psalm.xml
new file mode 100644
index 0000000..a9624f0
--- /dev/null
+++ b/psalm.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/API/AbstractAPIClient.php b/src/API/AbstractAPIClient.php
new file mode 100644
index 0000000..749c0d7
--- /dev/null
+++ b/src/API/AbstractAPIClient.php
@@ -0,0 +1,18 @@
+apiClient->call(
+ '/GetCompany',
+ json_encode(compact('accountId'))
+ );
+
+ return $this->apiClient->denormalize($response, Company::class);
+ }
+
+ /**
+ * @param int $parentAccountId
+ *
+ * @return Company[]
+ *
+ * @throws MarketplaceAPIException|ExceptionInterface
+ */
+ public function list(int $parentAccountId): array
+ {
+ $response = $this->apiClient->call(
+ '/GetCompanies',
+ json_encode(compact('parentAccountId'))
+ );
+
+ $companies = [];
+ foreach ($response as $item) {
+ $companies[] = $this->apiClient->denormalize($item, Company::class);
+ }
+
+ return $companies;
+ }
+}
diff --git a/src/API/SubscriptionsAPI.php b/src/API/SubscriptionsAPI.php
new file mode 100644
index 0000000..86568e5
--- /dev/null
+++ b/src/API/SubscriptionsAPI.php
@@ -0,0 +1,53 @@
+apiClient->call(
+ '/GetSubscription',
+ json_encode(compact('accountId'))
+ );
+
+ return $this->apiClient->denormalize($response, Subscription::class);
+ }
+
+ /**
+ * @param int $parentAccountId
+ * @param bool $excludeUserLevel
+ *
+ * @return Subscription[]
+ *
+ * @throws MarketplaceAPIException|ExceptionInterface
+ */
+ public function list(int $parentAccountId, bool $excludeUserLevel = false): array
+ {
+ $response = $this->apiClient->call(
+ '/GetSubscriptions',
+ json_encode(compact('parentAccountId', 'excludeUserLevel'))
+ );
+
+ $subscriptions = [];
+ foreach ($response as $item) {
+ $subscriptions[] = $this->apiClient->denormalize($item, Subscription::class);
+ }
+
+ return $subscriptions;
+ }
+}
diff --git a/src/Client/APIClient.php b/src/Client/APIClient.php
new file mode 100644
index 0000000..8f84245
--- /dev/null
+++ b/src/Client/APIClient.php
@@ -0,0 +1,185 @@
+normalizer = new ObjectNormalizer(
+ classMetadataFactory: $classMetadataFactory,
+ nameConverter: $nameConverter,
+ propertyTypeExtractor: $extractor,
+ defaultContext: [AbstractObjectNormalizer::SKIP_NULL_VALUES => true]
+ );
+
+ $this->serializer = new Serializer(
+ [$this->normalizer, new ArrayDenormalizer()],
+ [new JsonEncoder(), new XmlEncoder()]
+ );
+ }
+
+ /**
+ * @return ClientInterface
+ */
+ public function getClient(): ClientInterface
+ {
+ return $this->client;
+ }
+
+ /**
+ * @param string $sessionToken
+ *
+ * @return void
+ */
+ public function setSessionToken(#[\SensitiveParameter] string $sessionToken): void
+ {
+ $this->sessionToken = $sessionToken;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSessionToken(): ?string
+ {
+ return $this->sessionToken;
+ }
+
+ /**
+ * @param array $response
+ * @param string $class
+ *
+ * @return mixed
+ *
+ * @throws ExceptionInterface
+ */
+ public function denormalize(array $response, string $class): mixed
+ {
+ try {
+ return $this->normalizer->denormalize($response, $class);
+ } catch (Exception $exception) {
+ $this->logError(sprintf('(%s): %s', __FUNCTION__, $exception->getMessage()));
+
+ return null;
+ }
+ }
+
+ /**
+ * @param string $url
+ * @param string|null $body
+ *
+ * @return mixed
+ *
+ * @throws MarketplaceAPIException
+ */
+ public function call(string $url, ?string $body = null): mixed
+ {
+ try {
+ $request = new Request(
+ 'POST',
+ $this->getAPIUrl($url),
+ $this->getDefaultHeaders(),
+ $body
+ );
+ $response = $this->client->send($request);
+
+ return json_decode((string) $response->getBody(), true);
+ } catch (GuzzleException|BadResponseException $exception) {
+ $errorMessage = $exception->getMessage();
+
+ if ($exception instanceof RequestException) {
+ $errorResponse = $this->serializer->decode((string) $exception->getResponse()?->getBody(), 'xml');
+ $errorMessage = $errorResponse['Reason']['Text']['#'] ?? 'Invalid API call';
+ }
+
+ throw new MarketplaceAPIException(
+ sprintf('%s: %s', $url, $errorMessage),
+ $exception->getCode()
+ );
+ }
+ }
+
+ /**
+ * @return string[]
+ */
+ protected function getDefaultHeaders(): array
+ {
+ $headers = [
+ 'Content-Type' => 'application/json',
+ ];
+
+ if ($this->sessionToken !== null) {
+ $headers['Authenticate'] = $this->sessionToken;
+ }
+
+ return $headers;
+ }
+
+ /**
+ * @param string $url
+ *
+ * @return string
+ */
+ protected function getAPIUrl(string $url): string
+ {
+ return sprintf('/SimpleAPI/SimpleAPIService.svc/rest/%s', $url);
+ }
+
+ /**
+ * @param string $message
+ *
+ * @return void
+ */
+ private function logError(string $message): void
+ {
+ if (!$this->logger) {
+ return;
+ }
+
+ $this->logger->error($message);
+ }
+}
diff --git a/src/Exception/MarketplaceAPIException.php b/src/Exception/MarketplaceAPIException.php
new file mode 100644
index 0000000..9a7c414
--- /dev/null
+++ b/src/Exception/MarketplaceAPIException.php
@@ -0,0 +1,12 @@
+apiClient = new APIClient($this->client, $this->logger);
+ }
+
+ /**
+ * @param string $name
+ * @param array $arguments
+ *
+ * @return mixed
+ */
+ public function __call(string $name, array $arguments): mixed
+ {
+ return $this->__get($name);
+ }
+
+ /**
+ * @param string $name
+ *
+ * @return mixed
+ */
+ public function __get(string $name): mixed
+ {
+ $fqdnClass = sprintf('%s\\API\\%sAPI', __NAMESPACE__, ucfirst($name));
+
+ if (class_exists($fqdnClass)) {
+ return new $fqdnClass($this->apiClient);
+ }
+
+ return null;
+ }
+
+ /**
+ * @param string $username
+ * @param string $password
+ *
+ * @return string
+ *
+ * @throws MarketplaceAPIException
+ */
+ public function authenticate(string $username, #[\SensitiveParameter] string $password): string
+ {
+ $loginData = json_encode(compact('username', 'password'));
+ $sessionToken = $this->apiClient->call('GetSessionToken', $loginData);
+ $this->apiClient->setSessionToken($sessionToken);
+
+ return $sessionToken;
+ }
+}
diff --git a/src/Model/Company.php b/src/Model/Company.php
new file mode 100644
index 0000000..291f55c
--- /dev/null
+++ b/src/Model/Company.php
@@ -0,0 +1,575 @@
+parentAccountId;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getAccountId(): ?int
+ {
+ return $this->accountId;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getAccountState(): ?string
+ {
+ return $this->accountState;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCompanyName(): ?string
+ {
+ return $this->companyName;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getVatId(): ?string
+ {
+ return $this->vatId;
+ }
+
+ /**
+ * @return string[]|null
+ */
+ public function getDomain(): ?array
+ {
+ return $this->domain;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getBillingStartDate(): ?string
+ {
+ return $this->billingStartDate;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getContractId(): ?string
+ {
+ return $this->contractId;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCurrency(): ?string
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getAddress(): ?string
+ {
+ return $this->address;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCity(): ?string
+ {
+ return $this->city;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCountry(): ?string
+ {
+ return $this->country;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getZip(): ?string
+ {
+ return $this->zip;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getEmail(): ?string
+ {
+ return $this->email;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getMarketplace(): ?int
+ {
+ return $this->marketplace;
+ }
+
+ /**
+ * @return int[]|null
+ */
+ public function getMarketplaces(): ?array
+ {
+ return $this->marketplaces;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getVat(): ?string
+ {
+ return $this->vat;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getAccountType(): ?string
+ {
+ return $this->accountType;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getNumericId(): ?string
+ {
+ return $this->numericId;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getLanguage(): ?string
+ {
+ return $this->language;
+ }
+
+ /**
+ * @return bool|null
+ */
+ public function getOnlineBillSplitByEndCustomer(): ?bool
+ {
+ return $this->onlineBillSplitByEndCustomer;
+ }
+
+ /**
+ * @param int|null $parentAccountId
+ *
+ * @return $this
+ */
+ public function setParentAccountId(?int $parentAccountId): self
+ {
+ $this->parentAccountId = $parentAccountId;
+
+ return $this;
+ }
+
+ /**
+ * @param int|null $accountId
+ *
+ * @return $this
+ */
+ public function setAccountId(?int $accountId): self
+ {
+ $this->accountId = $accountId;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $accountState
+ *
+ * @return $this
+ */
+ public function setAccountState(?string $accountState): self
+ {
+ $this->accountState = $accountState;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $companyName
+ *
+ * @return $this
+ */
+ public function setCompanyName(?string $companyName): self
+ {
+ $this->companyName = $companyName;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $vatId
+ *
+ * @return $this
+ */
+ public function setVatid(?string $vatId): self
+ {
+ $this->vatId = $vatId;
+
+ return $this;
+ }
+
+ /**
+ * @param string[]|null $domain
+ */
+ public function setDomain(?array $domain): self
+ {
+ $this->domain = $domain;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $billingStartDate
+ *
+ * @return $this
+ */
+ public function setBillingStartDate(?string $billingStartDate): self
+ {
+ $this->billingStartDate = $billingStartDate;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $contractId
+ *
+ * @return $this
+ */
+ public function setContractId(?string $contractId): self
+ {
+ $this->contractId = $contractId;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $currency
+ *
+ * @return $this
+ */
+ public function setCurrency(?string $currency): self
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $address
+ *
+ * @return $this
+ */
+ public function setAddress(?string $address): self
+ {
+ $this->address = $address;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $city
+ *
+ * @return $this
+ */
+ public function setCity(?string $city): self
+ {
+ $this->city = $city;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $country
+ *
+ * @return $this
+ */
+ public function setCountry(?string $country): self
+ {
+ $this->country = $country;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $zip
+ *
+ * @return $this
+ */
+ public function setZip(?string $zip): self
+ {
+ $this->zip = $zip;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $email
+ *
+ * @return $this
+ */
+ public function setEmail(?string $email): self
+ {
+ $this->email = $email;
+
+ return $this;
+ }
+
+ /**
+ * @param int|null $marketplace
+ *
+ * @return $this
+ */
+ public function setMarketplace(?int $marketplace): self
+ {
+ $this->marketplace = $marketplace;
+
+ return $this;
+ }
+
+ /**
+ * @param int[]|null $marketplaces
+ */
+ public function setMarketplaces(?array $marketplaces): self
+ {
+ $this->marketplaces = $marketplaces;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $vat
+ *
+ * @return $this
+ */
+ public function setVat(?string $vat): self
+ {
+ $this->vat = $vat;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $accountType
+ *
+ * @return $this
+ */
+ public function setAccountType(?string $accountType): self
+ {
+ $this->accountType = $accountType;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $numericId
+ *
+ * @return $this
+ */
+ public function setNumericId(?string $numericId): self
+ {
+ $this->numericId = $numericId;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $language
+ *
+ * @return $this
+ */
+ public function setLanguage(?string $language): self
+ {
+ $this->language = $language;
+
+ return $this;
+ }
+
+ /**
+ * @param bool|null $onlineBillSplitByEndCustomer
+ *
+ * @return $this
+ */
+ public function setOnlineBillSplitByEndCustomer(?bool $onlineBillSplitByEndCustomer): self
+ {
+ $this->onlineBillSplitByEndCustomer = $onlineBillSplitByEndCustomer;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getPurchaseOrderNumber(): ?string
+ {
+ return $this->purchaseOrderNumber;
+ }
+
+ /**
+ * @param string|null $purchaseOrderNumber
+ *
+ * @return $this
+ */
+ public function setPurchaseOrderNumber(?string $purchaseOrderNumber): self
+ {
+ $this->purchaseOrderNumber = $purchaseOrderNumber;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCustomerId(): ?string
+ {
+ return $this->customerId;
+ }
+
+ /**
+ * @param string|null $customerId
+ *
+ * @return $this
+ */
+ public function setCustomerId(?string $customerId): self
+ {
+ $this->customerId = $customerId;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getTechnicalEmail(): ?string
+ {
+ return $this->technicalEmail;
+ }
+
+ /**
+ * @param string|null $technicalEmail
+ *
+ * @return $this
+ */
+ public function setTechnicalEmail(?string $technicalEmail): self
+ {
+ $this->technicalEmail = $technicalEmail;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSalesMan(): ?string
+ {
+ return $this->salesMan;
+ }
+
+ /**
+ * @param string|null $salesMan
+ *
+ * @return $this
+ */
+ public function setSalesMan(?string $salesMan): self
+ {
+ $this->salesMan = $salesMan;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCrefoNumber(): ?string
+ {
+ return $this->crefoNumber;
+ }
+
+ /**
+ * @param string|null $crefoNumber
+ *
+ * @return $this
+ */
+ public function setCrefoNumber(?string $crefoNumber): self
+ {
+ $this->crefoNumber = $crefoNumber;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getMpnId(): ?string
+ {
+ return $this->mpnId;
+ }
+
+ /**
+ * @param string|null $mpnId
+ *
+ * @return $this
+ */
+ public function setMpnId(?string $mpnId): self
+ {
+ $this->mpnId = $mpnId;
+
+ return $this;
+ }
+}
diff --git a/src/Model/Field.php b/src/Model/Field.php
new file mode 100644
index 0000000..f0dbdea
--- /dev/null
+++ b/src/Model/Field.php
@@ -0,0 +1,73 @@
+name;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getDisplayName(): ?string
+ {
+ return $this->displayName;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function getValue(): mixed
+ {
+ return $this->value;
+ }
+
+ /**
+ * @param string|null $name
+ *
+ * @return $this
+ */
+ public function setName(?string $name): self
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $displayName
+ *
+ * @return $this
+ */
+ public function setDisplayName(?string $displayName): self
+ {
+ $this->displayName = $displayName;
+
+ return $this;
+ }
+
+ /**
+ * @param mixed $value
+ *
+ * @return $this
+ */
+ public function setValue(mixed $value): self
+ {
+ $this->value = $value;
+
+ return $this;
+ }
+}
diff --git a/src/Model/PriceableItem.php b/src/Model/PriceableItem.php
new file mode 100644
index 0000000..6dd04e5
--- /dev/null
+++ b/src/Model/PriceableItem.php
@@ -0,0 +1,241 @@
+chargeType;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getPriceableItemDescription(): ?string
+ {
+ return $this->priceableItemDescription;
+ }
+
+ /**
+ * @return bool|null
+ */
+ public function getIsUdrcField(): ?bool
+ {
+ return $this->isUdrcField;
+ }
+
+ /**
+ * @return int|float|null
+ */
+ public function getPurchasePrice(): int|float|null
+ {
+ return $this->purchasePrice;
+ }
+
+ /**
+ * @return int|float|null
+ */
+ public function getSalesPrice(): int|float|null
+ {
+ return $this->salesPrice;
+ }
+
+ /**
+ * @return int|float|null
+ */
+ public function getSuggestedRetailPrice(): int|float|null
+ {
+ return $this->suggestedRetailPrice;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getCurrency(): ?string
+ {
+ return $this->currency;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getPriceableItemId(): ?int
+ {
+ return $this->priceableItemId;
+ }
+
+ /**
+ * @param string|null $chargeType
+ *
+ * @return $this
+ */
+ public function setChargeType(?string $chargeType): self
+ {
+ $this->chargeType = $chargeType;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $priceableItemDescription
+ *
+ * @return $this
+ */
+ public function setPriceableItemDescription(?string $priceableItemDescription): self
+ {
+ $this->priceableItemDescription = $priceableItemDescription;
+
+ return $this;
+ }
+
+ /**
+ * @param bool|null $isUdrcField
+ *
+ * @return $this
+ */
+ public function setIsUdrcField(?bool $isUdrcField): self
+ {
+ $this->isUdrcField = $isUdrcField;
+
+ return $this;
+ }
+
+ /**
+ * @param int|float|null $purchasePrice
+ *
+ * @return $this
+ */
+ public function setPurchasePrice(int|float|null $purchasePrice): self
+ {
+ $this->purchasePrice = $purchasePrice;
+
+ return $this;
+ }
+
+ /**
+ * @param int|float|null $salesPrice
+ *
+ * @return $this
+ */
+ public function setSalesPrice(int|float|null $salesPrice): self
+ {
+ $this->salesPrice = $salesPrice;
+
+ return $this;
+ }
+
+ /**
+ * @param int|float|null $suggestedRetailPrice
+ *
+ * @return $this
+ */
+ public function setSuggestedRetailPrice(int|float|null $suggestedRetailPrice): self
+ {
+ $this->suggestedRetailPrice = $suggestedRetailPrice;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $currency
+ *
+ * @return $this
+ */
+ public function setCurrency(?string $currency): self
+ {
+ $this->currency = $currency;
+
+ return $this;
+ }
+
+ /**
+ * @param int|null $priceableItemId
+ *
+ * @return $this
+ */
+ public function setPriceableItemId(?int $priceableItemId): self
+ {
+ $this->priceableItemId = $priceableItemId;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getMaterialNumber(): ?string
+ {
+ return $this->materialNumber;
+ }
+
+ /**
+ * @param string|null $materialNumber
+ *
+ * @return $this
+ */
+ public function setMaterialNumber(?string $materialNumber): self
+ {
+ $this->materialNumber = $materialNumber;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getFieldName(): ?string
+ {
+ return $this->fieldName;
+ }
+
+ /**
+ * @param string|null $fieldName
+ *
+ * @return $this
+ */
+ public function setFieldName(?string $fieldName): self
+ {
+ $this->fieldName = $fieldName;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getProductNumber(): ?string
+ {
+ return $this->productNumber;
+ }
+
+ /**
+ * @param string|null $productNumber
+ *
+ * @return $this
+ */
+ public function setProductNumber(?string $productNumber): self
+ {
+ $this->productNumber = $productNumber;
+
+ return $this;
+ }
+}
diff --git a/src/Model/Subscription.php b/src/Model/Subscription.php
new file mode 100644
index 0000000..3ca6960
--- /dev/null
+++ b/src/Model/Subscription.php
@@ -0,0 +1,537 @@
+parentAccountId;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getCompanyAccountId(): ?int
+ {
+ return $this->companyAccountId;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getParentType(): ?string
+ {
+ return $this->parentType;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getAccountId(): ?int
+ {
+ return $this->accountId;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getAccountState(): ?string
+ {
+ return $this->accountState;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getServiceName(): ?string
+ {
+ return $this->serviceName;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getServiceDisplayName(): ?string
+ {
+ return $this->serviceDisplayName;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getContractId(): ?string
+ {
+ return $this->contractId;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getProvisioningStatus(): ?string
+ {
+ return $this->provisioningStatus;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getErrorDetails(): ?string
+ {
+ return $this->errorDetails;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getVendorReferenceId(): ?string
+ {
+ return $this->vendorReferenceId;
+ }
+
+ /**
+ * @return bool|null
+ */
+ public function getHasRenewActionValuesConfigured(): ?bool
+ {
+ return $this->hasRenewActionValuesConfigured;
+ }
+
+ /**
+ * @return Field[]|null
+ */
+ public function getFields(): ?array
+ {
+ return $this->fields;
+ }
+
+ /**
+ * @return PriceableItem[]|null
+ */
+ public function getPriceableItems(): ?array
+ {
+ return $this->priceableItems;
+ }
+
+ /**
+ * @param int|null $parentAccountId
+ *
+ * @return $this
+ */
+ public function setParentAccountId(?int $parentAccountId): self
+ {
+ $this->parentAccountId = $parentAccountId;
+
+ return $this;
+ }
+
+ /**
+ * @param int|null $companyAccountId
+ *
+ * @return $this
+ */
+ public function setCompanyAccountId(?int $companyAccountId): self
+ {
+ $this->companyAccountId = $companyAccountId;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $parentType
+ *
+ * @return $this
+ */
+ public function setParentType(?string $parentType): self
+ {
+ $this->parentType = $parentType;
+
+ return $this;
+ }
+
+ /**
+ * @param int|null $accountId
+ *
+ * @return $this
+ */
+ public function setAccountId(?int $accountId): self
+ {
+ $this->accountId = $accountId;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $accountState
+ *
+ * @return $this
+ */
+ public function setAccountState(?string $accountState): self
+ {
+ $this->accountState = $accountState;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $serviceName
+ *
+ * @return $this
+ */
+ public function setServiceName(?string $serviceName): self
+ {
+ $this->serviceName = $serviceName;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $serviceDisplayName
+ *
+ * @return $this
+ */
+ public function setServiceDisplayName(?string $serviceDisplayName): self
+ {
+ $this->serviceDisplayName = $serviceDisplayName;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $contractId
+ *
+ * @return $this
+ */
+ public function setContractId(?string $contractId): self
+ {
+ $this->contractId = $contractId;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $provisioningStatus
+ *
+ * @return $this
+ */
+ public function setProvisioningStatus(?string $provisioningStatus): self
+ {
+ $this->provisioningStatus = $provisioningStatus;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $errorDetails
+ *
+ * @return $this
+ */
+ public function setErrorDetails(?string $errorDetails): self
+ {
+ $this->errorDetails = $errorDetails;
+
+ return $this;
+ }
+
+ /**
+ * @param string|null $vendorReferenceId
+ *
+ * @return $this
+ */
+ public function setVendorReferenceId(?string $vendorReferenceId): self
+ {
+ $this->vendorReferenceId = $vendorReferenceId;
+
+ return $this;
+ }
+
+ /**
+ * @param bool|null $hasRenewActionValuesConfigured
+ *
+ * @return $this
+ */
+ public function setHasRenewActionValuesConfigured(?bool $hasRenewActionValuesConfigured): self
+ {
+ $this->hasRenewActionValuesConfigured = $hasRenewActionValuesConfigured;
+
+ return $this;
+ }
+
+ /**
+ * @param Field[]|null $fields
+ */
+ public function setFields(?array $fields): self
+ {
+ $this->fields = $fields;
+
+ return $this;
+ }
+
+ /**
+ * @param PriceableItem[]|null $priceableItems
+ */
+ public function setPriceableItems(?array $priceableItems): self
+ {
+ $this->priceableItems = $priceableItems;
+
+ return $this;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getDependencyAccountId(): ?int
+ {
+ return $this->dependencyAccountId;
+ }
+
+ /**
+ * @param int|null $dependencyAccountId
+ *
+ * @return $this
+ */
+ public function setDependencyAccountId(?int $dependencyAccountId): self
+ {
+ $this->dependencyAccountId = $dependencyAccountId;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getDependencyServiceName(): ?string
+ {
+ return $this->dependencyServiceName;
+ }
+
+ /**
+ * @param string|null $dependencyServiceName
+ *
+ * @return $this
+ */
+ public function setDependencyServiceName(?string $dependencyServiceName): self
+ {
+ $this->dependencyServiceName = $dependencyServiceName;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getSecondVendorReferenceId(): ?string
+ {
+ return $this->secondVendorReferenceId;
+ }
+
+ /**
+ * @param string|null $secondVendorReferenceId
+ *
+ * @return $this
+ */
+ public function setSecondVendorReferenceId(?string $secondVendorReferenceId): self
+ {
+ $this->secondVendorReferenceId = $secondVendorReferenceId;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getProductName(): ?string
+ {
+ return $this->productName;
+ }
+
+ /**
+ * @param string|null $productName
+ *
+ * @return $this
+ */
+ public function setProductName(?string $productName): self
+ {
+ $this->productName = $productName;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getContractEndDate(): ?string
+ {
+ return $this->contractEndDate;
+ }
+
+ /**
+ * @param string|null $contractEndDate
+ *
+ * @return $this
+ */
+ public function setContractEndDate(?string $contractEndDate): self
+ {
+ $this->contractEndDate = $contractEndDate;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getPriceProtectionEndDate(): ?string
+ {
+ return $this->priceProtectionEndDate;
+ }
+
+ /**
+ * @param string|null $priceProtectionEndDate
+ *
+ * @return $this
+ */
+ public function setPriceProtectionEndDate(?string $priceProtectionEndDate): self
+ {
+ $this->priceProtectionEndDate = $priceProtectionEndDate;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getScheduledTerminationDate(): ?string
+ {
+ return $this->scheduledTerminationDate;
+ }
+
+ /**
+ * @param string|null $scheduledTerminationDate
+ *
+ * @return $this
+ */
+ public function setScheduledTerminationDate(?string $scheduledTerminationDate): self
+ {
+ $this->scheduledTerminationDate = $scheduledTerminationDate;
+
+ return $this;
+ }
+
+ /**
+ * @return int|null
+ */
+ public function getRemainingCreditLimit(): ?int
+ {
+ return $this->remainingCreditLimit;
+ }
+
+ /**
+ * @param int|null $remainingCreditLimit
+ *
+ * @return $this
+ */
+ public function setRemainingCreditLimit(?int $remainingCreditLimit): self
+ {
+ $this->remainingCreditLimit = $remainingCreditLimit;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getPurchaseOrderNumber(): ?string
+ {
+ return $this->purchaseOrderNumber;
+ }
+
+ /**
+ * @param string|null $purchaseOrderNumber
+ *
+ * @return $this
+ */
+ public function setPurchaseOrderNumber(?string $purchaseOrderNumber): self
+ {
+ $this->purchaseOrderNumber = $purchaseOrderNumber;
+
+ return $this;
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getAdvancePeriodEndDate(): ?string
+ {
+ return $this->advancePeriodEndDate;
+ }
+
+ /**
+ * @param string|null $advancePeriodEndDate
+ *
+ * @return $this
+ */
+ public function setAdvancePeriodEndDate(?string $advancePeriodEndDate): self
+ {
+ $this->advancePeriodEndDate = $advancePeriodEndDate;
+
+ return $this;
+ }
+
+ /**
+ * @return Field[]|null
+ */
+ public function getRenewFields(): ?array
+ {
+ return $this->renewFields;
+ }
+
+ /**
+ * @param array|null $renewFields
+ *
+ * @return $this
+ */
+ public function setRenewFields(?array $renewFields): self
+ {
+ $this->renewFields = $renewFields;
+
+ return $this;
+ }
+}
diff --git a/tests/MarketplaceAPITest.php b/tests/MarketplaceAPITest.php
new file mode 100644
index 0000000..9adc41f
--- /dev/null
+++ b/tests/MarketplaceAPITest.php
@@ -0,0 +1,99 @@
+setExpectedResponses([
+ new BadResponseException(
+ '',
+ new Request('POST', '/SimpleAPI/SimpleAPIService.svc/rest/GetSessionToken'),
+ new Response(body: $this->getErrorResponse('Invalid login!'))
+ ),
+ ]);
+
+ $this->expectExceptionMessage('GetSessionToken: Invalid login!');
+ $this->expectException(MarketplaceAPIException::class);
+ $this->marketplaceAPI->authenticate('invalid', 'password');
+ }
+
+ /**
+ * @return void
+ *
+ * @throws MarketplaceAPIException
+ */
+ public function testAuthenticate(): void
+ {
+ $this->setExpectedResponses([
+ new Response(200, [], json_encode('sessionToken')),
+ ]);
+
+ self::assertSame(
+ 'sessionToken',
+ $this->marketplaceAPI->authenticate('unit', 'test')
+ );
+ }
+
+ /**
+ * @param array $responses
+ *
+ * @return void
+ */
+ protected function setExpectedResponses(array $responses): void
+ {
+ $mockHandler = new MockHandler($responses);
+ $handlerStack = HandlerStack::create($mockHandler);
+
+ $this->httpClient = new Client(['handler' => $handlerStack]);
+ $this->marketplaceAPI = new MarketplaceAPI($this->httpClient);
+ }
+
+ /**
+ * @param string $message
+ *
+ * @return string
+ */
+ protected function getErrorResponse(string $message): string
+ {
+ // phpcs:disable
+ return sprintf('
+
+ Sender
+
+
+ %s
+
+
+
+ false
+
+ %s
+
+
+', $message, $message);
+ }
+}