From 257588fd1e8f67e8e908d28d81880c9dc861a50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20J=C3=B8nsson?= Date: Thu, 7 Dec 2023 11:09:02 +0100 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9A=A7=20Start=20Invoice=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Abstracts/Resource.php | 26 +++++++- src/DTOs/Attention.php | 11 ++++ src/DTOs/Recipient.php | 21 ++++++ src/Resources/Invoice.php | 27 -------- src/Resources/Invoice/BookedInvoice.php | 22 +++++++ src/Resources/Invoice/DraftInvoice.php | 74 +++++++++++++++++++++ src/Resources/Invoice/Invoice.php | 41 ++++++++++++ src/Resources/Layout.php | 7 ++ src/Resources/PaymentTerm.php | 2 +- src/Traits/Resources/Creatable.php | 21 ++++-- tests/Unit/InvoiceTest.php | 87 +++++++++++++++++++++++++ 11 files changed, 302 insertions(+), 37 deletions(-) create mode 100644 src/DTOs/Attention.php create mode 100644 src/DTOs/Recipient.php delete mode 100644 src/Resources/Invoice.php create mode 100644 src/Resources/Invoice/BookedInvoice.php create mode 100644 src/Resources/Invoice/DraftInvoice.php create mode 100644 src/Resources/Invoice/Invoice.php create mode 100644 tests/Unit/InvoiceTest.php diff --git a/src/Abstracts/Resource.php b/src/Abstracts/Resource.php index fcf06a9..4da0fae 100644 --- a/src/Abstracts/Resource.php +++ b/src/Abstracts/Resource.php @@ -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/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..00f72c5 --- /dev/null +++ b/src/Resources/Invoice/DraftInvoice.php @@ -0,0 +1,74 @@ + $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, + ]); + } + + 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..72b86c2 --- /dev/null +++ b/src/Resources/Invoice/Invoice.php @@ -0,0 +1,41 @@ + $customer, + 'layout' => $layout, + 'currency' => $currency, + 'paymentTerms' => $paymentTerms, + ]); + } + + public function addLine(ProductLine $line) + { + } +} 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/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/tests/Unit/InvoiceTest.php b/tests/Unit/InvoiceTest.php new file mode 100644 index 0000000..360b1b1 --- /dev/null +++ b/tests/Unit/InvoiceTest.php @@ -0,0 +1,87 @@ +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, + ], + ], + ]; + }) + ->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('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(); +}); From 8ce472f2081781254e6d25d0031e5542ff9f92e7 Mon Sep 17 00:00:00 2001 From: SimonJnsson Date: Thu, 7 Dec 2023 10:09:32 +0000 Subject: [PATCH 2/3] Fix styling --- src/Abstracts/Resource.php | 2 +- src/Classes/EconomicQueryBuilder.php | 4 +-- src/Classes/EconomicQueryFilterBuilder.php | 4 +-- src/Resources/Customer.php | 38 +++++++++++----------- src/Resources/Customer/Contact.php | 10 +++--- src/Services/EconomicLoggerService.php | 2 +- 6 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/Abstracts/Resource.php b/src/Abstracts/Resource.php index 4da0fae..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); 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/Resources/Customer.php b/src/Resources/Customer.php index 498230f..2cfa566 100644 --- a/src/Resources/Customer.php +++ b/src/Resources/Customer.php @@ -107,25 +107,25 @@ public static function create( Currency|string $currency, VatZone|int $vatZone, PaymentTerm|int $paymentTerms, - string $email = null, - string $address = null, - string $zip = null, - string $city = null, - string $country = null, - string $corporateIdentificationNumber = null, - string $pNumber = null, - string $vatNumber = null, - string $ean = null, - string $publicEntryNumber = null, - string $website = null, - string $mobilePhone = null, - string $telephoneAndFaxNumber = null, - bool $barred = null, - bool $eInvoicingDisabledByDefault = null, - float $creditLimit = null, - int $customerNumber = null, - Layout|int $layout = null, - Employee|int $salesPerson = null, + ?string $email = null, + ?string $address = null, + ?string $zip = null, + ?string $city = null, + ?string $country = null, + ?string $corporateIdentificationNumber = null, + ?string $pNumber = null, + ?string $vatNumber = null, + ?string $ean = null, + ?string $publicEntryNumber = null, + ?string $website = null, + ?string $mobilePhone = null, + ?string $telephoneAndFaxNumber = null, + ?bool $barred = null, + ?bool $eInvoicingDisabledByDefault = null, + ?float $creditLimit = null, + ?int $customerNumber = null, + Layout|int|null $layout = null, + Employee|int|null $salesPerson = null, ): static { return static::createRequest(array_filter(compact( 'name', diff --git a/src/Resources/Customer/Contact.php b/src/Resources/Customer/Contact.php index a70defe..1b545f9 100644 --- a/src/Resources/Customer/Contact.php +++ b/src/Resources/Customer/Contact.php @@ -55,11 +55,11 @@ public static function fromCustomer(Customer|int $customer) public static function create( Customer $customer, string $name, - string $email = null, - string $phone = null, - string $notes = null, - string $eInvoiceId = null, - array $emailNotifications = null + ?string $email = null, + ?string $phone = null, + ?string $notes = null, + ?string $eInvoiceId = null, + ?array $emailNotifications = null ) { return static::createRequest(array_filter(compact( 'customer', 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; From a2f1301a88278185291c6abca4979854f7228898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20J=C3=B8nsson?= Date: Thu, 7 Dec 2023 11:56:04 +0100 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20Handle=20lines=20in=20invoice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 2 +- src/Resources/Invoice/DraftInvoice.php | 3 ++ src/Resources/Invoice/Invoice.php | 7 ++- src/Resources/Invoice/ProductLine.php | 34 ++++++++++++++ src/Traits/Resources/HasLines.php | 20 +++++++++ tests/DummyEconomicDriver.php | 5 +++ tests/Unit/InvoiceTest.php | 61 +++++++++++++++++++++++++- 7 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 src/Traits/Resources/HasLines.php 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/Resources/Invoice/DraftInvoice.php b/src/Resources/Invoice/DraftInvoice.php index 00f72c5..9e3ada6 100644 --- a/src/Resources/Invoice/DraftInvoice.php +++ b/src/Resources/Invoice/DraftInvoice.php @@ -15,6 +15,7 @@ use MorningTrain\Economic\Traits\Resources\Creatable; use MorningTrain\Economic\Traits\Resources\GetCollectionable; use MorningTrain\Economic\Traits\Resources\GetSingleable; +use MorningTrain\Economic\Traits\Resources\HasLines; #[GetCollection('invoices/drafts')] #[GetSingle('invoices/drafts/:draftInvoiceNumber', ':draftInvoiceNumber')] @@ -22,6 +23,7 @@ class DraftInvoice extends Resource { use Creatable, GetCollectionable, GetSingleable; + use HasLines; public Customer $customer; @@ -64,6 +66,7 @@ public function create() 'paymentTerms' => $this->paymentTerms, 'date' => $this->date->format('Y-m-d'), 'recipient' => $this->recipient, + 'lines' => $this->lines ?? null, ]); } diff --git a/src/Resources/Invoice/Invoice.php b/src/Resources/Invoice/Invoice.php index 72b86c2..c981cf6 100644 --- a/src/Resources/Invoice/Invoice.php +++ b/src/Resources/Invoice/Invoice.php @@ -25,7 +25,7 @@ public static function new( Customer|int $customer, Layout|int $layout, Currency|string $currency, - PaymentTerm|int $paymentTerms + PaymentTerm|int $paymentTerms, ) { return new static([ 'customer' => $customer, @@ -35,7 +35,10 @@ public static function new( ]); } - public function addLine(ProductLine $line) + 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/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 index 360b1b1..aa4c177 100644 --- a/tests/Unit/InvoiceTest.php +++ b/tests/Unit/InvoiceTest.php @@ -3,6 +3,8 @@ use MorningTrain\Economic\Classes\EconomicResponse; use MorningTrain\Economic\DTOs\Recipient; use MorningTrain\Economic\Resources\Invoice\DraftInvoice; +use MorningTrain\Economic\Resources\Invoice\ProductLine; +use MorningTrain\Economic\Resources\Product; use MorningTrain\Economic\Resources\VatZone; it('creates draft invoice', function () { @@ -29,6 +31,12 @@ 'vatZoneNumber' => 1, ], ], + 'lines' => [ + [ + 'quantity' => 1, + 'unitNetPrice' => 500, + ], + ], ]; }) ->once() @@ -45,8 +53,12 @@ new VatZone(1), ) ) + ->addLine(ProductLine::new( + product: new Product(1), + quantity: 1, + unitNetPrice: 500 + )) ->create(); - //->book(); }); it('books draft invoice', function () { @@ -85,3 +97,50 @@ ->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); +});