mirror of
https://github.com/bitwarden/server
synced 2025-12-22 11:13:27 +00:00
Remove unused endpoints and code paths
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
#nullable enable
|
using Bit.Api.Billing.Models.Responses;
|
||||||
using Bit.Api.Billing.Models.Responses;
|
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Tax.Requests;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@@ -16,6 +14,7 @@ public class AccountsBillingController(
|
|||||||
IUserService userService,
|
IUserService userService,
|
||||||
IPaymentHistoryService paymentHistoryService) : Controller
|
IPaymentHistoryService paymentHistoryService) : Controller
|
||||||
{
|
{
|
||||||
|
// TODO: Migrate to Query / AccountBillingVNextController
|
||||||
[HttpGet("history")]
|
[HttpGet("history")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<BillingHistoryResponseModel> GetBillingHistoryAsync()
|
public async Task<BillingHistoryResponseModel> GetBillingHistoryAsync()
|
||||||
@@ -30,20 +29,7 @@ public class AccountsBillingController(
|
|||||||
return new BillingHistoryResponseModel(billingInfo);
|
return new BillingHistoryResponseModel(billingInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("payment-method")]
|
// TODO: Migrate to Query / AccountBillingVNextController
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<BillingPaymentResponseModel> GetPaymentMethodAsync()
|
|
||||||
{
|
|
||||||
var user = await userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var billingInfo = await paymentService.GetBillingAsync(user);
|
|
||||||
return new BillingPaymentResponseModel(billingInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("invoices")]
|
[HttpGet("invoices")]
|
||||||
public async Task<IResult> GetInvoicesAsync([FromQuery] string? status = null, [FromQuery] string? startAfter = null)
|
public async Task<IResult> GetInvoicesAsync([FromQuery] string? status = null, [FromQuery] string? startAfter = null)
|
||||||
{
|
{
|
||||||
@@ -62,6 +48,7 @@ public class AccountsBillingController(
|
|||||||
return TypedResults.Ok(invoices);
|
return TypedResults.Ok(invoices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate to Query / AccountBillingVNextController
|
||||||
[HttpGet("transactions")]
|
[HttpGet("transactions")]
|
||||||
public async Task<IResult> GetTransactionsAsync([FromQuery] DateTime? startAfter = null)
|
public async Task<IResult> GetTransactionsAsync([FromQuery] DateTime? startAfter = null)
|
||||||
{
|
{
|
||||||
@@ -78,18 +65,4 @@ public class AccountsBillingController(
|
|||||||
|
|
||||||
return TypedResults.Ok(transactions);
|
return TypedResults.Ok(transactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("preview-invoice")]
|
|
||||||
public async Task<IResult> PreviewInvoiceAsync([FromBody] PreviewIndividualInvoiceRequestBody model)
|
|
||||||
{
|
|
||||||
var user = await userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var invoice = await paymentService.PreviewInvoiceAsync(model, user.GatewayCustomerId, user.GatewaySubscriptionId);
|
|
||||||
|
|
||||||
return TypedResults.Ok(invoice);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
#nullable enable
|
using Bit.Api.Models.Request;
|
||||||
|
|
||||||
using Bit.Api.Models.Request;
|
|
||||||
using Bit.Api.Models.Request.Accounts;
|
using Bit.Api.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
@@ -29,6 +27,7 @@ public class AccountsController(
|
|||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ILicensingService licensingService) : Controller
|
ILicensingService licensingService) : Controller
|
||||||
{
|
{
|
||||||
|
// TODO: Remove when pm-24996-implement-upgrade-from-free-dialog is removed
|
||||||
[HttpPost("premium")]
|
[HttpPost("premium")]
|
||||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||||
PremiumRequestModel model,
|
PremiumRequestModel model,
|
||||||
@@ -76,6 +75,7 @@ public class AccountsController(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate to Query / AccountBillingVNextController as part of Premium -> Organization upgrade work.
|
||||||
[HttpGet("subscription")]
|
[HttpGet("subscription")]
|
||||||
public async Task<SubscriptionResponseModel> GetSubscriptionAsync(
|
public async Task<SubscriptionResponseModel> GetSubscriptionAsync(
|
||||||
[FromServices] GlobalSettings globalSettings,
|
[FromServices] GlobalSettings globalSettings,
|
||||||
@@ -114,29 +114,7 @@ public class AccountsController(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("payment")]
|
// TODO: Migrate to Command / AccountBillingVNextController as PUT /account/billing/vnext/subscription
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PostPaymentAsync([FromBody] PaymentRequestModel model)
|
|
||||||
{
|
|
||||||
var user = await userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
await userService.ReplacePaymentMethodAsync(user, model.PaymentToken, model.PaymentMethodType!.Value,
|
|
||||||
new TaxInfo
|
|
||||||
{
|
|
||||||
BillingAddressLine1 = model.Line1,
|
|
||||||
BillingAddressLine2 = model.Line2,
|
|
||||||
BillingAddressCity = model.City,
|
|
||||||
BillingAddressState = model.State,
|
|
||||||
BillingAddressCountry = model.Country,
|
|
||||||
BillingAddressPostalCode = model.PostalCode,
|
|
||||||
TaxIdNumber = model.TaxId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("storage")]
|
[HttpPost("storage")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<PaymentResponseModel> PostStorageAsync([FromBody] StorageRequestModel model)
|
public async Task<PaymentResponseModel> PostStorageAsync([FromBody] StorageRequestModel model)
|
||||||
@@ -151,8 +129,11 @@ public class AccountsController(
|
|||||||
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* TODO: A new version of this exists in the AccountBillingVNextController.
|
||||||
|
* The individual-self-hosting-license-uploader.component needs to be updated to use it.
|
||||||
|
* Then, this can be removed.
|
||||||
|
*/
|
||||||
[HttpPost("license")]
|
[HttpPost("license")]
|
||||||
[SelfHosted(SelfHostedOnly = true)]
|
[SelfHosted(SelfHostedOnly = true)]
|
||||||
public async Task PostLicenseAsync(LicenseRequestModel model)
|
public async Task PostLicenseAsync(LicenseRequestModel model)
|
||||||
@@ -172,6 +153,7 @@ public class AccountsController(
|
|||||||
await userService.UpdateLicenseAsync(user, license);
|
await userService.UpdateLicenseAsync(user, license);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate to Command / AccountBillingVNextController as DELETE /account/billing/vnext/subscription
|
||||||
[HttpPost("cancel")]
|
[HttpPost("cancel")]
|
||||||
public async Task PostCancelAsync(
|
public async Task PostCancelAsync(
|
||||||
[FromBody] SubscriptionCancellationRequestModel request,
|
[FromBody] SubscriptionCancellationRequestModel request,
|
||||||
@@ -189,6 +171,7 @@ public class AccountsController(
|
|||||||
user.IsExpired());
|
user.IsExpired());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate to Command / AccountBillingVNextController as POST /account/billing/vnext/subscription/reinstate
|
||||||
[HttpPost("reinstate-premium")]
|
[HttpPost("reinstate-premium")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task PostReinstateAsync()
|
public async Task PostReinstateAsync()
|
||||||
@@ -202,41 +185,6 @@ public class AccountsController(
|
|||||||
await userService.ReinstatePremiumAsync(user);
|
await userService.ReinstatePremiumAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("tax")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<TaxInfoResponseModel> GetTaxInfoAsync(
|
|
||||||
[FromServices] IPaymentService paymentService)
|
|
||||||
{
|
|
||||||
var user = await userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var taxInfo = await paymentService.GetTaxInfoAsync(user);
|
|
||||||
return new TaxInfoResponseModel(taxInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("tax")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PutTaxInfoAsync(
|
|
||||||
[FromBody] TaxInfoUpdateRequestModel model,
|
|
||||||
[FromServices] IPaymentService paymentService)
|
|
||||||
{
|
|
||||||
var user = await userService.GetUserByPrincipalAsync(User);
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
throw new UnauthorizedAccessException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var taxInfo = new TaxInfo
|
|
||||||
{
|
|
||||||
BillingAddressPostalCode = model.PostalCode,
|
|
||||||
BillingAddressCountry = model.Country,
|
|
||||||
};
|
|
||||||
await paymentService.SaveTaxInfoAsync(user, taxInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<IEnumerable<Guid>> GetOrganizationIdsClaimingUserAsync(Guid userId)
|
private async Task<IEnumerable<Guid>> GetOrganizationIdsClaimingUserAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var organizationsClaimingUser = await userService.GetOrganizationsClaimingUserAsync(userId);
|
var organizationsClaimingUser = await userService.GetOrganizationsClaimingUserAsync(userId);
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Billing.Tax.Requests;
|
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Controllers;
|
|
||||||
|
|
||||||
[Route("invoices")]
|
|
||||||
[Authorize("Application")]
|
|
||||||
public class InvoicesController : BaseBillingController
|
|
||||||
{
|
|
||||||
[HttpPost("preview-organization")]
|
|
||||||
public async Task<IResult> PreviewInvoiceAsync(
|
|
||||||
[FromBody] PreviewOrganizationInvoiceRequestBody model,
|
|
||||||
[FromServices] ICurrentContext currentContext,
|
|
||||||
[FromServices] IOrganizationRepository organizationRepository,
|
|
||||||
[FromServices] IPaymentService paymentService)
|
|
||||||
{
|
|
||||||
Organization organization = null;
|
|
||||||
if (model.OrganizationId != default)
|
|
||||||
{
|
|
||||||
if (!await currentContext.EditPaymentMethods(model.OrganizationId))
|
|
||||||
{
|
|
||||||
return Error.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
organization = await organizationRepository.GetByIdAsync(model.OrganizationId);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
return Error.NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var invoice = await paymentService.PreviewInvoiceAsync(model, organization?.GatewayCustomerId,
|
|
||||||
organization?.GatewaySubscriptionId);
|
|
||||||
|
|
||||||
return TypedResults.Ok(invoice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces;
|
|
||||||
using Bit.Core.Billing.Models.Business;
|
|
||||||
using Bit.Core.Billing.Organizations.Models;
|
|
||||||
using Bit.Core.Billing.Organizations.Queries;
|
|
||||||
using Bit.Core.Context;
|
|
||||||
using Bit.Core.Exceptions;
|
|
||||||
using Bit.Core.Models.Api.OrganizationLicenses;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
|
|
||||||
namespace Bit.Api.Billing.Controllers;
|
|
||||||
|
|
||||||
[Route("licenses")]
|
|
||||||
[Authorize("Licensing")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public class LicensesController : Controller
|
|
||||||
{
|
|
||||||
private readonly IUserRepository _userRepository;
|
|
||||||
private readonly IUserService _userService;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
|
||||||
private readonly IGetCloudOrganizationLicenseQuery _getCloudOrganizationLicenseQuery;
|
|
||||||
private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
|
||||||
|
|
||||||
public LicensesController(
|
|
||||||
IUserRepository userRepository,
|
|
||||||
IUserService userService,
|
|
||||||
IOrganizationRepository organizationRepository,
|
|
||||||
IGetCloudOrganizationLicenseQuery getCloudOrganizationLicenseQuery,
|
|
||||||
IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand,
|
|
||||||
ICurrentContext currentContext)
|
|
||||||
{
|
|
||||||
_userRepository = userRepository;
|
|
||||||
_userService = userService;
|
|
||||||
_organizationRepository = organizationRepository;
|
|
||||||
_getCloudOrganizationLicenseQuery = getCloudOrganizationLicenseQuery;
|
|
||||||
_validateBillingSyncKeyCommand = validateBillingSyncKeyCommand;
|
|
||||||
_currentContext = currentContext;
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("user/{id}")]
|
|
||||||
public async Task<UserLicense> GetUser(string id, [FromQuery] string key)
|
|
||||||
{
|
|
||||||
var user = await _userRepository.GetByIdAsync(new Guid(id));
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
else if (!user.LicenseKey.Equals(key))
|
|
||||||
{
|
|
||||||
await Task.Delay(2000);
|
|
||||||
throw new BadRequestException("Invalid license key.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var license = await _userService.GenerateLicenseAsync(user, null);
|
|
||||||
return license;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used by self-hosted installations to get an updated license file
|
|
||||||
/// </summary>
|
|
||||||
[HttpGet("organization/{id}")]
|
|
||||||
public async Task<OrganizationLicense> OrganizationSync(string id, [FromBody] SelfHostedOrganizationLicenseRequestModel model)
|
|
||||||
{
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(new Guid(id));
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException("Organization not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!organization.LicenseKey.Equals(model.LicenseKey))
|
|
||||||
{
|
|
||||||
await Task.Delay(2000);
|
|
||||||
throw new BadRequestException("Invalid license key.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(organization, model.BillingSyncKey))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid Billing Sync Key");
|
|
||||||
}
|
|
||||||
|
|
||||||
var license = await _getCloudOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value);
|
|
||||||
return license;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,9 +20,9 @@ public class OrganizationBillingController(
|
|||||||
IOrganizationBillingService organizationBillingService,
|
IOrganizationBillingService organizationBillingService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IPaymentService paymentService,
|
IPaymentService paymentService,
|
||||||
ISubscriberService subscriberService,
|
|
||||||
IPaymentHistoryService paymentHistoryService) : BaseBillingController
|
IPaymentHistoryService paymentHistoryService) : BaseBillingController
|
||||||
{
|
{
|
||||||
|
// TODO: Remove when pm-25379-use-new-organization-metadata-structure is removed.
|
||||||
[HttpGet("metadata")]
|
[HttpGet("metadata")]
|
||||||
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
||||||
{
|
{
|
||||||
@@ -41,6 +41,7 @@ public class OrganizationBillingController(
|
|||||||
return TypedResults.Ok(metadata);
|
return TypedResults.Ok(metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate to Query / OrganizationBillingVNextController
|
||||||
[HttpGet("history")]
|
[HttpGet("history")]
|
||||||
public async Task<IResult> GetHistoryAsync([FromRoute] Guid organizationId)
|
public async Task<IResult> GetHistoryAsync([FromRoute] Guid organizationId)
|
||||||
{
|
{
|
||||||
@@ -61,6 +62,7 @@ public class OrganizationBillingController(
|
|||||||
return TypedResults.Ok(billingInfo);
|
return TypedResults.Ok(billingInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate to Query / OrganizationBillingVNextController
|
||||||
[HttpGet("invoices")]
|
[HttpGet("invoices")]
|
||||||
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null)
|
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null)
|
||||||
{
|
{
|
||||||
@@ -85,6 +87,7 @@ public class OrganizationBillingController(
|
|||||||
return TypedResults.Ok(invoices);
|
return TypedResults.Ok(invoices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate to Query / OrganizationBillingVNextController
|
||||||
[HttpGet("transactions")]
|
[HttpGet("transactions")]
|
||||||
public async Task<IResult> GetTransactionsAsync([FromRoute] Guid organizationId, [FromQuery] DateTime? startAfter = null)
|
public async Task<IResult> GetTransactionsAsync([FromRoute] Guid organizationId, [FromQuery] DateTime? startAfter = null)
|
||||||
{
|
{
|
||||||
@@ -108,6 +111,7 @@ public class OrganizationBillingController(
|
|||||||
return TypedResults.Ok(transactions);
|
return TypedResults.Ok(transactions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Can be removed once we do away with the organization-plans.component.
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<IResult> GetBillingAsync(Guid organizationId)
|
public async Task<IResult> GetBillingAsync(Guid organizationId)
|
||||||
@@ -131,127 +135,7 @@ public class OrganizationBillingController(
|
|||||||
return TypedResults.Ok(response);
|
return TypedResults.Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("payment-method")]
|
// TODO: Migrate to Command / OrganizationBillingVNextController
|
||||||
public async Task<IResult> GetPaymentMethodAsync([FromRoute] Guid organizationId)
|
|
||||||
{
|
|
||||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
|
||||||
{
|
|
||||||
return Error.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
return Error.NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var paymentMethod = await subscriberService.GetPaymentMethod(organization);
|
|
||||||
|
|
||||||
var response = PaymentMethodResponse.From(paymentMethod);
|
|
||||||
|
|
||||||
return TypedResults.Ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("payment-method")]
|
|
||||||
public async Task<IResult> UpdatePaymentMethodAsync(
|
|
||||||
[FromRoute] Guid organizationId,
|
|
||||||
[FromBody] UpdatePaymentMethodRequestBody requestBody)
|
|
||||||
{
|
|
||||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
|
||||||
{
|
|
||||||
return Error.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
return Error.NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var tokenizedPaymentSource = requestBody.PaymentSource.ToDomain();
|
|
||||||
|
|
||||||
var taxInformation = requestBody.TaxInformation.ToDomain();
|
|
||||||
|
|
||||||
await organizationBillingService.UpdatePaymentMethod(organization, tokenizedPaymentSource, taxInformation);
|
|
||||||
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("payment-method/verify-bank-account")]
|
|
||||||
public async Task<IResult> VerifyBankAccountAsync(
|
|
||||||
[FromRoute] Guid organizationId,
|
|
||||||
[FromBody] VerifyBankAccountRequestBody requestBody)
|
|
||||||
{
|
|
||||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
|
||||||
{
|
|
||||||
return Error.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestBody.DescriptorCode.Length != 6 || !requestBody.DescriptorCode.StartsWith("SM"))
|
|
||||||
{
|
|
||||||
return Error.BadRequest("Statement descriptor should be a 6-character value that starts with 'SM'");
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
return Error.NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
await subscriberService.VerifyBankAccount(organization, requestBody.DescriptorCode);
|
|
||||||
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("tax-information")]
|
|
||||||
public async Task<IResult> GetTaxInformationAsync([FromRoute] Guid organizationId)
|
|
||||||
{
|
|
||||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
|
||||||
{
|
|
||||||
return Error.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
return Error.NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var taxInformation = await subscriberService.GetTaxInformation(organization);
|
|
||||||
|
|
||||||
var response = TaxInformationResponse.From(taxInformation);
|
|
||||||
|
|
||||||
return TypedResults.Ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("tax-information")]
|
|
||||||
public async Task<IResult> UpdateTaxInformationAsync(
|
|
||||||
[FromRoute] Guid organizationId,
|
|
||||||
[FromBody] TaxInformationRequestBody requestBody)
|
|
||||||
{
|
|
||||||
if (!await currentContext.EditPaymentMethods(organizationId))
|
|
||||||
{
|
|
||||||
return Error.Unauthorized();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
return Error.NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
var taxInformation = requestBody.ToDomain();
|
|
||||||
|
|
||||||
await subscriberService.UpdateTaxInformation(organization, taxInformation);
|
|
||||||
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost("setup-business-unit")]
|
[HttpPost("setup-business-unit")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<IResult> SetupBusinessUnitAsync(
|
public async Task<IResult> SetupBusinessUnitAsync(
|
||||||
@@ -280,6 +164,7 @@ public class OrganizationBillingController(
|
|||||||
return TypedResults.Ok(providerId);
|
return TypedResults.Ok(providerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate to Command / OrganizationBillingVNextController
|
||||||
[HttpPost("change-frequency")]
|
[HttpPost("change-frequency")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task<IResult> ChangePlanSubscriptionFrequencyAsync(
|
public async Task<IResult> ChangePlanSubscriptionFrequencyAsync(
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ using Bit.Core.Billing.Services;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Business;
|
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -249,53 +248,6 @@ public class OrganizationsController(
|
|||||||
await organizationService.ReinstateSubscriptionAsync(id);
|
await organizationService.ReinstateSubscriptionAsync(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id:guid}/tax")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<TaxInfoResponseModel> GetTaxInfo(Guid id)
|
|
||||||
{
|
|
||||||
if (!await currentContext.OrganizationOwner(id))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(id);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var taxInfo = await paymentService.GetTaxInfoAsync(organization);
|
|
||||||
return new TaxInfoResponseModel(taxInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id:guid}/tax")]
|
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task PutTaxInfo(Guid id, [FromBody] ExpandedTaxInfoUpdateRequestModel model)
|
|
||||||
{
|
|
||||||
if (!await currentContext.OrganizationOwner(id))
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await organizationRepository.GetByIdAsync(id);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
var taxInfo = new TaxInfo
|
|
||||||
{
|
|
||||||
TaxIdNumber = model.TaxId,
|
|
||||||
BillingAddressLine1 = model.Line1,
|
|
||||||
BillingAddressLine2 = model.Line2,
|
|
||||||
BillingAddressCity = model.City,
|
|
||||||
BillingAddressState = model.State,
|
|
||||||
BillingAddressPostalCode = model.PostalCode,
|
|
||||||
BillingAddressCountry = model.Country,
|
|
||||||
};
|
|
||||||
await paymentService.SaveTaxInfoAsync(organization, taxInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to grant owner access to the Secrets Manager for the organization
|
/// Tries to grant owner access to the Secrets Manager for the organization
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// FIXME: Update this file to be null safe and then delete the line below
|
// FIXME: Update this file to be null safe and then delete the line below
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using Bit.Api.Billing.Models.Requests;
|
|
||||||
using Bit.Api.Billing.Models.Responses;
|
using Bit.Api.Billing.Models.Responses;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
@@ -9,7 +8,6 @@ using Bit.Core.Billing.Providers.Models;
|
|||||||
using Bit.Core.Billing.Providers.Repositories;
|
using Bit.Core.Billing.Providers.Repositories;
|
||||||
using Bit.Core.Billing.Providers.Services;
|
using Bit.Core.Billing.Providers.Services;
|
||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Billing.Tax.Models;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Models.BitStripe;
|
using Bit.Core.Models.BitStripe;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@@ -34,6 +32,7 @@ public class ProviderBillingController(
|
|||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
IUserService userService) : BaseProviderController(currentContext, logger, providerRepository, userService)
|
||||||
{
|
{
|
||||||
|
// TODO: Migrate to Query / ProviderBillingVNextController
|
||||||
[HttpGet("invoices")]
|
[HttpGet("invoices")]
|
||||||
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
|
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid providerId)
|
||||||
{
|
{
|
||||||
@@ -54,6 +53,7 @@ public class ProviderBillingController(
|
|||||||
return TypedResults.Ok(response);
|
return TypedResults.Ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Migrate to Query / ProviderBillingVNextController
|
||||||
[HttpGet("invoices/{invoiceId}")]
|
[HttpGet("invoices/{invoiceId}")]
|
||||||
public async Task<IResult> GenerateClientInvoiceReportAsync([FromRoute] Guid providerId, string invoiceId)
|
public async Task<IResult> GenerateClientInvoiceReportAsync([FromRoute] Guid providerId, string invoiceId)
|
||||||
{
|
{
|
||||||
@@ -76,51 +76,7 @@ public class ProviderBillingController(
|
|||||||
"text/csv");
|
"text/csv");
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("payment-method")]
|
// TODO: Migrate to Query / ProviderBillingVNextController
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("subscription")]
|
[HttpGet("subscription")]
|
||||||
public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId)
|
public async Task<IResult> GetSubscriptionAsync([FromRoute] Guid providerId)
|
||||||
{
|
{
|
||||||
@@ -172,53 +128,4 @@ public class ProviderBillingController(
|
|||||||
|
|
||||||
return TypedResults.Ok(response);
|
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);
|
|
||||||
|
|
||||||
return TypedResults.Ok(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("tax-information")]
|
|
||||||
public async Task<IResult> UpdateTaxInformationAsync(
|
|
||||||
[FromRoute] Guid providerId,
|
|
||||||
[FromBody] TaxInformationRequestBody requestBody)
|
|
||||||
{
|
|
||||||
var (provider, result) = await TryGetBillableProviderForAdminOperation(providerId);
|
|
||||||
|
|
||||||
if (provider == null)
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestBody is not { Country: not null, PostalCode: not null })
|
|
||||||
{
|
|
||||||
return Error.BadRequest("Country and postal code are required to update your tax information.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var taxInformation = new TaxInformation(
|
|
||||||
requestBody.Country,
|
|
||||||
requestBody.PostalCode,
|
|
||||||
requestBody.TaxId,
|
|
||||||
requestBody.TaxIdType,
|
|
||||||
requestBody.Line1,
|
|
||||||
requestBody.Line2,
|
|
||||||
requestBody.City,
|
|
||||||
requestBody.State);
|
|
||||||
|
|
||||||
await subscriberService.UpdateTaxInformation(provider, taxInformation);
|
|
||||||
|
|
||||||
return TypedResults.Ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#nullable enable
|
using Bit.Api.Billing.Attributes;
|
||||||
using Bit.Api.Billing.Attributes;
|
|
||||||
using Bit.Api.Billing.Models.Requests.Premium;
|
using Bit.Api.Billing.Models.Requests.Premium;
|
||||||
using Bit.Api.Utilities;
|
using Bit.Api.Utilities;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
@@ -17,7 +16,7 @@ namespace Bit.Api.Billing.Controllers.VNext;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
[Route("account/billing/vnext/self-host")]
|
[Route("account/billing/vnext/self-host")]
|
||||||
[SelfHosted(SelfHostedOnly = true)]
|
[SelfHosted(SelfHostedOnly = true)]
|
||||||
public class SelfHostedAccountBillingController(
|
public class SelfHostedAccountBillingVNextController(
|
||||||
ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController
|
ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController
|
||||||
{
|
{
|
||||||
[HttpPost("license")]
|
[HttpPost("license")]
|
||||||
@@ -14,7 +14,7 @@ namespace Bit.Api.Billing.Controllers.VNext;
|
|||||||
[Authorize("Application")]
|
[Authorize("Application")]
|
||||||
[Route("organizations/{organizationId:guid}/billing/vnext/self-host")]
|
[Route("organizations/{organizationId:guid}/billing/vnext/self-host")]
|
||||||
[SelfHosted(SelfHostedOnly = true)]
|
[SelfHosted(SelfHostedOnly = true)]
|
||||||
public class SelfHostedBillingController(
|
public class SelfHostedOrganizationBillingVNextController(
|
||||||
IGetOrganizationMetadataQuery getOrganizationMetadataQuery) : BaseBillingController
|
IGetOrganizationMetadataQuery getOrganizationMetadataQuery) : BaseBillingController
|
||||||
{
|
{
|
||||||
[Authorize<MemberOrProviderRequirement>]
|
[Authorize<MemberOrProviderRequirement>]
|
||||||
@@ -6,7 +6,6 @@ using Bit.Core.Billing.Tax.Models;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Stripe;
|
using Stripe;
|
||||||
using PaymentMethod = Bit.Core.Billing.Models.PaymentMethod;
|
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services;
|
namespace Bit.Core.Billing.Services;
|
||||||
|
|
||||||
@@ -64,16 +63,6 @@ public interface ISubscriberService
|
|||||||
ISubscriber subscriber,
|
ISubscriber subscriber,
|
||||||
CustomerGetOptions customerGetOptions = null);
|
CustomerGetOptions customerGetOptions = null);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the account credit, a masked representation of the default payment source and the tax information for the
|
|
||||||
/// provided <paramref name="subscriber"/>. This is essentially a consolidated invocation of the <see cref="GetPaymentSource"/>
|
|
||||||
/// and <see cref="GetTaxInformation"/> methods with a response that includes the customer's <see cref="Stripe.Customer.Balance"/> as account credit in order to cut down on Stripe API calls.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscriber">The subscriber to retrieve payment method for.</param>
|
|
||||||
/// <returns>A <see cref="Models.PaymentMethod"/> containing the subscriber's account credit, payment source and tax information.</returns>
|
|
||||||
Task<PaymentMethod> GetPaymentMethod(
|
|
||||||
ISubscriber subscriber);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves a masked representation of the subscriber's payment source for presentation to a client.
|
/// Retrieves a masked representation of the subscriber's payment source for presentation to a client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -107,16 +96,6 @@ public interface ISubscriberService
|
|||||||
ISubscriber subscriber,
|
ISubscriber subscriber,
|
||||||
SubscriptionGetOptions subscriptionGetOptions = null);
|
SubscriptionGetOptions subscriptionGetOptions = null);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Retrieves the <paramref name="subscriber"/>'s tax information using their Stripe <see cref="Stripe.Customer"/>'s <see cref="Stripe.Customer.Address"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscriber">The subscriber to retrieve the tax information for.</param>
|
|
||||||
/// <returns>A <see cref="TaxInformation"/> representing the <paramref name="subscriber"/>'s tax information.</returns>
|
|
||||||
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="subscriber"/> is <see langword="null"/>.</exception>
|
|
||||||
/// <remarks>This method opts for returning <see langword="null"/> rather than throwing exceptions, making it ideal for surfacing data from API endpoints.</remarks>
|
|
||||||
Task<TaxInformation> GetTaxInformation(
|
|
||||||
ISubscriber subscriber);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Attempts to remove a subscriber's saved payment source. If the Stripe <see cref="Stripe.Customer"/> representing the
|
/// Attempts to remove a subscriber's saved payment source. If the Stripe <see cref="Stripe.Customer"/> representing the
|
||||||
/// <paramref name="subscriber"/> contains a valid <b>"btCustomerId"</b> key in its <see cref="Stripe.Customer.Metadata"/> property,
|
/// <paramref name="subscriber"/> contains a valid <b>"btCustomerId"</b> key in its <see cref="Stripe.Customer.Metadata"/> property,
|
||||||
@@ -147,17 +126,6 @@ public interface ISubscriberService
|
|||||||
ISubscriber subscriber,
|
ISubscriber subscriber,
|
||||||
TaxInformation taxInformation);
|
TaxInformation taxInformation);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Verifies the subscriber's pending bank account using the provided <paramref name="descriptorCode"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscriber">The subscriber to verify the bank account for.</param>
|
|
||||||
/// <param name="descriptorCode">The code attached to a deposit made to the subscriber's bank account in order to ensure they have access to it.
|
|
||||||
/// <a href="https://docs.stripe.com/payments/ach-debit/set-up-payment">Learn more.</a></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
Task VerifyBankAccount(
|
|
||||||
ISubscriber subscriber,
|
|
||||||
string descriptorCode);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates whether the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> exists in the gateway.
|
/// Validates whether the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> exists in the gateway.
|
||||||
/// If the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> is <see langword="null"/> or empty, returns <see langword="true"/>.
|
/// If the <paramref name="subscriber"/>'s <see cref="ISubscriber.GatewayCustomerId"/> is <see langword="null"/> or empty, returns <see langword="true"/>.
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ using Stripe;
|
|||||||
|
|
||||||
using static Bit.Core.Billing.Utilities;
|
using static Bit.Core.Billing.Utilities;
|
||||||
using Customer = Stripe.Customer;
|
using Customer = Stripe.Customer;
|
||||||
using PaymentMethod = Bit.Core.Billing.Models.PaymentMethod;
|
|
||||||
using Subscription = Stripe.Subscription;
|
using Subscription = Stripe.Subscription;
|
||||||
|
|
||||||
namespace Bit.Core.Billing.Services.Implementations;
|
namespace Bit.Core.Billing.Services.Implementations;
|
||||||
@@ -330,38 +329,6 @@ public class SubscriberService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PaymentMethod> GetPaymentMethod(
|
|
||||||
ISubscriber subscriber)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(subscriber);
|
|
||||||
|
|
||||||
var customer = await GetCustomer(subscriber, new CustomerGetOptions
|
|
||||||
{
|
|
||||||
Expand = ["default_source", "invoice_settings.default_payment_method", "subscriptions", "tax_ids"]
|
|
||||||
});
|
|
||||||
|
|
||||||
if (customer == null)
|
|
||||||
{
|
|
||||||
return PaymentMethod.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
var accountCredit = customer.Balance * -1 / 100M;
|
|
||||||
|
|
||||||
var paymentMethod = await GetPaymentSourceAsync(subscriber.Id, customer);
|
|
||||||
|
|
||||||
var subscriptionStatus = customer.Subscriptions
|
|
||||||
.FirstOrDefault(subscription => subscription.Id == subscriber.GatewaySubscriptionId)?
|
|
||||||
.Status;
|
|
||||||
|
|
||||||
var taxInformation = GetTaxInformation(customer);
|
|
||||||
|
|
||||||
return new PaymentMethod(
|
|
||||||
accountCredit,
|
|
||||||
paymentMethod,
|
|
||||||
subscriptionStatus,
|
|
||||||
taxInformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<PaymentSource> GetPaymentSource(
|
public async Task<PaymentSource> GetPaymentSource(
|
||||||
ISubscriber subscriber)
|
ISubscriber subscriber)
|
||||||
{
|
{
|
||||||
@@ -449,16 +416,6 @@ public class SubscriberService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<TaxInformation> GetTaxInformation(
|
|
||||||
ISubscriber subscriber)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(subscriber);
|
|
||||||
|
|
||||||
var customer = await GetCustomerOrThrow(subscriber, new CustomerGetOptions { Expand = ["tax_ids"] });
|
|
||||||
|
|
||||||
return GetTaxInformation(customer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RemovePaymentSource(
|
public async Task RemovePaymentSource(
|
||||||
ISubscriber subscriber)
|
ISubscriber subscriber)
|
||||||
{
|
{
|
||||||
@@ -823,57 +780,6 @@ public class SubscriberService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task VerifyBankAccount(
|
|
||||||
ISubscriber subscriber,
|
|
||||||
string descriptorCode)
|
|
||||||
{
|
|
||||||
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(subscriber.Id);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(setupIntentId))
|
|
||||||
{
|
|
||||||
logger.LogError("No setup intent ID exists to verify for subscriber with ID ({SubscriberID})", subscriber.Id);
|
|
||||||
throw new BillingException();
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await stripeAdapter.SetupIntentVerifyMicroDeposit(setupIntentId,
|
|
||||||
new SetupIntentVerifyMicrodepositsOptions { DescriptorCode = descriptorCode });
|
|
||||||
|
|
||||||
var setupIntent = await stripeAdapter.SetupIntentGet(setupIntentId);
|
|
||||||
|
|
||||||
await stripeAdapter.PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
|
|
||||||
new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId });
|
|
||||||
|
|
||||||
await stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId,
|
|
||||||
new CustomerUpdateOptions
|
|
||||||
{
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
|
||||||
{
|
|
||||||
DefaultPaymentMethod = setupIntent.PaymentMethodId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (StripeException stripeException)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(stripeException.StripeError?.Code))
|
|
||||||
{
|
|
||||||
var message = stripeException.StripeError.Code switch
|
|
||||||
{
|
|
||||||
StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationAttemptsExceeded => "You have exceeded the number of allowed verification attempts. Please contact support.",
|
|
||||||
StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationDescriptorCodeMismatch => "The verification code you provided does not match the one sent to your bank account. Please try again.",
|
|
||||||
StripeConstants.ErrorCodes.PaymentMethodMicroDepositVerificationTimeout => "Your bank account was not verified within the required time period. Please contact support.",
|
|
||||||
_ => BillingException.DefaultMessage
|
|
||||||
};
|
|
||||||
|
|
||||||
throw new BadRequestException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.LogError(stripeException, "An unhandled Stripe exception was thrown while verifying subscriber's ({SubscriberID}) bank account", subscriber.Id);
|
|
||||||
throw new BillingException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> IsValidGatewayCustomerIdAsync(ISubscriber subscriber)
|
public async Task<bool> IsValidGatewayCustomerIdAsync(ISubscriber subscriber)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(subscriber);
|
ArgumentNullException.ThrowIfNull(subscriber);
|
||||||
@@ -970,25 +876,6 @@ public class SubscriberService(
|
|||||||
return PaymentSource.From(setupIntent);
|
return PaymentSource.From(setupIntent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TaxInformation GetTaxInformation(
|
|
||||||
Customer customer)
|
|
||||||
{
|
|
||||||
if (customer.Address == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TaxInformation(
|
|
||||||
customer.Address.Country,
|
|
||||||
customer.Address.PostalCode,
|
|
||||||
customer.TaxIds?.FirstOrDefault()?.Value,
|
|
||||||
customer.TaxIds?.FirstOrDefault()?.Type,
|
|
||||||
customer.Address.Line1,
|
|
||||||
customer.Address.Line2,
|
|
||||||
customer.Address.City,
|
|
||||||
customer.Address.State);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RemoveBraintreeCustomerIdAsync(
|
private async Task RemoveBraintreeCustomerIdAsync(
|
||||||
Customer customer)
|
Customer customer)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -521,49 +521,4 @@ public class ProviderBillingControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region UpdateTaxInformationAsync
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task UpdateTaxInformation_NoCountry_BadRequest(
|
|
||||||
Provider provider,
|
|
||||||
TaxInformationRequestBody requestBody,
|
|
||||||
SutProvider<ProviderBillingController> sutProvider)
|
|
||||||
{
|
|
||||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
|
||||||
|
|
||||||
requestBody.Country = null;
|
|
||||||
|
|
||||||
var result = await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody);
|
|
||||||
|
|
||||||
Assert.IsType<BadRequest<ErrorResponseModel>>(result);
|
|
||||||
|
|
||||||
var response = (BadRequest<ErrorResponseModel>)result;
|
|
||||||
|
|
||||||
Assert.Equal("Country and postal code are required to update your tax information.", response.Value.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task UpdateTaxInformation_Ok(
|
|
||||||
Provider provider,
|
|
||||||
TaxInformationRequestBody requestBody,
|
|
||||||
SutProvider<ProviderBillingController> sutProvider)
|
|
||||||
{
|
|
||||||
ConfigureStableProviderAdminInputs(provider, sutProvider);
|
|
||||||
|
|
||||||
await sutProvider.Sut.UpdateTaxInformationAsync(provider.Id, requestBody);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<ISubscriberService>().Received(1).UpdateTaxInformation(
|
|
||||||
provider, Arg.Is<TaxInformation>(
|
|
||||||
options =>
|
|
||||||
options.Country == requestBody.Country &&
|
|
||||||
options.PostalCode == requestBody.PostalCode &&
|
|
||||||
options.TaxId == requestBody.TaxId &&
|
|
||||||
options.Line1 == requestBody.Line1 &&
|
|
||||||
options.Line2 == requestBody.Line2 &&
|
|
||||||
options.City == requestBody.City &&
|
|
||||||
options.State == requestBody.State));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -328,157 +328,6 @@ public class SubscriberServiceTests
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region GetPaymentMethod
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetPaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
|
||||||
SutProvider<SubscriberService> sutProvider) =>
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetPaymentSource(null));
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetPaymentMethod_WithNegativeStripeAccountBalance_ReturnsCorrectAccountCreditAmount(Organization organization,
|
|
||||||
SutProvider<SubscriberService> sutProvider)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
// Stripe reports balance in cents as a negative number for credit
|
|
||||||
const int stripeAccountBalance = -593; // $5.93 credit (negative cents)
|
|
||||||
const decimal creditAmount = 5.93M; // Same value in dollars
|
|
||||||
|
|
||||||
|
|
||||||
var customer = new Customer
|
|
||||||
{
|
|
||||||
Balance = stripeAccountBalance,
|
|
||||||
Subscriptions = new StripeList<Subscription>()
|
|
||||||
{
|
|
||||||
Data =
|
|
||||||
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
|
|
||||||
},
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettings
|
|
||||||
{
|
|
||||||
DefaultPaymentMethod = new PaymentMethod
|
|
||||||
{
|
|
||||||
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
|
|
||||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
|
|
||||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
|
||||||
options.Expand.Contains("invoice_settings.default_payment_method")
|
|
||||||
&& options.Expand.Contains("subscriptions")
|
|
||||||
&& options.Expand.Contains("tax_ids")))
|
|
||||||
.Returns(customer);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await sutProvider.Sut.GetPaymentMethod(organization);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(creditAmount, result.AccountCredit);
|
|
||||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
|
|
||||||
organization.GatewayCustomerId,
|
|
||||||
Arg.Is<CustomerGetOptions>(options =>
|
|
||||||
options.Expand.Contains("default_source") &&
|
|
||||||
options.Expand.Contains("invoice_settings.default_payment_method") &&
|
|
||||||
options.Expand.Contains("subscriptions") &&
|
|
||||||
options.Expand.Contains("tax_ids")));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetPaymentMethod_WithZeroStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
|
|
||||||
Organization organization, SutProvider<SubscriberService> sutProvider)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
const int stripeAccountBalance = 0;
|
|
||||||
|
|
||||||
var customer = new Customer
|
|
||||||
{
|
|
||||||
Balance = stripeAccountBalance,
|
|
||||||
Subscriptions = new StripeList<Subscription>()
|
|
||||||
{
|
|
||||||
Data =
|
|
||||||
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
|
|
||||||
},
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettings
|
|
||||||
{
|
|
||||||
DefaultPaymentMethod = new PaymentMethod
|
|
||||||
{
|
|
||||||
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
|
|
||||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
|
|
||||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
|
||||||
options.Expand.Contains("invoice_settings.default_payment_method")
|
|
||||||
&& options.Expand.Contains("subscriptions")
|
|
||||||
&& options.Expand.Contains("tax_ids")))
|
|
||||||
.Returns(customer);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await sutProvider.Sut.GetPaymentMethod(organization);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(0, result.AccountCredit);
|
|
||||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
|
|
||||||
organization.GatewayCustomerId,
|
|
||||||
Arg.Is<CustomerGetOptions>(options =>
|
|
||||||
options.Expand.Contains("default_source") &&
|
|
||||||
options.Expand.Contains("invoice_settings.default_payment_method") &&
|
|
||||||
options.Expand.Contains("subscriptions") &&
|
|
||||||
options.Expand.Contains("tax_ids")));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetPaymentMethod_WithPositiveStripeAccountBalance_ReturnsCorrectAccountCreditAmount(
|
|
||||||
Organization organization, SutProvider<SubscriberService> sutProvider)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
const int stripeAccountBalance = 593; // $5.93 charge balance
|
|
||||||
const decimal accountBalance = -5.93M; // account balance
|
|
||||||
var customer = new Customer
|
|
||||||
{
|
|
||||||
Balance = stripeAccountBalance,
|
|
||||||
Subscriptions = new StripeList<Subscription>()
|
|
||||||
{
|
|
||||||
Data =
|
|
||||||
[new Subscription { Id = organization.GatewaySubscriptionId, Status = "active" }]
|
|
||||||
},
|
|
||||||
InvoiceSettings = new CustomerInvoiceSettings
|
|
||||||
{
|
|
||||||
DefaultPaymentMethod = new PaymentMethod
|
|
||||||
{
|
|
||||||
Type = StripeConstants.PaymentMethodTypes.USBankAccount,
|
|
||||||
UsBankAccount = new PaymentMethodUsBankAccount { BankName = "Chase", Last4 = "9999" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId,
|
|
||||||
Arg.Is<CustomerGetOptions>(options => options.Expand.Contains("default_source") &&
|
|
||||||
options.Expand.Contains("invoice_settings.default_payment_method")
|
|
||||||
&& options.Expand.Contains("subscriptions")
|
|
||||||
&& options.Expand.Contains("tax_ids")))
|
|
||||||
.Returns(customer);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await sutProvider.Sut.GetPaymentMethod(organization);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.Equal(accountBalance, result.AccountCredit);
|
|
||||||
await sutProvider.GetDependency<IStripeAdapter>().Received(1).CustomerGetAsync(
|
|
||||||
organization.GatewayCustomerId,
|
|
||||||
Arg.Is<CustomerGetOptions>(options =>
|
|
||||||
options.Expand.Contains("default_source") &&
|
|
||||||
options.Expand.Contains("invoice_settings.default_payment_method") &&
|
|
||||||
options.Expand.Contains("subscriptions") &&
|
|
||||||
options.Expand.Contains("tax_ids")));
|
|
||||||
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region GetPaymentSource
|
#region GetPaymentSource
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
@@ -889,65 +738,6 @@ public class SubscriberServiceTests
|
|||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region GetTaxInformation
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetTaxInformation_NullSubscriber_ThrowsArgumentNullException(
|
|
||||||
SutProvider<SubscriberService> sutProvider) =>
|
|
||||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.GetTaxInformation(null));
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetTaxInformation_NullAddress_ReturnsNull(
|
|
||||||
Organization organization,
|
|
||||||
SutProvider<SubscriberService> sutProvider)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
|
|
||||||
.Returns(new Customer());
|
|
||||||
|
|
||||||
var taxInformation = await sutProvider.Sut.GetTaxInformation(organization);
|
|
||||||
|
|
||||||
Assert.Null(taxInformation);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task GetTaxInformation_Success(
|
|
||||||
Organization organization,
|
|
||||||
SutProvider<SubscriberService> sutProvider)
|
|
||||||
{
|
|
||||||
var address = new Address
|
|
||||||
{
|
|
||||||
Country = "US",
|
|
||||||
PostalCode = "12345",
|
|
||||||
Line1 = "123 Example St.",
|
|
||||||
Line2 = "Unit 1",
|
|
||||||
City = "Example Town",
|
|
||||||
State = "NY"
|
|
||||||
};
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IStripeAdapter>().CustomerGetAsync(organization.GatewayCustomerId, Arg.Any<CustomerGetOptions>())
|
|
||||||
.Returns(new Customer
|
|
||||||
{
|
|
||||||
Address = address,
|
|
||||||
TaxIds = new StripeList<TaxId>
|
|
||||||
{
|
|
||||||
Data = [new TaxId { Value = "tax_id" }]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var taxInformation = await sutProvider.Sut.GetTaxInformation(organization);
|
|
||||||
|
|
||||||
Assert.NotNull(taxInformation);
|
|
||||||
Assert.Equal(address.Country, taxInformation.Country);
|
|
||||||
Assert.Equal(address.PostalCode, taxInformation.PostalCode);
|
|
||||||
Assert.Equal("tax_id", taxInformation.TaxId);
|
|
||||||
Assert.Equal(address.Line1, taxInformation.Line1);
|
|
||||||
Assert.Equal(address.Line2, taxInformation.Line2);
|
|
||||||
Assert.Equal(address.City, taxInformation.City);
|
|
||||||
Assert.Equal(address.State, taxInformation.State);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region RemovePaymentMethod
|
#region RemovePaymentMethod
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task RemovePaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
public async Task RemovePaymentMethod_NullSubscriber_ThrowsArgumentNullException(
|
||||||
@@ -1844,48 +1634,6 @@ public class SubscriberServiceTests
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region VerifyBankAccount
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task VerifyBankAccount_NoSetupIntentId_ThrowsBillingException(
|
|
||||||
Provider provider,
|
|
||||||
SutProvider<SubscriberService> sutProvider) => await ThrowsBillingExceptionAsync(() => sutProvider.Sut.VerifyBankAccount(provider, ""));
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task VerifyBankAccount_MakesCorrectInvocations(
|
|
||||||
Provider provider,
|
|
||||||
SutProvider<SubscriberService> sutProvider)
|
|
||||||
{
|
|
||||||
const string descriptorCode = "SM1234";
|
|
||||||
|
|
||||||
var setupIntent = new SetupIntent
|
|
||||||
{
|
|
||||||
Id = "setup_intent_id",
|
|
||||||
PaymentMethodId = "payment_method_id"
|
|
||||||
};
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntent.Id);
|
|
||||||
|
|
||||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
|
||||||
|
|
||||||
stripeAdapter.SetupIntentGet(setupIntent.Id).Returns(setupIntent);
|
|
||||||
|
|
||||||
await sutProvider.Sut.VerifyBankAccount(provider, descriptorCode);
|
|
||||||
|
|
||||||
await stripeAdapter.Received(1).SetupIntentVerifyMicroDeposit(setupIntent.Id,
|
|
||||||
Arg.Is<SetupIntentVerifyMicrodepositsOptions>(
|
|
||||||
options => options.DescriptorCode == descriptorCode));
|
|
||||||
|
|
||||||
await stripeAdapter.Received(1).PaymentMethodAttachAsync(setupIntent.PaymentMethodId,
|
|
||||||
Arg.Is<PaymentMethodAttachOptions>(
|
|
||||||
options => options.Customer == provider.GatewayCustomerId));
|
|
||||||
|
|
||||||
await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is<CustomerUpdateOptions>(
|
|
||||||
options => options.InvoiceSettings.DefaultPaymentMethod == setupIntent.PaymentMethodId));
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IsValidGatewayCustomerIdAsync
|
#region IsValidGatewayCustomerIdAsync
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
|
|||||||
Reference in New Issue
Block a user