mirror of
https://github.com/bitwarden/server.git
synced 2025-07-05 10:02:47 -05:00
[AC-1938] Update provider payment method (#4140)
* Refactored GET provider subscription Refactoring this endpoint and its associated tests in preparation for the addition of more endpoints that share similar patterns * Replaced StripePaymentService call in AccountsController, OrganizationsController This was made in error during a previous PR. Since this is not related to Consolidated Billing, we want to try not to include it in these changes. * Removing GetPaymentInformation call from ProviderBillingService This method is a good call for the SubscriberService as we'll want to extend the functionality to all subscriber types * Refactored GetTaxInformation to use Billing owned DTO * Add UpdateTaxInformation to SubscriberService * Added GetTaxInformation and UpdateTaxInformation endpoints to ProviderBillingController * Added controller to manage creation of Stripe SetupIntents With the deprecation of the Sources API, we need to move the bank account creation process to using SetupIntents. This controller brings both the creation of "card" and "us_bank_account" SetupIntents under billing management. * Added UpdatePaymentMethod method to SubscriberService This method utilizes the SetupIntents created by the StripeController from the previous commit when a customer adds a card or us_bank_account payment method (Stripe). We need to cache the most recent SetupIntent for the subscriber so that we know which PaymentMethod is their most recent even when it hasn't been confirmed yet. * Refactored GetPaymentMethod to use billing owned DTO and check setup intents * Added GetPaymentMethod and UpdatePaymentMethod endpoints to ProviderBillingController * Re-added GetPaymentInformation endpoint to consolidate API calls on the payment method page * Added VerifyBankAccount endpoint to ProviderBillingController in order to finalize bank account payment methods * Updated BitPayInvoiceRequestModel to support providers * run dotnet format * Conner's feedback * Run dotnet format'
This commit is contained in:
@ -2,6 +2,6 @@
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record ProviderSubscriptionDTO(
|
||||
public record ConsolidatedBillingSubscriptionDTO(
|
||||
List<ConfiguredProviderPlanDTO> ProviderPlans,
|
||||
Subscription Subscription);
|
156
src/Core/Billing/Models/MaskedPaymentMethodDTO.cs
Normal file
156
src/Core/Billing/Models/MaskedPaymentMethodDTO.cs
Normal file
@ -0,0 +1,156 @@
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record MaskedPaymentMethodDTO(
|
||||
PaymentMethodType Type,
|
||||
string Description,
|
||||
bool NeedsVerification)
|
||||
{
|
||||
public static MaskedPaymentMethodDTO From(Stripe.Customer customer)
|
||||
{
|
||||
var defaultPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod;
|
||||
|
||||
if (defaultPaymentMethod == null)
|
||||
{
|
||||
return customer.DefaultSource != null ? FromStripeLegacyPaymentSource(customer.DefaultSource) : null;
|
||||
}
|
||||
|
||||
return defaultPaymentMethod.Type switch
|
||||
{
|
||||
"card" => FromStripeCardPaymentMethod(defaultPaymentMethod.Card),
|
||||
"us_bank_account" => FromStripeBankAccountPaymentMethod(defaultPaymentMethod.UsBankAccount),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
public static MaskedPaymentMethodDTO From(Stripe.SetupIntent setupIntent)
|
||||
{
|
||||
if (!setupIntent.IsUnverifiedBankAccount())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var bankAccount = setupIntent.PaymentMethod.UsBankAccount;
|
||||
|
||||
var description = $"{bankAccount.BankName}, *{bankAccount.Last4}";
|
||||
|
||||
return new MaskedPaymentMethodDTO(
|
||||
PaymentMethodType.BankAccount,
|
||||
description,
|
||||
true);
|
||||
}
|
||||
|
||||
public static MaskedPaymentMethodDTO From(Braintree.Customer customer)
|
||||
{
|
||||
var defaultPaymentMethod = customer.DefaultPaymentMethod;
|
||||
|
||||
if (defaultPaymentMethod == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (defaultPaymentMethod)
|
||||
{
|
||||
case Braintree.PayPalAccount payPalAccount:
|
||||
{
|
||||
return new MaskedPaymentMethodDTO(
|
||||
PaymentMethodType.PayPal,
|
||||
payPalAccount.Email,
|
||||
false);
|
||||
}
|
||||
case Braintree.CreditCard creditCard:
|
||||
{
|
||||
var paddedExpirationMonth = creditCard.ExpirationMonth.PadLeft(2, '0');
|
||||
|
||||
var description =
|
||||
$"{creditCard.CardType}, *{creditCard.LastFour}, {paddedExpirationMonth}/{creditCard.ExpirationYear}";
|
||||
|
||||
return new MaskedPaymentMethodDTO(
|
||||
PaymentMethodType.Card,
|
||||
description,
|
||||
false);
|
||||
}
|
||||
case Braintree.UsBankAccount bankAccount:
|
||||
{
|
||||
return new MaskedPaymentMethodDTO(
|
||||
PaymentMethodType.BankAccount,
|
||||
$"{bankAccount.BankName}, *{bankAccount.Last4}",
|
||||
false);
|
||||
}
|
||||
default:
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeBankAccountPaymentMethod(
|
||||
Stripe.PaymentMethodUsBankAccount bankAccount)
|
||||
{
|
||||
var description = $"{bankAccount.BankName}, *{bankAccount.Last4}";
|
||||
|
||||
return new MaskedPaymentMethodDTO(
|
||||
PaymentMethodType.BankAccount,
|
||||
description,
|
||||
false);
|
||||
}
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeCardPaymentMethod(Stripe.PaymentMethodCard card)
|
||||
=> new(
|
||||
PaymentMethodType.Card,
|
||||
GetCardDescription(card.Brand, card.Last4, card.ExpMonth, card.ExpYear),
|
||||
false);
|
||||
|
||||
#region Legacy Source Payments
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeLegacyPaymentSource(Stripe.IPaymentSource paymentSource)
|
||||
=> paymentSource switch
|
||||
{
|
||||
Stripe.BankAccount bankAccount => FromStripeBankAccountLegacySource(bankAccount),
|
||||
Stripe.Card card => FromStripeCardLegacySource(card),
|
||||
Stripe.Source { Card: not null } source => FromStripeSourceCardLegacySource(source.Card),
|
||||
_ => null
|
||||
};
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeBankAccountLegacySource(Stripe.BankAccount bankAccount)
|
||||
{
|
||||
var status = bankAccount.Status switch
|
||||
{
|
||||
"verified" => "Verified",
|
||||
"errored" => "Invalid",
|
||||
"verification_failed" => "Verification failed",
|
||||
_ => "Unverified"
|
||||
};
|
||||
|
||||
var description = $"{bankAccount.BankName}, *{bankAccount.Last4} - {status}";
|
||||
|
||||
var needsVerification = bankAccount.Status is "new" or "validated";
|
||||
|
||||
return new MaskedPaymentMethodDTO(
|
||||
PaymentMethodType.BankAccount,
|
||||
description,
|
||||
needsVerification);
|
||||
}
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeCardLegacySource(Stripe.Card card)
|
||||
=> new(
|
||||
PaymentMethodType.Card,
|
||||
GetCardDescription(card.Brand, card.Last4, card.ExpMonth, card.ExpYear),
|
||||
false);
|
||||
|
||||
private static MaskedPaymentMethodDTO FromStripeSourceCardLegacySource(Stripe.SourceCard card)
|
||||
=> new(
|
||||
PaymentMethodType.Card,
|
||||
GetCardDescription(card.Brand, card.Last4, card.ExpMonth, card.ExpYear),
|
||||
false);
|
||||
|
||||
#endregion
|
||||
|
||||
private static string GetCardDescription(
|
||||
string brand,
|
||||
string last4,
|
||||
long expirationMonth,
|
||||
long expirationYear) => $"{brand.ToUpperInvariant()}, *{last4}, {expirationMonth:00}/{expirationYear}";
|
||||
}
|
6
src/Core/Billing/Models/PaymentInformationDTO.cs
Normal file
6
src/Core/Billing/Models/PaymentInformationDTO.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record PaymentInformationDTO(
|
||||
long AccountCredit,
|
||||
MaskedPaymentMethodDTO PaymentMethod,
|
||||
TaxInformationDTO TaxInformation);
|
@ -1,6 +0,0 @@
|
||||
using Bit.Core.Models.Business;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record ProviderPaymentInfoDTO(BillingInfo.BillingSource billingSource,
|
||||
TaxInfo taxInfo);
|
149
src/Core/Billing/Models/TaxInformationDTO.cs
Normal file
149
src/Core/Billing/Models/TaxInformationDTO.cs
Normal file
@ -0,0 +1,149 @@
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record TaxInformationDTO(
|
||||
string Country,
|
||||
string PostalCode,
|
||||
string TaxId,
|
||||
string Line1,
|
||||
string Line2,
|
||||
string City,
|
||||
string State)
|
||||
{
|
||||
public string GetTaxIdType()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Country) || string.IsNullOrEmpty(TaxId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (Country.ToUpper())
|
||||
{
|
||||
case "AD":
|
||||
return "ad_nrt";
|
||||
case "AE":
|
||||
return "ae_trn";
|
||||
case "AR":
|
||||
return "ar_cuit";
|
||||
case "AU":
|
||||
return "au_abn";
|
||||
case "BO":
|
||||
return "bo_tin";
|
||||
case "BR":
|
||||
return "br_cnpj";
|
||||
case "CA":
|
||||
// May break for those in Québec given the assumption of QST
|
||||
if (State?.Contains("bec") ?? false)
|
||||
{
|
||||
return "ca_qst";
|
||||
}
|
||||
return "ca_bn";
|
||||
case "CH":
|
||||
return "ch_vat";
|
||||
case "CL":
|
||||
return "cl_tin";
|
||||
case "CN":
|
||||
return "cn_tin";
|
||||
case "CO":
|
||||
return "co_nit";
|
||||
case "CR":
|
||||
return "cr_tin";
|
||||
case "DO":
|
||||
return "do_rcn";
|
||||
case "EC":
|
||||
return "ec_ruc";
|
||||
case "EG":
|
||||
return "eg_tin";
|
||||
case "GE":
|
||||
return "ge_vat";
|
||||
case "ID":
|
||||
return "id_npwp";
|
||||
case "IL":
|
||||
return "il_vat";
|
||||
case "IS":
|
||||
return "is_vat";
|
||||
case "KE":
|
||||
return "ke_pin";
|
||||
case "AT":
|
||||
case "BE":
|
||||
case "BG":
|
||||
case "CY":
|
||||
case "CZ":
|
||||
case "DE":
|
||||
case "DK":
|
||||
case "EE":
|
||||
case "ES":
|
||||
case "FI":
|
||||
case "FR":
|
||||
case "GB":
|
||||
case "GR":
|
||||
case "HR":
|
||||
case "HU":
|
||||
case "IE":
|
||||
case "IT":
|
||||
case "LT":
|
||||
case "LU":
|
||||
case "LV":
|
||||
case "MT":
|
||||
case "NL":
|
||||
case "PL":
|
||||
case "PT":
|
||||
case "RO":
|
||||
case "SE":
|
||||
case "SI":
|
||||
case "SK":
|
||||
return "eu_vat";
|
||||
case "HK":
|
||||
return "hk_br";
|
||||
case "IN":
|
||||
return "in_gst";
|
||||
case "JP":
|
||||
return "jp_cn";
|
||||
case "KR":
|
||||
return "kr_brn";
|
||||
case "LI":
|
||||
return "li_uid";
|
||||
case "MX":
|
||||
return "mx_rfc";
|
||||
case "MY":
|
||||
return "my_sst";
|
||||
case "NO":
|
||||
return "no_vat";
|
||||
case "NZ":
|
||||
return "nz_gst";
|
||||
case "PE":
|
||||
return "pe_ruc";
|
||||
case "PH":
|
||||
return "ph_tin";
|
||||
case "RS":
|
||||
return "rs_pib";
|
||||
case "RU":
|
||||
return "ru_inn";
|
||||
case "SA":
|
||||
return "sa_vat";
|
||||
case "SG":
|
||||
return "sg_gst";
|
||||
case "SV":
|
||||
return "sv_nit";
|
||||
case "TH":
|
||||
return "th_vat";
|
||||
case "TR":
|
||||
return "tr_tin";
|
||||
case "TW":
|
||||
return "tw_vat";
|
||||
case "UA":
|
||||
return "ua_vat";
|
||||
case "US":
|
||||
return "us_ein";
|
||||
case "UY":
|
||||
return "uy_ruc";
|
||||
case "VE":
|
||||
return "ve_rif";
|
||||
case "VN":
|
||||
return "vn_tin";
|
||||
case "ZA":
|
||||
return "za_vat";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
7
src/Core/Billing/Models/TokenizedPaymentMethodDTO.cs
Normal file
7
src/Core/Billing/Models/TokenizedPaymentMethodDTO.cs
Normal file
@ -0,0 +1,7 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record TokenizedPaymentMethodDTO(
|
||||
PaymentMethodType Type,
|
||||
string Token);
|
Reference in New Issue
Block a user