From 9e14af3223ea9860f929bcb2978903cc39bc364e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sun, 3 Feb 2019 00:00:21 -0500 Subject: [PATCH] attempt to pay invoices via braintree --- src/Billing/Controllers/StripeController.cs | 115 ++++++++++++++++++-- 1 file changed, 106 insertions(+), 9 deletions(-) diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index b965c02f3..0803e785d 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -1,4 +1,5 @@ -using Bit.Core.Enums; +using Bit.Core; +using Bit.Core.Enums; using Bit.Core.Models.Table; using Bit.Core.Repositories; using Bit.Core.Services; @@ -24,8 +25,10 @@ namespace Bit.Billing.Controllers private readonly ITransactionRepository _transactionRepository; private readonly IUserService _userService; private readonly IMailService _mailService; + private readonly Braintree.BraintreeGateway _btGateway; public StripeController( + GlobalSettings globalSettings, IOptions billingSettings, IHostingEnvironment hostingEnvironment, IOrganizationService organizationService, @@ -41,6 +44,15 @@ namespace Bit.Billing.Controllers _transactionRepository = transactionRepository; _userService = userService; _mailService = mailService; + + _btGateway = new Braintree.BraintreeGateway + { + Environment = globalSettings.Braintree.Production ? + Braintree.Environment.PRODUCTION : Braintree.Environment.SANDBOX, + MerchantId = globalSettings.Braintree.MerchantId, + PublicKey = globalSettings.Braintree.PublicKey, + PrivateKey = globalSettings.Braintree.PrivateKey + }; } [HttpPost("webhook")] @@ -74,8 +86,7 @@ namespace Bit.Billing.Controllers if(subDeleted || subUpdated) { - var subscription = parsedEvent.Data.Object as Subscription; - if(subscription == null) + if(!(parsedEvent.Data.Object is Subscription subscription)) { throw new Exception("Subscription is null."); } @@ -117,8 +128,7 @@ namespace Bit.Billing.Controllers } else if(parsedEvent.Type.Equals("invoice.upcoming")) { - var invoice = parsedEvent.Data.Object as Invoice; - if(invoice == null) + if(!(parsedEvent.Data.Object is Invoice invoice)) { throw new Exception("Invoice is null."); } @@ -160,8 +170,7 @@ namespace Bit.Billing.Controllers } else if(parsedEvent.Type.Equals("charge.succeeded")) { - var charge = parsedEvent.Data.Object as Charge; - if(charge == null) + if(!(parsedEvent.Data.Object is Charge charge)) { throw new Exception("Charge is null."); } @@ -224,8 +233,7 @@ namespace Bit.Billing.Controllers } else if(parsedEvent.Type.Equals("charge.refunded")) { - var charge = parsedEvent.Data.Object as Charge; - if(charge == null) + if(!(parsedEvent.Data.Object is Charge charge)) { throw new Exception("Charge is null."); } @@ -273,6 +281,30 @@ namespace Bit.Billing.Controllers } } } + else if(parsedEvent.Type.Equals("invoice.payment_failed")) + { + if(!(parsedEvent.Data.Object is Invoice invoice)) + { + throw new Exception("Invoice is null."); + } + + if(invoice.AttemptCount > 1 && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice)) + { + await AttemptToPayInvoiceWithBraintreeAsync(invoice); + } + } + else if(parsedEvent.Type.Equals("invoice.created")) + { + if(!(parsedEvent.Data.Object is Invoice invoice)) + { + throw new Exception("Invoice is null."); + } + + if(UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice)) + { + await AttemptToPayInvoiceWithBraintreeAsync(invoice); + } + } return new OkResult(); } @@ -328,5 +360,70 @@ namespace Bit.Billing.Controllers return false; } } + + private async Task AttemptToPayInvoiceWithBraintreeAsync(Invoice invoice) + { + var customerService = new CustomerService(); + var customer = await customerService.GetAsync(invoice.CustomerId); + if(!customer?.Metadata?.ContainsKey("btCustomerId") ?? true) + { + return false; + } + + var subscriptionService = new SubscriptionService(); + var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId); + var ids = GetIdsFromMetaData(subscription?.Metadata); + if(!ids.Item1.HasValue && !ids.Item2.HasValue) + { + return false; + } + + var btObjIdField = ids.Item1.HasValue ? "organization_id" : "user_id"; + var btObjId = ids.Item1 ?? ids.Item2.Value; + var btInvoiceAmount = (invoice.AmountDue / 100M); + + var transactionResult = await _btGateway.Transaction.SaleAsync( + new Braintree.TransactionRequest + { + Amount = btInvoiceAmount, + CustomerId = customer.Metadata["btCustomerId"], + Options = new Braintree.TransactionOptionsRequest + { + SubmitForSettlement = true, + PayPal = new Braintree.TransactionOptionsPayPalRequest + { + CustomField = $"{btObjIdField}:{btObjId}" + } + }, + CustomFields = new Dictionary + { + [btObjIdField] = btObjId.ToString() + } + }); + + if(!transactionResult.IsSuccess()) + { + return false; + } + + var invoiceService = new InvoiceService(); + await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions + { + Metadata = new Dictionary + { + ["btTransactionId"] = transactionResult.Target.Id, + ["btPayPalTransactionId"] = + transactionResult.Target.PayPalDetails?.AuthorizationId + } + }); + await invoiceService.PayAsync(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true }); + return true; + } + + private bool UnpaidAutoChargeInvoiceForSubscriptionCycle(Invoice invoice) + { + return invoice.AmountDue > 0 && !invoice.Paid && invoice.Billing == Stripe.Billing.ChargeAutomatically && + invoice.BillingReason == "subscription_cycle" && invoice.SubscriptionId != null; + } } }