diff --git a/src/Payment/Application/Contract/SystemServiceContract.php b/src/Payment/Application/Contract/SystemServiceContract.php new file mode 100644 index 0000000..23ad869 --- /dev/null +++ b/src/Payment/Application/Contract/SystemServiceContract.php @@ -0,0 +1,12 @@ +addIfNotNull($params, 'filter[status]', $this->filterStatus); + $this->addIfNotNull($params, 'filter[amount]', $this->filterAmount); + $this->addIfNotNull($params, 'filter[externalReferenceId]', $this->filterExternalReferenceId); + $this->addIfNotNull($params, 'filter[orderId]', $this->filterOrderId); + $this->addIfNotNull($params, 'filter[customerName]', $this->filterCustomerName); + $this->addIfNotNull($params, 'filter[customerEmail]', $this->filterCustomerEmail); + $this->addIfNotNull($params, 'filter[blikId]', $this->filterBlikId); + $this->addIfNotNull($params, 'filter[cardBin]', $this->filterCardBin); + $this->addIfNotNull($params, 'filter[provider]', $this->filterProvider); + $this->addIfNotNull($params, 'filter[createdAt]', $this->filterCreatedAt); + $this->addIfNotNull($params, 'filter[paidAt]', $this->filterPaidAt); + $this->addIfNotNull($params, 'query[full]', $this->queryFull); + $this->addIfNotNull($params, 'query[customerName]', $this->queryCustomerName); + $this->addIfNotNull($params, 'query[customerEmail]', $this->queryCustomerEmail); + $this->addIfNotNull($params, 'query[title]', $this->queryTitle); + $this->addIfNotNull($params, 'page[number]', $this->pageNumber !== null ? (string) $this->pageNumber : null); + $this->addIfNotNull($params, 'page[size]', $this->pageSize !== null ? (string) $this->pageSize : null); + + if ([] === $params) { + return ''; + } + + return '?' . http_build_query($params); + } + + /** + * @param array $params + */ + private function addIfNotNull(array &$params, string $key, ?string $value): void + { + if (null !== $value) { + $params[$key] = $value; + } + } +} diff --git a/src/Payment/Application/DTO/ListTransactionsResponse.php b/src/Payment/Application/DTO/ListTransactionsResponse.php new file mode 100644 index 0000000..6e76910 --- /dev/null +++ b/src/Payment/Application/DTO/ListTransactionsResponse.php @@ -0,0 +1,20 @@ + $data + */ + public static function fromArray(array $data): self + { + return new self( + message: $data['message'], + environment: $data['environment'], + tokenId: $data['tokenId'], + clientId: $data['clientId'], + version: $data['version'], + scopes: $data['scopes'] ?? [], + ); + } +} diff --git a/src/Payment/Application/Mapper/ChannelMapper.php b/src/Payment/Application/Mapper/ChannelMapper.php index 4d7934c..03c6e02 100644 --- a/src/Payment/Application/Mapper/ChannelMapper.php +++ b/src/Payment/Application/Mapper/ChannelMapper.php @@ -13,6 +13,9 @@ use Paymentic\Sdk\Payment\Domain\ValueObject\ChannelAuthorization; use Paymentic\Sdk\Payment\Domain\ValueObject\ChannelCommission; use Paymentic\Sdk\Payment\Domain\ValueObject\ChannelImage; +use Paymentic\Sdk\Payment\Domain\ValueObject\ComplianceContent; +use Paymentic\Sdk\Payment\Domain\ValueObject\ComplianceItem; +use Paymentic\Sdk\Payment\Domain\ValueObject\ComplianceLink; final readonly class ChannelMapper { @@ -47,6 +50,8 @@ public static function fromArray(array $data): Channel ), ), paymentType: $data['paymentType'], + aliases: $data['aliases'] ?? null, + compliance: isset($data['compliance']) ? self::mapCompliance($data['compliance']) : null, enablingAt: isset($data['enablingAt']) ? new DateTimeImmutable($data['enablingAt']) : null, disablingAt: isset($data['disablingAt']) ? new DateTimeImmutable($data['disablingAt']) : null, ); @@ -64,4 +69,37 @@ public static function fromArrayCollection(array $data): array $data, ); } + + /** + * @param array> $items + * @return ComplianceItem[] + */ + private static function mapCompliance(array $items): array + { + return array_map(static function (array $item): ComplianceItem { + $content = isset($item['content']) ? new ComplianceContent( + text: $item['content']['text'] ?? null, + html: $item['content']['html'] ?? null, + markdown: $item['content']['markdown'] ?? null, + ) : null; + + $links = array_map( + static fn (array $link): ComplianceLink => new ComplianceLink( + id: $link['id'] ?? null, + label: $link['label'] ?? null, + url: $link['url'] ?? null, + ), + $item['links'] ?? [], + ); + + return new ComplianceItem( + id: $item['id'], + type: $item['type'], + required: $item['required'], + checked: $item['checked'] ?? null, + content: $content, + links: $links, + ); + }, $items); + } } diff --git a/src/Payment/Application/Mapper/TransactionListItemMapper.php b/src/Payment/Application/Mapper/TransactionListItemMapper.php new file mode 100644 index 0000000..a3f7cdb --- /dev/null +++ b/src/Payment/Application/Mapper/TransactionListItemMapper.php @@ -0,0 +1,51 @@ + $data + * @throws Exception + */ + public static function fromArray(array $data): TransactionListItem + { + return new TransactionListItem( + id: $data['id'], + status: TransactionStatus::from($data['status']), + amount: $data['amount'], + title: $data['title'], + commission: $data['commission'] ?? null, + customerName: $data['customerName'] ?? null, + customerEmail: $data['customerEmail'] ?? null, + externalReferenceId: $data['externalReferenceId'] ?? null, + paymentMethod: $data['paymentMethod'] ?? null, + paymentChannel: $data['paymentChannel'] ?? null, + orderId: $data['orderId'] ?? null, + blikId: $data['blikId'] ?? null, + cardBin: $data['cardBin'] ?? null, + paidAt: isset($data['paidAt']) ? new DateTimeImmutable($data['paidAt']) : null, + createdAt: isset($data['createdAt']) ? new DateTimeImmutable($data['createdAt']) : null, + ); + } + + /** + * @param array> $data + * @return TransactionListItem[] + * @throws Exception + */ + public static function fromArrayCollection(array $data): array + { + return array_map( + static fn (array $item): TransactionListItem => self::fromArray($item), + $data, + ); + } +} diff --git a/src/Payment/Application/Service/SystemService.php b/src/Payment/Application/Service/SystemService.php new file mode 100644 index 0000000..319821b --- /dev/null +++ b/src/Payment/Application/Service/SystemService.php @@ -0,0 +1,32 @@ +httpClient->get( + uri: '/payment/ping', + ); + + return PingResponse::fromArray($response['data']); + } +} diff --git a/src/Payment/Application/Service/TransactionService.php b/src/Payment/Application/Service/TransactionService.php index 24945bb..15fad43 100644 --- a/src/Payment/Application/Service/TransactionService.php +++ b/src/Payment/Application/Service/TransactionService.php @@ -9,10 +9,14 @@ use Paymentic\Sdk\Payment\Application\Contract\TransactionServiceContract; use Paymentic\Sdk\Payment\Application\DTO\CreateTransactionRequest; use Paymentic\Sdk\Payment\Application\DTO\CreateTransactionResponse; +use Paymentic\Sdk\Payment\Application\DTO\ListTransactionsRequest; +use Paymentic\Sdk\Payment\Application\DTO\ListTransactionsResponse; +use Paymentic\Sdk\Payment\Application\Mapper\TransactionListItemMapper; use Paymentic\Sdk\Payment\Application\Mapper\TransactionMapper; use Paymentic\Sdk\Payment\Domain\Entity\Transaction; use Paymentic\Sdk\Shared\Exception\PaymenticException; use Paymentic\Sdk\Shared\Http\HttpClient; +use Paymentic\Sdk\Shared\ValueObject\Pagination; final readonly class TransactionService implements TransactionServiceContract { @@ -59,4 +63,23 @@ public function capture(string $pointId, string $transactionId): void uri: "/payment/points/{$pointId}/transactions/{$transactionId}/capture", ); } + + /** + * @throws PaymenticException + * @throws JsonException + * @throws Exception + */ + public function list(string $pointId, ?ListTransactionsRequest $request = null): ListTransactionsResponse + { + $queryString = $request?->toQueryString() ?? ''; + + $response = $this->httpClient->get( + uri: "/payment/points/{$pointId}/transactions{$queryString}", + ); + + return new ListTransactionsResponse( + data: TransactionListItemMapper::fromArrayCollection($response['data']), + pagination: Pagination::fromArray($response['pagination']), + ); + } } diff --git a/src/Payment/Domain/Entity/Channel.php b/src/Payment/Domain/Entity/Channel.php index 0874c06..47bad45 100644 --- a/src/Payment/Domain/Entity/Channel.php +++ b/src/Payment/Domain/Entity/Channel.php @@ -10,11 +10,14 @@ use Paymentic\Sdk\Payment\Domain\ValueObject\ChannelAuthorization; use Paymentic\Sdk\Payment\Domain\ValueObject\ChannelCommission; use Paymentic\Sdk\Payment\Domain\ValueObject\ChannelImage; +use Paymentic\Sdk\Payment\Domain\ValueObject\ComplianceItem; final readonly class Channel { /** * @param string[] $currencies + * @param string[]|null $aliases + * @param ComplianceItem[]|null $compliance */ public function __construct( public string $id, @@ -27,6 +30,8 @@ public function __construct( public ChannelCommission $commission, public ChannelAuthorization $authorization, public string $paymentType, + public ?array $aliases = null, + public ?array $compliance = null, public ?DateTimeInterface $enablingAt = null, public ?DateTimeInterface $disablingAt = null, ) { diff --git a/src/Payment/Domain/Entity/TransactionListItem.php b/src/Payment/Domain/Entity/TransactionListItem.php new file mode 100644 index 0000000..dc8a033 --- /dev/null +++ b/src/Payment/Domain/Entity/TransactionListItem.php @@ -0,0 +1,30 @@ +refundService = new RefundService($httpClient); $this->blikService = new BlikService($httpClient); $this->pointService = new PointService($httpClient); + $this->systemService = new SystemService($httpClient); } public function transactions(): TransactionServiceContract @@ -49,4 +53,9 @@ public function points(): PointServiceContract { return $this->pointService; } + + public function system(): SystemServiceContract + { + return $this->systemService; + } } diff --git a/src/Shared/ValueObject/Pagination.php b/src/Shared/ValueObject/Pagination.php new file mode 100644 index 0000000..fbb74b0 --- /dev/null +++ b/src/Shared/ValueObject/Pagination.php @@ -0,0 +1,35 @@ + $data + */ + public static function fromArray(array $data): self + { + return new self( + page: $data['page'], + pageSize: $data['pageSize'], + total: $data['total'], + totalPages: $data['totalPages'], + from: $data['from'] ?? null, + to: $data['to'] ?? null, + links: isset($data['links']) ? PaginationLinks::fromArray($data['links']) : null, + ); + } +} diff --git a/src/Shared/ValueObject/PaginationLinks.php b/src/Shared/ValueObject/PaginationLinks.php new file mode 100644 index 0000000..a75573b --- /dev/null +++ b/src/Shared/ValueObject/PaginationLinks.php @@ -0,0 +1,29 @@ + $data + */ + public static function fromArray(array $data): self + { + return new self( + first: $data['first'] ?? null, + prev: $data['prev'] ?? null, + next: $data['next'] ?? null, + last: $data['last'] ?? null, + ); + } +} diff --git a/tests/Feature/ListTransactionsFeatureTest.php b/tests/Feature/ListTransactionsFeatureTest.php new file mode 100644 index 0000000..239fe99 --- /dev/null +++ b/tests/Feature/ListTransactionsFeatureTest.php @@ -0,0 +1,249 @@ + [ + [ + 'id' => 'ABCD-123-XYZ-9876', + 'status' => 'PAID', + 'amount' => '123.45', + 'title' => 'Order #12345', + 'commission' => '1.23', + 'customerName' => 'John Doe', + 'customerEmail' => 'john@example.com', + 'externalReferenceId' => 'EXT-REF-123', + 'paymentMethod' => 'BLIK', + 'paymentChannel' => 'blik-psp', + 'orderId' => 'ORD-12345', + 'blikId' => 'BLIK123', + 'cardBin' => null, + 'paidAt' => '2024-01-15T11:55:00+00:00', + 'createdAt' => '2024-01-15T11:50:00+00:00', + ], + [ + 'id' => 'EFGH-456-UVW-3210', + 'status' => 'CREATED', + 'amount' => '50.00', + 'title' => 'Order #67890', + 'commission' => null, + 'customerName' => null, + 'customerEmail' => null, + 'externalReferenceId' => null, + 'paymentMethod' => null, + 'paymentChannel' => null, + 'orderId' => null, + 'blikId' => null, + 'cardBin' => null, + 'paidAt' => null, + 'createdAt' => '2024-01-16T09:00:00+00:00', + ], + ], + 'pagination' => [ + 'page' => 1, + 'pageSize' => 25, + 'total' => 2, + 'totalPages' => 1, + 'from' => 1, + 'to' => 2, + 'links' => [ + 'first' => 'https://api.paymentic.com/v1_2/payment/points/b8e6e2fc/transactions?page[number]=1', + 'prev' => null, + 'next' => null, + 'last' => 'https://api.paymentic.com/v1_2/payment/points/b8e6e2fc/transactions?page[number]=1', + ], + ], + ], JSON_THROW_ON_ERROR); + + $client = $this->createClient($responseBody, 200); + + $response = $client->payment()->transactions()->list('b8e6e2fc'); + + $this->assertCount(2, $response->data); + + $first = $response->data[0]; + $this->assertSame('ABCD-123-XYZ-9876', $first->id); + $this->assertSame(TransactionStatus::PAID, $first->status); + $this->assertSame('123.45', $first->amount); + $this->assertSame('Order #12345', $first->title); + $this->assertSame('1.23', $first->commission); + $this->assertSame('John Doe', $first->customerName); + $this->assertSame('john@example.com', $first->customerEmail); + $this->assertSame('BLIK', $first->paymentMethod); + $this->assertSame('blik-psp', $first->paymentChannel); + $this->assertSame('ORD-12345', $first->orderId); + $this->assertSame('BLIK123', $first->blikId); + $this->assertNotNull($first->paidAt); + $this->assertNotNull($first->createdAt); + + $second = $response->data[1]; + $this->assertSame('EFGH-456-UVW-3210', $second->id); + $this->assertSame(TransactionStatus::CREATED, $second->status); + $this->assertNull($second->customerName); + + $this->assertSame(1, $response->pagination->page); + $this->assertSame(25, $response->pagination->pageSize); + $this->assertSame(2, $response->pagination->total); + $this->assertSame(1, $response->pagination->totalPages); + $this->assertSame(1, $response->pagination->from); + $this->assertSame(2, $response->pagination->to); + $this->assertNotNull($response->pagination->links); + $this->assertNotNull($response->pagination->links->first); + $this->assertNull($response->pagination->links->prev); + $this->assertNull($response->pagination->links->next); + $this->assertNotNull($response->pagination->links->last); + } + + /** + * @throws JsonException + */ + #[Test] + public function listsTransactionsWithFilters(): void + { + $responseBody = json_encode([ + 'data' => [ + [ + 'id' => 'ABCD-123-XYZ-9876', + 'status' => 'PAID', + 'amount' => '123.45', + 'title' => 'Order #12345', + 'paidAt' => '2024-01-15T11:55:00+00:00', + 'createdAt' => '2024-01-15T11:50:00+00:00', + ], + ], + 'pagination' => [ + 'page' => 1, + 'pageSize' => 10, + 'total' => 1, + 'totalPages' => 1, + 'from' => 1, + 'to' => 1, + 'links' => [ + 'first' => null, + 'prev' => null, + 'next' => null, + 'last' => null, + ], + ], + ], JSON_THROW_ON_ERROR); + + $mockHttpClient = new MockHttpClient($responseBody, 200); + + $client = PaymenticClientFactory::create('test-api-key') + ->withSandbox() + ->withHttpClient($mockHttpClient) + ->withRequestFactory(new MockRequestFactory()) + ->withStreamFactory(new MockStreamFactory()) + ->build(); + + $request = new ListTransactionsRequest( + filterStatus: 'PAID', + filterProvider: 'blik', + pageNumber: 1, + pageSize: 10, + ); + + $response = $client->payment()->transactions()->list('b8e6e2fc', $request); + + $this->assertCount(1, $response->data); + $this->assertSame(1, $response->pagination->page); + + $lastRequest = $mockHttpClient->getLastRequest(); + $this->assertNotNull($lastRequest); + $this->assertSame('GET', $lastRequest->getMethod()); + $uri = (string) $lastRequest->getUri(); + $this->assertStringContainsString('/payment/points/b8e6e2fc/transactions', $uri); + $this->assertStringContainsString('filter%5Bstatus%5D=PAID', $uri); + $this->assertStringContainsString('filter%5Bprovider%5D=blik', $uri); + } + + /** + * @throws JsonException + */ + #[Test] + public function listsEmptyTransactions(): void + { + $responseBody = json_encode([ + 'data' => [], + 'pagination' => [ + 'page' => 1, + 'pageSize' => 25, + 'total' => 0, + 'totalPages' => 0, + 'from' => null, + 'to' => null, + 'links' => [ + 'first' => null, + 'prev' => null, + 'next' => null, + 'last' => null, + ], + ], + ], JSON_THROW_ON_ERROR); + + $client = $this->createClient($responseBody, 200); + + $response = $client->payment()->transactions()->list('b8e6e2fc'); + + $this->assertCount(0, $response->data); + $this->assertSame(0, $response->pagination->total); + $this->assertNull($response->pagination->from); + $this->assertNull($response->pagination->to); + } + + /** + * @throws JsonException + */ + #[Test] + public function throwsNotFoundWhenPointNotFound(): void + { + $responseBody = json_encode([ + 'errors' => [ + [ + 'code' => 'POINT_NOT_FOUND', + 'message' => 'Point not found.', + 'docsUrl' => 'https://docs.paymentic.com/errors#POINT_NOT_FOUND', + 'details' => null, + ], + ], + ], JSON_THROW_ON_ERROR); + + $client = $this->createClient($responseBody, 404); + + $this->expectException(NotFoundException::class); + + $client->payment()->transactions()->list('xxxxxxxx'); + } + + private function createClient(string $responseBody, int $statusCode): PaymenticClient + { + return PaymenticClientFactory::create('test-api-key') + ->withSandbox() + ->withHttpClient(new MockHttpClient($responseBody, $statusCode)) + ->withRequestFactory(new MockRequestFactory()) + ->withStreamFactory(new MockStreamFactory()) + ->build(); + } +} diff --git a/tests/Feature/PingFeatureTest.php b/tests/Feature/PingFeatureTest.php new file mode 100644 index 0000000..9f0a4f8 --- /dev/null +++ b/tests/Feature/PingFeatureTest.php @@ -0,0 +1,116 @@ + [ + 'message' => 'pong', + 'environment' => 'sandbox', + 'tokenId' => '2a77157f-7a73-413d-90a1-cd1263533d61', + 'clientId' => '72b631fe', + 'version' => '1.2', + 'scopes' => ['*'], + ], + ], JSON_THROW_ON_ERROR); + + $client = $this->createClient($responseBody, 200); + + $response = $client->payment()->system()->ping(); + + $this->assertSame('pong', $response->message); + $this->assertSame('sandbox', $response->environment); + $this->assertSame('2a77157f-7a73-413d-90a1-cd1263533d61', $response->tokenId); + $this->assertSame('72b631fe', $response->clientId); + $this->assertSame('1.2', $response->version); + $this->assertSame(['*'], $response->scopes); + } + + /** + * @throws JsonException + */ + #[Test] + public function pingsWithMultipleScopes(): void + { + $responseBody = json_encode([ + 'data' => [ + 'message' => 'pong', + 'environment' => 'production', + 'tokenId' => 'token-abc-123', + 'clientId' => 'client-xyz', + 'version' => '1.2', + 'scopes' => ['payments.read', 'payments.write', 'refunds.read'], + ], + ], JSON_THROW_ON_ERROR); + + $client = $this->createClient($responseBody, 200); + + $response = $client->payment()->system()->ping(); + + $this->assertSame('production', $response->environment); + $this->assertCount(3, $response->scopes); + $this->assertSame('payments.read', $response->scopes[0]); + } + + /** + * @throws JsonException + */ + #[Test] + public function pingVerifiesRequestMethod(): void + { + $responseBody = json_encode([ + 'data' => [ + 'message' => 'pong', + 'environment' => 'sandbox', + 'tokenId' => 'token-123', + 'clientId' => 'client-456', + 'version' => '1.2', + 'scopes' => ['*'], + ], + ], JSON_THROW_ON_ERROR); + + $mockHttpClient = new MockHttpClient($responseBody, 200); + + $client = PaymenticClientFactory::create('test-api-key') + ->withSandbox() + ->withHttpClient($mockHttpClient) + ->withRequestFactory(new MockRequestFactory()) + ->withStreamFactory(new MockStreamFactory()) + ->build(); + + $client->payment()->system()->ping(); + + $lastRequest = $mockHttpClient->getLastRequest(); + $this->assertNotNull($lastRequest); + $this->assertSame('GET', $lastRequest->getMethod()); + $this->assertStringContainsString('/payment/ping', (string) $lastRequest->getUri()); + } + + private function createClient(string $responseBody, int $statusCode): PaymenticClient + { + return PaymenticClientFactory::create('test-api-key') + ->withSandbox() + ->withHttpClient(new MockHttpClient($responseBody, $statusCode)) + ->withRequestFactory(new MockRequestFactory()) + ->withStreamFactory(new MockStreamFactory()) + ->build(); + } +} diff --git a/tests/Feature/PointFeatureTest.php b/tests/Feature/PointFeatureTest.php index 354a0ae..62ee0d5 100644 --- a/tests/Feature/PointFeatureTest.php +++ b/tests/Feature/PointFeatureTest.php @@ -204,6 +204,88 @@ public function handlesChannelWithScheduledDates(): void $this->assertSame('2025-12-31', $channel->disablingAt->format('Y-m-d')); } + /** + * @throws JsonException + */ + #[Test] + public function handlesChannelWithAliasesAndCompliance(): void + { + $responseBody = json_encode([ + 'data' => [ + [ + 'id' => 'blik', + 'available' => true, + 'method' => 'BLIK', + 'name' => 'BLIK', + 'image' => [ + 'default' => 'https://example.com/blik.png', + ], + 'amount' => [ + 'minimum' => '0.01', + 'maximum' => '10000.00', + ], + 'currencies' => ['PLN'], + 'commission' => [ + 'value' => '0.90', + 'minimum' => '0.20', + 'fixed' => null, + ], + 'authorization' => [ + 'type' => ['MULTI_FACTOR'], + ], + 'paymentType' => 'INSTANT', + 'aliases' => ['blik-code', 'blik-psp'], + 'compliance' => [ + [ + 'id' => 'info_gdpr', + 'type' => 'DISPLAYABLE', + 'required' => false, + 'checked' => null, + 'content' => [ + 'text' => 'Administratorem Twoich danych jest Paymentic...', + 'html' => 'Administratorem Twoich danych jest Paymentic...', + 'markdown' => 'Administratorem Twoich danych jest **Paymentic**...', + ], + 'links' => [ + [ + 'id' => 'privacy_policy', + 'label' => 'Polityka prywatności', + 'url' => 'https://example.com/pp.pdf', + ], + ], + ], + ], + 'enablingAt' => null, + 'disablingAt' => null, + ], + ], + ], JSON_THROW_ON_ERROR); + + $client = $this->createClient($responseBody, 200); + + $channels = $client->payment()->points()->getChannels('b8e6e2fc'); + + $this->assertCount(1, $channels); + $channel = $channels[0]; + + $this->assertSame(['blik-code', 'blik-psp'], $channel->aliases); + + $this->assertNotNull($channel->compliance); + $this->assertCount(1, $channel->compliance); + + $compliance = $channel->compliance[0]; + $this->assertSame('info_gdpr', $compliance->id); + $this->assertSame('DISPLAYABLE', $compliance->type); + $this->assertFalse($compliance->required); + $this->assertNull($compliance->checked); + $this->assertNotNull($compliance->content); + $this->assertSame('Administratorem Twoich danych jest Paymentic...', $compliance->content->text); + $this->assertCount(1, $compliance->links); + $this->assertSame('privacy_policy', $compliance->links[0]->id); + $this->assertSame('Polityka prywatności', $compliance->links[0]->label); + $this->assertSame('https://example.com/pp.pdf', $compliance->links[0]->url); + } + private function createClient(string $responseBody, int $statusCode): PaymenticClient { return PaymenticClientFactory::create('test-api-key') diff --git a/tests/Unit/Application/DTO/ListTransactionsRequestTest.php b/tests/Unit/Application/DTO/ListTransactionsRequestTest.php new file mode 100644 index 0000000..65c0328 --- /dev/null +++ b/tests/Unit/Application/DTO/ListTransactionsRequestTest.php @@ -0,0 +1,115 @@ +assertSame('', $request->toQueryString()); + } + + #[Test] + public function generatesQueryStringWithStatusFilter(): void + { + $request = new ListTransactionsRequest(filterStatus: 'PAID'); + + $queryString = $request->toQueryString(); + + $this->assertStringContainsString('filter%5Bstatus%5D=PAID', $queryString); + $this->assertStringStartsWith('?', $queryString); + } + + #[Test] + public function generatesQueryStringWithPagination(): void + { + $request = new ListTransactionsRequest(pageNumber: 2, pageSize: 25); + + $queryString = $request->toQueryString(); + + $this->assertStringContainsString('page%5Bnumber%5D=2', $queryString); + $this->assertStringContainsString('page%5Bsize%5D=25', $queryString); + } + + #[Test] + public function generatesQueryStringWithMultipleFilters(): void + { + $request = new ListTransactionsRequest( + filterStatus: 'PAID', + filterCustomerEmail: 'john@example.com', + filterProvider: 'blik', + ); + + $queryString = $request->toQueryString(); + + $this->assertStringContainsString('filter%5Bstatus%5D=PAID', $queryString); + $this->assertStringContainsString('filter%5BcustomerEmail%5D=john%40example.com', $queryString); + $this->assertStringContainsString('filter%5Bprovider%5D=blik', $queryString); + } + + #[Test] + public function generatesQueryStringWithQuerySearch(): void + { + $request = new ListTransactionsRequest( + queryFull: 'John', + queryTitle: 'Order #12345', + ); + + $queryString = $request->toQueryString(); + + $this->assertStringContainsString('query%5Bfull%5D=John', $queryString); + $this->assertStringContainsString('query%5Btitle%5D=Order', $queryString); + } + + #[Test] + public function generatesQueryStringWithDateRangeFilter(): void + { + $request = new ListTransactionsRequest( + filterCreatedAt: '2024-01-01T00:00:00Z,2024-12-31T23:59:59Z', + ); + + $queryString = $request->toQueryString(); + + $this->assertStringContainsString('filter%5BcreatedAt%5D=', $queryString); + } + + #[Test] + public function generatesQueryStringWithAllFilters(): void + { + $request = new ListTransactionsRequest( + filterStatus: 'PAID', + filterAmount: '100.50', + filterExternalReferenceId: 'EXT-REF-123', + filterOrderId: 'ORD-12345', + filterCustomerName: 'John Doe', + filterCustomerEmail: 'john@example.com', + filterBlikId: 'BLIK123', + filterCardBin: '411111', + filterProvider: 'blik', + filterCreatedAt: '2024-01-01T00:00:00Z,2024-12-31T23:59:59Z', + filterPaidAt: '2024-01-01T00:00:00Z,2024-12-31T23:59:59Z', + queryFull: 'John', + queryCustomerName: 'John Doe', + queryCustomerEmail: 'john@example.com', + queryTitle: 'Order #12345', + pageNumber: 1, + pageSize: 25, + ); + + $queryString = $request->toQueryString(); + + $this->assertStringStartsWith('?', $queryString); + $this->assertStringContainsString('filter%5Bstatus%5D=PAID', $queryString); + $this->assertStringContainsString('page%5Bnumber%5D=1', $queryString); + $this->assertStringContainsString('page%5Bsize%5D=25', $queryString); + } +} diff --git a/tests/Unit/Application/DTO/ListTransactionsResponseTest.php b/tests/Unit/Application/DTO/ListTransactionsResponseTest.php new file mode 100644 index 0000000..6f8bf28 --- /dev/null +++ b/tests/Unit/Application/DTO/ListTransactionsResponseTest.php @@ -0,0 +1,65 @@ +assertCount(1, $response->data); + $this->assertSame('ABCD-123-XYZ-9876', $response->data[0]->id); + $this->assertSame(1, $response->pagination->page); + $this->assertSame(25, $response->pagination->pageSize); + $this->assertSame(1, $response->pagination->total); + } + + #[Test] + public function createsWithEmptyData(): void + { + $pagination = new Pagination( + page: 1, + pageSize: 25, + total: 0, + totalPages: 0, + ); + + $response = new ListTransactionsResponse( + data: [], + pagination: $pagination, + ); + + $this->assertCount(0, $response->data); + $this->assertSame(0, $response->pagination->total); + } +} diff --git a/tests/Unit/Application/DTO/PingResponseTest.php b/tests/Unit/Application/DTO/PingResponseTest.php new file mode 100644 index 0000000..c6fdef2 --- /dev/null +++ b/tests/Unit/Application/DTO/PingResponseTest.php @@ -0,0 +1,70 @@ + 'pong', + 'environment' => 'sandbox', + 'tokenId' => '2a77157f-7a73-413d-90a1-cd1263533d61', + 'clientId' => '72b631fe', + 'version' => '1.2', + 'scopes' => ['*'], + ]; + + $response = PingResponse::fromArray($data); + + $this->assertSame('pong', $response->message); + $this->assertSame('sandbox', $response->environment); + $this->assertSame('2a77157f-7a73-413d-90a1-cd1263533d61', $response->tokenId); + $this->assertSame('72b631fe', $response->clientId); + $this->assertSame('1.2', $response->version); + $this->assertSame(['*'], $response->scopes); + } + + #[Test] + public function createsFromArrayWithMultipleScopes(): void + { + $data = [ + 'message' => 'pong', + 'environment' => 'production', + 'tokenId' => 'token-123', + 'clientId' => 'client-456', + 'version' => '1.2', + 'scopes' => ['payments.read', 'payments.write'], + ]; + + $response = PingResponse::fromArray($data); + + $this->assertSame('production', $response->environment); + $this->assertCount(2, $response->scopes); + $this->assertSame('payments.read', $response->scopes[0]); + $this->assertSame('payments.write', $response->scopes[1]); + } + + #[Test] + public function createsFromArrayWithEmptyScopes(): void + { + $data = [ + 'message' => 'pong', + 'environment' => 'sandbox', + 'tokenId' => 'token-123', + 'clientId' => 'client-456', + 'version' => '1.2', + ]; + + $response = PingResponse::fromArray($data); + + $this->assertSame([], $response->scopes); + } +} diff --git a/tests/Unit/Application/Mapper/ChannelMapperTest.php b/tests/Unit/Application/Mapper/ChannelMapperTest.php index fba5da9..3ca8afb 100644 --- a/tests/Unit/Application/Mapper/ChannelMapperTest.php +++ b/tests/Unit/Application/Mapper/ChannelMapperTest.php @@ -200,6 +200,113 @@ public function mapsAllAuthorizationTypes(): void } } + /** + * @throws Exception + */ + #[Test] + public function mapsChannelWithAliases(): void + { + $data = $this->createMinimalChannelData([ + 'aliases' => ['alias-1', 'alias-2'], + ]); + + $channel = ChannelMapper::fromArray($data); + + $this->assertSame(['alias-1', 'alias-2'], $channel->aliases); + } + + /** + * @throws Exception + */ + #[Test] + public function mapsChannelWithNullAliases(): void + { + $data = $this->createMinimalChannelData(); + + $channel = ChannelMapper::fromArray($data); + + $this->assertNull($channel->aliases); + } + + /** + * @throws Exception + */ + #[Test] + public function mapsChannelWithCompliance(): void + { + $data = $this->createMinimalChannelData([ + 'compliance' => [ + [ + 'id' => 'info_gdpr', + 'type' => 'DISPLAYABLE', + 'required' => false, + 'checked' => null, + 'content' => [ + 'text' => 'GDPR info text', + 'html' => 'GDPR info', + 'markdown' => '**GDPR** info', + ], + 'links' => [ + [ + 'id' => 'privacy_policy', + 'label' => 'Privacy Policy', + 'url' => 'https://example.com/pp.pdf', + ], + ], + ], + [ + 'id' => 'consent_terms', + 'type' => 'ACCEPTABLE', + 'required' => true, + 'checked' => false, + 'content' => [ + 'text' => 'I accept terms', + ], + 'links' => [], + ], + ], + ]); + + $channel = ChannelMapper::fromArray($data); + + $this->assertNotNull($channel->compliance); + $this->assertCount(2, $channel->compliance); + + $gdpr = $channel->compliance[0]; + $this->assertSame('info_gdpr', $gdpr->id); + $this->assertSame('DISPLAYABLE', $gdpr->type); + $this->assertFalse($gdpr->required); + $this->assertNull($gdpr->checked); + $this->assertNotNull($gdpr->content); + $this->assertSame('GDPR info text', $gdpr->content->text); + $this->assertSame('GDPR info', $gdpr->content->html); + $this->assertSame('**GDPR** info', $gdpr->content->markdown); + $this->assertCount(1, $gdpr->links); + $this->assertSame('privacy_policy', $gdpr->links[0]->id); + $this->assertSame('Privacy Policy', $gdpr->links[0]->label); + $this->assertSame('https://example.com/pp.pdf', $gdpr->links[0]->url); + + $terms = $channel->compliance[1]; + $this->assertSame('consent_terms', $terms->id); + $this->assertSame('ACCEPTABLE', $terms->type); + $this->assertTrue($terms->required); + $this->assertFalse($terms->checked); + $this->assertCount(0, $terms->links); + } + + /** + * @throws Exception + */ + #[Test] + public function mapsChannelWithNullCompliance(): void + { + $data = $this->createMinimalChannelData(); + + $channel = ChannelMapper::fromArray($data); + + $this->assertNull($channel->compliance); + } + /** * @param array $overrides * @return array diff --git a/tests/Unit/Application/Mapper/TransactionListItemMapperTest.php b/tests/Unit/Application/Mapper/TransactionListItemMapperTest.php new file mode 100644 index 0000000..8d51d48 --- /dev/null +++ b/tests/Unit/Application/Mapper/TransactionListItemMapperTest.php @@ -0,0 +1,164 @@ + 'ABCD-123-XYZ-9876', + 'status' => 'PAID', + 'amount' => '123.45', + 'title' => 'Order #12345', + ]; + + $item = TransactionListItemMapper::fromArray($data); + + $this->assertInstanceOf(TransactionListItem::class, $item); + $this->assertSame('ABCD-123-XYZ-9876', $item->id); + $this->assertSame(TransactionStatus::PAID, $item->status); + $this->assertSame('123.45', $item->amount); + $this->assertSame('Order #12345', $item->title); + $this->assertNull($item->commission); + $this->assertNull($item->customerName); + $this->assertNull($item->customerEmail); + $this->assertNull($item->externalReferenceId); + $this->assertNull($item->paymentMethod); + $this->assertNull($item->paymentChannel); + $this->assertNull($item->orderId); + $this->assertNull($item->blikId); + $this->assertNull($item->cardBin); + $this->assertNull($item->paidAt); + $this->assertNull($item->createdAt); + } + + /** + * @throws Exception + */ + #[Test] + public function mapsTransactionListItemWithAllFields(): void + { + $data = [ + 'id' => 'ABCD-123-XYZ-9876', + 'status' => 'PAID', + 'amount' => '123.45', + 'title' => 'Order #12345', + 'commission' => '1.23', + 'customerName' => 'John Doe', + 'customerEmail' => 'john@example.com', + 'externalReferenceId' => 'EXT-REF-123', + 'paymentMethod' => 'BLIK', + 'paymentChannel' => 'blik-psp', + 'orderId' => 'ORD-12345', + 'blikId' => 'BLIK123', + 'cardBin' => '411111', + 'paidAt' => '2024-01-15T11:55:00+00:00', + 'createdAt' => '2024-01-15T11:50:00+00:00', + ]; + + $item = TransactionListItemMapper::fromArray($data); + + $this->assertSame('ABCD-123-XYZ-9876', $item->id); + $this->assertSame(TransactionStatus::PAID, $item->status); + $this->assertSame('123.45', $item->amount); + $this->assertSame('Order #12345', $item->title); + $this->assertSame('1.23', $item->commission); + $this->assertSame('John Doe', $item->customerName); + $this->assertSame('john@example.com', $item->customerEmail); + $this->assertSame('EXT-REF-123', $item->externalReferenceId); + $this->assertSame('BLIK', $item->paymentMethod); + $this->assertSame('blik-psp', $item->paymentChannel); + $this->assertSame('ORD-12345', $item->orderId); + $this->assertSame('BLIK123', $item->blikId); + $this->assertSame('411111', $item->cardBin); + $this->assertInstanceOf(DateTimeImmutable::class, $item->paidAt); + $this->assertInstanceOf(DateTimeImmutable::class, $item->createdAt); + $this->assertSame('2024-01-15', $item->paidAt->format('Y-m-d')); + $this->assertSame('2024-01-15', $item->createdAt->format('Y-m-d')); + } + + /** + * @throws Exception + */ + #[Test] + public function mapsArrayCollection(): void + { + $data = [ + [ + 'id' => 'TXN1-123-ABC-4567', + 'status' => 'PAID', + 'amount' => '100.00', + 'title' => 'Order #1', + ], + [ + 'id' => 'TXN2-456-DEF-8901', + 'status' => 'CREATED', + 'amount' => '200.00', + 'title' => 'Order #2', + ], + ]; + + $items = TransactionListItemMapper::fromArrayCollection($data); + + $this->assertCount(2, $items); + $this->assertSame('TXN1-123-ABC-4567', $items[0]->id); + $this->assertSame(TransactionStatus::PAID, $items[0]->status); + $this->assertSame('TXN2-456-DEF-8901', $items[1]->id); + $this->assertSame(TransactionStatus::CREATED, $items[1]->status); + } + + /** + * @throws Exception + */ + #[Test] + public function mapsEmptyCollection(): void + { + $items = TransactionListItemMapper::fromArrayCollection([]); + + $this->assertCount(0, $items); + } + + /** + * @throws Exception + */ + #[Test] + public function mapsAllTransactionStatuses(): void + { + $statuses = ['CREATED', 'PENDING', 'PAID', 'FAILED', 'EXPIRED']; + $expectedEnums = [ + TransactionStatus::CREATED, + TransactionStatus::PENDING, + TransactionStatus::PAID, + TransactionStatus::FAILED, + TransactionStatus::EXPIRED, + ]; + + foreach ($statuses as $index => $status) { + $data = [ + 'id' => 'TXN-' . $index, + 'status' => $status, + 'amount' => '100.00', + 'title' => 'Test', + ]; + + $item = TransactionListItemMapper::fromArray($data); + + $this->assertSame($expectedEnums[$index], $item->status); + } + } +} diff --git a/tests/Unit/Payment/PaymentClientTest.php b/tests/Unit/Payment/PaymentClientTest.php index 8bf64ea..ef5afa4 100644 --- a/tests/Unit/Payment/PaymentClientTest.php +++ b/tests/Unit/Payment/PaymentClientTest.php @@ -7,6 +7,7 @@ use Paymentic\Sdk\Payment\Application\Contract\BlikServiceContract; use Paymentic\Sdk\Payment\Application\Contract\PointServiceContract; use Paymentic\Sdk\Payment\Application\Contract\RefundServiceContract; +use Paymentic\Sdk\Payment\Application\Contract\SystemServiceContract; use Paymentic\Sdk\Payment\Application\Contract\TransactionServiceContract; use Paymentic\Sdk\Payment\PaymentClient; use Paymentic\Tests\Support\MockHttpClientFactory; @@ -47,6 +48,14 @@ public function returnsPointService(): void $this->assertInstanceOf(PointServiceContract::class, $client->points()); } + #[Test] + public function returnsSystemService(): void + { + $client = new PaymentClient(MockHttpClientFactory::create()); + + $this->assertInstanceOf(SystemServiceContract::class, $client->system()); + } + #[Test] public function returnsSameServiceInstance(): void { @@ -56,5 +65,6 @@ public function returnsSameServiceInstance(): void $this->assertSame($client->refunds(), $client->refunds()); $this->assertSame($client->blik(), $client->blik()); $this->assertSame($client->points(), $client->points()); + $this->assertSame($client->system(), $client->system()); } }