From e9c5f22edde1a1f9b3e22ccac7e821bfacba408c Mon Sep 17 00:00:00 2001 From: Marcin Warzybok Date: Mon, 8 May 2023 13:52:55 +0200 Subject: [PATCH 1/2] SP-553 Payout Groups - PHP --- src/BitPaySDK/Client.php | 114 +++++++++++++++--- src/BitPaySDK/Env.php | 2 +- src/BitPaySDK/Model/Payout/Payout.php | 46 +++++++ src/BitPaySDK/Model/Payout/PayoutGroup.php | 52 ++++++++ .../Model/Payout/PayoutGroupFailed.php | 65 ++++++++++ src/BitPaySDK/Util/JsonMapper/JsonMapper.php | 1 + test/unit/BitPaySDK/ClientTest.php | 76 ++++++++++++ .../json/cancelPayoutGroupResponse.json | 46 +++++++ .../json/createPayoutGroupRequest.json | 1 + .../json/createPayoutGroupResponse.json | 29 +++++ 10 files changed, 414 insertions(+), 18 deletions(-) create mode 100644 src/BitPaySDK/Model/Payout/PayoutGroup.php create mode 100644 src/BitPaySDK/Model/Payout/PayoutGroupFailed.php create mode 100644 test/unit/BitPaySDK/json/cancelPayoutGroupResponse.json create mode 100644 test/unit/BitPaySDK/json/createPayoutGroupRequest.json create mode 100644 test/unit/BitPaySDK/json/createPayoutGroupResponse.json diff --git a/src/BitPaySDK/Client.php b/src/BitPaySDK/Client.php index 01e34d88..6b043444 100644 --- a/src/BitPaySDK/Client.php +++ b/src/BitPaySDK/Client.php @@ -11,32 +11,31 @@ use BitPaySDK\Exceptions\BillUpdateException; use BitPaySDK\Exceptions\BitPayException; use BitPaySDK\Exceptions\CurrencyQueryException; -use BitPaySDK\Exceptions\InvoiceCreationException; -use BitPaySDK\Exceptions\InvoiceUpdateException; -use BitPaySDK\Exceptions\InvoiceQueryException; use BitPaySDK\Exceptions\InvoiceCancellationException; +use BitPaySDK\Exceptions\InvoiceCreationException; use BitPaySDK\Exceptions\InvoicePaymentException; +use BitPaySDK\Exceptions\InvoiceQueryException; +use BitPaySDK\Exceptions\InvoiceUpdateException; use BitPaySDK\Exceptions\LedgerQueryException; -use BitPaySDK\Exceptions\PayoutRecipientCreationException; -use BitPaySDK\Exceptions\PayoutRecipientCancellationException; -use BitPaySDK\Exceptions\PayoutRecipientQueryException; -use BitPaySDK\Exceptions\PayoutRecipientUpdateException; -use BitPaySDK\Exceptions\PayoutRecipientNotificationException; +use BitPaySDK\Exceptions\PayoutBatchCancellationException; +use BitPaySDK\Exceptions\PayoutBatchCreationException; +use BitPaySDK\Exceptions\PayoutBatchNotificationException; +use BitPaySDK\Exceptions\PayoutBatchQueryException; use BitPaySDK\Exceptions\PayoutCancellationException; use BitPaySDK\Exceptions\PayoutCreationException; -use BitPaySDK\Exceptions\PayoutQueryException; -use BitPaySDK\Exceptions\PayoutUpdateException; use BitPaySDK\Exceptions\PayoutNotificationException; -use BitPaySDK\Exceptions\PayoutBatchCreationException; -use BitPaySDK\Exceptions\PayoutBatchQueryException; -use BitPaySDK\Exceptions\PayoutBatchCancellationException; -use BitPaySDK\Exceptions\PayoutBatchNotificationException; +use BitPaySDK\Exceptions\PayoutQueryException; +use BitPaySDK\Exceptions\PayoutRecipientCancellationException; +use BitPaySDK\Exceptions\PayoutRecipientCreationException; +use BitPaySDK\Exceptions\PayoutRecipientNotificationException; +use BitPaySDK\Exceptions\PayoutRecipientQueryException; +use BitPaySDK\Exceptions\PayoutRecipientUpdateException; use BitPaySDK\Exceptions\RateQueryException; -use BitPaySDK\Exceptions\RefundCreationException; -use BitPaySDK\Exceptions\RefundUpdateException; use BitPaySDK\Exceptions\RefundCancellationException; +use BitPaySDK\Exceptions\RefundCreationException; use BitPaySDK\Exceptions\RefundNotificationException; use BitPaySDK\Exceptions\RefundQueryException; +use BitPaySDK\Exceptions\RefundUpdateException; use BitPaySDK\Exceptions\SettlementQueryException; use BitPaySDK\Exceptions\SubscriptionCreationException; use BitPaySDK\Exceptions\SubscriptionQueryException; @@ -46,16 +45,18 @@ use BitPaySDK\Model\Facade; use BitPaySDK\Model\Invoice\Invoice; use BitPaySDK\Model\Invoice\Refund; -use BitPaySDK\Model\Wallet\Wallet; use BitPaySDK\Model\Ledger\Ledger; use BitPaySDK\Model\Payout\Payout; use BitPaySDK\Model\Payout\PayoutBatch; +use BitPaySDK\Model\Payout\PayoutGroup; +use BitPaySDK\Model\Payout\PayoutGroupFailed; use BitPaySDK\Model\Payout\PayoutRecipient; use BitPaySDK\Model\Payout\PayoutRecipients; use BitPaySDK\Model\Rate\Rate; use BitPaySDK\Model\Rate\Rates; use BitPaySDK\Model\Settlement\Settlement; use BitPaySDK\Model\Subscription\Subscription; +use BitPaySDK\Model\Wallet\Wallet; use BitPaySDK\Util\JsonMapper\JsonMapper; use BitPaySDK\Util\RESTcli\RESTcli; use Exception; @@ -1918,6 +1919,63 @@ public function cancelPayout(string $payoutId): bool return $result; } + /** + * Create Payout Group. + * + * @see Create Payout Group + * + * @param Payout[] $payouts + * @return PayoutGroup + * @throws PayoutCreationException + */ + public function createPayoutGroup(array $payouts): PayoutGroup + { + $request = []; + try { + $request['token'] = $this->_tokenCache->getTokenByFacade(Facade::Payout); + } catch (Exception $e) { + throw new PayoutCreationException("Missing facade token"); + } + + try { + foreach ($payouts as $payout) { + $request['instructions'][] = $payout->toArray(); + } + $responseJson = $this->_RESTcli->post("payouts/group", $request); + + return $this->getPayoutGroupResponse($responseJson, 'completed'); + } catch (Exception $e) { + throw new PayoutCreationException("failed to serialize Payout object : " . $e->getMessage()); + } + } + + /** + * Cancel Payout group. + * + * @see Cancel a Payout Group + * + * @param string $groupId + * @return PayoutGroup + * @throws PayoutCancellationException + */ + public function cancelPayoutGroup(string $groupId): PayoutGroup + { + $request = []; + try { + $request['token'] = $this->_tokenCache->getTokenByFacade(Facade::Payout); + } catch (Exception $e) { + throw new PayoutCancellationException("Missing facade token"); + } + + try { + $responseJson = $this->_RESTcli->delete("payouts/group/" . $groupId, $request); + + return $this->getPayoutGroupResponse($responseJson, 'cancelled'); + } catch (Exception $e) { + throw new PayoutCancellationException("failed to serialize Payout object : " . $e->getMessage()); + } + } + /** * Notify BitPay Payout. * @@ -2718,4 +2776,26 @@ private function isSmsCodeRequired(bool $autoVerify, string $buyerSms, string $s return $autoVerify == false && (!empty($buyerSms) && empty($smsCode)) || (!empty($smsCode) && empty($buyerSms)); } + + /** + * @param string $responseJson + * @param string $responseType completed/cancelled + * @return PayoutGroup + * @throws Exception + */ + private function getPayoutGroupResponse(string $responseJson, string $responseType): PayoutGroup + { + $mapper = new JsonMapper(); + $response = json_decode($responseJson, true, 512, JSON_THROW_ON_ERROR); + + $payouts = $mapper->mapArray($response[$responseType], [], Payout::class); + + $mapper->bIgnoreVisibility = true; + + $payoutGroup = new PayoutGroup(); + $payoutGroup->setPayouts($payouts); + $payoutGroup->setFailed($mapper->mapArray($response['failed'], [], PayoutGroupFailed::class)); + + return $payoutGroup; + } } diff --git a/src/BitPaySDK/Env.php b/src/BitPaySDK/Env.php index 3e180036..592c5e4a 100644 --- a/src/BitPaySDK/Env.php +++ b/src/BitPaySDK/Env.php @@ -9,7 +9,7 @@ interface Env const TestUrl = "https://test.bitpay.com/"; const ProdUrl = "https://bitpay.com/"; const BitpayApiVersion = "2.0.0"; - const BitpayPluginInfo = "BitPay_PHP_Client_v7.3.1"; + const BitpayPluginInfo = "BitPay_PHP_Client_v7.4.0"; const BitpayApiFrame = "std"; const BitpayApiFrameVersion = "1.0.0"; } diff --git a/src/BitPaySDK/Model/Payout/Payout.php b/src/BitPaySDK/Model/Payout/Payout.php index faad19bb..0034ae53 100644 --- a/src/BitPaySDK/Model/Payout/Payout.php +++ b/src/BitPaySDK/Model/Payout/Payout.php @@ -34,6 +34,7 @@ class Payout protected $_label = ''; protected $_supportPhone = ''; protected $_message = ''; + protected bool $ignoreEmails = false; protected $_percentFee = 0.0; protected $_fee = 0.0; protected $_depositTotal = 0.0; @@ -46,6 +47,7 @@ class Payout protected $_requestDate; protected $_exchangeRates; protected $_transactions; + protected ?string $_groupId = null; /** * Constructor, create a request Payout object. @@ -764,6 +766,48 @@ public function setTransactions(array $transactions) $this->_transactions = $transactions; } + /** + * Gets boolean to prevent email updates on a specific payout. + * Defaults to false if not provided - you will receive emails unless specified to true. + * + * @return bool + */ + public function isIgnoreEmails(): bool + { + return $this->ignoreEmails; + } + + /** + * Sets boolean to prevent email updates on a specific payout. + * Defaults to false if not provided - you will receive emails unless specified to true. + * + * @param bool $ignoreEmails + */ + public function setIgnoreEmails(bool $ignoreEmails): void + { + $this->ignoreEmails = $ignoreEmails; + } + + /** + * Gets group id. + * + * @return string|null + */ + public function getGroupId(): ?string + { + return $this->_groupId; + } + + /** + * Sets group id. + * + * @param string|null $groupId + */ + public function setGroupId(?string $groupId): void + { + $this->_groupId = $groupId; + } + /** * Return Payout values as array. * @@ -788,6 +832,8 @@ public function toArray() 'label' => $this->getLabel(), 'supportPhone' => $this->getSupportPhone(), 'message' => $this->getMessage(), + 'groupId' => $this->getGroupId(), + 'ignoreEmails' => $this->isIgnoreEmails(), 'percentFee' => $this->getPercentFee(), 'fee' => $this->getFee(), 'depositTotal' => $this->getDepositTotal(), diff --git a/src/BitPaySDK/Model/Payout/PayoutGroup.php b/src/BitPaySDK/Model/Payout/PayoutGroup.php new file mode 100644 index 00000000..90e6f276 --- /dev/null +++ b/src/BitPaySDK/Model/Payout/PayoutGroup.php @@ -0,0 +1,52 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + */ + +declare(strict_types=1); + +namespace BitPaySDK\Model\Payout; + +class PayoutGroup +{ + /** + * @var Payout[] + */ + private array $payouts = []; + + private array $failed = []; + + /** + * @return Payout[] + */ + public function getPayouts(): array + { + return $this->payouts; + } + + /** + * @param Payout[] $payouts + */ + public function setPayouts(array $payouts): void + { + $this->payouts = $payouts; + } + + /** + * @return PayoutGroupFailed[] + */ + public function getFailed(): array + { + return $this->failed; + } + + /** + * @param PayoutGroupFailed[] $failed + */ + public function setFailed(array $failed): void + { + $this->failed = $failed; + } +} diff --git a/src/BitPaySDK/Model/Payout/PayoutGroupFailed.php b/src/BitPaySDK/Model/Payout/PayoutGroupFailed.php new file mode 100644 index 00000000..aa21c389 --- /dev/null +++ b/src/BitPaySDK/Model/Payout/PayoutGroupFailed.php @@ -0,0 +1,65 @@ + + * @license http://www.opensource.org/licenses/mit-license.php MIT + */ + +declare(strict_types=1); + +namespace BitPaySDK\Model\Payout; + +class PayoutGroupFailed +{ + private string $errMessage = ''; + private ?string $payoutId; + private ?string $payee; + + /** + * @return string + */ + public function getErrorMessage(): string + { + return $this->errMessage; + } + + /** + * @param string $errMessage + */ + public function setErrorMessage(string $errMessage): void + { + $this->errMessage = $errMessage; + } + + /** + * @return string|null + */ + public function getPayoutId(): ?string + { + return $this->payoutId; + } + + /** + * @param string|null $payoutId + */ + public function setPayoutId(?string $payoutId): void + { + $this->payoutId = $payoutId; + } + + /** + * @return string|null + */ + public function getPayee(): ?string + { + return $this->payee; + } + + /** + * @param string|null $payee + */ + public function setPayee(?string $payee): void + { + $this->payee = $payee; + } +} diff --git a/src/BitPaySDK/Util/JsonMapper/JsonMapper.php b/src/BitPaySDK/Util/JsonMapper/JsonMapper.php index 0f16d5e8..370a4e55 100644 --- a/src/BitPaySDK/Util/JsonMapper/JsonMapper.php +++ b/src/BitPaySDK/Util/JsonMapper/JsonMapper.php @@ -16,6 +16,7 @@ use ReflectionClass; use ReflectionNamedType; +use ReflectionProperty; /** * Automatically map JSON structures into objects. diff --git a/test/unit/BitPaySDK/ClientTest.php b/test/unit/BitPaySDK/ClientTest.php index 62e79047..465ea680 100644 --- a/test/unit/BitPaySDK/ClientTest.php +++ b/test/unit/BitPaySDK/ClientTest.php @@ -67,6 +67,7 @@ class ClientTest extends TestCase { private const CORRUPT_JSON_STRING = '{"code":"USD""name":"US Dollar","rate":21205.85}'; private const CORRECT_JSON_STRING = '[ { "currency": "EUR", "balance": 0 }, { "currency": "USD", "balance": 2389.82 }, { "currency": "BTC", "balance": 0.000287 } ]'; + private const PAYOUT_TOKEN = 'testPayoutApiToken'; protected function setUp(): void { @@ -2328,6 +2329,81 @@ public function testSubmitPayoutShouldCatchJsonMapperException($testedObject) $testedObject->submitPayout($payoutMock); } + /** + * @depends testWithFileJsonConfig + */ + public function testCreatePayoutGroup(Client $testedObject): void + { + + $notificationURL = 'https://yournotiticationURL.com/wed3sa0wx1rz5bg0bv97851eqx'; + $shopperId = '7qohDf2zZnQK5Qanj8oyC2'; + + $payout = new Payout(); + $payout->setAmount(10); + $payout->setCurrency(Currency::USD); + $payout->setLedgerCurrency(Currency::USD); + $payout->setReference('payout_20210527'); + $payout->setNotificationEmail('merchant@email.com'); + $payout->setNotificationURL($notificationURL); + $payout->setEmail('john@doe.com'); + $payout->setRecipientId('LDxRZCGq174SF8AnQpdBPB'); + $payout->setShopperId($shopperId); + + $expectedRequest['instructions'][] = $payout->toArray(); + $expectedRequest['token'] = self::PAYOUT_TOKEN; + + $restCliMock = $this->getRestCliMock(); + $restCliMock->expects(self::once())->method('post') + ->with("payouts/group", $expectedRequest, true) + ->willReturn(file_get_contents(__DIR__ . '/json/createPayoutGroupResponse.json', true)); + $setRestCli = function () use ($restCliMock) { + $this->_RESTcli = $restCliMock; + }; + $doSetRestCli = $setRestCli->bindTo($testedObject, get_class($testedObject)); + $doSetRestCli(); + + $result = $testedObject->createPayoutGroup([$payout]); + $firstPayout = $result->getPayouts()[0]; + $firstFailed = $result->getFailed()[0]; + + self::assertCount(1, $result->getPayouts()); + self::assertEquals($notificationURL, $firstPayout->getNotificationURL()); + self::assertEquals($shopperId, $firstPayout->getShopperId()); + self::assertEquals('Ledger currency is required', $firstFailed->getErrorMessage()); + self::assertEquals('john@doe.com', $firstFailed->getPayee()); + } + + /** + * @depends testWithFileJsonConfig + * @throws PayoutCancellationException + */ + public function testCancelPayoutGroup(Client $testedObject): void + { + $groupId = '12345'; + $restCliMock = $this->getRestCliMock(); + $restCliMock->expects(self::once())->method('delete') + ->with("payouts/group/" . $groupId, ['token' => self::PAYOUT_TOKEN]) + ->willReturn(file_get_contents(__DIR__ . '/json/cancelPayoutGroupResponse.json', true)); + $setRestCli = function () use ($restCliMock) { + $this->_RESTcli = $restCliMock; + }; + $doSetRestCli = $setRestCli->bindTo($testedObject, get_class($testedObject)); + $doSetRestCli(); + + $result = $testedObject->cancelPayoutGroup($groupId); + $firstPayout = $result->getPayouts()[0]; + $firstFailed = $result->getFailed()[0]; + + self::assertCount(2, $result->getPayouts()); + self::assertEquals( + 'https://yournotiticationURL.com/wed3sa0wx1rz5bg0bv97851eqx', + $firstPayout->getNotificationURL() + ); + self::assertEquals('7qohDf2zZnQK5Qanj8oyC2', $firstPayout->getShopperId()); + self::assertEquals('PayoutId is missing or invalid', $firstFailed->getErrorMessage()); + self::assertEquals('D8tgWzn1psUua4NYWW1vYo', $firstFailed->getPayoutId()); + } + /** * @depends testWithFileJsonConfig */ diff --git a/test/unit/BitPaySDK/json/cancelPayoutGroupResponse.json b/test/unit/BitPaySDK/json/cancelPayoutGroupResponse.json new file mode 100644 index 00000000..846e9419 --- /dev/null +++ b/test/unit/BitPaySDK/json/cancelPayoutGroupResponse.json @@ -0,0 +1,46 @@ +{ + "cancelled": [ + { + "id": "JMwv8wQCXANoU2ZZQ9a9GH", + "recipientId": "LDxRZCGq174SF8AnQpdBPB", + "accountId": "2tRxwvX5JkVbhqBLGyanmF", + "shopperId": "7qohDf2zZnQK5Qanj8oyC2", + "amount": 10, + "currency": "USD", + "ledgerCurrency": "GBP", + "email": "john@doe.com", + "reference": "payout_20210527", + "label": "John Doe", + "notificationURL": "https://yournotiticationURL.com/wed3sa0wx1rz5bg0bv97851eqx", + "notificationEmail": "merchant@email.com", + "effectiveDate": "2021-05-27T09:00:00.000Z", + "requestDate": "2021-05-27T10:47:37.834Z", + "status": "cancelled", + "transactions": [] + }, + { + "id": "JMwv8wQCXANoU2ZZQ9a9GH", + "recipientId": "LDxRZCGq174SF8AnQpdBPB", + "accountId": "2tRxwvX5JkVbhqBLGyanmF", + "shopperId": "7qohDf2zZnQK5Qanj8oyC2", + "amount": 20, + "currency": "USD", + "ledgerCurrency": "GBP", + "email": "john@doe.com", + "reference": "payout_20210527", + "label": "John Doe 2", + "notificationURL": "https://yournotiticationURL.com/wed3sa0wx1rz5bg0bv97851eqx", + "notificationEmail": "merchant@email.com", + "effectiveDate": "2021-05-27T09:00:00.000Z", + "requestDate": "2021-05-27T10:47:37.834Z", + "status": "cancelled", + "transactions": [] + } + ], + "failed": [ + { + "errMessage": "PayoutId is missing or invalid", + "payoutId": "D8tgWzn1psUua4NYWW1vYo" + } + ] +} \ No newline at end of file diff --git a/test/unit/BitPaySDK/json/createPayoutGroupRequest.json b/test/unit/BitPaySDK/json/createPayoutGroupRequest.json new file mode 100644 index 00000000..bd863870 --- /dev/null +++ b/test/unit/BitPaySDK/json/createPayoutGroupRequest.json @@ -0,0 +1 @@ +{"instructions":[{"amount":10,"currency":"USD","ledgerCurrency":"USD","reference":"payout_20210527","notificationURL":"https:\/\/yournotiticationURL.com\/wed3sa0wx1rz5bg0bv97851eqx","notificationEmail":"merchant@email.com","email":"john@doe.com","recipientId":"LDxRZCGq174SF8AnQpdBPB","shopperId":"7qohDf2zZnQK5Qanj8oyC2"}],"token":"kQLZ7C9YKPSnMCC4EJwrqRHXuQkLzL1W8DfZCh37DHb"} \ No newline at end of file diff --git a/test/unit/BitPaySDK/json/createPayoutGroupResponse.json b/test/unit/BitPaySDK/json/createPayoutGroupResponse.json new file mode 100644 index 00000000..3fe71abc --- /dev/null +++ b/test/unit/BitPaySDK/json/createPayoutGroupResponse.json @@ -0,0 +1,29 @@ +{ + "completed": [ + { + "id": "JMwv8wQCXANoU2ZZQ9a9GH", + "recipientId": "LDxRZCGq174SF8AnQpdBPB", + "accountId": "SJcWZCFq344DL8QnXpdBNM", + "shopperId": "7qohDf2zZnQK5Qanj8oyC2", + "amount": 10, + "currency": "USD", + "ledgerCurrency": "GBP", + "email": "john@doe.com", + "reference": "payout_20210527", + "label": "John Doe", + "notificationURL": "https://yournotiticationURL.com/wed3sa0wx1rz5bg0bv97851eqx", + "notificationEmail": "merchant@email.com", + "effectiveDate": "2021-05-27T09:00:00.000Z", + "requestDate": "2021-05-27T10:47:37.834Z", + "status": "new", + "groupId": "SxKRk4MdW8Ae3vsoR6UPQE", + "transactions": [] + } + ], + "failed": [ + { + "errMessage": "Ledger currency is required", + "payee": "john@doe.com" + } + ] +} \ No newline at end of file From b9b014a8e336f79dc6f0f820b9a807a2e495896c Mon Sep 17 00:00:00 2001 From: Marcin Warzybok Date: Mon, 19 Jun 2023 11:16:05 +0200 Subject: [PATCH 2/2] SP-553 Payout Groups - PHP - add fixes after improve API --- src/BitPaySDK/Client.php | 2 +- test/unit/BitPaySDK/json/createPayoutGroupResponse.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BitPaySDK/Client.php b/src/BitPaySDK/Client.php index 6b043444..7ce8caa1 100644 --- a/src/BitPaySDK/Client.php +++ b/src/BitPaySDK/Client.php @@ -1943,7 +1943,7 @@ public function createPayoutGroup(array $payouts): PayoutGroup } $responseJson = $this->_RESTcli->post("payouts/group", $request); - return $this->getPayoutGroupResponse($responseJson, 'completed'); + return $this->getPayoutGroupResponse($responseJson, 'created'); } catch (Exception $e) { throw new PayoutCreationException("failed to serialize Payout object : " . $e->getMessage()); } diff --git a/test/unit/BitPaySDK/json/createPayoutGroupResponse.json b/test/unit/BitPaySDK/json/createPayoutGroupResponse.json index 3fe71abc..e58ea459 100644 --- a/test/unit/BitPaySDK/json/createPayoutGroupResponse.json +++ b/test/unit/BitPaySDK/json/createPayoutGroupResponse.json @@ -1,5 +1,5 @@ { - "completed": [ + "created": [ { "id": "JMwv8wQCXANoU2ZZQ9a9GH", "recipientId": "LDxRZCGq174SF8AnQpdBPB",