diff --git a/src/BitPaySDK/Client.php b/src/BitPaySDK/Client.php
index 01e34d88..7ce8caa1 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, 'created');
+ } 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..e58ea459
--- /dev/null
+++ b/test/unit/BitPaySDK/json/createPayoutGroupResponse.json
@@ -0,0 +1,29 @@
+{
+ "created": [
+ {
+ "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