Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion Core/Resgrid.Config/InfoConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public static class InfoConfig
LocationInfo =
"This is the Resgrid system hosted in Central Europe (on OVH). This system services Resgrid customers in the European Union to help with data (GDPR) compliance requirements.",
IsDefault = false,
AppUrl = "https://app.eu-central.resgrid.com",
AppUrl = "https://app-eu-central.resgrid.com",
ApiUrl = "https://api-eu-central.resgrid.com",
AllowsFreeAccounts = false
}
Expand Down
54 changes: 54 additions & 0 deletions Web/Resgrid.Web/Areas/User/Views/Shared/_MinimalLayout.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
@using Microsoft.AspNetCore.Http
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"]</title>
<meta http-equiv="X-UA-Compatible" content="IE=edge">

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only"
asp-fallback-test-property="position"
asp-fallback-test-value="absolute" />

<link rel="stylesheet" href="/css/int-bundle.css" />

<link rel="shortcut icon" href="~/favicon.ico" />

@if (IsSectionDefined("Styles"))
{
@RenderSection("Styles", required: false)
}
</head>
<body class="gray-bg">
<div class="container" style="padding-top: 20px;">
@RenderBody()
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.3/jquery.min.js"
asp-fallback-src="lib/jquery/jquery-1.12.3.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-ugqypGWrzPLdx2zEQTF17cVktjb01piRKaDNnbYGRSxyEoeAm+MKZVtbDUYjxfZ6">
</script>
Comment on lines +30 to +35
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 | 🟠 Major

Fix the jQuery fallback URL.

asp-fallback-src="lib/jquery/jquery-1.12.3.min.js" is route-relative, so when the CDN fallback is needed on /User/... pages the browser will request the wrong path and all page JS will break.

