├── tests ├── .gitkeep ├── Authorization │ ├── .incomplete-mock.env │ ├── .mock.env │ ├── .another-mock.env │ ├── SimpleProviderTest.php │ ├── Header │ │ └── BuilderTest.php │ └── EnvFileProviderTest.php ├── UseCase │ ├── Tag │ │ └── fixtures │ │ │ ├── all.json │ │ │ └── save-success.json │ ├── Client │ │ ├── fixtures │ │ │ ├── create.json │ │ │ ├── not-found.json │ │ │ ├── get-by-id-invalid-id.json │ │ │ ├── insufficient-permissions.json │ │ │ ├── edit-insufficient-permissions.json │ │ │ ├── delete-client-with-contacts.json │ │ │ ├── delete-insufficient-permissions.json │ │ │ ├── client.json │ │ │ └── clients.json │ │ ├── Contact │ │ │ └── fixtures │ │ │ │ ├── not-found.json │ │ │ │ ├── create-contact-missing-data.json │ │ │ │ ├── delete-contact-not-found.json │ │ │ │ ├── create-contact-client-not-found.json │ │ │ │ ├── delete-insufficient-permissions.json │ │ │ │ ├── create-contact-insufficient-permissions.json │ │ │ │ ├── client_without_contacts.json │ │ │ │ └── client_with_contacts.json │ │ └── ClientsQueryTest.php │ ├── Invoice │ │ ├── fixtures │ │ │ ├── detail-single.json │ │ │ ├── invalid-currency.json │ │ │ ├── export.xlsx │ │ │ ├── export.zip │ │ │ ├── change-language-insufficient-permissions.json │ │ │ ├── missing-client-data.json │ │ │ ├── insufficient-permissions.json │ │ │ ├── detail-multiple.json │ │ │ ├── zero-invoice-items.json │ │ │ └── list.json │ │ ├── Item │ │ │ ├── fixtures │ │ │ │ └── delete-item-error-response.json │ │ │ └── ItemsTest.php │ │ ├── Payment │ │ │ └── fixtures │ │ │ │ └── pay-success.json │ │ ├── InvoicesTestCase.php │ │ ├── InvoicesQueryTest.php │ │ └── InvoicesDownloadTest.php │ ├── Expense │ │ ├── fixtures │ │ │ ├── not-found.json │ │ │ ├── generic-error.json │ │ │ ├── create-error.json │ │ │ ├── expense.json │ │ │ ├── create-update-success.json │ │ │ ├── create-validation-error.json │ │ │ └── list-categories.json │ │ ├── Payment │ │ │ └── fixtures │ │ │ │ └── pay-success.json │ │ ├── ExpensesQueryTest.php │ │ └── ExpensesTestCase.php │ ├── fixtures │ │ └── unexpected-error.json │ ├── BankAccount │ │ └── fixtures │ │ │ ├── not-found.json │ │ │ ├── insufficient-permissions.json │ │ │ └── multiple-bank-accounts.json │ ├── Stock │ │ ├── fixtures │ │ │ ├── get-by-id-not-found.json │ │ │ ├── insufficient-permissions.json │ │ │ ├── create-insufficient-permissions.json │ │ │ ├── create-movement.json │ │ │ ├── get-by-id.json │ │ │ ├── create.json │ │ │ ├── update.json │ │ │ ├── stock-items.json │ │ │ └── stock-movements.json │ │ ├── ItemsQueryTest.php │ │ └── MovementsQueryTest.php │ ├── CashRegister │ │ ├── fixtures │ │ │ ├── create-item-with-non-existent-cash-register.json │ │ │ ├── not-found.json │ │ │ ├── create-item-insufficient-permissions.json │ │ │ ├── cash-register.json │ │ │ └── getAll-multiple-items.json │ │ ├── ItemsTest.php │ │ └── CashRegistersTest.php │ ├── Country │ │ ├── fixtures │ │ │ └── list.json │ │ └── CountriesTest.php │ ├── RelatedDocument │ │ └── fixtures │ │ │ └── link-success.json │ └── Export │ │ ├── fixtures │ │ └── get-status-success.json │ │ ├── ExportTestCase.php │ │ └── ExportTest.php ├── Response │ ├── fixtures │ │ ├── bar.pdf │ │ ├── foo.pdf │ │ └── export.zip │ └── ResponseTest.php ├── Utils │ ├── AssertRequest.php │ └── AssertRequestBuilder.php └── Filter │ └── NamedParamsConvertorTest.php ├── .gitignore ├── src ├── Version │ ├── Provider.php │ └── ComposerProvider.php ├── Authorization │ ├── CannotLoadFileException.php │ ├── Provider.php │ ├── InvalidDotEnvConfigException.php │ ├── DotEnvConfigKey.php │ ├── Authorization.php │ ├── SimpleProvider.php │ ├── Header │ │ └── Builder.php │ └── EnvFileProvider.php ├── Filter │ ├── SortDirection.php │ ├── Sort.php │ ├── QueryParamsConvertor.php │ ├── TimePeriod.php │ ├── TimePeriodEnum.php │ └── NamedParamsConvertor.php ├── Request │ ├── CannotCreateRequestException.php │ └── RequestException.php ├── Response │ ├── CannotCreateResponseException.php │ ├── RateLimit.php │ ├── Response.php │ ├── BinaryResponse.php │ ├── ResponseFactoryInterface.php │ └── ResponseFactory.php ├── Contract │ ├── Export │ │ ├── DocumentSort.php │ │ ├── Status.php │ │ ├── ExportNotFoundException.php │ │ ├── CannotDownloadExportException.php │ │ ├── CannotGetExportStatusException.php │ │ ├── Format.php │ │ └── Exports.php │ ├── RelatedDocument │ │ ├── DocumentType.php │ │ ├── CannotLinkDocumentsException.php │ │ ├── CannotUnlinkDocumentsException.php │ │ └── RelatedDocuments.php │ ├── Tag │ │ ├── TagNotFoundException.php │ │ ├── CannotCreateTagException.php │ │ ├── CannotDeleteTagException.php │ │ ├── CannotGetAllTagsException.php │ │ ├── CannotUpdateTagException.php │ │ ├── TagAlreadyExistsException.php │ │ └── Tags.php │ ├── Stock │ │ ├── ItemNotFoundException.php │ │ ├── CannotCreateItemException.php │ │ ├── CannotDeleteItemException.php │ │ ├── CannotUpdateItemException.php │ │ ├── CannotGetAllItemsException.php │ │ ├── CannotGetItemByIdException.php │ │ ├── CannotCreateMovementException.php │ │ ├── CannotGetAllMovementsException.php │ │ ├── Movements.php │ │ ├── MovementsQuery.php │ │ ├── Items.php │ │ └── ItemsQuery.php │ ├── Client │ │ ├── CannotGetClientException.php │ │ ├── ClientNotFoundException.php │ │ ├── CannotCreateClientException.php │ │ ├── CannotDeleteClientException.php │ │ ├── CannotUpdateClientException.php │ │ ├── CannotGetAllClientsException.php │ │ ├── Contact │ │ │ ├── ContactNotFoundException.php │ │ │ ├── CannotCreateContactException.php │ │ │ ├── CannotDeleteContactException.php │ │ │ ├── CannotGetAllContactsException.php │ │ │ └── Contacts.php │ │ └── Clients.php │ ├── Expense │ │ ├── ExpenseStatus.php │ │ ├── CannotGetExpenseException.php │ │ ├── ExpenseNotFoundException.php │ │ ├── CannotDeleteExpenseException.php │ │ ├── CannotGetAllCategoriesException.php │ │ ├── CannotGetAllExpensesException.php │ │ ├── Payment │ │ │ ├── CannotPayExpenseException.php │ │ │ ├── CannotDeleteExpensePaymentException.php │ │ │ └── Payments.php │ │ ├── ExpenseType.php │ │ ├── CannotUpdateExpenseException.php │ │ ├── CannotCreateExpenseException.php │ │ └── Expenses.php │ ├── Invoice │ │ ├── InvoiceStatus.php │ │ ├── CannotGetInvoiceException.php │ │ ├── CannotSendInvoiceException.php │ │ ├── InvoiceNotFoundException.php │ │ ├── CannotDeleteInvoiceException.php │ │ ├── CannotDownloadInvoiceException.php │ │ ├── CannotExportInvoicesException.php │ │ ├── CannotGetAllInvoicesException.php │ │ ├── CannotMarkInvoiceAsSentException.php │ │ ├── Payment │ │ │ ├── CannotPayInvoiceException.php │ │ │ ├── CannotMarkAsUnpayableException.php │ │ │ ├── CannotDeleteInvoicePaymentException.php │ │ │ └── Payments.php │ │ ├── CannotChangeInvoiceLanguageException.php │ │ ├── Item │ │ │ ├── CannotDeleteInvoiceItemException.php │ │ │ └── Items.php │ │ ├── DeliveryType.php │ │ ├── InvoiceType.php │ │ ├── CannotUpdateInvoiceException.php │ │ ├── CannotCreateInvoiceException.php │ │ └── Invoices.php │ ├── Country │ │ ├── CannotGetAllCountriesException.php │ │ └── Countries.php │ ├── BankAccount │ │ ├── BankAccountNotFoundException.php │ │ ├── CannotCreateBankAccountException.php │ │ ├── CannotDeleteBankAccountException.php │ │ ├── CannotUpdateBankAccountException.php │ │ ├── CannotGetAllBankAccountsException.php │ │ └── BankAccounts.php │ ├── CashRegister │ │ ├── CannotGetCashRegisterException.php │ │ ├── CashRegisterNotFoundException.php │ │ ├── CannotGetAllCashRegistersException.php │ │ ├── CannotCreateCashRegisterItemException.php │ │ ├── CashRegisters.php │ │ └── Items.php │ ├── Language.php │ └── PaymentType.php ├── MarketUri.php └── UseCase │ ├── RelatedDocument │ ├── Relation.php │ └── RelatedDocuments.php │ ├── Export │ ├── PdfExportOptions.php │ └── InvoiceExportRequestFactory.php │ ├── Invoice │ ├── Email.php │ ├── Address.php │ ├── Payment │ │ ├── Payment.php │ │ └── Payments.php │ ├── Items.php │ └── InvoicesQuery.php │ ├── Expense │ ├── Payment │ │ ├── Payment.php │ │ └── Payments.php │ └── ExpensesQuery.php │ ├── Client │ ├── ClientsQuery.php │ └── Contact │ │ └── Contacts.php │ ├── Country │ └── Countries.php │ ├── CashRegister │ ├── Items.php │ └── CashRegisters.php │ └── Stock │ └── Movements.php ├── phpstan.neon ├── docker-compose.yml ├── phpstan-baseline.neon ├── phpunit.xml ├── docker └── php │ └── conf.d │ └── app.dev.ini ├── Dockerfile ├── LICENSE ├── .editorconfig ├── .woodpecker └── php.yml └── composer.json /tests/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/Authorization/.incomplete-mock.env: -------------------------------------------------------------------------------- 1 | SF_APICLIENT_EMAIL=invalid_test@example.com 2 | -------------------------------------------------------------------------------- /tests/UseCase/Tag/fixtures/all.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": "fizz", 3 | "2": "buzz" 4 | } 5 | -------------------------------------------------------------------------------- /tests/UseCase/Client/fixtures/create.json: -------------------------------------------------------------------------------- 1 | {"Client": {"id": 123, "name": "Jozef Mrkvicka"}} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /coverage/ 3 | *.cache 4 | /.idea/ 5 | docker-compose.override.yml 6 | .env 7 | -------------------------------------------------------------------------------- /tests/UseCase/Invoice/fixtures/detail-single.json: -------------------------------------------------------------------------------- 1 | { 2 | "Invoice": { 3 | "id": "1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Expense/fixtures/not-found.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "error_message": "Expense not found" 4 | } 5 | -------------------------------------------------------------------------------- /tests/Response/fixtures/bar.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaktura/apiclient/HEAD/tests/Response/fixtures/bar.pdf -------------------------------------------------------------------------------- /tests/Response/fixtures/foo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaktura/apiclient/HEAD/tests/Response/fixtures/foo.pdf -------------------------------------------------------------------------------- /tests/Response/fixtures/export.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaktura/apiclient/HEAD/tests/Response/fixtures/export.zip -------------------------------------------------------------------------------- /tests/UseCase/Invoice/fixtures/invalid-currency.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "error_message": "Incorrect currency" 4 | } 5 | -------------------------------------------------------------------------------- /tests/UseCase/Invoice/Item/fixtures/delete-item-error-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "Chyba pri mazaní položky" 4 | } 5 | -------------------------------------------------------------------------------- /tests/UseCase/Invoice/fixtures/export.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaktura/apiclient/HEAD/tests/UseCase/Invoice/fixtures/export.xlsx -------------------------------------------------------------------------------- /tests/UseCase/Invoice/fixtures/export.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/superfaktura/apiclient/HEAD/tests/UseCase/Invoice/fixtures/export.zip -------------------------------------------------------------------------------- /tests/UseCase/Invoice/fixtures/change-language-insufficient-permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "message": "Permission denied.", 3 | "status": 0 4 | } 5 | -------------------------------------------------------------------------------- /tests/UseCase/Client/fixtures/not-found.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "Client not found", 4 | "error_message": "Client not found" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Expense/fixtures/generic-error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "error_message": "Expense error", 4 | "message": "Expense error" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/fixtures/unexpected-error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "Unexpected error", 4 | "error_message": "Unexpected error" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/BankAccount/fixtures/not-found.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "Účet neexistuje", 4 | "error_message": "Účet neexistuje" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Expense/fixtures/create-error.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "error_message": "Chýbajúce údaje", 4 | "message": "Chýbajúce údaje" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Tag/fixtures/save-success.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 0, 3 | "message": "Tag bol uložený", 4 | "tag_id": "1", 5 | "tag_name": "fizz" 6 | } 7 | -------------------------------------------------------------------------------- /tests/UseCase/Client/Contact/fixtures/not-found.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "Client not found", 4 | "error_message": "Client not found" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Expense/fixtures/expense.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "Expense": { 4 | "id": "1", 5 | "name": "Expense" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/UseCase/Client/fixtures/get-by-id-invalid-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "Invalid client id", 4 | "error_message": "Invalid client id" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Stock/fixtures/get-by-id-not-found.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "StockItem not found", 4 | "error_message": "StockItem not found" 5 | } 6 | -------------------------------------------------------------------------------- /tests/Authorization/.mock.env: -------------------------------------------------------------------------------- 1 | SF_APICLIENT_EMAIL=test@example.com 2 | SF_APICLIENT_KEY=test 3 | SF_APICLIENT_APP_TITLE='Example s.r.o.' 4 | SF_APICLIENT_COMPANY_ID=1 5 | -------------------------------------------------------------------------------- /tests/UseCase/Expense/fixtures/create-update-success.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "Expense": { 4 | "id": "1", 5 | "name": "Expense" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/UseCase/CashRegister/fixtures/create-item-with-non-existent-cash-register.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "status": "0", 4 | "message": "Cash register not found" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Client/Contact/fixtures/create-contact-missing-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "state": "ERROR", 3 | "error": 1, 4 | "message": "Prosím vyplňte povinné položky" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Client/Contact/fixtures/delete-contact-not-found.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "Contact not found", 4 | "error_message": "Contact not found" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Invoice/fixtures/missing-client-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 4, 3 | "error_message": { 4 | "data_bad_format": "Missing required client data." 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tests/Authorization/.another-mock.env: -------------------------------------------------------------------------------- 1 | SF_APICLIENT_EMAIL=test2@example.com 2 | SF_APICLIENT_KEY=test2 3 | SF_APICLIENT_APP_TITLE='Example2 s.r.o.' 4 | SF_APICLIENT_COMPANY_ID=2 5 | -------------------------------------------------------------------------------- /tests/UseCase/Client/Contact/fixtures/create-contact-client-not-found.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "Client not found", 4 | "error_message": "Client not found" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Client/fixtures/insufficient-permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "You can't create new items", 4 | "error_message": "You can't create new items" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Invoice/fixtures/insufficient-permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "error_message": "You can't create invoice", 4 | "message": "You can't create invoice" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/BankAccount/fixtures/insufficient-permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "error_message": "Nemáte právo zmazať účet", 4 | "message": "Nemáte právo zmazať účet" 5 | } 6 | -------------------------------------------------------------------------------- /tests/UseCase/Client/fixtures/edit-insufficient-permissions.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 1, 3 | "message": "You can't edit this item", 4 | "error_message": "You can't edit this item" 5 | } 6 | -------------------------------------------------------------------------------- /src/Version/Provider.php: -------------------------------------------------------------------------------- 1 | $params 11 | */ 12 | public function convert(array $params): string; 13 | } 14 | -------------------------------------------------------------------------------- /src/Contract/Country/Countries.php: -------------------------------------------------------------------------------- 1 | data['error'] ?? 0) > 0; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Contract/CashRegister/CashRegisters.php: -------------------------------------------------------------------------------- 1 | $data 12 | * 13 | * @throws CannotCreateCashRegisterItemException 14 | * @throws CannotCreateRequestException 15 | */ 16 | public function create(int $cash_register_id, array $data): Response; 17 | } 18 | -------------------------------------------------------------------------------- /src/UseCase/Export/PdfExportOptions.php: -------------------------------------------------------------------------------- 1 | email, 19 | $this->key, 20 | 'API', 21 | $this->app_title, 22 | $this->company_id, 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/UseCase/Stock/fixtures/get-by-id.json: -------------------------------------------------------------------------------- 1 | { 2 | "StockItem": { 3 | "id": "1", 4 | "user_id": "1", 5 | "user_profile_id": "1", 6 | "name": "Rozok grahamovy", 7 | "description": "Test description", 8 | "sku": "RZK-GRHMV", 9 | "unit": "ks", 10 | "unit_price": 0.1, 11 | "purchase_unit_price": "0.1200", 12 | "purchase_currency": null, 13 | "vat": 20, 14 | "purchase_vat": null, 15 | "watch_stock": true, 16 | "stock": 0, 17 | "import_type": null, 18 | "import_id": null, 19 | "internal_comment": null, 20 | "hide_in_autocomplete": null, 21 | "created": "2023-10-10 07:59:57", 22 | "modified": "2023-10-10 07:59:57" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Request/RequestException.php: -------------------------------------------------------------------------------- 1 | request; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/UseCase/Stock/fixtures/create.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 0, 3 | "error_message": "", 4 | "data": { 5 | "StockItem": { 6 | "description": "Basic stock item", 7 | "name": "Rozok grahamovy", 8 | "sku": "RZK-GRHMV", 9 | "unit": "ks", 10 | "unit_price": 0.1, 11 | "purchase_unit_price": 0.12, 12 | "vat": 20, 13 | "watch_stock": 1, 14 | "stock": 0, 15 | "id": "1" 16 | }, 17 | "StockLog": [ 18 | { 19 | "quantity": 0, 20 | "note": "Počiatočný stav skladu", 21 | "log_data": "{\"purchase_unit_price\":0.12,\"purchase_tax\":null,\"purchase_currency\":null,\"unit_price\":0.1,\"tax\":20,\"currency\":null}" 22 | } 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/UseCase/Invoice/Email.php: -------------------------------------------------------------------------------- 1 | errors; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Contract/Invoice/CannotUpdateInvoiceException.php: -------------------------------------------------------------------------------- 1 | errors; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Contract/Stock/Movements.php: -------------------------------------------------------------------------------- 1 | [] $data 11 | * 12 | * @throws CannotCreateMovementException 13 | * @throws ItemNotFoundException 14 | */ 15 | public function create(int $item_id, array $data): Response; 16 | 17 | /** 18 | * @param array[] $data 19 | * 20 | * @throws CannotCreateMovementException 21 | * @throws ItemNotFoundException 22 | */ 23 | public function createWithSku(string $sku, array $data): Response; 24 | 25 | /** 26 | * @throws CannotGetAllMovementsException 27 | */ 28 | public function getAll(int $id, MovementsQuery $query = new MovementsQuery()): Response; 29 | } 30 | -------------------------------------------------------------------------------- /src/UseCase/Invoice/Address.php: -------------------------------------------------------------------------------- 1 | page !== null && $this->page < 1) { 17 | throw new \InvalidArgumentException('Page argument must be greater than or equal to 1'); 18 | } 19 | 20 | if ($this->per_page !== null && ($this->per_page < 1 || $this->per_page > self::PER_PAGE_MAX)) { 21 | throw new \InvalidArgumentException(sprintf( 22 | 'Items per page argument must be greater than or equal to 1 and less than %d', 23 | self::PER_PAGE_MAX, 24 | )); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Contract/Expense/CannotCreateExpenseException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 26 | } 27 | 28 | /** 29 | * @return string[] 30 | */ 31 | public function getErrors(): array 32 | { 33 | return $this->errors; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Contract/Invoice/CannotCreateInvoiceException.php: -------------------------------------------------------------------------------- 1 | errors = $errors; 26 | } 27 | 28 | /** 29 | * @return string[] 30 | */ 31 | public function getErrors(): array 32 | { 33 | return $this->errors; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Contract/Tag/Tags.php: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests 16 | 17 | 18 | 19 | 20 | 21 | src 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docker/php/conf.d/app.dev.ini: -------------------------------------------------------------------------------- 1 | expose_php = 0 2 | date.timezone = UTC 3 | apc.enable_cli = 1 4 | session.use_strict_mode = 1 5 | zend.detect_unicode = 0 6 | 7 | ; https://symfony.com/doc/current/performance.html 8 | realpath_cache_size = 4096K 9 | realpath_cache_ttl = 600 10 | opcache.interned_strings_buffer = 16 11 | opcache.max_accelerated_files = 20000 12 | opcache.memory_consumption = 256 13 | opcache.enable_file_override = 1 14 | 15 | ; See https://docs.docker.com/desktop/networking/#i-want-to-connect-from-a-container-to-a-service-on-the-host 16 | ; See https://github.com/docker/for-linux/issues/264 17 | ; The `client_host` below may optionally be replaced with `discover_client_host=yes` 18 | ; Add `start_with_request=yes` to start debug session on each request 19 | xdebug.client_host = 'host.docker.internal' 20 | xdebug.client_port = 9001 21 | xdebug.start_with_request = yes 22 | xdebug.var_display_max_depth = 15 23 | xdebug.var_display_max_children = 256 24 | xdebug.var_display_max_data = 1024 25 | xdebug.log_level = 0 26 | -------------------------------------------------------------------------------- /tests/UseCase/Expense/ExpensesQueryTest.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public static function invalidPageArgumentProvider(): \Generator 21 | { 22 | yield 'negative' => [-1]; 23 | yield 'zero' => [0]; 24 | } 25 | 26 | #[DataProvider('invalidPageArgumentProvider')] 27 | public function testInvalidPageArgument(int $page): void 28 | { 29 | $this->expectException(\InvalidArgumentException::class); 30 | 31 | new ExpensesQuery(page: $page); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Contract/Client/Contact/Contacts.php: -------------------------------------------------------------------------------- 1 | $contact 21 | * 22 | * @throws CannotCreateContactException 23 | * @throws CannotCreateRequestException 24 | * @throws ClientNotFoundException 25 | */ 26 | public function create(int $client_id, array $contact): Response; 27 | 28 | /** 29 | * @throws CannotDeleteContactException 30 | * @throws ContactNotFoundException 31 | */ 32 | public function delete(int $contact_id): void; 33 | } 34 | -------------------------------------------------------------------------------- /tests/UseCase/Export/ExportTestCase.php: -------------------------------------------------------------------------------- 1 | $data 11 | * 12 | * @throws CannotCreateItemException 13 | */ 14 | public function create(array $data): Response; 15 | 16 | /** 17 | * @throws ItemNotFoundException 18 | * @throws CannotGetItemByIdException 19 | */ 20 | public function getById(int $id): Response; 21 | 22 | /** 23 | * @throws CannotGetAllItemsException 24 | */ 25 | public function getAll(ItemsQuery $query = new ItemsQuery()): Response; 26 | 27 | /** 28 | * @param array $data 29 | * 30 | * @throws ItemNotFoundException 31 | * @throws CannotUpdateItemException 32 | */ 33 | public function update(int $id, array $data): Response; 34 | 35 | /** 36 | * @throws ItemNotFoundException 37 | * @throws CannotDeleteItemException 38 | */ 39 | public function delete(int $id): void; 40 | } 41 | -------------------------------------------------------------------------------- /src/Contract/BankAccount/BankAccounts.php: -------------------------------------------------------------------------------- 1 | $bank_account 17 | * 18 | * @throws CannotCreateBankAccountException 19 | * @throws CannotCreateRequestException 20 | */ 21 | public function create(array $bank_account): Response; 22 | 23 | /** 24 | * @param array $bank_account 25 | * 26 | * @throws CannotUpdateBankAccountException 27 | * @throws CannotCreateRequestException 28 | */ 29 | public function update(int $id, array $bank_account): Response; 30 | 31 | /** 32 | * @throws CannotDeleteBankAccountException 33 | * @throws BankAccountNotFoundException 34 | */ 35 | public function delete(int $bank_account_id): void; 36 | } 37 | -------------------------------------------------------------------------------- /src/Authorization/Header/Builder.php: -------------------------------------------------------------------------------- 1 | $authorization->email, 19 | 'apikey' => $authorization->key, 20 | 'company_id' => $authorization->company_id, 21 | 'module' => $this->buildModule($authorization), 22 | ]); 23 | } 24 | 25 | private function buildModule(Authorization $authorization): string 26 | { 27 | return sprintf( 28 | '%s [%s] (w/ SFAPI %s) [%s]', 29 | $authorization->module, 30 | $authorization->app_title, 31 | $this->versionProvider->getVersion(), 32 | PHP_VERSION_ID, 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 SuperFaktura, s.r.o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/Contract/Client/Clients.php: -------------------------------------------------------------------------------- 1 | $data 25 | * 26 | * @throws ClientNotFoundException 27 | * @throws CannotUpdateClientException 28 | */ 29 | public function update(int $id, array $data): Response; 30 | 31 | /** 32 | * @throws ClientNotFoundException 33 | * @throws CannotDeleteClientException 34 | */ 35 | public function delete(int $id): Response; 36 | 37 | /** 38 | * @param array $data 39 | * 40 | * @throws CannotCreateClientException 41 | */ 42 | public function create(array $data): Response; 43 | } 44 | -------------------------------------------------------------------------------- /src/Contract/Stock/ItemsQuery.php: -------------------------------------------------------------------------------- 1 | page !== null && $this->page < 1) { 23 | throw new \InvalidArgumentException('Page argument must be greater than or equal to 1'); 24 | } 25 | 26 | if ($this->per_page !== null && ($this->per_page < 1 || $this->per_page > self::PER_PAGE_MAX)) { 27 | throw new \InvalidArgumentException(sprintf( 28 | 'Items per page argument must be greater than or equal to 1 and less than %d', 29 | self::PER_PAGE_MAX, 30 | )); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/UseCase/BankAccount/fixtures/multiple-bank-accounts.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 0, 3 | "BankAccounts": [ 4 | { 5 | "BankAccount": { 6 | "id": "10", 7 | "user_id": "1", 8 | "user_profile_id": "2", 9 | "currency": "EUR", 10 | "default": true, 11 | "show": true, 12 | "country_id": "191", 13 | "bank_name": "Tatra banka, a.s.", 14 | "bank_code": "1100", 15 | "account": "2926858237", 16 | "iban": "SK3211000000002926858237", 17 | "swift": "TATRSKBX", 18 | "created": "2023-09-06 07:02:29", 19 | "modified": "2023-09-06 07:02:29" 20 | } 21 | }, 22 | { 23 | "BankAccount": { 24 | "id": "11", 25 | "user_id": "1", 26 | "user_profile_id": "2", 27 | "currency": "CZK", 28 | "default": null, 29 | "show": true, 30 | "country_id": "191", 31 | "bank_name": "Fio banka, a.s.", 32 | "bank_code": "2010", 33 | "account": "2100961334", 34 | "iban": "CZ4720100000002100961334", 35 | "swift": "FIOBCZPP", 36 | "created": "2023-09-06 07:02:29", 37 | "modified": "2023-09-06 07:02:29" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /tests/UseCase/Expense/ExpensesTestCase.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static function expenseIdProvider(): \Generator 20 | { 21 | yield 'expense' => [1]; 22 | yield 'another expense' => [2]; 23 | } 24 | 25 | protected function getExpenses(ClientInterface $client): Expenses 26 | { 27 | return new Expenses( 28 | http_client: $client, 29 | request_factory: new HttpFactory(), 30 | response_factory: new ResponseFactory(), 31 | query_params_convertor: new NamedParamsConvertor(), 32 | base_uri: '', 33 | authorization_header_value: self::AUTHORIZATION_HEADER_VALUE, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/UseCase/Invoice/InvoicesTestCase.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static function invoiceIdProvider(): \Generator 20 | { 21 | yield 'invoice' => [1]; 22 | yield 'another invoice' => [2]; 23 | } 24 | 25 | protected function getInvoices(ClientInterface $client): Invoices 26 | { 27 | return new Invoices( 28 | http_client: $client, 29 | request_factory: new HttpFactory(), 30 | response_factory: new ResponseFactory(), 31 | query_params_convertor: new NamedParamsConvertor(), 32 | base_uri: '', 33 | authorization_header_value: self::AUTHORIZATION_HEADER_VALUE, 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/UseCase/Stock/fixtures/update.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": 0, 3 | "error_message": "", 4 | "data": { 5 | "StockItem": { 6 | "id": 1, 7 | "user_id": "22", 8 | "user_profile_id": "12", 9 | "name": "Rozok grahamovy", 10 | "description": "new description", 11 | "sku": "RZK-GRHMV", 12 | "unit": "ks", 13 | "unit_price": 0.1, 14 | "purchase_unit_price": "0.1200", 15 | "purchase_currency": null, 16 | "vat": 20, 17 | "purchase_vat": null, 18 | "watch_stock": true, 19 | "stock": 0, 20 | "import_type": null, 21 | "import_id": null, 22 | "internal_comment": null, 23 | "hide_in_autocomplete": null, 24 | "created": "2023-10-10 07:59:57", 25 | "modified": "2023-10-10 07:59:57", 26 | "stock_previous": 0 27 | }, 28 | "StockLog": [ 29 | { 30 | "quantity": 0, 31 | "note": "Ručná úprava stavu na sklade", 32 | "log_data": "{\"purchase_unit_price\":\"0.1200\",\"purchase_tax\":null,\"purchase_exchange_rate\":1,\"purchase_country_exchange_rate\":1,\"purchase_currency\":null,\"unit_price\":\"0.1\",\"tax\":20,\"exchange_rate\":1,\"country_exchange_rate\":1,\"currency\":\"EUR\",\"home_currency\":\"EUR\"}" 33 | } 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | # Change these settings to your own preference 9 | indent_style = space 10 | indent_size = 4 11 | 12 | # We recommend you to keep these unchanged 13 | end_of_line = lf 14 | charset = utf-8 15 | trim_trailing_whitespace = true 16 | insert_final_newline = true 17 | 18 | [*.json] 19 | indent_size = 2 20 | 21 | [*.md] 22 | trim_trailing_whitespace = false 23 | 24 | [*.php] 25 | indent_style = space 26 | indent_size = 4 27 | 28 | [*.sh] 29 | indent_style = tab 30 | indent_size = 4 31 | 32 | [*.xml{,.dist}] 33 | indent_style = space 34 | indent_size = 4 35 | 36 | [*.{yaml,yml}] 37 | indent_style = space 38 | indent_size = 2 39 | trim_trailing_whitespace = false 40 | 41 | [.github/workflows/*.yml] 42 | indent_style = space 43 | indent_size = 2 44 | 45 | [.gitmodules] 46 | indent_style = tab 47 | indent_size = 4 48 | 49 | [.php_cs{,.dist}] 50 | indent_style = space 51 | indent_size = 4 52 | 53 | [.travis.yml] 54 | indent_style = space 55 | indent_size = 2 56 | 57 | [composer.json] 58 | indent_style = space 59 | indent_size = 4 60 | 61 | [Dockerfile] 62 | indent_style = space 63 | indent_size = 4 64 | -------------------------------------------------------------------------------- /src/UseCase/Client/ClientsQuery.php: -------------------------------------------------------------------------------- 1 | page < 1) { 27 | throw new \InvalidArgumentException('Page argument must be greater than or equal to 1'); 28 | } 29 | 30 | if ($this->items_per_page < 1 || $this->items_per_page > self::ITEMS_PER_PAGE_MAX) { 31 | throw new \InvalidArgumentException(sprintf( 32 | 'Items per page argument must be greater than or equal to 1 and less than %d', 33 | self::ITEMS_PER_PAGE_MAX, 34 | )); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/UseCase/Client/fixtures/client.json: -------------------------------------------------------------------------------- 1 | { 2 | "Client": { 3 | "id": "3", 4 | "user_id": "1", 5 | "user_profile_id": "2", 6 | "uuid": "", 7 | "country_id": "191", 8 | "name": "stefan", 9 | "ico": "12345678", 10 | "dic": "", 11 | "ic_dph": "", 12 | "iban": "", 13 | "swift": "", 14 | "bank_account_prefix": "", 15 | "bank_account": "", 16 | "bank_code": "", 17 | "account": "", 18 | "email": "", 19 | "address": "", 20 | "city": "", 21 | "zip": "", 22 | "state": "", 23 | "country": "Slovensko", 24 | "delivery_name": "", 25 | "delivery_address": "", 26 | "delivery_city": "", 27 | "delivery_zip": "", 28 | "delivery_state": "", 29 | "delivery_country": "", 30 | "delivery_country_id": "", 31 | "phone": "", 32 | "delivery_phone": "", 33 | "fax": "", 34 | "due_date": "", 35 | "default_variable": "", 36 | "discount": "", 37 | "currency": "", 38 | "bank_account_id": "", 39 | "comment": "", 40 | "tags": "", 41 | "distance": "", 42 | "dont_travel": "", 43 | "created": "2023-08-02 12:51:29", 44 | "modified": "2023-08-02 12:51:29", 45 | "notices": true 46 | }, 47 | "Country": { 48 | "id": "191", 49 | "name": "Slovensko", 50 | "iso": "sk", 51 | "eu": true, 52 | "order": "1" 53 | }, 54 | "ContactPerson": [], 55 | "Tag": [] 56 | } 57 | -------------------------------------------------------------------------------- /tests/UseCase/Stock/ItemsQueryTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public static function invalidPageArgumentProvider(): \Generator 17 | { 18 | yield 'negative' => [-1]; 19 | yield 'zero' => [0]; 20 | } 21 | 22 | #[DataProvider('invalidPageArgumentProvider')] 23 | public function testInvalidPageArgument(int $page): void 24 | { 25 | $this->expectException(\InvalidArgumentException::class); 26 | new ItemsQuery(page: $page); 27 | } 28 | 29 | /** 30 | * @return \Generator 31 | */ 32 | public static function invalidPerPageArgumentProvider(): \Generator 33 | { 34 | yield 'negative' => [-1]; 35 | yield 'zero' => [0]; 36 | yield 'more than max' => [201]; 37 | } 38 | 39 | #[DataProvider('invalidPerPageArgumentProvider')] 40 | public function testInvalidPerPageArgument(int $per_page): void 41 | { 42 | $this->expectException(\InvalidArgumentException::class); 43 | new ItemsQuery(per_page: $per_page); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/UseCase/Client/Contact/fixtures/client_without_contacts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ContactPerson": { 4 | "id": "2", 5 | "user_id": "1", 6 | "user_profile_id": "2", 7 | "uuid": null, 8 | "country_id": "191", 9 | "name": "SuperFaktura, s.r.o.", 10 | "ico": "46655034", 11 | "dic": "2023513470", 12 | "ic_dph": "SK2023513470", 13 | "iban": "", 14 | "swift": "", 15 | "bank_account_prefix": "", 16 | "bank_account": "", 17 | "bank_code": "", 18 | "account": null, 19 | "email": "", 20 | "address": "Pri Suchom mlyne 6", 21 | "city": "Bratislava - mestská časť Staré Mesto", 22 | "zip": "811 04", 23 | "state": "", 24 | "country": "Slovensko", 25 | "delivery_name": "", 26 | "delivery_address": "", 27 | "delivery_city": "", 28 | "delivery_zip": "", 29 | "delivery_state": "", 30 | "delivery_country": "Slovensko", 31 | "delivery_country_id": "191", 32 | "phone": "", 33 | "delivery_phone": "", 34 | "fax": "", 35 | "due_date": null, 36 | "default_variable": "", 37 | "discount": null, 38 | "currency": null, 39 | "bank_account_id": "0", 40 | "comment": "", 41 | "tags": null, 42 | "distance": "0.00", 43 | "dont_travel": null, 44 | "created": "2023-06-12 07:00:39", 45 | "modified": "2023-08-23 11:17:12", 46 | "notices": true, 47 | "client": true 48 | } 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /tests/UseCase/Stock/MovementsQueryTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public static function invalidPageArgumentProvider(): \Generator 17 | { 18 | yield 'negative' => [-1]; 19 | yield 'zero' => [0]; 20 | } 21 | 22 | #[DataProvider('invalidPageArgumentProvider')] 23 | public function testInvalidPageArgument(int $page): void 24 | { 25 | $this->expectException(\InvalidArgumentException::class); 26 | new MovementsQuery(page: $page); 27 | } 28 | 29 | /** 30 | * @return \Generator 31 | */ 32 | public static function invalidPerPageArgumentProvider(): \Generator 33 | { 34 | yield 'negative' => [-1]; 35 | yield 'zero' => [0]; 36 | yield 'more than max' => [201]; 37 | } 38 | 39 | #[DataProvider('invalidPerPageArgumentProvider')] 40 | public function testInvalidPerPageArgument(int $per_page): void 41 | { 42 | $this->expectException(\InvalidArgumentException::class); 43 | new MovementsQuery(per_page: $per_page); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.woodpecker/php.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | &exec_event [ push ] 3 | 4 | workspace: 5 | base: /usr/src 6 | path: . 7 | 8 | clone: 9 | git: 10 | image: sf_ci_apiclient:latest 11 | commands: 12 | - mkdir -m 0700 /root/.ssh 13 | - echo "$${SSH_KEY_PRIV}" > /root/.ssh/id_ed25519 && chmod 0600 /root/.ssh/id_ed25519 14 | - echo "$${KNOWN_HOSTS}" > /root/.ssh/known_hosts 15 | - git clone "$${REPO_PATH}" 16 | - cd apiclient 17 | - git fetch 18 | - git checkout -q "${CI_COMMIT_SHA}" 19 | secrets: [ 'ssh_key_priv', 'known_hosts', 'git_host_url', 'git_host_port', 'repo_path' ] 20 | when: 21 | event: *exec_event 22 | 23 | steps: 24 | phpstan: 25 | image: sf_ci_apiclient:latest 26 | pull: false 27 | when: 28 | event: *exec_event 29 | commands: 30 | - cd apiclient 31 | - composer install --quiet 32 | - composer run php:phpstan 33 | phpunit: 34 | image: sf_ci_apiclient:latest 35 | pull: false 36 | when: 37 | event: *exec_event 38 | commands: 39 | - cd apiclient 40 | - composer install --quiet 41 | - composer run test:unit 42 | cs-check: 43 | image: sf_ci_apiclient:latest 44 | pull: false 45 | when: 46 | event: *exec_event 47 | commands: 48 | - cd apiclient 49 | - composer install --quiet 50 | - composer run php:cs-check 51 | -------------------------------------------------------------------------------- /tests/Utils/AssertRequest.php: -------------------------------------------------------------------------------- 1 | $expected_headers 12 | */ 13 | public function __construct( 14 | private ?RequestInterface $request, 15 | private ?string $expected_request_method, 16 | private ?string $expected_uri, 17 | private ?string $expected_request_body, 18 | private array $expected_headers, 19 | ) { 20 | } 21 | 22 | public function assert(): void 23 | { 24 | Assert::assertNotNull($this->request); 25 | 26 | if ($this->expected_request_method !== null) { 27 | Assert::assertSame($this->expected_request_method, $this->request->getMethod()); 28 | } 29 | 30 | if ($this->expected_uri !== null) { 31 | Assert::assertSame($this->expected_uri, $this->request->getUri()->getPath()); 32 | } 33 | 34 | if ($this->expected_request_body !== null) { 35 | Assert::assertJsonStringEqualsJsonString($this->expected_request_body, (string) $this->request->getBody()); 36 | } 37 | 38 | $this->assertHeaders(); 39 | } 40 | 41 | private function assertHeaders(): void 42 | { 43 | foreach ($this->expected_headers as $key => $expected_value) { 44 | Assert::assertSame($expected_value, $this->request?->getHeaderLine($key)); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/UseCase/Client/ClientsQueryTest.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public static function invalidPageArgumentProvider(): \Generator 21 | { 22 | yield 'negative' => [-1]; 23 | yield 'zero' => [0]; 24 | } 25 | 26 | #[DataProvider('invalidPageArgumentProvider')] 27 | public function testInvalidPageArgument(int $page): void 28 | { 29 | $this->expectException(\InvalidArgumentException::class); 30 | 31 | new ClientsQuery(page: $page); 32 | } 33 | 34 | /** 35 | * @return \Generator 36 | */ 37 | public static function invalidItemsPerPageArgumentProvider(): \Generator 38 | { 39 | yield 'negative' => [-1]; 40 | yield 'zero' => [0]; 41 | yield 'more than max' => [101]; 42 | } 43 | 44 | #[DataProvider('invalidItemsPerPageArgumentProvider')] 45 | public function testInvalidItemsPerPageArgument(int $items_per_page): void 46 | { 47 | $this->expectException(\InvalidArgumentException::class); 48 | 49 | new ClientsQuery(items_per_page: $items_per_page); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/UseCase/Invoice/InvoicesQueryTest.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public static function invalidPageArgumentProvider(): \Generator 21 | { 22 | yield 'negative' => [-1]; 23 | yield 'zero' => [0]; 24 | } 25 | 26 | #[DataProvider('invalidPageArgumentProvider')] 27 | public function testInvalidPageArgument(int $page): void 28 | { 29 | $this->expectException(\InvalidArgumentException::class); 30 | 31 | new InvoicesQuery(page: $page); 32 | } 33 | 34 | /** 35 | * @return \Generator 36 | */ 37 | public static function invalidItemsPerPageArgumentProvider(): \Generator 38 | { 39 | yield 'negative' => [-1]; 40 | yield 'zero' => [0]; 41 | yield 'more than max' => [201]; 42 | } 43 | 44 | #[DataProvider('invalidItemsPerPageArgumentProvider')] 45 | public function testInvalidItemsPerPageArgument(int $items_per_page): void 46 | { 47 | $this->expectException(\InvalidArgumentException::class); 48 | 49 | new InvoicesQuery(items_per_page: $items_per_page); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/UseCase/Expense/ExpensesQuery.php: -------------------------------------------------------------------------------- 1 | page < 1) { 41 | throw new \InvalidArgumentException('Page argument must be greater than or equal to 1'); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Authorization/EnvFileProvider.php: -------------------------------------------------------------------------------- 1 | loadEnv($path, overrideExistingVars: true); 19 | } catch (FormatException|PathException $exception) { 20 | throw new CannotLoadFileException(previous: $exception); 21 | } 22 | } 23 | 24 | /** 25 | * @throws InvalidDotEnvConfigException 26 | */ 27 | public function getAuthorization(): Authorization 28 | { 29 | return new Authorization( 30 | $this->getEnvByKey(DotEnvConfigKey::EMAIL) ?? throw new InvalidDotEnvConfigException(), 31 | $this->getEnvByKey(DotEnvConfigKey::KEY) ?? throw new InvalidDotEnvConfigException(), 32 | 'API', 33 | $this->getEnvByKey(DotEnvConfigKey::APP_TITLE) ?? throw new InvalidDotEnvConfigException(), 34 | $this->getEnvByKey(DotEnvConfigKey::COMPANY_ID) !== null 35 | ? (int) $this->getEnvByKey(DotEnvConfigKey::COMPANY_ID) 36 | : throw new InvalidDotEnvConfigException(), 37 | ); 38 | } 39 | 40 | private function getEnvByKey(string $key): ?string 41 | { 42 | return $_ENV[$key] ?? null; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/UseCase/Country/Countries.php: -------------------------------------------------------------------------------- 1 | request_factory 28 | ->createRequest( 29 | RequestMethodInterface::METHOD_GET, 30 | $this->base_uri . '/countries/index/view_full%3A1', 31 | ) 32 | ->withHeader('Authorization', $this->authorization_header_value); 33 | 34 | try { 35 | return $this->response_factory 36 | ->createFromJsonResponse($this->http_client->sendRequest($request)); 37 | } catch (ClientExceptionInterface|\JsonException $e) { 38 | throw new CannotGetAllCountriesException($request, $e->getMessage(), $e->getCode(), $e); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Response/ResponseTest.php: -------------------------------------------------------------------------------- 1 | [ 20 | 'expected' => true, 21 | 'response' => self::getApiResponse( 22 | data: ['error' => $value], 23 | ), 24 | ]; 25 | } 26 | 27 | foreach ([0, '0', false] as $value) { 28 | yield sprintf('response is not error if error property is %s (%s)', $value, gettype($value)) => [ 29 | 'expected' => false, 30 | 'response' => self::getApiResponse( 31 | data: ['error' => $value], 32 | ), 33 | ]; 34 | } 35 | 36 | yield 'response is not error if does not contain error property' => [ 37 | 'expected' => false, 38 | 'response' => self::getApiResponse( 39 | status_code: StatusCodeInterface::STATUS_OK, 40 | data: ['foo' => 'bar'], 41 | ), 42 | ]; 43 | } 44 | 45 | #[DataProvider('isErrorDataProvider')] 46 | public function testIsError(bool $expected, Response $response): void 47 | { 48 | self::assertSame($expected, $response->isError()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/UseCase/Invoice/Items.php: -------------------------------------------------------------------------------- 1 | request_factory 27 | ->createRequest( 28 | RequestMethodInterface::METHOD_DELETE, 29 | $this->base_uri . sprintf( 30 | '/invoice_items/delete/%s/invoice_id%%3A%d', 31 | implode(',', $item_ids), 32 | $invoice_id, 33 | ), 34 | ) 35 | ->withHeader('Authorization', $this->authorization_header_value); 36 | 37 | try { 38 | $response = $this->response_factory 39 | ->createFromJsonResponse($this->http_client->sendRequest($request)); 40 | } catch (ClientExceptionInterface|\JsonException $e) { 41 | throw new CannotDeleteInvoiceItemException($request, $e->getMessage(), $e->getCode(), $e); 42 | } 43 | 44 | if ($response->isError()) { 45 | throw new CannotDeleteInvoiceItemException($request, $response->data['message'] ?? ''); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superfaktura/apiclient", 3 | "minimum-stability": "stable", 4 | "description": "Api client for SuperFaktura | online invoicing tool", 5 | "type": "library", 6 | "keywords": ["invoice", "invoices", "business"], 7 | "homepage": "https://github.com/superfaktura/apiclient", 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "SuperFaktura team", 12 | "email": "info@superfaktura.sk", 13 | "homepage": "https://www.superfaktura.sk/", 14 | "role": "Developers" 15 | } 16 | ], 17 | "autoload": { 18 | "psr-4": { 19 | "SuperFaktura\\ApiClient\\": "src/", 20 | "SuperFaktura\\ApiClient\\Test\\": "tests/" 21 | } 22 | }, 23 | "require": { 24 | "php": ">=8.2", 25 | "ext-json": "*", 26 | "ext-zip": "*", 27 | "ext-mbstring": "*", 28 | "symfony/dotenv": "^6.4 | ^7.1 | ^7.2 | ^7.3", 29 | "guzzlehttp/guzzle": "^7.8", 30 | "fig/http-message-util": "^1.1", 31 | "guzzlehttp/psr7": "^2.6", 32 | "psr/http-factory": "^1.0", 33 | "psr/http-message": "^2.0", 34 | "psr/http-client": "^1.0" 35 | }, 36 | "require-dev": { 37 | "phpunit/phpunit": "^10.5", 38 | "phpstan/phpstan": "^1.10", 39 | "friendsofphp/php-cs-fixer": "^3.49", 40 | "phpstan/phpstan-strict-rules": "^1.5", 41 | "phpstan/phpstan-deprecation-rules": "^1.1" 42 | }, 43 | "scripts": { 44 | "test:unit": "./vendor/bin/phpunit", 45 | "test:coverage": "XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-filter src/ --coverage-html coverage", 46 | "php:phpstan": "XDEBUG_MODE=off ./vendor/bin/phpstan analyse src tests --memory-limit 256M", 47 | "php:phpstan-baseline": "XDEBUG_MODE=off ./vendor/bin/phpstan analyse src tests --generate-baseline", 48 | "php:cs-fix": "XDEBUG_MODE=off ./vendor/bin/php-cs-fixer fix --allow-risky=yes", 49 | "php:cs-check": "XDEBUG_MODE=off ./vendor/bin/php-cs-fixer fix --dry-run --allow-risky=yes -n 2> /dev/null" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/UseCase/Stock/fixtures/stock-items.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "StockItem": { 4 | "id": "9", 5 | "user_id": "22", 6 | "user_profile_id": "12", 7 | "name": "Rozok grahamovy", 8 | "description": "changed!!", 9 | "sku": "RZK-GRHMV", 10 | "unit": "ks", 11 | "unit_price": 0.1, 12 | "purchase_unit_price": "0.1200", 13 | "purchase_currency": null, 14 | "vat": 20, 15 | "purchase_vat": null, 16 | "watch_stock": true, 17 | "stock": 0, 18 | "import_type": null, 19 | "import_id": null, 20 | "internal_comment": null, 21 | "hide_in_autocomplete": null, 22 | "created": "2023-10-10 07:59:57", 23 | "modified": "2023-10-10 07:59:57" 24 | } 25 | }, 26 | { 27 | "StockItem": { 28 | "id": "6", 29 | "user_id": "22", 30 | "user_profile_id": "12", 31 | "name": "roxor", 32 | "description": "", 33 | "sku": "RX-123", 34 | "unit": "ks", 35 | "unit_price": 6, 36 | "purchase_unit_price": "5.0000", 37 | "purchase_currency": "EUR", 38 | "vat": 20, 39 | "purchase_vat": "20.00", 40 | "watch_stock": true, 41 | "stock": 7, 42 | "import_type": null, 43 | "import_id": null, 44 | "internal_comment": "", 45 | "hide_in_autocomplete": null, 46 | "created": "2023-01-20 13:36:32", 47 | "modified": "2023-01-20 14:10:20" 48 | } 49 | }, 50 | { 51 | "StockItem": { 52 | "id": "5", 53 | "user_id": "22", 54 | "user_profile_id": "12", 55 | "name": "Stock item example", 56 | "description": "Stock item description", 57 | "sku": null, 58 | "unit": null, 59 | "unit_price": 10, 60 | "purchase_unit_price": null, 61 | "purchase_currency": null, 62 | "vat": 20, 63 | "purchase_vat": null, 64 | "watch_stock": true, 65 | "stock": 100, 66 | "import_type": null, 67 | "import_id": null, 68 | "internal_comment": null, 69 | "hide_in_autocomplete": null, 70 | "created": "2022-08-23 08:56:37", 71 | "modified": "2022-08-23 08:56:38" 72 | } 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /tests/Authorization/SimpleProviderTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public static function getAuthorizationProvider(): \Generator 18 | { 19 | yield 'authorization' => [ 20 | 'expected' => new Authorization( 21 | email: 'test@example.com', 22 | key: 'a6b3f12', 23 | module: 'API', 24 | app_title: 'Example s.r.o.', 25 | company_id: 1, 26 | ), 27 | 'email' => 'test@example.com', 28 | 'key' => 'a6b3f12', 29 | 'company_id' => 1, 30 | 'app_title' => 'Example s.r.o.', 31 | ]; 32 | 33 | yield 'another authorization' => [ 34 | 'expected' => new Authorization( 35 | email: 'another@example.com', 36 | key: 'e1bac132', 37 | module: 'API', 38 | app_title: 'Example2 s.r.o.', 39 | company_id: 2, 40 | ), 41 | 'email' => 'another@example.com', 42 | 'key' => 'e1bac132', 43 | 'company_id' => 2, 44 | 'app_title' => 'Example2 s.r.o.', 45 | ]; 46 | } 47 | 48 | #[DataProvider('getAuthorizationProvider')] 49 | public function testGetAuthorization( 50 | Authorization $expected, 51 | string $email, 52 | string $key, 53 | int $company_id, 54 | string $app_title, 55 | ): void { 56 | $provider = new SimpleProvider($email, $key, $app_title, $company_id); 57 | self::assertEquals(expected: $expected, actual: $provider->getAuthorization()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/Filter/NamedParamsConvertorTest.php: -------------------------------------------------------------------------------- 1 | convertor = new NamedParamsConvertor(); 22 | } 23 | 24 | /** 25 | * @return \Generator}> 26 | */ 27 | public static function convertProvider(): \Generator 28 | { 29 | yield 'no parameters no problem' => [ 30 | 'expected' => '', 31 | 'params' => [], 32 | ]; 33 | 34 | yield 'parameter name and value is separated by urlencoded ":"' => [ 35 | 'expected' => 'foo%3Abar', 36 | 'params' => ['foo' => 'bar'], 37 | ]; 38 | 39 | yield 'parameters are separated by "/"' => [ 40 | 'expected' => 'foo%3Abar/baz%3Aqux', 41 | 'params' => ['foo' => 'bar', 'baz' => 'qux'], 42 | ]; 43 | 44 | yield 'parameter values are urlencoded' => [ 45 | 'expected' => 'foo%3ASuperFakt%C3%BAra/bar%3A%3A%40%2F', 46 | 'params' => ['foo' => 'SuperFaktúra', 'bar' => ':@/'], 47 | ]; 48 | 49 | yield 'null parameter value is not included into query string' => [ 50 | 'expected' => '', 51 | 'params' => ['foo' => null], 52 | ]; 53 | } 54 | 55 | /** 56 | * @param array $params 57 | */ 58 | #[DataProvider('convertProvider')] 59 | public function testConvert(string $expected, array $params): void 60 | { 61 | self::assertSame( 62 | expected: $expected, 63 | actual: $this->convertor->convert($params), 64 | ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/UseCase/Client/Contact/fixtures/client_with_contacts.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ContactPerson": { 4 | "id": "2", 5 | "user_id": "1", 6 | "user_profile_id": "2", 7 | "client_id": "2", 8 | "name": "Joe Doe", 9 | "email": "joe.doe@supefaktura.sk", 10 | "phone": "+421949123456", 11 | "created": "2023-08-23 11:17:12", 12 | "modified": "2023-08-23 11:17:12" 13 | } 14 | }, 15 | { 16 | "ContactPerson": { 17 | "id": "3", 18 | "user_id": "1", 19 | "user_profile_id": "2", 20 | "client_id": "2", 21 | "name": "Jane Doe", 22 | "email": "jane.doe@supefaktura.sk", 23 | "phone": "+421949654321", 24 | "created": "2023-08-23 11:17:12", 25 | "modified": "2023-08-23 11:17:12" 26 | } 27 | }, 28 | { 29 | "ContactPerson": { 30 | "id": "2", 31 | "user_id": "1", 32 | "user_profile_id": "2", 33 | "uuid": null, 34 | "country_id": "191", 35 | "name": "SuperFaktura, s.r.o.", 36 | "ico": "46655034", 37 | "dic": "2023513470", 38 | "ic_dph": "SK2023513470", 39 | "iban": "", 40 | "swift": "", 41 | "bank_account_prefix": "", 42 | "bank_account": "", 43 | "bank_code": "", 44 | "account": null, 45 | "email": "", 46 | "address": "Pri Suchom mlyne 6", 47 | "city": "Bratislava - mestská časť Staré Mesto", 48 | "zip": "811 04", 49 | "state": "", 50 | "country": "Slovensko", 51 | "delivery_name": "", 52 | "delivery_address": "", 53 | "delivery_city": "", 54 | "delivery_zip": "", 55 | "delivery_state": "", 56 | "delivery_country": "Slovensko", 57 | "delivery_country_id": "191", 58 | "phone": "", 59 | "delivery_phone": "", 60 | "fax": "", 61 | "due_date": null, 62 | "default_variable": "", 63 | "discount": null, 64 | "currency": null, 65 | "bank_account_id": "0", 66 | "comment": "", 67 | "tags": null, 68 | "distance": "0.00", 69 | "dont_travel": null, 70 | "created": "2023-06-12 07:00:39", 71 | "modified": "2023-08-23 11:17:12", 72 | "notices": true, 73 | "client": true 74 | } 75 | } 76 | ] 77 | -------------------------------------------------------------------------------- /tests/UseCase/Stock/fixtures/stock-movements.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemCount": 3, 3 | "pageCount": 1, 4 | "perPage": 50, 5 | "page": 1, 6 | "items": [ 7 | { 8 | "StockLog": { 9 | "id": "3", 10 | "stock_item_id": "1", 11 | "user_id": "1", 12 | "user_profile_id": "1", 13 | "invoice_id": null, 14 | "expense_id": null, 15 | "document_item_id": null, 16 | "quantity": 5, 17 | "note": "Hungry binge shopping at kaufland", 18 | "document": null, 19 | "document_subtype": null, 20 | "log_data": "{\"purchase_unit_price\":\"0.1200\",\"purchase_tax\":null,\"purchase_currency\":null,\"unit_price\":0.1,\"tax\":20,\"currency\":\"EUR\"}", 21 | "created": "2023-11-06 11:02:53", 22 | "modified": "2023-11-06 11:02:53" 23 | } 24 | }, 25 | { 26 | "StockLog": { 27 | "id": "2", 28 | "stock_item_id": "1", 29 | "user_id": "1", 30 | "user_profile_id": "1", 31 | "invoice_id": null, 32 | "expense_id": null, 33 | "document_item_id": null, 34 | "quantity": 0, 35 | "note": "Ručná úprava stavu na sklade", 36 | "document": null, 37 | "document_subtype": null, 38 | "log_data": "{\"purchase_unit_price\":\"0.1200\",\"purchase_tax\":null,\"purchase_exchange_rate\":1,\"purchase_country_exchange_rate\":1,\"purchase_currency\":null,\"unit_price\":\"0.1\",\"tax\":20,\"exchange_rate\":1,\"country_exchange_rate\":1,\"currency\":\"EUR\",\"home_currency\":\"EUR\"}", 39 | "created": "2023-10-24 11:45:19", 40 | "modified": "2023-10-24 11:45:19" 41 | } 42 | }, 43 | { 44 | "StockLog": { 45 | "id": "1", 46 | "stock_item_id": "1", 47 | "user_id": "1", 48 | "user_profile_id": "1", 49 | "invoice_id": null, 50 | "expense_id": null, 51 | "document_item_id": null, 52 | "quantity": 0, 53 | "note": "Počiatočný stav skladu", 54 | "document": null, 55 | "document_subtype": null, 56 | "log_data": "{\"purchase_unit_price\":0.12,\"purchase_tax\":null,\"purchase_currency\":null,\"unit_price\":0.1,\"tax\":20,\"currency\":null}", 57 | "created": "2023-10-10 07:59:57", 58 | "modified": "2023-10-10 07:59:57" 59 | } 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/Contract/Expense/Expenses.php: -------------------------------------------------------------------------------- 1 | $expense 29 | * @param array> $items 30 | * @param array $client 31 | * @param array $extra 32 | * @param array $my_data 33 | * @param int[] $tags 34 | * 35 | * @throws CannotCreateExpenseException 36 | * @throws CannotCreateRequestException 37 | */ 38 | public function create( 39 | array $expense, 40 | array $items = [], 41 | array $client = [], 42 | array $extra = [], 43 | array $my_data = [], 44 | array $tags = [], 45 | ): Response; 46 | 47 | /** 48 | * @param array $expense 49 | * @param array> $items 50 | * @param array $client 51 | * @param array $extra 52 | * @param array $my_data 53 | * @param int[] $tags 54 | * 55 | * @throws CannotUpdateExpenseException 56 | * @throws CannotCreateRequestException 57 | * @throws ExpenseNotFoundException 58 | */ 59 | public function update( 60 | int $id, 61 | array $expense = [], 62 | array $items = [], 63 | array $client = [], 64 | array $extra = [], 65 | array $my_data = [], 66 | array $tags = [], 67 | ): Response; 68 | 69 | /** 70 | * @throws CannotDeleteExpenseException 71 | * @throws ExpenseNotFoundException 72 | */ 73 | public function delete(int $id): void; 74 | } 75 | -------------------------------------------------------------------------------- /src/UseCase/Export/InvoiceExportRequestFactory.php: -------------------------------------------------------------------------------- 1 | true, 27 | ...$this->getFormatSpecificOptions($format, $pdf_options), 28 | 'pdf_lang_default' => $pdf_options->language?->value, 29 | 'hide_pdf_payment_info' => $pdf_options->hide_payment_info, 30 | 'hide_signature' => $pdf_options->hide_signature, 31 | ]); 32 | 33 | try { 34 | return json_encode( 35 | [ 36 | self::INVOICE => ['ids' => $ids], 37 | self::EXPORT => $export_options, 38 | ], 39 | JSON_THROW_ON_ERROR, 40 | ); 41 | } catch (\JsonException $e) { 42 | throw new CannotCreateRequestException($e->getMessage(), $e->getCode(), $e); 43 | } 44 | } 45 | 46 | /** 47 | * @return array 48 | */ 49 | private function getFormatSpecificOptions(Format $format, PdfExportOptions $pdf_options): array 50 | { 51 | return match ($format) { 52 | Format::PDF => [ 53 | 'invoices_pdf' => true, 54 | 'merge_pdf' => true, 55 | 'only_merge' => true, 56 | ], 57 | Format::ZIP => [ 58 | 'invoices_pdf' => true, 59 | ...match ($pdf_options->document_sort) { 60 | DocumentSort::CLIENT => ['pdf_sort_client' => true], 61 | DocumentSort::DATE => ['pdf_sort_date' => true], 62 | default => [], 63 | }, 64 | ], 65 | }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/UseCase/CashRegister/Items.php: -------------------------------------------------------------------------------- 1 | request_factory->createRequest( 28 | RequestMethodInterface::METHOD_POST, 29 | $this->base_uri . '/cash_register_items/add', 30 | )->withHeader('Authorization', $this->authorization_header_value) 31 | ->withHeader('Content-Type', 'application/json') 32 | ->withBody(Utils::streamFor($this->jsonFrom($cash_register_id, $data))); 33 | 34 | try { 35 | $http_response = $this->http_client->sendRequest($request); 36 | $response = $this->response_factory->createFromJsonResponse($http_response); 37 | } catch (ClientExceptionInterface|\JsonException $e) { 38 | throw new CashRegister\CannotCreateCashRegisterItemException($request, $e->getMessage(), $e->getCode(), $e); 39 | } 40 | 41 | if ($response->isError()) { 42 | throw new CashRegister\CannotCreateCashRegisterItemException($request, $response->data['message'] ?? ''); 43 | } 44 | 45 | return $response; 46 | } 47 | 48 | /** 49 | * @param array $data 50 | * 51 | * @throws \JsonException 52 | */ 53 | private function jsonFrom(int $cash_register_id, array $data): string 54 | { 55 | return json_encode(['CashRegisterItem' => [...$data, 'cash_register_id' => $cash_register_id]], JSON_THROW_ON_ERROR); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/UseCase/Invoice/InvoicesQuery.php: -------------------------------------------------------------------------------- 1 | page < 1) { 50 | throw new \InvalidArgumentException('Page argument must be greater than or equal to 1'); 51 | } 52 | 53 | if ($this->items_per_page < 1 || $this->items_per_page > self::ITEMS_PER_PAGE_MAX) { 54 | throw new \InvalidArgumentException(sprintf( 55 | 'Items per page argument must be greater than or equal to 1 and less than %d', 56 | self::ITEMS_PER_PAGE_MAX, 57 | )); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/UseCase/Country/CountriesTest.php: -------------------------------------------------------------------------------- 1 | getCountries($this->getHttpClientReturning($fixture)) 31 | ->getAll(); 32 | 33 | $this->request() 34 | ->get('/countries/index/view_full%3A1') 35 | ->withAuthorizationHeader(self::AUTHORIZATION_HEADER_VALUE) 36 | ->assert(); 37 | 38 | self::assertSame($this->arrayFromFixture($fixture), $response->data); 39 | } 40 | 41 | public function testGetAllRequestFailed(): void 42 | { 43 | $this->expectException(CannotGetAllCountriesException::class); 44 | 45 | $this 46 | ->getCountries($this->getHttpClientWithMockRequestException()) 47 | ->getAll(); 48 | } 49 | 50 | public function testGetAllResponseDecodeFailed(): void 51 | { 52 | $this->expectException(CannotGetAllCountriesException::class); 53 | 54 | $this 55 | ->getCountries($this->getHttpClientWithMockResponse($this->getHttpOkResponseContainingInvalidJson())) 56 | ->getAll(); 57 | } 58 | 59 | private function getCountries(ClientInterface $client): Countries 60 | { 61 | return new Countries( 62 | http_client: $client, 63 | request_factory: new HttpFactory(), 64 | response_factory: new ResponseFactory(), 65 | base_uri: '', 66 | authorization_header_value: self::AUTHORIZATION_HEADER_VALUE, 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Authorization/Header/BuilderTest.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public static function buildProvider(): \Generator 20 | { 21 | yield 'authorization' => [ 22 | 'expected' => 'SFAPI ' . http_build_query([ 23 | 'email' => 'test@example.com', 24 | 'apikey' => 'cd114a5', 25 | 'company_id' => 1, 26 | 'module' => self::getModuleString('Test', 'Example s.r.o.'), 27 | ]), 28 | 'authorization' => new Authorization( 29 | 'test@example.com', 30 | 'cd114a5', 31 | 'Test', 32 | 'Example s.r.o.', 33 | 1, 34 | ), 35 | ]; 36 | 37 | yield 'another authorization' => [ 38 | 'expected' => 'SFAPI ' . http_build_query([ 39 | 'email' => 'test2@example.com', 40 | 'apikey' => 'a6b3f12', 41 | 'company_id' => 2, 42 | 'module' => self::getModuleString('API', 'Example2 s.r.o'), 43 | ]), 44 | 'authorization' => new Authorization( 45 | 'test2@example.com', 46 | 'a6b3f12', 47 | 'API', 48 | 'Example2 s.r.o', 49 | 2, 50 | ), 51 | ]; 52 | } 53 | 54 | private static function getModuleString(string $module, string $app_title): string 55 | { 56 | return sprintf('%s [%s] (w/ SFAPI %s) [%s]', $module, $app_title, self::MOCK_PACKAGE_VERSION, PHP_VERSION_ID); 57 | } 58 | 59 | #[DataProvider('buildProvider')] 60 | public function testBuild(string $expected, Authorization $authorization): void 61 | { 62 | $fake_version_provider = $this->createMock(Version\Provider::class); 63 | $fake_version_provider->method('getVersion')->willReturn(self::MOCK_PACKAGE_VERSION); 64 | 65 | $builder = new Header\Builder($fake_version_provider); 66 | 67 | self::assertSame( 68 | expected: $expected, 69 | actual: $builder->build($authorization), 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/UseCase/CashRegister/CashRegisters.php: -------------------------------------------------------------------------------- 1 | items = new Items( 27 | $this->http_client, 28 | $this->request_factory, 29 | $this->response_factory, 30 | $this->base_uri, 31 | $this->authorization_header_value, 32 | ); 33 | } 34 | 35 | public function getAll(): Response 36 | { 37 | $request = $this->request_factory 38 | ->createRequest( 39 | RequestMethodInterface::METHOD_GET, 40 | $this->base_uri . '/cash_registers/getDetails', 41 | )->withHeader('Authorization', $this->authorization_header_value); 42 | 43 | try { 44 | $response = $this->response_factory->createFromJsonResponse( 45 | $this->http_client->sendRequest($request), 46 | ); 47 | } catch (\JsonException|ClientExceptionInterface $e) { 48 | throw new CannotGetAllCashRegistersException($request, $e->getMessage(), $e->getCode(), $e); 49 | } 50 | 51 | return $response; 52 | } 53 | 54 | public function getById(int $id): Response 55 | { 56 | $request = $this->request_factory 57 | ->createRequest( 58 | RequestMethodInterface::METHOD_GET, 59 | $this->base_uri . '/cash_registers/view/' . $id, 60 | )->withHeader('Authorization', $this->authorization_header_value); 61 | 62 | try { 63 | $response = $this->response_factory->createFromJsonResponse( 64 | $this->http_client->sendRequest($request), 65 | ); 66 | } catch (\JsonException|ClientExceptionInterface $e) { 67 | throw new Contract\CashRegister\CannotGetCashRegisterException($request, $e->getMessage(), $e->getCode(), $e); 68 | } 69 | 70 | if ($response->status_code === StatusCodeInterface::STATUS_NOT_FOUND) { 71 | throw new Contract\CashRegister\CashRegisterNotFoundException($request); 72 | } 73 | 74 | return $response; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/UseCase/Invoice/Item/ItemsTest.php: -------------------------------------------------------------------------------- 1 | [ 29 | 'invoice_id' => 1, 30 | 'items' => [2], 31 | ]; 32 | 33 | yield 'delete multiple items' => [ 34 | 'invoice_id' => 2, 35 | 'items' => [3, 4], 36 | ]; 37 | } 38 | 39 | /** 40 | * @param int[] $items 41 | */ 42 | #[DataProvider('deleteProvider')] 43 | public function testDelete(int $invoice_id, array $items): void 44 | { 45 | $this 46 | ->getItems($this->getHttpClientWithMockResponse($this->getHttpOkResponse())) 47 | ->delete($invoice_id, $items); 48 | 49 | $this->request() 50 | ->delete(sprintf('/invoice_items/delete/%s/invoice_id%%3A%d', implode(',', $items), $invoice_id)) 51 | ->withAuthorizationHeader(self::AUTHORIZATION_HEADER_VALUE) 52 | ->assert(); 53 | } 54 | 55 | public function testDeleteFailed(): void 56 | { 57 | $this->expectException(CannotDeleteInvoiceItemException::class); 58 | $this->expectExceptionMessage('Unexpected error'); 59 | 60 | $fixture = __DIR__ . '/../../fixtures/unexpected-error.json'; 61 | 62 | $this 63 | ->getItems($this->getHttpClientReturning($fixture)) 64 | ->delete(1, [2]); 65 | } 66 | 67 | public function testDeleteRequestFailed(): void 68 | { 69 | $this->expectException(CannotDeleteInvoiceItemException::class); 70 | 71 | $this 72 | ->getItems($this->getHttpClientWithMockRequestException()) 73 | ->delete(1, [2]); 74 | } 75 | 76 | private function getItems(ClientInterface $client): Items 77 | { 78 | return new Items( 79 | http_client: $client, 80 | request_factory: new HttpFactory(), 81 | response_factory: new ResponseFactory(), 82 | base_uri: '', 83 | authorization_header_value: self::AUTHORIZATION_HEADER_VALUE, 84 | ); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /tests/Utils/AssertRequestBuilder.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | private array $expected_headers = []; 22 | 23 | public function __construct(?RequestInterface $request) 24 | { 25 | $this->request = $request; 26 | } 27 | 28 | /** 29 | * @param \Fig\Http\Message\RequestMethodInterface::*|null $expected_request_method 30 | */ 31 | public function withMethod(?string $expected_request_method): AssertRequestBuilder 32 | { 33 | $this->expected_request_method = $expected_request_method; 34 | 35 | return $this; 36 | } 37 | 38 | public function get(string $expected_uri): AssertRequestBuilder 39 | { 40 | $this->withUri($expected_uri); 41 | 42 | return $this->withMethod(RequestMethodInterface::METHOD_GET); 43 | } 44 | 45 | public function post(string $expected_uri): AssertRequestBuilder 46 | { 47 | $this->withUri($expected_uri); 48 | 49 | return $this->withMethod(RequestMethodInterface::METHOD_POST); 50 | } 51 | 52 | public function patch(string $expected_uri): AssertRequestBuilder 53 | { 54 | $this->withUri($expected_uri); 55 | 56 | return $this->withMethod(RequestMethodInterface::METHOD_PATCH); 57 | } 58 | 59 | public function delete(string $expected_uri): AssertRequestBuilder 60 | { 61 | $this->withUri($expected_uri); 62 | 63 | return $this->withMethod(RequestMethodInterface::METHOD_DELETE); 64 | } 65 | 66 | public function withUri(string $expected_uri): AssertRequestBuilder 67 | { 68 | $this->expected_uri = $expected_uri; 69 | 70 | return $this; 71 | } 72 | 73 | public function withBody(string $expected_request_body): AssertRequestBuilder 74 | { 75 | $this->expected_request_body = $expected_request_body; 76 | 77 | return $this; 78 | } 79 | 80 | public function withHeader(string $header_name, string $expected_value): AssertRequestBuilder 81 | { 82 | $this->expected_headers[$header_name] = $expected_value; 83 | 84 | return $this; 85 | } 86 | 87 | public function withAuthorizationHeader(string $header_value): AssertRequestBuilder 88 | { 89 | return $this->withHeader('Authorization', $header_value); 90 | } 91 | 92 | public function withContentTypeJson(): AssertRequestBuilder 93 | { 94 | return $this->withHeader('Content-Type', 'application/json'); 95 | } 96 | 97 | public function assert(): void 98 | { 99 | (new AssertRequest( 100 | $this->request, 101 | $this->expected_request_method, 102 | $this->expected_uri, 103 | $this->expected_request_body, 104 | $this->expected_headers, 105 | ))->assert(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/Authorization/EnvFileProviderTest.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public static function envFileDataProvider(): \Generator 29 | { 30 | yield 'file with complete config data' => [ 31 | 'expected' => new Authorization( 32 | email: 'test@example.com', 33 | key: 'test', 34 | module: 'API', 35 | app_title: 'Example s.r.o.', 36 | company_id: 1, 37 | ), 38 | 'path' => __DIR__ . DIRECTORY_SEPARATOR . self::VALID_FILE, 39 | ]; 40 | 41 | yield 'another file with complete config data' => [ 42 | 'expected' => new \SuperFaktura\ApiClient\Authorization\Authorization( 43 | email: 'test2@example.com', 44 | key: 'test2', 45 | module: 'API', 46 | app_title: 'Example2 s.r.o.', 47 | company_id: 2, 48 | ), 49 | 'path' => __DIR__ . DIRECTORY_SEPARATOR . self::ANOTHER_VALID_FILE, 50 | ]; 51 | } 52 | 53 | protected function setUp(): void 54 | { 55 | $this->clearEnvironment(); 56 | } 57 | 58 | public function testWithMissingFile(): void 59 | { 60 | $this->expectException(CannotLoadFileException::class); 61 | new EnvFileProvider(__DIR__ . DIRECTORY_SEPARATOR . self::NON_EXISTING_FILE); 62 | } 63 | 64 | public function testWithIncompleteFile(): void 65 | { 66 | $this->expectException(InvalidDotEnvConfigException::class); 67 | $provider = new EnvFileProvider(__DIR__ . DIRECTORY_SEPARATOR . self::INCOMPLETE_FILE); 68 | $provider->getAuthorization(); 69 | } 70 | 71 | #[DataProvider('envFileDataProvider')] 72 | public function testWithValidFile(Authorization $expected, string $path): void 73 | { 74 | self::assertEquals( 75 | expected: $expected, 76 | actual: (new EnvFileProvider($path))->getAuthorization(), 77 | ); 78 | } 79 | 80 | /** 81 | * Clear all env variables accessed by provider 82 | */ 83 | private function clearEnvironment(): void 84 | { 85 | unset( 86 | $_ENV[DotEnvConfigKey::EMAIL], 87 | $_ENV[DotEnvConfigKey::KEY], 88 | $_ENV[DotEnvConfigKey::APP_TITLE], 89 | $_ENV[DotEnvConfigKey::COMPANY_ID], 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Response/ResponseFactory.php: -------------------------------------------------------------------------------- 1 | getStatusCode(), 21 | data: (array) json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR), 22 | rate_limit_daily: $this->getDailyRateLimit($response), 23 | rate_limit_monthly: $this->getMonthlyRateLimit($response), 24 | ); 25 | } 26 | 27 | public function createFromBinaryResponse(ResponseInterface $response): BinaryResponse 28 | { 29 | $resource = $response->getBody()->detach(); 30 | 31 | if (!is_resource($resource)) { 32 | throw new CannotCreateResponseException('Stream resource is not available'); 33 | } 34 | 35 | if (!$response->hasHeader('Content-Type')) { 36 | throw new CannotCreateResponseException('Missing content type header'); 37 | } 38 | 39 | return new BinaryResponse( 40 | status_code: $response->getStatusCode(), 41 | content_type: $response->getHeaderLine('Content-Type'), 42 | data: $resource, 43 | rate_limit_daily: $this->getDailyRateLimit($response), 44 | rate_limit_monthly: $this->getMonthlyRateLimit($response), 45 | ); 46 | } 47 | 48 | /** 49 | * @throws \UnexpectedValueException 50 | */ 51 | private function getDatetimeImmutable(string $input): \DateTimeImmutable 52 | { 53 | $datetime = \DateTimeImmutable::createFromFormat( 54 | self::RATE_LIMIT_RESET_DATETIME_FORMAT, 55 | $input, 56 | new \DateTimeZone(self::SF_API_TIMEZONE), 57 | ); 58 | 59 | if ($datetime === false) { 60 | throw new \UnexpectedValueException(sprintf('Invalid datetime "%s"', $input)); 61 | } 62 | 63 | return $datetime->setTimezone(new \DateTimeZone(self::RESPONSE_TIMEZONE)); 64 | } 65 | 66 | private function getDailyRateLimit(ResponseInterface $response): ?RateLimit 67 | { 68 | if (!$response->hasHeader('X-RateLimit-DailyLimit')) { 69 | return null; 70 | } 71 | 72 | return new RateLimit( 73 | limit: (int) $response->getHeaderLine('X-RateLimit-DailyLimit'), 74 | remaining: (int) $response->getHeaderLine('X-RateLimit-DailyRemaining'), 75 | resets_at: $this->getDatetimeImmutable( 76 | $response->getHeaderLine('X-RateLimit-DailyReset'), 77 | ), 78 | ); 79 | } 80 | 81 | private function getMonthlyRateLimit(ResponseInterface $response): ?RateLimit 82 | { 83 | if (!$response->hasHeader('X-RateLimit-MonthlyLimit')) { 84 | return null; 85 | } 86 | 87 | return new RateLimit( 88 | limit: (int) $response->getHeaderLine('X-RateLimit-MonthlyLimit'), 89 | remaining: (int) $response->getHeaderLine('X-RateLimit-MonthlyRemaining'), 90 | resets_at: $this->getDatetimeImmutable( 91 | $response->getHeaderLine('X-RateLimit-MonthlyReset'), 92 | ), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/UseCase/Expense/Payment/Payments.php: -------------------------------------------------------------------------------- 1 | request_factory 33 | ->createRequest( 34 | RequestMethodInterface::METHOD_POST, 35 | $this->base_uri . '/expense_payments/add', 36 | ) 37 | ->withHeader('Authorization', $this->authorization_header_value) 38 | ->withHeader('Content-Type', 'application/json') 39 | ->withBody(Utils::streamFor($this->transformDataToJson($id, $payment))); 40 | 41 | try { 42 | $response = $this->response_factory->createFromJsonResponse( 43 | $this->http_client->sendRequest($request), 44 | ); 45 | } catch (ClientExceptionInterface|\JsonException $e) { 46 | throw new CannotPayExpenseException($request, $e->getMessage(), $e->getCode(), $e); 47 | } 48 | 49 | if ($response->isError()) { 50 | throw new CannotPayExpenseException($request, $response->data['message'] ?? ''); 51 | } 52 | 53 | return $response; 54 | } 55 | 56 | public function delete(int $id): void 57 | { 58 | $request = $this->request_factory 59 | ->createRequest( 60 | RequestMethodInterface::METHOD_DELETE, 61 | $this->base_uri . '/expense_payments/delete/' . $id, 62 | ) 63 | ->withHeader('Authorization', $this->authorization_header_value); 64 | 65 | try { 66 | $response = $this->response_factory 67 | ->createFromJsonResponse($this->http_client->sendRequest($request)); 68 | } catch (ClientExceptionInterface|\JsonException $e) { 69 | throw new CannotDeleteExpensePaymentException($request, $e->getMessage(), $e->getCode(), $e); 70 | } 71 | 72 | if ($response->isError()) { 73 | throw new CannotDeleteExpensePaymentException($request, $response->data['message'] ?? ''); 74 | } 75 | } 76 | 77 | /** 78 | * @throws CannotCreateRequestException 79 | */ 80 | private function transformDataToJson(int $id, Payment $payment): string 81 | { 82 | try { 83 | return json_encode( 84 | [ 85 | self::EXPENSE_PAYMENT => array_filter([ 86 | 'expense_id' => $id, 87 | 'amount' => $payment->amount, 88 | 'currency' => $payment->currency?->value, 89 | 'payment_type' => $payment->payment_type?->value, 90 | 'created' => $payment->payment_date?->format('Y-m-d'), 91 | ]), 92 | ], 93 | JSON_THROW_ON_ERROR, 94 | ); 95 | } catch (\JsonException $e) { 96 | throw new CannotCreateRequestException($e->getMessage(), $e->getCode(), $e); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Contract/Invoice/Invoices.php: -------------------------------------------------------------------------------- 1 | $invoice 41 | * @param array> $items 42 | * @param array $client 43 | * @param array $settings 44 | * @param array $extra 45 | * @param array $my_data 46 | * @param int[] $tags 47 | * 48 | * @throws CannotCreateInvoiceException 49 | * @throws CannotCreateRequestException 50 | */ 51 | public function create( 52 | array $invoice, 53 | array $items, 54 | array $client, 55 | array $settings = [], 56 | array $extra = [], 57 | array $my_data = [], 58 | array $tags = [], 59 | ): Response; 60 | 61 | /** 62 | * @param array $invoice 63 | * @param array> $items 64 | * @param array $client 65 | * @param array $settings 66 | * @param array $extra 67 | * @param array $my_data 68 | * @param int[] $tags 69 | * 70 | * @throws CannotUpdateInvoiceException 71 | * @throws CannotCreateRequestException 72 | * @throws InvoiceNotFoundException 73 | */ 74 | public function update( 75 | int $id, 76 | array $invoice = [], 77 | array $items = [], 78 | array $client = [], 79 | array $settings = [], 80 | array $extra = [], 81 | array $my_data = [], 82 | array $tags = [], 83 | ): Response; 84 | 85 | /** 86 | * @throws CannotDeleteInvoiceException 87 | * @throws InvoiceNotFoundException 88 | */ 89 | public function delete(int $id): void; 90 | 91 | /** 92 | * @throws CannotChangeInvoiceLanguageException 93 | * @throws InvoiceNotFoundException 94 | */ 95 | public function changeLanguage(int $id, Language $language): void; 96 | 97 | /** 98 | * @throws InvoiceNotFoundException 99 | * @throws CannotMarkInvoiceAsSentException 100 | */ 101 | public function markAsSent(int $id): void; 102 | 103 | /** 104 | * @throws InvoiceNotFoundException 105 | * @throws CannotMarkInvoiceAsSentException 106 | */ 107 | public function markAsSentViaEmail( 108 | int $id, 109 | string $email, 110 | string $subject = '', 111 | string $message = '', 112 | ): void; 113 | 114 | /** 115 | * @throws CannotSendInvoiceException 116 | * @throws InvoiceNotFoundException 117 | */ 118 | public function sendViaEmail(int $id, Email $email): void; 119 | 120 | /** 121 | * @throws CannotSendInvoiceException 122 | * @throws InvoiceNotFoundException 123 | */ 124 | public function sendViaPostOffice(int $id, Address $address = new Address()): void; 125 | 126 | /** 127 | * @throws CannotCreateInvoiceException 128 | * @throws CannotCreateRequestException 129 | */ 130 | public function createRegularFromProforma(int $proforma_id): Response; 131 | } 132 | -------------------------------------------------------------------------------- /src/UseCase/RelatedDocument/RelatedDocuments.php: -------------------------------------------------------------------------------- 1 | request_factory 32 | ->createRequest( 33 | RequestMethodInterface::METHOD_POST, 34 | $this->base_uri . $this->getLinkRequestUri($relation->parent_type), 35 | ) 36 | ->withHeader('Authorization', $this->authorization_header_value) 37 | ->withHeader('Content-Type', 'application/json') 38 | ->withBody(Utils::streamFor($this->relationToJson($relation))); 39 | 40 | try { 41 | $response = $this->response_factory->createFromJsonResponse( 42 | $this->http_client->sendRequest($request), 43 | ); 44 | } catch (ClientExceptionInterface|\JsonException $e) { 45 | throw new CannotLinkDocumentsException($request, $e->getMessage(), $e->getCode(), $e); 46 | } 47 | 48 | if ($response->isError()) { 49 | throw new CannotLinkDocumentsException($request, $response->data['error_message'] ?? ''); 50 | } 51 | 52 | return $response; 53 | } 54 | 55 | public function unlink(int $relation_id): void 56 | { 57 | $request = $this->request_factory 58 | ->createRequest( 59 | RequestMethodInterface::METHOD_DELETE, 60 | $this->base_uri . '/invoices/deleteRelatedItem/' . $relation_id, 61 | ) 62 | ->withHeader('Authorization', $this->authorization_header_value); 63 | 64 | try { 65 | $response = $this->response_factory 66 | ->createFromJsonResponse($this->http_client->sendRequest($request)); 67 | } catch (ClientExceptionInterface|\JsonException $e) { 68 | throw new CannotUnlinkDocumentsException($request, $e->getMessage(), $e->getCode(), $e); 69 | } 70 | 71 | if ($response->isError()) { 72 | throw new CannotUnlinkDocumentsException($request, $response->data['error_message'] ?? ''); 73 | } 74 | } 75 | 76 | /** 77 | * @throws CannotCreateRequestException 78 | */ 79 | private function relationToJson(Relation $relation): string 80 | { 81 | try { 82 | return json_encode( 83 | [ 84 | 'parent_id' => $relation->parent_id, 85 | 'parent_type' => $relation->parent_type->value, 86 | 'child_id' => $relation->child_id, 87 | 'child_type' => $relation->child_type->value, 88 | ], 89 | JSON_THROW_ON_ERROR, 90 | ); 91 | } catch (\JsonException $e) { 92 | throw new CannotCreateRequestException($e->getMessage(), $e->getCode(), $e); 93 | } 94 | } 95 | 96 | private function getLinkRequestUri(DocumentType $parent_type): string 97 | { 98 | return match ($parent_type) { 99 | DocumentType::INVOICE => '/invoices/addRelatedItem', 100 | DocumentType::EXPENSE => '/expenses/addRelatedItem', 101 | }; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/UseCase/Invoice/InvoicesDownloadTest.php: -------------------------------------------------------------------------------- 1 | [ 34 | 'invoice_id' => 1, 35 | 'language' => Language::SLOVAK, 36 | 'fixture' => __DIR__ . '/../../Response/fixtures/foo.pdf', 37 | ]; 38 | 39 | yield 'download another PDF' => [ 40 | 'invoice_id' => 2, 41 | 'language' => Language::SLOVAK, 42 | 'fixture' => __DIR__ . '/../../Response/fixtures/bar.pdf', 43 | ]; 44 | 45 | yield 'download english PDF version' => [ 46 | 'invoice_id' => 1, 47 | 'language' => Language::ENGLISH, 48 | 'fixture' => __DIR__ . '/../../Response/fixtures/foo.pdf', 49 | ]; 50 | } 51 | 52 | #[DataProvider('downloadPdfProvider')] 53 | public function testDownloadPdf( 54 | int $invoice_id, 55 | Language $language, 56 | string $fixture, 57 | ): void { 58 | $response = $this->getInvoices( 59 | $this->getHttpClientWithMockResponse( 60 | self::getPsrBinaryResponse( 61 | filename: $fixture, 62 | status_code: StatusCodeInterface::STATUS_OK, 63 | headers: ['Content-Type' => 'application/pdf'], 64 | ), 65 | ), 66 | ) 67 | ->downloadPdf($invoice_id, $language); 68 | 69 | $request = $this->getLastRequest(); 70 | 71 | self::assertNotNull($request); 72 | self::assertGetRequest($request); 73 | self::assertAuthorizationHeader($request, self::AUTHORIZATION_HEADER_VALUE); 74 | self::assertSame( 75 | '/' . $language->value . '/invoices/pdf/' . $invoice_id, 76 | $request->getUri()->getPath(), 77 | ); 78 | self::assertStringEqualsFile($fixture, (string) stream_get_contents($response->data)); 79 | } 80 | 81 | public function testDownloadPdfNotFound(): void 82 | { 83 | $this->expectException(InvoiceNotFoundException::class); 84 | 85 | $this->getInvoices( 86 | $this->getHttpClientWithMockResponse( 87 | new Response( 88 | status: StatusCodeInterface::STATUS_NOT_FOUND, 89 | headers: ['Content-Type' => 'application/json'], 90 | ), 91 | ), 92 | ) 93 | ->downloadPdf(1, Language::SLOVAK); 94 | } 95 | 96 | public function testDownloadPdfInternalError(): void 97 | { 98 | $this->expectException(CannotDownloadInvoiceException::class); 99 | 100 | $this->getInvoices( 101 | $this->getHttpClientWithMockResponse( 102 | new Response(StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR), 103 | ), 104 | ) 105 | ->downloadPdf(1, Language::SLOVAK); 106 | } 107 | 108 | public function testDownloadPdfRequestFailed(): void 109 | { 110 | $this->expectException(CannotDownloadInvoiceException::class); 111 | 112 | $this->getInvoices( 113 | $this->getHttpClientWithMockRequestException(), 114 | ) 115 | ->downloadPdf(1, Language::SLOVAK); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/UseCase/CashRegister/ItemsTest.php: -------------------------------------------------------------------------------- 1 | getUseCase($this->getHttpClientWithMockResponse($this->getHttpOkResponse())) 41 | ->create( 42 | self::CASH_REGISTER_ID, 43 | [ 44 | 'cash_register_id' => 321, 45 | 'amount' => 1.25, 46 | ], 47 | ); 48 | 49 | $expected_body = json_encode(['CashRegisterItem' => [ 50 | 'cash_register_id' => self::CASH_REGISTER_ID, 51 | 'amount' => 1.25, 52 | ]], JSON_THROW_ON_ERROR); 53 | 54 | $this->request() 55 | ->post('/cash_register_items/add') 56 | ->withBody($expected_body) 57 | ->withContentTypeJson() 58 | ->withAuthorizationHeader(self::AUTHORIZATION_HEADER_VALUE) 59 | ->assert(); 60 | } 61 | 62 | public function testCreateWithNonExistentCashRegister(): void 63 | { 64 | $this->expectException(CashRegister\CannotCreateCashRegisterItemException::class); 65 | 66 | $this->getUseCase($this->getHttpClientWithMockResponse($this->getNonExistentCashRegisterErrorResponse())) 67 | ->create(self::NON_EXISTENT_CASH_REGISTER, ['amount' => 1.25]); 68 | } 69 | 70 | public function testCreateWithInsufficientPermissions(): void 71 | { 72 | $this->expectException(CashRegister\CannotCreateCashRegisterItemException::class); 73 | 74 | $this->getUseCase($this->getHttpClientWithMockResponse($this->getInsufficientPermissionsResponse())) 75 | ->create(self::CASH_REGISTER_ID, ['amount' => 1.25]); 76 | } 77 | 78 | public function testCreateResponseDecodeFailed(): void 79 | { 80 | $this->expectException(CashRegister\CannotCreateCashRegisterItemException::class); 81 | $this->expectExceptionMessage('Syntax error'); 82 | $this->getUseCase($this->getHttpClientWithMockResponse($this->getHttpOkResponseContainingInvalidJson())) 83 | ->create(0, []); 84 | } 85 | 86 | public function testCreateRequestFailed(): void 87 | { 88 | $this->expectException(CashRegister\CannotCreateCashRegisterItemException::class); 89 | $this->getUseCase($this->getHttpClientWithMockRequestException())->create(0, []); 90 | } 91 | 92 | private function getUseCase(Client $client): CashRegister\Items 93 | { 94 | return new Items( 95 | http_client: $client, 96 | request_factory: new HttpFactory(), 97 | response_factory: new ResponseFactory(), 98 | base_uri: '', 99 | authorization_header_value: self::AUTHORIZATION_HEADER_VALUE, 100 | ); 101 | } 102 | 103 | private function getNonExistentCashRegisterErrorResponse(): ResponseInterface 104 | { 105 | $fixture = __DIR__ . '/fixtures/create-item-with-non-existent-cash-register.json'; 106 | 107 | return new Response(StatusCodeInterface::STATUS_OK, [], 108 | $this->jsonFromFixture($fixture)); 109 | } 110 | 111 | private function getInsufficientPermissionsResponse(): ResponseInterface 112 | { 113 | return new Response(StatusCodeInterface::STATUS_FORBIDDEN, [], 114 | $this->jsonFromFixture(__DIR__ . '/fixtures/create-item-insufficient-permissions.json')); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/UseCase/Stock/Movements.php: -------------------------------------------------------------------------------- 1 | array_map( 35 | static fn ($movement) => array_merge($movement, ['stock_item_id' => $item_id]), 36 | $data, 37 | ), 38 | ]; 39 | 40 | return $this->createAndGetResponse($request_data); 41 | } 42 | 43 | public function createWithSku(string $sku, array $data): Response 44 | { 45 | $request_data = [ 46 | 'StockLog' => array_map( 47 | static fn ($movement) => array_merge($movement, ['sku' => $sku]), 48 | $data, 49 | ), 50 | ]; 51 | 52 | return $this->createAndGetResponse($request_data); 53 | } 54 | 55 | /** 56 | * @param array{StockLog: array[]} $data 57 | */ 58 | private function transformDataToJson(array $data): string 59 | { 60 | try { 61 | return json_encode($data, JSON_THROW_ON_ERROR); 62 | } catch (\JsonException $e) { 63 | throw new CannotCreateRequestException($e->getMessage(), $e->getCode(), $e); 64 | } 65 | } 66 | 67 | /** 68 | * @param array{StockLog: array[]} $data 69 | */ 70 | private function createAndGetResponse(array $data): Response 71 | { 72 | $request = $this->request_factory 73 | ->createRequest(RequestMethodInterface::METHOD_POST, $this->base_uri . '/stock_items/addStockMovement') 74 | ->withBody(Utils::streamFor($this->transformDataToJson($data))) 75 | ->withHeader('Authorization', $this->authorization_header_value) 76 | ->withHeader('Content-Type', 'application/json'); 77 | 78 | try { 79 | $response = $this->response_factory->createFromJsonResponse($this->http_client->sendRequest($request)); 80 | } catch (ClientExceptionInterface|\JsonException $e) { 81 | throw new CannotCreateMovementException($request, $e->getMessage(), $e->getCode(), $e); 82 | } 83 | 84 | if ($response->isError()) { 85 | throw new CannotCreateMovementException($request, $response->data['message'] ?? ''); 86 | } 87 | 88 | return $response; 89 | } 90 | 91 | public function getAll(int $id, MovementsQuery $query = new MovementsQuery()): Response 92 | { 93 | $request = $this->request_factory 94 | ->createRequest( 95 | RequestMethodInterface::METHOD_GET, 96 | $this->base_uri . '/stock_items/movements/' . $id . '/' . $this->getParamsOf($query), 97 | ) 98 | ->withHeader('Authorization', $this->authorization_header_value); 99 | 100 | try { 101 | $response = $this->response_factory->createFromJsonResponse($this->http_client->sendRequest($request)); 102 | } catch (\JsonException|ClientExceptionInterface $e) { 103 | throw new CannotGetAllMovementsException($request, $e->getMessage(), $e->getCode(), $e); 104 | } 105 | 106 | if ($response->isError()) { 107 | throw new CannotGetAllMovementsException($request, $response->data['message'] ?? ''); 108 | } 109 | 110 | return $response; 111 | } 112 | 113 | private function getParamsOf(MovementsQuery $query): string 114 | { 115 | return $this->query_params_convertor->convert([ 116 | 'sort' => $query->sort?->attribute, 117 | 'direction' => $query->sort?->direction->value, 118 | 'page' => $query->page, 119 | 'per_page' => $query->per_page, 120 | ]); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/UseCase/Export/ExportTest.php: -------------------------------------------------------------------------------- 1 | getExports($this->getHttpClientReturning($fixture)) 32 | ->getStatus(1); 33 | 34 | $this->request() 35 | ->get('/exports/getStatus/1') 36 | ->withAuthorizationHeader(self::AUTHORIZATION_HEADER_VALUE) 37 | ->assert(); 38 | 39 | self::assertSame($this->arrayFromFixture($fixture), $response->data); 40 | } 41 | 42 | public function testGetStatusNotFound(): void 43 | { 44 | $this->expectException(ExportNotFoundException::class); 45 | 46 | $this 47 | ->getExports($this->getHttpClientWithMockResponse($this->getHttpNotFoundResponse())) 48 | ->getStatus(1); 49 | } 50 | 51 | public function testGetStatusInsufficientPermissions(): void 52 | { 53 | $this->expectException(CannotGetExportStatusException::class); 54 | 55 | $fixture = __DIR__ . '/../fixtures/unexpected-error.json'; 56 | 57 | $this 58 | ->getExports($this->getHttpClientReturning($fixture, StatusCodeInterface::STATUS_UNAUTHORIZED)) 59 | ->getStatus(1); 60 | } 61 | 62 | public function testGetStatusRequestFailed(): void 63 | { 64 | $this->expectException(CannotGetExportStatusException::class); 65 | 66 | $this 67 | ->getExports($this->getHttpClientWithMockRequestException()) 68 | ->getStatus(1); 69 | } 70 | 71 | public function testGetStatusResponseDecodeFailed(): void 72 | { 73 | $this->expectException(CannotGetExportStatusException::class); 74 | 75 | $this 76 | ->getExports($this->getHttpClientWithMockResponse($this->getHttpOkResponseContainingInvalidJson())) 77 | ->getStatus(1); 78 | } 79 | 80 | public function testDownload(): void 81 | { 82 | $fixture = __DIR__ . '/../../Response/fixtures/foo.pdf'; 83 | $response = $this->getExports( 84 | $this->getHttpClientWithMockResponse( 85 | self::getPsrBinaryResponse( 86 | filename: $fixture, 87 | status_code: StatusCodeInterface::STATUS_OK, 88 | headers: ['Content-Type' => 'application/pdf'], 89 | ), 90 | ), 91 | ) 92 | ->download(1); 93 | 94 | $request = $this->getLastRequest(); 95 | 96 | self::assertNotNull($request); 97 | self::assertGetRequest($request); 98 | self::assertAuthorizationHeader($request, self::AUTHORIZATION_HEADER_VALUE); 99 | self::assertSame('/exports/download_export/1', $request->getUri()->getPath()); 100 | self::assertStringEqualsFile($fixture, (string) stream_get_contents($response->data)); 101 | } 102 | 103 | public function testDownloadNotFound(): void 104 | { 105 | $this->expectException(ExportNotFoundException::class); 106 | 107 | $this 108 | ->getExports($this->getHttpClientWithMockResponse($this->getHttpNotFoundResponse())) 109 | ->download(1); 110 | } 111 | 112 | public function testDownloadInsufficientPermissions(): void 113 | { 114 | $this->expectException(CannotDownloadExportException::class); 115 | 116 | $fixture = __DIR__ . '/../fixtures/unexpected-error.json'; 117 | 118 | $this 119 | ->getExports($this->getHttpClientReturning($fixture, StatusCodeInterface::STATUS_UNAUTHORIZED)) 120 | ->download(1); 121 | } 122 | 123 | public function testDownloadRequestFailed(): void 124 | { 125 | $this->expectException(CannotDownloadExportException::class); 126 | 127 | $this 128 | ->getExports($this->getHttpClientWithMockRequestException()) 129 | ->download(1); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /tests/UseCase/CashRegister/CashRegistersTest.php: -------------------------------------------------------------------------------- 1 | getCashRegisters( 36 | $this->getHttpClientWithMockResponse( 37 | new Response(StatusCodeInterface::STATUS_OK, [], $this->jsonFromFixture($fixture)), 38 | ), 39 | )->getAll(); 40 | 41 | $this->request() 42 | ->get('/cash_registers/getDetails') 43 | ->withAuthorizationHeader(self::AUTHORIZATION_HEADER_VALUE) 44 | ->assert(); 45 | 46 | self::assertSame($this->arrayFromFixture($fixture), $response->data); 47 | } 48 | 49 | public function testGetAllRequestFailed(): void 50 | { 51 | $this->expectException(CannotGetAllCashRegistersException::class); 52 | $this->expectExceptionMessage(self::ERROR_COMMUNICATING_WITH_SERVER_MESSAGE); 53 | $this->getCashRegisters($this->getHttpClientWithMockRequestException())->getAll(); 54 | } 55 | 56 | public function testGetAllResponseDecodeFailed(): void 57 | { 58 | $this->expectException(CannotGetAllCashRegistersException::class); 59 | $this->expectExceptionMessage('Syntax error'); 60 | $this->getCashRegisters($this->getHttpClientWithMockResponse($this->getHttpOkResponseContainingInvalidJson())) 61 | ->getAll(); 62 | } 63 | 64 | public function testGetById(): void 65 | { 66 | $fixture = __DIR__ . '/fixtures/cash-register.json'; 67 | 68 | $response = $this->getCashRegisters($this->getHttpClientReturningOkAndFixture($fixture))->getById(1); 69 | 70 | self::assertSame($this->arrayFromFixture($fixture), $response->data); 71 | 72 | $this->request() 73 | ->get('/cash_registers/view/1') 74 | ->withAuthorizationHeader(self::AUTHORIZATION_HEADER_VALUE) 75 | ->assert(); 76 | } 77 | 78 | public function testGetByIdNotFound(): void 79 | { 80 | $this->expectException(CashRegisterNotFoundException::class); 81 | 82 | $fixture = __DIR__ . '/fixtures/not-found.json'; 83 | 84 | $this->getCashRegisters( 85 | $this->getHttpClientWithMockResponse( 86 | new Response(StatusCodeInterface::STATUS_NOT_FOUND, [], $this->jsonFromFixture($fixture)), 87 | ), 88 | ) 89 | ->getById(123); 90 | } 91 | 92 | public function testGetByIdRequestFailed(): void 93 | { 94 | $this->expectException(CannotGetCashRegisterException::class); 95 | $this->getCashRegisters($this->getHttpClientWithMockRequestException())->getById(0); 96 | } 97 | 98 | public function testGetByIdResponseDecodeFailed(): void 99 | { 100 | $this->expectException(CannotGetCashRegisterException::class); 101 | $this->getCashRegisters($this->getHttpClientWithMockResponse($this->getHttpOkResponseContainingInvalidJson())) 102 | ->getById(0); 103 | } 104 | 105 | private function getHttpClientReturningOkAndFixture(string $fixture): \Psr\Http\Client\ClientInterface 106 | { 107 | return $this->getHttpClientWithMockResponse( 108 | new Response(StatusCodeInterface::STATUS_OK, [], $this->jsonFromFixture($fixture)), 109 | ); 110 | } 111 | 112 | private function getCashRegisters(\Psr\Http\Client\ClientInterface $client): CashRegisters 113 | { 114 | return new CashRegisters( 115 | http_client: $client, 116 | request_factory: new HttpFactory(), 117 | response_factory: new ResponseFactory(), 118 | base_uri: '', 119 | authorization_header_value: self::AUTHORIZATION_HEADER_VALUE, 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /tests/UseCase/Client/fixtures/clients.json: -------------------------------------------------------------------------------- 1 | { 2 | "itemCount": 2, 3 | "pageCount": 1, 4 | "perPage": 100, 5 | "page": 1, 6 | "items": [ 7 | { 8 | "Client": { 9 | "id": "1", 10 | "user_id": "1", 11 | "user_profile_id": "1", 12 | "uuid": null, 13 | "country_id": "191", 14 | "name": "SuperFaktura, s.r.o.", 15 | "ico": "46655034", 16 | "dic": null, 17 | "ic_dph": "SK2023513470", 18 | "iban": "", 19 | "swift": "", 20 | "bank_account_prefix": null, 21 | "bank_account": "", 22 | "bank_code": "", 23 | "account": null, 24 | "email": "", 25 | "address": "Pri Suchom mlyne 6", 26 | "city": "Bratislava - mestská časť Staré Mesto", 27 | "zip": "811 04", 28 | "state": "", 29 | "country": "Slovensko", 30 | "delivery_name": "", 31 | "delivery_address": "", 32 | "delivery_city": "", 33 | "delivery_zip": "", 34 | "delivery_state": "", 35 | "delivery_country": "Slovensko", 36 | "delivery_country_id": "191", 37 | "phone": "", 38 | "delivery_phone": "", 39 | "fax": "", 40 | "due_date": null, 41 | "default_variable": "", 42 | "discount": null, 43 | "currency": null, 44 | "bank_account_id": "0", 45 | "comment": "", 46 | "tags": null, 47 | "distance": null, 48 | "dont_travel": null, 49 | "created": "2023-08-09 13:24:50", 50 | "modified": "2023-08-09 13:24:50", 51 | "notices": true 52 | }, 53 | "ClientStat": { 54 | "id": "1", 55 | "client_id": "1", 56 | "regular_total": "0.00", 57 | "regular_count": "0", 58 | "regular_overdue_total": "0.00", 59 | "regular_overdue_count": "0", 60 | "proforma_total": "0.00", 61 | "proforma_count": "0", 62 | "proforma_overdue_total": "0.00", 63 | "proforma_overdue_count": "0", 64 | "estimate_total": "0.00", 65 | "estimate_count": "0", 66 | "order_total": "0.00", 67 | "order_count": "0", 68 | "expense_total": "0.00", 69 | "expense_count": "0", 70 | "expense_unpaid_total": "0.00", 71 | "expense_unpaid_count": "0", 72 | "delivery_total": "0.00", 73 | "delivery_count": "0", 74 | "cancel_total": "0.00", 75 | "cancel_count": "0", 76 | "reverse_order_total": "0.00", 77 | "reverse_order_count": "0", 78 | "pay_time": "0.00", 79 | "risk": "1", 80 | "date_founded": null 81 | } 82 | }, 83 | { 84 | "Client": { 85 | "id": "2", 86 | "user_id": "1", 87 | "user_profile_id": "1", 88 | "uuid": null, 89 | "country_id": "191", 90 | "name": "SuperFaktura 2, s.r.o.", 91 | "ico": "46655034", 92 | "dic": null, 93 | "ic_dph": "SK2023513470", 94 | "iban": "", 95 | "swift": "", 96 | "bank_account_prefix": null, 97 | "bank_account": "", 98 | "bank_code": "", 99 | "account": null, 100 | "email": "", 101 | "address": "Pri Suchom mlyne 6", 102 | "city": "Bratislava - mestská časť Staré Mesto", 103 | "zip": "811 04", 104 | "state": "", 105 | "country": "Slovensko", 106 | "delivery_name": "", 107 | "delivery_address": "", 108 | "delivery_city": "", 109 | "delivery_zip": "", 110 | "delivery_state": "", 111 | "delivery_country": "Slovensko", 112 | "delivery_country_id": "191", 113 | "phone": "", 114 | "delivery_phone": "", 115 | "fax": "", 116 | "due_date": null, 117 | "default_variable": "", 118 | "discount": null, 119 | "currency": null, 120 | "bank_account_id": "0", 121 | "comment": "", 122 | "tags": null, 123 | "distance": null, 124 | "dont_travel": null, 125 | "created": "2023-08-09 13:24:50", 126 | "modified": "2023-08-09 13:24:50", 127 | "notices": true 128 | }, 129 | "ClientStat": { 130 | "id": "2", 131 | "client_id": "2", 132 | "regular_total": "0.00", 133 | "regular_count": "0", 134 | "regular_overdue_total": "0.00", 135 | "regular_overdue_count": "0", 136 | "proforma_total": "0.00", 137 | "proforma_count": "0", 138 | "proforma_overdue_total": "0.00", 139 | "proforma_overdue_count": "0", 140 | "estimate_total": "0.00", 141 | "estimate_count": "0", 142 | "order_total": "0.00", 143 | "order_count": "0", 144 | "expense_total": "0.00", 145 | "expense_count": "0", 146 | "expense_unpaid_total": "0.00", 147 | "expense_unpaid_count": "0", 148 | "delivery_total": "0.00", 149 | "delivery_count": "0", 150 | "cancel_total": "0.00", 151 | "cancel_count": "0", 152 | "reverse_order_total": "0.00", 153 | "reverse_order_count": "0", 154 | "pay_time": "0.00", 155 | "risk": "1", 156 | "date_founded": null 157 | } 158 | } 159 | ] 160 | } 161 | -------------------------------------------------------------------------------- /src/UseCase/Client/Contact/Contacts.php: -------------------------------------------------------------------------------- 1 | request_factory 39 | ->createRequest( 40 | RequestMethodInterface::METHOD_GET, 41 | $this->base_uri . '/contact_people/getContactPeople/' . $client_id, 42 | ) 43 | ->withHeader('Authorization', $this->authorization_header_value); 44 | 45 | try { 46 | $response = $this->response_factory->createFromJsonResponse( 47 | $this->http_client->sendRequest($request), 48 | ); 49 | } catch (ClientExceptionInterface|\JsonException $e) { 50 | throw new CannotGetAllContactsException($request, $e->getMessage(), $e->getCode(), $e); 51 | } 52 | 53 | if ($response->status_code === StatusCodeInterface::STATUS_NOT_FOUND) { 54 | throw new ClientNotFoundException($request); 55 | } 56 | 57 | return $response; 58 | } 59 | 60 | public function create(int $client_id, array $contact): Response 61 | { 62 | $request = $this->request_factory 63 | ->createRequest( 64 | RequestMethodInterface::METHOD_POST, 65 | $this->base_uri . '/contact_people/add/api%3A1', 66 | ) 67 | ->withHeader('Authorization', $this->authorization_header_value) 68 | ->withHeader('Content-Type', 'application/json') 69 | ->withBody( 70 | Utils::streamFor($this->transformContactDataToJson($client_id, $contact)), 71 | ); 72 | 73 | try { 74 | $response = $this->response_factory->createFromJsonResponse( 75 | $this->http_client->sendRequest($request), 76 | ); 77 | } catch (ClientExceptionInterface|\JsonException $e) { 78 | throw new CannotCreateContactException($request, $e->getMessage(), $e->getCode(), $e); 79 | } 80 | 81 | if ($response->status_code === StatusCodeInterface::STATUS_NOT_FOUND) { 82 | throw new ClientNotFoundException($request); 83 | } 84 | 85 | if ($response->isError()) { 86 | throw new CannotCreateContactException($request, $response->data['message'] ?? ''); 87 | } 88 | 89 | return $response; 90 | } 91 | 92 | public function delete(int $contact_id): void 93 | { 94 | $request = $this->request_factory 95 | ->createRequest( 96 | RequestMethodInterface::METHOD_GET, 97 | $this->base_uri . '/contact_people/delete/' . $contact_id, 98 | ) 99 | ->withHeader('Authorization', $this->authorization_header_value); 100 | 101 | try { 102 | $response = $this->response_factory 103 | ->createFromJsonResponse($this->http_client->sendRequest($request)); 104 | } catch (ClientExceptionInterface|\JsonException $e) { 105 | throw new CannotDeleteContactException($request, $e->getMessage(), $e->getCode(), $e); 106 | } 107 | 108 | if ($response->status_code === StatusCodeInterface::STATUS_NOT_FOUND) { 109 | throw new ContactNotFoundException($request); 110 | } 111 | 112 | if ($response->isError()) { 113 | throw new CannotDeleteContactException($request, $response->data['error_message'] ?? ''); 114 | } 115 | } 116 | 117 | /** 118 | * @param array $contact 119 | * 120 | * @throws CannotCreateRequestException 121 | */ 122 | private function transformContactDataToJson(int $client_id, array $contact): string 123 | { 124 | try { 125 | return json_encode( 126 | [self::CONTACT => ['client_id' => $client_id, ...$contact]], 127 | JSON_THROW_ON_ERROR, 128 | ); 129 | } catch (\JsonException $e) { 130 | throw new CannotCreateRequestException($e->getMessage(), $e->getCode(), $e); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/UseCase/Invoice/Payment/Payments.php: -------------------------------------------------------------------------------- 1 | request_factory 36 | ->createRequest( 37 | RequestMethodInterface::METHOD_GET, 38 | $this->base_uri . '/invoices/will_not_be_paid/' . $invoice_id, 39 | ) 40 | ->withHeader('Authorization', $this->authorization_header_value); 41 | 42 | try { 43 | $response = $this->response_factory 44 | ->createFromJsonResponse($this->http_client->sendRequest($request)); 45 | } catch (ClientExceptionInterface|\JsonException $e) { 46 | throw new CannotMarkAsUnpayableException($request, $e->getMessage(), $e->getCode(), $e); 47 | } 48 | 49 | if ($response->status_code === StatusCodeInterface::STATUS_NOT_FOUND) { 50 | throw new InvoiceNotFoundException($request); 51 | } 52 | 53 | if ($response->isError()) { 54 | throw new CannotMarkAsUnpayableException($request, $response->data['error_message'] ?? ''); 55 | } 56 | } 57 | 58 | public function create(int $id, Payment $payment = new Payment()): Response 59 | { 60 | $request = $this->request_factory 61 | ->createRequest( 62 | RequestMethodInterface::METHOD_POST, 63 | $this->base_uri . '/invoice_payments/add/ajax%3A1/api%3A1', 64 | ) 65 | ->withHeader('Authorization', $this->authorization_header_value) 66 | ->withHeader('Content-Type', 'application/json') 67 | ->withBody(Utils::streamFor($this->transformDataToJson($id, $payment))); 68 | 69 | try { 70 | $response = $this->response_factory->createFromJsonResponse( 71 | $this->http_client->sendRequest($request), 72 | ); 73 | } catch (ClientExceptionInterface|\JsonException $e) { 74 | throw new CannotPayInvoiceException($request, $e->getMessage(), $e->getCode(), $e); 75 | } 76 | 77 | if ($response->isError()) { 78 | throw new CannotPayInvoiceException($request, $response->data['message'] ?? ''); 79 | } 80 | 81 | return $response; 82 | } 83 | 84 | public function delete(int $id): void 85 | { 86 | $request = $this->request_factory 87 | ->createRequest( 88 | RequestMethodInterface::METHOD_DELETE, 89 | $this->base_uri . '/invoice_payments/delete/' . $id, 90 | ) 91 | ->withHeader('Authorization', $this->authorization_header_value); 92 | 93 | try { 94 | $response = $this->response_factory 95 | ->createFromJsonResponse($this->http_client->sendRequest($request)); 96 | } catch (ClientExceptionInterface|\JsonException $e) { 97 | throw new CannotDeleteInvoicePaymentException($request, $e->getMessage(), $e->getCode(), $e); 98 | } 99 | 100 | if ($response->isError()) { 101 | throw new CannotDeleteInvoicePaymentException($request, $response->data['message'] ?? ''); 102 | } 103 | } 104 | 105 | /** 106 | * @throws CannotCreateRequestException 107 | */ 108 | private function transformDataToJson(int $id, Payment $payment): string 109 | { 110 | try { 111 | return json_encode( 112 | [ 113 | self::INVOICE_PAYMENT => array_filter([ 114 | 'invoice_id' => $id, 115 | 'amount' => $payment->amount, 116 | 'currency' => $payment->currency?->value, 117 | 'payment_type' => $payment->payment_type?->value, 118 | 'document_number' => $payment->document_number, 119 | 'cash_register_id' => $payment->cash_register_id, 120 | 'created' => $payment->payment_date?->format('Y-m-d'), 121 | ]), 122 | ], 123 | JSON_THROW_ON_ERROR, 124 | ); 125 | } catch (\JsonException $e) { 126 | throw new CannotCreateRequestException($e->getMessage(), $e->getCode(), $e); 127 | } 128 | } 129 | } 130 | --------------------------------------------------------------------------------