Skip to content

Commit 7f99e3d

Browse files
committed
RE1-T112 PR#327 fixes
1 parent 1955fec commit 7f99e3d

8 files changed

Lines changed: 176 additions & 74 deletions

File tree

Core/Resgrid.Model/DepartmentSettingTypes.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ public enum DepartmentSettingTypes
4545
WeatherAlertAutoMessageSeverity = 41,
4646
WeatherAlertCallIntegration = 42,
4747
WeatherAlertCacheMinutes = 43,
48+
WeatherAlertAutoMessageSchedule = 44,
4849
}
4950
}

Core/Resgrid.Services/WeatherAlertService.cs

Lines changed: 67 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Threading;
55
using System.Threading.Tasks;
6+
using Newtonsoft.Json;
67
using Resgrid.Framework;
78
using Resgrid.Model;
89
using Resgrid.Model.Helpers;
@@ -319,15 +320,27 @@ public async Task SendPendingNotificationsAsync(CancellationToken ct = default)
319320
continue;
320321
}
321322

322-
// Get the auto-message severity threshold setting
323-
var thresholdSetting = await _departmentSettingsRepository.GetDepartmentSettingByIdTypeAsync(
324-
departmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSeverity);
323+
// Load per-severity schedule (if configured)
324+
List<AutoMessageSeveritySchedule> schedule = null;
325+
var scheduleSetting = await _departmentSettingsRepository.GetDepartmentSettingByIdTypeAsync(
326+
departmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSchedule);
327+
if (scheduleSetting != null && !string.IsNullOrWhiteSpace(scheduleSetting.Setting))
328+
{
329+
try { schedule = JsonConvert.DeserializeObject<List<AutoMessageSeveritySchedule>>(scheduleSetting.Setting); }
330+
catch { }
331+
}
325332

326-
int threshold = (int)WeatherAlertSeverity.Severe; // Default: Severe=1
327-
if (thresholdSetting != null && int.TryParse(thresholdSetting.Setting, out var parsed))
328-
threshold = parsed;
333+
// Fall back to legacy threshold if no schedule configured
334+
int legacyThreshold = (int)WeatherAlertSeverity.Severe;
335+
if (schedule == null || schedule.Count == 0)
336+
{
337+
var thresholdSetting = await _departmentSettingsRepository.GetDepartmentSettingByIdTypeAsync(
338+
departmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSeverity);
339+
if (thresholdSetting != null && int.TryParse(thresholdSetting.Setting, out var parsed))
340+
legacyThreshold = parsed;
341+
}
329342

