diff --git a/composer.json b/composer.json index b41ecd1..c02a05f 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "docs": "https://openapi.tpay.com" }, "require": { - "php": ">=5.6.0", + "php": ">=7.1", "ext-curl": "*", "ext-fileinfo": "*", "ext-json": "*", diff --git a/examples/Notifications/AllNotificationsExample.php b/examples/Notifications/AllNotificationsExample.php index b780017..ec80e8a 100644 --- a/examples/Notifications/AllNotificationsExample.php +++ b/examples/Notifications/AllNotificationsExample.php @@ -11,6 +11,7 @@ use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasUnregister; use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasUpdated; use Tpay\OpenApi\Model\Objects\NotificationBody\MarketplaceTransaction; +use Tpay\OpenApi\Model\Objects\NotificationBody\Recurring; use Tpay\OpenApi\Model\Objects\NotificationBody\Tokenization; use Tpay\OpenApi\Model\Objects\NotificationBody\TokenUpdate; use Tpay\OpenApi\Model\Objects\Objects; @@ -130,6 +131,20 @@ public function getVerifiedNotification() exit('TRUE'); } + if ($notification instanceof Recurring) { + // Notification about successful recurring registered + + $recurringId = $notification->recurringId->getValue(); + // The above example will check the notification and return the value of recurring id + + $transactionId = $notification->transactionId->getValue(); + // The above example will check the notification and return the value of received transaction id field + // You can access any notification field by $notification->fieldName + + // $recurringProcessor->process($notification) + exit('{"result":true}'); + } + // Ignore and silence other notification types if not expected http_response_code(404); exit('FALSE'); diff --git a/src/Api/Recurring/RecurringApi.php b/src/Api/Recurring/RecurringApi.php new file mode 100644 index 0000000..03d51f1 --- /dev/null +++ b/src/Api/Recurring/RecurringApi.php @@ -0,0 +1,58 @@ +addQueryFields('/recurring', $queryFields); + + return $this->run(static::GET, $requestUrl); + } + + /** @param string $recurringId */ + public function getRecurringById($recurringId) + { + return $this->run(static::GET, sprintf('/recurring/%s', $recurringId)); + } + + /** + * @param string $recurringId + * @param array $queryFields + */ + public function getTransactionsByRecurringId($recurringId, $queryFields = []) + { + $requestUrl = $this->addQueryFields(sprintf('/recurring/%s/transactions', $recurringId), $queryFields); + + return $this->run(static::GET, $requestUrl); + } + + /** @param array $fields */ + public function createRecurring($fields) + { + return $this->run(static::POST, '/recurring', $fields, new Recurring()); + } + + /** @param string $recurringId */ + public function cancelTransaction($recurringId) + { + return $this->run(static::POST, sprintf('/recurring/%s/cancel', $recurringId)); + } + + /** @param string $recurringId */ + public function updatePaymentInstrument($fields, $recurringId) + { + return $this->run( + static::POST, + sprintf('/recurring/%s/payment_instrument', $recurringId), + $fields, + new UpdatePaymentInstrument() + ); + } +} diff --git a/src/Api/TpayApi.php b/src/Api/TpayApi.php index a4d353c..39158de 100644 --- a/src/Api/TpayApi.php +++ b/src/Api/TpayApi.php @@ -7,6 +7,7 @@ use Tpay\OpenApi\Api\Authorization\AuthorizationApi; use Tpay\OpenApi\Api\Blik\BlikApi; use Tpay\OpenApi\Api\Collect\CollectApi; +use Tpay\OpenApi\Api\Recurring\RecurringApi; use Tpay\OpenApi\Api\Refunds\RefundsApi; use Tpay\OpenApi\Api\Reports\ReportsApi; use Tpay\OpenApi\Api\Transactions\TransactionsApi; @@ -28,6 +29,9 @@ class TpayApi /** @var null|CollectApi */ private $collect; + /** @var null|RecurringApi */ + private $recurring; + /** @var null|RefundsApi */ private $refunds; @@ -147,6 +151,22 @@ public function authorization() return $this->authorization; } + /** @return RecurringApi */ + public function recurring() + { + $this->authorize(); + if (null === $this->recurring) { + $this->recurring = (new RecurringApi($this->token, $this->productionMode)) + ->overrideApiUrl($this->apiUrl); + + if ($this->clientName) { + $this->recurring->setClientName($this->clientName); + } + } + + return $this->recurring; + } + /** @return RefundsApi */ public function refunds() { diff --git a/src/Factory/ArrayObjectFactory.php b/src/Factory/ArrayObjectFactory.php index 983d97c..33676db 100644 --- a/src/Factory/ArrayObjectFactory.php +++ b/src/Factory/ArrayObjectFactory.php @@ -10,6 +10,8 @@ use Tpay\OpenApi\Model\Objects\Merchant\ContactPerson; use Tpay\OpenApi\Model\Objects\Merchant\PointOfSale as MerchantPointOfSale; use Tpay\OpenApi\Model\Objects\Objects; +use Tpay\OpenApi\Model\Objects\Recurring\RetryInterval; +use Tpay\OpenApi\Model\Objects\Recurring\Schedule; use Tpay\OpenApi\Model\Objects\RequestBody\Account; use Tpay\OpenApi\Model\Objects\RequestBody\Merchant; @@ -51,6 +53,15 @@ public function create($fieldName, $parentObject) } } + if ($parentObject instanceof Schedule) { + switch ($fieldName) { + case 'retryIntervals': + return new RetryInterval(); + default: + throw new InvalidArgumentException(sprintf('Unsupported field "%s" in %s', $fieldName, $parentObject->getName())); + } + } + throw new InvalidArgumentException(sprintf('Field %s as array is not supported in %s object', $fieldName, $parentObject->getName())); } } diff --git a/src/Model/Fields/Notification/Recurring/IterationCount.php b/src/Model/Fields/Notification/Recurring/IterationCount.php new file mode 100644 index 0000000..bc89484 --- /dev/null +++ b/src/Model/Fields/Notification/Recurring/IterationCount.php @@ -0,0 +1,14 @@ + Id::class, + 'transactionId' => TransactionId::class, + 'hiddenDescription' => HiddenDescription::class, + 'iterationCount' => IterationCount::class, + 'iterationAttemptCount' => IterationCountAttempt::class, + 'status' => Status::class, + 'nextChargeDate' => NextChargeDate::class, + 'reason' => Reason::class, + ]; + + /** @var Id */ + public $recurringId; + + /** @var TransactionId */ + public $transactionId; + + /** @var HiddenDescription */ + public $hiddenDescription; + + /** @var IterationCount */ + public $iterationCount; + + /** @var IterationCountAttempt */ + public $iterationAttemptCount; + + /** @var Status */ + public $status; + + /** @var NextChargeDate */ + public $nextChargeDate; + + /** @var Reason */ + public $reason; + + public function getRequiredFields() + { + return [ + $this->recurringId, + $this->transactionId, + $this->hiddenDescription, + $this->iterationCount, + $this->iterationAttemptCount, + $this->status, + ]; + } +} diff --git a/src/Model/Objects/Objects.php b/src/Model/Objects/Objects.php index 67d2464..1e13159 100644 --- a/src/Model/Objects/Objects.php +++ b/src/Model/Objects/Objects.php @@ -44,22 +44,42 @@ public function getName() public function setObjectValues(&$object, $values) { foreach ($values as $fieldName => $fieldValue) { - if (is_array($fieldValue) && property_exists($object, $fieldName)) { - $this->setObjectsInArray($object, $fieldValue, $fieldName); + if (!is_object($object)) { continue; } - if (property_exists($object, $fieldName) && $this->isField($object->{$fieldName})) { - $object->{$fieldName}->setValue($fieldValue); - } elseif (property_exists($object, $fieldName) && $this->isObject($object->{$fieldName})) { - $this->setObjectValues($object->{$fieldName}, $fieldValue); - } else { - $errorField = $fieldName; - if (0 === $errorField) { - $errorField = $fieldValue; - } + + if (!property_exists($object, $fieldName)) { if (true === $this->strictCheck) { - throw new InvalidArgumentException(sprintf('Field %s is not supported', $errorField)); + throw new InvalidArgumentException( + sprintf('Field %s is not supported', $fieldName) + ); } + + continue; + } + + if (is_array($fieldValue) && is_array($object->{$fieldName})) { + $this->setObjectsInArray($object, $fieldValue, $fieldName); + + continue; + } + + if (is_array($fieldValue) && $this->isObject($object->{$fieldName})) { + $this->setObjectValues($object->{$fieldName}, $fieldValue); + + continue; + } + + if ($this->isField($object->{$fieldName})) { + $object->{$fieldName}->setValue($fieldValue); + + continue; + } + + if (true === $this->strictCheck) { + throw new InvalidArgumentException( + sprintf('Field %s is not supported', $fieldName) + ); } } @@ -72,6 +92,7 @@ protected function injectObjectFields($objectFields) foreach ($objectFields as $objectVar => $fieldClass) { if (is_array($fieldClass)) { $this->{$objectVar}[] = new $fieldClass[0](); + continue; } $this->{$objectVar} = new $fieldClass(); @@ -86,25 +107,33 @@ protected function injectObjectFields($objectFields) private function setObjectsInArray($object, $fieldValue, $fieldName) { foreach ($fieldValue as $field => $value) { + if (is_array($object->{$fieldName})) { + if (!isset($object->{$fieldName}[$field])) { + $object->{$fieldName}[$field] = $this->factory->create($fieldName, $object); + } + $this->setObjectValues($object->{$fieldName}[$field], $value); + + continue; + } + if (is_array($value)) { if (isset($object->{$fieldName}->{$field})) { $this->setObjectValues($object->{$fieldName}->{$field}, $value); } - if (is_array($object->{$fieldName})) { - if (!isset($object->{$fieldName}[$field])) { - $object->{$fieldName}[] = $this->factory->create($fieldName, $object); - } - $this->setObjectValues($object->{$fieldName}[$field], $value); - } - } else { - if (isset($object->{$fieldName}->{$field}) && $this->isField($object->{$fieldName}->{$field})) { - $object->{$fieldName}->{$field}->setValue($value); - } else { - if (true === $this->strictCheck) { - throw new InvalidArgumentException(sprintf('Field %s is not supported', $field)); - } - } + continue; + } + + if (isset($object->{$fieldName}->{$field}) && $this->isField($object->{$fieldName}->{$field})) { + $object->{$fieldName}->{$field}->setValue($value); + + continue; + } + + if (true === $this->strictCheck) { + throw new InvalidArgumentException( + sprintf('Field %s is not supported', $field) + ); } } } diff --git a/src/Model/Objects/Recurring/Payer.php b/src/Model/Objects/Recurring/Payer.php new file mode 100644 index 0000000..666ff4c --- /dev/null +++ b/src/Model/Objects/Recurring/Payer.php @@ -0,0 +1,59 @@ + Email::class, + 'name' => Name::class, + 'phone' => Phone::class, + 'address' => Address::class, + 'code' => PostalCode::class, + 'city' => City::class, + 'country' => Country::class, + 'taxId' => TaxId::class, + ]; + + /** @var Email */ + public $email; + + /** @var Name */ + public $name; + + /** @var Phone */ + public $phone; + + /** @var Address */ + public $address; + + /** @var PostalCode */ + public $code; + + /** @var City */ + public $city; + + /** @var Country */ + public $country; + + /** @var TaxId */ + public $taxId; + + public function getRequiredFields() + { + return [ + $this->email, + $this->name, + ]; + } +} diff --git a/src/Model/Objects/Recurring/PaymentInstrument.php b/src/Model/Objects/Recurring/PaymentInstrument.php new file mode 100644 index 0000000..82cce19 --- /dev/null +++ b/src/Model/Objects/Recurring/PaymentInstrument.php @@ -0,0 +1,29 @@ + PaymentType::class, + 'value' => Value::class, + ]; + + /** @var PaymentType */ + public $paymentType; + + /** @var Value */ + public $value; + + public function getRequiredFields() + { + return [ + $this->paymentType, + $this->value, + ]; + } +} diff --git a/src/Model/Objects/Recurring/RetryInterval.php b/src/Model/Objects/Recurring/RetryInterval.php new file mode 100644 index 0000000..1d94307 --- /dev/null +++ b/src/Model/Objects/Recurring/RetryInterval.php @@ -0,0 +1,31 @@ + Interval::class, + 'unit' => IntervalType::class, + ]; + + /** @var Interval */ + public $value; + + /** @var IntervalType */ + public $unit; + + public function getRequiredFields() + { + return [ + $this->value, + $this->unit, + ]; + } +} diff --git a/src/Model/Objects/Recurring/Schedule.php b/src/Model/Objects/Recurring/Schedule.php new file mode 100644 index 0000000..9f0d14c --- /dev/null +++ b/src/Model/Objects/Recurring/Schedule.php @@ -0,0 +1,56 @@ + Amount::class, + 'currency' => Currency::class, + 'firstChargeDate' => FirstChargeDate::class, + 'interval' => Interval::class, + 'intervalType' => IntervalType::class, + 'chargeCount' => ChargeCount::class, + 'retryIntervals' => [RetryInterval::class], + ]; + + /** @var Amount */ + public $amount; + + /** @var Currency */ + public $currency; + + /** @var FirstChargeDate */ + public $firstChargeDate; + + /** @var Interval */ + public $interval; + + /** @var IntervalType */ + public $intervalType; + + /** @var ChargeCount */ + public $chargeCount; + + /** @var RetryInterval */ + public $retryIntervals; + + public function getRequiredFields() + { + return [ + $this->amount, + $this->currency, + $this->firstChargeDate, + $this->interval, + $this->intervalType, + ]; + } +} diff --git a/src/Model/Objects/RequestBody/Recurring.php b/src/Model/Objects/RequestBody/Recurring.php new file mode 100644 index 0000000..04a54f5 --- /dev/null +++ b/src/Model/Objects/RequestBody/Recurring.php @@ -0,0 +1,58 @@ + Id::class, + 'description' => Description::class, + 'hiddenDescription' => HiddenDescription::class, + 'payer' => Payer::class, + 'schedule' => Schedule::class, + 'paymentInstrument' => PaymentInstrument::class, + 'callbackUrl' => CallbackUrl::class, + ]; + + /** @var Id */ + public $id; + + /** @var Description */ + public $description; + + /** @var HiddenDescription */ + public $hiddenDescription; + + /** @var Payer */ + public $payer; + + /** @var Schedule */ + public $schedule; + + /** @var PaymentInstrument */ + public $paymentInstrument; + + /** @var CallbackUrl */ + public $callbackUrl; + + public function getRequiredFields() + { + return [ + $this->id, + $this->description, + $this->payer, + $this->schedule, + $this->paymentInstrument, + $this->callbackUrl, + ]; + } +} diff --git a/src/Model/Objects/RequestBody/UpdatePaymentInstrument.php b/src/Model/Objects/RequestBody/UpdatePaymentInstrument.php new file mode 100644 index 0000000..a5c7610 --- /dev/null +++ b/src/Model/Objects/RequestBody/UpdatePaymentInstrument.php @@ -0,0 +1,23 @@ + PaymentInstrument::class, + ]; + + /** @var PaymentInstrument */ + public $paymentInstrument; + + public function getRequiredFields() + { + return [ + $this->paymentInstrument, + ]; + } +} diff --git a/src/Webhook/JWSVerifiedPaymentNotification.php b/src/Webhook/JWSVerifiedPaymentNotification.php index 23578b8..3f58b04 100644 --- a/src/Webhook/JWSVerifiedPaymentNotification.php +++ b/src/Webhook/JWSVerifiedPaymentNotification.php @@ -9,6 +9,7 @@ use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasUnregister; use Tpay\OpenApi\Model\Objects\NotificationBody\BlikAliasUpdated; use Tpay\OpenApi\Model\Objects\NotificationBody\MarketplaceTransaction; +use Tpay\OpenApi\Model\Objects\NotificationBody\Recurring; use Tpay\OpenApi\Model\Objects\NotificationBody\Tokenization; use Tpay\OpenApi\Model\Objects\NotificationBody\TokenUpdate; use Tpay\OpenApi\Model\Objects\Objects; @@ -218,6 +219,8 @@ private function getNotificationObject() throw new TpayException('Not recognised or invalid notification event: '.json_encode($source)); } $source = $source['msg_value']; + } elseif (isset($source['recurringId'])) { + $requestBody = new Recurring(); } else { throw new TpayException( 'Cannot determine notification type. POST payload: '.json_encode($source) diff --git a/tests/Factory/ArrayObjectFactoryTest.php b/tests/Factory/ArrayObjectFactoryTest.php index 2c958a0..b764ae8 100644 --- a/tests/Factory/ArrayObjectFactoryTest.php +++ b/tests/Factory/ArrayObjectFactoryTest.php @@ -12,6 +12,8 @@ use Tpay\OpenApi\Model\Objects\Merchant\Address as MerchantAddress; use Tpay\OpenApi\Model\Objects\Merchant\ContactPerson; use Tpay\OpenApi\Model\Objects\Merchant\PointOfSale as MerchantPointOfSale; +use Tpay\OpenApi\Model\Objects\Recurring\RetryInterval; +use Tpay\OpenApi\Model\Objects\Recurring\Schedule; use Tpay\OpenApi\Model\Objects\RequestBody\Account; use Tpay\OpenApi\Model\Objects\RequestBody\Merchant; use Tpay\OpenApi\Model\Objects\RequestBody\Refund; @@ -75,6 +77,12 @@ public function validObjectProvider() 'object' => new Account(), 'expectedResult' => Person::class, ]; + + yield 'recurring retryIntervals' => [ + 'fieldName' => 'retryIntervals', + 'object' => new Schedule(), + 'expectedResult' => RetryInterval::class, + ]; } public function invalidObjectProvider() diff --git a/tests/Webhook/JWSVerifiedPaymentNotificationTest.php b/tests/Webhook/JWSVerifiedPaymentNotificationTest.php index 42bbe4b..830d0e0 100644 --- a/tests/Webhook/JWSVerifiedPaymentNotificationTest.php +++ b/tests/Webhook/JWSVerifiedPaymentNotificationTest.php @@ -7,6 +7,7 @@ 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\Recurring; use Tpay\OpenApi\Model\Objects\NotificationBody\Tokenization; use Tpay\OpenApi\Model\Objects\NotificationBody\TokenUpdate; use Tpay\OpenApi\Utilities\TpayException; @@ -223,6 +224,62 @@ public function positiveValidationProvider() $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']; + $payload = <<<'JSON' +{ + "recurringId": "01KHXNTSWXP42CYFTF5TM3FJJX", + "transactionId": "TR-1234-12012345678901234567822", + "hiddenDescription": "ORDER-123", + "iterationCount": "1", + "iterationAttemptCount": "1", + "status": "active", + "nextChargeDate": null, + "reason": null +} +JSON; + $data = json_decode($payload, true); + $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', true, Recurring::class, 'recurringId', '01KHXNTSWXP42CYFTF5TM3FJJX']; + + $payload = <<<'JSON' +{ + "recurringId": "01KHXNTSWXP42CYFTF5TM3FJJY", + "transactionId": "TR-1234-12012345678901234567822", + "hiddenDescription": "ORDER-123", + "iterationCount": "1", + "iterationAttemptCount": "1", + "status": "active" +} +JSON; + $data = json_decode($payload, true); + $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', true, Recurring::class, 'recurringId', '01KHXNTSWXP42CYFTF5TM3FJJY']; + + $payload = <<<'JSON' +{ + "recurringId": "01KHXNTSWXP42CYFTF5TM3FJJZ", + "transactionId": "TR-1234-12012345678901234567822", + "hiddenDescription": "ORDER-123", + "iterationCount": "1", + "iterationAttemptCount": "1", + "status": "active", + "nextChargeDate": "2024-12-10 09:27:59" +} +JSON; + $data = json_decode($payload, true); + $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', true, Recurring::class, 'recurringId', '01KHXNTSWXP42CYFTF5TM3FJJZ']; + + $payload = <<<'JSON' +{ + "recurringId": "01KHXNTSWXP42CYFTF5TM3FJJA", + "transactionId": "", + "hiddenDescription": "ORDER-123", + "iterationCount": "1", + "iterationAttemptCount": "1", + "status": "failed", + "reason": "insufficient funds" +} +JSON; + $data = json_decode($payload, true); + $result[] = ['application/json', $data, $payload, $this->sign($payload, true), 'x', true, Recurring::class, 'recurringId', '01KHXNTSWXP42CYFTF5TM3FJJA']; + return $result; }