diff --git a/.github/README.md b/.github/README.md index 906fa2f..654ffb6 100644 --- a/.github/README.md +++ b/.github/README.md @@ -84,6 +84,10 @@ $theConfig->setLanguage($language); Make sure to prepare the necessary dependencies before creating the `\ThePay\ApiClient\TheClient` instance. +In any case of dependencies preparation, you MUST check if your PSR-18 HTTP client, will return real PSR-7 network stream! +Because some API endpoints are not paginated for example: [getAccountStatementGPC](../doc/download-account-transaction-GPC.md), and can contain big amount of data! +If an HTTP client will try load full response to memory, some of your API calls can crash on out of memory error! + ### With dependency injection If you're using automatic dependency injection (as most frameworks do), all dependencies except `TheConfig` @@ -103,7 +107,9 @@ $signatureService = new \ThePay\ApiClient\Service\SignatureService($theConfig); /** @var \Psr\Http\Message\RequestFactoryInterface $requestFactory some PSR-17 implementation */ /** @var \Psr\Http\Message\StreamFactoryInterface $streamFactory some PSR-17 implementation */ // if you install suggested guzzle implementation you can use this: -// $httpClient = new \GuzzleHttp\Client(); +// you MUST use RequestOptions::STREAM with true value! +// https://docs.guzzlephp.org/en/stable/request-options.html#stream +// $httpClient = new \GuzzleHttp\Client([\GuzzleHttp\RequestOptions::STREAM => true]); // $requestFactory = $streamFactory = new \GuzzleHttp\Psr7\HttpFactory(); $apiService = new \ThePay\ApiClient\Service\ApiService( $theConfig, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6d3c1f..8ab8d12 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,11 @@ jobs: name: "php 8.4" runs-on: ubuntu-latest container: "nofutur3/php-tests:8.4" + services: + openAPImock: + image: mockserver/mockserver + ports: + - 1080:1080 steps: - name: Checkout repository uses: actions/checkout@v4 @@ -20,6 +25,9 @@ jobs: - name: Run static analysis run: composer stan + - name: Create mock server from our production OpenAPI + run: curl -X PUT -d "{\"specUrlOrPayload\":\"https://gate.thepay.cz/openapi.yaml\"}" http://openAPImock:1080/mockserver/openapi + - name: Run tests run: composer test @@ -27,6 +35,11 @@ jobs: name: "php 8.3" runs-on: ubuntu-latest container: "nofutur3/php-tests:8.3" + services: + openAPImock: + image: mockserver/mockserver + ports: + - 1080:1080 steps: - name: Checkout repository uses: actions/checkout@v4 @@ -37,6 +50,9 @@ jobs: - name: Run static analysis run: composer stan + - name: Create mock server from our production OpenAPI + run: curl -X PUT -d "{\"specUrlOrPayload\":\"https://gate.thepay.cz/openapi.yaml\"}" http://openAPImock:1080/mockserver/openapi + - name: Run tests run: composer test @@ -44,6 +60,11 @@ jobs: name: "php 8.2 psr/http-message 2.0" runs-on: ubuntu-latest container: "nofutur3/php-tests:8.2" + services: + openAPImock: + image: mockserver/mockserver + ports: + - 1080:1080 steps: - name: Checkout repository uses: actions/checkout@v3 @@ -57,6 +78,9 @@ jobs: - name: Run static analysis run: composer stan + - name: Create mock server from our production OpenAPI + run: curl -X PUT -d "{\"specUrlOrPayload\":\"https://gate.thepay.cz/openapi.yaml\"}" http://openAPImock:1080/mockserver/openapi + - name: Run tests run: composer test @@ -64,6 +88,11 @@ jobs: name: "php 8.2 psr/http-message 1.0" runs-on: ubuntu-latest container: "nofutur3/php-tests:8.2" + services: + openAPImock: + image: mockserver/mockserver + ports: + - 1080:1080 steps: - name: Checkout repository uses: actions/checkout@v3 @@ -74,6 +103,9 @@ jobs: - name: Run static analysis run: composer stan + - name: Create mock server from our production OpenAPI + run: curl -X PUT -d "{\"specUrlOrPayload\":\"https://gate.thepay.cz/openapi.yaml\"}" http://openAPImock:1080/mockserver/openapi + - name: Run tests run: composer test @@ -81,6 +113,11 @@ jobs: name: "php 8.1" runs-on: ubuntu-latest container: "nofutur3/php-tests:8.1" + services: + openAPImock: + image: mockserver/mockserver + ports: + - 1080:1080 steps: - name: Checkout repository uses: actions/checkout@v3 @@ -91,6 +128,9 @@ jobs: - name: Run static analysis run: composer stan + - name: Create mock server from our production OpenAPI + run: curl -X PUT -d "{\"specUrlOrPayload\":\"https://gate.thepay.cz/openapi.yaml\"}" http://openAPImock:1080/mockserver/openapi + - name: Run tests run: composer test @@ -98,6 +138,11 @@ jobs: name: "php 8.0" runs-on: ubuntu-latest container: "nofutur3/php-tests:8.0" + services: + openAPImock: + image: mockserver/mockserver + ports: + - 1080:1080 steps: - name: Checkout repository uses: actions/checkout@v3 @@ -108,6 +153,9 @@ jobs: - name: Run static analysis run: composer stan + - name: Create mock server from our production OpenAPI + run: curl -X PUT -d "{\"specUrlOrPayload\":\"https://gate.thepay.cz/openapi.yaml\"}" http://openAPImock:1080/mockserver/openapi + - name: Run tests run: composer test @@ -115,6 +163,11 @@ jobs: name: "php 7.4" runs-on: ubuntu-latest container: "nofutur3/php-tests:7.4" + services: + openAPImock: + image: mockserver/mockserver + ports: + - 1080:1080 steps: # Deprecation example for future support removal. # @@ -130,6 +183,9 @@ jobs: - name: Run static analysis run: composer stan + - name: Create mock server from our production OpenAPI + run: curl -X PUT -d "{\"specUrlOrPayload\":\"https://gate.thepay.cz/openapi.yaml\"}" http://openAPImock:1080/mockserver/openapi + - name: Run tests run: composer test diff --git a/doc/download-account-transaction-GPC.md b/doc/download-account-transaction-GPC.md new file mode 100644 index 0000000..c4a31b0 --- /dev/null +++ b/doc/download-account-transaction-GPC.md @@ -0,0 +1,30 @@ +# Download account transaction GPC statement + +Use the `getAccountStatementGPC` method to download the transaction GPC statement for a specific account within a given date range. + +## Example: Download statement to local file on server + +```php + +/** @var \ThePay\ApiClient\TheClient $thePayClient */ + +$from = new DateTime('2021-03-01'); +$to = new DateTime('2021-03-31'); +$filter = new \ThePay\ApiClient\Filter\TransactionFilter('TP3211114680489551165349', $from, $to); + +$psrResponseBodyStream = $thePayClient->getAccountStatementGPC($filter); +$phpResponseBodyStream = $psrResponseBodyStream->detach() ?? new \RuntimeException('PHP stream missing'); + +$file = fopen('some_path.gpc', 'w'); +stream_copy_to_stream($phpResponseBodyStream, $file); +fclose($file); + +``` + +**Parameters:** +- `$filter` - An instance of `\ThePay\ApiClient\Filter\TransactionFilter()`. (See online API documentation for all available filter options.) + +**Returns:** + +A `Psr\Http\Message\StreamInterface` object containing: +- Binary stream with GPC transaction statement in windows-1250 encoding diff --git a/doc/index.md b/doc/index.md index d0aa010..255b081 100644 --- a/doc/index.md +++ b/doc/index.md @@ -16,6 +16,7 @@ | getPaymentRefund | https://thepay.docs.apiary.io/#reference/0/project-level-resources/payment-refund-info | | createPaymentRefund | https://thepay.docs.apiary.io/#reference/0/project-level-resources/payment-refund-request | | getAccountTransactionHistory | https://thepay.docs.apiary.io/#reference/0/merchant-level-resources/get-account-transaction-history | +| getAccountStatementGPC | https://docs.thepay.cz/#tag/Transactions/paths/~1v1~1transactions~1%7Baccount_iban%7D~1account_statement~1gpc/get | | realizeRegularSubscriptionPayment | https://thepay.docs.apiary.io/#reference/0/project-level-resources/realize-regular-subscription-payment | | realizeIrregularSubscriptionPayment | https://thepay.docs.apiary.io/#reference/0/project-level-resources/realize-irregular-subscription-payment | | realizeUsageBasedSubscriptionPayment | https://thepay.docs.apiary.io/#reference/0/project-level-resources/realize-usage-based-subscription-payment | @@ -42,6 +43,8 @@ [Get account transaction history](get-transactions-history.md) +[Download account transaction GPC statement](download-account-transaction-GPC.md) + [Creating subscription](subscription.md) [Saving authorization](saving-authorization.md) diff --git a/src/Service/ApiService.php b/src/Service/ApiService.php index f1a1aa9..c79855c 100644 --- a/src/Service/ApiService.php +++ b/src/Service/ApiService.php @@ -6,6 +6,7 @@ use Psr\Http\Message\RequestFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamFactoryInterface; +use Psr\Http\Message\StreamInterface; use ThePay\ApiClient\Exception\ApiException; use ThePay\ApiClient\Filter\PaymentsFilter; use ThePay\ApiClient\Filter\TransactionFilter; @@ -32,6 +33,7 @@ use ThePay\ApiClient\TheConfig; use ThePay\ApiClient\Utils\Json; use ThePay\ApiClient\ValueObject\Amount; +use ThePay\ApiClient\ValueObject\GPCPaymentIdentifier; use ThePay\ApiClient\ValueObject\Identifier; use ThePay\ApiClient\ValueObject\LanguageCode; use ThePay\ApiClient\ValueObject\StringValue; @@ -192,6 +194,29 @@ public function getAccountTransactionHistory(TransactionFilter $filter, int $pag return new TransactionCollection(Json::decode($response->getBody()->getContents(), true), $page, $limit, (int) $response->getHeaderLine('X-Total-Count')); } + public function getAccountStatementGPC(TransactionFilter $filter, ?GPCPaymentIdentifier $paymentIdentifier = null): StreamInterface + { + $arguments = [ + 'date_from' => $filter->getDateFrom()->format('Y-m-d'), + 'date_to' => $filter->getDateTo()->format('Y-m-d'), + ]; + if ($filter->getCurrencyCode() !== null) { + $arguments['currency_code'] = $filter->getCurrencyCode()->getValue(); + } + if ($paymentIdentifier !== null) { + $arguments['payment_identifier'] = $paymentIdentifier->getValue(); + } + + $url = $this->url(['transactions', $filter->getAccountIban(), 'account_statement', 'gpc'], $arguments, false); + + $response = $this->sendRequest(self::METHOD_GET, $url); + if ($response->getStatusCode() !== 200) { + throw $this->buildException($url, $response); + } + + return $response->getBody(); + } + /** * Get complete information about the specified payment. * diff --git a/src/Service/ApiServiceInterface.php b/src/Service/ApiServiceInterface.php index d2032b5..7ee8aef 100644 --- a/src/Service/ApiServiceInterface.php +++ b/src/Service/ApiServiceInterface.php @@ -2,6 +2,7 @@ namespace ThePay\ApiClient\Service; +use Psr\Http\Message\StreamInterface; use ThePay\ApiClient\Exception\ApiException; use ThePay\ApiClient\Filter\PaymentsFilter; use ThePay\ApiClient\Filter\TransactionFilter; @@ -23,6 +24,7 @@ use ThePay\ApiClient\Model\RealizeUsageBasedSubscriptionPaymentParams; use ThePay\ApiClient\Model\RecurringPaymentResult; use ThePay\ApiClient\ValueObject\Amount; +use ThePay\ApiClient\ValueObject\GPCPaymentIdentifier; use ThePay\ApiClient\ValueObject\Identifier; use ThePay\ApiClient\ValueObject\LanguageCode; use ThePay\ApiClient\ValueObject\StringValue; @@ -112,6 +114,8 @@ public function getAccountsBalances(?StringValue $accountIban = null, $projectId */ public function getAccountTransactionHistory(TransactionFilter $filter, int $page = 1, int $limit = 100): TransactionCollection; + public function getAccountStatementGPC(TransactionFilter $filter, ?GPCPaymentIdentifier $paymentIdentifier = null): StreamInterface; + /** * @param non-empty-string|null $methodCode * diff --git a/src/TheClient.php b/src/TheClient.php index 112fbb0..1b0fc10 100644 --- a/src/TheClient.php +++ b/src/TheClient.php @@ -4,6 +4,7 @@ use Exception; use InvalidArgumentException; +use Psr\Http\Message\StreamInterface; use ThePay\ApiClient\Exception\ApiException; use ThePay\ApiClient\Filter\PaymentMethodFilter; use ThePay\ApiClient\Filter\PaymentsFilter; @@ -29,6 +30,7 @@ use ThePay\ApiClient\Service\GateService; use ThePay\ApiClient\Service\GateServiceInterface; use ThePay\ApiClient\ValueObject\Amount; +use ThePay\ApiClient\ValueObject\GPCPaymentIdentifier; use ThePay\ApiClient\ValueObject\Identifier; use ThePay\ApiClient\ValueObject\LanguageCode; use ThePay\ApiClient\ValueObject\StringValue; @@ -99,6 +101,20 @@ public function getAccountTransactionHistory(TransactionFilter $filter, $page = ->getAccountTransactionHistory($filter, $page, $limit); } + /** + * @see https://docs.thepay.cz/#tag/Transactions/paths/~1v1~1transactions~1%7Baccount_iban%7D~1account_statement~1gpc/get + * @see ../doc/download-account-transaction-GPC.md + * + * @return StreamInterface MUST return real PSR-7 network stream, make sure you have your PSR-18 HTTP client correctly configured, GPC format is not paginated! + * @return StreamInterface content is windows-1250 encoded + */ + public function getAccountStatementGPC(TransactionFilter $filter, ?GPCPaymentIdentifier $paymentIdentifier = null): StreamInterface + { + return $this + ->api + ->getAccountStatementGPC($filter, $paymentIdentifier); + } + /** * @param PaymentMethodFilter|null $filter * @param LanguageCode|null $languageCode language for payment method titles, null value language from TheConfig used diff --git a/src/TheConfig.php b/src/TheConfig.php index 85b8805..ceb060a 100644 --- a/src/TheConfig.php +++ b/src/TheConfig.php @@ -68,6 +68,11 @@ public function getApiUrl(?string $specificVersion = null): string return $this->apiUrl->getValue() . $this->apiVersion . '/'; } + public function getApiVersion(): string + { + return $this->apiVersion; + } + /** * @return string */ diff --git a/src/ValueObject/GPCPaymentIdentifier.php b/src/ValueObject/GPCPaymentIdentifier.php new file mode 100644 index 0000000..abbcdf6 --- /dev/null +++ b/src/ValueObject/GPCPaymentIdentifier.php @@ -0,0 +1,24 @@ + + */ + public static function cases(): array + { + return [ + self::ORDER_NUMBER, + self::UID, + self::ID, + ]; + } +} diff --git a/tests/BaseTestCase.php b/tests/BaseTestCase.php index 085888e..3f313b6 100644 --- a/tests/BaseTestCase.php +++ b/tests/BaseTestCase.php @@ -26,17 +26,32 @@ protected function setUp(): void } /** - * method return TheClient witch use apiary mock server + * method return TheClient witch use OpenAPI mock server */ - protected function getApiaryClient(): TheClient + protected function getOpenAPIMockClient(): TheClient { - $config = new TheConfig( - '6cdf1b24', - 1212, - 'password', - 'https://private-aa6aa3-thepay.apiary-mock.com/', - 'https://private-ddc40-gatezalozeniplatby.apiary-mock.com/' - ); + $config = new class () extends TheConfig { + public string $apiUrl = 'http://openAPImock:1080/'; + + public function __construct() + { + parent::__construct( + 'a471eab0-4054-11ef-ac09-116afd5362fb', + 1212, + 'password', + 'https://secure-url', + 'https://private-ddc40-gatezalozeniplatby.apiary-mock.com/', + ); + } + + public function getApiUrl(?string $specificVersion = null): string + { + if ($specificVersion !== null) { + return $this->apiUrl . $specificVersion . '/'; + } + return $this->apiUrl . $this->getApiVersion() . '/'; + } + }; $httpFactory = new HttpFactory(); diff --git a/tests/PaymentMethodsTest.php b/tests/PaymentMethodsTest.php index d73d3bc..34b3933 100644 --- a/tests/PaymentMethodsTest.php +++ b/tests/PaymentMethodsTest.php @@ -195,7 +195,7 @@ protected function setUp(): void public function testGettingActivePaymentMethods(): void { - $client = $this->getApiaryClient(); + $client = $this->getOpenAPIMockClient(); $methods = $client->getActivePaymentMethods(); $cardMethod = $methods->get('card'); diff --git a/tests/TheClientTest.php b/tests/TheClientTest.php index 9ca1efc..d62844a 100644 --- a/tests/TheClientTest.php +++ b/tests/TheClientTest.php @@ -2,11 +2,12 @@ namespace ThePay\ApiClient\Tests; +use ThePay\ApiClient\Filter\TransactionFilter; use ThePay\ApiClient\Model\Collection\PaymentMethodCollection; use ThePay\ApiClient\Service\ApiServiceInterface; use ThePay\ApiClient\TheClient; -class TheClientTest extends BaseTestCase +final class TheClientTest extends BaseTestCase { public function testPaymentMethods(): void { @@ -52,6 +53,24 @@ public function testPaymentMethods(): void static::assertSame('CZK', $methods[1]->getAvailableCurrencies()[0]); } + public function testGetAccountStatementGPC(): void + { + $thePayClient = $this->getOpenAPIMockClient(); + + $filter = new TransactionFilter( + 'TP7811112150822790787055', + new \DateTime('2024-01-01'), + new \DateTime('2024-01-31'), + ); + + $stream = $thePayClient->getAccountStatementGPC($filter); + + $content = $stream->getContents(); + + self::assertIsString($content); + self::assertGreaterThan(0, strlen($content)); + } + public function testRenderPaymentMethods(): void { self::markTestSkipped();