From 9dfee0a7c71b19bab548bcb680ac4d680993c398 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 11 Aug 2025 13:03:04 +0000
Subject: [PATCH 01/12] Initial plan
From 2395dcf070c77632c863a9af2e063a92a049c041 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 11 Aug 2025 13:11:58 +0000
Subject: [PATCH 02/12] Update Stripe.net from 47.4.0 to 48.0.2 and fix
breaking changes
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Exceptionless.Core.csproj | 2 +-
.../Controllers/OrganizationController.cs | 24 ++++++++++++++-----
2 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/src/Exceptionless.Core/Exceptionless.Core.csproj b/src/Exceptionless.Core/Exceptionless.Core.csproj
index d5c1a7023..73b13d447 100644
--- a/src/Exceptionless.Core/Exceptionless.Core.csproj
+++ b/src/Exceptionless.Core/Exceptionless.Core.csproj
@@ -31,7 +31,7 @@
-
+
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 4a2bb3d18..0f81fd901 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -246,10 +246,11 @@ public async Task> GetInvoiceAsync(string id)
foreach (var line in stripeInvoice.Lines.Data)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
- if (line.Plan is not null)
+ if (line.Price is not null)
{
- string planName = line.Plan.Nickname ?? _billingManager.GetBillingPlan(line.Plan.Id)?.Name ?? line.Plan.Id;
- item.Description = $"Exceptionless - {planName} Plan ({(line.Plan.Amount / 100.0):c}/{line.Plan.Interval})";
+ string planName = line.Price.Nickname ?? _billingManager.GetBillingPlan(line.Price.Id)?.Name ?? line.Price.Id;
+ var intervalText = line.Price.Recurring?.Interval ?? "one-time";
+ item.Description = $"Exceptionless - {planName} Plan ({(line.Price.UnitAmount / 100.0):c}/{intervalText})";
}
var periodStart = line.Period.Start >= DateTime.MinValue ? line.Period.Start : stripeInvoice.PeriodStart;
@@ -429,7 +430,6 @@ public async Task> ChangePlanAsync(string id, str
var createCustomer = new CustomerCreateOptions
{
Source = stripeToken,
- Plan = planId,
Description = organization.Name,
Email = CurrentUser.EmailAddress
};
@@ -439,6 +439,18 @@ public async Task> ChangePlanAsync(string id, str
var customer = await customerService.CreateAsync(createCustomer);
+ // Create subscription separately since Plan is deprecated in CustomerCreateOptions
+ var subscriptionCreateOptions = new SubscriptionCreateOptions
+ {
+ Customer = customer.Id,
+ Items = [new SubscriptionItemOptions { Price = planId }]
+ };
+
+ if (!String.IsNullOrWhiteSpace(couponId))
+ subscriptionCreateOptions.Coupon = couponId;
+
+ await subscriptionService.CreateAsync(subscriptionCreateOptions);
+
organization.BillingStatus = BillingStatus.Active;
organization.RemoveSuspension();
organization.StripeCustomerId = customer.Id;
@@ -466,12 +478,12 @@ public async Task> ChangePlanAsync(string id, str
var subscription = subscriptionList.FirstOrDefault(s => !s.CanceledAt.HasValue);
if (subscription is not null)
{
- update.Items.Add(new SubscriptionItemOptions { Id = subscription.Items.Data[0].Id, Plan = planId });
+ update.Items.Add(new SubscriptionItemOptions { Id = subscription.Items.Data[0].Id, Price = planId });
await subscriptionService.UpdateAsync(subscription.Id, update);
}
else
{
- create.Items.Add(new SubscriptionItemOptions { Plan = planId });
+ create.Items.Add(new SubscriptionItemOptions { Price = planId });
await subscriptionService.CreateAsync(create);
}
From b2479a2f6e8bb4666ca34f517f56b9f1b78afb4f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 25 Aug 2025 00:41:34 +0000
Subject: [PATCH 03/12] Fix Stripe.net v48 breaking changes: handle nullable
UnitAmount and collection expressions
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Controllers/OrganizationController.cs | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 0f81fd901..011089d94 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -250,7 +250,8 @@ public async Task> GetInvoiceAsync(string id)
{
string planName = line.Price.Nickname ?? _billingManager.GetBillingPlan(line.Price.Id)?.Name ?? line.Price.Id;
var intervalText = line.Price.Recurring?.Interval ?? "one-time";
- item.Description = $"Exceptionless - {planName} Plan ({(line.Price.UnitAmount / 100.0):c}/{intervalText})";
+ var priceAmount = line.Price.UnitAmount.HasValue ? (line.Price.UnitAmount.Value / 100.0) : 0.0;
+ item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
}
var periodStart = line.Period.Start >= DateTime.MinValue ? line.Period.Start : stripeInvoice.PeriodStart;
@@ -443,7 +444,7 @@ public async Task> ChangePlanAsync(string id, str
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
Customer = customer.Id,
- Items = [new SubscriptionItemOptions { Price = planId }]
+ Items = new List { new SubscriptionItemOptions { Price = planId } }
};
if (!String.IsNullOrWhiteSpace(couponId))
@@ -458,8 +459,8 @@ public async Task> ChangePlanAsync(string id, str
}
else
{
- var update = new SubscriptionUpdateOptions { Items = [] };
- var create = new SubscriptionCreateOptions { Customer = organization.StripeCustomerId, Items = [] };
+ var update = new SubscriptionUpdateOptions { Items = new List() };
+ var create = new SubscriptionCreateOptions { Customer = organization.StripeCustomerId, Items = new List() };
bool cardUpdated = false;
var customerUpdateOptions = new CustomerUpdateOptions { Description = organization.Name };
From 1f13deda8cb7b85fb358a24f39b692b2b7383207 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 26 Aug 2025 03:44:09 +0000
Subject: [PATCH 04/12] Fix remaining Stripe.net v48 breaking changes:
Invoice.Paid, Invoice.Discounts, and Coupon handling
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Controllers/OrganizationController.cs | 25 ++++++++++++++-----
1 file changed, 19 insertions(+), 6 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 011089d94..4d94fd75d 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -239,7 +239,7 @@ public async Task> GetInvoiceAsync(string id)
OrganizationId = organization.Id,
OrganizationName = organization.Name,
Date = stripeInvoice.Created,
- Paid = stripeInvoice.Paid,
+ Paid = stripeInvoice.Status == "paid",
Total = stripeInvoice.Total / 100.0m
};
@@ -260,7 +260,7 @@ public async Task> GetInvoiceAsync(string id)
invoice.Items.Add(item);
}
- var coupon = stripeInvoice.Discount?.Coupon;
+ var coupon = stripeInvoice.Discounts?.FirstOrDefault()?.Coupon;
if (coupon is not null)
{
if (coupon.AmountOff.HasValue)
@@ -435,9 +435,6 @@ public async Task> ChangePlanAsync(string id, str
Email = CurrentUser.EmailAddress
};
- if (!String.IsNullOrWhiteSpace(couponId))
- createCustomer.Coupon = couponId;
-
var customer = await customerService.CreateAsync(createCustomer);
// Create subscription separately since Plan is deprecated in CustomerCreateOptions
@@ -447,8 +444,14 @@ public async Task> ChangePlanAsync(string id, str
Items = new List { new SubscriptionItemOptions { Price = planId } }
};
+ // Apply coupon as discount if provided
if (!String.IsNullOrWhiteSpace(couponId))
- subscriptionCreateOptions.Coupon = couponId;
+ {
+ subscriptionCreateOptions.Discounts = new List
+ {
+ new SubscriptionDiscountOptions { Coupon = couponId }
+ };
+ }
await subscriptionService.CreateAsync(subscriptionCreateOptions);
@@ -485,6 +488,16 @@ public async Task> ChangePlanAsync(string id, str
else
{
create.Items.Add(new SubscriptionItemOptions { Price = planId });
+
+ // Apply coupon as discount if provided
+ if (!String.IsNullOrWhiteSpace(couponId))
+ {
+ create.Discounts = new List
+ {
+ new SubscriptionDiscountOptions { Coupon = couponId }
+ };
+ }
+
await subscriptionService.CreateAsync(create);
}
From 25a00e29e35ff3f32651a95ca55c58f53da982f8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 26 Aug 2025 13:46:14 +0000
Subject: [PATCH 05/12] Apply code review suggestions: use String.Equals for
status comparison and filter deleted discounts
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
src/Exceptionless.Web/Controllers/OrganizationController.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 4d94fd75d..7bb7344b5 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -239,7 +239,7 @@ public async Task> GetInvoiceAsync(string id)
OrganizationId = organization.Id,
OrganizationName = organization.Name,
Date = stripeInvoice.Created,
- Paid = stripeInvoice.Status == "paid",
+ Paid = String.Equals(stripeInvoice.Status, "paid"),
Total = stripeInvoice.Total / 100.0m
};
@@ -260,7 +260,7 @@ public async Task> GetInvoiceAsync(string id)
invoice.Items.Add(item);
}
- var coupon = stripeInvoice.Discounts?.FirstOrDefault()?.Coupon;
+ var coupon = stripeInvoice.Discounts?.FirstOrDefault(d => d.Deleted is false)?.Coupon;
if (coupon is not null)
{
if (coupon.AmountOff.HasValue)
From b0bc8c4a0085a8ed7884c9eecafc6035c1e47bf9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 26 Aug 2025 13:58:59 +0000
Subject: [PATCH 06/12] Fix InvoiceLineItem.Price property access for
Stripe.net v48 compatibility
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Controllers/OrganizationController.cs | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 7bb7344b5..2a9208c7b 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -246,11 +246,13 @@ public async Task> GetInvoiceAsync(string id)
foreach (var line in stripeInvoice.Lines.Data)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
- if (line.Price is not null)
+ // Note: In Stripe.net v48, Price property was removed from InvoiceLineItem
+ // We'll use available properties and fallback to basic information
+ if (!String.IsNullOrEmpty(line.PriceId))
{
- string planName = line.Price.Nickname ?? _billingManager.GetBillingPlan(line.Price.Id)?.Name ?? line.Price.Id;
- var intervalText = line.Price.Recurring?.Interval ?? "one-time";
- var priceAmount = line.Price.UnitAmount.HasValue ? (line.Price.UnitAmount.Value / 100.0) : 0.0;
+ string planName = _billingManager.GetBillingPlan(line.PriceId)?.Name ?? line.PriceId;
+ var intervalText = "one-time"; // Default since Recurring info is not directly available on line item
+ var priceAmount = line.UnitAmount.HasValue ? (line.UnitAmount.Value / 100.0) : 0.0;
item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
}
From 77a7c412296c41bb16bfb5e5ea89c48992cdbe9a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 28 Aug 2025 02:35:05 +0000
Subject: [PATCH 07/12] Fix InvoiceLineItem property access for Stripe.net v48
- remove Price dependency
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Controllers/OrganizationController.cs | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 2a9208c7b..84d15569b 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -247,14 +247,8 @@ public async Task> GetInvoiceAsync(string id)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
// Note: In Stripe.net v48, Price property was removed from InvoiceLineItem
- // We'll use available properties and fallback to basic information
- if (!String.IsNullOrEmpty(line.PriceId))
- {
- string planName = _billingManager.GetBillingPlan(line.PriceId)?.Name ?? line.PriceId;
- var intervalText = "one-time"; // Default since Recurring info is not directly available on line item
- var priceAmount = line.UnitAmount.HasValue ? (line.UnitAmount.Value / 100.0) : 0.0;
- item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
- }
+ // We'll use basic properties and avoid complex price details that are no longer available
+ // The line.Description already contains the necessary information
var periodStart = line.Period.Start >= DateTime.MinValue ? line.Period.Start : stripeInvoice.PeriodStart;
var periodEnd = line.Period.End >= DateTime.MinValue ? line.Period.End : stripeInvoice.PeriodEnd;
From 80a9faffd0c066006790f4254ebf41e3f6640605 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 28 Aug 2025 02:45:07 +0000
Subject: [PATCH 08/12] Attempt to fix InvoiceLineItem.Price access using
separate PriceService calls for Stripe.net v48
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Controllers/OrganizationController.cs | 24 ++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 84d15569b..373878b88 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -215,10 +215,12 @@ public async Task> GetInvoiceAsync(string id)
id = "in_" + id;
Stripe.Invoice? stripeInvoice = null;
+ PriceService? priceService = null;
try
{
var client = new StripeClient(_options.StripeOptions.StripeApiKey);
var invoiceService = new InvoiceService(client);
+ priceService = new PriceService(client);
stripeInvoice = await invoiceService.GetAsync(id);
}
catch (Exception ex)
@@ -246,9 +248,25 @@ public async Task> GetInvoiceAsync(string id)
foreach (var line in stripeInvoice.Lines.Data)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
- // Note: In Stripe.net v48, Price property was removed from InvoiceLineItem
- // We'll use basic properties and avoid complex price details that are no longer available
- // The line.Description already contains the necessary information
+
+ // In Stripe.net v48, the Price object property was removed from InvoiceLineItem
+ // but the price ID should be available as a direct string property
+ if (priceService is not null && !String.IsNullOrEmpty(line.Price))
+ {
+ try
+ {
+ var price = await priceService.GetAsync(line.Price);
+ string planName = price.Nickname ?? _billingManager.GetBillingPlan(price.Id)?.Name ?? price.Id;
+ var intervalText = price.Recurring?.Interval ?? "one-time";
+ var priceAmount = price.UnitAmount.HasValue ? (price.UnitAmount.Value / 100.0) : 0.0;
+ item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
+ }
+ catch (StripeException ex)
+ {
+ _logger.LogWarning(ex, "Failed to get price details for price ID: {PriceId}", line.Price);
+ // Fall back to original description if price lookup fails
+ }
+ }
var periodStart = line.Period.Start >= DateTime.MinValue ? line.Period.Start : stripeInvoice.PeriodStart;
var periodEnd = line.Period.End >= DateTime.MinValue ? line.Period.End : stripeInvoice.PeriodEnd;
From 048364e06daa1e6e02825caef31d0f3e00d108ac Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 28 Aug 2025 03:05:16 +0000
Subject: [PATCH 09/12] Attempt to fix Stripe.net v48 line item Price property
access with expansion
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Controllers/OrganizationController.cs | 38 +++++++++----------
1 file changed, 19 insertions(+), 19 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 373878b88..b6de0d58d 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -215,13 +215,17 @@ public async Task> GetInvoiceAsync(string id)
id = "in_" + id;
Stripe.Invoice? stripeInvoice = null;
- PriceService? priceService = null;
try
{
var client = new StripeClient(_options.StripeOptions.StripeApiKey);
var invoiceService = new InvoiceService(client);
- priceService = new PriceService(client);
- stripeInvoice = await invoiceService.GetAsync(id);
+
+ // In Stripe.net v48, expand line items to include price data
+ var options = new InvoiceGetOptions
+ {
+ Expand = new List { "lines.data.price" }
+ };
+ stripeInvoice = await invoiceService.GetAsync(id, options);
}
catch (Exception ex)
{
@@ -249,23 +253,13 @@ public async Task> GetInvoiceAsync(string id)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
- // In Stripe.net v48, the Price object property was removed from InvoiceLineItem
- // but the price ID should be available as a direct string property
- if (priceService is not null && !String.IsNullOrEmpty(line.Price))
+ // With expansion, the Price property should be available in Stripe.net v48
+ if (line.Price is not null)
{
- try
- {
- var price = await priceService.GetAsync(line.Price);
- string planName = price.Nickname ?? _billingManager.GetBillingPlan(price.Id)?.Name ?? price.Id;
- var intervalText = price.Recurring?.Interval ?? "one-time";
- var priceAmount = price.UnitAmount.HasValue ? (price.UnitAmount.Value / 100.0) : 0.0;
- item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
- }
- catch (StripeException ex)
- {
- _logger.LogWarning(ex, "Failed to get price details for price ID: {PriceId}", line.Price);
- // Fall back to original description if price lookup fails
- }
+ string planName = line.Price.Nickname ?? _billingManager.GetBillingPlan(line.Price.Id)?.Name ?? line.Price.Id;
+ var intervalText = line.Price.Recurring?.Interval ?? "one-time";
+ var priceAmount = line.Price.UnitAmount.HasValue ? (line.Price.UnitAmount.Value / 100.0) : 0.0;
+ item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
}
var periodStart = line.Period.Start >= DateTime.MinValue ? line.Period.Start : stripeInvoice.PeriodStart;
@@ -274,6 +268,12 @@ public async Task> GetInvoiceAsync(string id)
invoice.Items.Add(item);
}
+ var periodStart = line.Period.Start >= DateTime.MinValue ? line.Period.Start : stripeInvoice.PeriodStart;
+ var periodEnd = line.Period.End >= DateTime.MinValue ? line.Period.End : stripeInvoice.PeriodEnd;
+ item.Date = $"{periodStart.ToShortDateString()} - {periodEnd.ToShortDateString()}";
+ invoice.Items.Add(item);
+ }
+
var coupon = stripeInvoice.Discounts?.FirstOrDefault(d => d.Deleted is false)?.Coupon;
if (coupon is not null)
{
From 539e0efe23d74e5e877a8433c659b58ac8a3b8fe Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 28 Aug 2025 03:07:36 +0000
Subject: [PATCH 10/12] Use reflection to explore InvoiceLineItem properties
and fallback to Plan property for v48 compatibility
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Controllers/OrganizationController.cs | 57 ++++++++++++++-----
1 file changed, 44 insertions(+), 13 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index b6de0d58d..0617ff09f 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -219,13 +219,7 @@ public async Task> GetInvoiceAsync(string id)
{
var client = new StripeClient(_options.StripeOptions.StripeApiKey);
var invoiceService = new InvoiceService(client);
-
- // In Stripe.net v48, expand line items to include price data
- var options = new InvoiceGetOptions
- {
- Expand = new List { "lines.data.price" }
- };
- stripeInvoice = await invoiceService.GetAsync(id, options);
+ stripeInvoice = await invoiceService.GetAsync(id);
}
catch (Exception ex)
{
@@ -253,13 +247,50 @@ public async Task> GetInvoiceAsync(string id)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
- // With expansion, the Price property should be available in Stripe.net v48
- if (line.Price is not null)
+ // In Stripe.net v48, the Price object property was removed from InvoiceLineItem
+ // Try to find alternative ways to access price information
+ try
+ {
+ // Log available properties for debugging (in development only)
+ if (_logger.IsEnabled(LogLevel.Debug))
+ {
+ var properties = line.GetType().GetProperties().Select(p => p.Name);
+ _logger.LogDebug("Available InvoiceLineItem properties: {Properties}", string.Join(", ", properties));
+ }
+
+ // Try to get price ID from possible alternative properties
+ string? priceId = null;
+
+ // Check if there's a Plan property (fallback for legacy scenarios)
+ var planProperty = line.GetType().GetProperty("Plan");
+ if (planProperty is not null)
+ {
+ var plan = planProperty.GetValue(line);
+ if (plan is not null)
+ {
+ var planIdProperty = plan.GetType().GetProperty("Id");
+ if (planIdProperty is not null)
+ {
+ priceId = planIdProperty.GetValue(plan) as string;
+ }
+ }
+ }
+
+ // If we have a price ID, try to build the custom description
+ if (!String.IsNullOrEmpty(priceId))
+ {
+ var plan = _billingManager.GetBillingPlan(priceId);
+ if (plan is not null)
+ {
+ // Use billing manager plan data to construct description
+ item.Description = $"Exceptionless - {plan.Name} Plan";
+ }
+ }
+ }
+ catch (Exception ex)
{
- string planName = line.Price.Nickname ?? _billingManager.GetBillingPlan(line.Price.Id)?.Name ?? line.Price.Id;
- var intervalText = line.Price.Recurring?.Interval ?? "one-time";
- var priceAmount = line.Price.UnitAmount.HasValue ? (line.Price.UnitAmount.Value / 100.0) : 0.0;
- item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
+ _logger.LogWarning(ex, "Failed to process price information for invoice line item");
+ // Fall back to original description
}
var periodStart = line.Period.Start >= DateTime.MinValue ? line.Period.Start : stripeInvoice.PeriodStart;
From ba014dce792b7b452cc5f66d3f1f68de24753bed Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 28 Aug 2025 03:09:08 +0000
Subject: [PATCH 11/12] Implement full reflection-based approach for Price
property access in Stripe.net v48
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Controllers/OrganizationController.cs | 91 +++++++++++++------
1 file changed, 63 insertions(+), 28 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index 0617ff09f..be62414f1 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -219,7 +219,13 @@ public async Task> GetInvoiceAsync(string id)
{
var client = new StripeClient(_options.StripeOptions.StripeApiKey);
var invoiceService = new InvoiceService(client);
- stripeInvoice = await invoiceService.GetAsync(id);
+
+ // In Stripe.net v48, expand to include all necessary price information
+ var options = new InvoiceGetOptions
+ {
+ Expand = new List { "lines", "lines.data.price" }
+ };
+ stripeInvoice = await invoiceService.GetAsync(id, options);
}
catch (Exception ex)
{
@@ -247,43 +253,72 @@ public async Task> GetInvoiceAsync(string id)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
- // In Stripe.net v48, the Price object property was removed from InvoiceLineItem
- // Try to find alternative ways to access price information
+ // Try to access price information in multiple ways for Stripe.net v48 compatibility
try
{
- // Log available properties for debugging (in development only)
- if (_logger.IsEnabled(LogLevel.Debug))
- {
- var properties = line.GetType().GetProperties().Select(p => p.Name);
- _logger.LogDebug("Available InvoiceLineItem properties: {Properties}", string.Join(", ", properties));
- }
-
- // Try to get price ID from possible alternative properties
- string? priceId = null;
-
- // Check if there's a Plan property (fallback for legacy scenarios)
- var planProperty = line.GetType().GetProperty("Plan");
- if (planProperty is not null)
+ // First, try the expanded Price property using reflection (safe for v48)
+ var priceProperty = line.GetType().GetProperty("Price");
+ if (priceProperty is not null)
{
- var plan = planProperty.GetValue(line);
- if (plan is not null)
+ var price = priceProperty.GetValue(line);
+ if (price is not null)
{
- var planIdProperty = plan.GetType().GetProperty("Id");
- if (planIdProperty is not null)
+ var priceIdProperty = price.GetType().GetProperty("Id");
+ var nicknameProperty = price.GetType().GetProperty("Nickname");
+ var unitAmountProperty = price.GetType().GetProperty("UnitAmount");
+ var recurringProperty = price.GetType().GetProperty("Recurring");
+
+ if (priceIdProperty is not null)
{
- priceId = planIdProperty.GetValue(plan) as string;
+ var priceId = priceIdProperty.GetValue(price) as string;
+ var nickname = nicknameProperty?.GetValue(price) as string;
+ var unitAmount = unitAmountProperty?.GetValue(price) as long?;
+
+ string planName = nickname ?? _billingManager.GetBillingPlan(priceId)?.Name ?? priceId ?? "Unknown";
+
+ // Get interval from recurring property
+ string intervalText = "one-time";
+ if (recurringProperty is not null)
+ {
+ var recurring = recurringProperty.GetValue(price);
+ if (recurring is not null)
+ {
+ var intervalProperty = recurring.GetType().GetProperty("Interval");
+ if (intervalProperty is not null)
+ {
+ intervalText = intervalProperty.GetValue(recurring) as string ?? "one-time";
+ }
+ }
+ }
+
+ var priceAmount = unitAmount.HasValue ? (unitAmount.Value / 100.0) : 0.0;
+ item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
}
}
}
-
- // If we have a price ID, try to build the custom description
- if (!String.IsNullOrEmpty(priceId))
+ else
{
- var plan = _billingManager.GetBillingPlan(priceId);
- if (plan is not null)
+ // Fallback: Try to access through Plan property (legacy support)
+ var planProperty = line.GetType().GetProperty("Plan");
+ if (planProperty is not null)
{
- // Use billing manager plan data to construct description
- item.Description = $"Exceptionless - {plan.Name} Plan";
+ var plan = planProperty.GetValue(line);
+ if (plan is not null)
+ {
+ var planIdProperty = plan.GetType().GetProperty("Id");
+ if (planIdProperty is not null)
+ {
+ var priceId = planIdProperty.GetValue(plan) as string;
+ if (!String.IsNullOrEmpty(priceId))
+ {
+ var billingPlan = _billingManager.GetBillingPlan(priceId);
+ if (billingPlan is not null)
+ {
+ item.Description = $"Exceptionless - {billingPlan.Name} Plan";
+ }
+ }
+ }
+ }
}
}
}
From c07716a6e269dce47c0882763f7d9a868dc8acbd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 28 Aug 2025 14:09:36 +0000
Subject: [PATCH 12/12] Remove reflection-based approach and use direct Price
property access with expansion
Co-authored-by: niemyjski <1020579+niemyjski@users.noreply.github.com>
---
.../Controllers/OrganizationController.cs | 95 +------------------
1 file changed, 5 insertions(+), 90 deletions(-)
diff --git a/src/Exceptionless.Web/Controllers/OrganizationController.cs b/src/Exceptionless.Web/Controllers/OrganizationController.cs
index be62414f1..2d02746c6 100644
--- a/src/Exceptionless.Web/Controllers/OrganizationController.cs
+++ b/src/Exceptionless.Web/Controllers/OrganizationController.cs
@@ -252,87 +252,13 @@ public async Task> GetInvoiceAsync(string id)
foreach (var line in stripeInvoice.Lines.Data)
{
var item = new InvoiceLineItem { Amount = line.Amount / 100.0m, Description = line.Description };
-
- // Try to access price information in multiple ways for Stripe.net v48 compatibility
- try
+ if (line.Price is not null)
{
- // First, try the expanded Price property using reflection (safe for v48)
- var priceProperty = line.GetType().GetProperty("Price");
- if (priceProperty is not null)
- {
- var price = priceProperty.GetValue(line);
- if (price is not null)
- {
- var priceIdProperty = price.GetType().GetProperty("Id");
- var nicknameProperty = price.GetType().GetProperty("Nickname");
- var unitAmountProperty = price.GetType().GetProperty("UnitAmount");
- var recurringProperty = price.GetType().GetProperty("Recurring");
-
- if (priceIdProperty is not null)
- {
- var priceId = priceIdProperty.GetValue(price) as string;
- var nickname = nicknameProperty?.GetValue(price) as string;
- var unitAmount = unitAmountProperty?.GetValue(price) as long?;
-
- string planName = nickname ?? _billingManager.GetBillingPlan(priceId)?.Name ?? priceId ?? "Unknown";
-
- // Get interval from recurring property
- string intervalText = "one-time";
- if (recurringProperty is not null)
- {
- var recurring = recurringProperty.GetValue(price);
- if (recurring is not null)
- {
- var intervalProperty = recurring.GetType().GetProperty("Interval");
- if (intervalProperty is not null)
- {
- intervalText = intervalProperty.GetValue(recurring) as string ?? "one-time";
- }
- }
- }
-
- var priceAmount = unitAmount.HasValue ? (unitAmount.Value / 100.0) : 0.0;
- item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
- }
- }
- }
- else
- {
- // Fallback: Try to access through Plan property (legacy support)
- var planProperty = line.GetType().GetProperty("Plan");
- if (planProperty is not null)
- {
- var plan = planProperty.GetValue(line);
- if (plan is not null)
- {
- var planIdProperty = plan.GetType().GetProperty("Id");
- if (planIdProperty is not null)
- {
- var priceId = planIdProperty.GetValue(plan) as string;
- if (!String.IsNullOrEmpty(priceId))
- {
- var billingPlan = _billingManager.GetBillingPlan(priceId);
- if (billingPlan is not null)
- {
- item.Description = $"Exceptionless - {billingPlan.Name} Plan";
- }
- }
- }
- }
- }
- }
+ string planName = line.Price.Nickname ?? _billingManager.GetBillingPlan(line.Price.Id)?.Name ?? line.Price.Id;
+ var intervalText = line.Price.Recurring?.Interval ?? "one-time";
+ var priceAmount = line.Price.UnitAmount.HasValue ? (line.Price.UnitAmount.Value / 100.0) : 0.0;
+ item.Description = $"Exceptionless - {planName} Plan ({priceAmount:c}/{intervalText})";
}
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Failed to process price information for invoice line item");
- // Fall back to original description
- }
-
- var periodStart = line.Period.Start >= DateTime.MinValue ? line.Period.Start : stripeInvoice.PeriodStart;
- var periodEnd = line.Period.End >= DateTime.MinValue ? line.Period.End : stripeInvoice.PeriodEnd;
- item.Date = $"{periodStart.ToShortDateString()} - {periodEnd.ToShortDateString()}";
- invoice.Items.Add(item);
- }
var periodStart = line.Period.Start >= DateTime.MinValue ? line.Period.Start : stripeInvoice.PeriodStart;
var periodEnd = line.Period.End >= DateTime.MinValue ? line.Period.End : stripeInvoice.PeriodEnd;
@@ -524,7 +450,6 @@ public async Task> ChangePlanAsync(string id, str
Items = new List { new SubscriptionItemOptions { Price = planId } }
};
- // Apply coupon as discount if provided
if (!String.IsNullOrWhiteSpace(couponId))
{
subscriptionCreateOptions.Discounts = new List
@@ -568,16 +493,6 @@ public async Task> ChangePlanAsync(string id, str
else
{
create.Items.Add(new SubscriptionItemOptions { Price = planId });
-
- // Apply coupon as discount if provided
- if (!String.IsNullOrWhiteSpace(couponId))
- {
- create.Discounts = new List
- {
- new SubscriptionDiscountOptions { Coupon = couponId }
- };
- }
-
await subscriptionService.CreateAsync(create);
}