diff --git a/composer.json b/composer.json index cbb1979..3833ddd 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ }, "scripts": { "test": "vendor/bin/pest", - "test-coverage": "vendor/bin/pest --coverage", + "test-coverage": "vendor/bin/pest --coverage", "format": "vendor/bin/pint" }, "config": { diff --git a/src/Abstracts/Resource.php b/src/Abstracts/Resource.php index fcf06a9..523f10e 100644 --- a/src/Abstracts/Resource.php +++ b/src/Abstracts/Resource.php @@ -15,7 +15,7 @@ abstract class Resource { public string $self; - public function __construct(array|string|int|float $properties = null) + public function __construct(array|string|int|float|null $properties = null) { if (is_array($properties)) { $this->populate($properties); @@ -43,8 +43,15 @@ protected function resolvePropertyValue(string $property, mixed $value): mixed $propertyReflection = $reflection->getProperty($property); + $reflectionTypeName = $propertyReflection->getType()->getName(); + + // If is already the expected type + if ($value instanceof $reflectionTypeName) { + return $value; + } + // If is EconomicCollection - if (is_a($propertyReflection->getType()->getName(), EconomicCollection::class, true)) { + if (is_a($reflectionTypeName, EconomicCollection::class, true)) { $attribute = $propertyReflection->getAttributes(ResourceType::class); if (! empty($attribute[0])) { @@ -55,7 +62,7 @@ protected function resolvePropertyValue(string $property, mixed $value): mixed } // If is a class - if (class_exists($propertyReflection->getType()->getName())) { + if (class_exists($reflectionTypeName)) { return new ($propertyReflection->getType()->getName())($value); } @@ -151,4 +158,19 @@ public function __toString(): string { return json_encode($this->toArray()); } + + protected static function filterEmpty(array $values): array + { + foreach ($values as $key => $value) { + if (is_array($value)) { + $values[$key] = static::filterEmpty($value); + } + + if (empty($values[$key])) { + unset($values[$key]); + } + } + + return $values; + } } diff --git a/src/Classes/EconomicQueryBuilder.php b/src/Classes/EconomicQueryBuilder.php index d4c22b7..17b29e7 100644 --- a/src/Classes/EconomicQueryBuilder.php +++ b/src/Classes/EconomicQueryBuilder.php @@ -42,7 +42,7 @@ protected function getEndpoint(): string return $this->endpoint; } - public function where(int|string|Closure $propertyName, string $operatorOrValue = null, mixed $value = null): static + public function where(int|string|Closure $propertyName, ?string $operatorOrValue = null, mixed $value = null): static { if (! isset($this->filter)) { $this->filter = new EconomicQueryFilterBuilder(EconomicQueryFilterBuilder::FILTER_RELATION_AND); @@ -53,7 +53,7 @@ public function where(int|string|Closure $propertyName, string $operatorOrValue return $this; } - public function orWhere(int|string|Closure $propertyName, string $operatorOrValue = null, mixed $value = null): static + public function orWhere(int|string|Closure $propertyName, ?string $operatorOrValue = null, mixed $value = null): static { if (! isset($this->filter)) { $this->filter = new EconomicQueryFilterBuilder(EconomicQueryFilterBuilder::FILTER_RELATION_OR); diff --git a/src/Classes/EconomicQueryFilterBuilder.php b/src/Classes/EconomicQueryFilterBuilder.php index 3191a32..4ba7fd1 100644 --- a/src/Classes/EconomicQueryFilterBuilder.php +++ b/src/Classes/EconomicQueryFilterBuilder.php @@ -73,7 +73,7 @@ protected function whereNested(Closure $closure) return $this; } - public function where(int|string|Closure $propertyName, string $operatorOrValue = null, mixed $value = null): static + public function where(int|string|Closure $propertyName, ?string $operatorOrValue = null, mixed $value = null): static { if ($propertyName instanceof Closure) { return $this->whereNested($propertyName); @@ -91,7 +91,7 @@ public function where(int|string|Closure $propertyName, string $operatorOrValue return $this; } - public function orWhere(int|string|Closure $propertyName, string $operatorOrValue = null, mixed $value = null): static + public function orWhere(int|string|Closure $propertyName, ?string $operatorOrValue = null, mixed $value = null): static { $instance = new static(static::FILTER_RELATION_OR); diff --git a/src/DTOs/Attention.php b/src/DTOs/Attention.php new file mode 100644 index 0000000..2a22535 --- /dev/null +++ b/src/DTOs/Attention.php @@ -0,0 +1,11 @@ + $customer, - 'layout' => $layout, - 'currency' => $currency, - 'paymentTerms' => $paymentTerms, - ]); - } - - public function addLine(ProductLine $line) - { - } -} diff --git a/src/Resources/Invoice/BookedInvoice.php b/src/Resources/Invoice/BookedInvoice.php new file mode 100644 index 0000000..2d8e56b --- /dev/null +++ b/src/Resources/Invoice/BookedInvoice.php @@ -0,0 +1,22 @@ + [ + 'draftInvoiceNumber' => is_int($draft) ? $draft : $draft->draftInvoiceNumber, + ], + ]); + } +} diff --git a/src/Resources/Invoice/DraftInvoice.php b/src/Resources/Invoice/DraftInvoice.php new file mode 100644 index 0000000..9e3ada6 --- /dev/null +++ b/src/Resources/Invoice/DraftInvoice.php @@ -0,0 +1,77 @@ + $customer, + 'layout' => $layout, + 'currency' => $currency, + 'paymentTerms' => $paymentTerms, + 'date' => $date, + 'recipient' => $recipient, + ]); + } + + public function create() + { + return static::createRequest([ + 'customer' => $this->customer, + 'layout' => $this->layout, + 'currency' => $this->currency, + 'paymentTerms' => $this->paymentTerms, + 'date' => $this->date->format('Y-m-d'), + 'recipient' => $this->recipient, + 'lines' => $this->lines ?? null, + ]); + } + + public function book(): ?BookedInvoice + { + return BookedInvoice::createFromDraft($this); + } +} diff --git a/src/Resources/Invoice/Invoice.php b/src/Resources/Invoice/Invoice.php new file mode 100644 index 0000000..c981cf6 --- /dev/null +++ b/src/Resources/Invoice/Invoice.php @@ -0,0 +1,44 @@ + $customer, + 'layout' => $layout, + 'currency' => $currency, + 'paymentTerms' => $paymentTerms, + ]); + } + + public function addLine(ProductLine $line): static + { + $this->lines[] = $line; + + return $this; + } +} diff --git a/src/Resources/Invoice/ProductLine.php b/src/Resources/Invoice/ProductLine.php index 656ee8e..5b4ecf2 100644 --- a/src/Resources/Invoice/ProductLine.php +++ b/src/Resources/Invoice/ProductLine.php @@ -3,7 +3,41 @@ namespace MorningTrain\Economic\Resources\Invoice; use MorningTrain\Economic\Abstracts\Resource; +use MorningTrain\Economic\Resources\Product; class ProductLine extends Resource { + public ?string $description; + public ?float $discountPercentage; + public ?float $marginInBaseCurrency; + public ?float $marginPercentage; + public ?Product $product; + public ?float $quantity; + public ?int $sortKey; + public ?float $unitCostPrice; + public ?float $unitNetPrice; + + public static function new( + Product|int $product, + float $quantity, + float $unitNetPrice, + string $description = null, + float $discountPercentage = null, + float $marginInBaseCurrency = null, + float $marginPercentage = null, + int $sortKey = null, + float $unitCostPrice = null, + ): static { + return new static([ + 'product' => $product, + 'quantity' => $quantity, + 'unitNetPrice' => $unitNetPrice, + 'description' => $description, + 'discountPercentage' => $discountPercentage, + 'marginInBaseCurrency' => $marginInBaseCurrency, + 'marginPercentage' => $marginPercentage, + 'sortKey' => $sortKey, + 'unitCostPrice' => $unitCostPrice, + ]); + } } diff --git a/src/Resources/Layout.php b/src/Resources/Layout.php index 0134907..bedd5fd 100644 --- a/src/Resources/Layout.php +++ b/src/Resources/Layout.php @@ -5,13 +5,20 @@ use MorningTrain\Economic\Abstracts\Resource; use MorningTrain\Economic\Attributes\Resources\GetCollection; use MorningTrain\Economic\Attributes\Resources\GetSingle; +use MorningTrain\Economic\Attributes\Resources\Properties\PrimaryKey; +use MorningTrain\Economic\Traits\Resources\GetCollectionable; +use MorningTrain\Economic\Traits\Resources\GetSingleable; #[GetCollection('layouts')] #[GetSingle('/layouts/:layoutNumber', ':layoutNumber')] class Layout extends Resource { + use GetCollectionable; + use GetSingleable; + public bool $deleted; + #[PrimaryKey] public int $layoutNumber; public string $name; diff --git a/src/Resources/PaymentTerm.php b/src/Resources/PaymentTerm.php index c598965..3b5eb15 100644 --- a/src/Resources/PaymentTerm.php +++ b/src/Resources/PaymentTerm.php @@ -53,7 +53,7 @@ class PaymentTerm extends Resource public string $name; #[PrimaryKey] - public int $paymentTermNumber; + public int $paymentTermsNumber; public string $paymentTermsType; diff --git a/src/Services/EconomicLoggerService.php b/src/Services/EconomicLoggerService.php index 01ab159..36707a2 100644 --- a/src/Services/EconomicLoggerService.php +++ b/src/Services/EconomicLoggerService.php @@ -9,7 +9,7 @@ class EconomicLoggerService { protected static array $loggers = []; - public static function registerLogger(LoggerInterface $logger, string|array $logLevels = null): void + public static function registerLogger(LoggerInterface $logger, string|array|null $logLevels = null): void { if ($logLevels === null) { static::$loggers['all'][] = $logger; diff --git a/src/Traits/Resources/Creatable.php b/src/Traits/Resources/Creatable.php index 72ea238..4d9d703 100644 --- a/src/Traits/Resources/Creatable.php +++ b/src/Traits/Resources/Creatable.php @@ -16,16 +16,23 @@ public static function createRequest($args, array $endpointReferences = []): ?st { // TODO: add validation method to check if required properties are set and primary key is not set - throw exception if not + $args = json_decode(json_encode($args), true); + + if (method_exists(static::class, 'filterEmpty')) { + $args = static::filterEmpty($args); + } + $response = EconomicApiService::post(static::getEndpoint(Create::class, ...$endpointReferences), $args); if ($response->getStatusCode() !== 201) { - EconomicLoggerService::error('Economic API Service returned a non 201 status code when creating a resource', [ - 'status_code' => $response->getStatusCode(), - 'response_body' => $response->getBody(), - 'resource' => static::class, - 'args' => $args, - 'endpoint_references' => $endpointReferences, - ]); + EconomicLoggerService::error('Economic API Service returned a non 201 status code when creating a resource', + [ + 'status_code' => $response->getStatusCode(), + 'response_body' => $response->getBody(), + 'resource' => static::class, + 'args' => $args, + 'endpoint_references' => $endpointReferences, + ]); throw new Exception('Economic API Service returned a non 201 status code when creating a resource'); } diff --git a/src/Traits/Resources/HasLines.php b/src/Traits/Resources/HasLines.php new file mode 100644 index 0000000..418f1f7 --- /dev/null +++ b/src/Traits/Resources/HasLines.php @@ -0,0 +1,20 @@ +lines[] = $line; + + return $this; + } +} diff --git a/tests/DummyEconomicDriver.php b/tests/DummyEconomicDriver.php index 71acc64..e0eaf7e 100644 --- a/tests/DummyEconomicDriver.php +++ b/tests/DummyEconomicDriver.php @@ -20,25 +20,30 @@ public function __construct(string $appSecretToken, string $agreementGrantToken) public function get(string $url, array $queryArgs = []): EconomicResponse { // TODO: Implement get() method. + ray(func_get_args()); } public function post(string $url, array $body = []): EconomicResponse { // TODO: Implement post() method. + ray(func_get_args()); } public function put(string $url, array $body = []): EconomicResponse { // TODO: Implement put() method. + ray(func_get_args()); } public function delete(string $url): EconomicResponse { // TODO: Implement delete() method. + ray(func_get_args()); } public function patch(string $url): EconomicResponse { // TODO: Implement patch() method. + ray(func_get_args()); } } diff --git a/tests/Unit/InvoiceTest.php b/tests/Unit/InvoiceTest.php new file mode 100644 index 0000000..aa4c177 --- /dev/null +++ b/tests/Unit/InvoiceTest.php @@ -0,0 +1,146 @@ +driver->expects()->post() + ->withArgs(function (string $url, array $body) { + return $url === 'https://restapi.e-conomic.com/invoices/drafts' + && $body === [ + 'customer' => [ + 'customerNumber' => 1, + ], + 'layout' => [ + 'layoutNumber' => 1, + ], + 'currency' => [ + 'isoNumber' => 'DKK', + ], + 'paymentTerms' => [ + 'paymentTermsNumber' => 1, + ], + 'date' => '2021-01-01', + 'recipient' => [ + 'name' => 'John Doe', + 'vatZone' => [ + 'vatZoneNumber' => 1, + ], + ], + 'lines' => [ + [ + 'quantity' => 1, + 'unitNetPrice' => 500, + ], + ], + ]; + }) + ->once() + ->andReturn(new EconomicResponse(201, [])); + + DraftInvoice::new( + 1, + 1, + 'DKK', + 1, + DateTime::createFromFormat('Y-m-d', '2021-01-01'), + new Recipient( + 'John Doe', + new VatZone(1), + ) + ) + ->addLine(ProductLine::new( + product: new Product(1), + quantity: 1, + unitNetPrice: 500 + )) + ->create(); +}); + +it('books draft invoice', function () { + $this->driver->expects()->post() + ->withArgs(function (string $url, array $body) { + return $url === 'https://restapi.e-conomic.com/invoices/drafts'; + }) + ->once() + ->andReturn(new EconomicResponse(201, [ + 'draftInvoiceNumber' => 1, + ])); + + $this->driver->expects()->post() + ->withArgs(function (string $url, array $body) { + return $url === 'https://restapi.e-conomic.com/invoices/booked' + && $body === [ + 'draftInvoice' => [ + 'draftInvoiceNumber' => 1, + ], + ]; + }) + ->once() + ->andReturn(new EconomicResponse(201, [])); + + DraftInvoice::new( + 1, + 1, + 'DKK', + 1, + DateTime::createFromFormat('Y-m-d', '2021-01-01'), + new Recipient( + 'John Doe', + new VatZone(1), + ) + ) + ->create() + ->book(); +}); + +it('can add lines', function () { + $invoice = DraftInvoice::new( + 1, + 1, + 'DKK', + 1, + DateTime::createFromFormat('Y-m-d', '2021-01-01'), + new Recipient( + 'John Doe', + new VatZone(1), + ) + ) + ->addLine(ProductLine::new( + product: new Product(1), + quantity: 1, + unitNetPrice: 500 + )) + ->addLine(ProductLine::new( + product: new Product(2), + quantity: 5, + unitNetPrice: 100, + description: 'Some description', + )); + + expect($invoice->lines) + ->toBeArray() + ->toHaveCount(2); + + expect($invoice->lines[0]) + ->toBeInstanceOf(ProductLine::class) + ->description->toBeNull() + ->discountPercentage->toBeNull() + ->marginInBaseCurrency->toBeNull() + ->marginPercentage->toBeNull() + ->product->toBeInstanceOf(Product::class) + ->quantity->toBe(1.0); + + expect($invoice->lines[1]) + ->toBeInstanceOf(ProductLine::class) + ->description->toBe('Some description') + ->discountPercentage->toBeNull() + ->marginInBaseCurrency->toBeNull() + ->marginPercentage->toBeNull() + ->product->toBeInstanceOf(Product::class) + ->quantity->toBe(5.0); +});