330-
// Load department for sender info
343+
// Load department for sender info and time conversion
331344
Department department = null;
332345
try
333346
{
@@ -340,9 +353,8 @@ public async Task SendPendingNotificationsAsync(CancellationToken ct = default)
340353

341354
foreach (var alert in group)
342355
{
343-
// Only send notifications for alerts meeting severity threshold
344-
// Lower enum value = higher severity (Extreme=0, Severe=1, etc.)
345-
if (alert.Severity <= threshold)
356+
bool shouldSend = ShouldSendAutoMessage(alert.Severity, schedule, legacyThreshold, department);
357+
if (shouldSend)
346358
{
347359
try
348360
{
@@ -594,5 +606,50 @@ private static string Truncate(string value, int maxLength)
594606

595607
return value.Substring(0, maxLength);
596608
}
609+
610+
private static bool ShouldSendAutoMessage(int severity, List<AutoMessageSeveritySchedule> schedule, int legacyThreshold, Department department)
611+
{
612+
if (schedule != null && schedule.Count > 0)
613+
{
614+
var entry = schedule.FirstOrDefault(s => s.Severity == severity);
615+
616+
// Severity not in schedule — don't send
617+
if (entry == null || !entry.Enabled)
618+
return false;
619+
620+
// Check time window (StartHour == 0 && EndHour == 0 means 24h/always)
621+
if (entry.StartHour == 0 && entry.EndHour == 0)
622+
return true;
623+
624+
// Get department local time
625+
var now = DateTime.UtcNow;
626+
if (department != null)
627+
now = now.TimeConverter(department);
628+
629+
int currentHour = now.Hour;
630+
631+
if (entry.StartHour <= entry.EndHour)
632+
{
633+
// Same-day window: e.g. 6-18
634+
return currentHour >= entry.StartHour && currentHour < entry.EndHour;
635+
}
636+
else
637+
{
638+
// Overnight window: e.g. 18-6 (6pm to 6am)
639+
return currentHour >= entry.StartHour || currentHour < entry.EndHour;
640+
}
641+
}
642+
643+
// Legacy: simple severity threshold
644+
return severity <= legacyThreshold;
645+
}
646+
647+
private class AutoMessageSeveritySchedule
648+
{
649+
public int Severity { get; set; }
650+
public bool Enabled { get; set; }
651+
public int StartHour { get; set; }
652+
public int EndHour { get; set; }
653+
}
597654
}
598655
}

Web/Resgrid.Web.Services/Controllers/v4/WeatherAlertsController.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,13 +357,20 @@ public async Task<ActionResult<GetWeatherAlertSettingsResult>> SaveSettings([Fro
357357
await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, input.AutoMessageSeverity.ToString(), DepartmentSettingTypes.WeatherAlertAutoMessageSeverity);
358358
await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, input.CallIntegrationEnabled.ToString(), DepartmentSettingTypes.WeatherAlertCallIntegration);
359359

360+
if (input.AutoMessageSchedule != null)
361+
{
362+
var scheduleJson = Newtonsoft.Json.JsonConvert.SerializeObject(input.AutoMessageSchedule);
363+
await _departmentSettingsService.SaveOrUpdateSettingAsync(DepartmentId, scheduleJson, DepartmentSettingTypes.WeatherAlertAutoMessageSchedule);
364+
}
365+
360366
var result = new GetWeatherAlertSettingsResult();
361367
result.Data = new WeatherAlertSettingsData
362368
{
363369
WeatherAlertsEnabled = input.WeatherAlertsEnabled,
364370
MinimumSeverity = input.MinimumSeverity,
365371
AutoMessageSeverity = input.AutoMessageSeverity,
366-
CallIntegrationEnabled = input.CallIntegrationEnabled
372+
CallIntegrationEnabled = input.CallIntegrationEnabled,
373+
AutoMessageSchedule = input.AutoMessageSchedule
367374
};
368375

369376
ResponseHelper.PopulateV4ResponseData(result);
@@ -378,6 +385,7 @@ private async Task<WeatherAlertSettingsData> GetWeatherAlertSettingsDataAsync()
378385
var minSeveritySetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertMinimumSeverity);
379386
var autoMsgSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSeverity);
380387
var callIntSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertCallIntegration);
388+
var scheduleSetting = await _departmentSettingsService.GetSettingByTypeAsync(DepartmentId, DepartmentSettingTypes.WeatherAlertAutoMessageSchedule);
381389

382390
if (enabledSetting != null && !string.IsNullOrWhiteSpace(enabledSetting.Setting))
383391
settings.WeatherAlertsEnabled = bool.TryParse(enabledSetting.Setting, out var enabled) && enabled;
@@ -391,6 +399,15 @@ private async Task<WeatherAlertSettingsData> GetWeatherAlertSettingsDataAsync()
391399
if (callIntSetting != null && !string.IsNullOrWhiteSpace(callIntSetting.Setting))
392400
settings.CallIntegrationEnabled = bool.TryParse(callIntSetting.Setting, out var callInt) && callInt;
393401

402+
if (scheduleSetting != null && !string.IsNullOrWhiteSpace(scheduleSetting.Setting))
403+
{
404+
try
405+
{
406+
settings.AutoMessageSchedule = Newtonsoft.Json.JsonConvert.DeserializeObject<System.Collections.Generic.List<WeatherAlertSeverityScheduleData>>(scheduleSetting.Setting);
407+
}
408+
catch { }
409+
}
410+
394411
return settings;
395412
}
396413

Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/SaveWeatherAlertSettingsInput.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Generic;
2+
13
namespace Resgrid.Web.Services.Models.v4.WeatherAlerts
24
{
35
public class SaveWeatherAlertSettingsInput
@@ -6,5 +8,6 @@ public class SaveWeatherAlertSettingsInput
68
public int MinimumSeverity { get; set; }
79
public int AutoMessageSeverity { get; set; }
810
public bool CallIntegrationEnabled { get; set; }
11+
public List<WeatherAlertSeverityScheduleData> AutoMessageSchedule { get; set; }
912
}
1013
}

