diff --git a/README.md b/README.md index 49a6751..82ef583 100644 --- a/README.md +++ b/README.md @@ -251,6 +251,27 @@ $apiClient->authenticate($accessToken); $userInfo = $apiClient->user()->get(); ``` +### Получить информацию о необходимых платежах +```php +$apiClient->authenticate($accessToken); + +$userInfo = $apiClient->tax()->get(); +``` + +### Получить информацию о платежах +```php +$apiClient->authenticate($accessToken); + +$userInfo = $apiClient->tax()->payments(); +``` + +### Получить информацию о прошлых платежах +```php +$apiClient->authenticate($accessToken); + +$userInfo = $apiClient->tax()->history(); +``` + ## Использованные ресурсы Статья на Habr: [Автоматизация для самозанятых: как интегрировать налог с IT проектом](https://habr.com/ru/post/436656/) @@ -265,4 +286,4 @@ $userInfo = $apiClient->user()->get(); [Сделать пожертвование автору](https://www.tinkoff.ru/cf/7rZnC7N4bOO) ## License -The MIT License (MIT). Please see [License File](LICENSE) for more information. \ No newline at end of file +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/src/Api/Tax.php b/src/Api/Tax.php new file mode 100644 index 0000000..dab1bef --- /dev/null +++ b/src/Api/Tax.php @@ -0,0 +1,59 @@ + + */ +final class Tax extends BaseHttpApi +{ + /** + * @throws ClientExceptionInterface + */ + public function get(): TaxModel + { + $response = $this->httpGet('/taxes'); + + return $this->hydrator->hydrate($response, TaxModel::class); + } + + /** + * @throws \JsonException + * @throws ClientExceptionInterface + */ + public function history(?string $oktmo = null): HistoryRecords + { + $response = $this->httpPost('/taxes/history', [ + 'oktmo' => $oktmo, + ]); + + return $this->hydrator->hydrate($response, HistoryRecords::class); + } + + /** + * @throws \JsonException + * @throws ClientExceptionInterface + * @throws DomainException + */ + public function payments(?string $oktmo = null, bool $onlyPaid = false): PaymentRecords + { + $response = $this->httpPost('/taxes/payments', [ + 'oktmo' => $oktmo, + 'onlyPaid' => $onlyPaid, + ]); + + if ($response->getStatusCode() >= 400) { + (new ErrorHandler())->handleResponse($response); + } + + return $this->hydrator->hydrate($response, PaymentRecords::class); + } +} diff --git a/src/Api/User.php b/src/Api/User.php index 6031fb5..75d512d 100644 --- a/src/Api/User.php +++ b/src/Api/User.php @@ -4,6 +4,8 @@ namespace Shoman4eg\Nalog\Api; use Psr\Http\Client\ClientExceptionInterface; +use Shoman4eg\Nalog\ErrorHandler; +use Shoman4eg\Nalog\Exception\DomainException; use Shoman4eg\Nalog\Model\User\UserType; /** @@ -13,11 +15,16 @@ final class User extends BaseHttpApi { /** * @throws ClientExceptionInterface + * @throws DomainException */ public function get(): UserType { $response = $this->httpGet('/user'); + if ($response->getStatusCode() >= 400) { + (new ErrorHandler())->handleResponse($response); + } + return $this->hydrator->hydrate($response, UserType::class); } } diff --git a/src/ApiClient.php b/src/ApiClient.php index d7267ff..a2e4a52 100644 --- a/src/ApiClient.php +++ b/src/ApiClient.php @@ -15,7 +15,7 @@ /** * @author Artem Dubinin */ -class ApiClient +final class ApiClient { private RequestBuilder $requestBuilder; private ClientConfigurator $clientConfigurator; @@ -120,7 +120,7 @@ public function createNewAccessTokenByPhone(string $phone, string $challengeToke /** * Authenticate the client with an access token. This should be the full access token object with - * refresh token and expirery timestamps. + * refresh token and expire timestamps. * * ```php * $accessToken = $client->createNewAccessToken('inn', 'password'); @@ -173,6 +173,11 @@ public function paymentType(): Api\PaymentType return new Api\PaymentType($this->getHttpClient(), $this->requestBuilder); } + public function tax(): Api\Tax + { + return new Api\Tax($this->getHttpClient(), $this->requestBuilder); + } + private function getHttpClient(): ClientInterface { return $this->clientConfigurator->createConfiguredClient(); diff --git a/src/Exception/HydrationException.php b/src/Exception/HydrationException.php index be51363..2da54be 100644 --- a/src/Exception/HydrationException.php +++ b/src/Exception/HydrationException.php @@ -1,4 +1,5 @@ */ -class HydrationException extends \RuntimeException implements Exception {} +final class HydrationException extends \RuntimeException implements Exception {} diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index ab07118..18f639e 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -8,4 +8,4 @@ /** * @author Tobias Nyholm */ -class InvalidArgumentException extends \InvalidArgumentException implements Exception {} +final class InvalidArgumentException extends \InvalidArgumentException implements Exception {} diff --git a/src/Http/AuthenticationPlugin.php b/src/Http/AuthenticationPlugin.php index 049a6ce..ce01f99 100644 --- a/src/Http/AuthenticationPlugin.php +++ b/src/Http/AuthenticationPlugin.php @@ -5,6 +5,7 @@ use Http\Client\Common\Plugin; use Http\Promise\Promise; +use Psr\Http\Client\ClientExceptionInterface; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Shoman4eg\Nalog\Util\JSON; @@ -28,6 +29,11 @@ public function __construct(Authenticator $authenticator, string $accessToken) $this->accessToken = JSON::decode($accessToken); } + /** + * @throws \Exception + * @throws \JsonException + * @throws ClientExceptionInterface + */ public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { if ($this->accessToken === [] || $request->hasHeader('Authorization')) { diff --git a/src/Model/PaymentType/PaymentType.php b/src/Model/PaymentType/PaymentType.php index d4d7993..c149653 100644 --- a/src/Model/PaymentType/PaymentType.php +++ b/src/Model/PaymentType/PaymentType.php @@ -26,7 +26,6 @@ private function __construct() {} public static function createFromArray(array $data): self { $model = new self(); - $model->id = $data['id']; $model->type = $data['type']; $model->bankName = $data['bankName']; diff --git a/src/Model/PaymentType/PaymentTypeCollection.php b/src/Model/PaymentType/PaymentTypeCollection.php index 00c7489..8bb2c22 100644 --- a/src/Model/PaymentType/PaymentTypeCollection.php +++ b/src/Model/PaymentType/PaymentTypeCollection.php @@ -13,9 +13,11 @@ */ final class PaymentTypeCollection extends AbstractCollection implements CreatableFromArray { + private function __construct() {} + public static function createFromArray(array $data): self { - $items = array_map(fn (array $item) => PaymentType::createFromArray($item), $data['items']); + $items = array_map(static fn (array $item) => PaymentType::createFromArray($item), $data['items']); $model = new self(); $model->setItems($items); diff --git a/src/Model/Tax/History.php b/src/Model/Tax/History.php new file mode 100644 index 0000000..7e65bf7 --- /dev/null +++ b/src/Model/Tax/History.php @@ -0,0 +1,123 @@ + + */ +final class History implements CreatableFromArray +{ + private int $taxPeriodId; + private float $taxAmount; + private float $bonusAmount; + private float $paidAmount; + private ?float $taxBaseAmount; + private ?\DateTimeImmutable $chargeDate; + private ?\DateTimeImmutable $dueDate; + private string $oktmo; + private string $regionName; + private string $kbk; + private string $taxOrganCode; + private string $type; + private int $krsbTaxChargeId; + private int $receiptCount; + + private function __construct() {} + + /** + * @throws \Exception + */ + public static function createFromArray(array $data): self + { + $model = new self(); + $model->taxPeriodId = $data['taxPeriodId']; + $model->taxAmount = $data['taxAmount']; + $model->bonusAmount = $data['bonusAmount']; + $model->paidAmount = $data['paidAmount']; + $model->taxBaseAmount = $data['taxBaseAmount']; + $model->chargeDate = $data['chargeDate'] ? new \DateTimeImmutable($data['chargeDate']) : null; + $model->dueDate = $data['dueDate'] ? new \DateTimeImmutable($data['dueDate']) : null; + $model->oktmo = $data['oktmo']; + $model->regionName = $data['regionName']; + $model->kbk = $data['kbk']; + $model->taxOrganCode = $data['taxOrganCode']; + $model->type = $data['type']; + $model->krsbTaxChargeId = $data['krsbTaxChargeId']; + $model->receiptCount = $data['receiptCount']; + + return $model; + } + + public function getPaidAmount(): float + { + return $this->paidAmount; + } + + public function getBonusAmount(): float + { + return $this->bonusAmount; + } + + public function getTaxAmount(): float + { + return $this->taxAmount; + } + + public function getTaxPeriodId(): int + { + return $this->taxPeriodId; + } + + public function getOktmo(): string + { + return $this->oktmo; + } + + public function getRegionName(): string + { + return $this->regionName; + } + + public function getKbk(): string + { + return $this->kbk; + } + + public function getTaxOrganCode(): string + { + return $this->taxOrganCode; + } + + public function getKrsbTaxChargeId(): int + { + return $this->krsbTaxChargeId; + } + + public function getReceiptCount(): int + { + return $this->receiptCount; + } + + public function getType(): string + { + return $this->type; + } + + public function getChargeDate(): ?\DateTimeImmutable + { + return $this->chargeDate; + } + + public function getDueDate(): ?\DateTimeImmutable + { + return $this->dueDate; + } + + public function getTaxBaseAmount(): ?float + { + return $this->taxBaseAmount; + } +} diff --git a/src/Model/Tax/HistoryRecords.php b/src/Model/Tax/HistoryRecords.php new file mode 100644 index 0000000..5e8092a --- /dev/null +++ b/src/Model/Tax/HistoryRecords.php @@ -0,0 +1,30 @@ + + * + * @extends AbstractCollection + */ +final class HistoryRecords extends AbstractCollection implements CreatableFromArray +{ + private function __construct() {} + + /** + * @throws \Exception + */ + public static function createFromArray(array $data): self + { + $items = array_map(static fn (array $record) => History::createFromArray($record), $data['records']); + + $model = new self(); + $model->setItems($items); + + return $model; + } +} diff --git a/src/Model/Tax/Payment.php b/src/Model/Tax/Payment.php new file mode 100644 index 0000000..03d3e0e --- /dev/null +++ b/src/Model/Tax/Payment.php @@ -0,0 +1,109 @@ + + */ +final class Payment implements CreatableFromArray +{ + private string $sourceType; + private string $type; + private string $documentIndex; + private float $amount; + private \DateTimeImmutable $operationDate; + private \DateTimeImmutable $dueDate; + private string $oktmo; + private string $kbk; + private string $status; + private int $taxPeriodId; + private string $regionName; + private ?\DateTimeImmutable $krsbAcceptedDate; + + private function __construct() {} + + /** + * @throws \Exception + */ + public static function createFromArray(array $data): self + { + $model = new self(); + $model->sourceType = $data['sourceType']; + $model->type = $data['type']; + $model->documentIndex = $data['documentIndex']; + $model->amount = $data['amount']; + $model->operationDate = new \DateTimeImmutable($data['operationDate']); + $model->dueDate = new \DateTimeImmutable($data['dueDate']); + $model->oktmo = $data['oktmo']; + $model->kbk = $data['kbk']; + $model->status = $data['status']; + $model->taxPeriodId = $data['taxPeriodId']; + $model->regionName = $data['regionName']; + $model->krsbAcceptedDate = $data['krsbAcceptedDate'] ? new \DateTimeImmutable($data['krsbAcceptedDate']) : null; + + return $model; + } + + public function getSourceType(): string + { + return $this->sourceType; + } + + public function getKrsbAcceptedDate(): ?\DateTimeImmutable + { + return $this->krsbAcceptedDate; + } + + public function getType(): string + { + return $this->type; + } + + public function getDocumentIndex(): string + { + return $this->documentIndex; + } + + public function getAmount(): float + { + return $this->amount; + } + + public function getOperationDate(): \DateTimeImmutable + { + return $this->operationDate; + } + + public function getDueDate(): \DateTimeImmutable + { + return $this->dueDate; + } + + public function getOktmo(): string + { + return $this->oktmo; + } + + public function getStatus(): string + { + return $this->status; + } + + public function getKbk(): string + { + return $this->kbk; + } + + public function getTaxPeriodId(): int + { + return $this->taxPeriodId; + } + + public function getRegionName(): string + { + return $this->regionName; + } +} diff --git a/src/Model/Tax/PaymentRecords.php b/src/Model/Tax/PaymentRecords.php new file mode 100644 index 0000000..e7efdeb --- /dev/null +++ b/src/Model/Tax/PaymentRecords.php @@ -0,0 +1,30 @@ + + * + * @extends AbstractCollection + */ +final class PaymentRecords extends AbstractCollection implements CreatableFromArray +{ + private function __construct() {} + + /** + * @throws \Exception + */ + public static function createFromArray(array $data): self + { + $items = array_map(static fn (array $record) => Payment::createFromArray($record), $data['records']); + + $model = new self(); + $model->setItems($items); + + return $model; + } +} diff --git a/src/Model/Tax/Tax.php b/src/Model/Tax/Tax.php new file mode 100644 index 0000000..77a5b69 --- /dev/null +++ b/src/Model/Tax/Tax.php @@ -0,0 +1,110 @@ + + */ +final class Tax implements CreatableFromArray +{ + private float $totalForPayment; + private float $total; + private float $tax; + private float $debt; + private float $overpayment; + private float $penalty; + private float $nominalTax; + private float $nominalOverpayment; + private int $taxPeriodId; + private ?float $lastPaymentAmount; + private ?\DateTimeImmutable $lastPaymentDate; + private array $regions; + + private function __construct() {} + + /** + * @throws \Exception + */ + public static function createFromArray(array $data): self + { + $model = new self(); + + $model->totalForPayment = $data['totalForPayment']; + $model->total = $data['total']; + $model->tax = $data['tax']; + $model->debt = $data['debt']; + $model->overpayment = $data['overpayment']; + $model->penalty = $data['penalty']; + $model->nominalTax = $data['nominalTax']; + $model->nominalOverpayment = $data['nominalOverpayment']; + $model->taxPeriodId = $data['taxPeriodId']; + $model->lastPaymentAmount = $data['lastPaymentAmount']; + $model->lastPaymentDate = $data['lastPaymentDate'] ? new \DateTimeImmutable($data['lastPaymentDate']) : null; + $model->regions = $data['regions']; + + return $model; + } + + public function getTotalForPayment(): float + { + return $this->totalForPayment; + } + + public function getTotal(): float + { + return $this->total; + } + + public function getTax(): float + { + return $this->tax; + } + + public function getDebt(): float + { + return $this->debt; + } + + public function getOverpayment(): float + { + return $this->overpayment; + } + + public function getPenalty(): float + { + return $this->penalty; + } + + public function getNominalTax(): float + { + return $this->nominalTax; + } + + public function getNominalOverpayment(): float + { + return $this->nominalOverpayment; + } + + public function getTaxPeriodId(): int + { + return $this->taxPeriodId; + } + + public function getLastPaymentAmount(): ?float + { + return $this->lastPaymentAmount; + } + + public function getLastPaymentDate(): ?\DateTimeImmutable + { + return $this->lastPaymentDate; + } + + public function getRegions(): array + { + return $this->regions; + } +} diff --git a/src/Model/User/UserType.php b/src/Model/User/UserType.php index 7b0e76a..6c8ec2a 100644 --- a/src/Model/User/UserType.php +++ b/src/Model/User/UserType.php @@ -1,4 +1,5 @@ */ -class UserType implements CreatableFromArray +final class UserType implements CreatableFromArray { private ?string $lastName; private int $id; diff --git a/src/Util/JSON.php b/src/Util/JSON.php index 91a8b62..370338c 100644 --- a/src/Util/JSON.php +++ b/src/Util/JSON.php @@ -6,7 +6,7 @@ /** * @author TLe, Tarmo Leppänen */ -class JSON +final class JSON { /** * Generic JSON encode method with error handling support. diff --git a/src/Util/ModelHydrator.php b/src/Util/ModelHydrator.php index e9ab1ec..3173555 100644 --- a/src/Util/ModelHydrator.php +++ b/src/Util/ModelHydrator.php @@ -1,4 +1,5 @@ [ + [ + 'taxPeriodId' => 202211, + 'taxAmount' => 12.00, + 'bonusAmount' => 12.33, + 'paidAmount' => 44.23, + 'taxBaseAmount' => 12.23, + 'chargeDate' => '2022-11-12', + 'dueDate' => '2022-12-11', + 'oktmo' => '260000', + 'regionName' => 'Калининградская область', + 'kbk' => '', + 'taxOrganCode' => '', + 'type' => '', + 'krsbTaxChargeId' => 0, + 'receiptCount' => 0, + ], + ], + ]; + + $this->appendSuccessJson($data); + + $response = $this->client->tax()->history(); + + foreach ($response as $key => $item) { + $record = $data['records'][$key]; + self::assertSame($record['taxPeriodId'], $item->getTaxPeriodId()); + self::assertSame($record['taxAmount'], $item->getTaxAmount()); + self::assertSame($record['bonusAmount'], $item->getBonusAmount()); + self::assertSame($record['paidAmount'], $item->getPaidAmount()); + self::assertEquals( + $record['chargeDate'] ? new \DateTimeImmutable($record['chargeDate']) : null, + $item->getChargeDate() + ); + self::assertEquals( + $record['dueDate'] ? new \DateTimeImmutable($record['dueDate']) : null, + $item->getDueDate() + ); + self::assertSame($record['oktmo'], $item->getOktmo()); + self::assertSame($record['regionName'], $item->getRegionName()); + self::assertSame($record['kbk'], $item->getKbk()); + self::assertSame($record['taxOrganCode'], $item->getTaxOrganCode()); + self::assertSame($record['type'], $item->getType()); + self::assertSame($record['krsbTaxChargeId'], $item->getKrsbTaxChargeId()); + self::assertSame($record['receiptCount'], $item->getReceiptCount()); + } + } + + public function testPayments(): void + { + $data = [ + 'records' => [ + [ + 'sourceType' => '', + 'type' => '', + 'documentIndex' => '', + 'amount' => 44.23, + 'operationDate' => '2022-11-12', + 'dueDate' => '2022-11-12', + 'oktmo' => '260000', + 'kbk' => '', + 'status' => 'Калининградская область', + 'taxPeriodId' => 202211, + 'regionName' => '', + 'krsbAcceptedDate' => '2022-11-12', + ], + ], + ]; + + $this->appendSuccessJson($data); + + $response = $this->client->tax()->payments(); + + foreach ($response as $key => $item) { + $record = $data['records'][$key]; + self::assertSame($record['type'], $item->getType()); + self::assertSame($record['sourceType'], $item->getSourceType()); + self::assertSame($record['documentIndex'], $item->getDocumentIndex()); + self::assertSame($record['amount'], $item->getAmount()); + self::assertEquals(new \DateTimeImmutable($record['operationDate']), $item->getOperationDate()); + self::assertEquals(new \DateTimeImmutable($record['dueDate']), $item->getDueDate()); + self::assertSame($record['oktmo'], $item->getOktmo()); + self::assertSame($record['kbk'], $item->getKbk()); + self::assertSame($record['regionName'], $item->getRegionName()); + self::assertSame($record['status'], $item->getStatus()); + self::assertSame($record['type'], $item->getType()); + self::assertSame($record['taxPeriodId'], $item->getTaxPeriodId()); + self::assertSame(strtotime($record['krsbAcceptedDate']), $item->getKrsbAcceptedDate()->getTimestamp()); + } + } + + public function testGet(): void + { + $data = [ + 'totalForPayment' => 0, + 'total' => 0, + 'tax' => 0, + 'debt' => 0, + 'overpayment' => 0, + 'penalty' => 0, + 'nominalTax' => 0, + 'nominalOverpayment' => 0, + 'taxPeriodId' => 202305, + 'lastPaymentAmount' => null, + 'lastPaymentDate' => '2023-12-03', + 'regions' => [], + ]; + + $this->appendSuccessJson($data); + + $response = $this->client->tax()->get(); + + self::assertEquals($data['totalForPayment'], $response->getTotalForPayment()); + self::assertEquals($data['total'], $response->getTotal()); + self::assertEquals($data['tax'], $response->getTax()); + self::assertEquals($data['debt'], $response->getDebt()); + self::assertEquals($data['overpayment'], $response->getOverpayment()); + self::assertEquals($data['penalty'], $response->getPenalty()); + self::assertEquals($data['nominalTax'], $response->getNominalTax()); + self::assertEquals($data['nominalOverpayment'], $response->getNominalOverpayment()); + self::assertSame($data['taxPeriodId'], $response->getTaxPeriodId()); + self::assertEquals($data['lastPaymentAmount'], $response->getLastPaymentAmount()); + self::assertEquals( + $data['lastPaymentDate'] ? new \DateTimeImmutable($data['lastPaymentDate']) : null, + $response->getLastPaymentDate() + ); + self::assertSame($data['regions'], $response->getRegions()); + } +} diff --git a/tests/Api/UserTest.php b/tests/Api/UserTest.php index ea073e0..0678996 100644 --- a/tests/Api/UserTest.php +++ b/tests/Api/UserTest.php @@ -42,7 +42,7 @@ public function testGet(): void $response = $this->client->user()->get(); self::assertNull($response->getLastName()); - self::assertSame(1000000, $response->getId()); + self::assertSame($data['id'], $response->getId()); self::assertSame($data['displayName'], $response->getDisplayName()); self::assertSame($data['email'], $response->getEmail()); self::assertSame($data['phone'], $response->getPhone()); @@ -54,6 +54,7 @@ public function testGet(): void strtotime($data['initialRegistrationDate']), $response->getInitialRegistrationDate()->getTimestamp() ); + self::assertInstanceOf(\DateTimeImmutable::class, $response->getFirstReceiptRegisterTime()); self::assertSame( strtotime($data['firstReceiptRegisterTime']), $response->getFirstReceiptRegisterTime()->getTimestamp() diff --git a/tests/ApiTestCase.php b/tests/ApiTestCase.php index d4ed746..3521c62 100644 --- a/tests/ApiTestCase.php +++ b/tests/ApiTestCase.php @@ -1,4 +1,5 @@ mock->append(new Response(200, ['Content-Type' => 'application/json'], JSON::encode($data))); + $this->appendSuccessJsonString(JSON::encode($data)); + } + + protected function appendSuccessJsonString(string $data): void + { + $this->mock->append(new Response(200, ['Content-Type' => 'application/json'], $data)); } }