Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class CreatePaddleCheckoutResult : BillingApiResponseBase

public class CreatePaddleCheckoutData
{
public string TransactionId { get; set; }
public string PriceId { get; set; }
public string CustomerId { get; set; }
public string Environment { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion Core/Resgrid.Model/Services/ISubscriptionsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ Task<CreateStripeBillingPortalSessionData> CreateStripeSessionForCustomerPortal(

Task<ChangeActiveSubscriptionData> ChangeActiveSubscriptionAsync(string stripeCustomerId, string stripePlanId);

Task<CreatePaddleCheckoutData> CreatePaddleCheckoutForSub(int departmentId, string paddleCustomerId, string paddlePriceId, int planId, string email, string departmentName, int count, string discountCode = null);
Task<CreatePaddleCheckoutData> CreatePaddleCheckoutForSub(int departmentId, string paddleCustomerId, string paddleProductId, int planId, string email, string departmentName, int count, string discountCode = null);

Task<CreatePaddleCheckoutData> CreatePaddleCheckoutForUpdate(int departmentId, string paddleCustomerId, string email, string departmentName);

Expand Down
5 changes: 3 additions & 2 deletions Core/Resgrid.Services/SubscriptionsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,7 @@ public async Task<ChangeActiveSubscriptionData> ChangeActiveSubscriptionAsync(st
return null;
}

public async Task<CreatePaddleCheckoutData> CreatePaddleCheckoutForSub(int departmentId, string paddleCustomerId, string paddlePriceId, int planId, string email, string departmentName, int count, string discountCode = null)
public async Task<CreatePaddleCheckoutData> CreatePaddleCheckoutForSub(int departmentId, string paddleCustomerId, string paddleProductId, int planId, string email, string departmentName, int count, string discountCode = null)
{
if (!String.IsNullOrWhiteSpace(Config.SystemBehaviorConfig.BillingApiBaseUrl) && !String.IsNullOrWhiteSpace(Config.ApiConfig.BackendInternalApikey))
{
Expand All @@ -1233,7 +1233,8 @@ public async Task<CreatePaddleCheckoutData> CreatePaddleCheckoutForSub(int depar
request.AddHeader("Content-Type", "application/json");
request.AddParameter("paddleCustomerId", Uri.EscapeDataString(paddleCustomerId), ParameterType.QueryString);
request.AddParameter("departmentId", departmentId, ParameterType.QueryString);
request.AddParameter("paddlePriceId", paddlePriceId, ParameterType.QueryString);
request.AddParameter("paddleProductId", paddleProductId, ParameterType.QueryString);
request.AddParameter("paddlePriceId", paddleProductId, ParameterType.QueryString);
request.AddParameter("planId", planId, ParameterType.QueryString);
request.AddParameter("count", count, ParameterType.QueryString);
request.AddParameter("email", email, ParameterType.QueryString, true);
Expand Down
13 changes: 12 additions & 1 deletion Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ private static string GetPaddleConfigurationError(string paddleEnvironment, stri
return null;
}

private static string GetPaddleCheckoutProductId(Resgrid.Model.Plan plan)
{
return plan?.GetExternalKey() ?? string.Empty;
}

[HttpGet]
[Authorize]
public async Task<IActionResult> SelectRegistrationPlan(string discountCode = null)
Expand Down Expand Up @@ -782,10 +787,15 @@ public async Task<IActionResult> GetPaddleCheckout(int id, int count, string dis
return BadRequest("Invalid entity pack count.");

var plan = await _subscriptionsService.GetPlanByIdAsync(id);
var paddleProductId = GetPaddleCheckoutProductId(plan);

if (string.IsNullOrWhiteSpace(paddleProductId))
return StatusCode(StatusCodes.Status500InternalServerError, "Paddle checkout is not configured for this plan.");

Comment on lines 789 to +794
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing null-plan guard; 500 is misleading for an invalid id.

GetPlanByIdAsync(id) can return null for an invalid/unknown plan id (see SubscriptionsService.GetPlanByIdAsync which returns null on 404 or missing data). In that case:

  • paddleProductId becomes string.Empty via the helper,
  • The code then returns 500 Internal Server Error with "Paddle checkout is not configured for this plan."

But that is actually a client-supplied bad id, not a server misconfiguration. A 404 (or 400) with a clear message would be more accurate and avoids false "5xx" alerting noise in observability tooling.

🛠️ Proposed fix
 var plan = await _subscriptionsService.GetPlanByIdAsync(id);
+if (plan == null)
+    return NotFound("Plan not found.");
+
 var paddleProductId = GetPaddleCheckoutProductId(plan);

 if (string.IsNullOrWhiteSpace(paddleProductId))
     return StatusCode(StatusCodes.Status500InternalServerError, "Paddle checkout is not configured for this plan.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs` around
lines 789 - 794, Add a null check after calling
_subscriptionsService.GetPlanByIdAsync(id) to handle an invalid client-supplied
id: if plan is null return NotFound("Subscription plan not found.") from the
SubscriptionController action before calling GetPaddleCheckoutProductId(plan).
Keep the subsequent paddleProductId guard for misconfiguration but ensure the
GetPaddleCheckoutProductId(plan) call happens only when plan != null.

var paddleCustomerId = await _departmentSettingsService.GetPaddleCustomerIdForDepartmentAsync(DepartmentId);
var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId);
var user = _usersService.GetUserById(UserId);
var checkout = await _subscriptionsService.CreatePaddleCheckoutForSub(DepartmentId, paddleCustomerId, plan.GetExternalKey(), plan.PlanId, user.Email, department.Name, count, discountCode);
var checkout = await _subscriptionsService.CreatePaddleCheckoutForSub(DepartmentId, paddleCustomerId, paddleProductId, plan.PlanId, user.Email, department.Name, count, discountCode);

bool hasActiveSub = false;
if (!string.IsNullOrWhiteSpace(paddleCustomerId))
Expand All @@ -797,6 +807,7 @@ public async Task<IActionResult> GetPaddleCheckout(int id, int count, string dis

return Json(new
{
TransactionId = checkout?.TransactionId,
PriceId = checkout?.PriceId,
CustomerId = checkout?.CustomerId,
Environment = checkout?.Environment,
Expand Down
12 changes: 12 additions & 0 deletions Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -606,10 +606,22 @@
return;
}

if (data.TransactionId) {
Paddle.Checkout.open({
settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id },
transactionId: data.TransactionId
});
return;
}

if (!data.PriceId) {
swal({ title: "Checkout Error", text: "Unable to create a checkout session. Please try again.", icon: "error", buttons: true, dangerMode: false });
return;
}
if (!/^pri_/.test(data.PriceId)) {
swal({ title: "Checkout Error", text: "Paddle checkout returned an unsupported checkout payload. Please contact support.", icon: "error", buttons: true, dangerMode: false });
return;
}

var checkoutSettings = {
settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,10 +222,21 @@
swal({ title: "Active Subscription", text: "You already have an active subscription.", icon: "warning", buttons: true, dangerMode: false });
return;
}
if (data.TransactionId) {
Paddle.Checkout.open({
settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id },
transactionId: data.TransactionId
});
return;
}
if (!data.PriceId) {
swal({ title: "Checkout Error", text: "Unable to create a checkout session. Please try again.", icon: "error", buttons: true, dangerMode: false });
return;
}
if (!/^pri_/.test(data.PriceId)) {
swal({ title: "Checkout Error", text: "Paddle checkout returned an unsupported checkout payload. Please contact support.", icon: "error", buttons: true, dangerMode: false });
return;
}
var checkoutSettings = {
settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id },
items: [{ priceId: data.PriceId, quantity: packs }]
Expand Down
Loading