From 6ac691d559cca0ea8d0cff4988f8041b2310a444 Mon Sep 17 00:00:00 2001 From: Antonio Buedo Date: Mon, 19 Apr 2021 14:22:45 +0200 Subject: [PATCH] - Payouts new functionality added (#73) --- BitPay/BitPay.cs | 160 +++++++++++++++++- .../Exceptions/PayoutCancellationException.cs | 18 ++ BitPay/Exceptions/PayoutCreationException.cs | 18 ++ BitPay/Exceptions/PayoutException.cs | 28 +++ BitPay/Exceptions/PayoutQueryException.cs | 18 ++ BitPay/KeyUtils.cs | 18 +- BitPay/Models/Payout/PayoutInstruction.cs | 66 ++++++-- BitPay/Models/Payout/PayoutRecipient.cs | 82 +++++++++ BitPay/Models/Payout/PayoutRecipients.cs | 35 ++++ .../Models/Payout/RecipientReferenceMethod.cs | 9 + BitPay/Models/Payout/RecipientStatus.cs | 12 ++ BitPayXUnitTest/BitPayTests.cs | 64 ++++++- 12 files changed, 500 insertions(+), 28 deletions(-) create mode 100644 BitPay/Exceptions/PayoutCancellationException.cs create mode 100644 BitPay/Exceptions/PayoutCreationException.cs create mode 100644 BitPay/Exceptions/PayoutException.cs create mode 100644 BitPay/Exceptions/PayoutQueryException.cs create mode 100644 BitPay/Models/Payout/PayoutRecipient.cs create mode 100644 BitPay/Models/Payout/PayoutRecipients.cs create mode 100644 BitPay/Models/Payout/RecipientReferenceMethod.cs create mode 100644 BitPay/Models/Payout/RecipientStatus.cs diff --git a/BitPay/BitPay.cs b/BitPay/BitPay.cs index 62ab92d..4ebb4fa 100644 --- a/BitPay/BitPay.cs +++ b/BitPay/BitPay.cs @@ -715,6 +715,158 @@ public async Task> GetLedgers() } } + /** + * Submit BitPay Payout Recipients. + * + * @param recipients PayoutRecipients A PayoutRecipients object with request parameters defined. + * @return array A list of BitPay PayoutRecipients objects.. + * @throws BitPayException BitPayException class + * @throws PayoutCreationException PayoutCreationException class + */ + public async Task> SubmitPayoutRecipients(PayoutRecipients recipients) + { + try + { + recipients.Token = GetAccessToken(Facade.Payroll); + recipients.Guid = Guid.NewGuid().ToString(); + var json = JsonConvert.SerializeObject(recipients); + var response = await Post("recipients", json, true); + + var responseString = await ResponseToJsonString(response); + return JsonConvert.DeserializeObject>(responseString, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + } + catch (Exception ex) + { + if (!(ex.GetType().IsSubclassOf(typeof(BitPayException)) || ex.GetType() == typeof(BitPayException))) + throw new PayoutCreationException(ex); + + throw; + } + } + + /** + * Retrieve a collection of BitPay Payout Recipients. + * + * @param $status string|null The recipient status you want to query on. + * @param $limit int|null Maximum results that the query will return (useful for paging results). + * result). + * @return array A list of BitPayRecipient objects. + * @throws BitPayException BitPayException class + */ + public async Task> GetPayoutRecipients(string status = null, int limit = 100) + { + try + { + var parameters = InitParams(); + if (!string.IsNullOrEmpty(status)) + { + parameters.Add("status", status); + } + parameters.Add("limit", limit.ToString()); + + parameters.Add("token", GetAccessToken(Facade.Payroll)); + var response = await Get("recipients", parameters); + var responseString = await ResponseToJsonString(response); + return JsonConvert.DeserializeObject>(responseString, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + } + catch (Exception ex) + { + if (!(ex.GetType().IsSubclassOf(typeof(BitPayException)) || ex.GetType() == typeof(BitPayException))) + throw new PayoutQueryException(); + + throw; + } + } + + /** + * Retrieve a BitPay payout recipient by batch id using. The client must have been previously authorized for the + * payroll facade. + * + * @param $recipientId string The id of the recipient to retrieve. + * @return PayoutRecipient A BitPay PayoutRecipient object. + * @throws PayoutQueryException BitPayException class + */ + public async Task GetPayoutRecipient(string batchId) + { + try + { + Dictionary parameters; + try + { + parameters = new Dictionary {{"token", GetAccessToken(Facade.Payroll)}}; + } + catch (BitPayException) + { + // No token for batch. + parameters = null; + } + + var response = await Get("recipients/" + batchId, parameters); + var responseString = await ResponseToJsonString(response); + return JsonConvert.DeserializeObject(responseString, + new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + } + catch (Exception ex) + { + if (!(ex.GetType().IsSubclassOf(typeof(BitPayException)) || ex.GetType() == typeof(BitPayException))) + throw new PayoutQueryException(ex); + + throw; + } + } + + /** + * Cancel a BitPay Payout recipient. + * + * @param $recipientId string The id of the recipient to cancel. + * @return PayoutRecipient A BitPay generated PayoutRecipient object. + * @throws PayoutCancellationException BitPayException class + */ + public async Task DeletePayoutRecipient(string batchId) + { + try + { + bool result; + var b = await GetPayoutRecipient(batchId); + + Dictionary parameters; + try + { + parameters = new Dictionary {{"token", GetAccessToken(Facade.Payroll)}}; + } + catch (BitPayException) + { + // No token for batch. + parameters = null; + } + + var response = await Delete("recipients/" + batchId, parameters); + var responseString = await ResponseToJsonString(response); + JObject responseObject = JsonConvert.DeserializeObject(responseString); + bool.TryParse(responseObject.GetValue("success").ToString(), out result); + + return result; + } + catch (Exception ex) + { + if (!(ex.GetType().IsSubclassOf(typeof(BitPayException)) || ex.GetType() == typeof(BitPayException))) + throw new PayoutCancellationException(); + + throw; + } + } + /// /// Submit a BitPay Payout batch. /// @@ -744,7 +896,7 @@ public async Task SubmitPayoutBatch(PayoutBatch batch) catch (Exception ex) { if (!(ex.GetType().IsSubclassOf(typeof(BitPayException)) || ex.GetType() == typeof(BitPayException))) - throw new BatchException(ex); + throw new PayoutCreationException(ex); throw; } @@ -777,7 +929,7 @@ public async Task> GetPayoutBatches(string status = null) catch (Exception ex) { if (!(ex.GetType().IsSubclassOf(typeof(BitPayException)) || ex.GetType() == typeof(BitPayException))) - throw new BatchException(ex); + throw new PayoutQueryException(); throw; } @@ -815,7 +967,7 @@ public async Task GetPayoutBatch(string batchId) catch (Exception ex) { if (!(ex.GetType().IsSubclassOf(typeof(BitPayException)) || ex.GetType() == typeof(BitPayException))) - throw new BatchException(ex); + throw new PayoutQueryException(ex); throw; } @@ -854,7 +1006,7 @@ public async Task CancelPayoutBatch(string batchId) catch (Exception ex) { if (!(ex.GetType().IsSubclassOf(typeof(BitPayException)) || ex.GetType() == typeof(BitPayException))) - throw new BatchException(ex); + throw new PayoutCancellationException(); throw; } diff --git a/BitPay/Exceptions/PayoutCancellationException.cs b/BitPay/Exceptions/PayoutCancellationException.cs new file mode 100644 index 0000000..3ef13f0 --- /dev/null +++ b/BitPay/Exceptions/PayoutCancellationException.cs @@ -0,0 +1,18 @@ +using System; + +namespace BitPaySDK.Exceptions +{ + public class PayoutCancellationException : BitPayException + { + private const string BitPayCode = "BITPAY-PAYOUT-DELETE"; + private const string BitPayMessage = "Failed to create payout batch"; + + public PayoutCancellationException() : base(BitPayCode, BitPayMessage) + { + } + + public PayoutCancellationException(Exception ex) : base(BitPayCode, BitPayMessage, ex) + { + } + } +} \ No newline at end of file diff --git a/BitPay/Exceptions/PayoutCreationException.cs b/BitPay/Exceptions/PayoutCreationException.cs new file mode 100644 index 0000000..c77cf66 --- /dev/null +++ b/BitPay/Exceptions/PayoutCreationException.cs @@ -0,0 +1,18 @@ +using System; + +namespace BitPaySDK.Exceptions +{ + public class PayoutCreationException : BitPayException + { + private const string BitPayCode = "BITPAY-PAYOUT-CREATE"; + private const string BitPayMessage = "Failed to create payout batch"; + + public PayoutCreationException() : base(BitPayCode, BitPayMessage) + { + } + + public PayoutCreationException(Exception ex) : base(BitPayCode, BitPayMessage, ex) + { + } + } +} \ No newline at end of file diff --git a/BitPay/Exceptions/PayoutException.cs b/BitPay/Exceptions/PayoutException.cs new file mode 100644 index 0000000..ed81d2a --- /dev/null +++ b/BitPay/Exceptions/PayoutException.cs @@ -0,0 +1,28 @@ +using System; + +namespace BitPaySDK.Exceptions +{ + public class PayoutException : BitPayException + { + private const string BitPayMessage = "An unexpected error occured while trying to manage the payout batch"; + private readonly string _bitpayCode = "BITPAY-PAYOUT-GENERIC"; + + public PayoutException() : base(BitPayMessage) + { + BitpayCode = _bitpayCode; + } + + public PayoutException(Exception ex) : base(BitPayMessage, ex) + { + BitpayCode = _bitpayCode; + } + + public PayoutException(string bitpayCode, string message) : base(bitpayCode, message) + { + } + + public PayoutException(string bitpayCode, string message, Exception cause) : base(bitpayCode, message, cause) + { + } + } +} \ No newline at end of file diff --git a/BitPay/Exceptions/PayoutQueryException.cs b/BitPay/Exceptions/PayoutQueryException.cs new file mode 100644 index 0000000..287a23f --- /dev/null +++ b/BitPay/Exceptions/PayoutQueryException.cs @@ -0,0 +1,18 @@ +using System; + +namespace BitPaySDK.Exceptions +{ + public class PayoutQueryException : BitPayException + { + private const string BitPayCode = "BITPAY-PAYOUT-GET"; + private const string BitPayMessage = "Failed to create payout batch"; + + public PayoutQueryException() : base(BitPayCode, BitPayMessage) + { + } + + public PayoutQueryException(Exception ex) : base(BitPayCode, BitPayMessage, ex) + { + } + } +} \ No newline at end of file diff --git a/BitPay/KeyUtils.cs b/BitPay/KeyUtils.cs index 7e4b327..abec38f 100644 --- a/BitPay/KeyUtils.cs +++ b/BitPay/KeyUtils.cs @@ -47,13 +47,17 @@ public static EcKey CreateEcKeyFromHexStringFile(string privKeyFile) public static async Task LoadEcKey() { - using (var fs = File.OpenRead(PrivateKeyFile)) - { - var b = new byte[1024]; - await fs.ReadAsync(b, 0, b.Length); - var key = EcKey.FromAsn1(b); - return key; - } + // using (var fs = File.OpenRead(PrivateKeyFile)) + // { + // var b = new byte[1024]; + // await fs.ReadAsync(b, 0, b.Length); + // var key = EcKey.FromAsn1(b); + // return key; + // } + + byte[] file = System.IO.File.ReadAllBytes(PrivateKeyFile); + var key = EcKey.FromAsn1(file); + return key; } public static string GetKeyStringFromFile(string filename) diff --git a/BitPay/Models/Payout/PayoutInstruction.cs b/BitPay/Models/Payout/PayoutInstruction.cs index 4d85c75..b2e8526 100644 --- a/BitPay/Models/Payout/PayoutInstruction.cs +++ b/BitPay/Models/Payout/PayoutInstruction.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using BitPaySDK.Exceptions; using Newtonsoft.Json; namespace BitPaySDK.Models.Payout @@ -12,22 +14,50 @@ public PayoutInstruction() { } - /// - /// Constructor, create a PayoutInstruction object. - /// - /// BTC amount. - /// Bitcoin address. - public PayoutInstruction(double amount, string address) + /** + * Constructor, create a PayoutInstruction object. + * + * @param amount float amount (in currency of batch). + * @param method int Method used to target the recipient. + * @param methodValue string value for the choosen target method. + * @throws PayoutCreationException BitPayException class + */ + public PayoutInstruction(double amount, int method, string methodValue) { - Amount = amount; - Address = address; + try + { + Amount = amount; + switch (method) { + case RecipientReferenceMethod.EMAIL: + Email = methodValue; + break; + case RecipientReferenceMethod.RECIPIENT_ID: + RecipientId = methodValue; + break; + case RecipientReferenceMethod.SHOPPER_ID: + ShopperId = methodValue; + break; + default: + throw new PayoutCreationException(); + } + } + catch (Exception e) + { + throw new BitPayException(e); + } } [JsonProperty(PropertyName = "amount")] public double Amount { get; set; } - [JsonProperty(PropertyName = "address")] - public string Address { get; set; } + [JsonProperty(PropertyName = "email")] + public string Email { get; set; } + + [JsonProperty(PropertyName = "recipientId")] + public string RecipientId { get; set; } + + [JsonProperty(PropertyName = "shopperId")] + public string ShopperId { get; set; } [JsonProperty(PropertyName = "label")] public string Label { get; set; } @@ -61,9 +91,19 @@ public bool ShouldSerializeBtc() return false; } - public bool ShouldSerializeAddress() + public bool ShouldSerializeEmail() + { + return !string.IsNullOrEmpty(Email); + } + + public bool ShouldSerializeRecipientId() + { + return !string.IsNullOrEmpty(RecipientId); + } + + public bool ShouldSerializeShopperId() { - return !string.IsNullOrEmpty(Address); + return !string.IsNullOrEmpty(ShopperId); } public bool ShouldSerializeTransactions() diff --git a/BitPay/Models/Payout/PayoutRecipient.cs b/BitPay/Models/Payout/PayoutRecipient.cs new file mode 100644 index 0000000..0bd4924 --- /dev/null +++ b/BitPay/Models/Payout/PayoutRecipient.cs @@ -0,0 +1,82 @@ +using Newtonsoft.Json; + +namespace BitPaySDK.Models.Payout +{ + public class PayoutRecipient + { + /** + * Constructor, create a minimal Recipient object. + * + * @param email string Recipient email address to which the invite shall be sent. + * @param label string Recipient nickname assigned by the merchant (Optional). + * @param notificationURL string URL to which BitPay sends webhook notifications to inform the merchant about the + * status of a given recipient. HTTPS is mandatory (Optional). + */ + public PayoutRecipient(string email, string label, string notificationURL) { + Email = email; + Label = label; + NotificationURL = notificationURL; + } + + // Required fields + // + + [JsonProperty(PropertyName = "email")] + public string Email { get; set; } + + // Optional fields + // + + [JsonProperty(PropertyName = "label")] + public string Label { get; set; } + + [JsonProperty(PropertyName = "notificationURL")] + public string NotificationURL { get; set; } + + // Response fields + // + + public string Status { get; set; } + + public string Id { get; set; } + + public string ShopperId { get; set; } + + public string Token { get; set; } + + public bool ShouldSerializeEmail() + { + return !string.IsNullOrEmpty(Email); + } + + public bool ShouldSerializeLabel() + { + return !string.IsNullOrEmpty(Label); + } + + public bool ShouldSerializeNotificationURL() + { + return !string.IsNullOrEmpty(NotificationURL); + } + + public bool ShouldSerializeStatus() + { + return false; + } + + public bool ShouldSerializeId() + { + return false; + } + + public bool ShouldSerializeShopperId() + { + return false; + } + + public bool ShouldSerializeToken() + { + return false; + } + } +} \ No newline at end of file diff --git a/BitPay/Models/Payout/PayoutRecipients.cs b/BitPay/Models/Payout/PayoutRecipients.cs new file mode 100644 index 0000000..3e86b63 --- /dev/null +++ b/BitPay/Models/Payout/PayoutRecipients.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace BitPaySDK.Models.Payout +{ + public class PayoutRecipients + { + /** + * Constructor, create an recipient-full request PayoutBatch object. + * + * @param recipients array array of JSON objects, with containing the following parameters. + */ + public PayoutRecipients(List recipients) { + Recipients = recipients; + } + + // API fields + // + + [JsonProperty(PropertyName = "guid")] public string Guid { get; set; } + + [JsonProperty(PropertyName = "token")] public string Token { get; set; } + + // Required fields + // + + [JsonProperty(PropertyName = "recipients")] + public List Recipients { get; set; } + + public bool ShouldSerializeRecipients() + { + return true; + } + } +} \ No newline at end of file diff --git a/BitPay/Models/Payout/RecipientReferenceMethod.cs b/BitPay/Models/Payout/RecipientReferenceMethod.cs new file mode 100644 index 0000000..c05a474 --- /dev/null +++ b/BitPay/Models/Payout/RecipientReferenceMethod.cs @@ -0,0 +1,9 @@ +namespace BitPaySDK.Models.Payout +{ + public static class RecipientReferenceMethod + { + public const int EMAIL = 1; + public const int RECIPIENT_ID = 2; + public const int SHOPPER_ID = 3; + } +} \ No newline at end of file diff --git a/BitPay/Models/Payout/RecipientStatus.cs b/BitPay/Models/Payout/RecipientStatus.cs new file mode 100644 index 0000000..fb48a87 --- /dev/null +++ b/BitPay/Models/Payout/RecipientStatus.cs @@ -0,0 +1,12 @@ +namespace BitPaySDK.Models.Payout +{ + public static class RecipientStatus + { + public const string INVITED = "invited"; + public const string UNVERIFIED = "unverified"; + public const string VERIFIED = "verified"; + public const string ACTIVE = "active"; + public const string PAUSED = "paused"; + public const string REMOVED = "removed"; + } +} \ No newline at end of file diff --git a/BitPayXUnitTest/BitPayTests.cs b/BitPayXUnitTest/BitPayTests.cs index 39fd7b5..3784aa3 100644 --- a/BitPayXUnitTest/BitPayTests.cs +++ b/BitPayXUnitTest/BitPayTests.cs @@ -329,6 +329,62 @@ public async Task TestShouldGetLedgers() { } + [Fact] + public async Task testShouldSubmitPayoutRecipients() { + List recipientsList = new List(); + recipientsList.Add( new PayoutRecipient( + "sandbox+recipient1@bitpay.com", + "recipient1", + "https://hookb.in/wNDlQMV7WMFz88VDyGnJ")); + recipientsList.Add( new PayoutRecipient( + "sandbox+recipient2@bitpay.com", + "recipient2", + "https://hookb.in/QJOPBdMgRkukpp2WO60o")); + recipientsList.Add( new PayoutRecipient( + "sandbox+recipient3@bitpay.com", + "recipient3", + "https://hookb.in/QJOPBdMgRkukpp2WO60o")); + + var recipientsObj = new PayoutRecipients(recipientsList); + List recipients = await _bitpay.SubmitPayoutRecipients(recipientsObj); + + Assert.NotNull(recipients); + Assert.Equal(recipients[0].Email, "sandbox+recipient1@bitpay.com"); + } + + [Fact] + public async Task testShouldGetPayoutRecipientId() { + List recipientsList = new List(); + recipientsList.Add( new PayoutRecipient( + "sandbox+recipient1@bitpay.com", + "recipient1", + "https://hookb.in/wNDlQMV7WMFz88VDyGnJ")); + recipientsList.Add( new PayoutRecipient( + "sandbox+recipient2@bitpay.com", + "recipient2", + "https://hookb.in/QJOPBdMgRkukpp2WO60o")); + recipientsList.Add( new PayoutRecipient( + "sandbox+recipient3@bitpay.com", + "recipient3", + "https://hookb.in/QJOPBdMgRkukpp2WO60o")); + + PayoutRecipients recipientsObj = new PayoutRecipients(recipientsList); + var recipients = await _bitpay.SubmitPayoutRecipients(recipientsObj); + var firstRecipient = recipients.First(); + var retrieved = await _bitpay.GetPayoutRecipient(firstRecipient.Id); + + Assert.NotNull(firstRecipient); + Assert.NotNull(retrieved.Id); + Assert.Equal(firstRecipient.Id, retrieved.Id); + Assert.Equal(firstRecipient.Email, "sandbox+recipient1@bitpay.com"); + } + + [Fact] + public async Task testShouldGetPayoutRecipients() { + var recipients = await _bitpay.GetPayoutRecipients(null, 2); + Assert.Equal(2, recipients.Count); + } + [Fact] public async Task TestShouldSubmitPayoutBatch() { @@ -338,8 +394,8 @@ public async Task TestShouldSubmitPayoutBatch() { var effectiveDate = threeDaysFromNow; var currency = Currency.USD; var instructions = new List() { - new PayoutInstruction(100.0, "mtHDtQtkEkRRB5mgeWpLhALsSbga3iZV6u"), - new PayoutInstruction(200.0, "mvR4Xj7MYT7GJcL93xAQbSZ2p4eHJV5F7A") + new PayoutInstruction(100.0, RecipientReferenceMethod.EMAIL, "sandbox+recipient1@bitpay.com"), + new PayoutInstruction(100.0, RecipientReferenceMethod.EMAIL, "sandbox+recipient1@bitpay.com") }; var batch = new PayoutBatch(currency, effectiveDate, instructions); @@ -358,8 +414,8 @@ public async Task TestShouldSubmitGetAndDeletePayoutBatch() { var effectiveDate = threeDaysFromNow; var currency = Currency.USD; var instructions = new List() { - new PayoutInstruction(100.0, "mtHDtQtkEkRRB5mgeWpLhALsSbga3iZV6u"), - new PayoutInstruction(200.0, "mvR4Xj7MYT7GJcL93xAQbSZ2p4eHJV5F7A") + new PayoutInstruction(100.0, RecipientReferenceMethod.EMAIL, "sandbox+recipient1@bitpay.com"), + new PayoutInstruction(100.0, RecipientReferenceMethod.EMAIL, "sandbox+recipient1@bitpay.com") }; var batch0 = new PayoutBatch(currency, effectiveDate, instructions);