Web/Resgrid.Web.Services/Models/v4/WeatherAlerts/WeatherAlertSettingsData.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Generic;
2+
13
namespace Resgrid.Web.Services.Models.v4.WeatherAlerts
24
{
35
public class WeatherAlertSettingsData
@@ -6,5 +8,14 @@ public class WeatherAlertSettingsData
68
public int MinimumSeverity { get; set; }
79
public int AutoMessageSeverity { get; set; }
810
public bool CallIntegrationEnabled { get; set; }
11+
public List<WeatherAlertSeverityScheduleData> AutoMessageSchedule { get; set; }
12+
}
13+
14+
public class WeatherAlertSeverityScheduleData
15+
{
16+
public int Severity { get; set; }
17+
public bool Enabled { get; set; }
18+
public int StartHour { get; set; }
19+
public int EndHour { get; set; }
920
}
1021
}

Web/Resgrid.Web/Areas/User/Views/Subscription/Index.cshtml

Lines changed: 34 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -265,13 +265,13 @@
265265
@if (Model.Plan.Frequency == (int)PlanFrequency.Yearly)
266266
{
267267
<span>
268-
@Model.Plan.Cost.ToString("C", Cultures.UnitedStates)<small>/@localizer["Year"]</small>
268+
@currencySymbol@Model.Plan.Cost.ToString("N2")<small>/@localizer["Year"]</small>
269269
</span>
270270
}
271271
else
272272
{
273273
<span>
274-
@Model.Plan.Cost.ToString("C", Cultures.UnitedStates)<small>/@localizer["month"]</small>
274+
@currencySymbol@Model.Plan.Cost.ToString("N2")<small>/@localizer["month"]</small>
275275
</span>
276276
}
277277
</h3>
@@ -527,43 +527,32 @@
527527
528528
function stripeCheckout(id) {
529529
const amount = slider == 1 ? val : $("#amount").val();
530+
const minAmount = IS_EU ? 10 : 20;
530531
531-
if (amount && amount > 10) {
532-
const packs = (amount / 10) - 1; // First 10 users are free.
532+
if (amount && amount >= minAmount && (!IS_EU ? amount > 10 : true)) {
533+
const packs = IS_EU ? (amount / 10) : (amount / 10) - 1;
533534
534535
$.ajax({
535536
url: resgrid.absoluteBaseUrl + '/User/Subscription/GetStripeSession?id=' + id + '&count=' + packs,
536537
contentType: 'application/json',
537538
type: 'GET'
538539
}).done(function (data) {
539-
if (data) {
540-
if (data.SessionId) {
541-
stripe.redirectToCheckout({
542-
sessionId: data.SessionId
543-
}).then(function (result) {
544-
// If `redirectToCheckout` fails due to a browser or network
545-
// error, display the localized error message to your customer
546-
// using `result.error.message`.
547-
548-
swal({
549-
title: "Purchase Error",
550-
text: "Error redirecting to Stripe for checkout. Stripe error: " + result.error.message,
551-
icon: "error",
552-
buttons: true,
553-
dangerMode: false
554-
});
555-
});
556-
}
540+
if (data && data.SessionId) {
541+
stripe.redirectToCheckout({
542+
sessionId: data.SessionId
543+
}).then(function (result) {
544+
if (result.error) {
545+
swal({ title: "Purchase Error", text: "Error redirecting to Stripe: " + result.error.message, icon: "error", buttons: true, dangerMode: false });
546+
}
547+
});
548+
} else {
549+
swal({ title: "Checkout Error", text: "Unable to create a checkout session. Please try again.", icon: "error", buttons: true, dangerMode: false });
557550
}
551+
}).fail(function () {
552+
swal({ title: "Connection Error", text: "Unable to reach the server. Please check your connection and try again.", icon: "error", buttons: true, dangerMode: false });
558553
});
559554
} else {
560-
swal({
561-
title: "Cannot Purchase",
562-
text: "Resgrid includes 10 entities for free for all departments. Please select a entity count greater then 10 to purchase.",
563-
icon: "warning",
564-
buttons: true,
565-
dangerMode: false
566-
});
555+
swal({ title: "Cannot Purchase", text: "Please select more entities to purchase a plan.", icon: "warning", buttons: true, dangerMode: false });
567556
}
568557
}
569558
@@ -589,9 +578,10 @@
589578
590579
function paddleCheckout(id) {
591580
const amount = slider == 1 ? val : $("#amount").val();
581+
const minAmount = IS_EU ? 10 : 20;
592582
593-
if (amount && amount > 10) {
594-
const packs = (amount / 10) - 1;
583+
if (amount && amount >= minAmount && (!IS_EU ? amount > 10 : true)) {
584+
const packs = IS_EU ? (amount / 10) : (amount / 10) - 1;
595585
596586
$.ajax({
597587
url: resgrid.absoluteBaseUrl + '/User/Subscription/GetPaddleCheckout?id=' + id + '&count=' + packs,
@@ -600,41 +590,33 @@
600590
}).done(function (data) {
601591
if (data) {
602592
if (data.HasActiveSub) {
603-
swal({
604-
title: "Active Subscription",
605-
text: "You already have an active subscription. Please manage your existing subscription instead.",
606-
icon: "warning",
607-
buttons: true,
608-
dangerMode: false
609-
});
593+
swal({ title: "Active Subscription", text: "You already have an active subscription. Please manage your existing subscription instead.", icon: "warning", buttons: true, dangerMode: false });
594+
return;
595+
}
596+
597+
if (!data.PriceId) {
598+
swal({ title: "Checkout Error", text: "Unable to create a checkout session. Please try again.", icon: "error", buttons: true, dangerMode: false });
610599
return;
611600
}
612601
613602
var checkoutSettings = {
614-
settings: {
615-
successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id
616-
},
617-
items: [{
618-
priceId: data.PriceId,
619-
quantity: packs
620-
}]
603+
settings: { successUrl: resgrid.absoluteBaseUrl + '/User/Subscription/PaddleProcessing?planId=' + id },
604+
items: [{ priceId: data.PriceId, quantity: packs }]
621605
};
622606
623607
if (data.CustomerId) {
624608
checkoutSettings.customer = { id: data.CustomerId };
625609
}
626610
627611
Paddle.Checkout.open(checkoutSettings);
612+
} else {
613+
swal({ title: "Checkout Error", text: "Unable to create a checkout session. Please try again.", icon: "error", buttons: true, dangerMode: false });
628614
}
615+
}).fail(function () {
616+
swal({ title: "Connection Error", text: "Unable to reach the server. Please check your connection and try again.", icon: "error", buttons: true, dangerMode: false });
629617
});
630618
} else {
631-
swal({
632-
title: "Cannot Purchase",
633-
text: "Resgrid includes 10 entities for free for all departments. Please select a entity count greater then 10 to purchase.",
634-
icon: "warning",
635-
buttons: true,
636-
dangerMode: false
637-
});
619+
swal({ title: "Cannot Purchase", text: "Please select more entities to purchase a plan.", icon: "warning", buttons: true, dangerMode: false });
638620
}
639621
}
640622

Web/Resgrid.Web/Areas/User/Views/Subscription/SelectRegistrationPlan.cshtml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@
7575
}
7676

7777
<p>Select the number of Entities (Users + Units) you require using the slider or text box below.
78-
@if (!isEU) { <text>Your first 10 entities are included at no chargeeach</text> } else { <text>Each</text> }
79-
additional pack of 10 entities is billed at the rate shown. Select <strong>Buy Yearly</strong> or <strong>Buy Monthly</strong> to proceed to checkout.</p>
78+
@if (!isEU) { <text>Your first 10 entities are included at no chargeeach additional</text> } else { <text>Each</text> }
79+
pack of 10 entities is billed at the rate shown. Select <strong>Buy Yearly</strong> or <strong>Buy Monthly</strong> to proceed to checkout.</p>
8080

8181
<div class="price-box">
8282
<div class="price-slider">

0 commit comments

Comments
 (0)