diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs index 86cbdb92c3..28f156fa39 100644 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -7,7 +7,11 @@ public record OrganizationMetadataResponse( bool IsManaged, bool IsOnSecretsManagerStandalone, bool IsSubscriptionUnpaid, - bool HasSubscription) + bool HasSubscription, + bool HasOpenInvoice, + DateTime? InvoiceDueDate, + DateTime? InvoiceCreatedDate, + DateTime? SubPeriodEndDate) { public static OrganizationMetadataResponse From(OrganizationMetadata metadata) => new( @@ -15,5 +19,9 @@ public record OrganizationMetadataResponse( metadata.IsManaged, metadata.IsOnSecretsManagerStandalone, metadata.IsSubscriptionUnpaid, - metadata.HasSubscription); + metadata.HasSubscription, + metadata.HasOpenInvoice, + metadata.InvoiceDueDate, + metadata.InvoiceCreatedDate, + metadata.SubPeriodEndDate); } diff --git a/src/Core/Billing/Models/OrganizationMetadata.cs b/src/Core/Billing/Models/OrganizationMetadata.cs index 5bdb450dc6..b6442e4c19 100644 --- a/src/Core/Billing/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Models/OrganizationMetadata.cs @@ -5,4 +5,8 @@ public record OrganizationMetadata( bool IsManaged, bool IsOnSecretsManagerStandalone, bool IsSubscriptionUnpaid, - bool HasSubscription); + bool HasSubscription, + bool HasOpenInvoice, + DateTime? InvoiceDueDate, + DateTime? InvoiceCreatedDate, + DateTime? SubPeriodEndDate); diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index eadc589625..6d9c275444 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -68,19 +68,25 @@ public class OrganizationBillingService( if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) { return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false, - false, false); + false, false, false, null, null, null); } var customer = await subscriberService.GetCustomer(organization, new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] }); var subscription = await subscriberService.GetSubscription(organization); + var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription); var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription); var hasSubscription = true; + var openInvoice = await HasOpenInvoiceAsync(subscription); + var hasOpenInvoice = openInvoice.HasOpenInvoice; + var invoiceDueDate = openInvoice.DueDate; + var invoiceCreatedDate = openInvoice.CreatedDate; + var subPeriodEndDate = subscription?.CurrentPeriodEnd; return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone, - isSubscriptionUnpaid, hasSubscription); + isSubscriptionUnpaid, hasSubscription, hasOpenInvoice, invoiceDueDate, invoiceCreatedDate, subPeriodEndDate); } public async Task UpdatePaymentMethod( @@ -393,6 +399,18 @@ public class OrganizationBillingService( return subscription.Status == "unpaid"; } + private async Task<(bool HasOpenInvoice, DateTime? CreatedDate, DateTime? DueDate)> HasOpenInvoiceAsync(Subscription subscription) + { + if (subscription?.LatestInvoiceId == null) + { + return (false, null, null); + } + var invoice = await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()); + + return invoice?.Status == "open" + ? (true, invoice.Created, invoice.DueDate) + : (false, null, null); + } #endregion } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 54ff734dc9..e0c5564ede 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -163,6 +163,7 @@ public static class FeatureFlagKeys public const string AuthenticatorSynciOS = "enable-authenticator-sync-ios"; public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android"; public const string AppReviewPrompt = "app-review-prompt"; + public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs"; public static List GetAllKeys() { diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index 703475fc57..d500fb354a 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests { sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true, true, true, true, true)); + .Returns(new OrganizationMetadata(true, true, true, true, true, true, null, null, null)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId);