diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index eae0760ed0..27d243db7a 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -469,6 +469,7 @@ namespace Bit.Api.Controllers } [HttpGet("billing")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task GetBilling() { var user = await _userService.GetUserByPrincipalAsync(User); @@ -477,20 +478,33 @@ namespace Bit.Api.Controllers throw new UnauthorizedAccessException(); } + var billingInfo = await _paymentService.GetBillingAsync(user); + return new BillingResponseModel(billingInfo); + } + + [HttpGet("subscription")] + public async Task GetSubscription() + { + var user = await _userService.GetUserByPrincipalAsync(User); + if(user == null) + { + throw new UnauthorizedAccessException(); + } + if(!_globalSettings.SelfHosted && user.Gateway != null) { - var billingInfo = await _paymentService.GetBillingAsync(user); - var license = await _userService.GenerateLicenseAsync(user, billingInfo); - return new BillingResponseModel(user, billingInfo, license); + var subscriptionInfo = await _paymentService.GetSubscriptionAsync(user); + var license = await _userService.GenerateLicenseAsync(user, subscriptionInfo); + return new SubscriptionResponseModel(user, subscriptionInfo, license); } else if(!_globalSettings.SelfHosted) { var license = await _userService.GenerateLicenseAsync(user); - return new BillingResponseModel(user, license); + return new SubscriptionResponseModel(user, license); } else { - return new BillingResponseModel(user); + return new SubscriptionResponseModel(user); } } diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index f9e8213e6d..f02158835f 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -10,8 +10,6 @@ using Bit.Core.Services; using Bit.Core; using Bit.Api.Utilities; using Bit.Core.Models.Business; -using Stripe; -using Microsoft.Extensions.Options; using Bit.Core.Utilities; namespace Bit.Api.Controllers @@ -65,7 +63,27 @@ namespace Bit.Api.Controllers } [HttpGet("{id}/billing")] - public async Task GetBilling(string id) + [SelfHosted(NotSelfHostedOnly = true)] + public async Task GetBilling(string id) + { + var orgIdGuid = new Guid(id); + if(!_currentContext.OrganizationOwner(orgIdGuid)) + { + throw new NotFoundException(); + } + + var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); + if(organization == null) + { + throw new NotFoundException(); + } + + var billingInfo = await _paymentService.GetBillingAsync(organization); + return new BillingResponseModel(billingInfo); + } + + [HttpGet("{id}/subscription")] + public async Task GetSubscription(string id) { var orgIdGuid = new Guid(id); if(!_currentContext.OrganizationOwner(orgIdGuid)) @@ -81,49 +99,19 @@ namespace Bit.Api.Controllers if(!_globalSettings.SelfHosted && organization.Gateway != null) { - var billingInfo = await _paymentService.GetBillingAsync(organization); - if(billingInfo == null) + var subscriptionInfo = await _paymentService.GetSubscriptionAsync(organization); + if(subscriptionInfo == null) { throw new NotFoundException(); } - return new OrganizationBillingResponseModel(organization, billingInfo); + return new OrganizationSubscriptionResponseModel(organization, subscriptionInfo); } else { - return new OrganizationBillingResponseModel(organization); + return new OrganizationSubscriptionResponseModel(organization); } } - [HttpGet("{id}/billing-invoice/{invoiceId}")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task GetBillingInvoice(string id, string invoiceId) - { - var orgIdGuid = new Guid(id); - if(!_currentContext.OrganizationOwner(orgIdGuid)) - { - throw new NotFoundException(); - } - - var organization = await _organizationRepository.GetByIdAsync(orgIdGuid); - if(organization == null) - { - throw new NotFoundException(); - } - - try - { - var invoice = await new InvoiceService().GetAsync(invoiceId); - if(invoice != null && invoice.CustomerId == organization.GatewayCustomerId && - !string.IsNullOrWhiteSpace(invoice.HostedInvoiceUrl)) - { - return new RedirectResult(invoice.HostedInvoiceUrl); - } - } - catch(StripeException) { } - - throw new NotFoundException(); - } - [HttpGet("{id}/license")] [SelfHosted(NotSelfHostedOnly = true)] public async Task GetLicense(string id, [FromQuery]Guid installationId) diff --git a/src/Core/Models/Api/Response/BillingResponseModel.cs b/src/Core/Models/Api/Response/BillingResponseModel.cs index f53c0c8e9e..816dd531db 100644 --- a/src/Core/Models/Api/Response/BillingResponseModel.cs +++ b/src/Core/Models/Api/Response/BillingResponseModel.cs @@ -2,54 +2,25 @@ using System.Linq; using System.Collections.Generic; using Bit.Core.Models.Business; -using Bit.Core.Models.Table; using Bit.Core.Enums; namespace Bit.Core.Models.Api { public class BillingResponseModel : ResponseModel { - public BillingResponseModel(User user, BillingInfo billing, UserLicense license) + public BillingResponseModel(BillingInfo billing) : base("billing") { CreditAmount = billing.CreditAmount; PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null; - Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null; Transactions = billing.Transactions?.Select(t => new BillingTransaction(t)); Invoices = billing.Invoices?.Select(i => new BillingInvoice(i)); - UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoiceInfo(billing.UpcomingInvoice) : null; - StorageName = user.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(user.Storage.Value) : null; - StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB - MaxStorageGb = user.MaxStorageGb; - License = license; - Expiration = License.Expires; - } - - public BillingResponseModel(User user, UserLicense license = null) - : base("billing") - { - StorageName = user.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(user.Storage.Value) : null; - StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB - MaxStorageGb = user.MaxStorageGb; - Expiration = user.PremiumExpirationDate; - - if(license != null) - { - License = license; - } } public decimal CreditAmount { get; set; } - public string StorageName { get; set; } - public double? StorageGb { get; set; } - public short? MaxStorageGb { get; set; } public BillingSource PaymentSource { get; set; } - public BillingSubscription Subscription { get; set; } - public BillingInvoiceInfo UpcomingInvoice { get; set; } public IEnumerable Invoices { get; set; } public IEnumerable Transactions { get; set; } - public UserLicense License { get; set; } - public DateTime? Expiration { get; set; } } public class BillingSource @@ -68,74 +39,20 @@ namespace Bit.Core.Models.Api public bool NeedsVerification { get; set; } } - public class BillingSubscription + public class BillingInvoice { - public BillingSubscription(BillingInfo.BillingSubscription sub) - { - Status = sub.Status; - TrialStartDate = sub.TrialStartDate; - TrialEndDate = sub.TrialEndDate; - PeriodStartDate = sub.PeriodStartDate; - PeriodEndDate = sub.PeriodEndDate; - CancelledDate = sub.CancelledDate; - CancelAtEndDate = sub.CancelAtEndDate; - Cancelled = sub.Cancelled; - if(sub.Items != null) - { - Items = sub.Items.Select(i => new BillingSubscriptionItem(i)); - } - } - - public DateTime? TrialStartDate { get; set; } - public DateTime? TrialEndDate { get; set; } - public DateTime? PeriodStartDate { get; set; } - public DateTime? PeriodEndDate { get; set; } - public DateTime? CancelledDate { get; set; } - public bool CancelAtEndDate { get; set; } - public string Status { get; set; } - public bool Cancelled { get; set; } - public IEnumerable Items { get; set; } = new List(); - - public class BillingSubscriptionItem - { - public BillingSubscriptionItem(BillingInfo.BillingSubscription.BillingSubscriptionItem item) - { - Name = item.Name; - Amount = item.Amount; - Interval = item.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 BillingInvoiceInfo - { - public BillingInvoiceInfo(BillingInfo.BillingInvoiceInfo inv) + public BillingInvoice(BillingInfo.BillingInvoice inv) { Amount = inv.Amount; Date = inv.Date; - } - - public decimal Amount { get; set; } - public DateTime? Date { get; set; } - } - - public class BillingInvoice : BillingInvoiceInfo - { - public BillingInvoice(BillingInfo.BillingInvoice inv) - : base(inv) - { Url = inv.Url; PdfUrl = inv.PdfUrl; Number = inv.Number; Paid = inv.Paid; } + public decimal Amount { get; set; } + public DateTime? Date { get; set; } public string Url { get; set; } public string PdfUrl { get; set; } public string Number { get; set; } diff --git a/src/Core/Models/Api/Response/OrganizationResponseModel.cs b/src/Core/Models/Api/Response/OrganizationResponseModel.cs index 03623f3a0c..30cb088161 100644 --- a/src/Core/Models/Api/Response/OrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationResponseModel.cs @@ -60,38 +60,34 @@ namespace Bit.Core.Models.Api public bool UsersGetPremium { get; set; } } - public class OrganizationBillingResponseModel : OrganizationResponseModel + public class OrganizationSubscriptionResponseModel : OrganizationResponseModel { - public OrganizationBillingResponseModel(Organization organization, BillingInfo billing) - : base(organization, "organizationBilling") + public OrganizationSubscriptionResponseModel(Organization organization, SubscriptionInfo subscription = null) + : base(organization, "organizationSubscription") { - PaymentSource = billing.PaymentSource != null ? new BillingSource(billing.PaymentSource) : null; - Subscription = billing.Subscription != null ? new BillingSubscription(billing.Subscription) : null; - Transactions = billing.Transactions?.Select(t => new BillingTransaction(t)); - Invoices = billing.Invoices?.Select(i => new BillingInvoice(i)); - UpcomingInvoice = billing.UpcomingInvoice != null ? new BillingInvoiceInfo(billing.UpcomingInvoice) : null; - StorageName = organization.Storage.HasValue ? - Utilities.CoreHelpers.ReadableBytesSize(organization.Storage.Value) : null; - StorageGb = organization.Storage.HasValue ? Math.Round(organization.Storage.Value / 1073741824D) : 0; // 1 GB - Expiration = DateTime.UtcNow.AddYears(1); - } + if(subscription != null) + { + Subscription = subscription.Subscription != null ? + new BillingSubscription(subscription.Subscription) : null; + UpcomingInvoice = subscription.UpcomingInvoice != null ? + new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null; + Expiration = DateTime.UtcNow.AddYears(1); // TODO? + } + else + { + Expiration = organization.ExpirationDate; + } - public OrganizationBillingResponseModel(Organization organization) - : base(organization, "organizationBilling") - { StorageName = organization.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(organization.Storage.Value) : null; - StorageGb = organization.Storage.HasValue ? Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB - Expiration = organization.ExpirationDate; + StorageGb = organization.Storage.HasValue ? + Math.Round(organization.Storage.Value / 1073741824D, 2) : 0; // 1 GB } public string StorageName { get; set; } public double? StorageGb { get; set; } - public BillingSource PaymentSource { get; set; } public BillingSubscription Subscription { get; set; } - public BillingInvoiceInfo UpcomingInvoice { get; set; } - public IEnumerable Invoices { get; set; } - public IEnumerable Transactions { get; set; } + public BillingSubscriptionUpcomingInvoice UpcomingInvoice { get; set; } public DateTime? Expiration { get; set; } } } diff --git a/src/Core/Models/Api/Response/SubscriptionResponseModel.cs b/src/Core/Models/Api/Response/SubscriptionResponseModel.cs new file mode 100644 index 0000000000..951527f215 --- /dev/null +++ b/src/Core/Models/Api/Response/SubscriptionResponseModel.cs @@ -0,0 +1,103 @@ +using System; +using System.Linq; +using System.Collections.Generic; +using Bit.Core.Models.Business; +using Bit.Core.Models.Table; + +namespace Bit.Core.Models.Api +{ + public class SubscriptionResponseModel : ResponseModel + { + public SubscriptionResponseModel(User user, SubscriptionInfo subscription, UserLicense license) + : base("subscription") + { + Subscription = subscription.Subscription != null ? new BillingSubscription(subscription.Subscription) : null; + UpcomingInvoice = subscription.UpcomingInvoice != null ? + new BillingSubscriptionUpcomingInvoice(subscription.UpcomingInvoice) : null; + StorageName = user.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(user.Storage.Value) : null; + StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB + MaxStorageGb = user.MaxStorageGb; + License = license; + Expiration = License.Expires; + } + + public SubscriptionResponseModel(User user, UserLicense license = null) + : base("subscription") + { + StorageName = user.Storage.HasValue ? Utilities.CoreHelpers.ReadableBytesSize(user.Storage.Value) : null; + StorageGb = user.Storage.HasValue ? Math.Round(user.Storage.Value / 1073741824D, 2) : 0; // 1 GB + MaxStorageGb = user.MaxStorageGb; + Expiration = user.PremiumExpirationDate; + + if(license != null) + { + License = license; + } + } + + public string StorageName { get; set; } + public double? StorageGb { get; set; } + public short? MaxStorageGb { get; set; } + public BillingSubscriptionUpcomingInvoice UpcomingInvoice { get; set; } + public BillingSubscription Subscription { get; set; } + public UserLicense License { get; set; } + public DateTime? Expiration { get; set; } + } + + public class BillingSubscription + { + public BillingSubscription(SubscriptionInfo.BillingSubscription sub) + { + Status = sub.Status; + TrialStartDate = sub.TrialStartDate; + TrialEndDate = sub.TrialEndDate; + PeriodStartDate = sub.PeriodStartDate; + PeriodEndDate = sub.PeriodEndDate; + CancelledDate = sub.CancelledDate; + CancelAtEndDate = sub.CancelAtEndDate; + Cancelled = sub.Cancelled; + if(sub.Items != null) + { + Items = sub.Items.Select(i => new BillingSubscriptionItem(i)); + } + } + + public DateTime? TrialStartDate { get; set; } + public DateTime? TrialEndDate { get; set; } + public DateTime? PeriodStartDate { get; set; } + public DateTime? PeriodEndDate { get; set; } + public DateTime? CancelledDate { get; set; } + public bool CancelAtEndDate { get; set; } + public string Status { get; set; } + public bool Cancelled { get; set; } + public IEnumerable Items { get; set; } = new List(); + + public class BillingSubscriptionItem + { + public BillingSubscriptionItem(SubscriptionInfo.BillingSubscription.BillingSubscriptionItem item) + { + Name = item.Name; + Amount = item.Amount; + Interval = item.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 BillingSubscriptionUpcomingInvoice + { + public BillingSubscriptionUpcomingInvoice(SubscriptionInfo.BillingUpcomingInvoice inv) + { + Amount = inv.Amount; + Date = inv.Date; + } + + public decimal Amount { get; set; } + public DateTime? Date { get; set; } + } +} diff --git a/src/Core/Models/Business/BillingInfo.cs b/src/Core/Models/Business/BillingInfo.cs index ac8a4b6abe..4eb2c1b38f 100644 --- a/src/Core/Models/Business/BillingInfo.cs +++ b/src/Core/Models/Business/BillingInfo.cs @@ -3,7 +3,6 @@ using Bit.Core.Models.Table; using Stripe; using System; using System.Collections.Generic; -using System.Linq; namespace Bit.Core.Models.Business { @@ -11,8 +10,6 @@ namespace Bit.Core.Models.Business { public decimal CreditAmount { get; set; } public BillingSource PaymentSource { get; set; } - public BillingSubscription Subscription { get; set; } - public BillingInvoiceInfo UpcomingInvoice { get; set; } public IEnumerable Charges { get; set; } = new List(); public IEnumerable Invoices { get; set; } = new List(); public IEnumerable Transactions { get; set; } = new List(); @@ -88,136 +85,6 @@ namespace Bit.Core.Models.Business public bool NeedsVerification { get; set; } } - public class BillingSubscription - { - public BillingSubscription(Subscription sub) - { - Status = sub.Status; - TrialStartDate = sub.TrialStart; - TrialEndDate = sub.TrialEnd; - PeriodStartDate = sub.CurrentPeriodStart; - PeriodEndDate = sub.CurrentPeriodEnd; - CancelledDate = sub.CanceledAt; - CancelAtEndDate = sub.CancelAtPeriodEnd; - Cancelled = sub.Status == "canceled" || sub.Status == "unpaid"; - if(sub.Items?.Data != null) - { - Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i)); - } - } - - public BillingSubscription(Braintree.Subscription sub, Braintree.Plan plan) - { - Status = sub.Status.ToString(); - - if(sub.HasTrialPeriod.GetValueOrDefault() && sub.CreatedAt.HasValue && sub.TrialDuration.HasValue) - { - TrialStartDate = sub.CreatedAt.Value; - if(sub.TrialDurationUnit == Braintree.SubscriptionDurationUnit.DAY) - { - TrialEndDate = TrialStartDate.Value.AddDays(sub.TrialDuration.Value); - } - else - { - TrialEndDate = TrialStartDate.Value.AddMonths(sub.TrialDuration.Value); - } - } - - PeriodStartDate = sub.BillingPeriodStartDate; - PeriodEndDate = sub.BillingPeriodEndDate; - - CancelAtEndDate = !sub.NeverExpires.GetValueOrDefault(); - Cancelled = sub.Status == Braintree.SubscriptionStatus.CANCELED; - if(Cancelled) - { - CancelledDate = sub.UpdatedAt.Value; - } - - var items = new List(); - items.Add(new BillingSubscriptionItem(plan)); - if(sub.AddOns != null) - { - items.AddRange(sub.AddOns.Select(a => new BillingSubscriptionItem(plan, a))); - } - - if(items.Count > 0) - { - Items = items; - } - } - - public DateTime? TrialStartDate { get; set; } - public DateTime? TrialEndDate { get; set; } - public DateTime? PeriodStartDate { get; set; } - public DateTime? PeriodEndDate { get; set; } - public TimeSpan? PeriodDuration => PeriodEndDate - PeriodStartDate; - public DateTime? CancelledDate { get; set; } - public bool CancelAtEndDate { get; set; } - public string Status { get; set; } - public bool Cancelled { get; set; } - public IEnumerable Items { get; set; } = new List(); - - public class BillingSubscriptionItem - { - public BillingSubscriptionItem(SubscriptionItem item) - { - if(item.Plan != null) - { - Name = item.Plan.Nickname; - Amount = item.Plan.Amount.GetValueOrDefault() / 100M; - Interval = item.Plan.Interval; - } - - Quantity = (int)item.Quantity; - } - - public BillingSubscriptionItem(Braintree.Plan plan) - { - Name = plan.Name; - Amount = plan.Price.GetValueOrDefault(); - Interval = plan.BillingFrequency.GetValueOrDefault() == 12 ? "year" : "month"; - Quantity = 1; - } - - public BillingSubscriptionItem(Braintree.Plan plan, Braintree.AddOn addon) - { - Name = addon.Name; - Amount = addon.Amount.GetValueOrDefault(); - Interval = plan.BillingFrequency.GetValueOrDefault() == 12 ? "year" : "month"; - Quantity = addon.Quantity.GetValueOrDefault(); - } - - public string Name { get; set; } - public decimal Amount { get; set; } - public int Quantity { get; set; } - public string Interval { get; set; } - } - } - - public class BillingInvoiceInfo - { - public BillingInvoiceInfo() { } - - public BillingInvoiceInfo(Invoice inv) - { - Amount = inv.AmountDue / 100M; - Date = inv.Date.Value; - } - - public BillingInvoiceInfo(Braintree.Subscription sub) - { - Amount = sub.NextBillAmount.GetValueOrDefault() + sub.Balance.GetValueOrDefault(); - if(Amount < 0) - { - Amount = 0; - } - Date = sub.NextBillingDate; - } - - public decimal Amount { get; set; } - public DateTime? Date { get; set; } - } - public class BillingCharge { public BillingCharge(Charge charge) @@ -309,10 +176,12 @@ namespace Bit.Core.Models.Business public string Details { get; set; } } - public class BillingInvoice : BillingInvoiceInfo + public class BillingInvoice { public BillingInvoice(Invoice inv) { + Amount = inv.AmountDue / 100M; + Date = inv.Date.Value; Url = inv.HostedInvoiceUrl; PdfUrl = inv.InvoicePdf; Number = inv.Number; @@ -321,6 +190,8 @@ namespace Bit.Core.Models.Business Date = inv.Date.Value; } + public decimal Amount { get; set; } + public DateTime? Date { get; set; } public string Url { get; set; } public string PdfUrl { get; set; } public string Number { get; set; } diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index 3769835c7e..300146b4e0 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -16,7 +16,7 @@ namespace Bit.Core.Models.Business public OrganizationLicense() { } - public OrganizationLicense(Organization org, BillingInfo billingInfo, Guid installationId, + public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId, ILicensingService licenseService) { Version = 4; @@ -41,7 +41,7 @@ namespace Bit.Core.Models.Business UsersGetPremium = org.UsersGetPremium; Issued = DateTime.UtcNow; - if(billingInfo?.Subscription == null) + if(subscriptionInfo?.Subscription == null) { if(org.PlanType == PlanType.Custom && org.ExpirationDate.HasValue) { @@ -54,10 +54,10 @@ namespace Bit.Core.Models.Business Trial = true; } } - else if(billingInfo.Subscription.TrialEndDate.HasValue && - billingInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow) + else if(subscriptionInfo.Subscription.TrialEndDate.HasValue && + subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow) { - Expires = Refresh = billingInfo.Subscription.TrialEndDate.Value; + Expires = Refresh = subscriptionInfo.Subscription.TrialEndDate.Value; Trial = true; } else @@ -67,11 +67,11 @@ namespace Bit.Core.Models.Business // expired Expires = Refresh = org.ExpirationDate.Value; } - else if(billingInfo?.Subscription?.PeriodDuration != null && - billingInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180)) + else if(subscriptionInfo?.Subscription?.PeriodDuration != null && + subscriptionInfo.Subscription.PeriodDuration > TimeSpan.FromDays(180)) { Refresh = DateTime.UtcNow.AddDays(30); - Expires = billingInfo?.Subscription.PeriodEndDate.Value.AddDays(60); + Expires = subscriptionInfo?.Subscription.PeriodEndDate.Value.AddDays(60); } else { diff --git a/src/Core/Models/Business/SubscriptionInfo.cs b/src/Core/Models/Business/SubscriptionInfo.cs new file mode 100644 index 0000000000..b329d504b7 --- /dev/null +++ b/src/Core/Models/Business/SubscriptionInfo.cs @@ -0,0 +1,143 @@ +using Stripe; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Core.Models.Business +{ + public class SubscriptionInfo + { + public BillingSubscription Subscription { get; set; } + public BillingUpcomingInvoice UpcomingInvoice { get; set; } + + public class BillingSubscription + { + public BillingSubscription(Subscription sub) + { + Status = sub.Status; + TrialStartDate = sub.TrialStart; + TrialEndDate = sub.TrialEnd; + PeriodStartDate = sub.CurrentPeriodStart; + PeriodEndDate = sub.CurrentPeriodEnd; + CancelledDate = sub.CanceledAt; + CancelAtEndDate = sub.CancelAtPeriodEnd; + Cancelled = sub.Status == "canceled" || sub.Status == "unpaid"; + if(sub.Items?.Data != null) + { + Items = sub.Items.Data.Select(i => new BillingSubscriptionItem(i)); + } + } + + public BillingSubscription(Braintree.Subscription sub, Braintree.Plan plan) + { + Status = sub.Status.ToString(); + + if(sub.HasTrialPeriod.GetValueOrDefault() && sub.CreatedAt.HasValue && sub.TrialDuration.HasValue) + { + TrialStartDate = sub.CreatedAt.Value; + if(sub.TrialDurationUnit == Braintree.SubscriptionDurationUnit.DAY) + { + TrialEndDate = TrialStartDate.Value.AddDays(sub.TrialDuration.Value); + } + else + { + TrialEndDate = TrialStartDate.Value.AddMonths(sub.TrialDuration.Value); + } + } + + PeriodStartDate = sub.BillingPeriodStartDate; + PeriodEndDate = sub.BillingPeriodEndDate; + + CancelAtEndDate = !sub.NeverExpires.GetValueOrDefault(); + Cancelled = sub.Status == Braintree.SubscriptionStatus.CANCELED; + if(Cancelled) + { + CancelledDate = sub.UpdatedAt.Value; + } + + var items = new List(); + items.Add(new BillingSubscriptionItem(plan)); + if(sub.AddOns != null) + { + items.AddRange(sub.AddOns.Select(a => new BillingSubscriptionItem(plan, a))); + } + + if(items.Count > 0) + { + Items = items; + } + } + + public DateTime? TrialStartDate { get; set; } + public DateTime? TrialEndDate { get; set; } + public DateTime? PeriodStartDate { get; set; } + public DateTime? PeriodEndDate { get; set; } + public TimeSpan? PeriodDuration => PeriodEndDate - PeriodStartDate; + public DateTime? CancelledDate { get; set; } + public bool CancelAtEndDate { get; set; } + public string Status { get; set; } + public bool Cancelled { get; set; } + public IEnumerable Items { get; set; } = new List(); + + public class BillingSubscriptionItem + { + public BillingSubscriptionItem(SubscriptionItem item) + { + if(item.Plan != null) + { + Name = item.Plan.Nickname; + Amount = item.Plan.Amount.GetValueOrDefault() / 100M; + Interval = item.Plan.Interval; + } + + Quantity = (int)item.Quantity; + } + + public BillingSubscriptionItem(Braintree.Plan plan) + { + Name = plan.Name; + Amount = plan.Price.GetValueOrDefault(); + Interval = plan.BillingFrequency.GetValueOrDefault() == 12 ? "year" : "month"; + Quantity = 1; + } + + public BillingSubscriptionItem(Braintree.Plan plan, Braintree.AddOn addon) + { + Name = addon.Name; + Amount = addon.Amount.GetValueOrDefault(); + Interval = plan.BillingFrequency.GetValueOrDefault() == 12 ? "year" : "month"; + Quantity = addon.Quantity.GetValueOrDefault(); + } + + public string Name { get; set; } + public decimal Amount { get; set; } + public int Quantity { get; set; } + public string Interval { get; set; } + } + } + + public class BillingUpcomingInvoice + { + public BillingUpcomingInvoice() { } + + public BillingUpcomingInvoice(Invoice inv) + { + Amount = inv.AmountDue / 100M; + Date = inv.Date.Value; + } + + public BillingUpcomingInvoice(Braintree.Subscription sub) + { + Amount = sub.NextBillAmount.GetValueOrDefault() + sub.Balance.GetValueOrDefault(); + if(Amount < 0) + { + Amount = 0; + } + Date = sub.NextBillingDate; + } + + public decimal Amount { get; set; } + public DateTime? Date { get; set; } + } + } +} diff --git a/src/Core/Models/Business/UserLicense.cs b/src/Core/Models/Business/UserLicense.cs index 71a629c78f..4d6ff3188e 100644 --- a/src/Core/Models/Business/UserLicense.cs +++ b/src/Core/Models/Business/UserLicense.cs @@ -15,7 +15,7 @@ namespace Bit.Core.Models.Business public UserLicense() { } - public UserLicense(User user, BillingInfo billingInfo, ILicensingService licenseService) + public UserLicense(User user, SubscriptionInfo subscriptionInfo, ILicensingService licenseService) { LicenseKey = user.LicenseKey; Id = user.Id; @@ -25,10 +25,10 @@ namespace Bit.Core.Models.Business Premium = user.Premium; MaxStorageGb = user.MaxStorageGb; Issued = DateTime.UtcNow; - Expires = billingInfo?.UpcomingInvoice?.Date?.AddDays(7); - Refresh = billingInfo?.UpcomingInvoice?.Date; - Trial = (billingInfo?.Subscription?.TrialEndDate.HasValue ?? false) && - billingInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow; + Expires = subscriptionInfo?.UpcomingInvoice?.Date?.AddDays(7); + Refresh = subscriptionInfo?.UpcomingInvoice?.Date; + Trial = (subscriptionInfo?.Subscription?.TrialEndDate.HasValue ?? false) && + subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow; Hash = Convert.ToBase64String(ComputeHash()); Signature = Convert.ToBase64String(licenseService.SignLicense(this)); diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index cc220ce7ce..8f509644fc 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -17,7 +17,7 @@ namespace Bit.Core.Services Task ReinstateSubscriptionAsync(ISubscriber subscriber); Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, string paymentToken); - Task GetUpcomingInvoiceAsync(ISubscriber subscriber); Task GetBillingAsync(ISubscriber subscriber); + Task GetSubscriptionAsync(ISubscriber subscriber); } } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 0ee83ce77f..6bcaa5fe0d 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -52,7 +52,7 @@ namespace Bit.Core.Services Task DisablePremiumAsync(Guid userId, DateTime? expirationDate); Task DisablePremiumAsync(User user, DateTime? expirationDate); Task UpdatePremiumExpirationAsync(Guid userId, DateTime? expirationDate); - Task GenerateLicenseAsync(User user, BillingInfo billingInfo = null); + Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null); Task CheckPasswordAsync(User user, string password); Task CanAccessPremium(ITwoFactorProvidersUser user); Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 1321cb24ca..de61b1b9e5 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1207,8 +1207,8 @@ namespace Bit.Core.Services throw new BadRequestException("Invalid installation id"); } - var billingInfo = await _paymentService.GetBillingAsync(organization); - return new OrganizationLicense(organization, billingInfo, installationId, _licensingService); + var subInfo = await _paymentService.GetSubscriptionAsync(organization); + return new OrganizationLicense(organization, subInfo, installationId, _licensingService); } public async Task ImportAsync(Guid organizationId, diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index b6db5f87bb..98a2d773e7 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -885,35 +885,6 @@ namespace Bit.Core.Services return createdCustomer; } - public async Task GetUpcomingInvoiceAsync(ISubscriber subscriber) - { - if(!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) - { - var subscriptionService = new SubscriptionService(); - var invoiceService = new InvoiceService(); - var sub = await subscriptionService.GetAsync(subscriber.GatewaySubscriptionId); - if(sub != null) - { - if(!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) - { - try - { - var upcomingInvoice = await invoiceService.UpcomingAsync(new UpcomingInvoiceOptions - { - CustomerId = subscriber.GatewayCustomerId - }); - if(upcomingInvoice != null) - { - return new BillingInfo.BillingInvoiceInfo(upcomingInvoice); - } - } - catch(StripeException) { } - } - } - } - return null; - } - public async Task GetBillingAsync(ISubscriber subscriber) { var billingInfo = new BillingInfo(); @@ -990,12 +961,21 @@ namespace Bit.Core.Services } } + return billingInfo; + } + + public async Task GetSubscriptionAsync(ISubscriber subscriber) + { + var subscriptionInfo = new SubscriptionInfo(); + var subscriptionService = new SubscriptionService(); + var invoiceService = new InvoiceService(); + if(!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId)) { var sub = await subscriptionService.GetAsync(subscriber.GatewaySubscriptionId); if(sub != null) { - billingInfo.Subscription = new BillingInfo.BillingSubscription(sub); + subscriptionInfo.Subscription = new SubscriptionInfo.BillingSubscription(sub); } if(!sub.CanceledAt.HasValue && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) @@ -1006,14 +986,15 @@ namespace Bit.Core.Services new UpcomingInvoiceOptions { CustomerId = subscriber.GatewayCustomerId }); if(upcomingInvoice != null) { - billingInfo.UpcomingInvoice = new BillingInfo.BillingInvoiceInfo(upcomingInvoice); + subscriptionInfo.UpcomingInvoice = + new SubscriptionInfo.BillingUpcomingInvoice(upcomingInvoice); } } catch(StripeException) { } } } - return billingInfo; + return subscriptionInfo; } } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 4afb22049b..e483bcc43a 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -868,20 +868,20 @@ namespace Bit.Core.Services } } - public async Task GenerateLicenseAsync(User user, BillingInfo billingInfo = null) + public async Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null) { if(user == null) { throw new NotFoundException(); } - if(billingInfo == null && user.Gateway != null) + if(subscriptionInfo == null && user.Gateway != null) { - billingInfo = await _paymentService.GetBillingAsync(user); + subscriptionInfo = await _paymentService.GetSubscriptionAsync(user); } - return billingInfo == null ? new UserLicense(user, _licenseService) : - new UserLicense(user, billingInfo, _licenseService); + return subscriptionInfo == null ? new UserLicense(user, _licenseService) : + new UserLicense(user, subscriptionInfo, _licenseService); } public override async Task CheckPasswordAsync(User user, string password)