diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 5c0b524f88..2d49a5e276 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -54,7 +54,7 @@ namespace Bit.Api.Controllers } [HttpGet("{id}/billing")] - public async Task GetBilling(string id) + public async Task GetBilling(string id) { var orgIdGuid = new Guid(id); if(!_currentContext.OrganizationOwner(orgIdGuid)) @@ -68,9 +68,13 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } - // TODO: billing stuff + var billingInfo = await _organizationService.GetBillingAsync(organization); + if(billingInfo == null) + { + throw new NotFoundException(); + } - return new OrganizationResponseModel(organization); + return new OrganizationBillingResponseModel(organization, billingInfo); } [HttpGet("")] diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationUpdateRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationUpdateRequestModel.cs index 32918065e6..b866f12892 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationUpdateRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationUpdateRequestModel.cs @@ -1,14 +1,25 @@ using Bit.Core.Models.Table; +using System.ComponentModel.DataAnnotations; namespace Bit.Core.Models.Api { public class OrganizationUpdateRequestModel { + [Required] + [StringLength(50)] public string Name { get; set; } + [StringLength(50)] + public string BusinessName { get; set; } + [EmailAddress] + [Required] + [StringLength(50)] + public string BillingEmail { get; set; } public virtual Organization ToOrganization(Organization existingOrganization) { existingOrganization.Name = Name; + existingOrganization.BusinessName = BusinessName; + existingOrganization.BillingEmail = BillingEmail; return existingOrganization; } } diff --git a/src/Core/Models/Api/Response/OrganizationResponseModel.cs b/src/Core/Models/Api/Response/OrganizationResponseModel.cs index a7954fd22a..422ebf1706 100644 --- a/src/Core/Models/Api/Response/OrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationResponseModel.cs @@ -1,5 +1,9 @@ using System; +using System.Linq; using Bit.Core.Models.Table; +using System.Collections.Generic; +using Bit.Core.Models.Business; +using Stripe; namespace Bit.Core.Models.Api { @@ -15,6 +19,8 @@ namespace Bit.Core.Models.Api Id = organization.Id.ToString(); Name = organization.Name; + BusinessName = organization.BusinessName; + BillingEmail = organization.BillingEmail; Plan = organization.Plan; PlanType = organization.PlanType; PlanTrial = organization.PlanTrial; @@ -23,9 +29,120 @@ namespace Bit.Core.Models.Api public string Id { get; set; } public string Name { get; set; } + public string BusinessName { get; set; } + public string BillingEmail { get; set; } public string Plan { get; set; } public Enums.PlanType PlanType { get; set; } public bool PlanTrial { get; set; } public short MaxUsers { get; set; } } + + public class OrganizationBillingResponseModel : OrganizationResponseModel + { + public OrganizationBillingResponseModel(Organization organization, OrganizationBilling billing) + : base(organization, "organizationBilling") + { + PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null; + Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null; + Charges = billing.Charges.Select(c => new BillingCharge(c)); + } + + public BillingSource PaymentSource { get; set; } + public BillingSubscription Subscription { get; set; } + public IEnumerable Charges { get; set; } + + public class BillingSource + { + public BillingSource(Source source) + { + Type = source.Type; + + switch(source.Type) + { + case SourceType.Card: + Description = $"{source.Card.Brand}, *{source.Card.Last4}"; + CardBrand = source.Card.Brand; + break; + case SourceType.BankAccount: + Description = $"{source.BankAccount.BankName}, *{source.BankAccount.Last4}"; + break; + // bitcoin/alipay? + default: + break; + } + } + + public SourceType Type { get; set; } + public string CardBrand { get; set; } + public string Description { get; set; } + } + + public class BillingSubscription + { + public BillingSubscription(StripeSubscription sub) + { + Status = sub.Status; + TrialStartDate = sub.TrialStart; + TrialEndDate = sub.TrialEnd; + NextBillDate = sub.CurrentPeriodEnd; + CancelledDate = sub.CanceledAt; + CancelAtNextBillDate = sub.CancelAtPeriodEnd; + if(sub.Items?.Data != null) + { + Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i)); + } + } + + public DateTime? TrialStartDate { get; set; } + public DateTime? TrialEndDate { get; set; } + public DateTime? NextBillDate { get; set; } + public DateTime? CancelledDate { get; set; } + public bool CancelAtNextBillDate { get; set; } + public string Status { get; set; } + public IEnumerable Items { get; set; } = new List(); + + public class BillingSubscriptionItem + { + public BillingSubscriptionItem(StripeSubscriptionItem item) + { + if(item.Plan != null) + { + Name = item.Plan.Name; + Amount = item.Plan.Amount / 100; + Interval = item.Plan.Interval; + } + + Quantity = item.Quantity; + } + + public string Name { get; set; } + public decimal Amount { get; set; } + public int Quantity { get; set; } + public string Interval { get; set; } + } + } + + public class BillingCharge + { + public BillingCharge(StripeCharge charge) + { + Amount = charge.Amount / 100; + RefundedAmount = charge.AmountRefunded / 100; + PaymentSource = charge.Source != null ? new BillingSource(charge.Source) : null; + CreatedDate = charge.Created; + FailureMessage = charge.FailureMessage; + Refunded = charge.Refunded; + Status = charge.Status; + } + + public DateTime CreatedDate { get; set; } + public decimal Amount { get; set; } + public BillingSource PaymentSource { get; set; } + public string Status { get; set; } + public string FailureMessage { get; set; } + public bool Refunded { get; set; } + public bool PartiallyRefunded => !Refunded && RefundedAmount > 0; + public decimal RefundedAmount { get; set; } + } + } } diff --git a/src/Core/Models/Business/OrganizationBilling.cs b/src/Core/Models/Business/OrganizationBilling.cs new file mode 100644 index 0000000000..649162c393 --- /dev/null +++ b/src/Core/Models/Business/OrganizationBilling.cs @@ -0,0 +1,12 @@ +using Stripe; +using System.Collections.Generic; + +namespace Bit.Core.Models.Business +{ + public class OrganizationBilling + { + public Source PaymentSource { get; set; } + public StripeSubscription Subscription { get; set; } + public IEnumerable Charges { get; set; } = new List(); + } +} diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index 7c31cc14c1..a0b0914c80 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -8,6 +8,7 @@ namespace Bit.Core.Services { public interface IOrganizationService { + Task GetBillingAsync(Organization organization); 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 553ef8bf94..efbe91b1ea 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -39,6 +39,40 @@ namespace Bit.Core.Services _dataProtector = dataProtectionProvider.CreateProtector("OrganizationServiceDataProtector"); _mailService = mailService; } + public async Task GetBillingAsync(Organization organization) + { + var orgBilling = new OrganizationBilling(); + var customerService = new StripeCustomerService(); + var subscriptionService = new StripeSubscriptionService(); + var chargeService = new StripeChargeService(); + + if(!string.IsNullOrWhiteSpace(organization.StripeCustomerId)) + { + var customer = await customerService.GetAsync(organization.StripeCustomerId); + if(customer != null) + { + orgBilling.PaymentSource = customer.DefaultSource; + + var charges = await chargeService.ListAsync(new StripeChargeListOptions + { + CustomerId = customer.Id, + Limit = 20 + }); + orgBilling.Charges = charges.OrderByDescending(c => c.Created); + } + } + + if(!string.IsNullOrWhiteSpace(organization.StripeSubscriptionId)) + { + var sub = await subscriptionService.GetAsync(organization.StripeSubscriptionId); + if(sub != null) + { + orgBilling.Subscription = sub; + } + } + + return orgBilling; + } public async Task> SignUpAsync(OrganizationSignup signup) {