mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01:00
[euvr] Separate Billing Payment/History APIs (#1932)
* [euvr] Separate Billing Payment/History APIs * Formatting * Created AccountsBillingController // Deprecated GetBilling // Simplified PaymentService helpers * Formatting
This commit is contained in:
parent
6f60d24f5a
commit
9a1a7543c5
54
src/Api/Controllers/AccountsBillingController.cs
Normal file
54
src/Api/Controllers/AccountsBillingController.cs
Normal file
@ -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<BillingHistoryResponseModel> 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<BillingPaymentResponseModel> GetPaymentMethod()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var billingInfo = await _paymentService.GetBillingBalanceAndSourceAsync(user);
|
||||
return new BillingPaymentResponseModel(billingInfo);
|
||||
}
|
||||
}
|
||||
}
|
@ -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<BillingResponseModel> GetBilling()
|
||||
|
19
src/Api/Models/Response/BillingHistoryResponseModel.cs
Normal file
19
src/Api/Models/Response/BillingHistoryResponseModel.cs
Normal file
@ -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<BillingInvoice> Invoices { get; set; }
|
||||
public IEnumerable<BillingTransaction> Transactions { get; set; }
|
||||
}
|
||||
}
|
18
src/Api/Models/Response/BillingPaymentResponseModel.cs
Normal file
18
src/Api/Models/Response/BillingPaymentResponseModel.cs
Normal file
@ -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; }
|
||||
}
|
||||
}
|
@ -28,6 +28,8 @@ namespace Bit.Core.Services
|
||||
string paymentToken, bool allowInAppPurchases = false, TaxInfo taxInfo = null);
|
||||
Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount);
|
||||
Task<BillingInfo> GetBillingAsync(ISubscriber subscriber);
|
||||
Task<BillingInfo> GetBillingHistoryAsync(ISubscriber subscriber);
|
||||
Task<BillingInfo> GetBillingBalanceAndSourceAsync(ISubscriber subscriber);
|
||||
Task<SubscriptionInfo> GetSubscriptionAsync(ISubscriber subscriber);
|
||||
Task<TaxInfo> GetTaxInfoAsync(ISubscriber subscriber);
|
||||
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
|
||||
|
@ -1448,87 +1448,38 @@ namespace Bit.Core.Services
|
||||
|
||||
public async Task<BillingInfo> GetBillingAsync(ISubscriber subscriber)
|
||||
{
|
||||
var billingInfo = new BillingInfo();
|
||||
|
||||
ICollection<Transaction> 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<BillingInfo> 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<BillingInfo> 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<BillingInfo.BillingSource> 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<Stripe.Customer> 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<IEnumerable<BillingInfo.BillingTransaction>> GetBillingTransactionsAsync(ISubscriber subscriber)
|
||||
{
|
||||
ICollection<Transaction> 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<IEnumerable<BillingInfo.BillingInvoice>> 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));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user