-
-
Notifications
You must be signed in to change notification settings - Fork 79
RE1-T112 Fixes #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RE1-T112 Fixes #327
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| <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> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" : "$"; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| } | ||
|
|
||
| @section Styles | ||
|
|
@@ -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"> | ||
|
|
@@ -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"> | ||
|
|
@@ -627,6 +640,9 @@ | |
|
|
||
|
|
||
|
|
||
| var IS_EU = @(isEU ? "true" : "false"); | ||
| var EU_MULTIPLIER = 1.25; | ||
|
|
||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| $(document).ready(function () { | ||
| $("#slider").slider({ | ||
| animate: true, | ||
|
|
@@ -639,7 +655,7 @@ | |
| handle.text($(this).slider("value")); | ||
| }, | ||
| slide: function (event, ui) { | ||
| update(1, ui.value); //changed | ||
| update(1, ui.value); | ||
| } | ||
| }); | ||
|
|
||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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 csRepository: 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 -nRepository: 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 -nRepository: 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 -20Repository: 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 -30Repository: 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 -20Repository: 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 -nRepository: 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 -20Repository: 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 -20Repository: 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 -40Repository: 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 -50Repository: 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 -nRepository: Resgrid/Core Length of output: 5054 🏁 Script executed: # Search for how CreatePaddleCheckoutForSub handles pricing
rg -n 'CreatePaddleCheckoutForSub' --type cs -B5 -A15Repository: 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 -A2Repository: 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" -A15Repository: Resgrid/Core Length of output: 2115 The The calculated cost is only displayed to users via Use 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 |
||
|
|
||
| return finalCost; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
📝 Committable suggestion
🤖 Prompt for AI Agents