2025-07-08 11:46:24 -04:00
|
|
|
|
// FIXME: Update this file to be null safe and then delete the line below
|
|
|
|
|
|
#nullable disable
|
|
|
|
|
|
|
|
|
|
|
|
using Bit.Api.Billing.Models.Requests;
|
2024-06-03 11:00:52 -04:00
|
|
|
|
using Bit.Api.Billing.Models.Responses;
|
2025-05-21 09:04:30 -04:00
|
|
|
|
using Bit.Commercial.Core.Billing.Providers.Services;
|
2024-06-03 11:00:52 -04:00
|
|
|
|
using Bit.Core.AdminConsole.Repositories;
|
2025-02-27 07:55:46 -05:00
|
|
|
|
using Bit.Core.Billing.Pricing;
|
2025-05-21 09:04:30 -04:00
|
|
|
|
using Bit.Core.Billing.Providers.Models;
|
|
|
|
|
|
using Bit.Core.Billing.Providers.Repositories;
|
|
|
|
|
|
using Bit.Core.Billing.Providers.Services;
|
2024-05-23 10:17:00 -04:00
|
|
|
|
using Bit.Core.Billing.Services;
|
2025-05-13 09:28:31 -04:00
|
|
|
|
using Bit.Core.Billing.Tax.Models;
|
2024-03-28 08:46:12 -04:00
|
|
|
|
using Bit.Core.Context;
|
2024-07-31 09:26:44 -04:00
|
|
|
|
using Bit.Core.Models.BitStripe;
|
2024-03-28 08:46:12 -04:00
|
|
|
|
using Bit.Core.Services;
|
|
|
|
|
|
using Microsoft.AspNetCore.Authorization;
|
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2024-06-03 11:00:52 -04:00
|
|
|
|
using Stripe;
|
2024-03-28 08:46:12 -04:00
|
|
|
|
|
2024-07-31 09:26:44 -04:00
|
|
|
|
using static Bit.Core.Billing.Utilities;
|
|
|
|
|
|
|
2024-03-28 08:46:12 -04:00
|
|
|
|
namespace Bit.Api.Billing.Controllers;
|
|
|
|
|
|
|
|
|
|
|
|
[Route("providers/{providerId:guid}/billing")]
|
|
|
|
|
|
[Authorize("Application")]
|
|
|
|
|
|
public class ProviderBillingController(
|
|
|
|
|
|
ICurrentContext currentContext,
|
2024-07-31 09:26:44 -04:00
|
|
|
|
ILogger<BaseProviderController> logger,
|
2025-02-27 07:55:46 -05:00
|
|
|
|
IPricingClient pricingClient,
|
2024-06-03 11:00:52 -04:00
|
|
|
|
IProviderBillingService providerBillingService,
|
2024-07-31 09:26:44 -04:00
|
|
|
|
IProviderPlanRepository providerPlanRepository,
|
2024-06-03 11:00:52 -04:00
|
|
|
|
IProviderRepository providerRepository,
|
2024-07-31 09:26:44 -04:00
|
|
|
|
ISubscriberService subscriberService,
|
2024-06-03 11:00:52 -04:00
|
|
|
|
IStripeAdapter stripeAdapter,
|
2024-11-15 09:30:03 -05:00
|
|
|
|
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
2024-03-28 08:46:12 -04:00
|
|
|
|
{
|
2024-06-05 13:33:28 -04:00
|
|
|
|
[HttpGet("invoices")]
|
|
|
|
|
|
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
|
|
|
|
|
|
{
|
2024-06-24 11:15:47 -04:00
|
|
|
|
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
2024-06-05 13:33:28 -04:00
|
|
|
|
|
|
|
|
|
|
if (provider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-31 09:26:44 -04:00
|
|
|
|
var invoices = await stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
|
|
|
|
|
|
{
|
|
|
|
|
|
Customer = provider.GatewayCustomerId
|
|
|
|
|
|
});
|
2024-06-05 13:33:28 -04:00
|
|
|
|
|
|
|
|
|
|
var response = InvoicesResponse.From(invoices);
|
|
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-06-14 12:26:49 -04:00
|
|
|
|
[HttpGet("invoices/{invoiceId}")]
|
|
|
|
|
|
public async Task<IResult> GenerateClientInvoiceReportAsync([FromRoute] Guid providerId, string invoiceId)
|
|
|
|
|
|
{
|
2024-06-24 11:15:47 -04:00
|
|
|
|
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
2024-06-14 12:26:49 -04:00
|
|
|
|
|
|
|
|
|
|
if (provider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var reportContent = await providerBillingService.GenerateClientInvoiceReport(invoiceId);
|
|
|
|
|
|
|
|
|
|
|
|
if (reportContent == null)
|
|
|
|
|
|
{
|
2024-08-28 10:48:14 -04:00
|
|
|
|
return Error.ServerError("We had a problem generating your invoice CSV. Please contact support.");
|
2024-06-14 12:26:49 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return TypedResults.File(
|
|
|
|
|
|
reportContent,
|
|
|
|
|
|
"text/csv");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-14 11:33:24 -04:00
|
|
|
|
[HttpPut("payment-method")]
|
|
|
|
|
|
public async Task<IResult> UpdatePaymentMethodAsync(
|
|
|
|
|
|
[FromRoute] Guid providerId,
|
|
|
|
|
|
[FromBody] UpdatePaymentMethodRequestBody requestBody)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
|
|
|
|
|
|
|
|
|
|
|
if (provider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();
|
|
|
|
|
|
var taxInformation = requestBody.TaxInformation.ToDomain();
|
|
|
|
|
|
|
|
|
|
|
|
await providerBillingService.UpdatePaymentMethod(
|
|
|
|
|
|
provider,
|
|
|
|
|
|
tokenizedPaymentSource,
|
|
|
|
|
|
taxInformation);
|
|
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPost("payment-method/verify-bank-account")]
|
|
|
|
|
|
public async Task<IResult> VerifyBankAccountAsync(
|
|
|
|
|
|
[FromRoute] Guid providerId,
|
|
|
|
|
|
[FromBody] VerifyBankAccountRequestBody requestBody)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
|
|
|
|
|
|
|
|
|
|
|
if (provider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM"))
|
|
|
|
|
|
{
|
|
|
|
|
|
return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await subscriberService.VerifyBankAccount(provider, requestBody.DescriptorCode);
|
|
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-06-03 11:00:52 -04:00
|
|
|
|
[HttpGet("subscription")]
|
|
|
|
|
|
public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId)
|
|
|
|
|
|
{
|
2024-06-24 11:15:47 -04:00
|
|
|
|
var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);
|
2024-06-03 11:00:52 -04:00
|
|
|
|
|
|
|
|
|
|
if (provider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-31 09:26:44 -04:00
|
|
|
|
var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId,
|
|
|
|
|
|
new SubscriptionGetOptions { Expand = ["customer.tax_ids", "test_clock"] });
|
2024-06-03 11:00:52 -04:00
|
|
|
|
|
2024-07-31 09:26:44 -04:00
|
|
|
|
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
2024-05-23 10:17:00 -04:00
|
|
|
|
|
2025-02-27 07:55:46 -05:00
|
|
|
|
var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType);
|
2025-08-18 15:25:40 -04:00
|
|
|
|
var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type);
|
|
|
|
|
|
var price = await stripeAdapter.PriceGetAsync(priceId);
|
2025-05-21 08:10:34 -04:00
|
|
|
|
|
2025-08-18 15:25:40 -04:00
|
|
|
|
var unitAmount = price.UnitAmountDecimal.HasValue
|
|
|
|
|
|
? price.UnitAmountDecimal.Value / 100M
|
|
|
|
|
|
: plan.PasswordManager.ProviderPortalSeatPrice;
|
2025-05-21 08:10:34 -04:00
|
|
|
|
|
2025-02-27 07:55:46 -05:00
|
|
|
|
return new ConfiguredProviderPlan(
|
|
|
|
|
|
providerPlan.Id,
|
|
|
|
|
|
providerPlan.ProviderId,
|
|
|
|
|
|
plan,
|
2025-05-21 08:10:34 -04:00
|
|
|
|
unitAmount,
|
2025-02-27 07:55:46 -05:00
|
|
|
|
providerPlan.SeatMinimum ?? 0,
|
|
|
|
|
|
providerPlan.PurchasedSeats ?? 0,
|
|
|
|
|
|
providerPlan.AllocatedSeats ?? 0);
|
|
|
|
|
|
}));
|
|
|
|
|
|
|
2024-07-31 09:26:44 -04:00
|
|
|
|
var taxInformation = GetTaxInformation(subscription.Customer);
|
2024-05-23 10:17:00 -04:00
|
|
|
|
|
2024-07-31 09:26:44 -04:00
|
|
|
|
var subscriptionSuspension = await GetSubscriptionSuspensionAsync(stripeAdapter, subscription);
|
2024-05-23 10:17:00 -04:00
|
|
|
|
|
2025-03-14 11:33:24 -04:00
|
|
|
|
var paymentSource = await subscriberService.GetPaymentSource(provider);
|
|
|
|
|
|
|
2024-07-31 09:26:44 -04:00
|
|
|
|
var response = ProviderSubscriptionResponse.From(
|
|
|
|
|
|
subscription,
|
2025-02-27 07:55:46 -05:00
|
|
|
|
configuredProviderPlans,
|
2024-07-31 09:26:44 -04:00
|
|
|
|
taxInformation,
|
2024-11-06 15:46:36 +01:00
|
|
|
|
subscriptionSuspension,
|
2025-03-14 11:33:24 -04:00
|
|
|
|
provider,
|
|
|
|
|
|
paymentSource);
|
|
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpGet("tax-information")]
|
|
|
|
|
|
public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid providerId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
|
|
|
|
|
|
|
|
|
|
|
if (provider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var taxInformation = await subscriberService.GetTaxInformation(provider);
|
|
|
|
|
|
|
|
|
|
|
|
var response = TaxInformationResponse.From(taxInformation);
|
2024-06-03 11:00:52 -04:00
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok(response);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[HttpPut("tax-information")]
|
|
|
|
|
|
public async Task<IResult> UpdateTaxInformationAsync(
|
|
|
|
|
|
[FromRoute] Guid providerId,
|
|
|
|
|
|
[FromBody] TaxInformationRequestBody requestBody)
|
|
|
|
|
|
{
|
2024-06-24 11:15:47 -04:00
|
|
|
|
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
2024-06-03 11:00:52 -04:00
|
|
|
|
|
|
|
|
|
|
if (provider == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-31 09:26:44 -04:00
|
|
|
|
if (requestBody is not { Country: not null, PostalCode: not null })
|
|
|
|
|
|
{
|
2024-08-28 10:48:14 -04:00
|
|
|
|
return Error.BadRequest("Country and postal code are required to update your tax information.");
|
2024-07-31 09:26:44 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var taxInformation = new TaxInformation(
|
2024-06-03 11:00:52 -04:00
|
|
|
|
requestBody.Country,
|
|
|
|
|
|
requestBody.PostalCode,
|
|
|
|
|
|
requestBody.TaxId,
|
2025-01-02 20:27:53 +01:00
|
|
|
|
requestBody.TaxIdType,
|
2024-06-03 11:00:52 -04:00
|
|
|
|
requestBody.Line1,
|
|
|
|
|
|
requestBody.Line2,
|
|
|
|
|
|
requestBody.City,
|
|
|
|
|
|
requestBody.State);
|
2024-05-23 10:17:00 -04:00
|
|
|
|
|
2024-06-03 11:00:52 -04:00
|
|
|
|
await subscriberService.UpdateTaxInformation(provider, taxInformation);
|
|
|
|
|
|
|
|
|
|
|
|
return TypedResults.Ok();
|
|
|
|
|
|
}
|
2024-03-28 08:46:12 -04:00
|
|
|
|
}
|