From 4c84eeca5bfebce64a2df3b35402d58c966fb384 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Fri, 22 Feb 2019 08:49:11 -0500 Subject: [PATCH] bitpay ipn for processing credits --- src/Billing/Controllers/BitPayController.cs | 128 ++++++++++++++++++-- src/Billing/Controllers/PayPalController.cs | 3 +- src/Billing/Models/BitPayEventModel.cs | 28 +++++ src/Core/Enums/GatewayType.cs | 4 +- 4 files changed, 152 insertions(+), 11 deletions(-) create mode 100644 src/Billing/Models/BitPayEventModel.cs diff --git a/src/Billing/Controllers/BitPayController.cs b/src/Billing/Controllers/BitPayController.cs index 5159212467..a933faa57e 100644 --- a/src/Billing/Controllers/BitPayController.cs +++ b/src/Billing/Controllers/BitPayController.cs @@ -1,11 +1,16 @@ -using Bit.Core.Repositories; +using Bit.Billing.Models; +using Bit.Core.Enums; +using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using System; +using System.Data.SqlClient; using System.IO; using System.Text; using System.Threading.Tasks; +using System.Transactions; namespace Bit.Billing.Controllers { @@ -39,30 +44,137 @@ namespace Bit.Billing.Controllers } [HttpPost("ipn")] - public async Task PostIpn([FromQuery] string key) + public async Task PostIpn([FromBody] BitPayEventModel model, [FromQuery] string key) { if(key != _billingSettings.BitPayWebhookKey) { return new BadRequestResult(); } - - if(HttpContext?.Request == null) + if(model == null || string.IsNullOrWhiteSpace(model.Data?.Id) || + string.IsNullOrWhiteSpace(model.Event?.Name)) { return new BadRequestResult(); } - string body = null; - using(var reader = new StreamReader(HttpContext.Request.Body, Encoding.UTF8)) + if(model.Event.Name != "invoice_confirmed") { - body = await reader.ReadToEndAsync(); + // Only processing confirmed invoice events for now. + return new OkResult(); } - if(string.IsNullOrWhiteSpace(body)) + var invoice = await _bitPayClient.GetInvoiceAsync(model.Data.Id); + if(invoice == null || invoice.Status != "confirmed") { + // Request forged...? return new BadRequestResult(); } + if(invoice.Currency != "USD") + { + // Only process USD payments + return new OkResult(); + } + + var ids = GetIdsFromPosData(invoice); + if(!ids.Item1.HasValue && !ids.Item2.HasValue) + { + return new OkResult(); + } + + var isAccountCredit = IsAccountCredit(invoice); + if(!isAccountCredit) + { + // Only processing credits + return new OkResult(); + } + + var transaction = await _transactionRepository.GetByGatewayIdAsync(GatewayType.BitPay, invoice.Id); + if(transaction != null) + { + return new OkResult(); + } + + try + { + var tx = new Core.Models.Table.Transaction + { + Amount = invoice.Price, + CreationDate = invoice.CurrentTime.Date, + OrganizationId = ids.Item1, + UserId = ids.Item2, + Type = TransactionType.Charge, + Gateway = GatewayType.BitPay, + GatewayId = invoice.Id, + PaymentMethodType = PaymentMethodType.BitPay, + Details = invoice.Id + }; + await _transactionRepository.CreateAsync(tx); + + if(isAccountCredit) + { + if(tx.OrganizationId.HasValue) + { + var org = await _organizationRepository.GetByIdAsync(tx.OrganizationId.Value); + if(org != null) + { + if(await _paymentService.CreditAccountAsync(org, tx.Amount)) + { + await _organizationRepository.ReplaceAsync(org); + } + } + } + else + { + var user = await _userRepository.GetByIdAsync(tx.UserId.Value); + if(user != null) + { + if(await _paymentService.CreditAccountAsync(user, tx.Amount)) + { + await _userRepository.ReplaceAsync(user); + } + } + } + + // TODO: Send email about credit added? + } + } + // Catch foreign key violations because user/org could have been deleted. + catch(SqlException e) when(e.Number == 547) { } + return new OkResult(); } + + private bool IsAccountCredit(NBitpayClient.Invoice invoice) + { + return invoice != null && invoice.PosData != null && invoice.PosData.Contains("accountCredit:1"); + } + + public Tuple GetIdsFromPosData(NBitpayClient.Invoice invoice) + { + Guid? orgId = null; + Guid? userId = null; + + if(invoice != null && !string.IsNullOrWhiteSpace(invoice.PosData) && invoice.PosData.Contains(":")) + { + var mainParts = invoice.PosData.Split(','); + foreach(var mainPart in mainParts) + { + var parts = mainPart.Split(':'); + if(parts.Length > 1 && Guid.TryParse(parts[1], out var id)) + { + if(parts[0] == "userId") + { + userId = id; + } + else if(parts[0] == "organizationId") + { + orgId = id; + } + } + } + } + + return new Tuple(orgId, userId); + } } } diff --git a/src/Billing/Controllers/PayPalController.cs b/src/Billing/Controllers/PayPalController.cs index 3ec2482738..9e001d8bfd 100644 --- a/src/Billing/Controllers/PayPalController.cs +++ b/src/Billing/Controllers/PayPalController.cs @@ -206,7 +206,8 @@ namespace Bit.Billing.Controllers if(ipnTransaction.McCurrency != "USD") { - return new BadRequestResult(); + // Only process USD payments + return new OkResult(); } var ids = ipnTransaction.GetIdsFromCustom(); diff --git a/src/Billing/Models/BitPayEventModel.cs b/src/Billing/Models/BitPayEventModel.cs new file mode 100644 index 0000000000..b7ed06462d --- /dev/null +++ b/src/Billing/Models/BitPayEventModel.cs @@ -0,0 +1,28 @@ +namespace Bit.Billing.Models +{ + public class BitPayEventModel + { + public EventModel Event { get; set; } + public InvoiceDataModel Data { get; set; } + + public class EventModel + { + public int Code { get; set; } + public string Name { get; set; } + } + + public class InvoiceDataModel + { + public string Id { get; set; } + public string Url { get; set; } + public string Status { get; set; } + public string Currency { get; set; } + public decimal Price { get; set; } + public string PosData { get; set; } + public bool ExceptionStatus { get; set; } + public long CurrentTime { get; set; } + public long AmountPaid { get; set; } + public string TransactionCurrency { get; set; } + } + } +} diff --git a/src/Core/Enums/GatewayType.cs b/src/Core/Enums/GatewayType.cs index bb0e88f365..fbaf72df36 100644 --- a/src/Core/Enums/GatewayType.cs +++ b/src/Core/Enums/GatewayType.cs @@ -12,8 +12,8 @@ namespace Bit.Core.Enums AppStore = 2, [Display(Name = "Google Play Store")] PlayStore = 3, - [Display(Name = "Coinbase")] - Coinbase = 4, + [Display(Name = "BitPay")] + BitPay = 4, [Display(Name = "PayPal")] PayPal = 5, }