diff --git a/.github/workflows/run-workflow.yaml b/.github/workflows/run-workflow.yaml index 94b2bf7..88f1a30 100644 --- a/.github/workflows/run-workflow.yaml +++ b/.github/workflows/run-workflow.yaml @@ -14,7 +14,7 @@ jobs: continue-on-error: ${{ matrix.experimental }} strategy: matrix: - php-version: ['7.3', '7.4', '8.0', '8.1', '8.2'] + php-version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] experimental: [false] steps: - uses: actions/checkout@v3 @@ -34,7 +34,7 @@ jobs: uses: php-actions/phpstan@v3 with: php_version: ${{ matrix.php-version }} - version: 1.9.14 + version: latest configuration: phpstan.neon memory_limit: 256M diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e3ac96..ee01a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.7.3] - 2024-07-29 +### Added +- Support the new VAT in Finland + +## [2.7.2] - 2024-06-06 +### Changed +- Updated validation to support negative item rows + +## [2.7.1] - 2024-04-16 +### Added +- Run tests on PHP 8.3 +### Fixed +- Fix access to possibly undefined property +- Validate too long item descriptions on client side + +## [2.7.0] - 2024-03-21 +### Added +- Added customProviders to createPayment() response + +## [2.6.0] - 2023-10-12 +### Added +- Add pay and add card endpoint +### Fixed +- Fix payment provider unit test + ## [2.5.2] - 2023-05-05 ### Fixed - Improved refundRequest Validation diff --git a/README.md b/README.md index 816d2a8..63e12bf 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ List of `Client::class` methods | requestSettlements() | Request settlements | | requestPaymentReport() | Request payment report | | requestPaymentReportBySettlement() | Request payment report by settlement ID | +| createPaymentAndAddCard() | Create payment and save card details | --- diff --git a/composer.json b/composer.json index a07cee6..69c7df6 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,8 @@ "require": { "php": ">=7.3", "ext-json": "*", - "ext-curl": "*" + "ext-curl": "*", + "ext-mbstring": "*" }, "require-dev": { "phpunit/phpunit": "^10.0 || ^9.0", diff --git a/src/Client.php b/src/Client.php index 9a43845..512ab7b 100644 --- a/src/Client.php +++ b/src/Client.php @@ -38,6 +38,7 @@ use Paytrail\SDK\Exception\ValidationException; use Paytrail\SDK\Exception\RequestException; use Paytrail\SDK\Exception\ClientException; +use Paytrail\SDK\Response\AddCardPaymentResponse; /** * Class Client @@ -218,7 +219,8 @@ function ($decoded) { ->setTerms($decoded->terms ?? null) ->setGroups($decoded->groups ?? []) ->setReference($decoded->reference ?? null) - ->setProviders($decoded->providers ?? []); + ->setProviders($decoded->providers ?? []) + ->setCustomProviders((array)($decoded->customProviders ?? [])); } ); @@ -233,6 +235,7 @@ function ($decoded) { * @return PaymentResponse * @throws HmacException Thrown if HMAC calculation fails for responses. * @throws ValidationException Thrown if payment validation fails. + * @throws ClientException Thrown if API call fails. */ public function createShopInShopPayment(ShopInShopPaymentRequest $payment): PaymentResponse { @@ -262,6 +265,23 @@ function ($decoded) { return $paymentResponse; } + public function createPaymentAndAddCard(PaymentRequest $paymentRequest): AddCardPaymentResponse + { + $this->validateRequestItem($paymentRequest); + + $uri = '/tokenization/pay-and-add-card'; + + return $this->post( + $uri, + $paymentRequest, + function ($decoded) { + return (new AddCardPaymentResponse()) + ->setTransactionId($decoded->transactionId ?? null) + ->setRedirectUrl($decoded->redirectUrl ?? null); + } + ); + } + /** * Create a payment status request. * diff --git a/src/Interfaces/ItemInterface.php b/src/Interfaces/ItemInterface.php index 25b3a61..1f00fa2 100644 --- a/src/Interfaces/ItemInterface.php +++ b/src/Interfaces/ItemInterface.php @@ -59,17 +59,17 @@ public function setUnits(?int $units): ItemInterface; /** * Get the VAT percentage. * - * @return int + * @return float */ - public function getVatPercentage(): ?int; + public function getVatPercentage(): ?float; /** * Set the VAT percentage. * - * @param int|null $vatPercentage + * @param float|null $vatPercentage * @return ItemInterface Return self to enable chaining. */ - public function setVatPercentage(?int $vatPercentage): ItemInterface; + public function setVatPercentage(?float $vatPercentage): ItemInterface; /** * Get the product code. diff --git a/src/Model/Item.php b/src/Model/Item.php index 3847dca..a221f9e 100644 --- a/src/Model/Item.php +++ b/src/Model/Item.php @@ -43,7 +43,7 @@ class Item implements \JsonSerializable, ItemInterface /** * The VAT percentage. * - * @var integer + * @var float */ protected $vatPercentage; @@ -159,9 +159,9 @@ public function setUnits(?int $units): ItemInterface /** * Get the VAT percentage. * - * @return int + * @return float */ - public function getVatPercentage(): ?int + public function getVatPercentage(): ?float { return $this->vatPercentage; } @@ -169,10 +169,10 @@ public function getVatPercentage(): ?int /** * Set the VAT percentage. * - * @param int $vatPercentage + * @param float $vatPercentage * @return ItemInterface Return self to enable chaining. */ - public function setVatPercentage(?int $vatPercentage): ItemInterface + public function setVatPercentage(?float $vatPercentage): ItemInterface { $this->vatPercentage = $vatPercentage; @@ -379,9 +379,6 @@ public function validate() if ($props['unitPrice'] === null) { throw new ValidationException('Item unitPrice is empty'); } - if ($props['unitPrice'] < 0) { - throw new ValidationException('Items unitPrice can\'t be a negative number'); - } if ($props['unitPrice'] > 99999999) { throw new ValidationException('Items unitPrice can\'t be over 99999999'); } @@ -397,6 +394,9 @@ public function validate() if (empty($props['productCode'])) { throw new ValidationException('productCode is empty'); } + if ($props['description'] !== null && mb_strlen($props['description']) > 1000) { + throw new ValidationException('description does not meet maximum length of 1000'); + } return true; } @@ -414,6 +414,10 @@ public function validateShopInShop() throw new ValidationException('merchant is empty'); } + if ($props['unitPrice'] < 0) { + throw new ValidationException('Shop-in-shop item unitPrice can\'t be a negative number'); + } + return true; } } diff --git a/src/Request/ReportBySettlementRequest.php b/src/Request/ReportBySettlementRequest.php index 3a6645e..702705b 100644 --- a/src/Request/ReportBySettlementRequest.php +++ b/src/Request/ReportBySettlementRequest.php @@ -11,7 +11,7 @@ class ReportBySettlementRequest implements \JsonSerializable protected $requestType; protected $callbackUrl; protected $reportFields; - protected $subMerchant; + protected $submerchant; public function validate() { @@ -74,12 +74,12 @@ public function setReportFields(array $reportFields): self /** * Set submerchant. * - * @param int $subMerchant + * @param int $submerchant * @return $this */ - public function setSubMerchant(int $subMerchant): self + public function setSubMerchant(int $submerchant): self { - $this->subMerchant = $subMerchant; + $this->submerchant = $submerchant; return $this; } diff --git a/src/Request/ReportRequest.php b/src/Request/ReportRequest.php index 0a7c216..5cb9f48 100644 --- a/src/Request/ReportRequest.php +++ b/src/Request/ReportRequest.php @@ -22,7 +22,7 @@ class ReportRequest implements \JsonSerializable private $endDate; private $limit; private $reportFields; - private $subMerchant; + private $submerchant; public function validate() { @@ -164,12 +164,12 @@ public function setReportFields(array $reportFields): self /** * Set submerchant. * - * @param int $subMerchant + * @param int $submerchant * @return $this */ - public function setSubMerchant(int $subMerchant): self + public function setSubMerchant(int $submerchant): self { - $this->subMerchant = $subMerchant; + $this->submerchant = $submerchant; return $this; } diff --git a/src/Request/SettlementRequest.php b/src/Request/SettlementRequest.php index d9179ac..166883a 100644 --- a/src/Request/SettlementRequest.php +++ b/src/Request/SettlementRequest.php @@ -15,7 +15,7 @@ class SettlementRequest implements \JsonSerializable protected $endDate; protected $reference; protected $limit; - protected $subMerchant; + protected $submerchant; public function validate() { @@ -102,12 +102,12 @@ public function setLimit(int $limit): self /** * Set submerchant. * - * @param int $subMerchant + * @param int $submerchant * @return $this */ - public function setSubMerchant(int $subMerchant): self + public function setSubMerchant(int $submerchant): self { - $this->subMerchant = $subMerchant; + $this->submerchant = $submerchant; return $this; } diff --git a/src/Response/AddCardPaymentResponse.php b/src/Response/AddCardPaymentResponse.php new file mode 100644 index 0000000..c058b0f --- /dev/null +++ b/src/Response/AddCardPaymentResponse.php @@ -0,0 +1,79 @@ +transactionId; + } + + /** + * Set the transaction id. + * + * @param string|null $transactionId + * + * @return self Return self to enable chaining. + */ + public function setTransactionId(?string $transactionId): AddCardPaymentResponse + { + $this->transactionId = $transactionId; + + return $this; + } + + /** + * Get the redirectUrl. + * + * @return string|null + */ + public function getRedirectUrl(): ?string + { + return $this->redirectUrl; + } + + /** + * Set the redirectUrl. + * + * @param string|null $redirectUrl + * + * @return self Return self to enable chaining. + */ + public function setRedirectUrl(?string $redirectUrl): AddCardPaymentResponse + { + $this->redirectUrl = $redirectUrl; + + return $this; + } +} diff --git a/src/Response/PaymentResponse.php b/src/Response/PaymentResponse.php index 5ec4671..181fc8c 100644 --- a/src/Response/PaymentResponse.php +++ b/src/Response/PaymentResponse.php @@ -59,6 +59,13 @@ class PaymentResponse implements ResponseInterface */ protected $providers = []; + /** + * Custom providers. + * + * @var array + */ + protected $customProviders = []; + /** * Get the transaction id. * @@ -203,4 +210,27 @@ public function setProviders(array $providers): PaymentResponse return $this; } + + /** + * Get custom providers. + * + * @return array + */ + public function getCustomProviders(): array + { + return $this->customProviders ?? []; + } + + /** + * Set custom providers. + * + * @param array|null $customProviders + * @return PaymentResponse + */ + public function setCustomProviders(?array $customProviders): PaymentResponse + { + $this->customProviders = $customProviders; + + return $this; + } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 098d302..e0374cc 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -23,6 +23,7 @@ use Paytrail\SDK\Request\RevertPaymentAuthHoldRequest; use Paytrail\SDK\Request\SettlementRequest; use Paytrail\SDK\Request\ShopInShopPaymentRequest; +use Paytrail\SDK\Model\Provider; class ClientTest extends PaymentRequestTestCase { @@ -46,7 +47,7 @@ protected function setUp(): void $this->item = (new Item()) ->setProductCode('pr1') - ->setVatPercentage(24) + ->setVatPercentage(25.5) ->setReference('itemReference123') ->setStamp('itemStamp-1' . rand(1, 999999)) ->setUnits(1) @@ -56,7 +57,7 @@ protected function setUp(): void $this->item2 = (new Item()) ->setDeliveryDate('2020-12-12') ->setProductCode('pr2') - ->setVatPercentage(24) + ->setVatPercentage(25.5) ->setReference('itemReference123') ->setStamp('itemStamp-2' . rand(1, 999999)) ->setUnits(2) @@ -215,7 +216,13 @@ public function testAddCardFormRequest() public function testGetTokenRequest() { - $checkoutTokenizationId = '818c478e-5682-46bf-97fd-b9c2b93a3fcd'; + // To update after 11/2026 + // Get new tokenization_id, card and customer details. + // 'add-card' action will return tokenization_id, + // and with tokenization_id we could get card and customer details. + // Card used: 0024 https://docs.paytrail.com/#/payment-method-providers?id=test-cards-for-payments + + $checkoutTokenizationId = 'b34e5821-2a85-4840-8b27-21ef81168bec'; $client = $this->client; @@ -229,23 +236,23 @@ public function testGetTokenRequest() $responseJsonData = $response->jsonSerialize(); $expectedArray = [ - 'token' => 'c7441208-c2a1-4a10-8eb6-458bd8eaa64f', + 'token' => '798b445a-2216-46b7-ad1a-000f40ced6e8', 'card' => [ 'type' => 'Visa', 'bin' => '415301', 'partial_pan' => '0024', - 'expire_year' => '2023', + 'expire_year' => '2026', 'expire_month' => '11', - 'cvc_required' => 'no', + 'cvc_required' => 'not_tested', 'funding' => 'debit', 'category' => 'unknown', 'country_code' => 'FI', 'pan_fingerprint' => '693a68deec6d6fa363c72108f8d656d4fd0b6765f5457dd1c139523f4daaafce', - 'card_fingerprint' => 'c34cdd1952deb81734c012fbb11eabc56c4d61d198f28b448327ccf13f45417f' + 'card_fingerprint' => '24973f9037d418c0258ee61d90970c15a1c434a457d3974c9cdc12742f87c673' ], 'customer' => [ - 'network_address' => '93.174.192.154', - 'country_code' => 'FI' + 'network_address' => '89.35.145.204', + 'country_code' => 'PL' ] ]; @@ -260,7 +267,7 @@ public function testCitPaymentRequestCharge() $client = $this->client; $paymentRequest = $this->citPaymentRequest; - $citPaymentRequest = $paymentRequest->setToken('c7441208-c2a1-4a10-8eb6-458bd8eaa64f'); + $citPaymentRequest = $paymentRequest->setToken('798b445a-2216-46b7-ad1a-000f40ced6e8'); $this->assertTrue($citPaymentRequest->validate()); @@ -286,7 +293,7 @@ public function testMitPaymentRequestCharge() $client = $this->client; $paymentRequest = $this->mitPaymentRequest; - $mitPaymentRequest = $paymentRequest->setToken('c7441208-c2a1-4a10-8eb6-458bd8eaa64f'); + $mitPaymentRequest = $paymentRequest->setToken('798b445a-2216-46b7-ad1a-000f40ced6e8'); $this->assertTrue($mitPaymentRequest->validate()); @@ -309,10 +316,14 @@ public function testMitPaymentRequestCharge() public function testCitPaymentRequestCharge3DS() { + // To update after 11/2026 + // Card required with 3DS + // Card: 0170 https://docs.paytrail.com/#/payment-method-providers?id=test-cards-for-payments + $client = $this->client; $paymentRequest = $this->citPaymentRequest; - $citPaymentRequest = $paymentRequest->setToken('40037d79-5c7f-4ffe-bf86-2d2025b64c36'); + $citPaymentRequest = $paymentRequest->setToken('8d3cb70a-7911-42c4-81cd-5318a5f269a4'); $this->assertTrue($citPaymentRequest->validate()); @@ -327,7 +338,7 @@ public function testCitPaymentRequestAuthorizationHold() $client = $this->client; $paymentRequest = $this->citPaymentRequest; - $citPaymentRequest = $paymentRequest->setToken('c7441208-c2a1-4a10-8eb6-458bd8eaa64f'); + $citPaymentRequest = $paymentRequest->setToken('798b445a-2216-46b7-ad1a-000f40ced6e8'); $this->assertTrue($citPaymentRequest->validate()); @@ -353,7 +364,7 @@ public function testMitPaymentRequestAuthorizationHold() $client = $this->client; $paymentRequest = $this->mitPaymentRequest; - $mitPaymentRequest = $paymentRequest->setToken('c7441208-c2a1-4a10-8eb6-458bd8eaa64f'); + $mitPaymentRequest = $paymentRequest->setToken('798b445a-2216-46b7-ad1a-000f40ced6e8'); $this->assertTrue($mitPaymentRequest->validate()); @@ -379,7 +390,7 @@ public function testCitPaymentRequestAuthorizationHold3DS() $client = $this->client; $paymentRequest = $this->citPaymentRequest; - $citPaymentRequest = $paymentRequest->setToken('40037d79-5c7f-4ffe-bf86-2d2025b64c36'); + $citPaymentRequest = $paymentRequest->setToken('8d3cb70a-7911-42c4-81cd-5318a5f269a4'); $this->assertTrue($citPaymentRequest->validate()); @@ -395,7 +406,7 @@ public function testCitPaymentRequestCommit() $paymentRequest = $this->citPaymentRequest; $transactionId = 'c12e224e-806f-11ea-9de3-33451a6f6d70'; - $citPaymentRequest = $paymentRequest->setToken('c7441208-c2a1-4a10-8eb6-458bd8eaa64f'); + $citPaymentRequest = $paymentRequest->setToken('798b445a-2216-46b7-ad1a-000f40ced6e8'); $this->assertTrue($citPaymentRequest->validate()); @@ -409,7 +420,7 @@ public function testMitPaymentRequestCommit() $paymentRequest = $this->mitPaymentRequest; $transactionId = 'c12e224e-806f-11ea-9de3-33451a6f6d70'; - $mitPaymentRequest = $paymentRequest->setToken('c7441208-c2a1-4a10-8eb6-458bd8eaa64f'); + $mitPaymentRequest = $paymentRequest->setToken('798b445a-2216-46b7-ad1a-000f40ced6e8'); $this->assertTrue($mitPaymentRequest->validate()); @@ -422,7 +433,7 @@ public function testRevertCitPaymentAuthorizationHold() $client = $this->client; $paymentRequest = $this->citPaymentRequest; - $citPaymentRequest = $paymentRequest->setToken('c7441208-c2a1-4a10-8eb6-458bd8eaa64f'); + $citPaymentRequest = $paymentRequest->setToken('798b445a-2216-46b7-ad1a-000f40ced6e8'); $this->assertTrue($citPaymentRequest->validate()); @@ -447,7 +458,7 @@ public function testRevertMitPaymentAuthorizationHold() $client = $this->client; $paymentRequest = $this->mitPaymentRequest; - $mitPaymentRequest = $paymentRequest->setToken('c7441208-c2a1-4a10-8eb6-458bd8eaa64f'); + $mitPaymentRequest = $paymentRequest->setToken('798b445a-2216-46b7-ad1a-000f40ced6e8'); $this->assertTrue($mitPaymentRequest->validate()); @@ -510,7 +521,8 @@ public function testGetGroupedPaymentProvidersAcceptsLanguageParameters() { $providers = $this->client->getGroupedPaymentProviders(100, 'EN'); $this->assertIsArray($providers); - $this->assertEquals('Mobile payment methods', $providers['groups'][0]['name']); + // Get first provider groups providers and select first provider from array. + $this->assertInstanceOf(Provider::class, $providers['groups'][0]['providers'][0]); } public function testRequestPaymentReportReturnsRequestId() @@ -609,4 +621,11 @@ public function testRequestPaymentReportThrowsExceptionWhenEndDateIsInWrongForma ->setEndDate('1.1.2023'); $this->client->requestPaymentReport($reportRequest); } + + public function testPayAndAddCardReturnsTransactionIdAndUrl() + { + $response = $this->client->createPaymentAndAddCard($this->paymentRequest); + $this->assertIsString($response->getTransactionId()); + $this->assertIsString($response->getRedirectUrl()); + } } diff --git a/tests/Model/ItemTest.php b/tests/Model/ItemTest.php index b544c34..424768d 100644 --- a/tests/Model/ItemTest.php +++ b/tests/Model/ItemTest.php @@ -83,9 +83,19 @@ public function testItemWithoutProductCodeThrowsError() public static function providerForUnitPriceLimitValues() { return [ - 'Negative amount' => [-1, false], - 'Zero amount' => [0, true], - 'Maximum amount' => [99999999, true], + 'Negative amount' => [-1, true], + 'Zero amount' => [0, true], + 'Maximum amount' => [99999999, true], + 'Over maximum amount' => [100000000, false] + ]; + } + + public static function providerForUnitPriceLimitValuesShopInShop() + { + return [ + 'Negative amount' => [-1, false], + 'Zero amount' => [0, true], + 'Maximum amount' => [99999999, true], 'Over maximum amount' => [100000000, false] ]; } diff --git a/tests/Request/ShopInShopPaymentRequestTest.php b/tests/Request/ShopInShopPaymentRequestTest.php index b55f965..fce2d4c 100644 --- a/tests/Request/ShopInShopPaymentRequestTest.php +++ b/tests/Request/ShopInShopPaymentRequestTest.php @@ -129,4 +129,82 @@ public function testShopInShopPaymentRequestFail() $r->validate(); } + + public static function shopInShopPaymentRequestItems() + { + return [ + 'negative item failing validation' => [ + 'itemsPrice' => [20, -10], + 'amount' => 10, + 'expectedResult' => false + ], + 'positive validation' => [ + 'itemsPrice' => [20, 10], + 'amount' => 30, + 'expectedResult' => true + ], + ]; + } + + /** + * @dataProvider shopInShopPaymentRequestItems + */ + public function testNegativeRowsValidation($itemsPrice, $amount, $expectedResult) + { + $r = new ShopInShopPaymentRequest(); + $r->setAmount($amount); + $r->setStamp('RequestStamp'); + $r->setReference('RequestReference123'); + $r->setCurrency('EUR'); + $r->setLanguage('EN'); + + $i = 0; + $items = []; + foreach ($itemsPrice as $price) { + $com = new Commission(); + $com->setMerchant('123456'); + $com->setAmount(2); + + $item = new Item(); + $item->setStamp('someStamp' . $i) + ->setDeliveryDate('12.12.2020') + ->setProductCode('pr1' . $i) + ->setVatPercentage(25) + ->setUnitPrice($price) + ->setUnits(1) + ->setMerchant('222222') + ->setReference('1-2') + ->setCommission($com); + + $items[] = $item; + $i++; + } + + $r->setItems($items); + + $c = new Customer(); + $c->setEmail('customer@email.com'); + + $r->setCustomer($c); + + $cb = new CallbackUrl(); + $cb->setCancel('https://somedomain.com/cancel') + ->setSuccess('https://somedomain.com/success'); + + $r->setCallbackUrls($cb); + + $redirect = new CallbackUrl(); + $redirect->setSuccess('https://someother.com/success') + ->setCancel('https://someother.com/cancel'); + + $r->setRedirectUrls($redirect); + + try { + $result = $r->validate(); + } catch (ValidationException $e) { + $result = false; + } + + $this->assertEquals($expectedResult, $result); + } }