diff --git a/composer.json b/composer.json index 5dd7fcf..b41ecd1 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "require-dev": { "ext-openssl": "*", "php-parallel-lint/php-parallel-lint": "^1.3.2", - "phpunit/phpunit": "^5.7.27 || ^9.6.10", + "phpunit/phpunit": "^5.7 || ^7.5 || ^8.5 || ^9.5", "psx/cache": "^v1.0.2" }, "autoload": { diff --git a/examples/Notifications/AllNotificationsExample.php b/examples/Notifications/AllNotificationsExample.php index 10c8d07..1870fe9 100644 --- a/examples/Notifications/AllNotificationsExample.php +++ b/examples/Notifications/AllNotificationsExample.php @@ -6,6 +6,8 @@ use PSX\Cache\SimpleCache; use Tpay\Example\ExamplesConfig; use Tpay\OpenApi\Model\Objects\NotificationBody\BasicPayment; +use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasRegister; +use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasUnregister; use Tpay\OpenApi\Model\Objects\NotificationBody\MarketplaceTransaction; use Tpay\OpenApi\Model\Objects\NotificationBody\Tokenization; use Tpay\OpenApi\Model\Objects\NotificationBody\TokenUpdate; @@ -81,6 +83,28 @@ public function getVerifiedNotification() // $marketplaceTransactionProcessor->process($notification) exit('{"result":true}'); } + if ($notification instanceof BlikAliasRegister) { + // Notification about successful blik alias registered + + $value = $notification->value->getValue(); + // The above example will check the notification and return the value for future transactions, + // correlate this value with the payer/user of your system for subsequent payment handling + // You can access any notification field by $notification->fieldName + + // $blikAliasRegisteredProcessor->process($notification) + exit('TRUE'); + } + + if ($notification instanceof BlikAliasUnregister) { + // Notification about successful blik alias unregistered + + $value = $notification->value->getValue(); + // The above example will check the notification and return the value of deleted token + // You can access any notification field by $notification->fieldName + + // $blikAliasRegisteredProcessor->process($notification) + exit('TRUE'); + } // Ignore and silence other notification types if not expected http_response_code(404); diff --git a/src/Model/Fields/Notification/BlikAlias/Event.php b/src/Model/Fields/Notification/BlikAlias/Event.php new file mode 100644 index 0000000..7a4997e --- /dev/null +++ b/src/Model/Fields/Notification/BlikAlias/Event.php @@ -0,0 +1,11 @@ + Value::class, + 'type' => Type::class, + 'expirationDate' => ExpirationDate::class, + ]; + + /** @var Value */ + public $value; + + /** @var Type */ + public $type; + + /** @var ExpirationDate */ + public $expirationDate; + + public function getRequiredFields() + { + return [ + $this->value, + $this->type, + $this->expirationDate, + ]; + } + + public function toArray() + { + return [ + 'value' => $this->value->getValue(), + 'type' => $this->type->getValue(), + 'expirationDate' => $this->expirationDate->getValue(), + ]; + } +} diff --git a/src/Model/Objects/NotificationBody/BlikAliasUnregister.php b/src/Model/Objects/NotificationBody/BlikAliasUnregister.php new file mode 100644 index 0000000..7e8f587 --- /dev/null +++ b/src/Model/Objects/NotificationBody/BlikAliasUnregister.php @@ -0,0 +1,37 @@ + Value::class, + 'type' => Type::class, + ]; + + /** @var Value */ + public $value; + + /** @var Type */ + public $type; + + public function getRequiredFields() + { + return [ + $this->value, + $this->type, + ]; + } + + public function toArray() + { + return [ + 'value' => $this->value->getValue(), + 'type' => $this->type->getValue(), + ]; + } +} diff --git a/src/Utilities/RequestParser.php b/src/Utilities/RequestParser.php index 2783706..f15c973 100644 --- a/src/Utilities/RequestParser.php +++ b/src/Utilities/RequestParser.php @@ -4,10 +4,13 @@ class RequestParser { + /** @var null|string */ + private $rawBody; + /** @return string */ public function getContentType() { - return $_SERVER['CONTENT_TYPE'] ?: ''; + return isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : ''; } /** @@ -17,11 +20,14 @@ public function getContentType() */ public function getParsedContent() { - if ('application/json' === $this->getContentType()) { - $body = file_get_contents('php://input'); + if (false !== strpos($this->getContentType(), 'application/json')) { + $body = $this->getRawBody(); $jsonData = json_decode($body, true); + if (is_null($jsonData)) { - throw new TpayException('Invalid JSON body. Json Error: '.json_last_error_msg().' Body: '.$body); + throw new TpayException( + 'Invalid JSON body. Json Error: '.json_last_error_msg().' Body: '.$body + ); } return $jsonData; @@ -33,7 +39,7 @@ public function getParsedContent() /** @return string */ public function getPayload() { - return file_get_contents('php://input'); + return $this->getRawBody(); } /** @@ -50,4 +56,13 @@ public function getSignature() return $jws; } + + private function getRawBody() + { + if (null === $this->rawBody) { + $this->rawBody = file_get_contents('php://input'); + } + + return $this->rawBody; + } } diff --git a/src/Webhook/JWSVerifiedPaymentNotification.php b/src/Webhook/JWSVerifiedPaymentNotification.php index 68fa065..98317b3 100644 --- a/src/Webhook/JWSVerifiedPaymentNotification.php +++ b/src/Webhook/JWSVerifiedPaymentNotification.php @@ -2,7 +2,10 @@ namespace Tpay\OpenApi\Webhook; +use Tpay\OpenApi\Model\Fields\Field; use Tpay\OpenApi\Model\Objects\NotificationBody\BasicPayment; +use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasRegister; +use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasUnregister; use Tpay\OpenApi\Model\Objects\NotificationBody\MarketplaceTransaction; use Tpay\OpenApi\Model\Objects\NotificationBody\Tokenization; use Tpay\OpenApi\Model\Objects\NotificationBody\TokenUpdate; @@ -164,12 +167,12 @@ private function getResourcePrefix() */ private function getNotificationObject() { - if ('application/json' === $this->requestParser->getContentType()) { - $jsonData = $this->requestParser->getParsedContent(); - if (!isset($jsonData['type'])) { - throw new TpayException('Not recognised or invalid notification type. JSON: '.json_encode($jsonData)); - } - switch ($jsonData['type']) { + $source = $this->requestParser->getParsedContent(); + + if (isset($source['tr_id'])) { + $requestBody = new BasicPayment(); + } elseif (isset($source['type'])) { + switch ($source['type']) { case 'tokenization': $requestBody = new Tokenization(); break; @@ -180,28 +183,69 @@ private function getNotificationObject() $requestBody = new MarketplaceTransaction(); break; default: - throw new TpayException('Not recognised or invalid notification type. JSON: '.json_encode($jsonData)); + throw new TpayException( + 'Not recognised or invalid notification type: '.$source['type'] + ); } - if (!isset($jsonData['data'])) { - throw new TpayException('Not recognised or invalid notification type. JSON: '.json_encode($jsonData)); + + if (!isset($source['data'])) { + throw new TpayException('Not recognised or invalid notification type: '.json_encode($source)); } - $source = $jsonData['data']; - } else { - $source = $this->requestParser->getParsedContent(); - if (!isset($source['tr_id'])) { - throw new TpayException('Not recognised or invalid notification type. POST: '.json_encode($source)); + + $source = $source['data']; + } elseif (isset($source['event'])) { + switch ($source['event']) { + case 'ALIAS_REGISTER': + $requestBody = new BlikAliasRegister(); + break; + case 'ALIAS_UNREGISTER': + $requestBody = new BlikAliasUnregister(); + break; + default: + throw new TpayException( + 'Not recognised or invalid notification event: '.$source['event'] + ); } - $requestBody = new BasicPayment(); - } - foreach ($source as $parameter => $value) { - if (isset($requestBody->{$parameter})) { - $source[$parameter] = Util::cast($value, $requestBody->{$parameter}->getType()); + if (!isset($source['msg_value']) || !is_array($source['msg_value'])) { + throw new TpayException('Not recognised or invalid notification event: '.json_encode($source)); } + $source = $source['msg_value']; + } else { + throw new TpayException( + 'Cannot determine notification type. POST payload: '.json_encode($source) + ); } + + $source = $this->castRequestBody($source, $requestBody); + $this->Manager ->setRequestBody($requestBody) ->setFields($source, false); return $this->Manager->getRequestBody(); } + + private function castRequestBody($source, $requestBody) + { + $fields = []; + $definitions = $requestBody::OBJECT_FIELDS; + + foreach ($source as $parameter => $value) { + if (!isset($definitions[$parameter])) { + continue; + } + + $definition = $definitions[$parameter]; + + /** @var Field $field */ + $field = new $definition(); + + $fields[$parameter] = Util::cast( + $value, + $field->getType() + ); + } + + return $fields; + } } diff --git a/tests/Webhook/JWSVerifiedPaymentNotificationTest.php b/tests/Webhook/JWSVerifiedPaymentNotificationTest.php index 5cf25f3..42bbe4b 100644 --- a/tests/Webhook/JWSVerifiedPaymentNotificationTest.php +++ b/tests/Webhook/JWSVerifiedPaymentNotificationTest.php @@ -4,6 +4,8 @@ use PHPUnit\Framework\TestCase; use Tpay\OpenApi\Model\Objects\NotificationBody\BasicPayment; +use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasRegister; +use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasUnregister; use Tpay\OpenApi\Model\Objects\NotificationBody\MarketplaceTransaction; use Tpay\OpenApi\Model\Objects\NotificationBody\Tokenization; use Tpay\OpenApi\Model\Objects\NotificationBody\TokenUpdate; @@ -43,7 +45,9 @@ public function testPositiveValidationCases($contentType, $data, $payload, $sign $notificationObject = $notification->getNotification(); $this->assertInstanceOf($expectedClass, $notificationObject); - $this->assertEquals($notificationObject->{$fieldName}->getValue(), $fieldValue); + + $field = $notificationObject->{$fieldName}; + $this->assertEquals($field->getValue(), $fieldValue); } /** @@ -87,6 +91,9 @@ public function positiveValidationProvider() $data = json_decode($payload, true); $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $payload = http_build_query($data); + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload, true), 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $payload = <<<'JSON' { "type": "token_update", @@ -98,6 +105,9 @@ public function positiveValidationProvider() $data = json_decode($payload, true); $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', true, TokenUpdate::class, 'token', '1234567890123456789012345678901234567890123456789012345678901234']; + $payload = http_build_query($data); + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload, true), 'x', true, TokenUpdate::class, 'token', '1234567890123456789012345678901234567890123456789012345678901234']; + $payload = <<<'JSON' { "type": "marketplace_transaction", @@ -118,6 +128,9 @@ public function positiveValidationProvider() $data = json_decode($payload, true); $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', true, MarketplaceTransaction::class, 'transactionId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $payload = http_build_query($data); + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload, true), 'x', true, MarketplaceTransaction::class, 'transactionId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $id = '12345'; $tr_id = 'TR-1234-89012345678901234567890'; $tr_amount = '144.69'; @@ -177,6 +190,39 @@ public function positiveValidationProvider() $payload = http_build_query($data); $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload, true), self::CORRECT_CODE, true, BasicPayment::class, 'tokenPaymentData_tokenValue', '1234567890123456']; + $payload = <<<'JSON' +{ + "id": "1010", + "event": "ALIAS_REGISTER", + "msg_value": { + "value": "user_unique_alias_123", + "type": "UID", + "expirationDate": "2024-12-10 09:27:59" + } +} +JSON; + $data = json_decode($payload, true); + $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', true, BlikAliasRegister::class, 'value', 'user_unique_alias_123']; + + $payload = http_build_query($data); + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload, true), 'x', true, BlikAliasRegister::class, 'value', 'user_unique_alias_123']; + + $payload = <<<'JSON' +{ + "id": "1010", + "event": "ALIAS_UNREGISTER", + "msg_value": { + "value": "user_unique_alias_456", + "type": "UID" + } +} +JSON; + $data = json_decode($payload, true); + $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', true, BlikAliasUnregister::class, 'value', 'user_unique_alias_456']; + + $payload = http_build_query($data); + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload, true), 'x', true, BlikAliasUnregister::class, 'value', 'user_unique_alias_456']; + return $result; } @@ -203,21 +249,33 @@ public function negativeValidationProvider() $data = json_decode($payload, true); $result[] = ['application/json', $data, $payload, 'x', 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $result[] = ['application/x-www-form-urlencoded', $data, $payload, 'x', 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + // invalid signature (malformed token) $result[] = ['application/json', $data, $payload, 'fdsafsdfafdasfadsf', 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $result[] = ['application/x-www-form-urlencoded', $data, $payload, 'fdsafsdfafdasfadsf', 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + // invalid signature (payload difference) $result[] = ['application/json', $data, $payload, $this->sign($payload.'4324324324234', true), 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload.'4324324324234', true), 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + // invalid signature (invalid algorithm) $result[] = ['application/json', $data, $payload, $this->encode(json_encode(['alg' => 'none'])).'..fadsfdasfdsf', 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->encode(json_encode(['alg' => 'none'])).'..fadsfdasfdsf', 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + // invalid signature (not trusted signature URL) $result[] = ['application/json', $data, $payload, $this->encode(json_encode(['alg' => 'RS256', 'x5u' => 'https://example.com/hostile.pem'])).'..fadsfdasfdsf', 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->encode(json_encode(['alg' => 'RS256', 'x5u' => 'https://example.com/hostile.pem'])).'..fadsfdasfdsf', 'x', true, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + // invalid signature (prod certificate in sandbox environment) $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', false, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload, true), 'x', false, Tokenization::class, 'tokenizationId', 'TO-1234-890123456789012345678901234567890123456789012345678901234']; + // invalid md5sum $id = '12345'; $tr_id = 'TR-1234-89012345678901234567890';