🐛 Proposed fix
-            asp-fallback-src="lib/jquery/jquery-1.12.3.min.js"
+            asp-fallback-src="~/lib/jquery/jquery-1.12.3.min.js"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.3/jquery.min.js"
asp-fallback-src="lib/jquery/jquery-1.12.3.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-ugqypGWrzPLdx2zEQTF17cVktjb01piRKaDNnbYGRSxyEoeAm+MKZVtbDUYjxfZ6">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.3/jquery.min.js"
asp-fallback-src="~/lib/jquery/jquery-1.12.3.min.js"
asp-fallback-test="window.jQuery"
crossorigin="anonymous"
integrity="sha384-ugqypGWrzPLdx2zEQTF17cVktjb01piRKaDNnbYGRSxyEoeAm+MKZVtbDUYjxfZ6">
</script>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Web/Resgrid.Web/Areas/User/Views/Shared/_MinimalLayout.cshtml` around lines
30 - 35, The asp-fallback-src value on the script tag is route-relative and
breaks on nested routes; update the asp-fallback-src on the <script> tag in
_MinimalLayout.cshtml (the attribute
asp-fallback-src="lib/jquery/jquery-1.12.3.min.js") to be application-rooted
(e.g. prefix with "~/lib/..." or "/lib/...") so the browser requests the correct
file regardless of the current route; leave other attributes (asp-fallback-test,
integrity, crossorigin) unchanged.

<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.4.0/js/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
crossorigin="anonymous"
integrity="sha384-vhJnz1OVIdLktyixHY4Uk3OHEwdQqPppqYR8+5mjsauETgLOcEynD9oPHhhz18Nw">
</script>
<script src="~/lib/sweetalert/dist/sweetalert.min.js"></script>

<script>
var resgrid = resgrid || {};
resgrid.absoluteBaseUrl = "@Resgrid.Config.SystemBehaviorConfig.ResgridBaseUrl";
</script>

@if (IsSectionDefined("Scripts"))
{
@RenderSection("Scripts", required: false)
}
</body>
</html>
128 changes: 67 additions & 61 deletions Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
@inject IStringLocalizer<Resgrid.Localization.Areas.User.Subscription.Subscription> localizer
@{
ViewBag.Title = "Resgrid | " + @localizer["SubscriptionHeader"];

var locationName = SystemBehaviorConfig.LocationName ?? "US-West";
var location = InfoConfig.Locations.Find(l => l.Name == locationName) ?? InfoConfig.Locations[0];
var isEU = locationName.StartsWith("EU", StringComparison.OrdinalIgnoreCase);
var currencySymbol = isEU ? "\u20AC" : "$";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

@section Styles
Expand Down Expand Up @@ -319,7 +324,17 @@

@if (Model.Plan == null || Model.Plan.PlanId == 1)
{
<p>Move the blue slider below with the arrows to select the number of Entities (Users + Units) you require. You can also type in the Entity count (in increments of 10) in the text box below. Please the "Buy Yearly" or "Buy Monthly" depending on the payment interval you wish. This will then take you to the Stripe checkout page. Please note you cannot buy a 10 entity pack as that is our free plan.</p>
<p>Select the number of Entities (Users + Units) you require using the slider or text box below.
@if (!isEU) { <text>Your first 10 entities are included at no charge — each</text> } else { <text>Each</text> }
additional pack of 10 entities is billed at the rate shown. Select "Buy Yearly" or "Buy Monthly" to proceed to checkout.</p>

@if (isEU)
{
<div class="alert alert-info" style="font-size:13px;">
<strong>European Region</strong> — Pricing includes GDPR-compliant data hosting. All prices shown in EUR (@currencySymbol) with a regional adjustment. No free tier is available.
</div>
}

<div class="price-box">

<form class="form-horizontal form-pricing" role="form">
Expand All @@ -345,21 +360,19 @@
</div>
</div>
<div class="form-group">
<label for="amount" class="col-sm-6 control-label">Monthly ($): </label>
<label for="amount" class="col-sm-6 control-label">Monthly (@currencySymbol): </label>
<span class="help-text">Monthly billing amount</span>
<div class="col-sm-6">
<input type="hidden" id="amount" class="form-control">
<p class="price lead" id="monthly-label"></p>
<span class="price">.00</span>
<p class="price lead" id="monthly-label">0.00</p>
</div>
</div>
<div class="form-group">
<label for="duration" class="col-sm-6 control-label">Yearly ($): </label>
<label for="duration" class="col-sm-6 control-label">Yearly (@currencySymbol): </label>
<span class="help-text">Yearly (annual) billing amount</span>
<div class="col-sm-6">
<input type="hidden" id="duration" class="form-control">
<p class="price lead" id="yearly-label"></p>
<span class="price">.00</span>
<p class="price lead" id="yearly-label">0.00</p>
</div>
</div>
<hr class="style">
Expand Down Expand Up @@ -627,6 +640,9 @@



var IS_EU = @(isEU ? "true" : "false");
var EU_MULTIPLIER = 1.25;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
$(document).ready(function () {
$("#slider").slider({
animate: true,
Expand All @@ -639,7 +655,7 @@
handle.text($(this).slider("value"));
},
slide: function (event, ui) {
update(1, ui.value); //changed
update(1, ui.value);
}
});

Expand All @@ -663,97 +679,87 @@
}
});

//Added, set initial value.
$("#amount").val(10);
//$("#duration").val(0);
//$("#amount-label").text(0);
$("#amount-input").val(0);
$("#monthly-label").text(0);
$("#yearly-label").text(0);

//$("#duration-label").text(0);
$("#amount-input").val(10);
$("#monthly-label").text('0.00');
$("#yearly-label").text('0.00');

update();
});

//changed. now with parameter
function update(slider, val) {
function update(sliderFlag, sliderVal) {
let handle = $("#handle-text");

//changed. Now, directly take value from ui.value. if not set (initial, will use current value.)
var $amount = slider == 1 ? val : $("#amount").val();
var $duration = slider == 2 ? val : $("#duration").val();

/* commented
$amount = $( "#slider" ).slider( "value" );
$duration = $( "#slider2" ).slider( "value" );
*/
var $amount = sliderFlag == 1 ? sliderVal : $("#amount").val();
var showButtons = IS_EU ? ($amount >= 10) : ($amount > 10);

handle.text($amount);
//$total = "$" + ($amount * $duration);
$("#amount").val($amount);
//$("#amount-label").text($amount);
$("#amount-input").val($amount);

if ($amount > 10) {
const totalCostMonthly = calculateCostFromUsers($amount, true);
const totalCostYearly = calculateCostFromUsers($amount, false);
if (showButtons) {
var totalCostMonthly = calculateCostFromUsers($amount, true);
var totalCostYearly = calculateCostFromUsers($amount, false);

$("#monthly-label").text(totalCostMonthly);
$("#yearly-label").text(totalCostYearly);
$("#monthly-label").text(totalCostMonthly.toFixed(2));
$("#yearly-label").text(totalCostYearly.toFixed(2));

$("#buyYearlyButton").show();
$("#buyMonthlyButton").show();
} else {
$("#monthly-label").text(0);
$("#yearly-label").text(0);
$("#monthly-label").text('0.00');
$("#yearly-label").text('0.00');

$("#buyYearlyButton").hide();
$("#buyMonthlyButton").hide();
}
}


const calculateCostFromUsers = (totalNumUsers, isMonthly) => {
let marginalBreakdownStrs = [];

const pricingTiersMonthly = [
//{ tier: 0, marginalUserSlots: 1, costPerUser: 0.0 },
{ tier: 0, marginalUserSlots: 5, costPerUser: 20.0 },
{ tier: 1, marginalUserSlots: 100, costPerUser: 2.0 },
{ tier: 2, marginalUserSlots: 1000, costPerUser: 1.5 },
{ tier: 3, marginalUserSlots: 5000, costPerUser: 1.0 },
{ tier: 4, marginalUserSlots: 999999999, costPerUser: 0.5 },
{ marginalUserSlots: 1, costPerUser: 0.0 },
{ marginalUserSlots: 5, costPerUser: 20.0 },
{ marginalUserSlots: 100, costPerUser: 2.0 },
{ marginalUserSlots: 1000, costPerUser: 1.5 },
{ marginalUserSlots: 5000, costPerUser: 1.0 },
{ marginalUserSlots: 999999999, costPerUser: 0.5 },
];

const pricingTiersYearly = [
//{ tier: 0, marginalUserSlots: 1, costPerUser: 0.0 },
{ tier: 0, marginalUserSlots: 5, costPerUser: 200.0 },
{ tier: 1, marginalUserSlots: 100, costPerUser: 20.0 },
{ tier: 2, marginalUserSlots: 1000, costPerUser: 15.0 },
{ tier: 3, marginalUserSlots: 5000, costPerUser: 10.0 },
{ tier: 4, marginalUserSlots: 999999999, costPerUser: 5.0 },
{ marginalUserSlots: 1, costPerUser: 0.0 },
{ marginalUserSlots: 5, costPerUser: 200.0 },
{ marginalUserSlots: 100, costPerUser: 20.0 },
{ marginalUserSlots: 1000, costPerUser: 15.0 },
{ marginalUserSlots: 5000, costPerUser: 10.0 },
{ marginalUserSlots: 999999999, costPerUser: 5.0 },
];

let finalCost = 0.0;
let remainingUsers = (totalNumUsers / 10) - 1; // First 10 users are free.
let pricingTiers = isMonthly ? pricingTiersMonthly : pricingTiersYearly;
let remainingUsers = totalNumUsers / 10;
const baseTiers = isMonthly ? pricingTiersMonthly : pricingTiersYearly;

let tiers;
if (IS_EU) {
tiers = [{
marginalUserSlots: baseTiers[0].marginalUserSlots + baseTiers[1].marginalUserSlots,
costPerUser: baseTiers[1].costPerUser
}].concat(baseTiers.slice(2));
} else {
tiers = baseTiers;
}

for (let i = 0; i < pricingTiers.length; i++) {
let tier = pricingTiers[i];
for (let i = 0; i < tiers.length; i++) {
let tier = tiers[i];
if (tier.marginalUserSlots < remainingUsers) {
// calculate cost
finalCost += tier.marginalUserSlots * tier.costPerUser;
marginalBreakdownStrs.push(`${tier.marginalUserSlots} @@ $${tier.costPerUser}`);
// remove the users
remainingUsers -= tier.marginalUserSlots;
} else {
// only need the partial group of the marginal user slots/ potentially all of them
finalCost += tier.costPerUser * remainingUsers;
marginalBreakdownStrs.push(`${remainingUsers} @@ $${tier.costPerUser}`);
remainingUsers = 0;
}
//console.log("RemainingUsers: ", remainingUsers);
}

if (IS_EU) {
finalCost = Math.round(finalCost * EU_MULTIPLIER);
}
Comment on lines +743 to 745
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for EU multiplier or pricing calculation logic on the server side
rg -n -C3 'EU.*[Mm]ultiplier|1\.25' --type cs

Repository: Resgrid/Core

Length of output: 38


🏁 Script executed:

# First, let's examine the file mentioned in the review
wc -l "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml"

Repository: Resgrid/Core

Length of output: 118


🏁 Script executed:

# Read the section around lines 743-745 to see context
sed -n '730,750p' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml" | cat -n

Repository: Resgrid/Core

Length of output: 982


🏁 Script executed:

# Look at the entire calculateCostFromUsers function mentioned in the comment
sed -n '699,748p' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml" | cat -n

Repository: Resgrid/Core

Length of output: 2832


🏁 Script executed:

# Search for EU_MULTIPLIER definition and IS_EU definition
rg -n 'EU_MULTIPLIER|IS_EU|1\.25' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml"

Repository: Resgrid/Core

Length of output: 874


🏁 Script executed:

# Search for server-side subscription pricing logic more broadly
rg -n 'multiplier|EU.*price|pricing' --type cs -i | grep -i 'subscription\|eu\|multiplier' | head -20

Repository: Resgrid/Core

Length of output: 298


🏁 Script executed:

# Find where calculateCostFromUsers is called
rg -n 'calculateCostFromUsers' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml"

Repository: Resgrid/Core

Length of output: 318


🏁 Script executed:

# Look for toFixed usage and finalCost display
rg -n 'toFixed|finalCost' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml" | head -30

Repository: Resgrid/Core

Length of output: 553


🏁 Script executed:

# Search for where the calculated price is displayed to user
rg -n 'cost.*toFixed|price.*toFixed|amount.*toFixed' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml"

Repository: Resgrid/Core

Length of output: 38


🏁 Script executed:

# Search for server-side subscription validation/pricing logic
rg -n 'class.*Subscription|calculatePrice|validate.*price' --type cs -i | head -20

Repository: Resgrid/Core

Length of output: 1401


🏁 Script executed:

# Look at the SubscriptionController to see what it does with amounts
sed -n '1,100p' "Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs" | cat -n

Repository: Resgrid/Core

Length of output: 5002


🏁 Script executed:

# Search for checkout or payment processing
rg -n 'checkout|payment|charge|amount' --type cs "Web/Resgrid.Web/Areas/User/Controllers/SubscriptionController.cs" -i | head -20

Repository: Resgrid/Core

Length of output: 1584


🏁 Script executed:

# Search for Paddle or Stripe integration that validates/charges amounts
rg -n 'Paddle|Stripe|checkout' --type cs | grep -i 'amount\|price\|cost' | head -20

Repository: Resgrid/Core

Length of output: 1007


🏁 Script executed:

# Check what the view sends to backend after calculating cost
rg -n 'ajax\|post\|submit' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml" -i -A2 | head -40

Repository: Resgrid/Core

Length of output: 38


🏁 Script executed:

# Search for where amount/cost is used in checkout flow
rg -n 'checkout|payment|stripe|paddle' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml" -i -B2 -A2 | head -50

Repository: Resgrid/Core

Length of output: 3755


🏁 Script executed:

# Look at the SubscriptionsService to understand pricing
sed -n '1,100p' "Core/Resgrid.Services/SubscriptionsService.cs" | cat -n

Repository: Resgrid/Core

Length of output: 5054


🏁 Script executed:

# Search for how CreatePaddleCheckoutForSub handles pricing
rg -n 'CreatePaddleCheckoutForSub' --type cs -B5 -A15

Repository: Resgrid/Core

Length of output: 7907


🏁 Script executed:

# Search for where the JavaScript-calculated cost is used
rg -n 'totalCostMonthly|totalCostYearly|calculateCostFromUsers' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml" -B2 -A2

Repository: Resgrid/Core

Length of output: 754


🏁 Script executed:

# Look for stripeCheckout and paddleCheckout functions to see what parameters they pass
rg -n 'function.*stripeCheckout|function.*paddleCheckout|const.*stripeCheckout|const.*paddleCheckout' "Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml" -A15

Repository: Resgrid/Core

Length of output: 2115


The Math.round() call is unnecessary for display purposes.

The calculated cost is only displayed to users via toFixed(2) and is not sent to the server for billing. The server uses Paddle/Stripe price IDs and the pack count to determine actual charges. However, Math.round(finalCost * EU_MULTIPLIER) still unnecessarily loses decimal precision for display (e.g., 152.50 rounds to 153, displayed as "153.00").

Use toFixed(2) directly instead:

Suggested fix
    if (IS_EU) {
-       finalCost = Math.round(finalCost * EU_MULTIPLIER);
+       finalCost = parseFloat((finalCost * EU_MULTIPLIER).toFixed(2));
    }

This preserves decimal precision for accurate display without unnecessary rounding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml` around lines 743
- 745, The code uses Math.round when applying the EU multiplier which strips
cents before display; instead, remove Math.round and keep the full-precision
multiplication (i.e., set finalCost = finalCost * EU_MULTIPLIER or just multiply
when formatting) and rely on toFixed(2) when rendering; update the IS_EU branch
that references finalCost and EU_MULTIPLIER so finalCost retains decimal
precision for display.


return finalCost;
Expand Down
Loading
Loading