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/BlikAlias/BlikAliasUnregisterItem.php b/src/Model/Objects/NotificationBody/BlikAlias/BlikAliasUnregisterItem.php new file mode 100644 index 0000000..d2a6a42 --- /dev/null +++ b/src/Model/Objects/NotificationBody/BlikAlias/BlikAliasUnregisterItem.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/Model/Objects/NotificationBody/BlikAliasRegister.php b/src/Model/Objects/NotificationBody/BlikAliasRegister.php new file mode 100644 index 0000000..5269f11 --- /dev/null +++ b/src/Model/Objects/NotificationBody/BlikAliasRegister.php @@ -0,0 +1,40 @@ + Id::class, + 'event' => Event::class, + 'msg_value' => [BlikAliasRegisterItem::class], + 'md5sum' => Md5sum::class, + ]; + + /** @var Id */ + public $id; + + /** @var Event */ + public $event; + + /** @var BlikAliasRegisterItem */ + public $msg_value; + + /** @var Md5sum */ + public $md5sum; + + public function getRequiredFields() + { + return [ + $this->id, + $this->event, + $this->msg_value, + ]; + } +} diff --git a/src/Model/Objects/NotificationBody/BlikAliasUnregister.php b/src/Model/Objects/NotificationBody/BlikAliasUnregister.php new file mode 100644 index 0000000..66b6659 --- /dev/null +++ b/src/Model/Objects/NotificationBody/BlikAliasUnregister.php @@ -0,0 +1,40 @@ + Id::class, + 'event' => Event::class, + 'msg_value' => [BlikAliasUnregisterItem::class], + 'md5sum' => Md5sum::class, + ]; + + /** @var Id */ + public $id; + + /** @var Event */ + public $event; + + /** @var BlikAliasUnregisterItem */ + public $msg_value; + + /** @var Md5sum */ + public $md5sum; + + public function getRequiredFields() + { + return [ + $this->id, + $this->event, + $this->msg_value, + ]; + } +} 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..085b374 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,83 @@ 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)); } + } 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]; + + if (is_array($definition) && is_array($value)) { + $objectClass = $definition[0]; + $items = []; + + foreach ($value as $item) { + $object = new $objectClass(); + $items[] = $this->castRequestBody($item, $object); + } + + $fields[$parameter] = $items; + continue; + } + + if (is_string($definition)) { + /** @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..89bcfd6 100644 --- a/tests/Webhook/JWSVerifiedPaymentNotificationTest.php +++ b/tests/Webhook/JWSVerifiedPaymentNotificationTest.php @@ -4,6 +4,10 @@ use PHPUnit\Framework\TestCase; use Tpay\OpenApi\Model\Objects\NotificationBody\BasicPayment; +use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAlias\BlikAliasRegisterItem; +use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAlias\BlikAliasUnregisterItem; +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; @@ -23,17 +27,18 @@ class JWSVerifiedPaymentNotificationTest extends TestCase /** * @dataProvider positiveValidationProvider * - * @param mixed $contentType - * @param mixed $data - * @param mixed $payload - * @param mixed $signature - * @param mixed $secret - * @param mixed $productionMode - * @param mixed $expectedClass - * @param mixed $fieldName - * @param mixed $fieldValue + * @param mixed $contentType + * @param mixed $data + * @param mixed $payload + * @param mixed $signature + * @param mixed $secret + * @param mixed $productionMode + * @param mixed $expectedClass + * @param mixed $fieldName + * @param mixed $fieldValue + * @param null|mixed $expectedItemClass */ - public function testPositiveValidationCases($contentType, $data, $payload, $signature, $secret, $productionMode, $expectedClass, $fieldName, $fieldValue) + public function testPositiveValidationCases($contentType, $data, $payload, $signature, $secret, $productionMode, $expectedClass, $fieldName, $fieldValue, $expectedItemClass = null) { $requestMock = new RequestParserMock($contentType, $data, $payload, $signature); $certificateMock = $this->getCertificateMock(); @@ -43,7 +48,14 @@ public function testPositiveValidationCases($contentType, $data, $payload, $sign $notificationObject = $notification->getNotification(); $this->assertInstanceOf($expectedClass, $notificationObject); - $this->assertEquals($notificationObject->{$fieldName}->getValue(), $fieldValue); + + $field = $notificationObject->{$fieldName}; + if (is_array($field)) { + $this->assertInstanceOf($expectedItemClass, $field[0]); + $this->assertEquals($fieldValue, $field[0]->toArray()); + } else { + $this->assertEquals($field->getValue(), $fieldValue); + } } /** @@ -87,6 +99,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 +113,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 +136,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 +198,43 @@ 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, 'msg_value', ['value' => 'user_unique_alias_123', 'type' => 'UID', 'expirationDate' => '2024-12-10 09:27:59'], BlikAliasRegisterItem::class]; + + $payload = http_build_query($data); + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload, true), 'x', true, BlikAliasRegister::class, 'msg_value', ['value' => 'user_unique_alias_123', 'type' => 'UID', 'expirationDate' => '2024-12-10 09:27:59'], BlikAliasRegisterItem::class]; + + $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, 'msg_value', ['value' => 'user_unique_alias_456', 'type' => 'UID'], BlikAliasUnregisterItem::class]; + + $payload = http_build_query($data); + $result[] = ['application/x-www-form-urlencoded', $data, $payload, $this->sign($payload, true), 'x', true, BlikAliasUnregister::class, 'msg_value', ['value' => 'user_unique_alias_456', 'type' => 'UID'], BlikAliasUnregisterItem::class]; + return $result; } @@ -203,21 +261,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';