diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 2d49a5e276..6e0706278b 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -115,6 +115,19 @@ namespace Bit.Api.Controllers return new OrganizationResponseModel(organization); } + [HttpPut("{id}/payment")] + [HttpPost("{id}/payment")] + public async Task PutPayment(string id, [FromBody]OrganizationPaymentRequestModel model) + { + var orgIdGuid = new Guid(id); + if(!_currentContext.OrganizationOwner(orgIdGuid)) + { + throw new NotFoundException(); + } + + await _organizationService.ReplacePaymentMethodAsync(orgIdGuid, model.PaymentToken); + } + [HttpDelete("{id}")] [HttpPost("{id}/delete")] public async Task Delete(string id) diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs index cb0c7d6ef8..0cf3487b5c 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs @@ -19,7 +19,7 @@ namespace Bit.Core.Models.Api public PlanType PlanType { get; set; } [Required] public string Key { get; set; } - public string CardToken { get; set; } + public string PaymentToken { get; set; } [Range(0, double.MaxValue)] public short AdditionalUsers { get; set; } public bool Monthly { get; set; } @@ -32,7 +32,7 @@ namespace Bit.Core.Models.Api OwnerKey = Key, Name = Name, Plan = PlanType, - PaymentToken = CardToken, + PaymentToken = PaymentToken, AdditionalUsers = AdditionalUsers, BillingEmail = BillingEmail, BusinessName = BusinessName, @@ -42,9 +42,9 @@ namespace Bit.Core.Models.Api public IEnumerable Validate(ValidationContext validationContext) { - if(PlanType != PlanType.Free && string.IsNullOrWhiteSpace(CardToken)) + if(PlanType != PlanType.Free && string.IsNullOrWhiteSpace(PaymentToken)) { - yield return new ValidationResult("Card required.", new string[] { nameof(CardToken) }); + yield return new ValidationResult("Payment required.", new string[] { nameof(PaymentToken) }); } } } diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationPaymentRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationPaymentRequestModel.cs new file mode 100644 index 0000000000..d00c8b3445 --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/OrganizationPaymentRequestModel.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class OrganizationPaymentRequestModel + { + [Required] + public string PaymentToken { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/OrganizationResponseModel.cs b/src/Core/Models/Api/Response/OrganizationResponseModel.cs index 9a548bae4e..801303bd16 100644 --- a/src/Core/Models/Api/Response/OrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationResponseModel.cs @@ -58,7 +58,8 @@ namespace Bit.Core.Models.Api switch(source.Type) { case SourceType.Card: - Description = $"{source.Card.Brand}, *{source.Card.Last4}"; + Description = $"{source.Card.Brand}, *{source.Card.Last4}, " + + $"{source.Card.ExpirationMonth}/{source.Card.ExpirationYear}"; CardBrand = source.Card.Brand; break; case SourceType.BankAccount: diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index a0b0914c80..f90946c63f 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -9,6 +9,7 @@ namespace Bit.Core.Services public interface IOrganizationService { Task GetBillingAsync(Organization organization); + Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken); Task> SignUpAsync(OrganizationSignup organizationSignup); Task InviteUserAsync(Guid organizationId, Guid invitingUserId, string email, Enums.OrganizationUserType type, IEnumerable subvaults); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index db0eefc708..1775029558 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -51,7 +51,19 @@ namespace Bit.Core.Services var customer = await customerService.GetAsync(organization.StripeCustomerId); if(customer != null) { - orgBilling.PaymentSource = customer.DefaultSource; + if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId) && customer.Sources?.Data != null) + { + if(customer.DefaultSourceId.StartsWith("card_")) + { + orgBilling.PaymentSource = + customer.Sources.Data.FirstOrDefault(s => s.Card?.Id == customer.DefaultSourceId); + } + else if(customer.DefaultSourceId.StartsWith("ba_")) + { + orgBilling.PaymentSource = + customer.Sources.Data.FirstOrDefault(s => s.BankAccount?.Id == customer.DefaultSourceId); + } + } var charges = await chargeService.ListAsync(new StripeChargeListOptions { @@ -74,6 +86,47 @@ namespace Bit.Core.Services return orgBilling; } + public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if(organization == null) + { + throw new NotFoundException(); + } + + var cardService = new StripeCardService(); + var customerService = new StripeCustomerService(); + StripeCustomer customer = null; + + if(!string.IsNullOrWhiteSpace(organization.StripeCustomerId)) + { + customer = await customerService.GetAsync(organization.StripeCustomerId); + } + + if(customer == null) + { + customer = await customerService.CreateAsync(new StripeCustomerCreateOptions + { + Description = organization.BusinessName, + Email = organization.BillingEmail, + SourceToken = paymentToken + }); + + organization.StripeCustomerId = customer.Id; + await _organizationRepository.ReplaceAsync(organization); + } + + await cardService.CreateAsync(customer.Id, new StripeCardCreateOptions + { + SourceToken = paymentToken + }); + + if(!string.IsNullOrWhiteSpace(customer.DefaultSourceId)) + { + await cardService.DeleteAsync(customer.Id, customer.DefaultSourceId); + } + } + public async Task> SignUpAsync(OrganizationSignup signup) { var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan && !p.Disabled); @@ -87,7 +140,8 @@ namespace Bit.Core.Services StripeCustomer customer = null; StripeSubscription subscription = null; - if(signup.AdditionalUsers > plan.MaxAdditionalUsers.GetValueOrDefault(0)) + if(plan.CanBuyAdditionalUsers && plan.MaxAdditionalUsers.HasValue && + signup.AdditionalUsers > plan.MaxAdditionalUsers.Value) { throw new BadRequestException($"Selected plan allows a maximum of " + $"{plan.MaxAdditionalUsers.GetValueOrDefault(0)} additional users."); @@ -143,7 +197,7 @@ namespace Bit.Core.Services PlanType = plan.Type, MaxUsers = (short)(plan.BaseUsers + (plan.CanBuyAdditionalUsers ? signup.AdditionalUsers : 0)), MaxSubvaults = plan.MaxSubvaults, - Plan = plan.ToString(), + Plan = plan.Name, StripeCustomerId = customer?.Id, StripeSubscriptionId = subscription?.Id, CreationDate = DateTime.UtcNow,