diff --git a/src/Api/Controllers/AccountsBillingController.cs b/src/Api/Controllers/AccountsBillingController.cs new file mode 100644 index 000000000..5581da48c --- /dev/null +++ b/src/Api/Controllers/AccountsBillingController.cs @@ -0,0 +1,54 @@ +using System; +using System.Threading.Tasks; +using Bit.Api.Models.Response; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers +{ + [Route("accounts/billing")] + [Authorize("Application")] + public class AccountsBillingController : Controller + { + private readonly IPaymentService _paymentService; + private readonly IUserService _userService; + + public AccountsBillingController( + IPaymentService paymentService, + IUserService userService) + { + _paymentService = paymentService; + _userService = userService; + } + + [HttpGet("history")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task GetBillingHistory() + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var billingInfo = await _paymentService.GetBillingHistoryAsync(user); + return new BillingHistoryResponseModel(billingInfo); + } + + [HttpGet("payment-method")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task GetPaymentMethod() + { + var user = await _userService.GetUserByPrincipalAsync(User); + if (user == null) + { + throw new UnauthorizedAccessException(); + } + + var billingInfo = await _paymentService.GetBillingBalanceAndSourceAsync(user); + return new BillingPaymentResponseModel(billingInfo); + } + } +} diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 471de18ea..1aa5275bf 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -626,6 +626,7 @@ namespace Bit.Api.Controllers }; } + [Obsolete("2022-04-01 Use separate Billing History/Payment APIs, left for backwards compatability with older clients")] [HttpGet("billing")] [SelfHosted(NotSelfHostedOnly = true)] public async Task GetBilling() diff --git a/src/Api/Models/Response/BillingHistoryResponseModel.cs b/src/Api/Models/Response/BillingHistoryResponseModel.cs new file mode 100644 index 000000000..91ca99d18 --- /dev/null +++ b/src/Api/Models/Response/BillingHistoryResponseModel.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Models.Api; +using Bit.Core.Models.Business; + +namespace Bit.Api.Models.Response +{ + public class BillingHistoryResponseModel : ResponseModel + { + public BillingHistoryResponseModel(BillingInfo billing) + : base("billingHistory") + { + Transactions = billing.Transactions?.Select(t => new BillingTransaction(t)); + Invoices = billing.Invoices?.Select(i => new BillingInvoice(i)); + } + public IEnumerable Invoices { get; set; } + public IEnumerable Transactions { get; set; } + } +} diff --git a/src/Api/Models/Response/BillingPaymentResponseModel.cs b/src/Api/Models/Response/BillingPaymentResponseModel.cs new file mode 100644 index 000000000..12c14c4d6 --- /dev/null +++ b/src/Api/Models/Response/BillingPaymentResponseModel.cs @@ -0,0 +1,18 @@ +using Bit.Core.Models.Api; +using Bit.Core.Models.Business; + +namespace Bit.Api.Models.Response +{ + public class BillingPaymentResponseModel : ResponseModel + { + public BillingPaymentResponseModel(BillingInfo billing) + : base("billingPayment") + { + Balance = billing.Balance; + PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null; + } + + public decimal Balance { get; set; } + public BillingSource PaymentSource { get; set; } + } +} diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 3d0982cdb..f3287d9e0 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -28,6 +28,8 @@ namespace Bit.Core.Services string paymentToken, bool allowInAppPurchases = false, TaxInfo taxInfo = null); Task CreditAccountAsync(ISubscriber subscriber, decimal creditAmount); Task GetBillingAsync(ISubscriber subscriber); + Task GetBillingHistoryAsync(ISubscriber subscriber); + Task GetBillingBalanceAndSourceAsync(ISubscriber subscriber); Task GetSubscriptionAsync(ISubscriber subscriber); Task GetTaxInfoAsync(ISubscriber subscriber); Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 96e4500e3..329b2738e 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1448,87 +1448,38 @@ namespace Bit.Core.Services public async Task GetBillingAsync(ISubscriber subscriber) { - var billingInfo = new BillingInfo(); - - ICollection transactions = null; - if (subscriber is User) + var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions()); + var billingInfo = new BillingInfo { - transactions = await _transactionRepository.GetManyByUserIdAsync(subscriber.Id); - } - else if (subscriber is Organization) - { - transactions = await _transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id); - } - if (transactions != null) - { - billingInfo.Transactions = transactions?.OrderByDescending(i => i.CreationDate) - .Select(t => new BillingInfo.BillingTransaction(t)); - } + Balance = GetBillingBalance(customer), + PaymentSource = await GetBillingPaymentSourceAsync(customer), + Invoices = await GetBillingInvoicesAsync(customer), + Transactions = await GetBillingTransactionsAsync(subscriber) + }; - if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) + return billingInfo; + } + + public async Task GetBillingBalanceAndSourceAsync(ISubscriber subscriber) + { + var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions()); + var billingInfo = new BillingInfo { - Stripe.Customer customer = null; - try - { - var customerOptions = new Stripe.CustomerGetOptions(); - customerOptions.AddExpand("default_source"); - customerOptions.AddExpand("invoice_settings.default_payment_method"); - customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions); - } - catch (Stripe.StripeException) { } - if (customer != null) - { - billingInfo.Balance = customer.Balance / 100M; + Balance = GetBillingBalance(customer), + PaymentSource = await GetBillingPaymentSourceAsync(customer) + }; - if (customer.Metadata?.ContainsKey("appleReceipt") ?? false) - { - billingInfo.PaymentSource = new BillingInfo.BillingSource - { - Type = PaymentMethodType.AppleInApp - }; - } - else if (customer.Metadata?.ContainsKey("btCustomerId") ?? false) - { - try - { - var braintreeCustomer = await _btGateway.Customer.FindAsync( - customer.Metadata["btCustomerId"]); - if (braintreeCustomer?.DefaultPaymentMethod != null) - { - billingInfo.PaymentSource = new BillingInfo.BillingSource( - braintreeCustomer.DefaultPaymentMethod); - } - } - catch (Braintree.Exceptions.NotFoundException) { } - } - else if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card") - { - billingInfo.PaymentSource = new BillingInfo.BillingSource( - customer.InvoiceSettings.DefaultPaymentMethod); - } - else if (customer.DefaultSource != null && - (customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount)) - { - billingInfo.PaymentSource = new BillingInfo.BillingSource(customer.DefaultSource); - } - if (billingInfo.PaymentSource == null) - { - var paymentMethod = GetLatestCardPaymentMethod(customer.Id); - if (paymentMethod != null) - { - billingInfo.PaymentSource = new BillingInfo.BillingSource(paymentMethod); - } - } + return billingInfo; + } - var invoices = await _stripeAdapter.InvoiceListAsync(new Stripe.InvoiceListOptions - { - Customer = customer.Id, - Limit = 50 - }); - billingInfo.Invoices = invoices.Data.Where(i => i.Status != "void" && i.Status != "draft") - .OrderByDescending(i => i.Created).Select(i => new BillingInfo.BillingInvoice(i)); - } - } + public async Task GetBillingHistoryAsync(ISubscriber subscriber) + { + var customer = await GetCustomerAsync(subscriber.GatewayCustomerId); + var billingInfo = new BillingInfo + { + Transactions = await GetBillingTransactionsAsync(subscriber), + Invoices = await GetBillingInvoicesAsync(customer) + }; return billingInfo; } @@ -1707,5 +1658,116 @@ namespace Bit.Core.Services } } } + + private decimal GetBillingBalance(Stripe.Customer customer) + { + return customer != null ? customer.Balance / 100M : default; + } + + private async Task GetBillingPaymentSourceAsync(Stripe.Customer customer) + { + if (customer == null) + { + return null; + } + + if (customer.Metadata?.ContainsKey("appleReceipt") ?? false) + { + return new BillingInfo.BillingSource + { + Type = PaymentMethodType.AppleInApp + }; + } + + if (customer.Metadata?.ContainsKey("btCustomerId") ?? false) + { + try + { + var braintreeCustomer = await _btGateway.Customer.FindAsync( + customer.Metadata["btCustomerId"]); + if (braintreeCustomer?.DefaultPaymentMethod != null) + { + return new BillingInfo.BillingSource( + braintreeCustomer.DefaultPaymentMethod); + } + } + catch (Braintree.Exceptions.NotFoundException) { } + } + + if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card") + { + return new BillingInfo.BillingSource( + customer.InvoiceSettings.DefaultPaymentMethod); + } + + if (customer.DefaultSource != null && + (customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount)) + { + return new BillingInfo.BillingSource(customer.DefaultSource); + } + + var paymentMethod = GetLatestCardPaymentMethod(customer.Id); + return paymentMethod != null ? new BillingInfo.BillingSource(paymentMethod) : null; + } + + private Stripe.CustomerGetOptions GetCustomerPaymentOptions() + { + var customerOptions = new Stripe.CustomerGetOptions(); + customerOptions.AddExpand("default_source"); + customerOptions.AddExpand("invoice_settings.default_payment_method"); + return customerOptions; + } + + private async Task GetCustomerAsync(string gatewayCustomerId, Stripe.CustomerGetOptions options = null) + { + if (string.IsNullOrWhiteSpace(gatewayCustomerId)) + { + return null; + } + + Stripe.Customer customer = null; + try + { + customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId, options); + } + catch (Stripe.StripeException) { } + + return customer; + } + + private async Task> GetBillingTransactionsAsync(ISubscriber subscriber) + { + ICollection transactions = null; + if (subscriber is User) + { + transactions = await _transactionRepository.GetManyByUserIdAsync(subscriber.Id); + } + else if (subscriber is Organization) + { + transactions = await _transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id); + } + + return transactions?.OrderByDescending(i => i.CreationDate) + .Select(t => new BillingInfo.BillingTransaction(t)); + + } + + private async Task> GetBillingInvoicesAsync(Stripe.Customer customer) + { + if (customer == null) + { + return null; + } + + var invoices = await _stripeAdapter.InvoiceListAsync(new Stripe.InvoiceListOptions + { + Customer = customer.Id, + Limit = 50 + }); + + return invoices.Data.Where(i => i.Status != "void" && i.Status != "draft") + .OrderByDescending(i => i.Created).Select(i => new BillingInfo.BillingInvoice(i)); + + } } }