1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 08:02:49 -05:00

[AC-1942] Add endpoint to get provider invoices (#4158)

* Added endpoint to get provider invoices

* Added missing properties of invoice

* Run dotnet format'
This commit is contained in:
Alex Morask
2024-06-05 13:33:28 -04:00
committed by GitHub
parent 4a6113dc86
commit a0a7654077
6 changed files with 389 additions and 121 deletions

View File

@ -25,6 +25,23 @@ public class ProviderBillingController(
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService) : Controller
{
[HttpGet("invoices")]
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
{
var (provider, result) = await GetAuthorizedBillableProviderOrResultAsync(providerId);
if (provider == null)
{
return result;
}
var invoices = await subscriberService.GetInvoices(provider);
var response = InvoicesResponse.From(invoices);
return TypedResults.Ok(response);
}
[HttpGet("payment-information")]
public async Task<IResult> GetPaymentInformationAsync([FromRoute] Guid providerId)
{

View File

@ -0,0 +1,30 @@
using Stripe;
namespace Bit.Api.Billing.Models.Responses;
public record InvoicesResponse(
List<InvoiceDTO> Invoices)
{
public static InvoicesResponse From(IEnumerable<Invoice> invoices) => new(
invoices
.Where(i => i.Status is "open" or "paid" or "uncollectible")
.OrderByDescending(i => i.Created)
.Select(InvoiceDTO.From).ToList());
}
public record InvoiceDTO(
DateTime Date,
string Number,
decimal Total,
string Status,
string Url,
string PdfUrl)
{
public static InvoiceDTO From(Invoice invoice) => new(
invoice.Created,
invoice.Number,
invoice.Total / 100M,
invoice.Status,
invoice.HostedInvoiceUrl,
invoice.InvoicePdf);
}

View File

@ -1,6 +1,7 @@
using Bit.Core.Billing.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.BitStripe;
using Stripe;
namespace Bit.Core.Billing.Services;
@ -46,6 +47,18 @@ public interface ISubscriberService
ISubscriber subscriber,
CustomerGetOptions customerGetOptions = null);
/// <summary>
/// Retrieves a list of Stripe <see cref="Invoice"/> objects using the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> property.
/// </summary>
/// <param name="subscriber">The subscriber to retrieve the Stripe invoices for.</param>
/// <param name="invoiceListOptions">Optional parameters that can be passed to Stripe to expand, modify or filter the invoices. The <see cref="subscriber"/>'s
/// <see cref="ISubscriber.GatewayCustomerId"/> will be automatically attached to the provided options as the <see cref="InvoiceListOptions.Customer"/> parameter.</param>
/// <returns>A list of Stripe <see cref="Invoice"/> objects.</returns>
/// <remarks>This method opts for returning an empty list rather than throwing exceptions, making it ideal for surfacing data from API endpoints.</remarks>
Task<List<Invoice>> GetInvoices(
ISubscriber subscriber,
StripeInvoiceListOptions invoiceListOptions = null);
/// <summary>
/// Retrieves the account credit, a masked representation of the default payment method and the tax information for the
/// provided <paramref name="subscriber"/>. This is essentially a consolidated invocation of the <see cref="GetPaymentMethod"/>

View File

@ -2,6 +2,7 @@
using Bit.Core.Billing.Models;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.BitStripe;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
@ -137,6 +138,76 @@ public class SubscriberService(
}
}
public async Task<Customer> GetCustomerOrThrow(
ISubscriber subscriber,
CustomerGetOptions customerGetOptions = null)
{
ArgumentNullException.ThrowIfNull(subscriber);
if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))
{
logger.LogError("Cannot retrieve customer for subscriber ({SubscriberID}) with no {FieldName}", subscriber.Id, nameof(subscriber.GatewayCustomerId));
throw ContactSupport();
}
try
{
var customer = await stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerGetOptions);
if (customer != null)
{
return customer;
}
logger.LogError("Could not find Stripe customer ({CustomerID}) for subscriber ({SubscriberID})",
subscriber.GatewayCustomerId, subscriber.Id);
throw ContactSupport();
}
catch (StripeException exception)
{
logger.LogError("An error occurred while trying to retrieve Stripe customer ({CustomerID}) for subscriber ({SubscriberID}): {Error}",
subscriber.GatewayCustomerId, subscriber.Id, exception.Message);
throw ContactSupport("An error occurred while trying to retrieve a Stripe Customer", exception);
}
}
public async Task<List<Invoice>> GetInvoices(
ISubscriber subscriber,
StripeInvoiceListOptions invoiceListOptions = null)
{
ArgumentNullException.ThrowIfNull(subscriber);
if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))
{
logger.LogError("Cannot retrieve invoices for subscriber ({SubscriberID}) with no {FieldName}", subscriber.Id, nameof(subscriber.GatewayCustomerId));
return [];
}
try
{
if (invoiceListOptions == null)
{
invoiceListOptions = new StripeInvoiceListOptions { Customer = subscriber.GatewayCustomerId };
}
else
{
invoiceListOptions.Customer = subscriber.GatewayCustomerId;
}
return await stripeAdapter.InvoiceListAsync(invoiceListOptions);
}
catch (StripeException exception)
{
logger.LogError("An error occurred while trying to retrieve Stripe invoices for subscriber ({SubscriberID}): {Error}", subscriber.Id, exception.Message);
return [];
}
}
public async Task<PaymentInformationDTO> GetPaymentInformation(
ISubscriber subscriber)
{
@ -177,42 +248,6 @@ public class SubscriberService(
return await GetMaskedPaymentMethodDTOAsync(subscriber.Id, customer);
}
public async Task<Customer> GetCustomerOrThrow(
ISubscriber subscriber,
CustomerGetOptions customerGetOptions = null)
{
ArgumentNullException.ThrowIfNull(subscriber);
if (string.IsNullOrEmpty(subscriber.GatewayCustomerId))
{
logger.LogError("Cannot retrieve customer for subscriber ({SubscriberID}) with no {FieldName}", subscriber.Id, nameof(subscriber.GatewayCustomerId));
throw ContactSupport();
}
try
{
var customer = await stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerGetOptions);
if (customer != null)
{
return customer;
}
logger.LogError("Could not find Stripe customer ({CustomerID}) for subscriber ({SubscriberID})",
subscriber.GatewayCustomerId, subscriber.Id);
throw ContactSupport();
}
catch (StripeException exception)
{
logger.LogError("An error occurred while trying to retrieve Stripe customer ({CustomerID}) for subscriber ({SubscriberID}): {Error}",
subscriber.GatewayCustomerId, subscriber.Id, exception.Message);
throw ContactSupport("An error occurred while trying to retrieve a Stripe Customer", exception);
}
}
public async Task<Subscription> GetSubscription(
ISubscriber subscriber,
SubscriptionGetOptions subscriptionGetOptions = null)