diff --git a/Core/Resgrid.Config/InfoConfig.cs b/Core/Resgrid.Config/InfoConfig.cs
index 70f51f12..4fda92af 100644
--- a/Core/Resgrid.Config/InfoConfig.cs
+++ b/Core/Resgrid.Config/InfoConfig.cs
@@ -33,6 +33,7 @@ public static class InfoConfig
LocationInfo =
"This is the Resgrid system hosted in the Western United States (private datacenter). This system services most Resgrid customers.",
IsDefault = true,
+ AppUrl = "https://app.resgrid.com",
ApiUrl = "https://api.resgrid.com",
AllowsFreeAccounts = true
},
@@ -41,9 +42,10 @@ public static class InfoConfig
Name = "EU-Central",
DisplayName = "Resgrid Europe",
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 compliance requirements.",
+ "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,
- ApiUrl = "https://api.eu.resgrid.com",
+ AppUrl = "https://app.eu-central.resgrid.com",
+ ApiUrl = "https://api-eu-central.resgrid.com",
AllowsFreeAccounts = false
}
};
@@ -55,7 +57,13 @@ public class ResgridSystemLocation
public string DisplayName { get; set; }
public string LocationInfo { get; set; }
public bool IsDefault { get; set; }
+ public string AppUrl { get; set; }
public string ApiUrl { get; set; }
public bool AllowsFreeAccounts { get; set; }
+
+ public string GetLogonUrl()
+ {
+ return AppUrl + "/Account/LogOn";
+ }
}
}
diff --git a/Core/Resgrid.Config/PaymentProviderConfig.cs b/Core/Resgrid.Config/PaymentProviderConfig.cs
index 991d37eb..58758646 100644
--- a/Core/Resgrid.Config/PaymentProviderConfig.cs
+++ b/Core/Resgrid.Config/PaymentProviderConfig.cs
@@ -1,4 +1,6 @@
-namespace Resgrid.Config
+using System;
+
+namespace Resgrid.Config
{
public static class PaymentProviderConfig
{
@@ -36,6 +38,32 @@ public static class PaymentProviderConfig
public static string PaddleProductionClientToken = "";
public static string PaddleTestClientToken = "";
+ // Global toggle: 1 = Stripe (default), 7 = Paddle. Matches PaymentMethods enum values.
+ // Set per-instance via ResgridConfig.json: "PaymentProviderConfig.ActivePaymentProvider": "7"
+ public static int ActivePaymentProvider = 1;
+
+ public const int ProviderStripe = 1;
+ public const int ProviderPaddle = 7;
+
+ public static int GetActivePaymentProvider()
+ {
+ if (ActivePaymentProvider != ProviderStripe && ActivePaymentProvider != ProviderPaddle)
+ throw new InvalidOperationException(
+ $"Unsupported ActivePaymentProvider value '{ActivePaymentProvider}'. Expected {ProviderStripe} (Stripe) or {ProviderPaddle} (Paddle).");
+
+ return ActivePaymentProvider;
+ }
+
+ public static bool IsStripeActive()
+ {
+ return GetActivePaymentProvider() == ProviderStripe;
+ }
+
+ public static bool IsPaddleActive()
+ {
+ return GetActivePaymentProvider() == ProviderPaddle;
+ }
+
public static string GetStripeClientKey()
{
if (IsTestMode)
diff --git a/Core/Resgrid.Localization/Account/Login.en.resx b/Core/Resgrid.Localization/Account/Login.en.resx
index 18f1f33d..29fe28ec 100644
--- a/Core/Resgrid.Localization/Account/Login.en.resx
+++ b/Core/Resgrid.Localization/Account/Login.en.resx
@@ -258,4 +258,7 @@
Affiliate Code
+
+ Region
+
\ No newline at end of file
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.ar.resx b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.ar.resx
new file mode 100644
index 00000000..b93767e1
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.ar.resx
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Communication Tests
+
+
+ Communication Tests
+
+
+ New Test
+
+
+ Edit Test
+
+
+ Delete Test
+
+
+ Are you sure you want to delete this test?
+
+
+ Test Definitions
+
+
+ Name
+
+
+ Description
+
+
+ Schedule Type
+
+
+ On Demand
+
+
+ Weekly
+
+
+ Monthly
+
+
+ Days of Week
+
+
+ Day of Month
+
+
+ Time
+
+
+ Channels to Test
+
+
+ SMS
+
+
+ Email
+
+
+ Voice
+
+
+ Push
+
+
+ Active
+
+
+ Inactive
+
+
+ Response Window
+
+
+ Response Window (minutes)
+
+
+ Create Test
+
+
+ Save Changes
+
+
+ Cancel
+
+
+ Recent Test Runs
+
+
+ Run Now
+
+
+ Run Code
+
+
+ Started
+
+
+ Status
+
+
+ Users Tested
+
+
+ Responses
+
+
+ View Report
+
+
+ Communication Test Report
+
+
+ Run Details
+
+
+ Per-User Results
+
+
+ User
+
+
+ Contact
+
+
+ Carrier
+
+
+ Verification Status
+
+
+ Responded
+
+
+ No Response
+
+
+ Not Sent
+
+
+ Response Rate
+
+
+ Your communication test response has been recorded. Thank you.
+
+
+ Resgrid received your communication test response. Thank you.
+
+
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.cs b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.cs
new file mode 100644
index 00000000..57770760
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.cs
@@ -0,0 +1,6 @@
+namespace Resgrid.Localization.Areas.User.CommunicationTest
+{
+ public class CommunicationTest
+ {
+ }
+}
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.de.resx b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.de.resx
new file mode 100644
index 00000000..b93767e1
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.de.resx
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Communication Tests
+
+
+ Communication Tests
+
+
+ New Test
+
+
+ Edit Test
+
+
+ Delete Test
+
+
+ Are you sure you want to delete this test?
+
+
+ Test Definitions
+
+
+ Name
+
+
+ Description
+
+
+ Schedule Type
+
+
+ On Demand
+
+
+ Weekly
+
+
+ Monthly
+
+
+ Days of Week
+
+
+ Day of Month
+
+
+ Time
+
+
+ Channels to Test
+
+
+ SMS
+
+
+ Email
+
+
+ Voice
+
+
+ Push
+
+
+ Active
+
+
+ Inactive
+
+
+ Response Window
+
+
+ Response Window (minutes)
+
+
+ Create Test
+
+
+ Save Changes
+
+
+ Cancel
+
+
+ Recent Test Runs
+
+
+ Run Now
+
+
+ Run Code
+
+
+ Started
+
+
+ Status
+
+
+ Users Tested
+
+
+ Responses
+
+
+ View Report
+
+
+ Communication Test Report
+
+
+ Run Details
+
+
+ Per-User Results
+
+
+ User
+
+
+ Contact
+
+
+ Carrier
+
+
+ Verification Status
+
+
+ Responded
+
+
+ No Response
+
+
+ Not Sent
+
+
+ Response Rate
+
+
+ Your communication test response has been recorded. Thank you.
+
+
+ Resgrid received your communication test response. Thank you.
+
+
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.en.resx b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.en.resx
new file mode 100644
index 00000000..b93767e1
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.en.resx
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Communication Tests
+
+
+ Communication Tests
+
+
+ New Test
+
+
+ Edit Test
+
+
+ Delete Test
+
+
+ Are you sure you want to delete this test?
+
+
+ Test Definitions
+
+
+ Name
+
+
+ Description
+
+
+ Schedule Type
+
+
+ On Demand
+
+
+ Weekly
+
+
+ Monthly
+
+
+ Days of Week
+
+
+ Day of Month
+
+
+ Time
+
+
+ Channels to Test
+
+
+ SMS
+
+
+ Email
+
+
+ Voice
+
+
+ Push
+
+
+ Active
+
+
+ Inactive
+
+
+ Response Window
+
+
+ Response Window (minutes)
+
+
+ Create Test
+
+
+ Save Changes
+
+
+ Cancel
+
+
+ Recent Test Runs
+
+
+ Run Now
+
+
+ Run Code
+
+
+ Started
+
+
+ Status
+
+
+ Users Tested
+
+
+ Responses
+
+
+ View Report
+
+
+ Communication Test Report
+
+
+ Run Details
+
+
+ Per-User Results
+
+
+ User
+
+
+ Contact
+
+
+ Carrier
+
+
+ Verification Status
+
+
+ Responded
+
+
+ No Response
+
+
+ Not Sent
+
+
+ Response Rate
+
+
+ Your communication test response has been recorded. Thank you.
+
+
+ Resgrid received your communication test response. Thank you.
+
+
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.es.resx b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.es.resx
new file mode 100644
index 00000000..b93767e1
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.es.resx
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Communication Tests
+
+
+ Communication Tests
+
+
+ New Test
+
+
+ Edit Test
+
+
+ Delete Test
+
+
+ Are you sure you want to delete this test?
+
+
+ Test Definitions
+
+
+ Name
+
+
+ Description
+
+
+ Schedule Type
+
+
+ On Demand
+
+
+ Weekly
+
+
+ Monthly
+
+
+ Days of Week
+
+
+ Day of Month
+
+
+ Time
+
+
+ Channels to Test
+
+
+ SMS
+
+
+ Email
+
+
+ Voice
+
+
+ Push
+
+
+ Active
+
+
+ Inactive
+
+
+ Response Window
+
+
+ Response Window (minutes)
+
+
+ Create Test
+
+
+ Save Changes
+
+
+ Cancel
+
+
+ Recent Test Runs
+
+
+ Run Now
+
+
+ Run Code
+
+
+ Started
+
+
+ Status
+
+
+ Users Tested
+
+
+ Responses
+
+
+ View Report
+
+
+ Communication Test Report
+
+
+ Run Details
+
+
+ Per-User Results
+
+
+ User
+
+
+ Contact
+
+
+ Carrier
+
+
+ Verification Status
+
+
+ Responded
+
+
+ No Response
+
+
+ Not Sent
+
+
+ Response Rate
+
+
+ Your communication test response has been recorded. Thank you.
+
+
+ Resgrid received your communication test response. Thank you.
+
+
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.fr.resx b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.fr.resx
new file mode 100644
index 00000000..b93767e1
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.fr.resx
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Communication Tests
+
+
+ Communication Tests
+
+
+ New Test
+
+
+ Edit Test
+
+
+ Delete Test
+
+
+ Are you sure you want to delete this test?
+
+
+ Test Definitions
+
+
+ Name
+
+
+ Description
+
+
+ Schedule Type
+
+
+ On Demand
+
+
+ Weekly
+
+
+ Monthly
+
+
+ Days of Week
+
+
+ Day of Month
+
+
+ Time
+
+
+ Channels to Test
+
+
+ SMS
+
+
+ Email
+
+
+ Voice
+
+
+ Push
+
+
+ Active
+
+
+ Inactive
+
+
+ Response Window
+
+
+ Response Window (minutes)
+
+
+ Create Test
+
+
+ Save Changes
+
+
+ Cancel
+
+
+ Recent Test Runs
+
+
+ Run Now
+
+
+ Run Code
+
+
+ Started
+
+
+ Status
+
+
+ Users Tested
+
+
+ Responses
+
+
+ View Report
+
+
+ Communication Test Report
+
+
+ Run Details
+
+
+ Per-User Results
+
+
+ User
+
+
+ Contact
+
+
+ Carrier
+
+
+ Verification Status
+
+
+ Responded
+
+
+ No Response
+
+
+ Not Sent
+
+
+ Response Rate
+
+
+ Your communication test response has been recorded. Thank you.
+
+
+ Resgrid received your communication test response. Thank you.
+
+
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.it.resx b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.it.resx
new file mode 100644
index 00000000..b93767e1
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.it.resx
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Communication Tests
+
+
+ Communication Tests
+
+
+ New Test
+
+
+ Edit Test
+
+
+ Delete Test
+
+
+ Are you sure you want to delete this test?
+
+
+ Test Definitions
+
+
+ Name
+
+
+ Description
+
+
+ Schedule Type
+
+
+ On Demand
+
+
+ Weekly
+
+
+ Monthly
+
+
+ Days of Week
+
+
+ Day of Month
+
+
+ Time
+
+
+ Channels to Test
+
+
+ SMS
+
+
+ Email
+
+
+ Voice
+
+
+ Push
+
+
+ Active
+
+
+ Inactive
+
+
+ Response Window
+
+
+ Response Window (minutes)
+
+
+ Create Test
+
+
+ Save Changes
+
+
+ Cancel
+
+
+ Recent Test Runs
+
+
+ Run Now
+
+
+ Run Code
+
+
+ Started
+
+
+ Status
+
+
+ Users Tested
+
+
+ Responses
+
+
+ View Report
+
+
+ Communication Test Report
+
+
+ Run Details
+
+
+ Per-User Results
+
+
+ User
+
+
+ Contact
+
+
+ Carrier
+
+
+ Verification Status
+
+
+ Responded
+
+
+ No Response
+
+
+ Not Sent
+
+
+ Response Rate
+
+
+ Your communication test response has been recorded. Thank you.
+
+
+ Resgrid received your communication test response. Thank you.
+
+
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.pl.resx b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.pl.resx
new file mode 100644
index 00000000..b93767e1
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.pl.resx
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Communication Tests
+
+
+ Communication Tests
+
+
+ New Test
+
+
+ Edit Test
+
+
+ Delete Test
+
+
+ Are you sure you want to delete this test?
+
+
+ Test Definitions
+
+
+ Name
+
+
+ Description
+
+
+ Schedule Type
+
+
+ On Demand
+
+
+ Weekly
+
+
+ Monthly
+
+
+ Days of Week
+
+
+ Day of Month
+
+
+ Time
+
+
+ Channels to Test
+
+
+ SMS
+
+
+ Email
+
+
+ Voice
+
+
+ Push
+
+
+ Active
+
+
+ Inactive
+
+
+ Response Window
+
+
+ Response Window (minutes)
+
+
+ Create Test
+
+
+ Save Changes
+
+
+ Cancel
+
+
+ Recent Test Runs
+
+
+ Run Now
+
+
+ Run Code
+
+
+ Started
+
+
+ Status
+
+
+ Users Tested
+
+
+ Responses
+
+
+ View Report
+
+
+ Communication Test Report
+
+
+ Run Details
+
+
+ Per-User Results
+
+
+ User
+
+
+ Contact
+
+
+ Carrier
+
+
+ Verification Status
+
+
+ Responded
+
+
+ No Response
+
+
+ Not Sent
+
+
+ Response Rate
+
+
+ Your communication test response has been recorded. Thank you.
+
+
+ Resgrid received your communication test response. Thank you.
+
+
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.sv.resx b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.sv.resx
new file mode 100644
index 00000000..b93767e1
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.sv.resx
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Communication Tests
+
+
+ Communication Tests
+
+
+ New Test
+
+
+ Edit Test
+
+
+ Delete Test
+
+
+ Are you sure you want to delete this test?
+
+
+ Test Definitions
+
+
+ Name
+
+
+ Description
+
+
+ Schedule Type
+
+
+ On Demand
+
+
+ Weekly
+
+
+ Monthly
+
+
+ Days of Week
+
+
+ Day of Month
+
+
+ Time
+
+
+ Channels to Test
+
+
+ SMS
+
+
+ Email
+
+
+ Voice
+
+
+ Push
+
+
+ Active
+
+
+ Inactive
+
+
+ Response Window
+
+
+ Response Window (minutes)
+
+
+ Create Test
+
+
+ Save Changes
+
+
+ Cancel
+
+
+ Recent Test Runs
+
+
+ Run Now
+
+
+ Run Code
+
+
+ Started
+
+
+ Status
+
+
+ Users Tested
+
+
+ Responses
+
+
+ View Report
+
+
+ Communication Test Report
+
+
+ Run Details
+
+
+ Per-User Results
+
+
+ User
+
+
+ Contact
+
+
+ Carrier
+
+
+ Verification Status
+
+
+ Responded
+
+
+ No Response
+
+
+ Not Sent
+
+
+ Response Rate
+
+
+ Your communication test response has been recorded. Thank you.
+
+
+ Resgrid received your communication test response. Thank you.
+
+
diff --git a/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.uk.resx b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.uk.resx
new file mode 100644
index 00000000..b93767e1
--- /dev/null
+++ b/Core/Resgrid.Localization/Areas/User/CommunicationTest/CommunicationTest.uk.resx
@@ -0,0 +1,208 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Communication Tests
+
+
+ Communication Tests
+
+
+ New Test
+
+
+ Edit Test
+
+
+ Delete Test
+
+
+ Are you sure you want to delete this test?
+
+
+ Test Definitions
+
+
+ Name
+
+
+ Description
+
+
+ Schedule Type
+
+
+ On Demand
+
+
+ Weekly
+
+
+ Monthly
+
+
+ Days of Week
+
+
+ Day of Month
+
+
+ Time
+
+
+ Channels to Test
+
+
+ SMS
+
+
+ Email
+
+
+ Voice
+
+
+ Push
+
+
+ Active
+
+
+ Inactive
+
+
+ Response Window
+
+
+ Response Window (minutes)
+
+
+ Create Test
+
+
+ Save Changes
+
+
+ Cancel
+
+
+ Recent Test Runs
+
+
+ Run Now
+
+
+ Run Code
+
+
+ Started
+
+
+ Status
+
+
+ Users Tested
+
+
+ Responses
+
+
+ View Report
+
+
+ Communication Test Report
+
+
+ Run Details
+
+
+ Per-User Results
+
+
+ User
+
+
+ Contact
+
+
+ Carrier
+
+
+ Verification Status
+
+
+ Responded
+
+
+ No Response
+
+
+ Not Sent
+
+
+ Response Rate
+
+
+ Your communication test response has been recorded. Thank you.
+
+
+ Resgrid received your communication test response. Thank you.
+
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx
index c50c8844..5a26e4b7 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx
@@ -107,6 +107,12 @@
الإجابة على أسئلة البروتوكول
البروتوكولات
نص البروتوكول
+
+ If checked, entities removed from the dispatch list will receive a cancellation notification for this call.
+
+
+ Notify Cancelled
+
إذا أردت إعادة بث هذا البلاغ حدد هذا المربع. سيُرسَل البلاغ إلى جميع المستخدمين المحددين بغض النظر عن مشاركتهم في الإرسال الأول أم لا.
إعادة الإرسال
معرف المرجع
@@ -157,4 +163,31 @@
المدة (دقائق)
حد التحذير (دقائق)
المصدر
+ بث الفيديو
+ إضافة بث فيديو
+ تعديل بث الفيديو
+ حذف بث الفيديو
+ الاسم
+ الرابط
+ نوع البث
+ تنسيق البث
+ الوصف
+ الحالة
+ الموقع
+ أضيف بواسطة
+ تاريخ الإضافة
+ ترتيب الفرز
+ نشط
+ غير نشط
+ خطأ
+ طائرة مسيّرة
+ كاميرا ثابتة
+ كاميرا الجسم
+ كاميرا المرور
+ كاميرا الطقس
+ بث فضائي
+ كاميرا ويب
+ أخرى
+ هل أنت متأكد من رغبتك في حذف بث الفيديو هذا؟
+ لم تتم إضافة أي بث فيديو لهذه المكالمة.
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx
index 7851beef..fb6943bb 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx
@@ -374,6 +374,12 @@
Protocol Text
+
+ If checked, entities removed from the dispatch list will receive a cancellation notification for this call.
+
+
+ Notify Cancelled
+
If you want to rebroadcast this call check this box. This will dispatch the call to all users selected, regardless if they were on the first dispatch or not.
@@ -524,4 +530,85 @@
Quelle
+
+ Videoübertragungen
+
+
+ Videoübertragung hinzufügen
+
+
+ Videoübertragung bearbeiten
+
+
+ Videoübertragung löschen
+
+
+ Name
+
+
+ URL
+
+
+ Übertragungstyp
+
+
+ Übertragungsformat
+
+
+ Beschreibung
+
+
+ Status
+
+
+ Standort
+
+
+ Hinzugefügt von
+
+
+ Hinzugefügt am
+
+
+ Sortierreihenfolge
+
+
+ Aktiv
+
+
+ Inaktiv
+
+
+ Fehler
+
+
+ Drohne
+
+
+ Feste Kamera
+
+
+ Körperkamera
+
+
+ Verkehrskamera
+
+
+ Wetterkamera
+
+
+ Satellitenübertragung
+
+
+ Webkamera
+
+
+ Sonstiges
+
+
+ Sind Sie sicher, dass Sie diese Videoübertragung löschen möchten?
+
+
+ Keine Videoübertragungen für diesen Einsatz hinzugefügt.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx
index edf9c74c..03af7278 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx
@@ -423,6 +423,12 @@
Protocol Text
+
+ If checked, entities removed from the dispatch list will receive a cancellation notification for this call.
+
+
+ Notify Cancelled
+
If you want to rebroadcast this call check this box. This will dispatch the call to all users selected, regardless if they were on the first dispatch or not.
@@ -573,4 +579,85 @@
Source
+
+ Video Feeds
+
+
+ Add Video Feed
+
+
+ Edit Video Feed
+
+
+ Delete Video Feed
+
+
+ Name
+
+
+ URL
+
+
+ Feed Type
+
+
+ Feed Format
+
+
+ Description
+
+
+ Status
+
+
+ Location
+
+
+ Added By
+
+
+ Added On
+
+
+ Sort Order
+
+
+ Active
+
+
+ Inactive
+
+
+ Error
+
+
+ Drone
+
+
+ Fixed Camera
+
+
+ Body Cam
+
+
+ Traffic Cam
+
+
+ Weather Cam
+
+
+ Satellite Feed
+
+
+ Web Cam
+
+
+ Other
+
+
+ Are you sure you want to delete this video feed?
+
+
+ No video feeds have been added to this call.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx
index 2e8bbc40..6d06e966 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx
@@ -423,6 +423,12 @@
Texto del protocolo
+
+ If checked, entities removed from the dispatch list will receive a cancellation notification for this call.
+
+
+ Notify Cancelled
+
Si desea retransmitir esta llamada marque esta casilla. Esto enviará la llamada a todos los usuarios seleccionados, independientemente de si estaban en el primer envío o no.
@@ -573,4 +579,85 @@
Fuente
+
+ Transmisiones de Video
+
+
+ Agregar Transmisión de Video
+
+
+ Editar Transmisión de Video
+
+
+ Eliminar Transmisión de Video
+
+
+ Nombre
+
+
+ URL
+
+
+ Tipo de Transmisión
+
+
+ Formato de Transmisión
+
+
+ Descripción
+
+
+ Estado
+
+
+ Ubicación
+
+
+ Agregado Por
+
+
+ Fecha de Agregado
+
+
+ Orden
+
+
+ Activo
+
+
+ Inactivo
+
+
+ Error
+
+
+ Dron
+
+
+ Cámara Fija
+
+
+ Cámara Corporal
+
+
+ Cámara de Tráfico
+
+
+ Cámara Meteorológica
+
+
+ Transmisión Satelital
+
+
+ Cámara Web
+
+
+ Otro
+
+
+ ¿Está seguro de que desea eliminar esta transmisión de video?
+
+
+ No se han agregado transmisiones de video a esta llamada.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx
index 8b4ee8f6..215f5d88 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx
@@ -374,6 +374,12 @@
Protocol Text
+
+ If checked, entities removed from the dispatch list will receive a cancellation notification for this call.
+
+
+ Notify Cancelled
+
If you want to rebroadcast this call check this box. This will dispatch the call to all users selected, regardless if they were on the first dispatch or not.
@@ -524,4 +530,85 @@
Source
+
+ Flux Vidéo
+
+
+ Ajouter un Flux Vidéo
+
+
+ Modifier le Flux Vidéo
+
+
+ Supprimer le Flux Vidéo
+
+
+ Nom
+
+
+ URL
+
+
+ Type de Flux
+
+
+ Format de Flux
+
+
+ Description
+
+
+ Statut
+
+
+ Emplacement
+
+
+ Ajouté par
+
+
+ Ajouté le
+
+
+ Ordre de tri
+
+
+ Actif
+
+
+ Inactif
+
+
+ Erreur
+
+
+ Drone
+
+
+ Caméra Fixe
+
+
+ Caméra Corporelle
+
+
+ Caméra de Circulation
+
+
+ Caméra Météo
+
+
+ Flux Satellite
+
+
+ Webcam
+
+
+ Autre
+
+
+ Êtes-vous sûr de vouloir supprimer ce flux vidéo ?
+
+
+ Aucun flux vidéo n'a été ajouté à cet appel.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx
index 12f4f3d9..9e6c0afa 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx
@@ -374,6 +374,12 @@
Protocol Text
+
+ If checked, entities removed from the dispatch list will receive a cancellation notification for this call.
+
+
+ Notify Cancelled
+
If you want to rebroadcast this call check this box. This will dispatch the call to all users selected, regardless if they were on the first dispatch or not.
@@ -524,4 +530,85 @@
Fonte
+
+ Feed Video
+
+
+ Aggiungi Feed Video
+
+
+ Modifica Feed Video
+
+
+ Elimina Feed Video
+
+
+ Nome
+
+
+ URL
+
+
+ Tipo di Feed
+
+
+ Formato Feed
+
+
+ Descrizione
+
+
+ Stato
+
+
+ Posizione
+
+
+ Aggiunto da
+
+
+ Aggiunto il
+
+
+ Ordinamento
+
+
+ Attivo
+
+
+ Inattivo
+
+
+ Errore
+
+
+ Drone
+
+
+ Telecamera Fissa
+
+
+ Telecamera Corporea
+
+
+ Telecamera Traffico
+
+
+ Telecamera Meteo
+
+
+ Feed Satellitare
+
+
+ Webcam
+
+
+ Altro
+
+
+ Sei sicuro di voler eliminare questo feed video?
+
+
+ Nessun feed video è stato aggiunto a questa chiamata.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx
index 56b2c540..49a46cdb 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx
@@ -374,6 +374,12 @@
Protocol Text
+
+ If checked, entities removed from the dispatch list will receive a cancellation notification for this call.
+
+
+ Notify Cancelled
+
If you want to rebroadcast this call check this box. This will dispatch the call to all users selected, regardless if they were on the first dispatch or not.
@@ -524,4 +530,85 @@
Źródło
+
+ Transmisje Wideo
+
+
+ Dodaj Transmisję Wideo
+
+
+ Edytuj Transmisję Wideo
+
+
+ Usuń Transmisję Wideo
+
+
+ Nazwa
+
+
+ URL
+
+
+ Typ Transmisji
+
+
+ Format Transmisji
+
+
+ Opis
+
+
+ Status
+
+
+ Lokalizacja
+
+
+ Dodane przez
+
+
+ Data dodania
+
+
+ Kolejność
+
+
+ Aktywny
+
+
+ Nieaktywny
+
+
+ Błąd
+
+
+ Dron
+
+
+ Kamera Stała
+
+
+ Kamera Osobista
+
+
+ Kamera Drogowa
+
+
+ Kamera Pogodowa
+
+
+ Transmisja Satelitarna
+
+
+ Kamera Internetowa
+
+
+ Inne
+
+
+ Czy na pewno chcesz usunąć tę transmisję wideo?
+
+
+ Nie dodano żadnych transmisji wideo do tego zgłoszenia.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx
index 99ddf435..ab207c86 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx
@@ -374,6 +374,12 @@
Protocol Text
+
+ If checked, entities removed from the dispatch list will receive a cancellation notification for this call.
+
+
+ Notify Cancelled
+
If you want to rebroadcast this call check this box. This will dispatch the call to all users selected, regardless if they were on the first dispatch or not.
@@ -524,4 +530,85 @@
Källa
+
+ Videoflöden
+
+
+ Lägg till Videoflöde
+
+
+ Redigera Videoflöde
+
+
+ Ta bort Videoflöde
+
+
+ Namn
+
+
+ URL
+
+
+ Flödestyp
+
+
+ Flödesformat
+
+
+ Beskrivning
+
+
+ Status
+
+
+ Plats
+
+
+ Tillagd av
+
+
+ Tillagd den
+
+
+ Sorteringsordning
+
+
+ Aktiv
+
+
+ Inaktiv
+
+
+ Fel
+
+
+ Drönare
+
+
+ Fast Kamera
+
+
+ Kroppskamera
+
+
+ Trafikkamera
+
+
+ Väderkamera
+
+
+ Satellitflöde
+
+
+ Webbkamera
+
+
+ Övrigt
+
+
+ Är du säker på att du vill ta bort detta videoflöde?
+
+
+ Inga videoflöden har lagts till för detta ärende.
+
diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx
index a513d5c1..9c03bc8f 100644
--- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx
@@ -374,6 +374,12 @@
Protocol Text
+
+ If checked, entities removed from the dispatch list will receive a cancellation notification for this call.
+
+
+ Notify Cancelled
+
If you want to rebroadcast this call check this box. This will dispatch the call to all users selected, regardless if they were on the first dispatch or not.
@@ -524,4 +530,85 @@
Джерело
+
+ Відеотрансляції
+
+
+ Додати Відеотрансляцію
+
+
+ Редагувати Відеотрансляцію
+
+
+ Видалити Відеотрансляцію
+
+
+ Назва
+
+
+ URL
+
+
+ Тип Трансляції
+
+
+ Формат Трансляції
+
+
+ Опис
+
+
+ Статус
+
+
+ Місцезнаходження
+
+
+ Додано
+
+
+ Дата додавання
+
+
+ Порядок сортування
+
+
+ Активний
+
+
+ Неактивний
+
+
+ Помилка
+
+
+ Дрон
+
+
+ Стаціонарна Камера
+
+
+ Натільна Камера
+
+
+ Камера Дорожнього Руху
+
+
+ Метеокамера
+
+
+ Супутникова Трансляція
+
+
+ Веб-камера
+
+
+ Інше
+
+
+ Ви впевнені, що хочете видалити цю відеотрансляцію?
+
+
+ До цього виклику не додано жодних відеотрансляцій.
+
diff --git a/Core/Resgrid.Model/AuditLogTypes.cs b/Core/Resgrid.Model/AuditLogTypes.cs
index 9241f42f..5ae75e77 100644
--- a/Core/Resgrid.Model/AuditLogTypes.cs
+++ b/Core/Resgrid.Model/AuditLogTypes.cs
@@ -143,6 +143,11 @@ public enum AuditLogTypes
CalendarAdminCheckInPerformed,
// Log operations
LogCreated,
- LogDeleted
+ LogDeleted,
+ // Communication Test operations
+ CommunicationTestCreated,
+ CommunicationTestUpdated,
+ CommunicationTestDeleted,
+ CommunicationTestRunStarted
}
}
diff --git a/Core/Resgrid.Model/Billing/Api/GetActivePaymentProviderResult.cs b/Core/Resgrid.Model/Billing/Api/GetActivePaymentProviderResult.cs
new file mode 100644
index 00000000..a14b81c2
--- /dev/null
+++ b/Core/Resgrid.Model/Billing/Api/GetActivePaymentProviderResult.cs
@@ -0,0 +1,12 @@
+namespace Resgrid.Model.Billing.Api;
+
+public class GetActivePaymentProviderResult : BillingApiResponseBase
+{
+ public GetActivePaymentProviderData Data { get; set; }
+}
+
+public class GetActivePaymentProviderData
+{
+ public int ActiveProvider { get; set; }
+ public string ProviderName { get; set; }
+}
diff --git a/Core/Resgrid.Model/Call.cs b/Core/Resgrid.Model/Call.cs
index 7a843eb3..0bf24acd 100644
--- a/Core/Resgrid.Model/Call.cs
+++ b/Core/Resgrid.Model/Call.cs
@@ -139,6 +139,9 @@ public class Call : IEntity
[ProtoMember(32)]
public virtual ICollection Contacts { get; set; }
+ [ProtoMember(33)]
+ public virtual ICollection VideoFeeds { get; set; }
+
public string ContactName { get; set; }
public string ContactNumber { get; set; }
@@ -199,7 +202,7 @@ public object IdValue
public int IdType => 0;
[NotMapped]
- public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "ReportingUser", "ClosedByUser", "Department", "Dispatches", "Attachments", "CallNotes", "GroupDispatches", "UnitDispatches", "RoleDispatches", "Protocols", "ShortenedAudioUrl", "ShortenedCallUrl", "CallPriority", "PreviousDispatchCount", "References", "Contacts" };
+ public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "ReportingUser", "ClosedByUser", "Department", "Dispatches", "Attachments", "CallNotes", "GroupDispatches", "UnitDispatches", "RoleDispatches", "Protocols", "ShortenedAudioUrl", "ShortenedCallUrl", "CallPriority", "PreviousDispatchCount", "References", "Contacts", "VideoFeeds" };
public string GetIdentifier()
{
diff --git a/Core/Resgrid.Model/CallVideoFeed.cs b/Core/Resgrid.Model/CallVideoFeed.cs
new file mode 100644
index 00000000..23c10a10
--- /dev/null
+++ b/Core/Resgrid.Model/CallVideoFeed.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+using Resgrid.Framework;
+
+namespace Resgrid.Model
+{
+ [Table("CallVideoFeeds")]
+ public class CallVideoFeed : IEntity
+ {
+ public string CallVideoFeedId { get; set; }
+
+ public int CallId { get; set; }
+
+ public int DepartmentId { get; set; }
+
+ [Required]
+ public string Name { get; set; }
+
+ [Required]
+ public string Url { get; set; }
+
+ public int? FeedType { get; set; }
+
+ public int? FeedFormat { get; set; }
+
+ public string Description { get; set; }
+
+ public int Status { get; set; }
+
+ [DecimalPrecision(10, 7)]
+ public decimal? Latitude { get; set; }
+
+ [DecimalPrecision(10, 7)]
+ public decimal? Longitude { get; set; }
+
+ public string AddedByUserId { get; set; }
+
+ public DateTime AddedOn { get; set; }
+
+ public DateTime? UpdatedOn { get; set; }
+
+ public int SortOrder { get; set; }
+
+ public bool IsDeleted { get; set; }
+
+ public string DeletedByUserId { get; set; }
+
+ public DateTime? DeletedOn { get; set; }
+
+ public bool IsFlagged { get; set; }
+
+ public string FlaggedReason { get; set; }
+
+ public string FlaggedByUserId { get; set; }
+
+ public DateTime? FlaggedOn { get; set; }
+
+ [ForeignKey("CallId")]
+ public virtual Call Call { get; set; }
+
+ [NotMapped]
+ public string TableName => "CallVideoFeeds";
+
+ [NotMapped]
+ public string IdName => "CallVideoFeedId";
+
+ [NotMapped]
+ public int IdType => 1;
+
+ [NotMapped]
+ [JsonIgnore]
+ public object IdValue
+ {
+ get { return CallVideoFeedId; }
+ set { CallVideoFeedId = (string)value; }
+ }
+
+ [NotMapped]
+ public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "Call" };
+ }
+}
diff --git a/Core/Resgrid.Model/CallVideoFeedFormats.cs b/Core/Resgrid.Model/CallVideoFeedFormats.cs
new file mode 100644
index 00000000..c3261ca0
--- /dev/null
+++ b/Core/Resgrid.Model/CallVideoFeedFormats.cs
@@ -0,0 +1,14 @@
+namespace Resgrid.Model
+{
+ public enum CallVideoFeedFormats
+ {
+ RTSP = 0,
+ HLS = 1,
+ MJPEG = 2,
+ YouTubeLive = 3,
+ WebRTC = 4,
+ DASH = 5,
+ Embed = 6,
+ Other = 99
+ }
+}
diff --git a/Core/Resgrid.Model/CallVideoFeedStatuses.cs b/Core/Resgrid.Model/CallVideoFeedStatuses.cs
new file mode 100644
index 00000000..a5ebd910
--- /dev/null
+++ b/Core/Resgrid.Model/CallVideoFeedStatuses.cs
@@ -0,0 +1,9 @@
+namespace Resgrid.Model
+{
+ public enum CallVideoFeedStatuses
+ {
+ Active = 0,
+ Inactive = 1,
+ Error = 2
+ }
+}
diff --git a/Core/Resgrid.Model/CallVideoFeedTypes.cs b/Core/Resgrid.Model/CallVideoFeedTypes.cs
new file mode 100644
index 00000000..59b34ada
--- /dev/null
+++ b/Core/Resgrid.Model/CallVideoFeedTypes.cs
@@ -0,0 +1,14 @@
+namespace Resgrid.Model
+{
+ public enum CallVideoFeedTypes
+ {
+ Drone = 0,
+ FixedCamera = 1,
+ BodyCam = 2,
+ TrafficCam = 3,
+ WeatherCam = 4,
+ SatelliteFeed = 5,
+ WebCam = 6,
+ Other = 99
+ }
+}
diff --git a/Core/Resgrid.Model/CommunicationTest.cs b/Core/Resgrid.Model/CommunicationTest.cs
new file mode 100644
index 00000000..e6b2a842
--- /dev/null
+++ b/Core/Resgrid.Model/CommunicationTest.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+
+namespace Resgrid.Model
+{
+ [Table("CommunicationTests")]
+ public class CommunicationTest : IEntity
+ {
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public Guid CommunicationTestId { get; set; }
+
+ [Required]
+ [ForeignKey("Department"), DatabaseGenerated(DatabaseGeneratedOption.None)]
+ public int DepartmentId { get; set; }
+
+ public virtual Department Department { get; set; }
+
+ [Required]
+ [MaxLength(500)]
+ public string Name { get; set; }
+
+ [MaxLength(4000)]
+ public string Description { get; set; }
+
+ public int ScheduleType { get; set; }
+
+ public bool Sunday { get; set; }
+
+ public bool Monday { get; set; }
+
+ public bool Tuesday { get; set; }
+
+ public bool Wednesday { get; set; }
+
+ public bool Thursday { get; set; }
+
+ public bool Friday { get; set; }
+
+ public bool Saturday { get; set; }
+
+ public int? DayOfMonth { get; set; }
+
+ [MaxLength(50)]
+ public string Time { get; set; }
+
+ public bool TestSms { get; set; }
+
+ public bool TestEmail { get; set; }
+
+ public bool TestVoice { get; set; }
+
+ public bool TestPush { get; set; }
+
+ public bool Active { get; set; }
+
+ [Required]
+ [MaxLength(128)]
+ public string CreatedByUserId { get; set; }
+
+ public DateTime CreatedOn { get; set; }
+
+ public DateTime? UpdatedOn { get; set; }
+
+ public int ResponseWindowMinutes { get; set; }
+
+ [NotMapped]
+ [JsonIgnore]
+ public object IdValue
+ {
+ get { return CommunicationTestId == Guid.Empty ? null : (object)CommunicationTestId.ToString(); }
+ set { CommunicationTestId = value == null ? Guid.Empty : Guid.Parse(value.ToString()); }
+ }
+
+ [NotMapped]
+ public string TableName => "CommunicationTests";
+
+ [NotMapped]
+ public string IdName => "CommunicationTestId";
+
+ [NotMapped]
+ public int IdType => 1;
+
+ [NotMapped]
+ public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "Department" };
+ }
+}
diff --git a/Core/Resgrid.Model/CommunicationTestChannel.cs b/Core/Resgrid.Model/CommunicationTestChannel.cs
new file mode 100644
index 00000000..36b2dca8
--- /dev/null
+++ b/Core/Resgrid.Model/CommunicationTestChannel.cs
@@ -0,0 +1,10 @@
+namespace Resgrid.Model
+{
+ public enum CommunicationTestChannel
+ {
+ Sms = 0,
+ Email = 1,
+ Voice = 2,
+ Push = 3
+ }
+}
diff --git a/Core/Resgrid.Model/CommunicationTestResult.cs b/Core/Resgrid.Model/CommunicationTestResult.cs
new file mode 100644
index 00000000..245b8640
--- /dev/null
+++ b/Core/Resgrid.Model/CommunicationTestResult.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+
+namespace Resgrid.Model
+{
+ [Table("CommunicationTestResults")]
+ public class CommunicationTestResult : IEntity
+ {
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public Guid CommunicationTestResultId { get; set; }
+
+ [Required]
+ public Guid CommunicationTestRunId { get; set; }
+
+ [ForeignKey("CommunicationTestRunId")]
+ public virtual CommunicationTestRun CommunicationTestRun { get; set; }
+
+ [Required]
+ public int DepartmentId { get; set; }
+
+ [Required]
+ [MaxLength(128)]
+ public string UserId { get; set; }
+
+ public int Channel { get; set; }
+
+ [MaxLength(500)]
+ public string ContactValue { get; set; }
+
+ [MaxLength(200)]
+ public string ContactCarrier { get; set; }
+
+ public int VerificationStatus { get; set; }
+
+ public bool SendAttempted { get; set; }
+
+ public bool SendSucceeded { get; set; }
+
+ public DateTime? SentOn { get; set; }
+
+ public bool Responded { get; set; }
+
+ public DateTime? RespondedOn { get; set; }
+
+ [MaxLength(128)]
+ public string ResponseToken { get; set; }
+
+ [NotMapped]
+ [JsonIgnore]
+ public object IdValue
+ {
+ get { return CommunicationTestResultId == Guid.Empty ? null : (object)CommunicationTestResultId.ToString(); }
+ set { CommunicationTestResultId = value == null ? Guid.Empty : Guid.Parse(value.ToString()); }
+ }
+
+ [NotMapped]
+ public string TableName => "CommunicationTestResults";
+
+ [NotMapped]
+ public string IdName => "CommunicationTestResultId";
+
+ [NotMapped]
+ public int IdType => 1;
+
+ [NotMapped]
+ public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "CommunicationTestRun" };
+ }
+}
diff --git a/Core/Resgrid.Model/CommunicationTestRun.cs b/Core/Resgrid.Model/CommunicationTestRun.cs
new file mode 100644
index 00000000..b098e9f0
--- /dev/null
+++ b/Core/Resgrid.Model/CommunicationTestRun.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+using Newtonsoft.Json;
+
+namespace Resgrid.Model
+{
+ [Table("CommunicationTestRuns")]
+ public class CommunicationTestRun : IEntity
+ {
+ [Key]
+ [Required]
+ [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
+ public Guid CommunicationTestRunId { get; set; }
+
+ [Required]
+ public Guid CommunicationTestId { get; set; }
+
+ [ForeignKey("CommunicationTestId")]
+ public virtual CommunicationTest CommunicationTest { get; set; }
+
+ [Required]
+ public int DepartmentId { get; set; }
+
+ [MaxLength(128)]
+ public string InitiatedByUserId { get; set; }
+
+ public DateTime StartedOn { get; set; }
+
+ public DateTime? CompletedOn { get; set; }
+
+ public int Status { get; set; }
+
+ [Required]
+ [MaxLength(20)]
+ public string RunCode { get; set; }
+
+ public int TotalUsersTested { get; set; }
+
+ public int TotalResponses { get; set; }
+
+ [NotMapped]
+ [JsonIgnore]
+ public object IdValue
+ {
+ get { return CommunicationTestRunId == Guid.Empty ? null : (object)CommunicationTestRunId.ToString(); }
+ set { CommunicationTestRunId = value == null ? Guid.Empty : Guid.Parse(value.ToString()); }
+ }
+
+ [NotMapped]
+ public string TableName => "CommunicationTestRuns";
+
+ [NotMapped]
+ public string IdName => "CommunicationTestRunId";
+
+ [NotMapped]
+ public int IdType => 1;
+
+ [NotMapped]
+ public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "CommunicationTest" };
+ }
+}
diff --git a/Core/Resgrid.Model/CommunicationTestRunStatus.cs b/Core/Resgrid.Model/CommunicationTestRunStatus.cs
new file mode 100644
index 00000000..7c33e093
--- /dev/null
+++ b/Core/Resgrid.Model/CommunicationTestRunStatus.cs
@@ -0,0 +1,11 @@
+namespace Resgrid.Model
+{
+ public enum CommunicationTestRunStatus
+ {
+ Pending = 0,
+ Running = 1,
+ AwaitingResponses = 2,
+ Completed = 3,
+ Failed = 4
+ }
+}
diff --git a/Core/Resgrid.Model/CommunicationTestScheduleType.cs b/Core/Resgrid.Model/CommunicationTestScheduleType.cs
new file mode 100644
index 00000000..28d5854d
--- /dev/null
+++ b/Core/Resgrid.Model/CommunicationTestScheduleType.cs
@@ -0,0 +1,9 @@
+namespace Resgrid.Model
+{
+ public enum CommunicationTestScheduleType
+ {
+ OnDemand = 0,
+ Weekly = 1,
+ Monthly = 2
+ }
+}
diff --git a/Core/Resgrid.Model/Repositories/ICallVideoFeedRepository.cs b/Core/Resgrid.Model/Repositories/ICallVideoFeedRepository.cs
new file mode 100644
index 00000000..ca8b917c
--- /dev/null
+++ b/Core/Resgrid.Model/Repositories/ICallVideoFeedRepository.cs
@@ -0,0 +1,11 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Repositories
+{
+ public interface ICallVideoFeedRepository : IRepository
+ {
+ Task> GetByCallIdAsync(int callId);
+ Task> GetByDepartmentIdAsync(int departmentId);
+ }
+}
diff --git a/Core/Resgrid.Model/Repositories/ICommunicationTestRepository.cs b/Core/Resgrid.Model/Repositories/ICommunicationTestRepository.cs
new file mode 100644
index 00000000..cfc722d2
--- /dev/null
+++ b/Core/Resgrid.Model/Repositories/ICommunicationTestRepository.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Repositories
+{
+ public interface ICommunicationTestRepository : IRepository
+ {
+ Task> GetActiveTestsForScheduleTypeAsync(int scheduleType);
+ }
+}
diff --git a/Core/Resgrid.Model/Repositories/ICommunicationTestResultRepository.cs b/Core/Resgrid.Model/Repositories/ICommunicationTestResultRepository.cs
new file mode 100644
index 00000000..8538ddd9
--- /dev/null
+++ b/Core/Resgrid.Model/Repositories/ICommunicationTestResultRepository.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Repositories
+{
+ public interface ICommunicationTestResultRepository : IRepository
+ {
+ Task> GetResultsByRunIdAsync(Guid communicationTestRunId);
+ Task GetResultByResponseTokenAsync(string responseToken);
+ }
+}
diff --git a/Core/Resgrid.Model/Repositories/ICommunicationTestRunRepository.cs b/Core/Resgrid.Model/Repositories/ICommunicationTestRunRepository.cs
new file mode 100644
index 00000000..d7692af3
--- /dev/null
+++ b/Core/Resgrid.Model/Repositories/ICommunicationTestRunRepository.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Repositories
+{
+ public interface ICommunicationTestRunRepository : IRepository
+ {
+ Task> GetRunsByTestIdAsync(Guid communicationTestId);
+ Task GetRunByRunCodeAsync(string runCode);
+ Task> GetOpenRunsAsync();
+ }
+}
diff --git a/Core/Resgrid.Model/Services/ICallsService.cs b/Core/Resgrid.Model/Services/ICallsService.cs
index b47154b2..ae82eadd 100644
--- a/Core/Resgrid.Model/Services/ICallsService.cs
+++ b/Core/Resgrid.Model/Services/ICallsService.cs
@@ -419,7 +419,7 @@ Task ClearGroupForDispatchesAsync(int departmentGroupId,
/// if set to true [get protocols].
/// if set to true [get references].
/// Task<Call>.
- Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences, bool getContacts);
+ Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences, bool getContacts, bool getVideoFeeds = false);
Task> GetAllNonDispatchedScheduledCallsWithinDateRange(DateTime startDate, DateTime endDate);
@@ -433,5 +433,13 @@ Task ClearGroupForDispatchesAsync(int departmentGroupId,
Task> GetCallsByContactIdAsync(string contactId, int departmentId);
Task DeleteCallContactsAsync(int callId, CancellationToken cancellationToken = default(CancellationToken));
+
+ Task SaveCallVideoFeedAsync(CallVideoFeed feed, CancellationToken cancellationToken = default(CancellationToken));
+
+ Task GetCallVideoFeedByIdAsync(string callVideoFeedId);
+
+ Task> GetCallVideoFeedsByCallIdAsync(int callId);
+
+ Task DeleteCallVideoFeedAsync(CallVideoFeed feed, string deletedByUserId, CancellationToken cancellationToken = default(CancellationToken));
}
}
diff --git a/Core/Resgrid.Model/Services/ICommunicationService.cs b/Core/Resgrid.Model/Services/ICommunicationService.cs
index a758e599..a0bfe6f2 100644
--- a/Core/Resgrid.Model/Services/ICommunicationService.cs
+++ b/Core/Resgrid.Model/Services/ICommunicationService.cs
@@ -45,6 +45,18 @@ Task SendCallAsync(Call call, CallDispatch dispatch, string departmentNumb
Task SendUnitCallAsync(Call call, CallDispatchUnit dispatch, string departmentNumber,
string address = null);
+ ///
+ /// Sends a cancellation notification for a call dispatch to a user.
+ ///
+ Task SendCancelCallAsync(Call call, CallDispatch dispatch, string departmentNumber, int departmentId,
+ UserProfile profile = null, string address = null);
+
+ ///
+ /// Sends a cancellation notification for a call dispatch to a unit.
+ ///
+ Task SendCancelUnitCallAsync(Call call, CallDispatchUnit dispatch, string departmentNumber,
+ string address = null);
+
///
/// Sends the notification asynchronous.
///
diff --git a/Core/Resgrid.Model/Services/ICommunicationTestService.cs b/Core/Resgrid.Model/Services/ICommunicationTestService.cs
new file mode 100644
index 00000000..49aa1cb3
--- /dev/null
+++ b/Core/Resgrid.Model/Services/ICommunicationTestService.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Services
+{
+ public interface ICommunicationTestService
+ {
+ Task> GetTestsByDepartmentIdAsync(int departmentId);
+ Task GetTestByIdAsync(Guid communicationTestId);
+ Task CanCreateScheduledTestAsync(int departmentId, int scheduleType, Guid? excludeTestId = null);
+ Task SaveTestAsync(CommunicationTest test, CancellationToken cancellationToken = default);
+ Task DeleteTestAsync(Guid communicationTestId, CancellationToken cancellationToken = default);
+
+ Task CanStartOnDemandRunAsync(Guid communicationTestId);
+ Task StartTestRunAsync(Guid communicationTestId, int departmentId, string initiatedByUserId, CancellationToken cancellationToken = default);
+ Task> GetRunsByTestIdAsync(Guid communicationTestId);
+ Task GetRunByIdAsync(Guid communicationTestRunId);
+ Task> GetRunsByDepartmentIdAsync(int departmentId);
+
+ Task> GetResultsByRunIdAsync(Guid communicationTestRunId);
+
+ Task RecordSmsResponseAsync(string runCode, string fromPhoneNumber);
+ Task RecordEmailResponseAsync(string responseToken);
+ Task RecordVoiceResponseAsync(string responseToken);
+ Task RecordPushResponseAsync(string responseToken);
+
+ Task ProcessScheduledTestsAsync(CancellationToken cancellationToken = default);
+ Task CompleteExpiredRunsAsync(CancellationToken cancellationToken = default);
+ }
+}
diff --git a/Core/Resgrid.Model/Services/IEmailService.cs b/Core/Resgrid.Model/Services/IEmailService.cs
index 45f7d92c..c4d60960 100644
--- a/Core/Resgrid.Model/Services/IEmailService.cs
+++ b/Core/Resgrid.Model/Services/IEmailService.cs
@@ -75,6 +75,11 @@ Task SendPasswordResetEmail(string emailAddress, string name, string userN
/// Task<System.Boolean>.
Task SendCallAsync(Call call, CallDispatch dispatch, UserProfile profile = null);
+ ///
+ /// Sends a cancellation email for a call dispatch.
+ ///
+ Task SendCancelCallAsync(Call call, CallDispatch dispatch, UserProfile profile = null);
+
///
/// Sends the trouble alert.
///
diff --git a/Core/Resgrid.Model/Services/ISmsService.cs b/Core/Resgrid.Model/Services/ISmsService.cs
index 291c58c6..c976a962 100644
--- a/Core/Resgrid.Model/Services/ISmsService.cs
+++ b/Core/Resgrid.Model/Services/ISmsService.cs
@@ -32,6 +32,12 @@ Task SendMessageAsync(Message message, string departmentNumber, int depart
Task SendCallAsync(Call call, CallDispatch dispatch, string departmentNumber, int departmentId,
UserProfile profile = null, string address = null, Payment payment = null);
+ ///
+ /// Sends a cancellation SMS for a call dispatch.
+ ///
+ Task SendCancelCallAsync(Call call, CallDispatch dispatch, string departmentNumber, int departmentId,
+ UserProfile profile = null, string address = null, Payment payment = null);
+
///
/// Sends the trouble alert.
///
diff --git a/Core/Resgrid.Services/CallsService.cs b/Core/Resgrid.Services/CallsService.cs
index 44649901..32d2e92b 100644
--- a/Core/Resgrid.Services/CallsService.cs
+++ b/Core/Resgrid.Services/CallsService.cs
@@ -42,6 +42,7 @@ public class CallsService : ICallsService
private readonly ICallReferencesRepository _callReferencesRepository;
private readonly ICallContactsRepository _callContactsRepository;
private readonly IIndoorMapService _indoorMapService;
+ private readonly ICallVideoFeedRepository _callVideoFeedRepository;
public CallsService(ICallsRepository callsRepository, ICommunicationService communicationService,
ICallDispatchesRepository callDispatchesRepository, ICallTypesRepository callTypesRepository, ICallEmailFactory callEmailFactory,
@@ -51,7 +52,7 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm
IDepartmentCallPriorityRepository departmentCallPriorityRepository, IShortenUrlProvider shortenUrlProvider,
ICallProtocolsRepository callProtocolsRepository, IGeoLocationProvider geoLocationProvider, IDepartmentsService departmentsService,
ICallReferencesRepository callReferencesRepository, ICallContactsRepository callContactsRepository,
- IIndoorMapService indoorMapService)
+ IIndoorMapService indoorMapService, ICallVideoFeedRepository callVideoFeedRepository)
{
_callsRepository = callsRepository;
_communicationService = communicationService;
@@ -72,6 +73,7 @@ public CallsService(ICallsRepository callsRepository, ICommunicationService comm
_callReferencesRepository = callReferencesRepository;
_callContactsRepository = callContactsRepository;
_indoorMapService = indoorMapService;
+ _callVideoFeedRepository = callVideoFeedRepository;
}
public async Task SaveCallAsync(Call call, CancellationToken cancellationToken = default(CancellationToken))
@@ -498,7 +500,7 @@ public async Task> GetActiveCallPrioritiesForDepart
return activePriorities;
}
- public async Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences, bool getContacts)
+ public async Task PopulateCallData(Call call, bool getDispatches, bool getAttachments, bool getNotes, bool getGroupDispatches, bool getUnitDispatches, bool getRoleDispatches, bool getProtocols, bool getReferences, bool getContacts, bool getVideoFeeds = false)
{
if (call == null)
return null;
@@ -585,6 +587,15 @@ public async Task PopulateCallData(Call call, bool getDispatches, bool get
else
call.Contacts = new List();
}
+ if (getVideoFeeds && call.VideoFeeds == null)
+ {
+ var items = await _callVideoFeedRepository.GetByCallIdAsync(call.CallId);
+
+ if (items != null)
+ call.VideoFeeds = items.OrderBy(f => f.SortOrder).ToList();
+ else
+ call.VideoFeeds = new List();
+ }
return call;
}
@@ -987,5 +998,38 @@ public async Task> GetCallsByContactIdAsync(string contactId, int dep
return new List();
}
+
+ public async Task SaveCallVideoFeedAsync(CallVideoFeed feed, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ var saved = await _callVideoFeedRepository.SaveOrUpdateAsync(feed, cancellationToken);
+ return saved;
+ }
+
+ public async Task GetCallVideoFeedByIdAsync(string callVideoFeedId)
+ {
+ var feed = await _callVideoFeedRepository.GetByIdAsync(callVideoFeedId);
+ return feed;
+ }
+
+ public async Task> GetCallVideoFeedsByCallIdAsync(int callId)
+ {
+ var feeds = await _callVideoFeedRepository.GetByCallIdAsync(callId);
+
+ if (feeds != null && feeds.Any())
+ return feeds.OrderBy(f => f.SortOrder).ToList();
+
+ return new List();
+ }
+
+ public async Task DeleteCallVideoFeedAsync(CallVideoFeed feed, string deletedByUserId, CancellationToken cancellationToken = default(CancellationToken))
+ {
+ feed.IsDeleted = true;
+ feed.DeletedByUserId = deletedByUserId;
+ feed.DeletedOn = DateTime.UtcNow;
+
+ await _callVideoFeedRepository.SaveOrUpdateAsync(feed, cancellationToken);
+
+ return true;
+ }
}
}
diff --git a/Core/Resgrid.Services/CommunicationService.cs b/Core/Resgrid.Services/CommunicationService.cs
index ab732bc6..19bdfac3 100644
--- a/Core/Resgrid.Services/CommunicationService.cs
+++ b/Core/Resgrid.Services/CommunicationService.cs
@@ -177,7 +177,9 @@ public async Task SendCallAsync(Call call, CallDispatch dispatch, string d
spc.SubTitle = call.NatureOfCall.Truncate(200);
}
- if (!String.IsNullOrWhiteSpace(spc.SubTitle))
+ if (String.IsNullOrWhiteSpace(spc.SubTitle))
+ spc.SubTitle = String.Empty;
+ else
spc.SubTitle = StringHelpers.StripHtmlTagsCharArray(spc.SubTitle);
spc.SubTitle = Regex.Replace(spc.SubTitle, @"((http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?)", "");
@@ -312,6 +314,204 @@ public async Task SendUnitCallAsync(Call call, CallDispatchUnit dispatch,
return true;
}
+ public async Task SendCancelCallAsync(Call call, CallDispatch dispatch, string departmentNumber, int departmentId, UserProfile profile = null, string address = null)
+ {
+ if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId))
+ return false;
+
+ if (!await CanSendToUser(dispatch.UserId, departmentId))
+ return false;
+
+ if (profile == null)
+ profile = await _userProfileService.GetProfileByUserIdAsync(dispatch.UserId);
+
+ // Send a Push Notification
+ if (profile == null || profile.SendPush)
+ {
+ try
+ {
+ var spc = new StandardPushCall();
+ spc.CallId = call.CallId;
+ spc.Title = string.Format("Dispatch Cancelled - {0}", call.Name);
+ spc.Priority = call.Priority;
+ spc.ActiveCallCount = 1;
+ spc.DepartmentId = departmentId;
+ spc.DepartmentCode = call.Department?.Code;
+
+ if (call.CallPriority != null && !String.IsNullOrWhiteSpace(call.CallPriority.Color))
+ {
+ spc.Color = call.CallPriority.Color;
+ }
+ else
+ {
+ spc.Color = "#000000";
+ }
+
+ string subTitle = String.Empty;
+
+ if (String.IsNullOrWhiteSpace(address) && !String.IsNullOrWhiteSpace(call.Address))
+ {
+ subTitle = call.Address;
+ }
+ else if (!String.IsNullOrWhiteSpace(address))
+ {
+ subTitle = address;
+ }
+ else if (!string.IsNullOrEmpty(call.GeoLocationData) && call.GeoLocationData.Length > 1)
+ {
+ try
+ {
+ string[] points = call.GeoLocationData.Split(char.Parse(","));
+
+ if (points != null && points.Length == 2)
+ {
+ subTitle = await _geoLocationProvider.GetAproxAddressFromLatLong(double.Parse(points[0]), double.Parse(points[1]));
+ }
+ }
+ catch
+ { }
+ }
+
+ if (!string.IsNullOrEmpty(subTitle))
+ {
+ spc.SubTitle = subTitle.Truncate(200);
+ }
+ else
+ {
+ if (!string.IsNullOrEmpty(call.NatureOfCall))
+ spc.SubTitle = call.NatureOfCall.Truncate(200);
+ }
+
+ if (String.IsNullOrWhiteSpace(spc.SubTitle))
+ spc.SubTitle = String.Empty;
+ else
+ spc.SubTitle = StringHelpers.StripHtmlTagsCharArray(spc.SubTitle);
+
+ spc.SubTitle = Regex.Replace(spc.SubTitle, @"((http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?)", "");
+
+ spc.Title = StringHelpers.StripHtmlTagsCharArray(spc.Title);
+ spc.Title = Regex.Replace(spc.Title, @"((http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?)", "");
+
+ spc.Title = spc.Title.Replace(char.Parse("/"), char.Parse(" "));
+ spc.SubTitle = spc.SubTitle.Replace(char.Parse("/"), char.Parse(" "));
+
+ await _pushService.PushCall(spc, dispatch.UserId, profile, call.CallPriority);
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ }
+ }
+
+ // Send an SMS Message
+ if (profile == null || profile.SendSms)
+ {
+ if (profile == null || profile.MobileNumberVerified.IsContactMethodAllowedForSending())
+ {
+ try
+ {
+ var payment = await _subscriptionsService.GetCurrentPaymentForDepartmentAsync(departmentId);
+ await _smsService.SendCancelCallAsync(call, dispatch, departmentNumber, departmentId, profile, call.Address, payment);
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ }
+ }
+ }
+
+ // Send an Email
+ if (profile == null || profile.SendEmail)
+ {
+ if (profile == null || profile.EmailVerified.IsContactMethodAllowedForSending())
+ {
+ try
+ {
+ await _emailService.SendCancelCallAsync(call, dispatch, profile);
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ }
+ }
+ }
+
+ // No voice call for cancellation
+
+ return true;
+ }
+
+ public async Task SendCancelUnitCallAsync(Call call, CallDispatchUnit dispatch, string departmentNumber, string address = null)
+ {
+ var spc = new StandardPushCall();
+ spc.CallId = call.CallId;
+ spc.Title = string.Format("Dispatch Cancelled - {0}", call.Name);
+ spc.Priority = call.Priority;
+ spc.ActiveCallCount = 1;
+ spc.DepartmentId = call.DepartmentId;
+ spc.DepartmentCode = call.Department?.Code;
+
+ if (call.CallPriority != null && !String.IsNullOrWhiteSpace(call.CallPriority.Color))
+ {
+ spc.Color = call.CallPriority.Color;
+ }
+ else
+ {
+ spc.Color = "#000000";
+ }
+
+ string subTitle = String.Empty;
+
+ if (String.IsNullOrWhiteSpace(address) && !String.IsNullOrWhiteSpace(call.Address))
+ {
+ subTitle = call.Address;
+ }
+ else if (!String.IsNullOrWhiteSpace(address))
+ {
+ subTitle = address;
+ }
+ else if (!string.IsNullOrEmpty(call.GeoLocationData) && call.GeoLocationData.Length > 1)
+ {
+ try
+ {
+ string[] points = call.GeoLocationData.Split(char.Parse(","));
+
+ if (points != null && points.Length == 2)
+ {
+ subTitle = await _geoLocationProvider.GetAproxAddressFromLatLong(double.Parse(points[0]), double.Parse(points[1]));
+ }
+ }
+ catch
+ { }
+ }
+
+ if (!string.IsNullOrEmpty(subTitle))
+ {
+ spc.SubTitle = subTitle.Truncate(200);
+ }
+ else
+ {
+ if (!string.IsNullOrEmpty(call.NatureOfCall))
+ spc.SubTitle = call.NatureOfCall.Truncate(200);
+ }
+
+ if (!String.IsNullOrWhiteSpace(spc.SubTitle))
+ spc.SubTitle = StringHelpers.StripHtmlTagsCharArray(spc.SubTitle);
+
+ spc.Title = StringHelpers.StripHtmlTagsCharArray(spc.Title);
+
+ try
+ {
+ await _pushService.PushCallUnit(spc, dispatch.UnitId, call.CallPriority);
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ }
+
+ return true;
+ }
+
public async Task SendNotificationAsync(string userId, int departmentId, string message, string departmentNumber, Department department, string title = "Notification", UserProfile profile = null)
{
if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId))
diff --git a/Core/Resgrid.Services/CommunicationTestService.cs b/Core/Resgrid.Services/CommunicationTestService.cs
new file mode 100644
index 00000000..0fa325bb
--- /dev/null
+++ b/Core/Resgrid.Services/CommunicationTestService.cs
@@ -0,0 +1,437 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Resgrid.Framework;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Services;
+
+namespace Resgrid.Services
+{
+ public class CommunicationTestService : ICommunicationTestService
+ {
+ private readonly ICommunicationTestRepository _communicationTestRepository;
+ private readonly ICommunicationTestRunRepository _communicationTestRunRepository;
+ private readonly ICommunicationTestResultRepository _communicationTestResultRepository;
+ private readonly IDepartmentsService _departmentsService;
+ private readonly IUserProfileService _userProfileService;
+
+ public CommunicationTestService(
+ ICommunicationTestRepository communicationTestRepository,
+ ICommunicationTestRunRepository communicationTestRunRepository,
+ ICommunicationTestResultRepository communicationTestResultRepository,
+ IDepartmentsService departmentsService,
+ IUserProfileService userProfileService)
+ {
+ _communicationTestRepository = communicationTestRepository;
+ _communicationTestRunRepository = communicationTestRunRepository;
+ _communicationTestResultRepository = communicationTestResultRepository;
+ _departmentsService = departmentsService;
+ _userProfileService = userProfileService;
+ }
+
+ public async Task> GetTestsByDepartmentIdAsync(int departmentId)
+ {
+ return await _communicationTestRepository.GetAllByDepartmentIdAsync(departmentId);
+ }
+
+ public async Task GetTestByIdAsync(Guid communicationTestId)
+ {
+ return await _communicationTestRepository.GetByIdAsync(communicationTestId);
+ }
+
+ public async Task CanCreateScheduledTestAsync(int departmentId, int scheduleType, Guid? excludeTestId = null)
+ {
+ if (scheduleType == (int)CommunicationTestScheduleType.OnDemand)
+ return true;
+
+ var existing = await _communicationTestRepository.GetAllByDepartmentIdAsync(departmentId);
+ if (existing == null)
+ return true;
+
+ return !existing.Any(t =>
+ t.ScheduleType == scheduleType &&
+ (!excludeTestId.HasValue || t.CommunicationTestId != excludeTestId.Value));
+ }
+
+ public async Task SaveTestAsync(CommunicationTest test, CancellationToken cancellationToken = default)
+ {
+ return await _communicationTestRepository.SaveOrUpdateAsync(test, cancellationToken, true);
+ }
+
+ public async Task DeleteTestAsync(Guid communicationTestId, CancellationToken cancellationToken = default)
+ {
+ var test = await _communicationTestRepository.GetByIdAsync(communicationTestId);
+ if (test == null)
+ return false;
+
+ return await _communicationTestRepository.DeleteAsync(test, cancellationToken);
+ }
+
+ public async Task CanStartOnDemandRunAsync(Guid communicationTestId)
+ {
+ var existingRuns = await _communicationTestRunRepository.GetRunsByTestIdAsync(communicationTestId);
+ if (existingRuns == null || !existingRuns.Any())
+ return true;
+
+ var mostRecent = existingRuns.OrderByDescending(r => r.StartedOn).FirstOrDefault();
+ if (mostRecent == null)
+ return true;
+
+ return mostRecent.StartedOn.AddHours(48) <= DateTime.UtcNow;
+ }
+
+ public async Task StartTestRunAsync(Guid communicationTestId, int departmentId, string initiatedByUserId, CancellationToken cancellationToken = default)
+ {
+ var test = await _communicationTestRepository.GetByIdAsync(communicationTestId);
+ if (test == null)
+ return null;
+
+ // Rate limit: on-demand tests can only run once every 48 hours
+ if (test.ScheduleType == (int)CommunicationTestScheduleType.OnDemand)
+ {
+ if (!await CanStartOnDemandRunAsync(communicationTestId))
+ return null;
+ }
+
+ var runCode = GenerateRunCode();
+
+ var run = new CommunicationTestRun
+ {
+ CommunicationTestId = communicationTestId,
+ DepartmentId = departmentId,
+ InitiatedByUserId = initiatedByUserId,
+ StartedOn = DateTime.UtcNow,
+ Status = (int)CommunicationTestRunStatus.Running,
+ RunCode = runCode,
+ TotalUsersTested = 0,
+ TotalResponses = 0
+ };
+
+ run = await _communicationTestRunRepository.SaveOrUpdateAsync(run, cancellationToken, true);
+
+ var members = await _departmentsService.GetAllMembersForDepartmentAsync(departmentId);
+ var profiles = await _userProfileService.GetAllProfilesForDepartmentAsync(departmentId);
+
+ int totalUsersTested = 0;
+
+ foreach (var member in members)
+ {
+ profiles.TryGetValue(member.UserId, out var profile);
+ bool userHasResults = false;
+
+ if (test.TestEmail)
+ {
+ var emailVerified = profile?.EmailVerified;
+ var result = new CommunicationTestResult
+ {
+ CommunicationTestRunId = run.CommunicationTestRunId,
+ DepartmentId = departmentId,
+ UserId = member.UserId,
+ Channel = (int)CommunicationTestChannel.Email,
+ ContactValue = profile?.MembershipEmail,
+ VerificationStatus = (int)emailVerified.ToVerificationStatus(),
+ SendAttempted = emailVerified.IsContactMethodAllowedForSending(),
+ SendSucceeded = false,
+ Responded = false,
+ ResponseToken = Guid.NewGuid().ToString("N")
+ };
+
+ if (result.SendAttempted && !string.IsNullOrWhiteSpace(result.ContactValue))
+ {
+ result.SendSucceeded = true;
+ result.SentOn = DateTime.UtcNow;
+ }
+
+ await _communicationTestResultRepository.SaveOrUpdateAsync(result, cancellationToken, true);
+ userHasResults = true;
+ }
+
+ if (test.TestSms)
+ {
+ var mobileVerified = profile?.MobileNumberVerified;
+ var carrierName = "";
+ if (profile != null && profile.MobileCarrier > 0)
+ carrierName = ((MobileCarriers)profile.MobileCarrier).GetDescription();
+
+ var result = new CommunicationTestResult
+ {
+ CommunicationTestRunId = run.CommunicationTestRunId,
+ DepartmentId = departmentId,
+ UserId = member.UserId,
+ Channel = (int)CommunicationTestChannel.Sms,
+ ContactValue = profile?.GetPhoneNumber(),
+ ContactCarrier = carrierName,
+ VerificationStatus = (int)mobileVerified.ToVerificationStatus(),
+ SendAttempted = mobileVerified.IsContactMethodAllowedForSending(),
+ SendSucceeded = false,
+ Responded = false,
+ ResponseToken = Guid.NewGuid().ToString("N")
+ };
+
+ if (result.SendAttempted && !string.IsNullOrWhiteSpace(result.ContactValue))
+ {
+ result.SendSucceeded = true;
+ result.SentOn = DateTime.UtcNow;
+ }
+
+ await _communicationTestResultRepository.SaveOrUpdateAsync(result, cancellationToken, true);
+ userHasResults = true;
+ }
+
+ if (test.TestVoice)
+ {
+ var mobileVerified = profile?.MobileNumberVerified;
+ var result = new CommunicationTestResult
+ {
+ CommunicationTestRunId = run.CommunicationTestRunId,
+ DepartmentId = departmentId,
+ UserId = member.UserId,
+ Channel = (int)CommunicationTestChannel.Voice,
+ ContactValue = profile?.GetPhoneNumber(),
+ VerificationStatus = (int)mobileVerified.ToVerificationStatus(),
+ SendAttempted = mobileVerified.IsContactMethodAllowedForSending(),
+ SendSucceeded = false,
+ Responded = false,
+ ResponseToken = Guid.NewGuid().ToString("N")
+ };
+
+ if (result.SendAttempted && !string.IsNullOrWhiteSpace(result.ContactValue))
+ {
+ result.SendSucceeded = true;
+ result.SentOn = DateTime.UtcNow;
+ }
+
+ await _communicationTestResultRepository.SaveOrUpdateAsync(result, cancellationToken, true);
+ userHasResults = true;
+ }
+
+ if (test.TestPush)
+ {
+ var result = new CommunicationTestResult
+ {
+ CommunicationTestRunId = run.CommunicationTestRunId,
+ DepartmentId = departmentId,
+ UserId = member.UserId,
+ Channel = (int)CommunicationTestChannel.Push,
+ VerificationStatus = (int)ContactVerificationStatus.Verified,
+ SendAttempted = true,
+ SendSucceeded = true,
+ SentOn = DateTime.UtcNow,
+ Responded = false,
+ ResponseToken = Guid.NewGuid().ToString("N")
+ };
+
+ await _communicationTestResultRepository.SaveOrUpdateAsync(result, cancellationToken, true);
+ userHasResults = true;
+ }
+
+ if (userHasResults)
+ totalUsersTested++;
+ }
+
+ run.TotalUsersTested = totalUsersTested;
+ run.Status = (int)CommunicationTestRunStatus.AwaitingResponses;
+ run = await _communicationTestRunRepository.SaveOrUpdateAsync(run, cancellationToken, true);
+
+ return run;
+ }
+
+ public async Task> GetRunsByTestIdAsync(Guid communicationTestId)
+ {
+ return await _communicationTestRunRepository.GetRunsByTestIdAsync(communicationTestId);
+ }
+
+ public async Task GetRunByIdAsync(Guid communicationTestRunId)
+ {
+ return await _communicationTestRunRepository.GetByIdAsync(communicationTestRunId);
+ }
+
+ public async Task> GetRunsByDepartmentIdAsync(int departmentId)
+ {
+ return await _communicationTestRunRepository.GetAllByDepartmentIdAsync(departmentId);
+ }
+
+ public async Task> GetResultsByRunIdAsync(Guid communicationTestRunId)
+ {
+ return await _communicationTestResultRepository.GetResultsByRunIdAsync(communicationTestRunId);
+ }
+
+ public async Task RecordSmsResponseAsync(string runCode, string fromPhoneNumber)
+ {
+ var run = await _communicationTestRunRepository.GetRunByRunCodeAsync(runCode);
+ if (run == null || run.Status == (int)CommunicationTestRunStatus.Completed || run.Status == (int)CommunicationTestRunStatus.Failed)
+ return false;
+
+ var results = await _communicationTestResultRepository.GetResultsByRunIdAsync(run.CommunicationTestRunId);
+ var cleanPhone = fromPhoneNumber.Replace("+", "").Replace("-", "").Replace(" ", "").Replace("(", "").Replace(")", "");
+
+ var matchingResult = results.FirstOrDefault(r =>
+ r.Channel == (int)CommunicationTestChannel.Sms &&
+ !r.Responded &&
+ r.ContactValue != null &&
+ r.ContactValue.Replace("+", "").Replace("-", "").Replace(" ", "").Replace("(", "").Replace(")", "") == cleanPhone);
+
+ if (matchingResult == null)
+ return false;
+
+ matchingResult.Responded = true;
+ matchingResult.RespondedOn = DateTime.UtcNow;
+ await _communicationTestResultRepository.SaveOrUpdateAsync(matchingResult, CancellationToken.None, true);
+
+ await UpdateRunResponseCountAsync(run);
+ return true;
+ }
+
+ public async Task RecordEmailResponseAsync(string responseToken)
+ {
+ return await RecordResponseByTokenAsync(responseToken, CommunicationTestChannel.Email);
+ }
+
+ public async Task RecordVoiceResponseAsync(string responseToken)
+ {
+ return await RecordResponseByTokenAsync(responseToken, CommunicationTestChannel.Voice);
+ }
+
+ public async Task RecordPushResponseAsync(string responseToken)
+ {
+ return await RecordResponseByTokenAsync(responseToken, CommunicationTestChannel.Push);
+ }
+
+ public async Task ProcessScheduledTestsAsync(CancellationToken cancellationToken = default)
+ {
+ var now = DateTime.UtcNow;
+
+ // Process weekly tests
+ var weeklyTests = await _communicationTestRepository.GetActiveTestsForScheduleTypeAsync((int)CommunicationTestScheduleType.Weekly);
+ if (weeklyTests != null)
+ {
+ foreach (var test in weeklyTests)
+ {
+ if (ShouldRunWeeklyTest(test, now) && await HasPassedFirstEligiblePeriodAsync(test))
+ {
+ await StartTestRunAsync(test.CommunicationTestId, test.DepartmentId, test.CreatedByUserId, cancellationToken);
+ }
+ }
+ }
+
+ // Process monthly tests
+ var monthlyTests = await _communicationTestRepository.GetActiveTestsForScheduleTypeAsync((int)CommunicationTestScheduleType.Monthly);
+ if (monthlyTests != null)
+ {
+ foreach (var test in monthlyTests)
+ {
+ if (ShouldRunMonthlyTest(test, now) && await HasPassedFirstEligiblePeriodAsync(test))
+ {
+ await StartTestRunAsync(test.CommunicationTestId, test.DepartmentId, test.CreatedByUserId, cancellationToken);
+ }
+ }
+ }
+ }
+
+ public async Task CompleteExpiredRunsAsync(CancellationToken cancellationToken = default)
+ {
+ var openRuns = await _communicationTestRunRepository.GetOpenRunsAsync();
+ if (openRuns == null)
+ return;
+
+ foreach (var run in openRuns)
+ {
+ var test = await _communicationTestRepository.GetByIdAsync(run.CommunicationTestId);
+ if (test == null)
+ continue;
+
+ var windowMinutes = test.ResponseWindowMinutes > 0 ? test.ResponseWindowMinutes : 60;
+ if (run.StartedOn.AddMinutes(windowMinutes) <= DateTime.UtcNow)
+ {
+ run.Status = (int)CommunicationTestRunStatus.Completed;
+ run.CompletedOn = DateTime.UtcNow;
+ await _communicationTestRunRepository.SaveOrUpdateAsync(run, cancellationToken, true);
+ }
+ }
+ }
+
+ private async Task RecordResponseByTokenAsync(string responseToken, CommunicationTestChannel channel)
+ {
+ var result = await _communicationTestResultRepository.GetResultByResponseTokenAsync(responseToken);
+ if (result == null || result.Responded || result.Channel != (int)channel)
+ return false;
+
+ result.Responded = true;
+ result.RespondedOn = DateTime.UtcNow;
+ await _communicationTestResultRepository.SaveOrUpdateAsync(result, CancellationToken.None, true);
+
+ var run = await _communicationTestRunRepository.GetByIdAsync(result.CommunicationTestRunId);
+ if (run != null)
+ await UpdateRunResponseCountAsync(run);
+
+ return true;
+ }
+
+ private async Task UpdateRunResponseCountAsync(CommunicationTestRun run)
+ {
+ var allResults = await _communicationTestResultRepository.GetResultsByRunIdAsync(run.CommunicationTestRunId);
+ var respondedUsers = allResults.Where(r => r.Responded).Select(r => r.UserId).Distinct().Count();
+ run.TotalResponses = respondedUsers;
+ await _communicationTestRunRepository.SaveOrUpdateAsync(run, CancellationToken.None, true);
+ }
+
+ private static string GenerateRunCode()
+ {
+ const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
+ var random = new Random();
+ var code = new char[4];
+ for (int i = 0; i < 4; i++)
+ code[i] = chars[random.Next(chars.Length)];
+ return "CT-" + new string(code);
+ }
+
+ private static bool ShouldRunWeeklyTest(CommunicationTest test, DateTime utcNow)
+ {
+ switch (utcNow.DayOfWeek)
+ {
+ case DayOfWeek.Sunday: return test.Sunday;
+ case DayOfWeek.Monday: return test.Monday;
+ case DayOfWeek.Tuesday: return test.Tuesday;
+ case DayOfWeek.Wednesday: return test.Wednesday;
+ case DayOfWeek.Thursday: return test.Thursday;
+ case DayOfWeek.Friday: return test.Friday;
+ case DayOfWeek.Saturday: return test.Saturday;
+ default: return false;
+ }
+ }
+
+ private static bool ShouldRunMonthlyTest(CommunicationTest test, DateTime utcNow)
+ {
+ return test.DayOfMonth.HasValue && test.DayOfMonth.Value == utcNow.Day;
+ }
+
+ ///
+ /// Ensures the first run of a scheduled test happens in the NEXT eligible period
+ /// after creation, not the same week/month. This prevents users from abusing
+ /// scheduled tests to send immediately.
+ ///
+ private async Task HasPassedFirstEligiblePeriodAsync(CommunicationTest test)
+ {
+ var existingRuns = await _communicationTestRunRepository.GetRunsByTestIdAsync(test.CommunicationTestId);
+ if (existingRuns != null && existingRuns.Any())
+ return true; // Already ran before, normal schedule applies
+
+ // First run ever — must be at least one full period after creation
+ if (test.ScheduleType == (int)CommunicationTestScheduleType.Weekly)
+ {
+ // Must be at least 7 days after creation
+ return test.CreatedOn.AddDays(7) <= DateTime.UtcNow;
+ }
+ else if (test.ScheduleType == (int)CommunicationTestScheduleType.Monthly)
+ {
+ // Must be at least 28 days after creation (minimum month gap)
+ return test.CreatedOn.AddDays(28) <= DateTime.UtcNow;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/Core/Resgrid.Services/EmailService.cs b/Core/Resgrid.Services/EmailService.cs
index d3cdff73..3271c076 100644
--- a/Core/Resgrid.Services/EmailService.cs
+++ b/Core/Resgrid.Services/EmailService.cs
@@ -248,6 +248,83 @@ await _emailProvider.SendCallMail(emailAddress, subject, call.Name, priority, ca
return true;
}
+ public async Task SendCancelCallAsync(Call call, CallDispatch dispatch, UserProfile profile = null)
+ {
+ if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(call.DepartmentId))
+ return false;
+
+ if (profile == null)
+ profile = await _userProfileService.GetProfileByUserIdAsync(dispatch.UserId);
+
+ string emailAddress = String.Empty;
+
+ if (dispatch.User != null && dispatch.User != null)
+ emailAddress = dispatch.User.Email;
+ else
+ {
+ var user = _usersService.GetUserById(dispatch.UserId, false);
+
+ if (user != null && user != null)
+ emailAddress = user.Email;
+ }
+
+ string subject = string.Format("Resgrid CANCELLED Dispatch: P{0} {1}", call.Priority, call.Name);
+ string priority = string.Format("{0}", ((CallPriority)call.Priority).ToString());
+ string address = "No Address Supplied";
+
+ string coordinates = "No Coordinates Supplied";
+ if (!string.IsNullOrEmpty(call.GeoLocationData) && call.GeoLocationData.Length > 1)
+ coordinates = call.GeoLocationData;
+
+ if (!string.IsNullOrEmpty(call.Address))
+ address = call.Address;
+ else if (!string.IsNullOrEmpty(call.GeoLocationData) && call.GeoLocationData.Length > 1)
+ {
+ string[] points = call.GeoLocationData.Split(char.Parse(","));
+
+ if (points != null && points.Length == 2)
+ {
+ try
+ {
+ address = await _geoLocationProvider.GetAproxAddressFromLatLong(double.Parse(points[0]), double.Parse(points[1]));
+ }
+ catch (Exception)
+ {
+ }
+ }
+ }
+
+ string dispatchedOn = String.Empty;
+
+ if (call.Department != null)
+ dispatchedOn = call.LoggedOn.TimeConverterToString(call.Department);
+ else
+ dispatchedOn = call.LoggedOn.ToString("G") + " UTC";
+
+ string natureOfCall = "DISPATCH CANCELLED - " + call.NatureOfCall;
+
+ if (call.Protocols != null && call.Protocols.Any())
+ {
+ string protocols = String.Empty;
+ foreach (var protocol in call.Protocols)
+ {
+ if (String.IsNullOrWhiteSpace(protocols))
+ protocols = protocol.Data;
+ else
+ protocols = protocol + "," + protocol.Data;
+ }
+
+ if (!String.IsNullOrWhiteSpace(protocols))
+ natureOfCall = natureOfCall + " (" + protocols + ")";
+ }
+
+ if (profile != null && profile.SendEmail && !String.IsNullOrWhiteSpace(emailAddress))
+ await _emailProvider.SendCallMail(emailAddress, subject, call.Name, priority, natureOfCall, call.MapPage,
+ address, dispatchedOn, call.CallId, dispatch.UserId, coordinates, call.ShortenedAudioUrl);
+
+ return true;
+ }
+
public async Task SendTroubleAlert(TroubleAlertEvent troubleAlertEvent, Unit unit, Call call, string callAddress, string unitAddress, string personnelNames, UserProfile profile)
{
if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(unit.DepartmentId))
diff --git a/Core/Resgrid.Services/ServicesModule.cs b/Core/Resgrid.Services/ServicesModule.cs
index daad563b..4194a932 100644
--- a/Core/Resgrid.Services/ServicesModule.cs
+++ b/Core/Resgrid.Services/ServicesModule.cs
@@ -100,6 +100,9 @@ protected override void Load(ContainerBuilder builder)
// GDPR Services
builder.RegisterType().As().InstancePerLifetimeScope();
+
+ // Communication Test Services
+ builder.RegisterType().As().InstancePerLifetimeScope();
}
}
}
diff --git a/Core/Resgrid.Services/SmsService.cs b/Core/Resgrid.Services/SmsService.cs
index 4c93170b..e61418b4 100644
--- a/Core/Resgrid.Services/SmsService.cs
+++ b/Core/Resgrid.Services/SmsService.cs
@@ -209,6 +209,102 @@ public async Task SendCallAsync(Call call, CallDispatch dispatch, string d
return true;
}
+ public async Task SendCancelCallAsync(Call call, CallDispatch dispatch, string departmentNumber, int departmentId, UserProfile profile = null, string address = null, Payment payment = null)
+ {
+ if (Config.SystemBehaviorConfig.DoNotBroadcast && !Config.SystemBehaviorConfig.BypassDoNotBroadcastDepartments.Contains(departmentId))
+ return false;
+
+ if (profile == null)
+ profile = await _userProfileService.GetProfileByUserIdAsync(dispatch.UserId);
+
+ if (payment != null && !_subscriptionsService.CanPlanSendCallSms(payment.PlanId))
+ return true;
+
+ if (profile != null && profile.SendSms)
+ {
+ if (String.IsNullOrWhiteSpace(address))
+ {
+ if (!String.IsNullOrWhiteSpace(call.Address))
+ {
+ address = call.Address;
+ }
+ else if (!string.IsNullOrEmpty(call.GeoLocationData))
+ {
+ try
+ {
+ string[] points = call.GeoLocationData.Split(char.Parse(","));
+
+ if (points != null && points.Length == 2)
+ {
+ address = await _geoLocationProvider.GetAproxAddressFromLatLong(double.Parse(points[0]), double.Parse(points[1]));
+ }
+ }
+ catch
+ {
+ }
+ }
+ }
+
+ if (Carriers.DirectSendCarriers.Contains((MobileCarriers)profile.MobileCarrier))
+ {
+ string text = "CANCELLED: " + HtmlToTextHelper.ConvertHtml(call.NatureOfCall);
+ text = StringHelpers.StripHtmlTagsCharArray(text);
+ text = text + " " + address;
+
+ if (call.Protocols != null && call.Protocols.Any())
+ {
+ string protocols = String.Empty;
+ foreach (var protocol in call.Protocols)
+ {
+ if (!String.IsNullOrWhiteSpace(protocol.Data))
+ {
+ if (String.IsNullOrWhiteSpace(protocols))
+ protocols = protocol.Data;
+ else
+ protocols = protocol + "," + protocol.Data;
+ }
+ }
+
+ if (!String.IsNullOrWhiteSpace(protocols))
+ text = text + " (" + protocols + ")";
+ }
+
+ await _textMessageProvider.SendTextMessage(profile.GetPhoneNumber(), FormatTextForMessage(call.Name, text), departmentNumber, (MobileCarriers)profile.MobileCarrier, departmentId, false, true);
+ }
+ else
+ {
+ await SendCancelCallViaEmailSmsGatewayAsync(call, address, profile);
+ }
+ }
+
+ return true;
+ }
+
+ private async Task SendCancelCallViaEmailSmsGatewayAsync(Call call, string address, UserProfile profile)
+ {
+ MailMessage email = new MailMessage();
+ email.To.Add(string.Format(Carriers.CarriersMap[(MobileCarriers)profile.MobileCarrier], profile.GetPhoneNumber()));
+
+ email.From = new MailAddress(Config.OutboundEmailServerConfig.FromMail, "RGCall");
+ email.Subject = "CANCELLED: " + call.Name;
+
+ if (!string.IsNullOrEmpty(call.NatureOfCall))
+ {
+ string text = "CANCELLED: " + HtmlToTextHelper.ConvertHtml(call.NatureOfCall);
+ text = StringHelpers.StripHtmlTagsCharArray(text);
+ email.Body = text + " " + address;
+ }
+
+ try
+ {
+ await _emailSender.SendEmail(email);
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ }
+ }
+
private void SendCallViaEmailSmsGateway(Call call, string address, UserProfile profile)
{
MailMessage email = new MailMessage();
diff --git a/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs b/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs
index 0415b44f..9af06638 100644
--- a/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs
+++ b/Providers/Resgrid.Providers.Claims/ClaimsLogic.cs
@@ -1627,5 +1627,19 @@ public static void AddRouteClaims(ClaimsIdentity identity, bool isAdmin, List
+ /// Communication test management is restricted to department admins only.
+ ///
+ public static void AddCommunicationTestClaims(ClaimsIdentity identity, bool isAdmin)
+ {
+ if (isAdmin)
+ {
+ identity.AddClaim(new Claim(ResgridClaimTypes.Resources.CommunicationTest, ResgridClaimTypes.Actions.View));
+ identity.AddClaim(new Claim(ResgridClaimTypes.Resources.CommunicationTest, ResgridClaimTypes.Actions.Update));
+ identity.AddClaim(new Claim(ResgridClaimTypes.Resources.CommunicationTest, ResgridClaimTypes.Actions.Create));
+ identity.AddClaim(new Claim(ResgridClaimTypes.Resources.CommunicationTest, ResgridClaimTypes.Actions.Delete));
+ }
+ }
}
}
diff --git a/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs b/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs
index 77d92928..c44dc777 100644
--- a/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs
+++ b/Providers/Resgrid.Providers.Claims/ClaimsPrincipalFactory.cs
@@ -204,6 +204,7 @@ public override async Task CreateAsync(TUser user)
ClaimsLogic.AddWorkflowRunClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
ClaimsLogic.AddUdfClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
ClaimsLogic.AddRouteClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
+ ClaimsLogic.AddCommunicationTestClaims(id, departmentAdmin);
}
}
diff --git a/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs b/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs
index ab14bc4f..814a289f 100644
--- a/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs
+++ b/Providers/Resgrid.Providers.Claims/JwtTokenProvider.cs
@@ -127,6 +127,7 @@ public async Task BuildTokenAsync(string userId, int departmentId)
ClaimsLogic.AddWorkflowRunClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
ClaimsLogic.AddUdfClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
ClaimsLogic.AddRouteClaims(id, departmentAdmin, permissions, isGroupAdmin, roles);
+ ClaimsLogic.AddCommunicationTestClaims(id, departmentAdmin);
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtConfig.Key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
diff --git a/Providers/Resgrid.Providers.Claims/ResgridClaimTypes.cs b/Providers/Resgrid.Providers.Claims/ResgridClaimTypes.cs
index f4b66d8b..5fc38b32 100644
--- a/Providers/Resgrid.Providers.Claims/ResgridClaimTypes.cs
+++ b/Providers/Resgrid.Providers.Claims/ResgridClaimTypes.cs
@@ -64,6 +64,7 @@ public static class Resources
public const string Scim = "Scim";
public const string Udf = "Udf";
public const string Route = "Route";
+ public const string CommunicationTest = "CommunicationTest";
}
public static string CreateDepartmentClaimTypeString(int departmentId)
diff --git a/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs b/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs
index f36fb6ee..8c3405ab 100644
--- a/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs
+++ b/Providers/Resgrid.Providers.Claims/ResgridIdentity.cs
@@ -1082,5 +1082,10 @@ public void AddRouteClaims(bool isAdmin, List permissions, bool isGr
{
ClaimsLogic.AddRouteClaims(this, isAdmin, permissions, isGroupAdmin, roles);
}
+
+ public void AddCommunicationTestClaims(bool isAdmin)
+ {
+ ClaimsLogic.AddCommunicationTestClaims(this, isAdmin);
+ }
}
}
diff --git a/Providers/Resgrid.Providers.Claims/ResgridResources.cs b/Providers/Resgrid.Providers.Claims/ResgridResources.cs
index bef5362a..854fbdad 100644
--- a/Providers/Resgrid.Providers.Claims/ResgridResources.cs
+++ b/Providers/Resgrid.Providers.Claims/ResgridResources.cs
@@ -167,5 +167,10 @@ public static class ResgridResources
public const string Route_Update = "Route_Update";
public const string Route_Create = "Route_Create";
public const string Route_Delete = "Route_Delete";
+
+ public const string CommunicationTest_View = "CommunicationTest_View";
+ public const string CommunicationTest_Update = "CommunicationTest_Update";
+ public const string CommunicationTest_Create = "CommunicationTest_Create";
+ public const string CommunicationTest_Delete = "CommunicationTest_Delete";
}
}
diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0061_AddingCallVideoFeeds.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0061_AddingCallVideoFeeds.cs
new file mode 100644
index 00000000..eb71e9eb
--- /dev/null
+++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0061_AddingCallVideoFeeds.cs
@@ -0,0 +1,58 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.Migrations.Migrations
+{
+ [Migration(61)]
+ public class M0061_AddingCallVideoFeeds : Migration
+ {
+ public override void Up()
+ {
+ Create.Table("CallVideoFeeds")
+ .WithColumn("CallVideoFeedId").AsString(128).NotNullable().PrimaryKey()
+ .WithColumn("CallId").AsInt32().NotNullable()
+ .WithColumn("DepartmentId").AsInt32().NotNullable()
+ .WithColumn("Name").AsString(500).NotNullable()
+ .WithColumn("Url").AsString(2000).NotNullable()
+ .WithColumn("FeedType").AsInt32().Nullable()
+ .WithColumn("FeedFormat").AsInt32().Nullable()
+ .WithColumn("Description").AsString(4000).Nullable()
+ .WithColumn("Status").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("Latitude").AsDecimal(10, 7).Nullable()
+ .WithColumn("Longitude").AsDecimal(10, 7).Nullable()
+ .WithColumn("AddedByUserId").AsString(128).NotNullable()
+ .WithColumn("AddedOn").AsDateTime2().NotNullable()
+ .WithColumn("UpdatedOn").AsDateTime2().Nullable()
+ .WithColumn("SortOrder").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("IsDeleted").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("DeletedByUserId").AsString(128).Nullable()
+ .WithColumn("DeletedOn").AsDateTime2().Nullable()
+ .WithColumn("IsFlagged").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("FlaggedReason").AsString(4000).Nullable()
+ .WithColumn("FlaggedByUserId").AsString(128).Nullable()
+ .WithColumn("FlaggedOn").AsDateTime2().Nullable();
+
+ Create.ForeignKey("FK_CallVideoFeeds_Calls")
+ .FromTable("CallVideoFeeds").ForeignColumn("CallId")
+ .ToTable("Calls").PrimaryColumn("CallId");
+
+ Create.ForeignKey("FK_CallVideoFeeds_Departments")
+ .FromTable("CallVideoFeeds").ForeignColumn("DepartmentId")
+ .ToTable("Departments").PrimaryColumn("DepartmentId");
+
+ Create.Index("IX_CallVideoFeeds_CallId")
+ .OnTable("CallVideoFeeds")
+ .OnColumn("CallId");
+
+ Create.Index("IX_CallVideoFeeds_DepartmentId")
+ .OnTable("CallVideoFeeds")
+ .OnColumn("DepartmentId");
+ }
+
+ public override void Down()
+ {
+ Delete.ForeignKey("FK_CallVideoFeeds_Calls").OnTable("CallVideoFeeds");
+ Delete.ForeignKey("FK_CallVideoFeeds_Departments").OnTable("CallVideoFeeds");
+ Delete.Table("CallVideoFeeds");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0062_AddingCommunicationTests.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0062_AddingCommunicationTests.cs
new file mode 100644
index 00000000..8f35d875
--- /dev/null
+++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0062_AddingCommunicationTests.cs
@@ -0,0 +1,113 @@
+using FluentMigrator;
+using System;
+
+namespace Resgrid.Providers.Migrations.Migrations
+{
+ [Migration(62)]
+ public class M0062_AddingCommunicationTests : Migration
+ {
+ public override void Up()
+ {
+ Create.Table("CommunicationTests")
+ .WithColumn("CommunicationTestId").AsGuid().NotNullable().PrimaryKey().WithDefault(SystemMethods.NewGuid)
+ .WithColumn("DepartmentId").AsInt32().NotNullable()
+ .WithColumn("Name").AsString(500).NotNullable()
+ .WithColumn("Description").AsString(4000).Nullable()
+ .WithColumn("ScheduleType").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("Sunday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("Monday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("Tuesday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("Wednesday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("Thursday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("Friday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("Saturday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("DayOfMonth").AsInt32().Nullable()
+ .WithColumn("Time").AsString(50).Nullable()
+ .WithColumn("TestSms").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("TestEmail").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("TestVoice").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("TestPush").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("Active").AsBoolean().NotNullable().WithDefaultValue(true)
+ .WithColumn("CreatedByUserId").AsString(128).NotNullable()
+ .WithColumn("CreatedOn").AsDateTime2().NotNullable()
+ .WithColumn("UpdatedOn").AsDateTime2().Nullable()
+ .WithColumn("ResponseWindowMinutes").AsInt32().NotNullable().WithDefaultValue(60);
+
+ Create.ForeignKey("FK_CommunicationTests_Departments")
+ .FromTable("CommunicationTests").ForeignColumn("DepartmentId")
+ .ToTable("Departments").PrimaryColumn("DepartmentId");
+
+ Create.Index("IX_CommunicationTests_DepartmentId")
+ .OnTable("CommunicationTests")
+ .OnColumn("DepartmentId").Ascending();
+
+ Create.Table("CommunicationTestRuns")
+ .WithColumn("CommunicationTestRunId").AsGuid().NotNullable().PrimaryKey().WithDefault(SystemMethods.NewGuid)
+ .WithColumn("CommunicationTestId").AsGuid().NotNullable()
+ .WithColumn("DepartmentId").AsInt32().NotNullable()
+ .WithColumn("InitiatedByUserId").AsString(128).Nullable()
+ .WithColumn("StartedOn").AsDateTime2().NotNullable()
+ .WithColumn("CompletedOn").AsDateTime2().Nullable()
+ .WithColumn("Status").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("RunCode").AsString(20).NotNullable()
+ .WithColumn("TotalUsersTested").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("TotalResponses").AsInt32().NotNullable().WithDefaultValue(0);
+
+ Create.ForeignKey("FK_CommunicationTestRuns_CommunicationTests")
+ .FromTable("CommunicationTestRuns").ForeignColumn("CommunicationTestId")
+ .ToTable("CommunicationTests").PrimaryColumn("CommunicationTestId");
+
+ Create.Index("IX_CommunicationTestRuns_CommunicationTestId")
+ .OnTable("CommunicationTestRuns")
+ .OnColumn("CommunicationTestId").Ascending();
+
+ Create.Index("IX_CommunicationTestRuns_DepartmentId")
+ .OnTable("CommunicationTestRuns")
+ .OnColumn("DepartmentId").Ascending();
+
+ Create.Index("IX_CommunicationTestRuns_RunCode")
+ .OnTable("CommunicationTestRuns")
+ .OnColumn("RunCode").Ascending()
+ .WithOptions().Unique();
+
+ Create.Table("CommunicationTestResults")
+ .WithColumn("CommunicationTestResultId").AsGuid().NotNullable().PrimaryKey().WithDefault(SystemMethods.NewGuid)
+ .WithColumn("CommunicationTestRunId").AsGuid().NotNullable()
+ .WithColumn("DepartmentId").AsInt32().NotNullable()
+ .WithColumn("UserId").AsString(128).NotNullable()
+ .WithColumn("Channel").AsInt32().NotNullable()
+ .WithColumn("ContactValue").AsString(500).Nullable()
+ .WithColumn("ContactCarrier").AsString(200).Nullable()
+ .WithColumn("VerificationStatus").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("SendAttempted").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("SendSucceeded").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("SentOn").AsDateTime2().Nullable()
+ .WithColumn("Responded").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("RespondedOn").AsDateTime2().Nullable()
+ .WithColumn("ResponseToken").AsString(128).Nullable();
+
+ Create.ForeignKey("FK_CommunicationTestResults_CommunicationTestRuns")
+ .FromTable("CommunicationTestResults").ForeignColumn("CommunicationTestRunId")
+ .ToTable("CommunicationTestRuns").PrimaryColumn("CommunicationTestRunId");
+
+ Create.Index("IX_CommunicationTestResults_CommunicationTestRunId")
+ .OnTable("CommunicationTestResults")
+ .OnColumn("CommunicationTestRunId").Ascending();
+
+ Create.Index("IX_CommunicationTestResults_DepartmentId")
+ .OnTable("CommunicationTestResults")
+ .OnColumn("DepartmentId").Ascending();
+
+ Create.Index("IX_CommunicationTestResults_ResponseToken")
+ .OnTable("CommunicationTestResults")
+ .OnColumn("ResponseToken").Ascending();
+ }
+
+ public override void Down()
+ {
+ Delete.Table("CommunicationTestResults");
+ Delete.Table("CommunicationTestRuns");
+ Delete.Table("CommunicationTests");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0061_AddingCallVideoFeedsPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0061_AddingCallVideoFeedsPg.cs
new file mode 100644
index 00000000..4ac08fa2
--- /dev/null
+++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0061_AddingCallVideoFeedsPg.cs
@@ -0,0 +1,58 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.MigrationsPg.Migrations
+{
+ [Migration(61)]
+ public class M0061_AddingCallVideoFeedsPg : Migration
+ {
+ public override void Up()
+ {
+ Create.Table("callvideofeeds")
+ .WithColumn("callvideofeedid").AsCustom("citext").NotNullable().PrimaryKey()
+ .WithColumn("callid").AsInt32().NotNullable()
+ .WithColumn("departmentid").AsInt32().NotNullable()
+ .WithColumn("name").AsCustom("citext").NotNullable()
+ .WithColumn("url").AsCustom("text").NotNullable()
+ .WithColumn("feedtype").AsInt32().Nullable()
+ .WithColumn("feedformat").AsInt32().Nullable()
+ .WithColumn("description").AsCustom("text").Nullable()
+ .WithColumn("status").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("latitude").AsDecimal(10, 7).Nullable()
+ .WithColumn("longitude").AsDecimal(10, 7).Nullable()
+ .WithColumn("addedbyuserid").AsCustom("citext").NotNullable()
+ .WithColumn("addedon").AsDateTime().NotNullable()
+ .WithColumn("updatedon").AsDateTime().Nullable()
+ .WithColumn("sortorder").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("isdeleted").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("deletedbyuserid").AsCustom("citext").Nullable()
+ .WithColumn("deletedon").AsDateTime().Nullable()
+ .WithColumn("isflagged").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("flaggedreason").AsCustom("text").Nullable()
+ .WithColumn("flaggedbyuserid").AsCustom("citext").Nullable()
+ .WithColumn("flaggedon").AsDateTime().Nullable();
+
+ Create.ForeignKey("fk_callvideofeeds_calls")
+ .FromTable("callvideofeeds").ForeignColumn("callid")
+ .ToTable("calls").PrimaryColumn("callid");
+
+ Create.ForeignKey("fk_callvideofeeds_departments")
+ .FromTable("callvideofeeds").ForeignColumn("departmentid")
+ .ToTable("departments").PrimaryColumn("departmentid");
+
+ Create.Index("ix_callvideofeeds_callid")
+ .OnTable("callvideofeeds")
+ .OnColumn("callid");
+
+ Create.Index("ix_callvideofeeds_departmentid")
+ .OnTable("callvideofeeds")
+ .OnColumn("departmentid");
+ }
+
+ public override void Down()
+ {
+ Delete.ForeignKey("fk_callvideofeeds_calls").OnTable("callvideofeeds");
+ Delete.ForeignKey("fk_callvideofeeds_departments").OnTable("callvideofeeds");
+ Delete.Table("callvideofeeds");
+ }
+ }
+}
diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0062_AddingCommunicationTestsPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0062_AddingCommunicationTestsPg.cs
new file mode 100644
index 00000000..816114a7
--- /dev/null
+++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0062_AddingCommunicationTestsPg.cs
@@ -0,0 +1,112 @@
+using FluentMigrator;
+
+namespace Resgrid.Providers.MigrationsPg.Migrations
+{
+ [Migration(62)]
+ public class M0062_AddingCommunicationTestsPg : Migration
+ {
+ public override void Up()
+ {
+ Create.Table("communicationtests")
+ .WithColumn("communicationtestid").AsCustom("citext").NotNullable().PrimaryKey()
+ .WithColumn("departmentid").AsInt32().NotNullable()
+ .WithColumn("name").AsCustom("citext").NotNullable()
+ .WithColumn("description").AsCustom("text").Nullable()
+ .WithColumn("scheduletype").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("sunday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("monday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("tuesday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("wednesday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("thursday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("friday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("saturday").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("dayofmonth").AsInt32().Nullable()
+ .WithColumn("time").AsCustom("citext").Nullable()
+ .WithColumn("testsms").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("testemail").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("testvoice").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("testpush").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("active").AsBoolean().NotNullable().WithDefaultValue(true)
+ .WithColumn("createdbyuserid").AsCustom("citext").NotNullable()
+ .WithColumn("createdon").AsDateTime().NotNullable()
+ .WithColumn("updatedon").AsDateTime().Nullable()
+ .WithColumn("responsewindowminutes").AsInt32().NotNullable().WithDefaultValue(60);
+
+ Create.ForeignKey("fk_communicationtests_departments")
+ .FromTable("communicationtests").ForeignColumn("departmentid")
+ .ToTable("departments").PrimaryColumn("departmentid");
+
+ Create.Index("ix_communicationtests_departmentid")
+ .OnTable("communicationtests")
+ .OnColumn("departmentid");
+
+ Create.Table("communicationtestruns")
+ .WithColumn("communicationtestrunid").AsCustom("citext").NotNullable().PrimaryKey()
+ .WithColumn("communicationtestid").AsCustom("citext").NotNullable()
+ .WithColumn("departmentid").AsInt32().NotNullable()
+ .WithColumn("initiatedbyuserid").AsCustom("citext").Nullable()
+ .WithColumn("startedon").AsDateTime().NotNullable()
+ .WithColumn("completedon").AsDateTime().Nullable()
+ .WithColumn("status").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("runcode").AsCustom("citext").NotNullable()
+ .WithColumn("totaluserstested").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("totalresponses").AsInt32().NotNullable().WithDefaultValue(0);
+
+ Create.ForeignKey("fk_communicationtestruns_communicationtests")
+ .FromTable("communicationtestruns").ForeignColumn("communicationtestid")
+ .ToTable("communicationtests").PrimaryColumn("communicationtestid");
+
+ Create.Index("ix_communicationtestruns_communicationtestid")
+ .OnTable("communicationtestruns")
+ .OnColumn("communicationtestid");
+
+ Create.Index("ix_communicationtestruns_departmentid")
+ .OnTable("communicationtestruns")
+ .OnColumn("departmentid");
+
+ Create.Index("ix_communicationtestruns_runcode")
+ .OnTable("communicationtestruns")
+ .OnColumn("runcode")
+ .Unique();
+
+ Create.Table("communicationtestresults")
+ .WithColumn("communicationtestresultid").AsCustom("citext").NotNullable().PrimaryKey()
+ .WithColumn("communicationtestrunid").AsCustom("citext").NotNullable()
+ .WithColumn("departmentid").AsInt32().NotNullable()
+ .WithColumn("userid").AsCustom("citext").NotNullable()
+ .WithColumn("channel").AsInt32().NotNullable()
+ .WithColumn("contactvalue").AsCustom("citext").Nullable()
+ .WithColumn("contactcarrier").AsCustom("citext").Nullable()
+ .WithColumn("verificationstatus").AsInt32().NotNullable().WithDefaultValue(0)
+ .WithColumn("sendattempted").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("sendsucceeded").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("senton").AsDateTime().Nullable()
+ .WithColumn("responded").AsBoolean().NotNullable().WithDefaultValue(false)
+ .WithColumn("respondedon").AsDateTime().Nullable()
+ .WithColumn("responsetoken").AsCustom("citext").Nullable();
+
+ Create.ForeignKey("fk_communicationtestresults_communicationtestruns")
+ .FromTable("communicationtestresults").ForeignColumn("communicationtestrunid")
+ .ToTable("communicationtestruns").PrimaryColumn("communicationtestrunid");
+
+ Create.Index("ix_communicationtestresults_communicationtestrunid")
+ .OnTable("communicationtestresults")
+ .OnColumn("communicationtestrunid");
+
+ Create.Index("ix_communicationtestresults_departmentid")
+ .OnTable("communicationtestresults")
+ .OnColumn("departmentid");
+
+ Create.Index("ix_communicationtestresults_responsetoken")
+ .OnTable("communicationtestresults")
+ .OnColumn("responsetoken");
+ }
+
+ public override void Down()
+ {
+ Delete.Table("communicationtestresults");
+ Delete.Table("communicationtestruns");
+ Delete.Table("communicationtests");
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/CallVideoFeedRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CallVideoFeedRepository.cs
new file mode 100644
index 00000000..26b5032f
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/CallVideoFeedRepository.cs
@@ -0,0 +1,108 @@
+using Dapper;
+using Resgrid.Framework;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Repositories.Connection;
+using Resgrid.Model.Repositories.Queries;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Queries.CallVideoFeeds;
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+
+namespace Resgrid.Repositories.DataRepository
+{
+ public class CallVideoFeedRepository : RepositoryBase, ICallVideoFeedRepository
+ {
+ private readonly IConnectionProvider _connectionProvider;
+ private readonly SqlConfiguration _sqlConfiguration;
+ private readonly IQueryFactory _queryFactory;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CallVideoFeedRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory)
+ : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory)
+ {
+ _connectionProvider = connectionProvider;
+ _sqlConfiguration = sqlConfiguration;
+ _queryFactory = queryFactory;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task> GetByCallIdAsync(int callId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("CallId", callId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task> GetByDepartmentIdAsync(int departmentId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("DepartmentId", departmentId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/CommunicationTestRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CommunicationTestRepository.cs
new file mode 100644
index 00000000..a6facd48
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/CommunicationTestRepository.cs
@@ -0,0 +1,70 @@
+using Dapper;
+using Resgrid.Framework;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Repositories.Connection;
+using Resgrid.Model.Repositories.Queries;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Queries.CommunicationTests;
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+
+namespace Resgrid.Repositories.DataRepository
+{
+ public class CommunicationTestRepository : RepositoryBase, ICommunicationTestRepository
+ {
+ private readonly IConnectionProvider _connectionProvider;
+ private readonly SqlConfiguration _sqlConfiguration;
+ private readonly IQueryFactory _queryFactory;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CommunicationTestRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory)
+ : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory)
+ {
+ _connectionProvider = connectionProvider;
+ _sqlConfiguration = sqlConfiguration;
+ _queryFactory = queryFactory;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task> GetActiveTestsForScheduleTypeAsync(int scheduleType)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("ScheduleType", scheduleType);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/CommunicationTestResultRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CommunicationTestResultRepository.cs
new file mode 100644
index 00000000..deec22fc
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/CommunicationTestResultRepository.cs
@@ -0,0 +1,111 @@
+using Dapper;
+using Resgrid.Framework;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Repositories.Connection;
+using Resgrid.Model.Repositories.Queries;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Queries.CommunicationTests;
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Resgrid.Repositories.DataRepository
+{
+ public class CommunicationTestResultRepository : RepositoryBase, ICommunicationTestResultRepository
+ {
+ private readonly IConnectionProvider _connectionProvider;
+ private readonly SqlConfiguration _sqlConfiguration;
+ private readonly IQueryFactory _queryFactory;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CommunicationTestResultRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory)
+ : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory)
+ {
+ _connectionProvider = connectionProvider;
+ _sqlConfiguration = sqlConfiguration;
+ _queryFactory = queryFactory;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task> GetResultsByRunIdAsync(Guid communicationTestRunId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("CommunicationTestRunId", communicationTestRunId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task GetResultByResponseTokenAsync(string responseToken)
+ {
+ try
+ {
+ var selectFunction = new Func>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("ResponseToken", responseToken);
+
+ var query = _queryFactory.GetQuery();
+
+ var result = await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+
+ return result.FirstOrDefault();
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/CommunicationTestRunRepository.cs b/Repositories/Resgrid.Repositories.DataRepository/CommunicationTestRunRepository.cs
new file mode 100644
index 00000000..8dcc7057
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/CommunicationTestRunRepository.cs
@@ -0,0 +1,148 @@
+using Dapper;
+using Resgrid.Framework;
+using Resgrid.Model;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Repositories.Connection;
+using Resgrid.Model.Repositories.Queries;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Queries.CommunicationTests;
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Resgrid.Repositories.DataRepository
+{
+ public class CommunicationTestRunRepository : RepositoryBase, ICommunicationTestRunRepository
+ {
+ private readonly IConnectionProvider _connectionProvider;
+ private readonly SqlConfiguration _sqlConfiguration;
+ private readonly IQueryFactory _queryFactory;
+ private readonly IUnitOfWork _unitOfWork;
+
+ public CommunicationTestRunRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory)
+ : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory)
+ {
+ _connectionProvider = connectionProvider;
+ _sqlConfiguration = sqlConfiguration;
+ _queryFactory = queryFactory;
+ _unitOfWork = unitOfWork;
+ }
+
+ public async Task> GetRunsByTestIdAsync(Guid communicationTestId)
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("CommunicationTestId", communicationTestId);
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task GetRunByRunCodeAsync(string runCode)
+ {
+ try
+ {
+ var selectFunction = new Func>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+ dynamicParameters.Add("RunCode", runCode);
+
+ var query = _queryFactory.GetQuery();
+
+ var result = await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+
+ return result.FirstOrDefault();
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+
+ public async Task> GetOpenRunsAsync()
+ {
+ try
+ {
+ var selectFunction = new Func>>(async x =>
+ {
+ var dynamicParameters = new DynamicParametersExtension();
+
+ var query = _queryFactory.GetQuery();
+
+ return await x.QueryAsync(sql: query,
+ param: dynamicParameters,
+ transaction: _unitOfWork.Transaction);
+ });
+
+ DbConnection conn = null;
+ if (_unitOfWork?.Connection == null)
+ {
+ using (conn = _connectionProvider.Create())
+ {
+ await conn.OpenAsync();
+ return await selectFunction(conn);
+ }
+ }
+ else
+ {
+ conn = _unitOfWork.CreateOrGetConnection();
+ return await selectFunction(conn);
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.LogException(ex);
+ throw;
+ }
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs
index e086d2ee..35a301fc 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Configs/SqlConfiguration.cs
@@ -330,6 +330,9 @@ protected SqlConfiguration() { }
public string SelectFlaggedCallNotesByDepartmentIdQuery { get; set; }
public string SelectFlaggedCallImagesByDepartmentIdQuery { get; set; }
public string SelectFlaggedCallFilesByDepartmentIdQuery { get; set; }
+ public string CallVideoFeedsTable { get; set; }
+ public string SelectCallVideoFeedsByCallIdQuery { get; set; }
+ public string SelectCallVideoFeedsByDepartmentIdQuery { get; set; }
#endregion Calls
#region Dispatch Protocols
@@ -531,6 +534,18 @@ protected SqlConfiguration() { }
public string SelectCalendarItemCheckInsByUserDateRangeQuery { get; set; }
#endregion CalendarItemCheckIns
+ #region CommunicationTests
+ public string CommunicationTestsTable { get; set; }
+ public string CommunicationTestRunsTable { get; set; }
+ public string CommunicationTestResultsTable { get; set; }
+ public string SelectActiveCommTestsByScheduleTypeQuery { get; set; }
+ public string SelectCommTestRunsByTestIdQuery { get; set; }
+ public string SelectCommTestRunByRunCodeQuery { get; set; }
+ public string SelectOpenCommTestRunsQuery { get; set; }
+ public string SelectCommTestResultsByRunIdQuery { get; set; }
+ public string SelectCommTestResultByResponseTokenQuery { get; set; }
+ #endregion CommunicationTests
+
// Identity
#region Table Names
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs
index 220ee03f..c408e5bf 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/ApiDataModule.cs
@@ -133,6 +133,10 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs
index 48a8e786..6aa3addd 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs
@@ -132,6 +132,10 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs
index d92d974c..3937d42c 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/NonWebDataModule.cs
@@ -132,6 +132,10 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs
index 90b38dbb..e79a417e 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs
@@ -132,6 +132,10 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
+ builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
builder.RegisterType().As().InstancePerLifetimeScope();
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByCallIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByCallIdQuery.cs
new file mode 100644
index 00000000..85707dfb
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByCallIdQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CallVideoFeeds
+{
+ public class SelectCallVideoFeedsByCallIdQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCallVideoFeedsByCallIdQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCallVideoFeedsByCallIdQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CallVideoFeedsTable,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%CALLID%" },
+ new string[] { "CallId" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByDepartmentIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByDepartmentIdQuery.cs
new file mode 100644
index 00000000..159a9336
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CallVideoFeeds/SelectCallVideoFeedsByDepartmentIdQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CallVideoFeeds
+{
+ public class SelectCallVideoFeedsByDepartmentIdQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCallVideoFeedsByDepartmentIdQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCallVideoFeedsByDepartmentIdQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CallVideoFeedsTable,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%DEPARTMENTID%" },
+ new string[] { "DepartmentId" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectActiveCommTestsByScheduleTypeQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectActiveCommTestsByScheduleTypeQuery.cs
new file mode 100644
index 00000000..aad93c1f
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectActiveCommTestsByScheduleTypeQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CommunicationTests
+{
+ public class SelectActiveCommTestsByScheduleTypeQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectActiveCommTestsByScheduleTypeQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectActiveCommTestsByScheduleTypeQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CommunicationTestsTable,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%SCHEDULETYPE%" },
+ new string[] { "ScheduleType" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestResultByResponseTokenQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestResultByResponseTokenQuery.cs
new file mode 100644
index 00000000..d0414a4c
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestResultByResponseTokenQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CommunicationTests
+{
+ public class SelectCommTestResultByResponseTokenQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCommTestResultByResponseTokenQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCommTestResultByResponseTokenQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CommunicationTestResultsTable,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%TOKEN%" },
+ new string[] { "ResponseToken" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestResultsByRunIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestResultsByRunIdQuery.cs
new file mode 100644
index 00000000..319bdeaf
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestResultsByRunIdQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CommunicationTests
+{
+ public class SelectCommTestResultsByRunIdQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCommTestResultsByRunIdQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCommTestResultsByRunIdQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CommunicationTestResultsTable,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%RUNID%" },
+ new string[] { "CommunicationTestRunId" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestRunByRunCodeQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestRunByRunCodeQuery.cs
new file mode 100644
index 00000000..6e29f0ae
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestRunByRunCodeQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CommunicationTests
+{
+ public class SelectCommTestRunByRunCodeQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCommTestRunByRunCodeQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCommTestRunByRunCodeQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CommunicationTestRunsTable,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%RUNCODE%" },
+ new string[] { "RunCode" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestRunsByTestIdQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestRunsByTestIdQuery.cs
new file mode 100644
index 00000000..21c9274d
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectCommTestRunsByTestIdQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CommunicationTests
+{
+ public class SelectCommTestRunsByTestIdQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectCommTestRunsByTestIdQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectCommTestRunsByTestIdQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CommunicationTestRunsTable,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { "%COMMTESTID%" },
+ new string[] { "CommunicationTestId" });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectOpenCommTestRunsQuery.cs b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectOpenCommTestRunsQuery.cs
new file mode 100644
index 00000000..b26c64d1
--- /dev/null
+++ b/Repositories/Resgrid.Repositories.DataRepository/Queries/CommunicationTests/SelectOpenCommTestRunsQuery.cs
@@ -0,0 +1,33 @@
+using Resgrid.Model;
+using Resgrid.Model.Repositories.Queries.Contracts;
+using Resgrid.Repositories.DataRepository.Configs;
+using Resgrid.Repositories.DataRepository.Extensions;
+
+namespace Resgrid.Repositories.DataRepository.Queries.CommunicationTests
+{
+ public class SelectOpenCommTestRunsQuery : ISelectQuery
+ {
+ private readonly SqlConfiguration _sqlConfiguration;
+ public SelectOpenCommTestRunsQuery(SqlConfiguration sqlConfiguration)
+ {
+ _sqlConfiguration = sqlConfiguration;
+ }
+
+ public string GetQuery()
+ {
+ var query = _sqlConfiguration.SelectOpenCommTestRunsQuery
+ .ReplaceQueryParameters(_sqlConfiguration, _sqlConfiguration.SchemaName,
+ _sqlConfiguration.CommunicationTestRunsTable,
+ _sqlConfiguration.ParameterNotation,
+ new string[] { },
+ new string[] { });
+
+ return query;
+ }
+
+ public string GetQuery() where TEntity : class, IEntity
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs
index 924646e2..9982d199 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Servers/PostgreSql/PostgreSqlConfiguration.cs
@@ -1114,6 +1114,9 @@ UPDATE CallDispatches
SelectAllCallUnitDispsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CallId = %CALLID%";
SelectAllCallRoleDispsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CallId = %CALLID%";
SelectCallNotesByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CallId = %CALLID%";
+ CallVideoFeedsTable = "CallVideoFeeds";
+ SelectCallVideoFeedsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CallId = %CALLID% AND IsDeleted = false";
+ SelectCallVideoFeedsByDepartmentIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE DepartmentId = %DEPARTMENTID% AND IsDeleted = false";
SelectCallYearsByDeptQuery = @"
SELECT extract(year from c.LoggedOn)
FROM Calls c WHERE c.DepartmentId = %DID%
@@ -1641,6 +1644,18 @@ ORDER BY Timestamp DESC
ORDER BY CheckInTime DESC";
#endregion CalendarItemCheckIns
+ #region CommunicationTests
+ CommunicationTestsTable = "CommunicationTests";
+ CommunicationTestRunsTable = "CommunicationTestRuns";
+ CommunicationTestResultsTable = "CommunicationTestResults";
+ SelectActiveCommTestsByScheduleTypeQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE Active = true AND ScheduleType = %SCHEDULETYPE%";
+ SelectCommTestRunsByTestIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CommunicationTestId = %COMMTESTID% ORDER BY StartedOn DESC";
+ SelectCommTestRunByRunCodeQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE RunCode = %RUNCODE% LIMIT 1";
+ SelectOpenCommTestRunsQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE Status IN (0, 1, 2)";
+ SelectCommTestResultsByRunIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE CommunicationTestRunId = %RUNID%";
+ SelectCommTestResultByResponseTokenQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE ResponseToken = %TOKEN% LIMIT 1";
+ #endregion CommunicationTests
+
#region User Defined Fields
UdfDefinitionsTableName = "UdfDefinitions";
UdfFieldsTableName = "UdfFields";
diff --git a/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs b/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs
index 649c4878..10cbc2ee 100644
--- a/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs
+++ b/Repositories/Resgrid.Repositories.DataRepository/Servers/SqlServer/SqlServerConfiguration.cs
@@ -1078,6 +1078,9 @@ UPDATE CallDispatches
SelectAllCallUnitDispsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CallId] = %CALLID%";
SelectAllCallRoleDispsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CallId] = %CALLID%";
SelectCallNotesByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CallId] = %CALLID%";
+ CallVideoFeedsTable = "CallVideoFeeds";
+ SelectCallVideoFeedsByCallIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CallId] = %CALLID% AND [IsDeleted] = 0";
+ SelectCallVideoFeedsByDepartmentIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [DepartmentId] = %DEPARTMENTID% AND [IsDeleted] = 0";
SelectCallYearsByDeptQuery = @"
SELECT DISTINCT YEAR(c.LoggedOn)
FROM Calls c WHERE c.DepartmentId = %DID%
@@ -1602,6 +1605,18 @@ SELECT TOP 1 *
ORDER BY [CheckInTime] DESC";
#endregion CalendarItemCheckIns
+ #region CommunicationTests
+ CommunicationTestsTable = "CommunicationTests";
+ CommunicationTestRunsTable = "CommunicationTestRuns";
+ CommunicationTestResultsTable = "CommunicationTestResults";
+ SelectActiveCommTestsByScheduleTypeQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [Active] = 1 AND [ScheduleType] = %SCHEDULETYPE%";
+ SelectCommTestRunsByTestIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CommunicationTestId] = %COMMTESTID% ORDER BY [StartedOn] DESC";
+ SelectCommTestRunByRunCodeQuery = "SELECT TOP 1 * FROM %SCHEMA%.%TABLENAME% WHERE [RunCode] = %RUNCODE%";
+ SelectOpenCommTestRunsQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [Status] IN (0, 1, 2)";
+ SelectCommTestResultsByRunIdQuery = "SELECT * FROM %SCHEMA%.%TABLENAME% WHERE [CommunicationTestRunId] = %RUNID%";
+ SelectCommTestResultByResponseTokenQuery = "SELECT TOP 1 * FROM %SCHEMA%.%TABLENAME% WHERE [ResponseToken] = %TOKEN%";
+ #endregion CommunicationTests
+
#region User Defined Fields
UdfDefinitionsTableName = "UdfDefinitions";
UdfFieldsTableName = "UdfFields";
diff --git a/Tests/Resgrid.Tests/Services/CallVideoFeedTests.cs b/Tests/Resgrid.Tests/Services/CallVideoFeedTests.cs
new file mode 100644
index 00000000..2c96f226
--- /dev/null
+++ b/Tests/Resgrid.Tests/Services/CallVideoFeedTests.cs
@@ -0,0 +1,198 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Moq;
+using NUnit.Framework;
+using Resgrid.Model;
+using Resgrid.Model.Providers;
+using Resgrid.Model.Repositories;
+using Resgrid.Model.Services;
+using Resgrid.Services;
+
+namespace Resgrid.Tests.Services
+{
+ [TestFixture]
+ public class CallVideoFeedTests
+ {
+ private Mock _callsRepo;
+ private Mock _communicationService;
+ private Mock _callDispatchesRepo;
+ private Mock _callTypesRepo;
+ private Mock _callEmailFactory;
+ private Mock _cacheProvider;
+ private Mock _callNotesRepo;
+ private Mock _callAttachmentRepo;
+ private Mock _callDispatchGroupRepo;
+ private Mock _callDispatchUnitRepo;
+ private Mock _callDispatchRoleRepo;
+ private Mock _callPriorityRepo;
+ private Mock _shortenUrlProvider;
+ private Mock _callProtocolsRepo;
+ private Mock _geoLocationProvider;
+ private Mock _departmentsService;
+ private Mock _callReferencesRepo;
+ private Mock _callContactsRepo;
+ private Mock _indoorMapService;
+ private Mock _callVideoFeedRepo;
+ private CallsService _service;
+
+ [SetUp]
+ public void SetUp()
+ {
+ _callsRepo = new Mock();
+ _communicationService = new Mock();
+ _callDispatchesRepo = new Mock();
+ _callTypesRepo = new Mock();
+ _callEmailFactory = new Mock();
+ _cacheProvider = new Mock();
+ _callNotesRepo = new Mock