From 8f31fbd10e003aa4e012852feb08badb99a7f6d5 Mon Sep 17 00:00:00 2001 From: Krishan Koenig Date: Tue, 10 Dec 2024 13:50:24 +0100 Subject: [PATCH] add sales invoices endpoint --- docs/endpoint-collections.md | 66 +++++++ .../SalesInvoiceEndpointCollection.php | 109 ++++++++++++ .../CreateSalesInvoicePayloadFactory.php | 40 +++++ .../InvoiceLineCollectionFactory.php | 16 ++ src/Factories/InvoiceLineFactory.php | 20 +++ src/Factories/RecipientFactory.php | 30 ++++ .../UpdateSalesInvoicePayloadFactory.php | 36 ++++ .../Payload/CreateSalesInvoicePayload.php | 93 ++++++++++ src/Http/Payload/Discount.php | 31 ++++ src/Http/Payload/EmailDetails.php | 30 ++++ src/Http/Payload/InvoiceLine.php | 41 +++++ src/Http/Payload/PaymentDetails.php | 31 ++++ src/Http/Payload/Recipient.php | 98 ++++++++++ .../Payload/UpdateSalesInvoicePayload.php | 69 ++++++++ .../Requests/CreateSalesInvoiceRequest.php | 34 ++++ .../Requests/DeleteSalesInvoiceRequest.php | 23 +++ .../GetPaginatedSalesInvoicesRequest.php | 20 +++ src/Http/Requests/GetSalesInvoiceRequest.php | 31 ++++ .../Requests/UpdateSalesInvoiceRequest.php | 36 ++++ src/MollieApiClient.php | 6 +- src/Resources/SalesInvoice.php | 167 ++++++++++++++++++ src/Resources/SalesInvoiceCollection.php | 16 ++ src/Traits/HasEndpoints.php | 2 + src/Types/PaymentTerm.php | 14 ++ src/Types/RecipientType.php | 9 + src/Types/SalesInvoiceStatus.php | 21 +++ src/Types/VatMode.php | 9 + src/Types/VatScheme.php | 9 + .../SalesInvoiceEndpointCollectionTest.php | 136 ++++++++++++++ .../Responses/sales-invoice-list.json | 83 +++++++++ tests/Fixtures/Responses/sales-invoice.json | 81 +++++++++ .../CreateSalesInvoiceRequestTest.php | 98 ++++++++++ .../DeleteSalesInvoiceRequestTest.php | 34 ++++ .../GetPaginatedSalesInvoicesRequestTest.php | 69 ++++++++ .../Requests/GetSalesInvoiceRequestTest.php | 26 +++ .../UpdateSalesInvoiceRequestTest.php | 44 +++++ 36 files changed, 1676 insertions(+), 2 deletions(-) create mode 100644 src/EndpointCollection/SalesInvoiceEndpointCollection.php create mode 100644 src/Factories/CreateSalesInvoicePayloadFactory.php create mode 100644 src/Factories/InvoiceLineCollectionFactory.php create mode 100644 src/Factories/InvoiceLineFactory.php create mode 100644 src/Factories/RecipientFactory.php create mode 100644 src/Factories/UpdateSalesInvoicePayloadFactory.php create mode 100644 src/Http/Payload/CreateSalesInvoicePayload.php create mode 100644 src/Http/Payload/Discount.php create mode 100644 src/Http/Payload/EmailDetails.php create mode 100644 src/Http/Payload/InvoiceLine.php create mode 100644 src/Http/Payload/PaymentDetails.php create mode 100644 src/Http/Payload/Recipient.php create mode 100644 src/Http/Payload/UpdateSalesInvoicePayload.php create mode 100644 src/Http/Requests/CreateSalesInvoiceRequest.php create mode 100644 src/Http/Requests/DeleteSalesInvoiceRequest.php create mode 100644 src/Http/Requests/GetPaginatedSalesInvoicesRequest.php create mode 100644 src/Http/Requests/GetSalesInvoiceRequest.php create mode 100644 src/Http/Requests/UpdateSalesInvoiceRequest.php create mode 100644 src/Resources/SalesInvoice.php create mode 100644 src/Resources/SalesInvoiceCollection.php create mode 100644 src/Types/PaymentTerm.php create mode 100644 src/Types/RecipientType.php create mode 100644 src/Types/SalesInvoiceStatus.php create mode 100644 src/Types/VatMode.php create mode 100644 src/Types/VatScheme.php create mode 100644 tests/EndpointCollection/SalesInvoiceEndpointCollectionTest.php create mode 100644 tests/Fixtures/Responses/sales-invoice-list.json create mode 100644 tests/Fixtures/Responses/sales-invoice.json create mode 100644 tests/Http/Requests/CreateSalesInvoiceRequestTest.php create mode 100644 tests/Http/Requests/DeleteSalesInvoiceRequestTest.php create mode 100644 tests/Http/Requests/GetPaginatedSalesInvoicesRequestTest.php create mode 100644 tests/Http/Requests/GetSalesInvoiceRequestTest.php create mode 100644 tests/Http/Requests/UpdateSalesInvoiceRequestTest.php diff --git a/docs/endpoint-collections.md b/docs/endpoint-collections.md index 6a0f7c2c..4dee3bf8 100644 --- a/docs/endpoint-collections.md +++ b/docs/endpoint-collections.md @@ -521,6 +521,72 @@ $refund = $mollie->refunds->createForPayment($paymentId, [ $refunds = $mollie->refunds->page(); ``` +## Sales Invoices + +[Official Documentation TBA] + +**Available Payloads:** +- `CreateSalesInvoicePayload` - For creating sales invoices +- `UpdateSalesInvoicePayload` - For updating existing sales invoices + +**Available Queries:** +- `GetPaginatedSalesInvoiceQuery` - For listing sales invoices with pagination + +### Sales Invoice Management + +```php +use Mollie\Api\Types\VatMode; +use Mollie\Api\Types\VatScheme; +use Mollie\Api\Types\PaymentTerm; +use Mollie\Api\Types\RecipientType; +use Mollie\Api\Types\RecipientType; +use Mollie\Api\Types\SalesInvoiceStatus; + +// Create a sales invoice +$salesInvoice = $mollie->salesInvoices->create([ + 'currency' => 'EUR', + 'status' => SalesInvoiceStatus::DRAFT, + 'vatScheme' => VatScheme::STANDARD, + 'vatMode' => VatMode::INCLUSIVE, + 'paymentTerm' => PaymentTerm::DAYS_30, + 'recipientIdentifier' => 'XXXXX', + 'recipient' => [ + 'type' => RecipientType::CONSUMER, + 'email' => 'darth@vader.deathstar', + 'streetAndNumber' => 'Sample Street 12b', + 'postalCode' => '2000 AA', + 'city' => 'Amsterdam', + 'country' => 'NL', + 'locale' => 'nl_NL' + ], + 'lines' => [ + [ + 'description' => 'Monthly subscription fee', + 'quantity' => 1, + 'vatRate' => '21', + 'unitPrice' => [ + 'currency' => 'EUR', + 'value' => '10,00' + ] + ] + ] +]); + +// Get a sales invoice +$salesInvoice = $mollie->salesInvoices->get('invoice_12345'); + +// Update a sales invoice +$salesInvoice = $mollie->salesInvoices->update('invoice_12345', [ + 'description' => 'Updated description' +]); + +// Delete a sales invoice +$mollie->salesInvoices->delete('invoice_12345'); + +// List sales invoices +$salesInvoices = $mollie->salesInvoices->page(); +``` + ## Sessions [Official Documentation](https://docs.mollie.com/reference/v2/sessions-api/create-session) diff --git a/src/EndpointCollection/SalesInvoiceEndpointCollection.php b/src/EndpointCollection/SalesInvoiceEndpointCollection.php new file mode 100644 index 00000000..c1b28f78 --- /dev/null +++ b/src/EndpointCollection/SalesInvoiceEndpointCollection.php @@ -0,0 +1,109 @@ +send(new GetSalesInvoiceRequest($id)); + } + + /** + * Creates a SalesInvoice in Mollie. + * + * @param array|CreateSalesInvoicePayload $payload + * + * @throws ApiException + */ + public function create($payload = []): SalesInvoice + { + if (! $payload instanceof CreateSalesInvoicePayload) { + $payload = CreateSalesInvoicePayloadFactory::new($payload)->create(); + } + + return $this->send(new CreateSalesInvoiceRequest($payload)); + } + + /** + * Update a specific SalesInvoice resource. + * + * @throws ApiException + */ + public function update(string $id, $payload = []): ?SalesInvoice + { + if (! $payload instanceof UpdateSalesInvoicePayload) { + $payload = UpdateSalesInvoicePayloadFactory::new($payload)->create(); + } + + return $this->send(new UpdateSalesInvoiceRequest($id, $payload)); + } + + /** + * Delete a SalesInvoice from Mollie. + * + * @throws ApiException + */ + public function delete(string $id): void + { + $this->send(new DeleteSalesInvoiceRequest($id)); + } + + /** + * Retrieves a collection of SalesInvoices from Mollie. + * + * @throws ApiException + */ + public function page(?string $from = null, ?int $limit = null): SalesInvoiceCollection + { + $query = PaginatedQueryFactory::new([ + 'from' => $from, + 'limit' => $limit, + ])->create(); + + return $this->send(new GetPaginatedSalesInvoicesRequest($query)); + } + + /** + * Create an iterator for iterating over sales invoices retrieved from Mollie. + * + * @param string|null $from The first resource ID you want to include in your list. + * @param bool $iterateBackwards Set to true for reverse order iteration (default is false). + */ + public function iterator( + ?string $from = null, + ?int $limit = null, + bool $iterateBackwards = false + ): LazyCollection { + $query = PaginatedQueryFactory::new([ + 'from' => $from, + 'limit' => $limit, + ])->create(); + + return $this->send( + (new GetPaginatedSalesInvoicesRequest($query)) + ->useIterator() + ->setIterationDirection($iterateBackwards) + ); + } +} diff --git a/src/Factories/CreateSalesInvoicePayloadFactory.php b/src/Factories/CreateSalesInvoicePayloadFactory.php new file mode 100644 index 00000000..de6200a6 --- /dev/null +++ b/src/Factories/CreateSalesInvoicePayloadFactory.php @@ -0,0 +1,40 @@ +get('currency'), + $this->get('status'), + $this->get('vatScheme'), + $this->get('vatMode'), + $this->get('paymentTerm'), + $this->get('recipientIdentifier'), + RecipientFactory::new($this->get('recipient'))->create(), + $this + ->mapIfNotNull( + 'lines', + fn(array $items) => InvoiceLineCollectionFactory::new($items)->create() + ), + $this->get('profileId'), + $this->get('memo'), + $this->mapIfNotNull('paymentDetails', fn(array $data) => PaymentDetails::fromArray($data)), + $this->mapIfNotNull('emailDetails', fn(array $data) => EmailDetails::fromArray($data)), + $this->get('webhookUrl'), + $this->mapIfNotNull('discount', fn(array $data) => Discount::fromArray($data)) + ); + } +} diff --git a/src/Factories/InvoiceLineCollectionFactory.php b/src/Factories/InvoiceLineCollectionFactory.php new file mode 100644 index 00000000..d90f10f3 --- /dev/null +++ b/src/Factories/InvoiceLineCollectionFactory.php @@ -0,0 +1,16 @@ + InvoiceLineFactory::new($item)->create(), + $this->data + )); + } +} diff --git a/src/Factories/InvoiceLineFactory.php b/src/Factories/InvoiceLineFactory.php new file mode 100644 index 00000000..bd929792 --- /dev/null +++ b/src/Factories/InvoiceLineFactory.php @@ -0,0 +1,20 @@ +get('description'), + $this->get('quantity'), + $this->get('vatRate'), + MoneyFactory::new($this->get('unitPrice'))->create(), + $this->mapIfNotNull('discount', fn(array $data) => Discount::fromArray($data)) + ); + } +} diff --git a/src/Factories/RecipientFactory.php b/src/Factories/RecipientFactory.php new file mode 100644 index 00000000..31d64a87 --- /dev/null +++ b/src/Factories/RecipientFactory.php @@ -0,0 +1,30 @@ +get('type'), + $this->get('email'), + $this->get('streetAndNumber'), + $this->get('postalCode'), + $this->get('city'), + $this->get('country'), + $this->get('locale'), + $this->get('title'), + $this->get('givenName'), + $this->get('familyName'), + $this->get('organizationName'), + $this->get('organizationNumber'), + $this->get('vatNumber'), + $this->get('phone'), + $this->get('streetAdditional'), + $this->get('region'), + ); + } +} diff --git a/src/Factories/UpdateSalesInvoicePayloadFactory.php b/src/Factories/UpdateSalesInvoicePayloadFactory.php new file mode 100644 index 00000000..005780d2 --- /dev/null +++ b/src/Factories/UpdateSalesInvoicePayloadFactory.php @@ -0,0 +1,36 @@ +get('status'), + $this->get('recipientIdentifier'), + $this->get('paymentTerm'), + $this->get('memo'), + $this->mapIfNotNull('paymentDetails', fn(array $data) => PaymentDetails::fromArray($data)), + $this->mapIfNotNull('emailDetails', fn(array $data) => EmailDetails::fromArray($data)), + $this->mapIfNotNull('recipient', fn(array $data) => RecipientFactory::new($data)->create()), + $this + ->mapIfNotNull( + 'lines', + fn(array $items) => InvoiceLineCollectionFactory::new($items)->create() + ), + $this->get('webhookUrl'), + $this->mapIfNotNull('discount', fn(array $data) => Discount::fromArray($data)) + ); + } +} diff --git a/src/Http/Payload/CreateSalesInvoicePayload.php b/src/Http/Payload/CreateSalesInvoicePayload.php new file mode 100644 index 00000000..4ff3b308 --- /dev/null +++ b/src/Http/Payload/CreateSalesInvoicePayload.php @@ -0,0 +1,93 @@ + + */ + public DataCollection $lines; + + public ?string $webhookUrl; + + public ?Discount $discount; + + public function __construct( + string $currency, + string $status, + string $vatScheme, + string $vatMode, + string $paymentTerm, + string $recipientIdentifier, + Recipient $recipient, + DataCollection $lines, + ?string $profileId = null, + ?string $memo = null, + ?PaymentDetails $paymentDetails = null, + ?EmailDetails $emailDetails = null, + ?string $webhookUrl = null, + ?Discount $discount = null, + ) { + $this->profileId = $profileId; + $this->currency = $currency; + $this->status = $status; + $this->vatScheme = $vatScheme; + $this->vatMode = $vatMode; + $this->memo = $memo; + $this->paymentTerm = $paymentTerm; + $this->paymentDetails = $paymentDetails; + $this->emailDetails = $emailDetails; + $this->recipientIdentifier = $recipientIdentifier; + $this->recipient = $recipient; + $this->lines = $lines; + $this->webhookUrl = $webhookUrl; + $this->discount = $discount; + } + + public function data(): array + { + return [ + 'profileId' => $this->profileId, + 'currency' => $this->currency, + 'status' => $this->status, + 'vatScheme' => $this->vatScheme, + 'vatMode' => $this->vatMode, + 'memo' => $this->memo, + 'paymentTerm' => $this->paymentTerm, + 'paymentDetails' => $this->paymentDetails, + 'emailDetails' => $this->emailDetails, + 'recipientIdentifier' => $this->recipientIdentifier, + 'recipient' => $this->recipient, + 'lines' => $this->lines, + 'webhookUrl' => $this->webhookUrl, + 'discount' => $this->discount, + ]; + } +} diff --git a/src/Http/Payload/Discount.php b/src/Http/Payload/Discount.php new file mode 100644 index 00000000..3207fe1e --- /dev/null +++ b/src/Http/Payload/Discount.php @@ -0,0 +1,31 @@ +type = $type; + $this->value = $value; + } + + public function data() + { + return [ + 'type' => $this->type, + 'value' => $this->value, + ]; + } +} diff --git a/src/Http/Payload/EmailDetails.php b/src/Http/Payload/EmailDetails.php new file mode 100644 index 00000000..be78009d --- /dev/null +++ b/src/Http/Payload/EmailDetails.php @@ -0,0 +1,30 @@ +subject = $subject; + $this->body = $body; + } + + public function data() + { + return [ + 'subject' => $this->subject, + 'body' => $this->body, + ]; + } +} diff --git a/src/Http/Payload/InvoiceLine.php b/src/Http/Payload/InvoiceLine.php new file mode 100644 index 00000000..ced72bd6 --- /dev/null +++ b/src/Http/Payload/InvoiceLine.php @@ -0,0 +1,41 @@ +description = $description; + $this->quantity = $quantity; + $this->vatRate = $vatRate; + $this->unitPrice = $unitPrice; + $this->discount = $discount; + } + + public function data() + { + return [ + 'description' => $this->description, + 'quantity' => $this->quantity, + 'vatRate' => $this->vatRate, + 'unitPrice' => $this->unitPrice, + 'discount' => $this->discount, + ]; + } +} diff --git a/src/Http/Payload/PaymentDetails.php b/src/Http/Payload/PaymentDetails.php new file mode 100644 index 00000000..ad224023 --- /dev/null +++ b/src/Http/Payload/PaymentDetails.php @@ -0,0 +1,31 @@ +source = $source; + $this->sourceDescription = $sourceDescription; + } + + public function data() + { + return [ + 'source' => $this->source, + 'sourceDescription' => $this->sourceDescription, + ]; + } +} diff --git a/src/Http/Payload/Recipient.php b/src/Http/Payload/Recipient.php new file mode 100644 index 00000000..bff22594 --- /dev/null +++ b/src/Http/Payload/Recipient.php @@ -0,0 +1,98 @@ +type = $type; + $this->title = $title; + $this->givenName = $givenName; + $this->familyName = $familyName; + $this->organizationName = $organizationName; + $this->organizationNumber = $organizationNumber; + $this->vatNumber = $vatNumber; + $this->email = $email; + $this->phone = $phone; + $this->streetAndNumber = $streetAndNumber; + $this->streetAdditional = $streetAdditional; + $this->postalCode = $postalCode; + $this->city = $city; + $this->region = $region; + $this->country = $country; + $this->locale = $locale; + } + + public function data() + { + return [ + 'type' => $this->type, + 'title' => $this->title, + 'givenName' => $this->givenName, + 'familyName' => $this->familyName, + 'organizationName' => $this->organizationName, + 'organizationNumber' => $this->organizationNumber, + 'vatNumber' => $this->vatNumber, + 'email' => $this->email, + 'phone' => $this->phone, + 'streetAndNumber' => $this->streetAndNumber, + 'streetAdditional' => $this->streetAdditional, + 'postalCode' => $this->postalCode, + 'city' => $this->city, + 'region' => $this->region, + 'country' => $this->country, + 'locale' => $this->locale, + ]; + } +} diff --git a/src/Http/Payload/UpdateSalesInvoicePayload.php b/src/Http/Payload/UpdateSalesInvoicePayload.php new file mode 100644 index 00000000..634a25f0 --- /dev/null +++ b/src/Http/Payload/UpdateSalesInvoicePayload.php @@ -0,0 +1,69 @@ + + */ + public ?DataCollection $lines; + + public ?string $webhookUrl; + + public ?Discount $discount; + + public function __construct( + string $status, + string $recipientIdentifier, + ?string $paymentTerm = null, + ?string $memo = null, + ?PaymentDetails $paymentDetails = null, + ?EmailDetails $emailDetails = null, + ?Recipient $recipient = null, + ?DataCollection $lines = null, + ?string $webhookUrl = null, + ?Discount $discount = null, + ) { + $this->status = $status; + $this->paymentTerm = $paymentTerm; + $this->recipientIdentifier = $recipientIdentifier; + $this->memo = $memo; + $this->paymentDetails = $paymentDetails; + $this->emailDetails = $emailDetails; + $this->recipient = $recipient; + $this->lines = $lines; + $this->webhookUrl = $webhookUrl; + $this->discount = $discount; + } + + public function data(): array + { + return [ + 'status' => $this->status, + 'memo' => $this->memo, + 'paymentTerm' => $this->paymentTerm, + 'paymentDetails' => $this->paymentDetails, + 'emailDetails' => $this->emailDetails, + 'recipientIdentifier' => $this->recipientIdentifier, + 'recipient' => $this->recipient, + 'lines' => $this->lines, + 'webhookUrl' => $this->webhookUrl, + 'discount' => $this->discount, + ]; + } +} diff --git a/src/Http/Requests/CreateSalesInvoiceRequest.php b/src/Http/Requests/CreateSalesInvoiceRequest.php new file mode 100644 index 00000000..18168770 --- /dev/null +++ b/src/Http/Requests/CreateSalesInvoiceRequest.php @@ -0,0 +1,34 @@ +payload = $payload; + } + + public function resolveResourcePath(): string + { + return 'sales-invoices'; + } + + public function defaultPayload(): array + { + return $this->payload->toArray(); + } +} diff --git a/src/Http/Requests/DeleteSalesInvoiceRequest.php b/src/Http/Requests/DeleteSalesInvoiceRequest.php new file mode 100644 index 00000000..c1d56cf2 --- /dev/null +++ b/src/Http/Requests/DeleteSalesInvoiceRequest.php @@ -0,0 +1,23 @@ +id = $id; + } + + public function resolveResourcePath(): string + { + return "sales-invoices/{$this->id}"; + } +} diff --git a/src/Http/Requests/GetPaginatedSalesInvoicesRequest.php b/src/Http/Requests/GetPaginatedSalesInvoicesRequest.php new file mode 100644 index 00000000..d88095a9 --- /dev/null +++ b/src/Http/Requests/GetPaginatedSalesInvoicesRequest.php @@ -0,0 +1,20 @@ +id = $id; + } + + public function resolveResourcePath(): string + { + return "sales-invoices/{$this->id}"; + } +} diff --git a/src/Http/Requests/UpdateSalesInvoiceRequest.php b/src/Http/Requests/UpdateSalesInvoiceRequest.php new file mode 100644 index 00000000..1584378d --- /dev/null +++ b/src/Http/Requests/UpdateSalesInvoiceRequest.php @@ -0,0 +1,36 @@ +id = $id; + $this->payload = $payload; + } + + public function resolveResourcePath(): string + { + return "sales-invoices/{$this->id}"; + } + + public function defaultPayload(): array + { + return $this->payload->toArray(); + } +} diff --git a/src/MollieApiClient.php b/src/MollieApiClient.php index 02747e76..77935bc6 100644 --- a/src/MollieApiClient.php +++ b/src/MollieApiClient.php @@ -42,6 +42,7 @@ use Mollie\Api\EndpointCollection\SubscriptionPaymentEndpointCollection; use Mollie\Api\EndpointCollection\TerminalEndpointCollection; use Mollie\Api\EndpointCollection\WalletEndpointCollection; +use Mollie\Api\EndpointCollection\SalesInvoiceEndpointCollection; use Mollie\Api\Helpers\Url; use Mollie\Api\Http\Adapter\MollieHttpAdapterPicker; use Mollie\Api\Idempotency\DefaultIdempotencyKeyGenerator; @@ -84,15 +85,16 @@ * @property ProfileEndpointCollection $profiles * @property ProfileMethodEndpointCollection $profileMethods * @property RefundEndpointCollection $refunds - * @property SettlementEndpointCollection $settlements + * @property SalesInvoiceEndpointCollection $salesInvoices + * @property SessionEndpointCollection $sessions * @property SettlementCaptureEndpointCollection $settlementCaptures * @property SettlementChargebackEndpointCollection $settlementChargebacks + * @property SettlementEndpointCollection $settlements * @property SettlementPaymentEndpointCollection $settlementPayments * @property SettlementRefundEndpointCollection $settlementRefunds * @property SubscriptionEndpointCollection $subscriptions * @property SubscriptionPaymentEndpointCollection $subscriptionPayments * @property TerminalEndpointCollection $terminals - * @property SessionEndpointCollection $sessions * @property WalletEndpointCollection $wallets * @property HttpAdapterContract $httpClient */ diff --git a/src/Resources/SalesInvoice.php b/src/Resources/SalesInvoice.php new file mode 100644 index 00000000..432d0cde --- /dev/null +++ b/src/Resources/SalesInvoice.php @@ -0,0 +1,167 @@ +status === SalesInvoiceStatus::DRAFT; + } + + /** + * Returns whether the sales invoice is issued. + * + * @return bool + */ + public function isIssued() + { + return $this->status === SalesInvoiceStatus::ISSUED; + } + + /** + * Returns whether the sales invoice is paid. + * + * @return bool + */ + public function isPaid() + { + return $this->status === SalesInvoiceStatus::PAID; + } +} diff --git a/src/Resources/SalesInvoiceCollection.php b/src/Resources/SalesInvoiceCollection.php new file mode 100644 index 00000000..4123f8b8 --- /dev/null +++ b/src/Resources/SalesInvoiceCollection.php @@ -0,0 +1,16 @@ + ProfileEndpointCollection::class, 'profileMethods' => ProfileMethodEndpointCollection::class, 'refunds' => RefundEndpointCollection::class, + 'salesInvoices' => SalesInvoiceEndpointCollection::class, 'sessions' => SessionEndpointCollection::class, 'settlementCaptures' => SettlementCaptureEndpointCollection::class, 'settlementChargebacks' => SettlementChargebackEndpointCollection::class, diff --git a/src/Types/PaymentTerm.php b/src/Types/PaymentTerm.php new file mode 100644 index 00000000..9b9e4372 --- /dev/null +++ b/src/Types/PaymentTerm.php @@ -0,0 +1,14 @@ + new MockResponse(200, 'sales-invoice'), + ]); + + $salesInvoice = $client->salesInvoices->get('inv_123'); + + $this->assertInstanceOf(SalesInvoice::class, $salesInvoice); + } + + /** @test */ + public function create() + { + $client = new MockClient([ + CreateSalesInvoiceRequest::class => new MockResponse(201, 'sales-invoice'), + ]); + + $invoiceLines = [ + new InvoiceLine( + 'Monthly subscription fee', + 1, + '21', + new Money('EUR', '10,00'), + ) + ]; + + // Create a sales invoice + $payload = new CreateSalesInvoicePayload( + 'EUR', + SalesInvoiceStatus::DRAFT, + VatScheme::STANDARD, + VatMode::INCLUSIVE, + PaymentTerm::DAYS_30, + 'XXXXX', + new Recipient( + RecipientType::CONSUMER, + 'darth@vader.deathstar', + 'Sample Street 12b', + '2000 AA', + 'Amsterdam', + 'NL', + 'nl_NL' + ), + new DataCollection($invoiceLines) + ); + + $salesInvoice = $client->salesInvoices->create($payload); + + $this->assertInstanceOf(SalesInvoice::class, $salesInvoice); + } + + /** @test */ + public function update() + { + $client = new MockClient([ + UpdateSalesInvoiceRequest::class => new MockResponse(200, 'sales-invoice'), + ]); + + $payload = new UpdateSalesInvoicePayload( + SalesInvoiceStatus::PAID, + 'XXXXX', + ); + $salesInvoice = $client->salesInvoices->update('invoice_123', $payload); + + $this->assertInstanceOf(SalesInvoice::class, $salesInvoice); + } + + /** @test */ + public function delete() + { + $client = new MockClient([ + DeleteSalesInvoiceRequest::class => new MockResponse(204), + ]); + + $client->salesInvoices->delete('invoice_123'); + + $this->assertTrue(true); // Test passes if no exception is thrown + } + + /** @test */ + public function page() + { + $client = new MockClient([ + GetPaginatedSalesInvoicesRequest::class => new MockResponse(200, 'sales-invoice-list'), + ]); + + $salesInvoices = $client->salesInvoices->page(); + + $this->assertInstanceOf(SalesInvoiceCollection::class, $salesInvoices); + } + + /** @test */ + public function iterate() + { + $client = new MockClient([ + GetPaginatedSalesInvoicesRequest::class => new MockResponse(200, 'sales-invoice-list'), + DynamicGetRequest::class => new MockResponse(200, 'empty-list', 'sales_invoices'), + ]); + + /** @var SalesInvoice $salesInvoice */ + foreach ($client->salesInvoices->iterator() as $salesInvoice) { + $this->assertInstanceOf(SalesInvoice::class, $salesInvoice); + } + } +} diff --git a/tests/Fixtures/Responses/sales-invoice-list.json b/tests/Fixtures/Responses/sales-invoice-list.json new file mode 100644 index 00000000..fdd397d2 --- /dev/null +++ b/tests/Fixtures/Responses/sales-invoice-list.json @@ -0,0 +1,83 @@ +{ + "count": 1, + "_embedded": { + "sales_invoices": [ + { + "resource": "sales-invoice", + "id": "invoice_4Y0eZitmBnQ6IDoMqZQKh", + "profileId": "pfl_QkEhN94Ba", + "invoiceNumber": null, + "currency": "EUR", + "status": "draft", + "vatScheme": "standard", + "paymentTerm": "30 days", + "recipientIdentifier": "123532354", + "recipient": { + "title": null, + "givenName": "Given", + "familyName": "Family", + "email": "given.family@mollie.com", + "phone": null, + "streetAndNumber": "Street 1", + "streetAdditional": null, + "postalCode": "1000 AA", + "city": "Amsterdam", + "region": null, + "country": "NL" + }, + "lines": [ + { + "description": "LEGO 4440 Forest Police Station", + "quantity": 1, + "vatRate": "21", + "unitPrice": { + "value": "89.00", + "currency": "EUR" + }, + "discount": null + } + ], + "discount": null, + "amountDue": { + "value": "107.69", + "currency": "EUR" + }, + "subtotalAmount": { + "value": "89.00", + "currency": "EUR" + }, + "totalAmount": { + "value": "107.69", + "currency": "EUR" + }, + "totalVatAmount": { + "value": "18.69", + "currency": "EUR" + }, + "discountedSubtotalAmount": { + "value": "89.00", + "currency": "EUR" + }, + "createdAt": "2024-10-03T10:47:38.457381+00:00", + "issuedAt": null, + "dueAt": null, + "memo": null + } + ] + }, + "_links": { + "self": { + "href": "...", + "type": "application/hal+json" + }, + "previous": null, + "next": { + "href": "https://api.mollie.com/v2/sales/invoices?from=invoice_4yUfQpbKnd2DUTouUdUwH&limit=5", + "type": "application/hal+json" + }, + "documentation": { + "href": "...", + "type": "text/html" + } + } +} diff --git a/tests/Fixtures/Responses/sales-invoice.json b/tests/Fixtures/Responses/sales-invoice.json new file mode 100644 index 00000000..bda9bba0 --- /dev/null +++ b/tests/Fixtures/Responses/sales-invoice.json @@ -0,0 +1,81 @@ +{ + "resource": "sales-invoice", + "id": "invoice_4Y0eZitmBnQ6IDoMqZQKh", + "profileId": "pfl_QkEhN94Ba", + "invoiceNumber": null, + "currency": "EUR", + "status": "draft", + "vatScheme": "standard", + "paymentTerm": "30 days", + "recipientIdentifier": "123532354", + "recipient": { + "type": "consumer", + "title": null, + "givenName": "Given", + "familyName": "Family", + "email": "given.family@mollie.com", + "phone": null, + "streetAndNumber": "Street 1", + "streetAdditional": null, + "postalCode": "1000 AA", + "city": "Amsterdam", + "region": null, + "country": "NL", + "locale": "nl_NL" + }, + "lines": [ + { + "description": "LEGO 4440 Forest Police Station", + "quantity": 1, + "vatRate": "21", + "unitPrice": { + "value": "89.00", + "currency": "EUR" + }, + "discount": null + } + ], + "discount": null, + "amountDue": { + "value": "107.69", + "currency": "EUR" + }, + "subtotalAmount": { + "value": "89.00", + "currency": "EUR" + }, + "totalAmount": { + "value": "107.69", + "currency": "EUR" + }, + "totalVatAmount": { + "value": "18.69", + "currency": "EUR" + }, + "discountedSubtotalAmount": { + "value": "89.00", + "currency": "EUR" + }, + "createdAt": "2024-10-03T10:47:38.457381+00:00", + "issuedAt": null, + "dueAt": null, + "memo": null, + "_links": { + "self": { + "href": "...", + "type": "application/hal+json" + }, + "invoicePayment": { + "href": "...", + "type": "application/hal+json" + }, + "pdfLink": { + "href": "...", + "type": "application/hal+json" + }, + "documentation": { + "href": "...", + "type": "text/html" + } + } +} diff --git a/tests/Http/Requests/CreateSalesInvoiceRequestTest.php b/tests/Http/Requests/CreateSalesInvoiceRequestTest.php new file mode 100644 index 00000000..60f71ef5 --- /dev/null +++ b/tests/Http/Requests/CreateSalesInvoiceRequestTest.php @@ -0,0 +1,98 @@ + new MockResponse(201, 'sales-invoice'), + ]); + + $invoiceLines = [ + new InvoiceLine( + 'Monthly subscription fee', + 1, + '21', + new Money('EUR', '10,00'), + ) + ]; + + // Create a sales invoice + $payload = new CreateSalesInvoicePayload( + 'EUR', + SalesInvoiceStatus::DRAFT, + VatScheme::STANDARD, + VatMode::INCLUSIVE, + PaymentTerm::DAYS_30, + 'XXXXX', + new Recipient( + RecipientType::CONSUMER, + 'darth@vader.deathstar', + 'Sample Street 12b', + '2000 AA', + 'Amsterdam', + 'NL', + 'nl_NL' + ), + new DataCollection($invoiceLines) + ); + $request = new CreateSalesInvoiceRequest($payload); + + /** @var Response */ + $response = $client->send($request); + + $this->assertTrue($response->successful()); + $this->assertInstanceOf(SalesInvoice::class, $response->toResource()); + } + + /** @test */ + public function it_resolves_correct_resource_path() + { + $request = new CreateSalesInvoiceRequest(new CreateSalesInvoicePayload( + 'EUR', + SalesInvoiceStatus::DRAFT, + VatScheme::STANDARD, + VatMode::INCLUSIVE, + PaymentTerm::DAYS_30, + 'XXXXX', + new Recipient( + RecipientType::CONSUMER, + 'darth@vader.deathstar', + 'Sample Street 12b', + '2000 AA', + 'Amsterdam', + 'NL', + 'nl_NL' + ), + new DataCollection([ + new InvoiceLine( + 'Monthly subscription fee', + 1, + '21', + new Money('EUR', '10,00'), + ) + ]) + )); + + $this->assertEquals('sales-invoices', $request->resolveResourcePath()); + } +} diff --git a/tests/Http/Requests/DeleteSalesInvoiceRequestTest.php b/tests/Http/Requests/DeleteSalesInvoiceRequestTest.php new file mode 100644 index 00000000..dfa4397b --- /dev/null +++ b/tests/Http/Requests/DeleteSalesInvoiceRequestTest.php @@ -0,0 +1,34 @@ + new MockResponse(204), + ]); + + $request = new DeleteSalesInvoiceRequest('invoice_123'); + + /** @var Response */ + $response = $client->send($request); + + $this->assertTrue($response->successful()); + $this->assertEquals(204, $response->status()); + } + + /** @test */ + public function it_resolves_correct_resource_path() + { + $request = new DeleteSalesInvoiceRequest('invoice_123'); + $this->assertEquals('sales-invoices/invoice_123', $request->resolveResourcePath()); + } +} diff --git a/tests/Http/Requests/GetPaginatedSalesInvoicesRequestTest.php b/tests/Http/Requests/GetPaginatedSalesInvoicesRequestTest.php new file mode 100644 index 00000000..399741e9 --- /dev/null +++ b/tests/Http/Requests/GetPaginatedSalesInvoicesRequestTest.php @@ -0,0 +1,69 @@ + new MockResponse(200, 'sales-invoice-list'), + ]); + + $request = new GetPaginatedSalesInvoicesRequest; + + /** @var Response */ + $response = $client->send($request); + + $this->assertTrue($response->successful()); + + /** @var SalesInvoiceCollection */ + $salesInvoices = $response->toResource(); + $this->assertInstanceOf(SalesInvoiceCollection::class, $salesInvoices); + $this->assertGreaterThan(0, $salesInvoices->count()); + } + + /** @test */ + public function it_can_iterate_over_sales_invoices() + { + $client = new MockClient([ + GetPaginatedSalesInvoicesRequest::class => new MockResponse(200, 'sales-invoice-list'), + DynamicGetRequest::class => new SequenceMockResponse( + new MockResponse(200, 'sales-invoice-list'), + new MockResponse(200, 'empty-list', 'sales_invoices'), + ), + ]); + + $request = (new GetPaginatedSalesInvoicesRequest)->useIterator(); + + /** @var Response */ + $response = $client->send($request); + $this->assertTrue($response->successful()); + + /** @var LazyCollection */ + $salesInvoices = $response->toResource(); + + foreach ($salesInvoices as $salesInvoice) { + $this->assertInstanceOf(SalesInvoice::class, $salesInvoice); + } + + $client->assertSentCount(3); + } + + /** @test */ + public function it_resolves_correct_resource_path() + { + $request = new GetPaginatedSalesInvoicesRequest; + $this->assertEquals('sales-invoices', $request->resolveResourcePath()); + } +} diff --git a/tests/Http/Requests/GetSalesInvoiceRequestTest.php b/tests/Http/Requests/GetSalesInvoiceRequestTest.php new file mode 100644 index 00000000..b38fc655 --- /dev/null +++ b/tests/Http/Requests/GetSalesInvoiceRequestTest.php @@ -0,0 +1,26 @@ + new MockResponse(200, 'sales-invoice'), + ]); + + $request = new GetSalesInvoiceRequest('invoice_123'); + $response = $client->send($request); + + $this->assertTrue($response->successful()); + $this->assertInstanceOf(SalesInvoice::class, $response->toResource()); + } +} diff --git a/tests/Http/Requests/UpdateSalesInvoiceRequestTest.php b/tests/Http/Requests/UpdateSalesInvoiceRequestTest.php new file mode 100644 index 00000000..a58b65ae --- /dev/null +++ b/tests/Http/Requests/UpdateSalesInvoiceRequestTest.php @@ -0,0 +1,44 @@ + new MockResponse(200, 'sales-invoice'), + ]); + + $payload = new UpdateSalesInvoicePayload( + SalesInvoiceStatus::PAID, + 'XXXXX', + ); + $request = new UpdateSalesInvoiceRequest('invoice_123', $payload); + + /** @var Response */ + $response = $client->send($request); + + $this->assertTrue($response->successful()); + $this->assertInstanceOf(SalesInvoice::class, $response->toResource()); + } + + /** @test */ + public function it_resolves_correct_resource_path() + { + $request = new UpdateSalesInvoiceRequest('invoice_123', new UpdateSalesInvoicePayload( + SalesInvoiceStatus::PAID, + 'XXXXX', + )); + $this->assertEquals('sales-invoices/invoice_123', $request->resolveResourcePath()); + } +}