From 2887c2ecafbe4deb120a3e1fb13233cc15a86737 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Thu, 3 Sep 2026 19:38:07 -0700 Subject: [PATCH] RG-T55 RMS Pass 2 --- Core/Resgrid.Config/NerisConfig.cs | 39 + .../Areas/User/Dispatch/Call.ar.resx | 3 + .../Areas/User/Dispatch/Call.de.resx | 3 + .../Areas/User/Dispatch/Call.el.resx | 3 + .../Areas/User/Dispatch/Call.en.resx | 3 + .../Areas/User/Dispatch/Call.es.resx | 3 + .../Areas/User/Dispatch/Call.fr.resx | 3 + .../Areas/User/Dispatch/Call.it.resx | 3 + .../Areas/User/Dispatch/Call.pl.resx | 3 + .../Areas/User/Dispatch/Call.sv.resx | 3 + .../Areas/User/Dispatch/Call.uk.resx | 3 + .../Areas/User/Records/Records.ar.resx | 130 ++ .../Areas/User/Records/Records.de.resx | 130 ++ .../Areas/User/Records/Records.el.resx | 130 ++ .../Areas/User/Records/Records.en.resx | 130 ++ .../Areas/User/Records/Records.es.resx | 130 ++ .../Areas/User/Records/Records.fr.resx | 130 ++ .../Areas/User/Records/Records.it.resx | 130 ++ .../Areas/User/Records/Records.pl.resx | 130 ++ .../Areas/User/Records/Records.sv.resx | 130 ++ .../Areas/User/Records/Records.uk.resx | 130 ++ .../Providers/INerisProviders.cs | 79 + .../Records/IncidentReportContracts.cs | 136 ++ .../IncidentReportValidationException.cs | 21 + Core/Resgrid.Model/Records/NerisContracts.cs | 95 ++ .../Records/RecordsApiContracts.cs | 265 +++ .../Records/RmsIncidentReport.cs | 499 ++++++ Core/Resgrid.Model/Records/RmsLifecycle.cs | 3 + Core/Resgrid.Model/Records/RmsNerisProfile.cs | 113 ++ Core/Resgrid.Model/Records/RmsSubmission.cs | 121 ++ .../Repositories/IRmsIncidentRepositories.cs | 92 + .../Services/IIncidentReportsService.cs | 85 + .../Services/IRecordsCutoverService.cs | 4 +- .../Services/IRecordsNotificationService.cs | 3 + .../Resgrid.Model/Services/IRecordsService.cs | 4 + .../WorkflowTemplateVariableCatalog.cs | 31 + .../Resgrid.Model/WorkflowTriggerEventType.cs | 17 +- Core/Resgrid.Search/LuceneRecordsIndexHost.cs | 2 +- .../Records/IncidentReportsService.cs | 1475 +++++++++++++++++ .../Records/RecordSnapshotSerializer.cs | 2 +- .../Records/RecordsApiSupport.cs | 366 ++++ .../Records/RecordsAuthorizationService.cs | 38 +- .../Records/RecordsCutoverService.cs | 6 +- .../Records/RecordsNotificationService.cs | 57 +- .../Records/RecordsService.cs | 6 + .../Records/RecordsSubmissionService.cs | 361 ++++ Core/Resgrid.Services/ServicesModule.cs | 7 + .../WorkflowSampleDataGenerator.cs | 21 + .../WorkflowTemplateContextBuilder.cs | 10 +- .../M0164_AddRmsIncidentReportCore.cs | 304 ++++ .../M0165_AddRmsSubmissionsAndSignatures.cs | 94 ++ .../M0166_AddRmsNerisProfilesAndValueSets.cs | 92 + .../M0164_AddRmsIncidentReportCorePg.cs | 289 ++++ .../M0165_AddRmsSubmissionsAndSignaturesPg.cs | 85 + ...M0166_AddRmsNerisProfilesAndValueSetsPg.cs | 85 + .../neris-openapi-v1.4.78-2026-09-03.json | 1 + .../neris-value-sets-v1.4.78-2026-09-03.json | 1309 +++++++++++++++ .../Resgrid.Providers.Neris/NerisApiClient.cs | 311 ++++ .../NerisMappingService.cs | 200 +++ .../NerisProfileService.cs | 252 +++ .../NerisProviderModule.cs | 22 + .../NerisSubmissionService.cs | 49 + .../NerisValidationService.cs | 199 +++ .../NerisValueSetCatalog.cs | 67 + .../Resgrid.Providers.Neris.csproj | 27 + .../Modules/DataModule.cs | 15 + .../Modules/TestingDataModule.cs | 15 + .../RmsIncidentRepositories.cs | 371 +++++ Resgrid.sln | 39 + .../Allocations/trigger-baseline.json | 6 +- Tests/Resgrid.Tests/Bootstrapper.cs | 1 + .../Providers/NerisApiClientTests.cs | 195 +++ .../Providers/NerisMappingTests.cs | 238 +++ .../Providers/NerisValidationTests.cs | 120 ++ Tests/Resgrid.Tests/Resgrid.Tests.csproj | 1 + Tests/Resgrid.Tests/Rms/FakeIncidentStore.cs | 184 ++ .../Rms/IncidentReportsServiceTests.cs | 453 +++++ .../Rms/RecordAttachmentUploadServiceTests.cs | 251 +++ .../Rms/RecordsGroupScopePreviewTests.cs | 2 +- .../Rms/RecordsNotificationServiceTests.cs | 2 +- .../Rms/RecordsSubmissionServiceTests.cs | 333 ++++ .../Rms/RmsCascadeSafetyTests.cs | 6 +- .../Rms/RmsContainerCompositionTests.cs | 26 + .../Rms/RmsIdentifierPinTests.cs | 4 + .../Web/Services/RecordsApiControllerTests.cs | 392 +++++ .../v4/IncidentReportsController.cs | 472 ++++++ .../Controllers/v4/RecordsController.cs | 977 +++++++++++ .../Helpers/RecordsApiHelper.cs | 437 +++++ .../v4/Records/IncidentReportsApiModels.cs | 327 ++++ .../Models/v4/Records/RecordsApiModels.cs | 530 ++++++ .../Resgrid.Web.Services.csproj | 1 + .../Resgrid.Web.Services.xml | 247 +++ Web/Resgrid.Web.Services/Startup.cs | 1 + .../Controllers/IncidentReportsController.cs | 731 ++++++++ .../User/Controllers/RecordsController.cs | 2 +- .../Records/IncidentReportsViewModels.cs | 217 +++ .../Areas/User/Views/Dispatch/ViewCall.cshtml | 4 + .../User/Views/IncidentReports/Details.cshtml | 464 ++++++ .../User/Views/IncidentReports/Edit.cshtml | 341 ++++ .../User/Views/IncidentReports/Index.cshtml | 165 ++ .../Views/IncidentReports/Settings.cshtml | 160 ++ .../Areas/User/Views/Records/Index.cshtml | 6 +- Web/Resgrid.Web/Resgrid.Web.csproj | 1 + Web/Resgrid.Web/Startup.cs | 1 + Web/Resgrid.Web/WebBootstrapper.cs | 1 + .../security/resgrid.security.permissions.js | 4 + .../Commands/RmsSubmissionCommand.cs | 19 + Workers/Resgrid.Workers.Console/Program.cs | 8 + .../Tasks/RmsSubmissionTask.cs | 45 + .../Resgrid.Workers.Framework/Bootstrapper.cs | 1 + .../Logic/NotificationBroadcastLogic.cs | 8 + .../Logic/RmsSubmissionLogic.cs | 44 + .../Resgrid.Workers.Framework.csproj | 1 + 113 files changed, 16517 insertions(+), 26 deletions(-) create mode 100644 Core/Resgrid.Config/NerisConfig.cs create mode 100644 Core/Resgrid.Model/Providers/INerisProviders.cs create mode 100644 Core/Resgrid.Model/Records/IncidentReportContracts.cs create mode 100644 Core/Resgrid.Model/Records/IncidentReportValidationException.cs create mode 100644 Core/Resgrid.Model/Records/NerisContracts.cs create mode 100644 Core/Resgrid.Model/Records/RecordsApiContracts.cs create mode 100644 Core/Resgrid.Model/Records/RmsIncidentReport.cs create mode 100644 Core/Resgrid.Model/Records/RmsNerisProfile.cs create mode 100644 Core/Resgrid.Model/Records/RmsSubmission.cs create mode 100644 Core/Resgrid.Model/Repositories/IRmsIncidentRepositories.cs create mode 100644 Core/Resgrid.Model/Services/IIncidentReportsService.cs create mode 100644 Core/Resgrid.Services/Records/IncidentReportsService.cs create mode 100644 Core/Resgrid.Services/Records/RecordsApiSupport.cs create mode 100644 Core/Resgrid.Services/Records/RecordsSubmissionService.cs create mode 100644 Providers/Resgrid.Providers.Migrations/Migrations/M0164_AddRmsIncidentReportCore.cs create mode 100644 Providers/Resgrid.Providers.Migrations/Migrations/M0165_AddRmsSubmissionsAndSignatures.cs create mode 100644 Providers/Resgrid.Providers.Migrations/Migrations/M0166_AddRmsNerisProfilesAndValueSets.cs create mode 100644 Providers/Resgrid.Providers.MigrationsPg/Migrations/M0164_AddRmsIncidentReportCorePg.cs create mode 100644 Providers/Resgrid.Providers.MigrationsPg/Migrations/M0165_AddRmsSubmissionsAndSignaturesPg.cs create mode 100644 Providers/Resgrid.Providers.MigrationsPg/Migrations/M0166_AddRmsNerisProfilesAndValueSetsPg.cs create mode 100644 Providers/Resgrid.Providers.Neris/Contract/neris-openapi-v1.4.78-2026-09-03.json create mode 100644 Providers/Resgrid.Providers.Neris/Contract/neris-value-sets-v1.4.78-2026-09-03.json create mode 100644 Providers/Resgrid.Providers.Neris/NerisApiClient.cs create mode 100644 Providers/Resgrid.Providers.Neris/NerisMappingService.cs create mode 100644 Providers/Resgrid.Providers.Neris/NerisProfileService.cs create mode 100644 Providers/Resgrid.Providers.Neris/NerisProviderModule.cs create mode 100644 Providers/Resgrid.Providers.Neris/NerisSubmissionService.cs create mode 100644 Providers/Resgrid.Providers.Neris/NerisValidationService.cs create mode 100644 Providers/Resgrid.Providers.Neris/NerisValueSetCatalog.cs create mode 100644 Providers/Resgrid.Providers.Neris/Resgrid.Providers.Neris.csproj create mode 100644 Repositories/Resgrid.Repositories.DataRepository/RmsIncidentRepositories.cs create mode 100644 Tests/Resgrid.Tests/Providers/NerisApiClientTests.cs create mode 100644 Tests/Resgrid.Tests/Providers/NerisMappingTests.cs create mode 100644 Tests/Resgrid.Tests/Providers/NerisValidationTests.cs create mode 100644 Tests/Resgrid.Tests/Rms/FakeIncidentStore.cs create mode 100644 Tests/Resgrid.Tests/Rms/IncidentReportsServiceTests.cs create mode 100644 Tests/Resgrid.Tests/Rms/RecordAttachmentUploadServiceTests.cs create mode 100644 Tests/Resgrid.Tests/Rms/RecordsSubmissionServiceTests.cs create mode 100644 Tests/Resgrid.Tests/Web/Services/RecordsApiControllerTests.cs create mode 100644 Web/Resgrid.Web.Services/Controllers/v4/IncidentReportsController.cs create mode 100644 Web/Resgrid.Web.Services/Controllers/v4/RecordsController.cs create mode 100644 Web/Resgrid.Web.Services/Helpers/RecordsApiHelper.cs create mode 100644 Web/Resgrid.Web.Services/Models/v4/Records/IncidentReportsApiModels.cs create mode 100644 Web/Resgrid.Web.Services/Models/v4/Records/RecordsApiModels.cs create mode 100644 Web/Resgrid.Web/Areas/User/Controllers/IncidentReportsController.cs create mode 100644 Web/Resgrid.Web/Areas/User/Models/Records/IncidentReportsViewModels.cs create mode 100644 Web/Resgrid.Web/Areas/User/Views/IncidentReports/Details.cshtml create mode 100644 Web/Resgrid.Web/Areas/User/Views/IncidentReports/Edit.cshtml create mode 100644 Web/Resgrid.Web/Areas/User/Views/IncidentReports/Index.cshtml create mode 100644 Web/Resgrid.Web/Areas/User/Views/IncidentReports/Settings.cshtml create mode 100644 Workers/Resgrid.Workers.Console/Commands/RmsSubmissionCommand.cs create mode 100644 Workers/Resgrid.Workers.Console/Tasks/RmsSubmissionTask.cs create mode 100644 Workers/Resgrid.Workers.Framework/Logic/RmsSubmissionLogic.cs diff --git a/Core/Resgrid.Config/NerisConfig.cs b/Core/Resgrid.Config/NerisConfig.cs new file mode 100644 index 00000000..7338978f --- /dev/null +++ b/Core/Resgrid.Config/NerisConfig.cs @@ -0,0 +1,39 @@ +namespace Resgrid.Config +{ + /// + /// NERIS integration (RMS plan section 5.5, RMS-2). Submission is off unless a department has an enabled + /// RmsNerisProfile AND this switch is on; Records is useful without NERIS (plan decision 21). + /// Environment keys: RESGRID:NerisConfig:Enabled, :BaseUrl, :SandboxBaseUrl, :TimeoutSeconds, :MaxAttempts. + /// + public static class NerisConfig + { + /// Master switch for outbound NERIS calls in every process. + public static bool Enabled = false; + + /// Production API root of the pinned contract. + public static string BaseUrl = "https://api.neris.fsri.org/v1"; + + /// Sandbox root used by profiles whose Environment is sandbox; empty means "same as BaseUrl". + public static string SandboxBaseUrl = ""; + + /// Contract version the provider was generated against (Providers/Resgrid.Providers.Neris/Contract). + public static string ContractVersion = "1.4.78"; + + public static int TimeoutSeconds = 30; + + /// Delivery attempts before a submission is marked Failed (trigger 111). + public static int MaxAttempts = 5; + + /// Base backoff between attempts in minutes; doubled per attempt. + public static int RetryBackoffMinutes = 5; + + /// Submissions one worker sweep claims. + public static int BatchSize = 25; + + /// Lease held by a worker while it talks to the destination. + public static int LeaseSeconds = 300; + + /// How often a submission awaiting the destination's review is polled for status. + public static int StatusPollMinutes = 30; + } +} diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx index 014bbf03..955b3830 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.ar.resx @@ -278,4 +278,7 @@ تاريخ الإرسال المجدول + + تقرير الحادث + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx index 049c909c..6b790dd3 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.de.resx @@ -845,4 +845,7 @@ Geplantes Alarmierungsdatum + + Einsatzbericht + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.el.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.el.resx index bb9914bd..80a223f3 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.el.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.el.resx @@ -748,4 +748,7 @@ Προγραμματισμένη Ημερομηνία Αποστολής + + Αναφορά συμβάντος + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx index 7e75d5ea..d37f729e 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.en.resx @@ -748,4 +748,7 @@ Scheduled Dispatch Date + + Incident report + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx index fdd54328..cadc7671 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.es.resx @@ -894,4 +894,7 @@ Fecha de despacho programada + + Informe de incidente + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx index 7c8b65f2..fa72b9a9 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.fr.resx @@ -845,4 +845,7 @@ Date d'envoi planifiée + + Rapport d'incident + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx index df3f925d..218c0323 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.it.resx @@ -845,4 +845,7 @@ Data di invio programmata + + Rapporto di incidente + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx index 19aa4c7d..b9bcb404 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.pl.resx @@ -845,4 +845,7 @@ Zaplanowana data wysyłki + + Raport ze zdarzenia + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx index cd2d8b84..0da08d12 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.sv.resx @@ -845,4 +845,7 @@ Schemalagt larmdatum + + Insatsrapport + diff --git a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx index bae02ba7..82a680b6 100644 --- a/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx +++ b/Core/Resgrid.Localization/Areas/User/Dispatch/Call.uk.resx @@ -845,4 +845,7 @@ Запланована дата відправки + + Звіт про інцидент + diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.ar.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.ar.resx index 3f436a5c..37e4bb73 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.ar.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.ar.resx @@ -213,4 +213,134 @@ الإجماليات المالك المجموعة + تقارير الحوادث + تقارير حوادث NERIS + تقرير حادث واحد معتمد لكل نداء. تبدأ التقارير من النداء المُرسل، وتحمل مصدر كل قيمة معبأة مسبقًا، ويتم التحقق منها وفق عقد NERIS المثبت قبل التصديق عليها وإرسالها. + تقرير الحادث + تعديل تقرير الحادث + البدء من النداء + بدء تقرير الحادث + اختر نداءً + لا توجد نداءات نشطة؛ افتح نداءً مغلقًا من الإرسال لبدء تقريره. + بدأ تقرير الحادث من النداء. + هذا النداء له تقرير حادث بالفعل. + رقم الحادث + معرّف حادث NERIS + الإرسال + الإرسالات + حالة الإرسال + لم يتم إرسال هذه المراجعة. + المحاولات + حالة الوجهة + المشكلات + في قائمة الانتظار + أُرسل + اكتمل + الحمولة + إرسال إلى NERIS + تصحيح وإعادة الإرسال + تحقق + تحقق مع NERIS + مشكلات التحقق + لا توجد مشكلات تحقق مفتوحة. + اكتمل التحقق مع {0} مشكلة (مشكلات). + الخطورة + القاعدة + الرسالة + المصدر + أصلح الأخطاء أدناه قبل الإنهاء. + إرسال NERIS غير مفعّل لهذا القسم. تبقى التقارير المكتملة محلية حتى يقوم مسؤول بإعداد ملف NERIS. + إرسال NERIS متوقف على مستوى النظام؛ يمكن إنشاء التقارير وإكمالها لكن لا يمكن تسليمها. + لم يتم إعداد ملف NERIS. + رفض NERIS هذا التقرير. صحّح الحقول وأعد الإرسال؛ سيتم إنشاء مراجعة جديدة ومفتاح تكرار جديد. + هذا التقرير لدى NERIS. ينتظر التعديل حتى يرد الطرف المستقبل. + قبل NERIS هذا التقرير. + أوقات الإرسال + إنشاء النداء + الرد على النداء + أول وصول + انتهاء الحادث + مركز الإرسال + رمز المحدد + رمز حادث الإرسال + النتيجة + وجود أشخاص + عدد النازحين + الحيوانات المنقذة + المعدّلات الخاصة + أنواع الحادث + أساسي + نوع NERIS + العنوان + الرقم + الشارع + البلدية + المقاطعة + الولاية + الرمز البريدي + الدولة + نوع المكان + استخدام الموقع + الشارع المتقاطع + الإحداثيات + معرّف وحدة NERIS + الطاقم + في الانتظار + أُلغي أثناء الطريق + انتهى + وضع الاستجابة + المساعدة المتبادلة + الاتجاه + نوع المساعدة + الجهة + معرّف NERIS للجهة + جهة غير إطفائية + الإجراءات والتكتيكات + التكتيك + الوحدة + الوقت + العوائق + النتيجة + مصدر كل قيمة معبأة مسبقًا. تحتفظ التصحيحات بالقيمة الأصلية بجانب القيمة المصححة. + الحقيقة + القيمة المصدرية + القيمة الحالية + صُحّح بواسطة + الإرسال + التطبيق + مشتق + مصحّح + حفظ وتحقق + التصديقات + وقّعه + وُقّع + إرسال للمراجعة + إعادة للتصحيح + موعد المراجعة + إعدادات NERIS + هوية القسم في NERIS وبيانات الاعتماد والبيئة، بالإضافة إلى ربط أنواع النداءات المحلية بأنواع حوادث NERIS. تُخزَّن بيانات الاعتماد مشفرة لكل قسم ولا تُعرض مرة أخرى. + معرّف كيان NERIS + اسم الكيان + البيئة + تجاوز عنوان API الأساسي + نوع المنحة + اسم المستخدم + كلمة المرور + معرّف العميل + سر العميل + بيانات الاعتماد مخزنة. اترك الحقول فارغة للاحتفاظ بها. + لا توجد بيانات اعتماد مخزنة. + إرسال تلقائي عند إكمال التقرير + تفعيل إرسال NERIS لهذا القسم + إصدار العقد + آخر رمز صادر + آخر اتصال ناجح + آخر خطأ + ربط أنواع النداءات + اربط كل نوع نداء إرسال بنوع حادث NERIS الذي يعبّئه مسبقًا. الأنواع غير المربوطة تترك نوع الحادث ليختاره المؤلف. + نوع النداء + غير مربوط + قائمة انتظار الإرسال + تم حفظ إعدادات NERIS. + تمت إضافة الإرسال إلى قائمة انتظار NERIS. diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.de.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.de.resx index 3a32fdc2..54b39584 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.de.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.de.resx @@ -213,4 +213,134 @@ Summen Eigentümer Gruppe + Einsatzberichte + NERIS-Einsatzberichte + Ein maßgeblicher Einsatzbericht pro Einsatz. Berichte beginnen beim disponierten Einsatz, führen die Quelle jedes vorausgefüllten Werts mit und werden vor Bestätigung und Übermittlung gegen den festgelegten NERIS-Vertrag geprüft. + Einsatzbericht + Einsatzbericht bearbeiten + Aus Einsatz starten + Einsatzbericht starten + Einsatz auswählen + Keine aktiven Einsätze; öffnen Sie einen abgeschlossenen Einsatz in der Disposition, um seinen Bericht zu starten. + Einsatzbericht aus dem Einsatz gestartet. + Für diesen Einsatz gibt es bereits einen Einsatzbericht. + Einsatznummer + NERIS-Einsatz-ID + Übermittlung + Übermittlungen + Übermittlungsstatus + Diese Revision wurde nicht übermittelt. + Versuche + Zielstatus + Probleme + Eingereiht + Gesendet + Abgeschlossen + Nutzdaten + An NERIS übermitteln + Korrigieren und erneut übermitteln + Prüfen + Mit NERIS prüfen + Prüfprobleme + Keine offenen Prüfprobleme. + Prüfung abgeschlossen mit {0} Problem(en). + Schweregrad + Regel + Meldung + Quelle + Beheben Sie die folgenden Fehler vor dem Abschließen. + Die NERIS-Übermittlung ist für diese Abteilung nicht aktiviert. Abgeschlossene Berichte bleiben lokal, bis ein Administrator das NERIS-Profil einrichtet. + Die NERIS-Übermittlung ist systemweit abgeschaltet; Berichte können erstellt und abgeschlossen, aber nicht übermittelt werden. + Das NERIS-Profil ist nicht eingerichtet. + NERIS hat diesen Bericht abgelehnt. Korrigieren Sie die Felder und übermitteln Sie erneut; eine neue Revision und ein neuer Idempotenzschlüssel werden erzeugt. + Dieser Bericht liegt bei NERIS. Die Bearbeitung wartet auf die Antwort des Ziels. + NERIS hat diesen Bericht akzeptiert. + Dispositionszeiten + Einsatz erstellt + Anruf angenommen + Erstes Eintreffen + Einsatz beendet + Leitstelle + Determinantencode + Dispositions-Einsatzcode + Ergebnis + Personen anwesend + Vertriebene Personen + Gerettete Tiere + Besondere Kennzeichen + Einsatzarten + Primär + NERIS-Typ + Adresse + Hausnummer + Straße + Gemeinde + Landkreis + Bundesland + Postleitzahl + Land + Ortstyp + Nutzung + Querstraße + Koordinaten + NERIS-Einheiten-ID + Besatzung + Bereitstellung + Unterwegs abgebrochen + Frei + Fahrmodus + Nachbarschaftshilfe + Richtung + Hilfeart + Organisation + NERIS-ID der Organisation + Nicht-Feuerwehr + Maßnahmen und Taktik + Maßnahme + Einheit + Zeit + Hindernisse + Ergebnis + Woher jeder vorausgefüllte Wert stammt. Korrekturen behalten das Original neben dem korrigierten Wert. + Fakt + Quellwert + Aktueller Wert + Korrigiert von + Disposition + Anwendung + Abgeleitet + Korrigiert + Speichern und prüfen + Bestätigungen + Unterzeichnet von + Unterzeichnet + Zur Prüfung einreichen + Zur Korrektur zurückgeben + Prüfung fällig + NERIS-Einstellungen + NERIS-Identität, Zugangsdaten und Umgebung der Abteilung sowie die Zuordnung lokaler Einsatzarten zu NERIS-Einsatzarten. Zugangsdaten werden pro Abteilung verschlüsselt gespeichert und nie wieder angezeigt. + NERIS-Entitäts-ID + Entitätsname + Umgebung + API-Basis-URL überschreiben + Grant-Typ + Benutzername + Passwort + Client-ID + Client-Secret + Zugangsdaten sind gespeichert. Felder leer lassen, um sie zu behalten. + Keine Zugangsdaten gespeichert. + Beim Abschließen eines Berichts automatisch übermitteln + NERIS-Übermittlung für diese Abteilung aktivieren + Vertragsversion + Letztes Token ausgestellt + Letzter erfolgreicher Aufruf + Letzter Fehler + Zuordnung der Einsatzarten + Ordnen Sie jede Einsatzart der NERIS-Einsatzart zu, die sie vorausfüllt. Nicht zugeordnete Arten überlassen die Wahl dem Verfasser. + Einsatzart + Nicht zugeordnet + Übermittlungswarteschlange + NERIS-Einstellungen gespeichert. + Übermittlung an NERIS eingereiht. diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.el.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.el.resx index 0f610ad3..9eefd3e3 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.el.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.el.resx @@ -213,4 +213,134 @@ Σύνολα Κάτοχος Ομάδα + Αναφορές συμβάντων + Αναφορές συμβάντων NERIS + Μία έγκυρη αναφορά συμβάντος ανά κλήση. Οι αναφορές ξεκινούν από την κλήση αποστολής, φέρουν την πηγή κάθε προσυμπληρωμένης τιμής και επικυρώνονται με το καθορισμένο συμβόλαιο NERIS πριν από τη βεβαίωση και την υποβολή. + Αναφορά συμβάντος + Επεξεργασία αναφοράς συμβάντος + Έναρξη από κλήση + Έναρξη αναφοράς συμβάντος + Επιλέξτε κλήση + Δεν υπάρχουν ενεργές κλήσεις. Ανοίξτε μια κλειστή κλήση από την Αποστολή για να ξεκινήσετε την αναφορά της. + Η αναφορά συμβάντος ξεκίνησε από την κλήση. + Αυτή η κλήση έχει ήδη αναφορά συμβάντος. + Αριθμός συμβάντος + Αναγνωριστικό συμβάντος NERIS + Υποβολή + Υποβολές + Κατάσταση υποβολής + Αυτή η αναθεώρηση δεν έχει υποβληθεί. + Προσπάθειες + Κατάσταση προορισμού + Ζητήματα + Σε ουρά + Απεστάλη + Ολοκληρώθηκε + Ωφέλιμο φορτίο + Υποβολή στο NERIS + Διόρθωση και επανυποβολή + Επικύρωση + Επικύρωση με NERIS + Ζητήματα επικύρωσης + Δεν υπάρχουν εκκρεμή ζητήματα επικύρωσης. + Η επικύρωση ολοκληρώθηκε με {0} ζήτημα(τα). + Σοβαρότητα + Κανόνας + Μήνυμα + Πηγή + Διορθώστε τα παρακάτω σφάλματα πριν την οριστικοποίηση. + Η υποβολή στο NERIS δεν είναι ενεργή για αυτό το τμήμα. Οι οριστικοποιημένες αναφορές παραμένουν τοπικές μέχρι ένας διαχειριστής να ρυθμίσει το προφίλ NERIS. + Η υποβολή στο NERIS είναι απενεργοποιημένη σε επίπεδο συστήματος. Οι αναφορές μπορούν να συνταχθούν και να οριστικοποιηθούν αλλά όχι να παραδοθούν. + Το προφίλ NERIS δεν έχει ρυθμιστεί. + Το NERIS απέρριψε αυτή την αναφορά. Διορθώστε τα πεδία και επανυποβάλετε· δημιουργούνται νέα αναθεώρηση και νέο κλειδί αδρανοποίησης. + Αυτή η αναφορά βρίσκεται στο NERIS. Η επεξεργασία αναμένει την απάντηση του προορισμού. + Το NERIS αποδέχθηκε αυτή την αναφορά. + Χρόνοι αποστολής + Δημιουργία κλήσης + Απάντηση κλήσης + Πρώτη άφιξη + Λήξη συμβάντος + Κέντρο αποστολής + Κωδικός καθοριστή + Κωδικός συμβάντος αποστολής + Έκβαση + Παρόντα άτομα + Εκτοπισμένα άτομα + Διασωθέντα ζώα + Ειδικοί τροποποιητές + Τύποι συμβάντος + Κύριος + Τύπος NERIS + Διεύθυνση + Αριθμός + Οδός + Δήμος + Νομός + Πολιτεία + Ταχυδρομικός κώδικας + Χώρα + Τύπος τόπου + Χρήση τοποθεσίας + Κάθετη οδός + Συντεταγμένες + Αναγνωριστικό μονάδας NERIS + Στελέχωση + Σε αναμονή + Ακύρωση καθ' οδόν + Ελεύθερο + Τρόπος απόκρισης + Αμοιβαία βοήθεια + Κατεύθυνση + Τύπος βοήθειας + Φορέας + Αναγνωριστικό NERIS φορέα + Μη πυροσβεστικός φορέας + Ενέργειες και τακτικές + Τακτική + Μονάδα + Ώρα + Εμπόδια + Αποτέλεσμα + Από πού προήλθε κάθε προσυμπληρωμένη τιμή. Οι διορθώσεις διατηρούν το αρχικό δίπλα στη διορθωμένη τιμή. + Στοιχείο + Τιμή πηγής + Τρέχουσα τιμή + Διορθώθηκε από + Αποστολή + Εφαρμογή + Παράγωγο + Διορθωμένο + Αποθήκευση και επικύρωση + Βεβαιώσεις + Υπογράφηκε από + Υπογράφηκε + Υποβολή για έλεγχο + Επιστροφή για διόρθωση + Προθεσμία ελέγχου + Ρυθμίσεις NERIS + Η ταυτότητα, τα διαπιστευτήρια και το περιβάλλον NERIS του τμήματος, καθώς και η αντιστοίχιση τοπικών τύπων κλήσης σε τύπους συμβάντος NERIS. Τα διαπιστευτήρια αποθηκεύονται κρυπτογραφημένα ανά τμήμα και δεν εμφανίζονται ξανά. + Αναγνωριστικό οντότητας NERIS + Όνομα οντότητας + Περιβάλλον + Παράκαμψη βασικού URL API + Τύπος εξουσιοδότησης + Όνομα χρήστη + Κωδικός πρόσβασης + Αναγνωριστικό πελάτη + Μυστικό πελάτη + Υπάρχουν αποθηκευμένα διαπιστευτήρια. Αφήστε τα πεδία κενά για να τα διατηρήσετε. + Δεν υπάρχουν αποθηκευμένα διαπιστευτήρια. + Αυτόματη υποβολή κατά την οριστικοποίηση αναφοράς + Ενεργοποίηση υποβολής NERIS για αυτό το τμήμα + Έκδοση συμβολαίου + Τελευταίο διακριτικό + Τελευταία επιτυχής κλήση + Τελευταίο σφάλμα + Αντιστοίχιση τύπων κλήσης + Αντιστοιχίστε κάθε τύπο κλήσης αποστολής στον τύπο συμβάντος NERIS που προσυμπληρώνει. Οι μη αντιστοιχισμένοι τύποι αφήνουν την επιλογή στον συντάκτη. + Τύπος κλήσης + Μη αντιστοιχισμένο + Ουρά υποβολών + Οι ρυθμίσεις NERIS αποθηκεύτηκαν. + Η υποβολή μπήκε στην ουρά για το NERIS. diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.en.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.en.resx index 4cf0dcd8..6a41a5d3 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.en.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.en.resx @@ -213,4 +213,134 @@ Totals Owner Group + Incident reports + NERIS incident reports + One authoritative incident report per call. Reports start from the dispatched call, carry the source of every prefilled value, and are validated against the pinned NERIS contract before they are attested and submitted. + Incident report + Edit incident report + Start from call + Start incident report + Select a call + No active calls; open a closed call from Dispatch to start its report. + Incident report started from the call. + This call already has an incident report. + Incident number + NERIS incident ID + Submission + Submissions + Submission state + This revision has not been submitted. + Attempts + Destination status + Issues + Queued + Sent + Completed + Payload + Submit to NERIS + Correct and resubmit + Validate + Validate with NERIS + Validation issues + No open validation issues. + Validation completed with {0} issue(s). + Severity + Rule + Message + Source + Fix the errors below before finalizing. + NERIS submission is not enabled for this department. Finalized reports stay local until an administrator configures the NERIS profile. + NERIS submission is switched off system-wide; reports can be authored and finalized but not delivered. + The NERIS profile is not configured. + NERIS rejected this report. Correct the fields and resubmit; a new revision and a new idempotency key are created. + This report is with NERIS. Editing waits until the destination answers. + NERIS accepted this report. + Dispatch times + Call created + Call answered + First arrival + Incident cleared + Dispatch center + Determinant code + Dispatch incident code + Disposition + People present + People displaced + Animals rescued + Special modifiers + Incident types + Primary + NERIS type + Address + Number + Street + Municipality + County + State + Postal code + Country + Place type + Location use + Cross street + Coordinates + NERIS unit ID + Staffing + Staged + Canceled en route + Cleared + Response mode + Mutual aid + Direction + Aid type + Agency + Agency NERIS ID + Non-fire agency + Actions and tactics + Tactic + Unit + Time + Impediments + Outcome + Where each prefilled value came from. Corrections keep the original beside the corrected value. + Fact + Source value + Current value + Corrected by + Dispatch + App + Derived + Corrected + Save and validate + Attestations + Signed by + Signed + Submit for review + Return for correction + Review due + NERIS settings + The department's NERIS identity, credential and environment, plus the crosswalk from local call types to NERIS incident types. Credentials are stored encrypted per department and are never shown again. + NERIS entity ID + Entity name + Environment + API base URL override + Grant type + Username + Password + Client ID + Client secret + A credential is stored. Leave the fields blank to keep it. + No credential is stored. + Submit automatically when a report is finalized + Enable NERIS submission for this department + Contract version + Last token issued + Last successful call + Last error + Call type crosswalk + Map each dispatch call type to the NERIS incident type it prefills. Unmapped types leave the incident type for the author to choose. + Call type + Unmapped + Submission queue + NERIS settings saved. + Submission queued for NERIS. diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.es.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.es.resx index fae66b68..6bdab4fe 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.es.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.es.resx @@ -213,4 +213,134 @@ Totales Propietario Grupo + Informes de incidentes + Informes de incidentes NERIS + Un informe de incidente autorizado por llamada. Los informes parten de la llamada despachada, conservan el origen de cada valor precargado y se validan contra el contrato NERIS fijado antes de certificarse y enviarse. + Informe de incidente + Editar informe de incidente + Iniciar desde la llamada + Iniciar informe de incidente + Seleccione una llamada + No hay llamadas activas; abra una llamada cerrada desde Despacho para iniciar su informe. + Informe de incidente iniciado desde la llamada. + Esta llamada ya tiene un informe de incidente. + Número de incidente + ID de incidente NERIS + Envío + Envíos + Estado del envío + Esta revisión no se ha enviado. + Intentos + Estado en destino + Problemas + En cola + Enviado + Completado + Carga útil + Enviar a NERIS + Corregir y reenviar + Validar + Validar con NERIS + Problemas de validación + No hay problemas de validación pendientes. + Validación completada con {0} problema(s). + Gravedad + Regla + Mensaje + Origen + Corrija los errores siguientes antes de finalizar. + El envío a NERIS no está habilitado para este departamento. Los informes finalizados permanecen locales hasta que un administrador configure el perfil NERIS. + El envío a NERIS está desactivado en todo el sistema; los informes pueden redactarse y finalizarse pero no entregarse. + El perfil NERIS no está configurado. + NERIS rechazó este informe. Corrija los campos y reenvíe; se crean una nueva revisión y una nueva clave de idempotencia. + Este informe está en NERIS. La edición espera la respuesta del destino. + NERIS aceptó este informe. + Tiempos de despacho + Llamada creada + Llamada contestada + Primera llegada + Incidente despejado + Centro de despacho + Código determinante + Código de incidente de despacho + Resolución + Personas presentes + Personas desplazadas + Animales rescatados + Modificadores especiales + Tipos de incidente + Principal + Tipo NERIS + Dirección + Número + Calle + Municipio + Condado + Estado + Código postal + País + Tipo de lugar + Uso del lugar + Calle transversal + Coordenadas + ID de unidad NERIS + Dotación + En espera + Cancelado en ruta + Liberado + Modo de respuesta + Ayuda mutua + Dirección + Tipo de ayuda + Agencia + ID NERIS de la agencia + Agencia no bomberil + Acciones y tácticas + Táctica + Unidad + Hora + Impedimentos + Resultado + De dónde proviene cada valor precargado. Las correcciones conservan el original junto al valor corregido. + Dato + Valor de origen + Valor actual + Corregido por + Despacho + Aplicación + Derivado + Corregido + Guardar y validar + Certificaciones + Firmado por + Firmado + Enviar a revisión + Devolver para corrección + Revisión pendiente + Configuración NERIS + La identidad, credencial y entorno NERIS del departamento, más la correspondencia de tipos de llamada locales a tipos de incidente NERIS. Las credenciales se guardan cifradas por departamento y no vuelven a mostrarse. + ID de entidad NERIS + Nombre de la entidad + Entorno + Sobrescritura de URL base de la API + Tipo de concesión + Usuario + Contraseña + ID de cliente + Secreto de cliente + Hay una credencial guardada. Deje los campos en blanco para conservarla. + No hay credencial guardada. + Enviar automáticamente al finalizar un informe + Habilitar el envío a NERIS para este departamento + Versión del contrato + Último token emitido + Última llamada correcta + Último error + Correspondencia de tipos de llamada + Asocie cada tipo de llamada de despacho al tipo de incidente NERIS que precarga. Los tipos sin asociar dejan la elección al autor. + Tipo de llamada + Sin asociar + Cola de envíos + Configuración NERIS guardada. + Envío en cola para NERIS. diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.fr.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.fr.resx index bdb79387..72b4cf68 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.fr.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.fr.resx @@ -213,4 +213,134 @@ Totaux Propriétaire Groupe + Rapports d'incident + Rapports d'incident NERIS + Un rapport d'incident faisant foi par appel. Les rapports partent de l'appel dispatché, conservent la source de chaque valeur préremplie et sont validés selon le contrat NERIS épinglé avant attestation et envoi. + Rapport d'incident + Modifier le rapport d'incident + Démarrer depuis l'appel + Démarrer le rapport d'incident + Sélectionner un appel + Aucun appel actif ; ouvrez un appel clôturé depuis la Régulation pour démarrer son rapport. + Rapport d'incident démarré depuis l'appel. + Cet appel a déjà un rapport d'incident. + Numéro d'incident + ID d'incident NERIS + Envoi + Envois + État de l'envoi + Cette révision n'a pas été envoyée. + Tentatives + Statut de la destination + Problèmes + En file + Envoyé + Terminé + Charge utile + Envoyer à NERIS + Corriger et renvoyer + Valider + Valider avec NERIS + Problèmes de validation + Aucun problème de validation ouvert. + Validation terminée avec {0} problème(s). + Gravité + Règle + Libellé + Origine + Corrigez les erreurs ci-dessous avant de finaliser. + L'envoi à NERIS n'est pas activé pour ce service. Les rapports finalisés restent locaux jusqu'à ce qu'un administrateur configure le profil NERIS. + L'envoi à NERIS est désactivé à l'échelle du système ; les rapports peuvent être rédigés et finalisés mais pas transmis. + Le profil NERIS n'est pas configuré. + NERIS a rejeté ce rapport. Corrigez les champs et renvoyez ; une nouvelle révision et une nouvelle clé d'idempotence sont créées. + Ce rapport est chez NERIS. La modification attend la réponse de la destination. + NERIS a accepté ce rapport. + Horaires de régulation + Appel créé + Appel décroché + Première arrivée + Incident terminé + Centre de régulation + Code déterminant + Code d'incident de régulation + Issue + Personnes présentes + Personnes déplacées + Animaux secourus + Modificateurs spéciaux + Types d'incident + Principal + Type NERIS + Adresse + Numéro + Rue + Commune + Comté + État + Code postal + Pays + Type de lieu + Usage du lieu + Rue transversale + Coordonnées + ID d'unité NERIS + Effectif + En attente + Annulé en route + Libéré + Mode de réponse + Entraide + Sens + Type d'aide + Organisme + ID NERIS de l'organisme + Organisme hors pompiers + Actions et tactiques + Tactique + Unité + Heure + Obstacles + Résultat + D'où provient chaque valeur préremplie. Les corrections conservent l'original à côté de la valeur corrigée. + Donnée + Valeur source + Valeur actuelle + Corrigé par + Régulation + Application + Dérivé + Corrigé + Enregistrer et valider + Attestations signées + Signé par + Signé + Soumettre pour relecture + Renvoyer pour correction + Relecture due + Paramètres NERIS + L'identité, l'identifiant et l'environnement NERIS du service, plus la correspondance des types d'appel locaux vers les types d'incident NERIS. Les identifiants sont chiffrés par service et ne sont jamais réaffichés. + ID d'entité NERIS + Nom de l'entité + Environnement + Remplacement de l'URL de base de l'API + Type d'octroi + Nom d'utilisateur + Mot de passe + ID client + Secret client + Un identifiant est enregistré. Laissez les champs vides pour le conserver. + Aucun identifiant enregistré. + Envoyer automatiquement à la finalisation d'un rapport + Activer l'envoi à NERIS pour ce service + Version du contrat + Dernier jeton émis + Dernier appel réussi + Dernière erreur + Correspondance des types d'appel + Associez chaque type d'appel au type d'incident NERIS qu'il préremplit. Les types non associés laissent le choix à l'auteur. + Type d'appel + Non associé + File d'envoi + Paramètres NERIS enregistrés. + Envoi mis en file pour NERIS. diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.it.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.it.resx index d2c8ad12..36704af2 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.it.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.it.resx @@ -213,4 +213,134 @@ Totali Proprietario Gruppo + Rapporti di incidente + Rapporti di incidente NERIS + Un rapporto di incidente autorevole per chiamata. I rapporti partono dalla chiamata inviata, conservano l'origine di ogni valore precompilato e vengono convalidati con il contratto NERIS fissato prima di attestazione e invio. + Rapporto di incidente + Modifica rapporto di incidente + Avvia dalla chiamata + Avvia rapporto di incidente + Seleziona una chiamata + Nessuna chiamata attiva; apri una chiamata chiusa dal Dispatch per avviarne il rapporto. + Rapporto di incidente avviato dalla chiamata. + Questa chiamata ha già un rapporto di incidente. + Numero incidente + ID incidente NERIS + Invio + Invii + Stato invio + Questa revisione non è stata inviata. + Tentativi + Stato destinazione + Problemi + In coda + Inviato + Completato + Contenuto inviato + Invia a NERIS + Correggi e reinvia + Convalida + Convalida con NERIS + Problemi di convalida + Nessun problema di convalida aperto. + Convalida completata con {0} problema/i. + Gravità + Regola + Messaggio + Origine + Correggi gli errori seguenti prima di finalizzare. + L'invio a NERIS non è abilitato per questo dipartimento. I rapporti finalizzati restano locali finché un amministratore non configura il profilo NERIS. + L'invio a NERIS è disattivato a livello di sistema; i rapporti possono essere redatti e finalizzati ma non consegnati. + Il profilo NERIS non è configurato. + NERIS ha respinto questo rapporto. Correggi i campi e reinvia; vengono create una nuova revisione e una nuova chiave di idempotenza. + Questo rapporto è presso NERIS. La modifica attende la risposta della destinazione. + NERIS ha accettato questo rapporto. + Tempi di dispatch + Chiamata creata + Chiamata risposta + Primo arrivo + Incidente concluso + Centrale operativa + Codice determinante + Codice incidente dispatch + Esito + Persone presenti + Persone sfollate + Animali salvati + Modificatori speciali + Tipi di incidente + Principale + Tipo NERIS + Indirizzo + Numero civico + Via + Comune + Provincia + Stato + CAP + Paese + Tipo di luogo + Uso del luogo + Via trasversale + Coordinate + ID unità NERIS + Personale + In attesa + Annullato in viaggio + Libero + Modalità di risposta + Mutuo soccorso + Direzione + Tipo di aiuto + Ente + ID NERIS dell'ente + Ente non VVF + Azioni e tattiche + Tattica + Unità + Ora + Impedimenti + Esito + Da dove proviene ogni valore precompilato. Le correzioni conservano l'originale accanto al valore corretto. + Dato + Valore di origine + Valore attuale + Corretto da + Centrale operativa + Applicazione + Derivato + Corretto + Salva e convalida + Attestazioni + Firmato da + Firmato + Invia per revisione + Restituisci per correzione + Revisione entro + Impostazioni NERIS + Identità, credenziale e ambiente NERIS del dipartimento, più la corrispondenza dai tipi di chiamata locali ai tipi di incidente NERIS. Le credenziali sono cifrate per dipartimento e non vengono più mostrate. + ID entità NERIS + Nome entità + Ambiente + Override URL base API + Tipo di grant + Nome utente + Parola chiave + ID client + Segreto client + È memorizzata una credenziale. Lascia i campi vuoti per conservarla. + Nessuna credenziale memorizzata. + Invia automaticamente alla finalizzazione di un rapporto + Abilita l'invio a NERIS per questo dipartimento + Versione contratto + Ultimo token emesso + Ultima chiamata riuscita + Ultimo errore + Corrispondenza tipi di chiamata + Associa ogni tipo di chiamata al tipo di incidente NERIS che precompila. I tipi non associati lasciano la scelta all'autore. + Tipo di chiamata + Non associato + Coda di invio + Impostazioni NERIS salvate. + Invio in coda per NERIS. diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.pl.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.pl.resx index a08b75f7..b4300514 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.pl.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.pl.resx @@ -213,4 +213,134 @@ Razem Właściciel Grupa + Raporty ze zdarzeń + Raporty ze zdarzeń NERIS + Jeden wiążący raport ze zdarzenia na wezwanie. Raporty zaczynają się od zadysponowanego wezwania, zachowują źródło każdej wstępnie wypełnionej wartości i są sprawdzane względem ustalonego kontraktu NERIS przed poświadczeniem i wysłaniem. + Raport ze zdarzenia + Edytuj raport ze zdarzenia + Rozpocznij z wezwania + Rozpocznij raport ze zdarzenia + Wybierz wezwanie + Brak aktywnych wezwań; otwórz zamknięte wezwanie w Dyspozytorni, aby rozpocząć jego raport. + Raport ze zdarzenia rozpoczęty z wezwania. + To wezwanie ma już raport ze zdarzenia. + Numer zdarzenia + ID zdarzenia NERIS + Wysyłka + Wysyłki + Stan wysyłki + Ta rewizja nie została wysłana. + Próby + Stan w miejscu docelowym + Problemy + W kolejce + Wysłano + Zakończono + Ładunek + Wyślij do NERIS + Popraw i wyślij ponownie + Sprawdź + Sprawdź z NERIS + Problemy walidacji + Brak otwartych problemów walidacji. + Walidacja zakończona: {0} problem(y). + Waga + Reguła + Komunikat + Źródło + Popraw poniższe błędy przed finalizacją. + Wysyłka do NERIS nie jest włączona dla tej jednostki. Sfinalizowane raporty pozostają lokalne, dopóki administrator nie skonfiguruje profilu NERIS. + Wysyłka do NERIS jest wyłączona w całym systemie; raporty można tworzyć i finalizować, ale nie dostarczać. + Profil NERIS nie jest skonfigurowany. + NERIS odrzucił ten raport. Popraw pola i wyślij ponownie; powstaną nowa rewizja i nowy klucz idempotencji. + Ten raport jest w NERIS. Edycja czeka na odpowiedź odbiorcy. + NERIS przyjął ten raport. + Czasy dysponowania + Utworzenie wezwania + Odebranie zgłoszenia + Pierwsze przybycie + Zakończenie zdarzenia + Centrum dyspozytorskie + Kod determinanta + Kod zdarzenia z dyspozytorni + Rozstrzygnięcie + Obecność osób + Osoby wysiedlone + Uratowane zwierzęta + Modyfikatory specjalne + Typy zdarzenia + Główny + Typ NERIS + Adres + Numer + Ulica + Gmina + Powiat + Stan + Kod pocztowy + Kraj + Typ miejsca + Przeznaczenie miejsca + Ulica poprzeczna + Współrzędne + ID jednostki NERIS + Obsada + W gotowości + Odwołany w drodze + Zwolniony + Tryb odpowiedzi + Pomoc wzajemna + Kierunek + Rodzaj pomocy + Agencja + ID NERIS agencji + Agencja spoza straży + Działania i taktyka + Taktyka + Jednostka + Czas + Utrudnienia + Wynik + Skąd pochodzi każda wstępnie wypełniona wartość. Poprawki zachowują oryginał obok poprawionej wartości. + Fakt + Wartość źródłowa + Wartość bieżąca + Poprawione przez + Dyspozytornia + Aplikacja + Pochodne + Poprawione + Zapisz i sprawdź + Poświadczenia + Podpisane przez + Podpisano + Prześlij do przeglądu + Zwróć do poprawy + Termin przeglądu + Ustawienia NERIS + Tożsamość, poświadczenie i środowisko NERIS jednostki oraz mapowanie lokalnych typów wezwań na typy zdarzeń NERIS. Poświadczenia są szyfrowane per jednostka i nigdy nie są ponownie wyświetlane. + ID podmiotu NERIS + Nazwa podmiotu + Środowisko + Nadpisanie bazowego URL API + Typ grantu + Nazwa użytkownika + Hasło + ID klienta + Sekret klienta + Poświadczenie jest zapisane. Pozostaw pola puste, aby je zachować. + Brak zapisanego poświadczenia. + Wysyłaj automatycznie po sfinalizowaniu raportu + Włącz wysyłkę do NERIS dla tej jednostki + Wersja kontraktu + Ostatni wydany token + Ostatnie udane wywołanie + Ostatni błąd + Mapowanie typów wezwań + Przypisz każdy typ wezwania do typu zdarzenia NERIS, który wstępnie wypełnia. Nieprzypisane typy pozostawiają wybór autorowi. + Typ wezwania + Nieprzypisany + Kolejka wysyłek + Zapisano ustawienia NERIS. + Wysyłka do NERIS w kolejce. diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.sv.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.sv.resx index aee86ee6..c3ecbd1c 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.sv.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.sv.resx @@ -213,4 +213,134 @@ Totalt Ägare Grupp + Insatsrapporter + NERIS-insatsrapporter + En auktoritativ insatsrapport per larm. Rapporter utgår från det utlarmade ärendet, bär källan till varje förifyllt värde och valideras mot det låsta NERIS-kontraktet innan de attesteras och skickas. + Insatsrapport + Redigera insatsrapport + Starta från larm + Starta insatsrapport + Välj ett larm + Inga aktiva larm; öppna ett avslutat larm från Utlarmning för att starta dess rapport. + Insatsrapport startad från larmet. + Detta larm har redan en insatsrapport. + Insatsnummer + NERIS-insats-ID + Inlämning + Inlämningar + Inlämningsstatus + Denna revision har inte skickats. + Försök + Mottagarstatus + Problem + Köad + Skickad + Slutförd + Nyttolast + Skicka till NERIS + Rätta och skicka igen + Validera + Validera med NERIS + Valideringsproblem + Inga öppna valideringsproblem. + Validering klar med {0} problem. + Allvarlighet + Regel + Meddelande + Källa + Åtgärda felen nedan innan du slutför. + NERIS-inlämning är inte aktiverad för denna organisation. Slutförda rapporter stannar lokalt tills en administratör konfigurerar NERIS-profilen. + NERIS-inlämning är avstängd i hela systemet; rapporter kan skrivas och slutföras men inte levereras. + NERIS-profilen är inte konfigurerad. + NERIS avvisade denna rapport. Rätta fälten och skicka igen; en ny revision och en ny idempotensnyckel skapas. + Denna rapport ligger hos NERIS. Redigering väntar tills mottagaren svarar. + NERIS godkände denna rapport. + Utlarmningstider + Larm skapat + Samtal besvarat + Första ankomst + Insats avslutad + Larmcentral + Determinantkod + Utlarmningskod + Utfall + Personer närvarande + Evakuerade personer + Räddade djur + Särskilda modifierare + Insatstyper + Primär + NERIS-typ + Adress + Nummer + Gata + Kommun + Län + Delstat + Postnummer + Land + Platstyp + Användning + Tvärgata + Koordinater + NERIS-enhets-ID + Bemanning + Uppställd + Avbruten under färd + Klar + Körsätt + Ömsesidig hjälp + Riktning + Hjälptyp + Organisation + Organisationens NERIS-ID + Icke-räddningstjänst + Åtgärder och taktik + Taktik + Enhet + Tid + Hinder + Resultat + Var varje förifyllt värde kom ifrån. Rättelser behåller originalet bredvid det rättade värdet. + Faktum + Källvärde + Aktuellt värde + Rättat av + Utlarmning + Applikation + Härlett + Rättat + Spara och validera + Attesteringar + Signerat av + Signerat + Skicka för granskning + Returnera för rättelse + Granskning senast + NERIS-inställningar + Organisationens NERIS-identitet, inloggning och miljö samt mappningen från lokala larmtyper till NERIS-insatstyper. Inloggningsuppgifter lagras krypterade per organisation och visas aldrig igen. + NERIS-entitets-ID + Entitetsnamn + Miljö + Åsidosätt API-bas-URL + Grant-typ + Användarnamn + Lösenord + Klient-ID + Klienthemlighet + En inloggning är lagrad. Lämna fälten tomma för att behålla den. + Ingen inloggning är lagrad. + Skicka automatiskt när en rapport slutförs + Aktivera NERIS-inlämning för denna organisation + Kontraktsversion + Senaste token utfärdad + Senaste lyckade anrop + Senaste fel + Mappning av larmtyper + Mappa varje larmtyp till den NERIS-insatstyp den förifyller. Omappade typer lämnar valet till författaren. + Larmtyp + Omappad + Inlämningskö + NERIS-inställningar sparade. + Inlämning köad för NERIS. diff --git a/Core/Resgrid.Localization/Areas/User/Records/Records.uk.resx b/Core/Resgrid.Localization/Areas/User/Records/Records.uk.resx index d4b8c31f..375ce00d 100644 --- a/Core/Resgrid.Localization/Areas/User/Records/Records.uk.resx +++ b/Core/Resgrid.Localization/Areas/User/Records/Records.uk.resx @@ -213,4 +213,134 @@ Разом Власник Група + Звіти про інциденти + Звіти про інциденти NERIS + Один авторитетний звіт про інцидент на виклик. Звіти починаються з диспетчеризованого виклику, зберігають джерело кожного попередньо заповненого значення та перевіряються за закріпленим контрактом NERIS перед засвідченням і поданням. + Звіт про інцидент + Редагувати звіт про інцидент + Почати з виклику + Почати звіт про інцидент + Виберіть виклик + Немає активних викликів; відкрийте закритий виклик у Диспетчеризації, щоб почати його звіт. + Звіт про інцидент розпочато з виклику. + Цей виклик уже має звіт про інцидент. + Номер інциденту + Ідентифікатор інциденту NERIS + Подання + Подання + Стан подання + Цю редакцію не подано. + Спроби + Стан призначення + Проблеми + У черзі + Надіслано + Завершено + Корисне навантаження + Подати до NERIS + Виправити та подати повторно + Перевірити + Перевірити з NERIS + Проблеми перевірки + Немає відкритих проблем перевірки. + Перевірку завершено, проблем: {0}. + Серйозність + Правило + Повідомлення + Джерело + Виправте помилки нижче перед завершенням. + Подання до NERIS не ввімкнено для цього підрозділу. Завершені звіти залишаються локальними, доки адміністратор не налаштує профіль NERIS. + Подання до NERIS вимкнено в усій системі; звіти можна створювати та завершувати, але не доставляти. + Профіль NERIS не налаштовано. + NERIS відхилив цей звіт. Виправте поля та подайте повторно; буде створено нову редакцію та новий ключ ідемпотентності. + Цей звіт перебуває в NERIS. Редагування очікує відповіді призначення. + NERIS прийняв цей звіт. + Часи диспетчеризації + Виклик створено + Виклик прийнято + Перше прибуття + Інцидент завершено + Диспетчерський центр + Код детермінанти + Код інциденту диспетчеризації + Результат + Присутні люди + Переміщені особи + Врятовані тварини + Спеціальні модифікатори + Типи інциденту + Основний + Тип NERIS + Адреса + Номер + Вулиця + Муніципалітет + Округ + Штат + Поштовий індекс + Країна + Тип місця + Використання місця + Перехресна вулиця + Координати + Ідентифікатор підрозділу NERIS + Укомплектування + Очікування + Скасовано в дорозі + Звільнено + Режим реагування + Взаємодопомога + Напрям + Тип допомоги + Організація + NERIS-ідентифікатор організації + Непожежна організація + Дії та тактика + Тактика + Підрозділ + Час + Перешкоди + Результат + Звідки походить кожне попередньо заповнене значення. Виправлення зберігають оригінал поруч із виправленим значенням. + Факт + Значення джерела + Поточне значення + Виправив + Диспетчеризація + Застосунок + Похідне + Виправлено + Зберегти та перевірити + Засвідчення + Підписав + Підписано + Надіслати на перевірку + Повернути на виправлення + Термін перевірки + Налаштування NERIS + Ідентичність, облікові дані та середовище NERIS підрозділу, а також відповідність локальних типів викликів типам інцидентів NERIS. Облікові дані зберігаються зашифрованими для кожного підрозділу й більше не показуються. + Ідентифікатор суб'єкта NERIS + Назва суб'єкта + Середовище + Заміна базового URL API + Тип гранту + Ім'я користувача + Пароль + Ідентифікатор клієнта + Секрет клієнта + Облікові дані збережено. Залиште поля порожніми, щоб зберегти їх. + Облікові дані не збережено. + Подавати автоматично після завершення звіту + Увімкнути подання до NERIS для цього підрозділу + Версія контракту + Останній виданий токен + Останній успішний виклик + Остання помилка + Відповідність типів викликів + Зіставте кожен тип виклику з типом інциденту NERIS, який він попередньо заповнює. Незіставлені типи залишають вибір автору. + Тип виклику + Не зіставлено + Черга подань + Налаштування NERIS збережено. + Подання поставлено в чергу для NERIS. diff --git a/Core/Resgrid.Model/Providers/INerisProviders.cs b/Core/Resgrid.Model/Providers/INerisProviders.cs new file mode 100644 index 00000000..5f0ffdc6 --- /dev/null +++ b/Core/Resgrid.Model/Providers/INerisProviders.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Resgrid.Model.Providers +{ + /// Department NERIS profile, credential, value sets and crosswalks (RMS plan section 5.5). + public interface INerisProfileService + { + Task GetProfileAsync(int departmentId); + + /// Saves the profile; a non-null credential replaces the stored one (encrypted per department), null keeps it. + Task SaveProfileAsync(RmsNerisProfile profile, NerisCredential credential, string userId, CancellationToken cancellationToken = default); + + /// Decrypts the stored credential for one call; never cached, never logged. + Task GetCredentialAsync(RmsNerisProfile profile); + + /// True when NerisConfig.Enabled, the profile is enabled, and it carries an entity ID and a credential. + Task IsSubmissionEnabledAsync(int departmentId); + + /// A value set of the pinned contract, from the embedded snapshot (seeded to RmsNerisValueSets on first use). + NerisValueSet GetValueSet(string setKey); + + IReadOnlyList ValueSetKeys { get; } + + string ContractVersion { get; } + + Task EnsureValueSetsSeededAsync(CancellationToken cancellationToken = default); + + /// The department's mapped NERIS code for a local code, or null when unmapped. + Task ResolveCrosswalkAsync(int departmentId, string setKey, string localSource, string localCode); + + Task> GetCrosswalksAsync(int departmentId); + + Task SaveCrosswalkAsync(int departmentId, string userId, string setKey, string localSource, string localCode, string nerisCode, CancellationToken cancellationToken = default); + + /// Removes a department mapping so the local code is unmapped again; false when there was none. + Task RemoveCrosswalkAsync(int departmentId, string setKey, string localSource, string localCode, CancellationToken cancellationToken = default); + } + + /// Pure aggregate-to-payload mapping against the pinned contract; original codes ride beside mapped ones. + public interface INerisMappingService + { + /// The exact IncidentPayload JSON the destination receives (the immutable submission artifact). + string BuildIncidentPayloadJson(NerisIncidentSnapshot snapshot, RmsNerisProfile profile); + } + + /// Local (offline) validation against the pinned contract's requiredness, value sets and time sequence; remote validation through the client. + public interface INerisValidationService + { + List ValidateLocal(NerisIncidentSnapshot snapshot, RmsNerisProfile profile); + + Task> ValidateRemoteAsync(RmsNerisProfile profile, string payloadJson, CancellationToken cancellationToken = default); + } + + /// Low-level NERIS API boundary (token, validate, create, update, status, history). + public interface INerisApiClient + { + Task ValidateAsync(RmsNerisProfile profile, NerisCredential credential, string payloadJson, CancellationToken cancellationToken = default); + + Task CreateIncidentAsync(RmsNerisProfile profile, NerisCredential credential, string payloadJson, CancellationToken cancellationToken = default); + + Task UpdateIncidentAsync(RmsNerisProfile profile, NerisCredential credential, string nerisIncidentId, string payloadJson, CancellationToken cancellationToken = default); + + Task GetStatusAsync(RmsNerisProfile profile, NerisCredential credential, string nerisIncidentId, CancellationToken cancellationToken = default); + } + + /// + /// Orchestrates one submission attempt: create on first delivery, update when the destination already holds the + /// incident, status poll while the destination reviews. Never holds a database transaction (plan 5.3); the + /// worker persists the outcome afterwards. + /// + public interface INerisSubmissionService + { + Task DeliverAsync(RmsNerisProfile profile, RmsSubmission submission, string existingNerisIncidentId, CancellationToken cancellationToken = default); + + Task CheckStatusAsync(RmsNerisProfile profile, string nerisIncidentId, CancellationToken cancellationToken = default); + } +} diff --git a/Core/Resgrid.Model/Records/IncidentReportContracts.cs b/Core/Resgrid.Model/Records/IncidentReportContracts.cs new file mode 100644 index 00000000..cffe2943 --- /dev/null +++ b/Core/Resgrid.Model/Records/IncidentReportContracts.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; + +namespace Resgrid.Model +{ + /// The incident report aggregate as the Web and the mapper read it: header plus the working-draft child rows (or a revision's copies). + public class IncidentReportAggregate + { + public RmsIncidentReport Report { get; set; } + public RmsLocation Location { get; set; } + public List Types { get; set; } = new List(); + public List Units { get; set; } = new List(); + public List Aids { get; set; } = new List(); + public List Tactics { get; set; } = new List(); + public RmsNarrative Narrative { get; set; } + public List Facts { get; set; } = new List(); + public List Issues { get; set; } = new List(); + public List Submissions { get; set; } = new List(); + public List Signatures { get; set; } = new List(); + public List Revisions { get; set; } = new List(); + public List GroupScope { get; set; } = new List(); + + public RmsRecordState State => (RmsRecordState)(Report?.State ?? 0); + public bool HasBlockingIssues => Issues.Exists(i => i.Severity == (int)RmsValidationSeverity.Error); + public string SpecialModifierCodes => Report?.SpecialModifiersCsv ?? string.Empty; + } + + public class IncidentTypeInput + { + public string TypeCode { get; set; } + public bool IsPrimary { get; set; } + } + + public class IncidentUnitResponseInput + { + public int? UnitId { get; set; } + public string UnitNerisId { get; set; } + public string ReportedUnitId { get; set; } + public int? Staffing { get; set; } + public bool UnableToDispatch { get; set; } + public DateTime? DispatchedOn { get; set; } + public DateTime? EnrouteOn { get; set; } + public DateTime? OnSceneOn { get; set; } + public DateTime? CanceledEnrouteOn { get; set; } + public DateTime? StagingOn { get; set; } + public DateTime? ClearedOn { get; set; } + public string ResponseMode { get; set; } + public string TransportMode { get; set; } + } + + public class IncidentAidInput + { + public string Direction { get; set; } + public string AidType { get; set; } + public string CounterpartNerisId { get; set; } + public string CounterpartName { get; set; } + public bool IsNonFireDepartment { get; set; } + public string NonFdType { get; set; } + } + + public class IncidentTacticInput + { + public string TacticCode { get; set; } + public int? ActorUnitId { get; set; } + public DateTime? OccurredOn { get; set; } + } + + public class IncidentLocationInput + { + public string AddressText { get; set; } + public string Number { get; set; } + public string NumberPrefix { get; set; } + public string NumberSuffix { get; set; } + public string Street { get; set; } + public string UnitValue { get; set; } + public string Municipality { get; set; } + public string County { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string PlaceType { get; set; } + public string LocationUse { get; set; } + public string CrossStreet1 { get; set; } + public string CrossStreet2 { get; set; } + public decimal? Latitude { get; set; } + public decimal? Longitude { get; set; } + public string Jurisdiction { get; set; } + } + + /// + /// A draft save. Every list replaces the draft rows wholesale (the working draft is the only mutable state); + /// prefilled values that the author changes keep their provenance row with the corrected value recorded. + /// + public class IncidentReportDraftInput + { + public string IncidentNumber { get; set; } + public DateTime? CallCreatedOn { get; set; } + public DateTime? CallAnsweredOn { get; set; } + public DateTime? CallArrivalOn { get; set; } + public DateTime? IncidentClearedOn { get; set; } + public string DispatchCenterId { get; set; } + public string DeterminantCode { get; set; } + public string DispatchIncidentCode { get; set; } + public string Disposition { get; set; } + public bool? PeoplePresent { get; set; } + public int? DisplacementCount { get; set; } + public int? AnimalsRescued { get; set; } + public List SpecialModifiers { get; set; } = new List(); + public int? StationGroupId { get; set; } + public IncidentLocationInput Location { get; set; } + public List Types { get; set; } = new List(); + public List Units { get; set; } = new List(); + public List Aids { get; set; } = new List(); + public List Tactics { get; set; } = new List(); + public string Narrative { get; set; } + public string ImpedimentNarrative { get; set; } + public string OutcomeNarrative { get; set; } + public string SupplementalJson { get; set; } + public RmsOriginClient OriginClient { get; set; } = RmsOriginClient.Web; + } + + /// Stable provenance keys for the prefilled facts (RmsSourceFact.FactKey). + public static class NerisFactKeys + { + public const string CallCreate = "dispatch.call_create"; + public const string CallAnswered = "dispatch.call_answered"; + public const string CallArrival = "dispatch.call_arrival"; + public const string IncidentClear = "dispatch.incident_clear"; + public const string IncidentNumber = "dispatch.incident_number"; + public const string IncidentCode = "dispatch.incident_code"; + public const string Location = "base.location"; + public const string Point = "base.point"; + public const string IncidentType = "incident_types.primary"; + public static string UnitTime(int unitId, string field) => $"unit.{unitId}.{field}"; + } +} diff --git a/Core/Resgrid.Model/Records/IncidentReportValidationException.cs b/Core/Resgrid.Model/Records/IncidentReportValidationException.cs new file mode 100644 index 00000000..4504a222 --- /dev/null +++ b/Core/Resgrid.Model/Records/IncidentReportValidationException.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Resgrid.Model +{ + /// Finalize/resubmit refused because local validation found errors; the issues are stored on the report for the author. + public class IncidentReportValidationException : InvalidOperationException + { + public IncidentReportValidationException(string reportId, IReadOnlyList issues) + : base($"Incident report {reportId} has {issues?.Count ?? 0} validation error(s) and cannot be signed: " + + string.Join("; ", (issues ?? Array.Empty()).Take(5).Select(i => $"{i.FieldPath ?? i.RuleKey}: {i.Message}"))) + { + ReportId = reportId; + Issues = issues ?? Array.Empty(); + } + + public string ReportId { get; } + public IReadOnlyList Issues { get; } + } +} diff --git a/Core/Resgrid.Model/Records/NerisContracts.cs b/Core/Resgrid.Model/Records/NerisContracts.cs new file mode 100644 index 00000000..289eb703 --- /dev/null +++ b/Core/Resgrid.Model/Records/NerisContracts.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; + +namespace Resgrid.Model +{ + /// + /// Everything the NERIS mapping needs from one incident report, read once and handed to a pure mapper + /// (RMS plan section 5.5). Built by the incident report service from the aggregate; the mapper never + /// touches a repository. + /// + public class NerisIncidentSnapshot + { + public RmsIncidentReport Report { get; set; } + public RmsLocation Location { get; set; } + public List Types { get; set; } = new List(); + public List Units { get; set; } = new List(); + public List Aids { get; set; } = new List(); + public List Tactics { get; set; } = new List(); + public RmsNarrative Narrative { get; set; } + public List Facts { get; set; } = new List(); + /// Dispatch comments (call notes) carried into dispatch.comments; header text only, never restricted content. + public List DispatchComments { get; set; } = new List(); + /// NERIS special modifiers (MCI, declared disaster ...), value-set codes. + public List SpecialModifiers { get; set; } = new List(); + } + + public class NerisDispatchComment + { + public DateTime? Timestamp { get; set; } + public string Comment { get; set; } + } + + /// Department integration credential, decrypted only inside the provider for the duration of a call. + public class NerisCredential + { + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public string Username { get; set; } + public string Password { get; set; } + } + + public class NerisValueSet + { + public string SetKey { get; set; } + public string SchemaName { get; set; } + public IReadOnlyList Codes { get; set; } = new List(); + } + + public enum NerisOutcomeKind + { + /// The destination created the incident (first submission). + Created = 1, + /// The destination accepted an update to an existing incident. + Updated = 2, + /// The destination's status shows the incident approved/accepted. + Accepted = 3, + /// The destination rejected the payload (validation 422 or a REJECTED status). + Rejected = 4, + /// Network, throttling or 5xx: retry with the same idempotency key. + Transient = 5, + /// Configuration or authentication problem an operator must fix; no automatic retry. + Fatal = 6, + /// Created at the destination but still awaiting its review outcome. + Pending = 7 + } + + /// One exchange with the destination, reduced to what the submission row and the workflow triggers may see. + public class NerisSubmissionOutcome + { + public NerisOutcomeKind Kind { get; set; } + public int? StatusCode { get; set; } + public string ExternalId { get; set; } + public string ExternalStatus { get; set; } + /// Verbatim response body for the immutable artifact. + public string ResponseJson { get; set; } + /// Normalized, non-sensitive error codes and field paths. + public List Errors { get; set; } = new List(); + public string Message { get; set; } + } + + public class NerisSubmissionError + { + public string Code { get; set; } + public string FieldPath { get; set; } + public string Message { get; set; } + } + + public class NerisIncidentStatus + { + public string NerisIncidentId { get; set; } + public string Status { get; set; } + public DateTime? LastModified { get; set; } + public string CreatedBy { get; set; } + } +} diff --git a/Core/Resgrid.Model/Records/RecordsApiContracts.cs b/Core/Resgrid.Model/Records/RecordsApiContracts.cs new file mode 100644 index 00000000..b208705f --- /dev/null +++ b/Core/Resgrid.Model/Records/RecordsApiContracts.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Resgrid.Model +{ + /// + /// The v4 Records client contract (RMS plan sections 5.3, 5.4, 5.9.1): a versioned capability manifest, ETag + /// conflicts that name the changed field paths, scoped idempotency for creates and commands, a delta cursor with + /// tombstones, and resumable, checksummed attachment sessions. Web and the field apps share this contract. + /// + public static class RecordsApiContract + { + /// Bumped when a field or rule type is added that older clients cannot render; the manifest carries it. + public const string Version = "records.v1"; + + /// The minimum capability every locked Logs-parity definition needs; department definitions (RMS-1B designer) derive their own. + public const string LockedDefinitionCapability = "records.v1"; + + public const string ETagHeader = "ETag"; + public const string IfMatchHeader = "If-Match"; + public const string IdempotencyKeyHeader = "Idempotency-Key"; + public const string ClientHeader = "X-Resgrid-Client"; + + /// Weak ETag from a row version: W/"7". + public static string ToETag(long rowVersion) => $"W/\"{rowVersion}\""; + + /// Parses W/"7", "7" or 7; null when absent or malformed. + public static long? ParseETag(string value) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + var text = value.Trim(); + if (text.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) + text = text.Substring(2); + text = text.Trim('"', ' '); + return long.TryParse(text, out var parsed) && parsed >= 0 ? parsed : (long?)null; + } + } + + /// One field of a locked definition as the manifest describes it. + public class RecordFieldDescriptor + { + public string Key { get; set; } + public string Section { get; set; } + /// text, longtext, datetime, int, user, unit, bool. + public string Type { get; set; } + public bool Required { get; set; } + public bool RequiredToFinalize { get; set; } + /// Restricted-section field: withheld without RecordRestricted_View, never in search or summaries. + public bool Restricted { get; set; } + } + + /// A published definition as the capability manifest lists it (plan section 5.4). + public class RecordDefinitionDescriptor + { + public string Key { get; set; } + public int Version { get; set; } + public string Name { get; set; } + /// RmsOperationalRecordType for the locked Logs-parity definitions; null for the NERIS incident report. + public int? RecordType { get; set; } + public string RecordKind { get; set; } + public int LifecyclePreset { get; set; } + public string LifecyclePresetName { get; set; } + public string Cardinality { get; set; } + public bool Restricted { get; set; } + public string NumberPrefix { get; set; } + public bool RequiresCall { get; set; } + public bool SupportsParticipants { get; set; } + public bool SupportsUnits { get; set; } + public bool SupportsAttachments { get; set; } + public string MinimumClientCapability { get; set; } + public bool Locked { get; set; } = true; + public List Fields { get; set; } = new List(); + } + + /// + /// The locked definition catalog the manifest publishes. Field applicability mirrors the Records authoring form: + /// a narrative on every type, the type-specific columns, and the Coroner restricted section. + /// + public static class RecordDefinitionCatalog + { + public static List Describe() + { + var list = new List(); + foreach (var kv in RmsDefinitionKeys.LockedTypes) + { + var type = kv.Value; + var restricted = RmsDefinitionKeys.RestrictedClass.Contains(kv.Key); + var descriptor = new RecordDefinitionDescriptor + { + Key = kv.Key, + Version = RmsDefinitionKeys.LockedDefinitionVersion, + Name = type.ToString(), + RecordType = (int)type, + RecordKind = RmsRecordKind.Operational.ToString(), + LifecyclePreset = (int)RmsDefinitionKeys.LockedDefaultPreset, + LifecyclePresetName = RmsDefinitionKeys.LockedDefaultPreset.ToString(), + Cardinality = RmsDefinitionKeys.CardinalityFor(kv.Key).ToString(), + Restricted = restricted, + NumberPrefix = RmsDefinitionKeys.DefaultNumberPrefix(kv.Key), + RequiresCall = false, + SupportsParticipants = type != RmsOperationalRecordType.UnitActivity, + SupportsUnits = true, + SupportsAttachments = true, + MinimumClientCapability = RecordsApiContract.LockedDefinitionCapability + }; + descriptor.Fields.AddRange(FieldsFor(type)); + list.Add(descriptor); + } + + list.Add(new RecordDefinitionDescriptor + { + Key = RmsDefinitionKeys.NerisIncidentReport, + Version = RmsDefinitionKeys.LockedDefinitionVersion, + Name = "NERIS incident report", + RecordType = null, + RecordKind = RmsRecordKind.IncidentReport.ToString(), + LifecyclePreset = (int)RmsDefinitionKeys.LockedDefaultPreset, + LifecyclePresetName = RmsDefinitionKeys.LockedDefaultPreset.ToString(), + Cardinality = RmsDefinitionKeys.CardinalityFor(RmsDefinitionKeys.NerisIncidentReport).ToString(), + Restricted = false, + NumberPrefix = "INC", + RequiresCall = true, + SupportsParticipants = false, + SupportsUnits = true, + SupportsAttachments = false, + MinimumClientCapability = RecordsApiContract.LockedDefinitionCapability, + Fields = new List + { + F("IncidentNumber", "Dispatch", "text", true, true), F("CallCreatedOn", "Dispatch", "datetime", true, true), F("CallAnsweredOn", "Dispatch", "datetime", true, true), + F("CallArrivalOn", "Dispatch", "datetime", false, true), F("IncidentClearedOn", "Dispatch", "datetime"), F("DispatchIncidentCode", "Dispatch", "text"), + F("Location", "Location", "location", true, true), F("Types", "Incident", "codes", true, true), F("Units", "Units", "unit-responses", true, true), + F("Aids", "Incident", "aids"), F("Tactics", "Incident", "tactics"), F("Narrative", "Narrative", "longtext"), F("SpecialModifiers", "Incident", "codes") + } + }); + + return list; + } + + public static List FieldsFor(RmsOperationalRecordType type) + { + var fields = new List + { + F("StartedOn", "Record", "datetime"), F("EndedOn", "Record", "datetime"), F("StationGroupId", "Record", "group"), F("ExternalId", "Record", "text"), + F("Narrative", "Details", "longtext", false, true) + }; + switch (type) + { + case RmsOperationalRecordType.Training: + fields.AddRange(new[] { F("Course", "Details", "text"), F("CourseCode", "Details", "text"), F("Instructors", "Details", "text") }); + break; + case RmsOperationalRecordType.Meeting: + fields.AddRange(new[] { F("Type", "Details", "text"), F("Facilitator", "Details", "text") }); + break; + case RmsOperationalRecordType.Run: + case RmsOperationalRecordType.Callback: + fields.AddRange(new[] { F("CallId", "Record", "call"), F("InitialReport", "Details", "text"), F("Cause", "Details", "text"), F("InvestigatedByUserId", "Details", "user"), F("OtherAgencies", "Details", "text"), F("OtherUnits", "Details", "text") }); + break; + case RmsOperationalRecordType.UnitActivity: + fields.AddRange(new[] { F("UnitId", "Details", "unit", true, true), F("ActivityOn", "Details", "datetime", false, true) }); + break; + case RmsOperationalRecordType.Coroner: + fields.AddRange(new[] + { + F("CaseNumber", "Restricted", "text", false, false, true), F("PronouncedDeceasedBy", "Restricted", "text", false, false, true), F("BodyLocation", "Restricted", "text", false, false, true), + F("Destination", "Restricted", "text", false, false, true), F("OtherPersonnel", "Restricted", "text", false, false, true), F("ContactName", "Restricted", "text"), F("ContactNumber", "Restricted", "text") + }); + break; + } + if (type != RmsOperationalRecordType.Coroner) + fields.AddRange(new[] { F("Location", "Details", "text"), F("OtherPersonnel", "Details", "text") }); + return fields; + } + + private static RecordFieldDescriptor F(string key, string section, string type, bool required = false, bool requiredToFinalize = false, bool restricted = false) + { + return new RecordFieldDescriptor { Key = key, Section = section, Type = type, Required = required, RequiredToFinalize = requiredToFinalize, Restricted = restricted }; + } + } + + /// Draft save refused because the client's row version is stale: what changed, so the client reconciles deliberately (plan section 5.3). + public class RecordDraftConflict + { + public string RecordId { get; set; } + public long ExpectedRowVersion { get; set; } + public long CurrentRowVersion { get; set; } + public RmsRecordState CurrentState { get; set; } + public string CurrentRevisionId { get; set; } + public List ChangedFieldPaths { get; set; } = new List(); + } + + public enum RecordUploadSessionState + { + Open = 1, + Completed = 2, + Aborted = 3 + } + + /// A resumable attachment upload (plan section 5.3): declared size and SHA-256 up front, chunks in order, finalize only after checksum, hygiene and scanning pass. + public class RecordAttachmentUploadSession + { + public string UploadId { get; set; } + public int DepartmentId { get; set; } + public string RecordId { get; set; } + public string UserId { get; set; } + public string FileName { get; set; } + public string ContentType { get; set; } + public long DeclaredSize { get; set; } + /// Lower-case hex SHA-256 of the whole file, declared by the client and verified at completion. + public string Sha256 { get; set; } + public long ReceivedBytes { get; set; } + public int ChunkSize { get; set; } + public int ChunkCount { get; set; } + public RecordUploadSessionState State { get; set; } = RecordUploadSessionState.Open; + public string AttachmentId { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ExpiresOn { get; set; } + public bool IsComplete => ReceivedBytes >= DeclaredSize; + } + + public class RecordUploadSessionException : InvalidOperationException + { + public RecordUploadSessionException(string code, string message) : base(message) + { + Code = code; + } + + /// not_found, expired, closed, bad_offset, too_large, checksum_mismatch, incomplete, rejected. + public string Code { get; } + } +} + +namespace Resgrid.Model.Services +{ + using System.Threading; + using System.Threading.Tasks; + + /// Short-lived, keyed state behind the v4 Records contract (upload sessions, command idempotency). Redis when caching is on; process memory otherwise. + public interface IRecordsApiStateStore + { + Task GetAsync(string key); + Task SetAsync(string key, string value, TimeSpan timeToLive); + Task RemoveAsync(string key); + } + + /// Resumable, checksummed attachment sessions for Records (plan section 5.3). + public interface IRecordAttachmentUploadService + { + int ChunkSize { get; } + Task BeginAsync(int departmentId, string userId, string recordId, string fileName, string contentType, long declaredSize, string sha256); + Task GetAsync(int departmentId, string userId, string uploadId); + Task AppendAsync(int departmentId, string userId, string uploadId, long offset, byte[] data); + /// Verifies size and SHA-256, then stores the attachment through the same hygiene/scanner path as a direct upload. + Task CompleteAsync(int departmentId, string userId, string uploadId, string description, CancellationToken cancellationToken = default); + Task AbortAsync(int departmentId, string userId, string uploadId); + } + + /// Scoped idempotency for v4 Records commands: the same (department, user, key) replays the first outcome instead of re-running the transition. + public interface IRecordsApiIdempotencyService + { + Task TryGetRecordIdAsync(int departmentId, string userId, string idempotencyKey); + Task RememberAsync(int departmentId, string userId, string idempotencyKey, string recordId); + } +} diff --git a/Core/Resgrid.Model/Records/RmsIncidentReport.cs b/Core/Resgrid.Model/Records/RmsIncidentReport.cs new file mode 100644 index 00000000..2aacdcd9 --- /dev/null +++ b/Core/Resgrid.Model/Records/RmsIncidentReport.cs @@ -0,0 +1,499 @@ +using System; +using System.Collections.Generic; +using ProtoBuf; + +namespace Resgrid.Model +{ + /// + /// NERIS incident report aggregate root (RMS plan sections 4.2, 5.2, RMS-2; registry M0164). One per + /// (DepartmentId, CallId, ReportingEntityId, DefinitionKey) — the SingleAuthoritative cardinality of + /// section 5.2.1: one official incident report per responding agency, never one per company. Shares the + /// lifecycle vocabulary of RmsOperationalRecord (RmsRecordState, RmsLifecycle) and the reference shape + /// (DepartmentId, RecordId, RecordKind = IncidentReport, RevisionId) that RmsRevision, RmsAccessAudit, + /// RmsSubmission and RmsSignature key on. Times live on the typed rows below; every prefilled value's + /// provenance is an RmsSourceFact. + /// + [ProtoContract] + public class RmsIncidentReport : IEntity + { + [ProtoMember(1)] + public string RmsIncidentReportId { get; set; } + + [ProtoMember(2)] + public int DepartmentId { get; set; } + + [ProtoMember(3)] + public string ProtectionId { get; set; } + + [ProtoMember(4)] + public int CallId { get; set; } + + /// The department's NERIS entity ID at creation, or a department placeholder before a profile exists. + [ProtoMember(5)] + public string ReportingEntityId { get; set; } + + [ProtoMember(6)] + public string DefinitionKey { get; set; } + + [ProtoMember(7)] + public int DefinitionVersion { get; set; } + + /// Pinned NERIS contract version (value sets and payload shape) this report was authored against. + [ProtoMember(8)] + public string ProfileVersion { get; set; } + + [ProtoMember(9)] + public int LifecyclePreset { get; set; } + + /// , including the reporting-destination states. + [ProtoMember(10)] + public int State { get; set; } + + [ProtoMember(11)] + public string RecordNumber { get; set; } + + [ProtoMember(12)] + public string DraftReference { get; set; } + + /// Department incident number sent to NERIS (Call.Number unless the author overrides it). + [ProtoMember(13)] + public string IncidentNumber { get; set; } + + [ProtoMember(14)] + public string DisplaySummary { get; set; } + + [ProtoMember(15)] + public int? StationGroupId { get; set; } + + [ProtoMember(16)] + public string AuthorUserId { get; set; } + + [ProtoMember(17)] + public string OwnerUserId { get; set; } + + [ProtoMember(18)] + public string ReviewerUserId { get; set; } + + [ProtoMember(19)] + public string ApproverUserId { get; set; } + + [ProtoMember(20)] + public DateTime? ReviewDueOn { get; set; } + + [ProtoMember(21)] + public DateTime? SubmittedForReviewOn { get; set; } + + [ProtoMember(22)] + public DateTime? ReturnedOn { get; set; } + + [ProtoMember(23)] + public string ReturnReasonCode { get; set; } + + [ProtoMember(24)] + public string ReturnReasonText { get; set; } + + [ProtoMember(25)] + public int ReturnCount { get; set; } + + [ProtoMember(26)] + public DateTime? ApprovedOn { get; set; } + + [ProtoMember(27)] + public DateTime? FinalizedOn { get; set; } + + [ProtoMember(28)] + public string FinalizedByUserId { get; set; } + + [ProtoMember(29)] + public string CurrentRevisionId { get; set; } + + [ProtoMember(30)] + public int RevisionCount { get; set; } + + [ProtoMember(31)] + public string AmendsRevisionId { get; set; } + + [ProtoMember(32)] + public DateTime? VoidedOn { get; set; } + + [ProtoMember(33)] + public string VoidedByUserId { get; set; } + + [ProtoMember(34)] + public string VoidReasonCode { get; set; } + + [ProtoMember(35)] + public string VoidReasonText { get; set; } + + [ProtoMember(36)] + public DateTime? CancelledOn { get; set; } + + [ProtoMember(37)] + public string CancelledByUserId { get; set; } + + /// NERIS-assigned incident ID once the first create succeeded; later submissions update it in place. + [ProtoMember(38)] + public string NerisIncidentId { get; set; } + + [ProtoMember(39)] + public string LastSubmissionId { get; set; } + + /// of the latest submission, denormalized for queues. + [ProtoMember(40)] + public int? LastSubmissionState { get; set; } + + [ProtoMember(41)] + public DateTime? LastSubmittedOn { get; set; } + + [ProtoMember(42)] + public DateTime? AcceptedOn { get; set; } + + [ProtoMember(43)] + public DateTime? RejectedOn { get; set; } + + /// Normalized, non-sensitive summary of the last rejection (codes and field paths, never payloads). + [ProtoMember(44)] + public string RejectionSummary { get; set; } + + // Dispatch-level facts (NERIS dispatch.*): prefilled from the Call with provenance rows, editable in Draft. + [ProtoMember(53)] + public DateTime? CallCreatedOn { get; set; } + + [ProtoMember(54)] + public DateTime? CallAnsweredOn { get; set; } + + /// First unit on scene (NERIS call_arrival). + [ProtoMember(55)] + public DateTime? CallArrivalOn { get; set; } + + [ProtoMember(56)] + public DateTime? IncidentClearedOn { get; set; } + + [ProtoMember(57)] + public string DispatchCenterId { get; set; } + + [ProtoMember(58)] + public string DeterminantCode { get; set; } + + /// The CAD/Resgrid incident code as dispatched (Call.Type), kept verbatim beside the mapped incident type. + [ProtoMember(59)] + public string DispatchIncidentCode { get; set; } + + [ProtoMember(60)] + public string Disposition { get; set; } + + [ProtoMember(61)] + public bool? PeoplePresent { get; set; } + + [ProtoMember(62)] + public int? DisplacementCount { get; set; } + + [ProtoMember(63)] + public int? AnimalsRescued { get; set; } + + /// Comma-separated NERIS special_modifiers codes. + [ProtoMember(64)] + public string SpecialModifiersCsv { get; set; } + + [ProtoMember(45)] + public string IdempotencyKey { get; set; } + + [ProtoMember(46)] + public int OriginClient { get; set; } + + [ProtoMember(47)] + public DateTime CreatedOn { get; set; } + + [ProtoMember(48)] + public string CreatedByUserId { get; set; } + + [ProtoMember(49)] + public DateTime ModifiedOn { get; set; } + + [ProtoMember(50)] + public string ModifiedByUserId { get; set; } + + [ProtoMember(51)] + public long RowVersion { get; set; } + + [ProtoMember(52)] + public DateTime? DeletedOn { get; set; } + + public string RecordId => RmsIncidentReportId; + + public RmsRecordKind RecordKind => RmsRecordKind.IncidentReport; + + public object IdValue + { + get => RmsIncidentReportId; + set => RmsIncidentReportId = (string)value; + } + + public string TableName => "RmsIncidentReports"; + + public string IdName => "RmsIncidentReportId"; + + public int IdType => 1; + + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "RecordId", "RecordKind" }; + } + + /// + /// Provenance ledger for every prefilled value (plan section 4.2 "Timestamp authority"): where the value came + /// from, what it was when imported, and what the author changed it to. Typed rows hold the current value; + /// this row keeps the SourceKind and the original so provenance is never lost. + /// + public class RmsSourceFact : IEntity + { + public string RmsSourceFactId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string RecordId { get; set; } + /// Null for the working draft; the revision copy on finalize. + public string RevisionId { get; set; } + /// Stable key such as dispatch.call_create, unit.{unitId}.on_scene, location.address. + public string FactKey { get; set; } + /// . + public int SourceKind { get; set; } + public string SourceSystem { get; set; } + public string SourceEntityType { get; set; } + public string SourceEntityId { get; set; } + /// The value as imported (ISO-8601 for times), retained verbatim. + public string SourceValue { get; set; } + /// The value currently on the typed row; differs from SourceValue once the author edited it. + public string CurrentValue { get; set; } + public DateTime? SourceTime { get; set; } + public DateTime ImportedOn { get; set; } + public DateTime? CorrectedOn { get; set; } + public string CorrectedByUserId { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + + public object IdValue { get => RmsSourceFactId; set => RmsSourceFactId = (string)value; } + public string TableName => "RmsSourceFacts"; + public string IdName => "RmsSourceFactId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } + + /// Unit response on an incident report (NERIS unit_responses), with per-unit times and their provenance. + public class RmsUnitResponse : IEntity + { + public string RmsUnitResponseId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string RecordId { get; set; } + public string RevisionId { get; set; } + public int? UnitId { get; set; } + public string UnitNameSnapshot { get; set; } + public string UnitTypeSnapshot { get; set; } + public string UnitNerisId { get; set; } + public int? StationGroupIdSnapshot { get; set; } + public int? Staffing { get; set; } + public bool UnableToDispatch { get; set; } + public DateTime? DispatchedOn { get; set; } + public DateTime? EnrouteOn { get; set; } + public DateTime? OnSceneOn { get; set; } + public DateTime? CanceledEnrouteOn { get; set; } + public DateTime? StagingOn { get; set; } + public DateTime? ClearedOn { get; set; } + /// NERIS response_mode: EMERGENT or NON_EMERGENT. + public string ResponseMode { get; set; } + public string TransportMode { get; set; } + /// of the prefilled times. + public int TimesSourceKind { get; set; } + public string Disposition { get; set; } + public int Ordinal { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + + public object IdValue { get => RmsUnitResponseId; set => RmsUnitResponseId = (string)value; } + public string TableName => "RmsUnitResponses"; + public string IdName => "RmsUnitResponseId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } + + /// Versioned NERIS incident type assignment; LocalCode keeps the original Resgrid call type beside the mapped value. + public class RmsIncidentType : IEntity + { + public string RmsIncidentTypeId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string RecordId { get; set; } + public string RevisionId { get; set; } + /// NERIS incident_type value, e.g. FIRE||STRUCTURE_FIRE||... + public string TypeCode { get; set; } + public bool IsPrimary { get; set; } + public string LocalCode { get; set; } + public string ValueSetVersion { get; set; } + public int Ordinal { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + + public object IdValue { get => RmsIncidentTypeId; set => RmsIncidentTypeId = (string)value; } + public string TableName => "RmsIncidentTypes"; + public string IdName => "RmsIncidentTypeId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } + + /// Action/tactic taken on the incident (NERIS actions_tactics), with actor, time, and provenance. + public class RmsActionTactic : IEntity + { + public string RmsActionTacticId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string RecordId { get; set; } + public string RevisionId { get; set; } + public string TacticCode { get; set; } + public int? ActorUnitId { get; set; } + public DateTime? OccurredOn { get; set; } + public int SourceKind { get; set; } + public string Outcome { get; set; } + public int Ordinal { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + + public object IdValue { get => RmsActionTacticId; set => RmsActionTacticId = (string)value; } + public string TableName => "RmsActionTactics"; + public string IdName => "RmsActionTacticId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } + + /// Aid given or received on the single report (plan section 5.2.1: never a fabricated counterpart report). + public class RmsAid : IEntity + { + public string RmsAidId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string RecordId { get; set; } + public string RevisionId { get; set; } + /// GIVEN or RECEIVED. + public string Direction { get; set; } + /// ACTING_AS_AID, IN_LIEU_AID or SUPPORT_AID. + public string AidType { get; set; } + public string CounterpartNerisId { get; set; } + public string CounterpartName { get; set; } + public bool IsNonFireDepartment { get; set; } + public string NonFdType { get; set; } + public int Ordinal { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + + public object IdValue { get => RmsAidId; set => RmsAidId = (string)value; } + public string TableName => "RmsAids"; + public string IdName => "RmsAidId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } + + /// Structured incident location snapshot (NERIS base.location) plus the geopoint. + public class RmsLocation : IEntity + { + public string RmsLocationId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string RecordId { get; set; } + public string RevisionId { get; set; } + public string AddressText { get; set; } + public string Number { get; set; } + public string NumberPrefix { get; set; } + public string NumberSuffix { get; set; } + public string Street { get; set; } + public string UnitValue { get; set; } + public string Municipality { get; set; } + public string County { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string PlaceType { get; set; } + public string LocationUse { get; set; } + public string CrossStreet1 { get; set; } + public string CrossStreet2 { get; set; } + public decimal? Latitude { get; set; } + public decimal? Longitude { get; set; } + public string Jurisdiction { get; set; } + public int SourceKind { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + + public object IdValue { get => RmsLocationId; set => RmsLocationId = (string)value; } + public string TableName => "RmsLocations"; + public string IdName => "RmsLocationId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } + + /// Authored narrative sections; a protected-candidate holder, so it carries the inert envelope columns. + public class RmsNarrative : IEntity + { + public string RmsNarrativeId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string RecordId { get; set; } + public string RevisionId { get; set; } + public string Narrative { get; set; } + public string ImpedimentNarrative { get; set; } + public string OutcomeNarrative { get; set; } + /// Supplemental department questions captured on the report; never enters the submission payload. + public string SupplementalJson { get; set; } + public bool IsProtected { get; set; } + public string ProtectedEnvelope { get; set; } + public int ProtectedCatalogVersion { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + + public object IdValue { get => RmsNarrativeId; set => RmsNarrativeId = (string)value; } + public string TableName => "RmsNarratives"; + public string IdName => "RmsNarrativeId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } + + public enum RmsValidationSeverity + { + Error = 1, + Warning = 2, + Info = 3 + } + + public enum RmsValidationSource + { + Local = 1, + Destination = 2 + } + + /// One validation finding against a profile/rule version; replaced on every validation run. + public class RmsValidationIssue : IEntity + { + public string RmsValidationIssueId { get; set; } + public int DepartmentId { get; set; } + public string RecordId { get; set; } + public string RevisionId { get; set; } + public string ProfileVersion { get; set; } + public string RuleKey { get; set; } + /// . + public int Severity { get; set; } + public string FieldPath { get; set; } + public string Message { get; set; } + /// . + public int Source { get; set; } + public DateTime? ResolvedOn { get; set; } + public DateTime CreatedOn { get; set; } + + public object IdValue { get => RmsValidationIssueId; set => RmsValidationIssueId = (string)value; } + public string TableName => "RmsValidationIssues"; + public string IdName => "RmsValidationIssueId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/Records/RmsLifecycle.cs b/Core/Resgrid.Model/Records/RmsLifecycle.cs index 554642b5..296fcc62 100644 --- a/Core/Resgrid.Model/Records/RmsLifecycle.cs +++ b/Core/Resgrid.Model/Records/RmsLifecycle.cs @@ -106,6 +106,9 @@ public static bool IsEditable(RmsRecordState state) case RmsRecordState.Amended: return WorkflowTriggerEventType.RecordAmended; case RmsRecordState.Voided: return WorkflowTriggerEventType.RecordVoided; case RmsRecordState.Cancelled: return WorkflowTriggerEventType.RecordCancelled; + case RmsRecordState.Submitted: return WorkflowTriggerEventType.RecordSubmissionQueued; + case RmsRecordState.Accepted: return WorkflowTriggerEventType.RecordSubmissionAccepted; + case RmsRecordState.Rejected: return WorkflowTriggerEventType.RecordSubmissionRejected; default: return null; } } diff --git a/Core/Resgrid.Model/Records/RmsNerisProfile.cs b/Core/Resgrid.Model/Records/RmsNerisProfile.cs new file mode 100644 index 00000000..bfe20117 --- /dev/null +++ b/Core/Resgrid.Model/Records/RmsNerisProfile.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; + +namespace Resgrid.Model +{ + public static class NerisEnvironments + { + public const string Production = "production"; + public const string Sandbox = "sandbox"; + } + + public static class NerisGrantTypes + { + public const string ClientCredentials = "client_credentials"; + public const string Password = "password"; + } + + /// + /// A department's NERIS reporting profile (registry M0166): its NERIS entity ID, environment, and integration + /// credential (encrypted per department through IEncryptionService). Records is useful without one (plan + /// decision 21); the profile only turns on submission. + /// + public class RmsNerisProfile : IEntity + { + public string RmsNerisProfileId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + /// NERIS entity ID (neris_id_entity) the department files under. + public string NerisEntityId { get; set; } + public string EntityName { get; set; } + /// . + public string Environment { get; set; } + /// Optional base URL override (sandbox or a test double); the config default otherwise. + public string BaseUrlOverride { get; set; } + /// . + public string GrantType { get; set; } + /// Department-encrypted JSON: {"client_id","client_secret"} or {"username","password"}. + public string EncryptedCredentialJson { get; set; } + /// Pinned contract version the department maps against. + public string ContractVersion { get; set; } + public bool AutoSubmitOnFinalize { get; set; } + public bool IsEnabled { get; set; } + public DateTime? LastTokenIssuedOn { get; set; } + public DateTime? LastSuccessfulCallOn { get; set; } + public string LastError { get; set; } + public string UpdatedByUserId { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + + public bool HasCredential => !string.IsNullOrWhiteSpace(EncryptedCredentialJson); + + public object IdValue { get => RmsNerisProfileId; set => RmsNerisProfileId = (string)value; } + public string TableName => "RmsNerisProfiles"; + public string IdName => "RmsNerisProfileId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName", "HasCredential" }; + } + + /// One code of one NERIS value set at one contract version (seeded from the pinned snapshot; global reference data). + public class RmsNerisValueSetEntry : IEntity + { + public int RmsNerisValueSetEntryId { get; set; } + public string ContractVersion { get; set; } + /// Snapshot set key, e.g. incident_type, action_tactic, aid_type. + public string SetKey { get; set; } + public string Code { get; set; } + public string Label { get; set; } + public string ParentCode { get; set; } + public int SortOrder { get; set; } + public bool IsRetired { get; set; } + public DateTime CreatedOn { get; set; } + + public object IdValue { get => RmsNerisValueSetEntryId; set => RmsNerisValueSetEntryId = (int)value; } + public string TableName => "RmsNerisValueSets"; + public string IdName => "RmsNerisValueSetEntryId"; + public int IdType => 0; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } + + public static class NerisCrosswalkSources + { + public const string CallType = "CallType"; + public const string CallPriority = "CallPriority"; + public const string UnitType = "UnitType"; + } + + /// Department-owned crosswalk from a Resgrid/CAD code to a NERIS value; the original code is always kept beside the mapped one. + public class RmsNerisCrosswalk : IEntity + { + public string RmsNerisCrosswalkId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string ContractVersion { get; set; } + public string SetKey { get; set; } + /// . + public string LocalSource { get; set; } + public string LocalCode { get; set; } + public string NerisCode { get; set; } + public bool IsDefault { get; set; } + public string CreatedByUserId { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + public DateTime? DeletedOn { get; set; } + + public object IdValue { get => RmsNerisCrosswalkId; set => RmsNerisCrosswalkId = (string)value; } + public string TableName => "RmsNerisCrosswalks"; + public string IdName => "RmsNerisCrosswalkId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/Records/RmsSubmission.cs b/Core/Resgrid.Model/Records/RmsSubmission.cs new file mode 100644 index 00000000..523175cb --- /dev/null +++ b/Core/Resgrid.Model/Records/RmsSubmission.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; + +namespace Resgrid.Model +{ + public enum RmsSubmissionState + { + Queued = 0, + InFlight = 1, + Accepted = 2, + Rejected = 3, + /// Delivery exhausted its retries or needs an operator; the record is not silently dropped. + Failed = 4, + /// A newer revision issued a new idempotency key; this submission is history only. + Superseded = 5, + /// Created at the destination and awaiting its review outcome (NERIS SUBMITTED / PENDING_APPROVAL). + AwaitingDestination = 6 + } + + public static class RmsSubmissionDestinations + { + public const string Neris = "NERIS"; + } + + /// + /// One outbound exchange with a reporting destination (plan section 5.3: every exact payload and response is + /// an immutable, checksummed artifact; retries reuse the idempotency key; an amendment issues a new one). Worker + /// 41 (RmsSubmissionCommand) claims Queued/AwaitingDestination rows with a lease and never holds a database + /// transaction across the destination call. + /// + public class RmsSubmission : IEntity + { + public string RmsSubmissionId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string RecordId { get; set; } + /// . + public int RecordKind { get; set; } + public string RevisionId { get; set; } + public string Destination { get; set; } + public string DestinationVersion { get; set; } + /// Scoped idempotency key: one per (record, revision); reused on every retry. + public string IdempotencyKey { get; set; } + /// . + public int State { get; set; } + public int Attempts { get; set; } + public int MaxAttempts { get; set; } + public DateTime? NextAttemptOn { get; set; } + public string LeaseOwner { get; set; } + public DateTime? LeaseExpiresOn { get; set; } + /// The exact outbound payload, immutable once queued. + public string PayloadJson { get; set; } + public string PayloadChecksum { get; set; } + /// The last destination response body, stored verbatim for audit; never surfaced to workflows or notifications. + public string ResponseJson { get; set; } + public string ResponseChecksum { get; set; } + public int? ResponseStatusCode { get; set; } + /// Destination identifier (NERIS incident ID). + public string ExternalId { get; set; } + public string ExternalStatus { get; set; } + /// Normalized, non-sensitive error codes and field paths; what workflows and notifications may see. + public string ErrorSummary { get; set; } + public DateTime QueuedOn { get; set; } + public DateTime? SentOn { get; set; } + public DateTime? CompletedOn { get; set; } + public string CreatedByUserId { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + + public object IdValue { get => RmsSubmissionId; set => RmsSubmissionId = (string)value; } + public string TableName => "RmsSubmissions"; + public string IdName => "RmsSubmissionId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } + + public enum RmsSignatureIntent + { + Attestation = 1, + Review = 2, + Approval = 3 + } + + public enum RmsSignatureMethod + { + WebAttestation = 1 + } + + /// Officer attestation captured at finalize: signer, role, intent, statement version, method, time, and the revision checksum it binds to. + public class RmsSignature : IEntity + { + public string RmsSignatureId { get; set; } + public int DepartmentId { get; set; } + public string ProtectionId { get; set; } + public string RecordId { get; set; } + public int RecordKind { get; set; } + public string RevisionId { get; set; } + public string SignerUserId { get; set; } + public string SignerNameSnapshot { get; set; } + public string SignerRoleSnapshot { get; set; } + /// . + public int Intent { get; set; } + public string StatementVersion { get; set; } + public string StatementText { get; set; } + /// . + public int Method { get; set; } + public DateTime SignedOn { get; set; } + public string IpAddress { get; set; } + /// SHA-256 of the revision snapshot the signature covers. + public string ArtifactChecksum { get; set; } + public DateTime CreatedOn { get; set; } + public long RowVersion { get; set; } + + public object IdValue { get => RmsSignatureId; set => RmsSignatureId = (string)value; } + public string TableName => "RmsSignatures"; + public string IdName => "RmsSignatureId"; + public int IdType => 1; + public IEnumerable IgnoredProperties => new string[] { "IdValue", "IdType", "TableName", "IdName" }; + } +} diff --git a/Core/Resgrid.Model/Repositories/IRmsIncidentRepositories.cs b/Core/Resgrid.Model/Repositories/IRmsIncidentRepositories.cs new file mode 100644 index 00000000..ae9c2a20 --- /dev/null +++ b/Core/Resgrid.Model/Repositories/IRmsIncidentRepositories.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Resgrid.Model.Repositories +{ + /// Paging/filter for the incident report queue (Records Index for RecordKind = IncidentReport). + public sealed class RmsIncidentReportQuery + { + public IList States { get; set; } + public int? Year { get; set; } + public int? CallId { get; set; } + public string OwnerUserId { get; set; } + public int? StationGroupId { get; set; } + public int Skip { get; set; } + public int Take { get; set; } = 50; + } + + public interface IRmsIncidentReportsRepository : IRepository + { + Task GetByIdForDepartmentAsync(int departmentId, string reportId); + /// The department's own report for a Call (SingleAuthoritative, plan 5.2.1); null when none exists. + Task GetByCallAsync(int departmentId, int callId, string reportingEntityId); + Task> GetByCallAnyEntityAsync(int departmentId, int callId); + Task GetByIdempotencyKeyAsync(int departmentId, string idempotencyKey); + Task GetByNerisIncidentIdAsync(int departmentId, string nerisIncidentId); + Task> QueryAsync(int departmentId, RmsIncidentReportQuery query); + Task CountAsync(int departmentId, RmsIncidentReportQuery query); + Task> GetYearsAsync(int departmentId); + Task GetMaxRecordNumberSequenceAsync(int departmentId, string numberPrefix); + Task TryBumpRowVersionAsync(int departmentId, string reportId, long expectedRowVersion, CancellationToken cancellationToken = default); + } + + /// Shared contract of every per-report child row set: a working draft (RevisionId null) and immutable revision copies. + public interface IRmsIncidentChildRepository : IRepository where T : class, IEntity + { + Task> GetForRecordAsync(int departmentId, string recordId, string revisionId); + Task DeleteDraftForRecordAsync(int departmentId, string recordId, CancellationToken cancellationToken = default); + } + + public interface IRmsSourceFactsRepository : IRmsIncidentChildRepository { } + public interface IRmsUnitResponsesRepository : IRmsIncidentChildRepository { } + public interface IRmsIncidentTypesRepository : IRmsIncidentChildRepository { } + public interface IRmsActionTacticsRepository : IRmsIncidentChildRepository { } + public interface IRmsAidsRepository : IRmsIncidentChildRepository { } + public interface IRmsLocationsRepository : IRmsIncidentChildRepository { } + public interface IRmsNarrativesRepository : IRmsIncidentChildRepository { } + + public interface IRmsValidationIssuesRepository : IRepository + { + Task> GetForRecordAsync(int departmentId, string recordId); + /// Replaces every open issue of one source for the record (a validation run is a whole answer, not a delta). + Task ReplaceForRecordAsync(int departmentId, string recordId, RmsValidationSource source, IEnumerable issues, CancellationToken cancellationToken = default); + } + + public interface IRmsSubmissionsRepository : IRepository + { + Task GetByIdForDepartmentAsync(int departmentId, string submissionId); + Task> GetForRecordAsync(int departmentId, string recordId); + Task GetByIdempotencyKeyAsync(string idempotencyKey); + /// Leases due Queued/AwaitingDestination submissions across departments for one worker sweep (plan 5.3: no transaction across the destination call). + Task> ClaimDueBatchAsync(string leaseOwner, TimeSpan leaseDuration, int batchSize, DateTime utcNow, CancellationToken cancellationToken = default); + Task CountByStateAsync(int state); + /// Marks every non-terminal submission of the record Superseded (a new revision issued a new idempotency key). + Task SupersedeOpenForRecordAsync(int departmentId, string recordId, string exceptSubmissionId, DateTime utcNow, CancellationToken cancellationToken = default); + } + + public interface IRmsSignaturesRepository : IRepository + { + Task> GetForRecordAsync(int departmentId, string recordId); + Task GetForRevisionAsync(int departmentId, string revisionId, RmsSignatureIntent intent); + } + + public interface IRmsNerisProfilesRepository : IRepository + { + Task GetByDepartmentIdAsync(int departmentId); + } + + public interface IRmsNerisValueSetsRepository : IRepository + { + Task> GetSetAsync(string contractVersion, string setKey); + Task CountForVersionAsync(string contractVersion); + Task ExistsAsync(string contractVersion, string setKey, string code); + } + + public interface IRmsNerisCrosswalksRepository : IRepository + { + Task> GetForDepartmentAsync(int departmentId, string contractVersion); + Task GetAsync(int departmentId, string contractVersion, string setKey, string localSource, string localCode); + } +} diff --git a/Core/Resgrid.Model/Services/IIncidentReportsService.cs b/Core/Resgrid.Model/Services/IIncidentReportsService.cs new file mode 100644 index 00000000..7348ccd7 --- /dev/null +++ b/Core/Resgrid.Model/Services/IIncidentReportsService.cs @@ -0,0 +1,85 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Resgrid.Model.Repositories; + +namespace Resgrid.Model.Services +{ + /// + /// NERIS incident report lifecycle (RMS plan sections 4.2, 5.2.1, 5.3; RMS-2): start from a Call with source- + /// aware prefill, draft saves, local/destination validation, review, finalization with attestation and an + /// immutable revision, submission queueing, correction after rejection, amendment, void and cancel. One + /// report per responding entity per Call; a second start returns the existing report. + /// + public interface IIncidentReportsService + { + Task StartFromCallAsync(int departmentId, string userId, int callId, RmsOriginClient origin = RmsOriginClient.Web, CancellationToken cancellationToken = default); + + Task GetAsync(int departmentId, string reportId, bool includeHistory = false); + + Task GetForCallAsync(int departmentId, int callId); + + Task SaveDraftAsync(int departmentId, string userId, string reportId, long expectedRowVersion, IncidentReportDraftInput input, CancellationToken cancellationToken = default); + + /// Runs local validation (and the destination's validate endpoint when asked and configured) and stores the issues on the report. + Task> ValidateAsync(int departmentId, string reportId, bool includeDestination, CancellationToken cancellationToken = default); + + Task SubmitForReviewAsync(int departmentId, string userId, string reportId, long expectedRowVersion, CancellationToken cancellationToken = default); + + Task ReturnForCorrectionAsync(int departmentId, string userId, string reportId, string reasonCode, string reasonText, CancellationToken cancellationToken = default); + + /// Validates, writes the revision and attestation signature, queues the submission when the profile allows, and emits the lifecycle events. + Task FinalizeAsync(int departmentId, string userId, string reportId, long expectedRowVersion, string attestationStatementVersion, string ipAddress, string reasonCode, string reasonText, CancellationToken cancellationToken = default); + + /// After a rejection: a new revision (N+1 referencing N) and a new idempotency key, then back into the submission queue. + Task CorrectAndResubmitAsync(int departmentId, string userId, string reportId, long expectedRowVersion, string attestationStatementVersion, string ipAddress, string reasonCode, string reasonText, CancellationToken cancellationToken = default); + + Task OpenAmendmentAsync(int departmentId, string userId, string reportId, CancellationToken cancellationToken = default); + + Task AbandonAmendmentAsync(int departmentId, string userId, string reportId, CancellationToken cancellationToken = default); + + /// Queues (or re-queues) the current revision for the destination; used when auto-submit is off or after a failed delivery. + Task QueueSubmissionAsync(int departmentId, string userId, string reportId, CancellationToken cancellationToken = default); + + Task VoidAsync(int departmentId, string userId, string reportId, string reasonCode, string reasonText, CancellationToken cancellationToken = default); + + Task CancelAsync(int departmentId, string userId, string reportId, CancellationToken cancellationToken = default); + + Task> QueryAsync(int departmentId, RmsIncidentReportQuery query); + + Task CountAsync(int departmentId, RmsIncidentReportQuery query); + + Task> GetYearsAsync(int departmentId); + + /// The snapshot the mapper consumes, from the draft rows or from a revision's copies. + Task BuildSnapshotAsync(int departmentId, string reportId, string revisionId = null); + + Task RecordAccessAsync(int departmentId, string userId, string reportId, string revisionId, RmsAccessAuditAction action, string purpose = null, string ipAddress = null); + } + + /// Result of one worker sweep over the submission queue. + public class RecordsSubmissionSweepResult + { + public int Claimed { get; set; } + public int Delivered { get; set; } + public int Accepted { get; set; } + public int Rejected { get; set; } + public int Deferred { get; set; } + public int Failed { get; set; } + public int Errors { get; set; } + public string Message { get; set; } + } + + /// + /// Worker 41 (RmsSubmissionCommand): claims due submissions, talks to the destination outside any database + /// transaction, persists the immutable response artifact, moves the report through Submitted / Accepted / + /// Rejected, retries transient failures with backoff, and emits triggers 108–111 plus notification 33. + /// + public interface IRecordsSubmissionService + { + Task SweepAsync(CancellationToken cancellationToken = default); + + /// Processes one claimed submission; public so a single delivery can be driven and tested directly. + Task ProcessAsync(RmsSubmission submission, CancellationToken cancellationToken = default); + } +} diff --git a/Core/Resgrid.Model/Services/IRecordsCutoverService.cs b/Core/Resgrid.Model/Services/IRecordsCutoverService.cs index e42c499b..2fad0846 100644 --- a/Core/Resgrid.Model/Services/IRecordsCutoverService.cs +++ b/Core/Resgrid.Model/Services/IRecordsCutoverService.cs @@ -28,13 +28,13 @@ public interface IRecordsCutoverService /// (registry section 4.6), ViewGroupRecords row when the administrator chose LockToGroup, history /// event and access audit. Refuses when the flag is off, preflight blocks, or already activated. /// - Task ActivateAsync(int departmentId, string userId, string reason, bool viewGroupRecordsLockToGroup, CancellationToken cancellationToken = default); + Task ActivateAsync(int departmentId, string userId, string reason, bool viewGroupRecordsLockToGroup, string ipAddress = null, CancellationToken cancellationToken = default); /// Which rollback outcome the decision frame permits right now. Task GetRollbackOutcomeAsync(int departmentId); /// Clean revert only (zero Records rows since activation); anything else returns a failure naming the outcome. - Task RevertAsync(int departmentId, string userId, string reason, CancellationToken cancellationToken = default); + Task RevertAsync(int departmentId, string userId, string reason, string ipAddress = null, CancellationToken cancellationToken = default); Task InvalidateCacheAsync(int departmentId); } diff --git a/Core/Resgrid.Model/Services/IRecordsNotificationService.cs b/Core/Resgrid.Model/Services/IRecordsNotificationService.cs index 2b235b10..4a775380 100644 --- a/Core/Resgrid.Model/Services/IRecordsNotificationService.cs +++ b/Core/Resgrid.Model/Services/IRecordsNotificationService.cs @@ -13,5 +13,8 @@ public interface IRecordsNotificationService { /// Notifies the author that their record was returned; false when nothing was sent (record missing, not Returned, no author). Task NotifyReturnedForCorrectionAsync(int departmentId, string recordId, CancellationToken cancellationToken = default); + + /// EventTypes 33: the destination rejected the author's incident report; codes and field paths only, plus a link. + Task NotifySubmissionRejectedAsync(int departmentId, string reportId, CancellationToken cancellationToken = default); } } diff --git a/Core/Resgrid.Model/Services/IRecordsService.cs b/Core/Resgrid.Model/Services/IRecordsService.cs index de9704d2..0c290551 100644 --- a/Core/Resgrid.Model/Services/IRecordsService.cs +++ b/Core/Resgrid.Model/Services/IRecordsService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -59,6 +60,9 @@ public interface IRecordsService Task> GetYearsAsync(int departmentId); + /// Delta cursor for clients (plan section 5.3): projections modified after , oldest first, including tombstones (cancelled, voided, deleted). + Task> GetChangesSinceAsync(int departmentId, DateTime? since, int take); + Task AddAttachmentAsync(int departmentId, string userId, string recordId, string fileName, string contentType, byte[] data, string description, CancellationToken cancellationToken = default); /// Loads bytes; the caller authorizes per Record before serving. diff --git a/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs b/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs index 654c3614..2b9c507c 100644 --- a/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs +++ b/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs @@ -141,6 +141,24 @@ private static List GetCommon() => new TemplateVariableDescriptor("record_change.reason_code", "Normalized reason code", "string", false), }; + // submission.* (RMS-2 triggers 108-111): the sanitized delivery outcome — state, external id/status, attempts, + // codes and field paths. Never the destination payload or response body (plan section 5.6). + private static readonly List SubmissionVariables = new List + { + new TemplateVariableDescriptor("submission.id", "Submission ID", "string", false), + new TemplateVariableDescriptor("submission.destination", "Reporting destination, e.g. NERIS", "string", false), + new TemplateVariableDescriptor("submission.destination_version", "Pinned destination contract version", "string", false), + new TemplateVariableDescriptor("submission.state", "Submission state (Queued, AwaitingDestination, Accepted, Rejected, Failed)", "string", false), + new TemplateVariableDescriptor("submission.external_id", "Destination incident ID once assigned", "string", false), + new TemplateVariableDescriptor("submission.external_status", "Destination status value", "string", false), + new TemplateVariableDescriptor("submission.attempts", "Delivery attempts so far", "int", false), + new TemplateVariableDescriptor("submission.max_attempts", "Attempts allowed before the submission fails", "int", false), + new TemplateVariableDescriptor("submission.error_summary", "Normalized rejection/failure summary (codes and field paths)", "string", false), + new TemplateVariableDescriptor("submission.queued_on", "When the revision was queued (UTC)", "datetime", false), + new TemplateVariableDescriptor("submission.sent_on", "When the last attempt was sent (UTC)", "datetime", false), + new TemplateVariableDescriptor("submission.completed_on", "When the submission reached a final state (UTC)", "datetime", false), + }; + // review.* rides only on the two review-path triggers: review bookkeeping, never record content. private static readonly List ReviewVariables = new List { @@ -638,6 +656,19 @@ public static IReadOnlyList GetVariableCatalog(Workf if (eventType == WorkflowTriggerEventType.RecordSubmittedForReview || eventType == WorkflowTriggerEventType.RecordReturnedForCorrection) list.AddRange(ReviewVariables); break; + + case WorkflowTriggerEventType.RecordSubmissionQueued: + case WorkflowTriggerEventType.RecordSubmissionAccepted: + case WorkflowTriggerEventType.RecordSubmissionRejected: + case WorkflowTriggerEventType.RecordSubmissionFailed: + list.AddRange(RecordEventVariables); + list.AddRange(RecordVariables); + list.Add(new TemplateVariableDescriptor("record.kind", "Record kind (Operational or IncidentReport)", "string", false)); + list.Add(new TemplateVariableDescriptor("record.incident_number", "Department incident number sent to the destination", "string", false)); + list.Add(new TemplateVariableDescriptor("record.neris_incident_id", "NERIS incident ID once assigned", "string", false)); + list.AddRange(RecordChangeVariables); + list.AddRange(SubmissionVariables); + break; } return list.AsReadOnly(); diff --git a/Core/Resgrid.Model/WorkflowTriggerEventType.cs b/Core/Resgrid.Model/WorkflowTriggerEventType.cs index 08537827..27eb4d66 100644 --- a/Core/Resgrid.Model/WorkflowTriggerEventType.cs +++ b/Core/Resgrid.Model/WorkflowTriggerEventType.cs @@ -84,7 +84,22 @@ public enum WorkflowTriggerEventType RecordVoided = 106, /// A non-finalized Record was abandoned; carries the reserved-number disposition. - RecordCancelled = 107 + RecordCancelled = 107, + + // RMS-2 reporting-destination triggers (plan section 5.6). Emitted by the submission worker (41), never + // from the Records transaction; payloads carry sanitized submission.* data and never the destination payload. + + /// A finalized revision was queued for NERIS or another configured reporting destination. + RecordSubmissionQueued = 108, + + /// The destination accepted the immutable revision. + RecordSubmissionAccepted = 109, + + /// The destination rejected it with normalized, non-sensitive error codes. + RecordSubmissionRejected = 110, + + /// Delivery exhausted its retries or requires operator attention. + RecordSubmissionFailed = 111 } public static class WorkflowTriggerEventTypes diff --git a/Core/Resgrid.Search/LuceneRecordsIndexHost.cs b/Core/Resgrid.Search/LuceneRecordsIndexHost.cs index 4ac04a45..35c21951 100644 --- a/Core/Resgrid.Search/LuceneRecordsIndexHost.cs +++ b/Core/Resgrid.Search/LuceneRecordsIndexHost.cs @@ -150,7 +150,7 @@ public void Dispose() { try { _directory.Dispose(); } catch (Exception ex) { Logging.LogException(ex); } } - Analyzer.Dispose(); + try { Analyzer.Dispose(); } catch (Exception ex) { Logging.LogException(ex); } } } } diff --git a/Core/Resgrid.Services/Records/IncidentReportsService.cs b/Core/Resgrid.Services/Records/IncidentReportsService.cs new file mode 100644 index 00000000..47bc7cd7 --- /dev/null +++ b/Core/Resgrid.Services/Records/IncidentReportsService.cs @@ -0,0 +1,1475 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Resgrid.Framework; +using Resgrid.Model; +using Resgrid.Model.Providers; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Model.Services; + +namespace Resgrid.Services.Records +{ + /// + /// NERIS incident report lifecycle (RMS plan sections 4.2, 5.2.1, 5.3, 5.5; RMS-2). Mirrors RecordsService for + /// the second aggregate: the same state machine (RmsLifecycle), the same revision/audit/projection/group-scope + /// tables, the same outbox. What is specific here: source-aware prefill from the Call with an RmsSourceFact + /// per prefilled value, local validation against the pinned NERIS contract before finalize, an attestation + /// signature bound to the revision checksum, and a submission row per revision with its own idempotency key. + /// + public class IncidentReportsService : IIncidentReportsService + { + public const string AttestationStatementVersion = "1"; + public const string IncidentAggregate = "RmsIncidentReport"; + public const string DispatchCommentFactPrefix = "dispatch.comment."; + public const string NumberPrefix = "INC"; + + private readonly IRmsIncidentReportsRepository _reports; + private readonly IRmsSourceFactsRepository _facts; + private readonly IRmsUnitResponsesRepository _units; + private readonly IRmsIncidentTypesRepository _types; + private readonly IRmsActionTacticsRepository _tactics; + private readonly IRmsAidsRepository _aids; + private readonly IRmsLocationsRepository _locations; + private readonly IRmsNarrativesRepository _narratives; + private readonly IRmsValidationIssuesRepository _issues; + private readonly IRmsSubmissionsRepository _submissions; + private readonly IRmsSignaturesRepository _signatures; + private readonly IRmsRevisionsRepository _revisions; + private readonly IRmsAccessAuditsRepository _audits; + private readonly IRmsRecordGroupScopesRepository _scopes; + private readonly IRmsRecordSharesRepository _shares; + private readonly IRmsRecordSearchProjectionsRepository _projections; + private readonly IDomainEventOutboxService _outbox; + private readonly IDepartmentSettingsService _settings; + private readonly IDepartmentGroupsService _groups; + private readonly IUserProfileService _profiles; + private readonly IPersonnelRolesService _roles; + private readonly IUnitsService _unitsService; + private readonly ICallsService _calls; + private readonly IDepartmentDataProtectionService _dataProtection; + private readonly IUnitOfWork _unitOfWork; + private readonly INerisProfileService _neris; + private readonly INerisMappingService _mapping; + private readonly INerisValidationService _validation; + + public IncidentReportsService(IRmsIncidentReportsRepository reports, IRmsSourceFactsRepository facts, IRmsUnitResponsesRepository units, + IRmsIncidentTypesRepository types, IRmsActionTacticsRepository tactics, IRmsAidsRepository aids, IRmsLocationsRepository locations, + IRmsNarrativesRepository narratives, IRmsValidationIssuesRepository issues, IRmsSubmissionsRepository submissions, IRmsSignaturesRepository signatures, + IRmsRevisionsRepository revisions, IRmsAccessAuditsRepository audits, IRmsRecordGroupScopesRepository scopes, IRmsRecordSharesRepository shares, + IRmsRecordSearchProjectionsRepository projections, IDomainEventOutboxService outbox, IDepartmentSettingsService settings, + IDepartmentGroupsService groups, IUserProfileService profiles, IPersonnelRolesService roles, IUnitsService unitsService, ICallsService calls, + IDepartmentDataProtectionService dataProtection, IUnitOfWork unitOfWork, INerisProfileService neris, INerisMappingService mapping, + INerisValidationService validation) + { + _reports = reports; + _facts = facts; + _units = units; + _types = types; + _tactics = tactics; + _aids = aids; + _locations = locations; + _narratives = narratives; + _issues = issues; + _submissions = submissions; + _signatures = signatures; + _revisions = revisions; + _audits = audits; + _scopes = scopes; + _shares = shares; + _projections = projections; + _outbox = outbox; + _settings = settings; + _groups = groups; + _profiles = profiles; + _roles = roles; + _unitsService = unitsService; + _calls = calls; + _dataProtection = dataProtection; + _unitOfWork = unitOfWork; + _neris = neris; + _mapping = mapping; + _validation = validation; + } + + #region Start / read + + public async Task StartFromCallAsync(int departmentId, string userId, int callId, RmsOriginClient origin = RmsOriginClient.Web, CancellationToken cancellationToken = default) + { + if (callId <= 0) throw new ArgumentException("A call is required.", nameof(callId)); + if (string.IsNullOrWhiteSpace(userId)) throw new ArgumentException("A user is required.", nameof(userId)); + + var profile = await _neris.GetProfileAsync(departmentId); + var entity = ReportingEntityFor(departmentId, profile); + + // SingleAuthoritative (plan 5.2.1): a second start returns the existing report, never a duplicate. + var existing = await _reports.GetByCallAsync(departmentId, callId, entity); + if (existing != null && !existing.DeletedOn.HasValue) + return await GetAsync(departmentId, existing.RmsIncidentReportId, false); + + var call = await _calls.GetCallByIdAsync(callId); + if (call == null || call.DepartmentId != departmentId) + throw new ArgumentException($"Call {callId} does not belong to this department."); + call = await _calls.PopulateCallData(call, true, false, true, false, true, false, false, false, false) ?? call; + + var now = DateTime.UtcNow; + var reportId = Guid.NewGuid().ToString(); + var authorGroup = await _groups.GetGroupForUserAsync(userId, departmentId); + var facts = new List(); + + var report = new RmsIncidentReport + { + RmsIncidentReportId = reportId, + DepartmentId = departmentId, + ProtectionId = Guid.NewGuid().ToString(), + CallId = callId, + ReportingEntityId = entity, + DefinitionKey = RmsDefinitionKeys.NerisIncidentReport, + DefinitionVersion = RmsDefinitionKeys.LockedDefinitionVersion, + ProfileVersion = _neris.ContractVersion, + LifecyclePreset = (int)RmsDefinitionKeys.LockedDefaultPreset, + State = (int)RmsRecordState.Draft, + DraftReference = NewDraftReference(), + IncidentNumber = string.IsNullOrWhiteSpace(call.Number) ? null : call.Number, + DispatchIncidentCode = string.IsNullOrWhiteSpace(call.Type) ? null : call.Type, + CallCreatedOn = call.LoggedOn, + CallAnsweredOn = call.LoggedOn, + IncidentClearedOn = call.ClosedOn, + StationGroupId = authorGroup?.DepartmentGroupId, + AuthorUserId = userId, + OwnerUserId = userId, + OriginClient = (int)origin, + CreatedOn = now, + CreatedByUserId = userId, + ModifiedOn = now, + ModifiedByUserId = userId, + RowVersion = 1 + }; + facts.Add(Fact(report, NerisFactKeys.IncidentNumber, RmsSourceKind.Dispatch, "Calls", "Call", callId.ToString(), call.Number, call.LoggedOn, now)); + facts.Add(Fact(report, NerisFactKeys.IncidentCode, RmsSourceKind.Dispatch, "Calls", "Call", callId.ToString(), call.Type, call.LoggedOn, now)); + facts.Add(Fact(report, NerisFactKeys.CallCreate, RmsSourceKind.Dispatch, "Calls", "Call", callId.ToString(), Iso(call.LoggedOn), call.LoggedOn, now)); + // Resgrid holds no PSAP answered time; the create time stands in and is marked Derived so the author sees it. + facts.Add(Fact(report, NerisFactKeys.CallAnswered, RmsSourceKind.Derived, "Calls", "Call", callId.ToString(), Iso(call.LoggedOn), call.LoggedOn, now)); + if (call.ClosedOn.HasValue) + facts.Add(Fact(report, NerisFactKeys.IncidentClear, RmsSourceKind.Dispatch, "Calls", "Call", callId.ToString(), Iso(call.ClosedOn), call.ClosedOn, now)); + + var location = BuildLocationFromCall(report, call, now); + if (location != null) + { + facts.Add(Fact(report, NerisFactKeys.Location, RmsSourceKind.Dispatch, "Calls", "Call", callId.ToString(), location.AddressText, call.LoggedOn, now)); + if (location.Latitude.HasValue) + facts.Add(Fact(report, NerisFactKeys.Point, RmsSourceKind.Dispatch, "Calls", "Call", callId.ToString(), $"{location.Latitude},{location.Longitude}", call.LoggedOn, now)); + } + + var units = await BuildUnitsFromCallAsync(report, call, facts, now); + report.CallArrivalOn = units.Where(u => u.OnSceneOn.HasValue).Select(u => u.OnSceneOn).OrderBy(t => t).FirstOrDefault(); + if (report.CallArrivalOn.HasValue) + facts.Add(Fact(report, NerisFactKeys.CallArrival, RmsSourceKind.App, "UnitStates", "Call", callId.ToString(), Iso(report.CallArrivalOn), report.CallArrivalOn, now)); + + var types = new List(); + var mappedType = await _neris.ResolveCrosswalkAsync(departmentId, "incident_type", NerisCrosswalkSources.CallType, call.Type); + if (!string.IsNullOrWhiteSpace(mappedType)) + { + types.Add(new RmsIncidentType + { + RmsIncidentTypeId = Guid.NewGuid().ToString(), DepartmentId = departmentId, ProtectionId = Guid.NewGuid().ToString(), RecordId = reportId, + TypeCode = mappedType, IsPrimary = true, LocalCode = call.Type, ValueSetVersion = _neris.ContractVersion, Ordinal = 0, CreatedOn = now, ModifiedOn = now, RowVersion = 1 + }); + facts.Add(Fact(report, NerisFactKeys.IncidentType, RmsSourceKind.Derived, "Crosswalk", "CallType", call.Type, mappedType, call.LoggedOn, now)); + } + + var ordinal = 0; + foreach (var note in (call.CallNotes ?? new List()).Where(n => !string.IsNullOrWhiteSpace(n.Note)).OrderBy(n => n.Timestamp)) + facts.Add(Fact(report, DispatchCommentFactPrefix + ordinal++, RmsSourceKind.Dispatch, "Calls", "CallNote", note.CallNoteId.ToString(), note.Note, note.Timestamp, now)); + + var narrative = new RmsNarrative + { + RmsNarrativeId = Guid.NewGuid().ToString(), DepartmentId = departmentId, ProtectionId = Guid.NewGuid().ToString(), RecordId = reportId, + CreatedOn = now, ModifiedOn = now, RowVersion = 1 + }; + + report.DisplaySummary = BuildSummary(report, types, call.Name); + + var outboxIds = new List(); + await InTransactionAsync(async () => + { + await _reports.InsertAsync(report, cancellationToken, true); + if (location != null) + await _locations.InsertAsync(location, cancellationToken, true); + foreach (var unit in units) + await _units.InsertAsync(unit, cancellationToken, true); + foreach (var type in types) + await _types.InsertAsync(type, cancellationToken, true); + await _narratives.InsertAsync(narrative, cancellationToken, true); + foreach (var fact in facts) + await _facts.InsertAsync(fact, cancellationToken, true); + + var aggregate = new IncidentReportAggregate { Report = report, Location = location, Units = units, Types = types, Narrative = narrative, Facts = facts }; + await RecomputeGroupScopeAsync(aggregate, authorGroup?.DepartmentGroupId, cancellationToken); + await UpsertProjectionAsync(aggregate, cancellationToken); + outboxIds.Add((await EnqueueLifecycleEventAsync(report, null, WorkflowTriggerEventType.RecordCreated, RmsRecordState.Draft, RmsRecordState.Draft, null, null, cancellationToken)).DomainEventOutboxId); + await AuditAsync(departmentId, userId, reportId, null, RmsAccessAuditAction.Change, "Start incident report from call", origin, cancellationToken, new { callId, prefilledFacts = facts.Count }); + }); + await _outbox.DispatchAfterCommitAsync(outboxIds, cancellationToken); + + return await GetAsync(departmentId, reportId, false); + } + + public async Task GetAsync(int departmentId, string reportId, bool includeHistory = false) + { + var report = await _reports.GetByIdForDepartmentAsync(departmentId, reportId); + if (report == null || report.DeletedOn.HasValue) + return null; + + return await HydrateAsync(report, null, includeHistory); + } + + public async Task GetForCallAsync(int departmentId, int callId) + { + var profile = await _neris.GetProfileAsync(departmentId); + var report = await _reports.GetByCallAsync(departmentId, callId, ReportingEntityFor(departmentId, profile)) + ?? (await _reports.GetByCallAnyEntityAsync(departmentId, callId))?.FirstOrDefault(r => !r.DeletedOn.HasValue); + return report == null ? null : await HydrateAsync(report, null, false); + } + + public async Task BuildSnapshotAsync(int departmentId, string reportId, string revisionId = null) + { + var report = await _reports.GetByIdForDepartmentAsync(departmentId, reportId); + if (report == null) + return null; + + var aggregate = await HydrateAsync(report, revisionId, false); + return ToSnapshot(aggregate); + } + + public static NerisIncidentSnapshot ToSnapshot(IncidentReportAggregate aggregate) + { + return new NerisIncidentSnapshot + { + Report = aggregate.Report, + Location = aggregate.Location, + Types = aggregate.Types, + Units = aggregate.Units, + Aids = aggregate.Aids, + Tactics = aggregate.Tactics, + Narrative = aggregate.Narrative, + Facts = aggregate.Facts, + DispatchComments = aggregate.Facts.Where(f => f.FactKey != null && f.FactKey.StartsWith(DispatchCommentFactPrefix, StringComparison.Ordinal)) + .OrderBy(f => f.SourceTime).Select(f => new NerisDispatchComment { Timestamp = f.SourceTime, Comment = f.CurrentValue ?? f.SourceValue }).ToList(), + SpecialModifiers = SplitCsv(aggregate.Report.SpecialModifiersCsv) + }; + } + + #endregion + + #region Draft / validate + + public async Task SaveDraftAsync(int departmentId, string userId, string reportId, long expectedRowVersion, IncidentReportDraftInput input, CancellationToken cancellationToken = default) + { + if (input == null) throw new ArgumentNullException(nameof(input)); + var report = await LoadAsync(departmentId, reportId); + RequireEditable(report); + + var now = DateTime.UtcNow; + await InTransactionAsync(async () => + { + await GuardVersionAsync(report, expectedRowVersion, cancellationToken); + var facts = (await _facts.GetForRecordAsync(departmentId, reportId, null))?.ToList() ?? new List(); + + ApplyHeader(report, input, facts, userId, now); + var location = await ReplaceLocationAsync(report, input.Location, facts, userId, now, cancellationToken); + var types = await ReplaceTypesAsync(report, input.Types, now, cancellationToken); + var units = await ReplaceUnitsAsync(report, input.Units, facts, userId, now, cancellationToken); + var aids = await ReplaceAidsAsync(report, input.Aids, now, cancellationToken); + var tactics = await ReplaceTacticsAsync(report, input.Tactics, now, cancellationToken); + var narrative = await ReplaceNarrativeAsync(report, input, now, cancellationToken); + foreach (var fact in facts.Where(f => f.CorrectedOn == now)) + await _facts.UpdateAsync(fact, cancellationToken, true); + + report.DisplaySummary = BuildSummary(report, types, null); + report.ModifiedOn = now; + report.ModifiedByUserId = userId; + await _reports.UpdateAsync(report, cancellationToken, true); + + var aggregate = new IncidentReportAggregate { Report = report, Location = location, Types = types, Units = units, Aids = aids, Tactics = tactics, Narrative = narrative, Facts = facts }; + await RecomputeGroupScopeAsync(aggregate, null, cancellationToken); + await UpsertProjectionAsync(aggregate, cancellationToken); + await AuditAsync(departmentId, userId, reportId, null, RmsAccessAuditAction.Change, "Save draft", input.OriginClient, cancellationToken); + }); + + return await GetAsync(departmentId, reportId, false); + } + + public async Task> ValidateAsync(int departmentId, string reportId, bool includeDestination, CancellationToken cancellationToken = default) + { + var report = await LoadAsync(departmentId, reportId); + var aggregate = await HydrateAsync(report, null, false); + var profile = await _neris.GetProfileAsync(departmentId); + var snapshot = ToSnapshot(aggregate); + + var local = _validation.ValidateLocal(snapshot, profile); + await _issues.ReplaceForRecordAsync(departmentId, reportId, RmsValidationSource.Local, local, cancellationToken); + + if (includeDestination && await _neris.IsSubmissionEnabledAsync(departmentId) && !local.Any(i => i.Severity == (int)RmsValidationSeverity.Error)) + { + var remote = await _validation.ValidateRemoteAsync(profile, _mapping.BuildIncidentPayloadJson(snapshot, profile), cancellationToken); + foreach (var issue in remote) + { + issue.DepartmentId = departmentId; + issue.RecordId = reportId; + } + await _issues.ReplaceForRecordAsync(departmentId, reportId, RmsValidationSource.Destination, remote, cancellationToken); + } + else + { + await _issues.ReplaceForRecordAsync(departmentId, reportId, RmsValidationSource.Destination, Enumerable.Empty(), cancellationToken); + } + + await AuditAsync(departmentId, null, reportId, null, RmsAccessAuditAction.Change, includeDestination ? "Validate (local + destination)" : "Validate (local)", RmsOriginClient.Web, cancellationToken, new { errors = local.Count(i => i.Severity == (int)RmsValidationSeverity.Error) }); + return (await _issues.GetForRecordAsync(departmentId, reportId))?.ToList() ?? new List(); + } + + #endregion + + #region Review + + public async Task SubmitForReviewAsync(int departmentId, string userId, string reportId, long expectedRowVersion, CancellationToken cancellationToken = default) + { + var report = await LoadAsync(departmentId, reportId); + var from = (RmsRecordState)report.State; + RequireTransition(report, from, RmsRecordState.ReadyForReview); + + var now = DateTime.UtcNow; + var outboxIds = new List(); + await InTransactionAsync(async () => + { + await GuardVersionAsync(report, expectedRowVersion, cancellationToken); + report.State = (int)RmsRecordState.ReadyForReview; + report.SubmittedForReviewOn = now; + report.ReviewDueOn = now.AddHours(await _settings.GetRecordsReviewDueHoursAsync(departmentId)); + report.ModifiedOn = now; + report.ModifiedByUserId = userId; + await _reports.UpdateAsync(report, cancellationToken, true); + await RefreshProjectionAsync(report, cancellationToken); + outboxIds.Add((await EnqueueLifecycleEventAsync(report, null, WorkflowTriggerEventType.RecordSubmittedForReview, from, RmsRecordState.ReadyForReview, null, null, cancellationToken)).DomainEventOutboxId); + await AuditAsync(departmentId, userId, reportId, null, RmsAccessAuditAction.Change, "Submit for review", RmsOriginClient.Web, cancellationToken); + }); + await _outbox.DispatchAfterCommitAsync(outboxIds, cancellationToken); + return await GetAsync(departmentId, reportId, false); + } + + public async Task ReturnForCorrectionAsync(int departmentId, string userId, string reportId, string reasonCode, string reasonText, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(reasonCode)) throw new ArgumentException("A reason code is required to return a report.", nameof(reasonCode)); + var report = await LoadAsync(departmentId, reportId); + var from = (RmsRecordState)report.State; + RequireTransition(report, from, RmsRecordState.Returned); + + var now = DateTime.UtcNow; + var outboxIds = new List(); + await InTransactionAsync(async () => + { + await GuardVersionAsync(report, report.RowVersion, cancellationToken); + report.State = (int)RmsRecordState.Returned; + report.ReturnedOn = now; + report.ReturnReasonCode = reasonCode; + report.ReturnReasonText = reasonText; + report.ReturnCount += 1; + report.ReviewerUserId = userId; + report.ModifiedOn = now; + report.ModifiedByUserId = userId; + await _reports.UpdateAsync(report, cancellationToken, true); + await RefreshProjectionAsync(report, cancellationToken); + outboxIds.Add((await EnqueueLifecycleEventAsync(report, null, WorkflowTriggerEventType.RecordReturnedForCorrection, from, RmsRecordState.Returned, reasonCode, null, cancellationToken)).DomainEventOutboxId); + await AuditAsync(departmentId, userId, reportId, null, RmsAccessAuditAction.Change, "Return for correction", RmsOriginClient.Web, cancellationToken, new { reasonCode }); + }); + await _outbox.DispatchAfterCommitAsync(outboxIds, cancellationToken); + return await GetAsync(departmentId, reportId, false); + } + + #endregion + + #region Finalize / correct / amend + + public Task FinalizeAsync(int departmentId, string userId, string reportId, long expectedRowVersion, string attestationStatementVersion, string ipAddress, string reasonCode, string reasonText, CancellationToken cancellationToken = default) + { + return SignRevisionAsync(departmentId, userId, reportId, expectedRowVersion, attestationStatementVersion, ipAddress, reasonCode, reasonText, false, cancellationToken); + } + + public Task CorrectAndResubmitAsync(int departmentId, string userId, string reportId, long expectedRowVersion, string attestationStatementVersion, string ipAddress, string reasonCode, string reasonText, CancellationToken cancellationToken = default) + { + return SignRevisionAsync(departmentId, userId, reportId, expectedRowVersion, attestationStatementVersion, ipAddress, reasonCode, reasonText, true, cancellationToken); + } + + private async Task SignRevisionAsync(int departmentId, string userId, string reportId, long expectedRowVersion, string attestationStatementVersion, string ipAddress, string reasonCode, string reasonText, bool correction, CancellationToken cancellationToken) + { + var report = await LoadAsync(departmentId, reportId); + var from = (RmsRecordState)report.State; + var isAmendment = report.AmendsRevisionId != null; + RmsRecordState to; + if (correction) + { + if (from != RmsRecordState.Rejected) + throw new RecordTransitionException(reportId, from, RmsRecordState.Corrected, "only a rejected report can be corrected and resubmitted"); + to = RmsRecordState.Corrected; + } + else + { + to = isAmendment ? RmsRecordState.Amended : RmsRecordState.Finalized; + RequireTransition(report, from, to); + } + + var needsReason = isAmendment || correction; + if (needsReason && string.IsNullOrWhiteSpace(reasonCode)) + throw new ArgumentException("A reason code is required to finalize an amendment or a correction.", nameof(reasonCode)); + + var now = DateTime.UtcNow; + var profile = await _neris.GetProfileAsync(departmentId); + var outboxIds = new List(); + await InTransactionAsync(async () => + { + await GuardVersionAsync(report, expectedRowVersion, cancellationToken); + var draft = await HydrateAsync(report, null, false); + + // Validation blocks the signature (plan 4.2 "progressive validation"): the issues stay on the report for the author. + var local = _validation.ValidateLocal(ToSnapshot(draft), profile); + await _issues.ReplaceForRecordAsync(departmentId, reportId, RmsValidationSource.Local, local, cancellationToken); + var errors = local.Where(i => i.Severity == (int)RmsValidationSeverity.Error).ToList(); + if (errors.Count > 0) + throw new IncidentReportValidationException(reportId, errors); + + if (string.IsNullOrWhiteSpace(report.RecordNumber)) + report.RecordNumber = await AllocateRecordNumberAsync(report, cancellationToken); + + var transition = isAmendment || correction ? RmsRevisionTransition.Amended : RmsRevisionTransition.Finalized; + var revision = await WriteRevisionAsync(report, draft, transition, userId, reasonCode, reasonText, attestationStatementVersion ?? AttestationStatementVersion, now, cancellationToken); + await WriteSignatureAsync(report, revision, userId, attestationStatementVersion ?? AttestationStatementVersion, ipAddress, now, cancellationToken); + + var priorState = from; + report.State = (int)to; + if (!isAmendment && !correction) + { + report.FinalizedOn = now; + report.FinalizedByUserId = userId; + } + report.CurrentRevisionId = revision.RmsRevisionId; + report.RevisionCount = revision.RevisionNumber; + report.AmendsRevisionId = null; + report.ModifiedOn = now; + report.ModifiedByUserId = userId; + await _reports.UpdateAsync(report, cancellationToken, true); + + draft.Report = report; + await RecomputeGroupScopeAsync(draft, null, cancellationToken); + await UpsertProjectionAsync(draft, cancellationToken); + + var trigger = correction || isAmendment ? WorkflowTriggerEventType.RecordAmended : WorkflowTriggerEventType.RecordFinalized; + var lifecycle = await EnqueueLifecycleEventAsync(report, revision, trigger, priorState, to, reasonCode, null, cancellationToken); + outboxIds.Add(lifecycle.DomainEventOutboxId); + await AuditAsync(departmentId, userId, reportId, revision.RmsRevisionId, RmsAccessAuditAction.Sign, correction ? "Correct and resubmit" : isAmendment ? "Finalize amendment" : "Finalize", (RmsOriginClient)report.OriginClient, cancellationToken, new { revision.RevisionNumber, revision.Checksum, reasonCode }, ipAddress); + + // Submission: every finalized/corrected revision gets its own idempotency key (plan 5.3). Queued here when the + // profile allows; otherwise the author queues it explicitly. + var autoSubmit = correction || (profile != null && profile.AutoSubmitOnFinalize); + if (autoSubmit && await _neris.IsSubmissionEnabledAsync(departmentId)) + { + var queued = await QueueSubmissionCoreAsync(report, draft, revision, profile, userId, now, cancellationToken); + outboxIds.Add(queued.outboxId); + } + }); + await _outbox.DispatchAfterCommitAsync(outboxIds, cancellationToken); + return await GetAsync(departmentId, reportId, true); + } + + public async Task QueueSubmissionAsync(int departmentId, string userId, string reportId, CancellationToken cancellationToken = default) + { + var report = await LoadAsync(departmentId, reportId); + var state = (RmsRecordState)report.State; + if (!RmsLifecycle.IsFinalizedFamily(state) || RmsLifecycle.IsTerminal(state) || string.IsNullOrWhiteSpace(report.CurrentRevisionId)) + throw new RecordTransitionException(reportId, state, RmsRecordState.Submitted, "only a finalized revision can be submitted"); + if (report.AmendsRevisionId != null) + throw new RecordTransitionException(reportId, state, RmsRecordState.Submitted, "close the open amendment before submitting"); + if (!await _neris.IsSubmissionEnabledAsync(departmentId)) + throw new InvalidOperationException("NERIS submission is not enabled for this department."); + + var profile = await _neris.GetProfileAsync(departmentId); + var now = DateTime.UtcNow; + var outboxIds = new List(); + await InTransactionAsync(async () => + { + await GuardVersionAsync(report, report.RowVersion, cancellationToken); + var revision = await _revisions.GetByIdForDepartmentAsync(departmentId, report.CurrentRevisionId); + var aggregate = await HydrateAsync(report, revision.RmsRevisionId, false); + var queued = await QueueSubmissionCoreAsync(report, aggregate, revision, profile, userId, now, cancellationToken); + outboxIds.Add(queued.outboxId); + report.ModifiedOn = now; + report.ModifiedByUserId = userId; + await _reports.UpdateAsync(report, cancellationToken, true); + await RefreshProjectionAsync(report, cancellationToken); + }); + await _outbox.DispatchAfterCommitAsync(outboxIds, cancellationToken); + return await GetAsync(departmentId, reportId, true); + } + + private async Task<(RmsSubmission submission, long outboxId)> QueueSubmissionCoreAsync(RmsIncidentReport report, IncidentReportAggregate aggregate, RmsRevision revision, RmsNerisProfile profile, string userId, DateTime now, CancellationToken cancellationToken) + { + var payload = _mapping.BuildIncidentPayloadJson(ToSnapshot(aggregate), profile); + var key = IdempotencyKey(report.DepartmentId, report.RmsIncidentReportId, revision.RmsRevisionId); + + var submission = await _submissions.GetByIdempotencyKeyAsync(key); + if (submission != null && submission.State != (int)RmsSubmissionState.Failed && submission.State != (int)RmsSubmissionState.Superseded && submission.State != (int)RmsSubmissionState.Rejected) + throw new InvalidOperationException("This revision is already queued or delivered."); + + if (submission == null) + { + submission = new RmsSubmission + { + RmsSubmissionId = Guid.NewGuid().ToString(), + DepartmentId = report.DepartmentId, + ProtectionId = Guid.NewGuid().ToString(), + RecordId = report.RmsIncidentReportId, + RecordKind = (int)RmsRecordKind.IncidentReport, + RevisionId = revision.RmsRevisionId, + Destination = RmsSubmissionDestinations.Neris, + DestinationVersion = profile?.ContractVersion ?? _neris.ContractVersion, + IdempotencyKey = key, + MaxAttempts = Math.Max(1, Config.NerisConfig.MaxAttempts), + PayloadJson = payload, + PayloadChecksum = RecordSnapshotSerializer.Checksum(payload), + QueuedOn = now, + CreatedByUserId = userId, + CreatedOn = now, + ModifiedOn = now, + RowVersion = 1 + }; + await _submissions.SupersedeOpenForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, submission.RmsSubmissionId, now, cancellationToken); + await _submissions.InsertAsync(submission, cancellationToken, true); + } + else + { + // A failed or rejected delivery of the same revision is re-queued with the same key: retry, not a new payload. + submission.State = (int)RmsSubmissionState.Queued; + submission.Attempts = 0; + submission.NextAttemptOn = null; + submission.LeaseOwner = null; + submission.LeaseExpiresOn = null; + submission.ErrorSummary = null; + submission.QueuedOn = now; + submission.ModifiedOn = now; + submission.RowVersion += 1; + await _submissions.UpdateAsync(submission, cancellationToken, true); + } + + var priorState = (RmsRecordState)report.State; + report.State = (int)RmsRecordState.Submitted; + report.LastSubmissionId = submission.RmsSubmissionId; + report.LastSubmissionState = submission.State; + report.LastSubmittedOn = now; + await _reports.UpdateAsync(report, cancellationToken, true); + await RefreshProjectionAsync(report, cancellationToken); + + var entry = await EnqueueLifecycleEventAsync(report, revision, WorkflowTriggerEventType.RecordSubmissionQueued, priorState, RmsRecordState.Submitted, null, SubmissionBlock(submission), cancellationToken); + await AuditAsync(report.DepartmentId, userId, report.RmsIncidentReportId, revision.RmsRevisionId, RmsAccessAuditAction.Submit, "Queue submission", RmsOriginClient.Web, cancellationToken, new { submission.RmsSubmissionId, submission.IdempotencyKey, submission.PayloadChecksum }); + return (submission, entry.DomainEventOutboxId); + } + + public async Task OpenAmendmentAsync(int departmentId, string userId, string reportId, CancellationToken cancellationToken = default) + { + var report = await LoadAsync(departmentId, reportId); + var state = (RmsRecordState)report.State; + if (!RmsLifecycle.CanTransition((RmsLifecyclePreset)report.LifecyclePreset, state, RmsRecordState.Amended)) + throw new RecordTransitionException(reportId, state, RmsRecordState.Amended); + if (report.AmendsRevisionId != null) + throw new RecordTransitionException(reportId, state, RmsRecordState.Amended, "an amendment draft is already open"); + + await InTransactionAsync(async () => + { + await GuardVersionAsync(report, report.RowVersion, cancellationToken); + report.AmendsRevisionId = report.CurrentRevisionId; + report.OwnerUserId = userId; + report.ModifiedOn = DateTime.UtcNow; + report.ModifiedByUserId = userId; + await _reports.UpdateAsync(report, cancellationToken, true); + await AuditAsync(departmentId, userId, reportId, report.CurrentRevisionId, RmsAccessAuditAction.Change, "Open amendment", RmsOriginClient.Web, cancellationToken); + }); + return await GetAsync(departmentId, reportId, true); + } + + public async Task AbandonAmendmentAsync(int departmentId, string userId, string reportId, CancellationToken cancellationToken = default) + { + var report = await LoadAsync(departmentId, reportId); + if (report.AmendsRevisionId == null) + throw new RecordTransitionException(reportId, (RmsRecordState)report.State, (RmsRecordState)report.State, "no amendment draft is open"); + + var now = DateTime.UtcNow; + await InTransactionAsync(async () => + { + await GuardVersionAsync(report, report.RowVersion, cancellationToken); + // The draft rows are rebuilt from the current revision's copies so the finalized content is what the author sees again. + var current = await HydrateAsync(report, report.CurrentRevisionId, false); + await ReplaceDraftRowsFromAsync(report, current, now, cancellationToken); + report.AmendsRevisionId = null; + report.ModifiedOn = now; + report.ModifiedByUserId = userId; + await _reports.UpdateAsync(report, cancellationToken, true); + await AuditAsync(departmentId, userId, reportId, report.CurrentRevisionId, RmsAccessAuditAction.Change, "Abandon amendment", RmsOriginClient.Web, cancellationToken); + }); + return await GetAsync(departmentId, reportId, true); + } + + public async Task VoidAsync(int departmentId, string userId, string reportId, string reasonCode, string reasonText, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(reasonCode)) throw new ArgumentException("A reason code is required to void a report.", nameof(reasonCode)); + var report = await LoadAsync(departmentId, reportId); + var from = (RmsRecordState)report.State; + RequireTransition(report, from, RmsRecordState.Voided); + if (report.AmendsRevisionId != null) + throw new RecordTransitionException(reportId, from, RmsRecordState.Voided, "abandon the open amendment first"); + + var now = DateTime.UtcNow; + var outboxIds = new List(); + await InTransactionAsync(async () => + { + await GuardVersionAsync(report, report.RowVersion, cancellationToken); + var current = await HydrateAsync(report, report.CurrentRevisionId, false); + var revision = await WriteRevisionAsync(report, current, RmsRevisionTransition.Voided, userId, reasonCode, reasonText, null, now, cancellationToken); + report.State = (int)RmsRecordState.Voided; + report.VoidedOn = now; + report.VoidedByUserId = userId; + report.VoidReasonCode = reasonCode; + report.VoidReasonText = reasonText; + report.CurrentRevisionId = revision.RmsRevisionId; + report.RevisionCount = revision.RevisionNumber; + report.ModifiedOn = now; + report.ModifiedByUserId = userId; + await _reports.UpdateAsync(report, cancellationToken, true); + await _submissions.SupersedeOpenForRecordAsync(departmentId, reportId, null, now, cancellationToken); + await RefreshProjectionAsync(report, cancellationToken); + outboxIds.Add((await EnqueueLifecycleEventAsync(report, revision, WorkflowTriggerEventType.RecordVoided, from, RmsRecordState.Voided, reasonCode, null, cancellationToken)).DomainEventOutboxId); + await AuditAsync(departmentId, userId, reportId, revision.RmsRevisionId, RmsAccessAuditAction.Change, "Void", RmsOriginClient.Web, cancellationToken, new { reasonCode }); + }); + await _outbox.DispatchAfterCommitAsync(outboxIds, cancellationToken); + return await GetAsync(departmentId, reportId, true); + } + + public async Task CancelAsync(int departmentId, string userId, string reportId, CancellationToken cancellationToken = default) + { + var report = await LoadAsync(departmentId, reportId); + var from = (RmsRecordState)report.State; + RequireTransition(report, from, RmsRecordState.Cancelled); + + var now = DateTime.UtcNow; + var outboxIds = new List(); + await InTransactionAsync(async () => + { + await GuardVersionAsync(report, report.RowVersion, cancellationToken); + report.State = (int)RmsRecordState.Cancelled; + report.CancelledOn = now; + report.CancelledByUserId = userId; + report.ModifiedOn = now; + report.ModifiedByUserId = userId; + await _reports.UpdateAsync(report, cancellationToken, true); + await RefreshProjectionAsync(report, cancellationToken); + outboxIds.Add((await EnqueueLifecycleEventAsync(report, null, WorkflowTriggerEventType.RecordCancelled, from, RmsRecordState.Cancelled, null, null, cancellationToken, new { number_disposition = string.IsNullOrWhiteSpace(report.RecordNumber) ? "none" : "voided" })).DomainEventOutboxId); + await AuditAsync(departmentId, userId, reportId, null, RmsAccessAuditAction.Change, "Cancel", RmsOriginClient.Web, cancellationToken); + }); + await _outbox.DispatchAfterCommitAsync(outboxIds, cancellationToken); + return await GetAsync(departmentId, reportId, false); + } + + #endregion + + #region Queries / audit + + public async Task> QueryAsync(int departmentId, RmsIncidentReportQuery query) + { + return (await _reports.QueryAsync(departmentId, query ?? new RmsIncidentReportQuery()))?.ToList() ?? new List(); + } + + public Task CountAsync(int departmentId, RmsIncidentReportQuery query) + { + return _reports.CountAsync(departmentId, query ?? new RmsIncidentReportQuery()); + } + + public async Task> GetYearsAsync(int departmentId) + { + return (await _reports.GetYearsAsync(departmentId))?.ToList() ?? new List(); + } + + public Task RecordAccessAsync(int departmentId, string userId, string reportId, string revisionId, RmsAccessAuditAction action, string purpose = null, string ipAddress = null) + { + return AuditAsync(departmentId, userId, reportId, revisionId, action, purpose, RmsOriginClient.Web, CancellationToken.None, null, ipAddress); + } + + #endregion + + #region Prefill helpers + + public static string ReportingEntityFor(int departmentId, RmsNerisProfile profile) + { + return string.IsNullOrWhiteSpace(profile?.NerisEntityId) ? $"department:{departmentId}" : profile.NerisEntityId; + } + + private static RmsLocation BuildLocationFromCall(RmsIncidentReport report, Call call, DateTime now) + { + decimal? latitude = null, longitude = null; + if (!string.IsNullOrWhiteSpace(call.GeoLocationData) && call.GeoLocationData.Contains(",")) + { + var parts = call.GeoLocationData.Split(','); + if (parts.Length == 2 && decimal.TryParse(parts[0].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var lat) && decimal.TryParse(parts[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out var lon)) + { + latitude = lat; + longitude = lon; + } + } + + if (string.IsNullOrWhiteSpace(call.Address) && latitude == null) + return null; + + return new RmsLocation + { + RmsLocationId = Guid.NewGuid().ToString(), + DepartmentId = report.DepartmentId, + ProtectionId = Guid.NewGuid().ToString(), + RecordId = report.RmsIncidentReportId, + AddressText = string.IsNullOrWhiteSpace(call.Address) ? null : call.Address.Trim(), + Latitude = latitude, + Longitude = longitude, + SourceKind = (int)RmsSourceKind.Dispatch, + CreatedOn = now, + ModifiedOn = now, + RowVersion = 1 + }; + } + + /// One unit response per dispatched unit; times come from the unit state log (App) and the dispatch row (Dispatch), each with a provenance fact. + private async Task> BuildUnitsFromCallAsync(RmsIncidentReport report, Call call, List facts, DateTime now) + { + var result = new List(); + var dispatches = (call.UnitDispatches ?? new List()).GroupBy(d => d.UnitId).Select(g => g.OrderBy(d => d.DispatchedOn).First()).ToList(); + if (dispatches.Count == 0) + return result; + + var states = (await _unitsService.GetUnitStatesForCallAsync(report.DepartmentId, call.CallId) ?? new List()).OrderBy(s => s.Timestamp).ToList(); + var ordinal = 0; + foreach (var dispatch in dispatches) + { + var unit = await _unitsService.GetUnitByIdAsync(dispatch.UnitId); + if (unit == null || unit.DepartmentId != report.DepartmentId) + continue; + + var unitStates = states.Where(s => s.UnitId == dispatch.UnitId).ToList(); + DateTime? At(params UnitStateTypes[] kinds) => unitStates.FirstOrDefault(s => kinds.Contains((UnitStateTypes)s.State))?.Timestamp; + var onScene = At(UnitStateTypes.OnScene); + var cleared = unitStates.Where(s => onScene.HasValue && s.Timestamp >= onScene.Value && ((UnitStateTypes)s.State == UnitStateTypes.Released || (UnitStateTypes)s.State == UnitStateTypes.Returning || (UnitStateTypes)s.State == UnitStateTypes.Available)).Select(s => (DateTime?)s.Timestamp).FirstOrDefault(); + + var response = new RmsUnitResponse + { + RmsUnitResponseId = Guid.NewGuid().ToString(), + DepartmentId = report.DepartmentId, + ProtectionId = Guid.NewGuid().ToString(), + RecordId = report.RmsIncidentReportId, + UnitId = unit.UnitId, + UnitNameSnapshot = unit.Name, + UnitTypeSnapshot = unit.Type, + StationGroupIdSnapshot = unit.StationGroupId, + DispatchedOn = dispatch.DispatchedOn, + EnrouteOn = At(UnitStateTypes.Responding, UnitStateTypes.Enroute), + OnSceneOn = onScene, + StagingOn = At(UnitStateTypes.Staging), + CanceledEnrouteOn = onScene.HasValue ? null : At(UnitStateTypes.Cancelled), + ClearedOn = cleared, + ResponseMode = "EMERGENT", + TimesSourceKind = (int)RmsSourceKind.App, + Ordinal = ordinal++, + CreatedOn = now, + ModifiedOn = now, + RowVersion = 1 + }; + result.Add(response); + + facts.Add(Fact(report, NerisFactKeys.UnitTime(unit.UnitId, "dispatch"), RmsSourceKind.Dispatch, "Calls", "CallDispatchUnit", dispatch.CallDispatchUnitId.ToString(), Iso(dispatch.DispatchedOn), dispatch.DispatchedOn, now)); + foreach (var (field, value) in new[] { ("enroute_to_scene", response.EnrouteOn), ("on_scene", response.OnSceneOn), ("staging", response.StagingOn), ("canceled_enroute", response.CanceledEnrouteOn), ("unit_clear", response.ClearedOn) }) + { + if (value.HasValue) + facts.Add(Fact(report, NerisFactKeys.UnitTime(unit.UnitId, field), RmsSourceKind.App, "UnitStates", "Unit", unit.UnitId.ToString(), Iso(value), value, now)); + } + } + + return result; + } + + private static RmsSourceFact Fact(RmsIncidentReport report, string key, RmsSourceKind kind, string system, string entityType, string entityId, string value, DateTime? sourceTime, DateTime now) + { + return new RmsSourceFact + { + RmsSourceFactId = Guid.NewGuid().ToString(), + DepartmentId = report.DepartmentId, + ProtectionId = Guid.NewGuid().ToString(), + RecordId = report.RmsIncidentReportId, + FactKey = key, + SourceKind = (int)kind, + SourceSystem = system, + SourceEntityType = entityType, + SourceEntityId = entityId, + SourceValue = value, + CurrentValue = value, + SourceTime = sourceTime, + ImportedOn = now, + CreatedOn = now, + ModifiedOn = now, + RowVersion = 1 + }; + } + + /// An edit to a prefilled value keeps the original and records the correction on its provenance row (plan 4.2). + private static void Correct(List facts, string key, string newValue, string userId, DateTime now) + { + var fact = facts.FirstOrDefault(f => string.Equals(f.FactKey, key, StringComparison.Ordinal)); + if (fact == null || string.Equals(fact.CurrentValue, newValue, StringComparison.Ordinal)) + return; + + fact.CurrentValue = newValue; + fact.CorrectedOn = now; + fact.CorrectedByUserId = userId; + fact.ModifiedOn = now; + fact.RowVersion += 1; + } + + public static string Iso(DateTime? value) + { + return value.HasValue ? value.Value.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture) : null; + } + + #endregion + + #region Draft replacement + + private static void ApplyHeader(RmsIncidentReport report, IncidentReportDraftInput input, List facts, string userId, DateTime now) + { + Correct(facts, NerisFactKeys.IncidentNumber, input.IncidentNumber, userId, now); + Correct(facts, NerisFactKeys.IncidentCode, input.DispatchIncidentCode, userId, now); + Correct(facts, NerisFactKeys.CallCreate, Iso(input.CallCreatedOn), userId, now); + Correct(facts, NerisFactKeys.CallAnswered, Iso(input.CallAnsweredOn), userId, now); + Correct(facts, NerisFactKeys.CallArrival, Iso(input.CallArrivalOn), userId, now); + Correct(facts, NerisFactKeys.IncidentClear, Iso(input.IncidentClearedOn), userId, now); + + report.IncidentNumber = Trim(input.IncidentNumber); + report.DispatchIncidentCode = Trim(input.DispatchIncidentCode); + report.CallCreatedOn = input.CallCreatedOn; + report.CallAnsweredOn = input.CallAnsweredOn; + report.CallArrivalOn = input.CallArrivalOn; + report.IncidentClearedOn = input.IncidentClearedOn; + report.DispatchCenterId = Trim(input.DispatchCenterId); + report.DeterminantCode = Trim(input.DeterminantCode); + report.Disposition = Trim(input.Disposition); + report.PeoplePresent = input.PeoplePresent; + report.DisplacementCount = input.DisplacementCount; + report.AnimalsRescued = input.AnimalsRescued; + report.SpecialModifiersCsv = input.SpecialModifiers == null || input.SpecialModifiers.Count == 0 ? null : string.Join(",", input.SpecialModifiers.Where(m => !string.IsNullOrWhiteSpace(m)).Select(m => m.Trim()).Distinct()); + if (input.StationGroupId.HasValue) + report.StationGroupId = input.StationGroupId; + } + + private async Task ReplaceLocationAsync(RmsIncidentReport report, IncidentLocationInput input, List facts, string userId, DateTime now, CancellationToken cancellationToken) + { + await _locations.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + if (input == null) + return null; + + var location = new RmsLocation + { + RmsLocationId = Guid.NewGuid().ToString(), DepartmentId = report.DepartmentId, ProtectionId = Guid.NewGuid().ToString(), RecordId = report.RmsIncidentReportId, + AddressText = Trim(input.AddressText), Number = Trim(input.Number), NumberPrefix = Trim(input.NumberPrefix), NumberSuffix = Trim(input.NumberSuffix), Street = Trim(input.Street), + UnitValue = Trim(input.UnitValue), Municipality = Trim(input.Municipality), County = Trim(input.County), State = Trim(input.State)?.ToUpperInvariant(), PostalCode = Trim(input.PostalCode), + Country = Trim(input.Country)?.ToUpperInvariant(), PlaceType = Trim(input.PlaceType), LocationUse = Trim(input.LocationUse), CrossStreet1 = Trim(input.CrossStreet1), CrossStreet2 = Trim(input.CrossStreet2), + Latitude = input.Latitude, Longitude = input.Longitude, Jurisdiction = Trim(input.Jurisdiction), + SourceKind = (int)RmsSourceKind.None, CreatedOn = now, ModifiedOn = now, RowVersion = 1 + }; + Correct(facts, NerisFactKeys.Location, location.AddressText, userId, now); + Correct(facts, NerisFactKeys.Point, location.Latitude.HasValue ? $"{location.Latitude},{location.Longitude}" : null, userId, now); + await _locations.InsertAsync(location, cancellationToken, true); + return location; + } + + private async Task> ReplaceTypesAsync(RmsIncidentReport report, List inputs, DateTime now, CancellationToken cancellationToken) + { + await _types.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + var result = new List(); + var ordinal = 0; + foreach (var input in (inputs ?? new List()).Where(i => !string.IsNullOrWhiteSpace(i.TypeCode))) + { + var row = new RmsIncidentType + { + RmsIncidentTypeId = Guid.NewGuid().ToString(), DepartmentId = report.DepartmentId, ProtectionId = Guid.NewGuid().ToString(), RecordId = report.RmsIncidentReportId, + TypeCode = input.TypeCode.Trim(), IsPrimary = input.IsPrimary, LocalCode = ordinal == 0 ? report.DispatchIncidentCode : null, ValueSetVersion = _neris.ContractVersion, + Ordinal = ordinal++, CreatedOn = now, ModifiedOn = now, RowVersion = 1 + }; + await _types.InsertAsync(row, cancellationToken, true); + result.Add(row); + } + if (result.Count > 0 && !result.Any(t => t.IsPrimary)) + result[0].IsPrimary = true; + return result; + } + + private async Task> ReplaceUnitsAsync(RmsIncidentReport report, List inputs, List facts, string userId, DateTime now, CancellationToken cancellationToken) + { + await _units.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + var result = new List(); + var ordinal = 0; + foreach (var input in inputs ?? new List()) + { + Unit unit = null; + if (input.UnitId.HasValue && input.UnitId > 0) + { + unit = await _unitsService.GetUnitByIdAsync(input.UnitId.Value); + if (unit == null || unit.DepartmentId != report.DepartmentId) + throw new ArgumentException($"Unit {input.UnitId} does not belong to this department."); + } + if (unit == null && string.IsNullOrWhiteSpace(input.ReportedUnitId) && string.IsNullOrWhiteSpace(input.UnitNerisId)) + continue; + + var row = new RmsUnitResponse + { + RmsUnitResponseId = Guid.NewGuid().ToString(), DepartmentId = report.DepartmentId, ProtectionId = Guid.NewGuid().ToString(), RecordId = report.RmsIncidentReportId, + UnitId = unit?.UnitId, UnitNameSnapshot = unit?.Name ?? Trim(input.ReportedUnitId), UnitTypeSnapshot = unit?.Type, StationGroupIdSnapshot = unit?.StationGroupId, + UnitNerisId = Trim(input.UnitNerisId)?.ToUpperInvariant(), Staffing = input.Staffing, UnableToDispatch = input.UnableToDispatch, + DispatchedOn = input.DispatchedOn, EnrouteOn = input.EnrouteOn, OnSceneOn = input.OnSceneOn, CanceledEnrouteOn = input.CanceledEnrouteOn, StagingOn = input.StagingOn, ClearedOn = input.ClearedOn, + ResponseMode = Trim(input.ResponseMode)?.ToUpperInvariant(), TransportMode = Trim(input.TransportMode)?.ToUpperInvariant(), + TimesSourceKind = (int)RmsSourceKind.None, Ordinal = ordinal++, CreatedOn = now, ModifiedOn = now, RowVersion = 1 + }; + if (unit != null) + { + Correct(facts, NerisFactKeys.UnitTime(unit.UnitId, "dispatch"), Iso(row.DispatchedOn), userId, now); + Correct(facts, NerisFactKeys.UnitTime(unit.UnitId, "enroute_to_scene"), Iso(row.EnrouteOn), userId, now); + Correct(facts, NerisFactKeys.UnitTime(unit.UnitId, "on_scene"), Iso(row.OnSceneOn), userId, now); + Correct(facts, NerisFactKeys.UnitTime(unit.UnitId, "staging"), Iso(row.StagingOn), userId, now); + Correct(facts, NerisFactKeys.UnitTime(unit.UnitId, "canceled_enroute"), Iso(row.CanceledEnrouteOn), userId, now); + Correct(facts, NerisFactKeys.UnitTime(unit.UnitId, "unit_clear"), Iso(row.ClearedOn), userId, now); + // Provenance survives an edit: the fact keeps its App/Dispatch origin, the row shows the source that still applies. + row.TimesSourceKind = facts.Any(f => f.FactKey.StartsWith($"unit.{unit.UnitId}.", StringComparison.Ordinal) && f.CorrectedOn == null) ? (int)RmsSourceKind.App : (int)RmsSourceKind.None; + } + await _units.InsertAsync(row, cancellationToken, true); + result.Add(row); + } + return result; + } + + private async Task> ReplaceAidsAsync(RmsIncidentReport report, List inputs, DateTime now, CancellationToken cancellationToken) + { + await _aids.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + var result = new List(); + var ordinal = 0; + foreach (var input in inputs ?? new List()) + { + if (!input.IsNonFireDepartment && string.IsNullOrWhiteSpace(input.AidType) && string.IsNullOrWhiteSpace(input.CounterpartNerisId)) + continue; + if (input.IsNonFireDepartment && string.IsNullOrWhiteSpace(input.NonFdType)) + continue; + + var row = new RmsAid + { + RmsAidId = Guid.NewGuid().ToString(), DepartmentId = report.DepartmentId, ProtectionId = Guid.NewGuid().ToString(), RecordId = report.RmsIncidentReportId, + Direction = Trim(input.Direction)?.ToUpperInvariant() ?? "RECEIVED", AidType = Trim(input.AidType)?.ToUpperInvariant() ?? string.Empty, + CounterpartNerisId = Trim(input.CounterpartNerisId)?.ToUpperInvariant(), CounterpartName = Trim(input.CounterpartName), + IsNonFireDepartment = input.IsNonFireDepartment, NonFdType = Trim(input.NonFdType)?.ToUpperInvariant(), + Ordinal = ordinal++, CreatedOn = now, ModifiedOn = now, RowVersion = 1 + }; + await _aids.InsertAsync(row, cancellationToken, true); + result.Add(row); + } + return result; + } + + private async Task> ReplaceTacticsAsync(RmsIncidentReport report, List inputs, DateTime now, CancellationToken cancellationToken) + { + await _tactics.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + var result = new List(); + var ordinal = 0; + foreach (var input in (inputs ?? new List()).Where(i => !string.IsNullOrWhiteSpace(i.TacticCode))) + { + var row = new RmsActionTactic + { + RmsActionTacticId = Guid.NewGuid().ToString(), DepartmentId = report.DepartmentId, ProtectionId = Guid.NewGuid().ToString(), RecordId = report.RmsIncidentReportId, + TacticCode = input.TacticCode.Trim(), ActorUnitId = input.ActorUnitId, OccurredOn = input.OccurredOn, SourceKind = (int)RmsSourceKind.None, + Ordinal = ordinal++, CreatedOn = now, ModifiedOn = now, RowVersion = 1 + }; + await _tactics.InsertAsync(row, cancellationToken, true); + result.Add(row); + } + return result; + } + + private async Task ReplaceNarrativeAsync(RmsIncidentReport report, IncidentReportDraftInput input, DateTime now, CancellationToken cancellationToken) + { + await _narratives.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + var row = new RmsNarrative + { + RmsNarrativeId = Guid.NewGuid().ToString(), DepartmentId = report.DepartmentId, ProtectionId = Guid.NewGuid().ToString(), RecordId = report.RmsIncidentReportId, + Narrative = input.Narrative, ImpedimentNarrative = input.ImpedimentNarrative, OutcomeNarrative = input.OutcomeNarrative, SupplementalJson = input.SupplementalJson, + CreatedOn = now, ModifiedOn = now, RowVersion = 1 + }; + await _narratives.InsertAsync(row, cancellationToken, true); + return row; + } + + private async Task ReplaceDraftRowsFromAsync(RmsIncidentReport report, IncidentReportAggregate source, DateTime now, CancellationToken cancellationToken) + { + await _locations.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + await _types.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + await _units.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + await _aids.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + await _tactics.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + await _narratives.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + await _facts.DeleteDraftForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, cancellationToken); + + if (source.Location != null) await _locations.InsertAsync(Copy(source.Location, l => l.RmsLocationId = Guid.NewGuid().ToString(), null, now), cancellationToken, true); + foreach (var t in source.Types) await _types.InsertAsync(Copy(t, x => x.RmsIncidentTypeId = Guid.NewGuid().ToString(), null, now), cancellationToken, true); + foreach (var u in source.Units) await _units.InsertAsync(Copy(u, x => x.RmsUnitResponseId = Guid.NewGuid().ToString(), null, now), cancellationToken, true); + foreach (var a in source.Aids) await _aids.InsertAsync(Copy(a, x => x.RmsAidId = Guid.NewGuid().ToString(), null, now), cancellationToken, true); + foreach (var t in source.Tactics) await _tactics.InsertAsync(Copy(t, x => x.RmsActionTacticId = Guid.NewGuid().ToString(), null, now), cancellationToken, true); + if (source.Narrative != null) await _narratives.InsertAsync(Copy(source.Narrative, n => n.RmsNarrativeId = Guid.NewGuid().ToString(), null, now), cancellationToken, true); + foreach (var f in source.Facts) await _facts.InsertAsync(Copy(f, x => x.RmsSourceFactId = Guid.NewGuid().ToString(), null, now), cancellationToken, true); + } + + #endregion + + #region Revision / signature / number + + private async Task WriteRevisionAsync(RmsIncidentReport report, IncidentReportAggregate draft, RmsRevisionTransition transition, string userId, string reasonCode, string reasonText, string attestationVersion, DateTime now, CancellationToken cancellationToken) + { + var json = SerializeSnapshot(draft); + var revision = new RmsRevision + { + RmsRevisionId = Guid.NewGuid().ToString(), + DepartmentId = report.DepartmentId, + ProtectionId = Guid.NewGuid().ToString(), + RecordId = report.RmsIncidentReportId, + RecordKind = (int)RmsRecordKind.IncidentReport, + RevisionNumber = report.RevisionCount + 1, + Transition = (int)transition, + PriorRevisionId = report.CurrentRevisionId, + DefinitionKey = report.DefinitionKey, + DefinitionVersion = report.DefinitionVersion, + SnapshotJson = json, + Checksum = RecordSnapshotSerializer.Checksum(json), + ActorUserId = userId, + ReasonCode = reasonCode, + ReasonText = reasonText, + AttestationStatementVersion = transition == RmsRevisionTransition.Voided ? null : attestationVersion, + AttestedOn = transition == RmsRevisionTransition.Voided ? (DateTime?)null : now, + OriginClient = report.OriginClient, + CreatedOn = now + }; + await _revisions.InsertAsync(revision, cancellationToken, true); + + // Revision-bound copies keep finalized data queryable without touching the draft rows. + var id = revision.RmsRevisionId; + if (draft.Location != null) await _locations.InsertAsync(Copy(draft.Location, l => l.RmsLocationId = Guid.NewGuid().ToString(), id, now), cancellationToken, true); + foreach (var t in draft.Types) await _types.InsertAsync(Copy(t, x => x.RmsIncidentTypeId = Guid.NewGuid().ToString(), id, now), cancellationToken, true); + foreach (var u in draft.Units) await _units.InsertAsync(Copy(u, x => x.RmsUnitResponseId = Guid.NewGuid().ToString(), id, now), cancellationToken, true); + foreach (var a in draft.Aids) await _aids.InsertAsync(Copy(a, x => x.RmsAidId = Guid.NewGuid().ToString(), id, now), cancellationToken, true); + foreach (var t in draft.Tactics) await _tactics.InsertAsync(Copy(t, x => x.RmsActionTacticId = Guid.NewGuid().ToString(), id, now), cancellationToken, true); + if (draft.Narrative != null) await _narratives.InsertAsync(Copy(draft.Narrative, n => n.RmsNarrativeId = Guid.NewGuid().ToString(), id, now), cancellationToken, true); + foreach (var f in draft.Facts) await _facts.InsertAsync(Copy(f, x => x.RmsSourceFactId = Guid.NewGuid().ToString(), id, now), cancellationToken, true); + + return revision; + } + + private async Task WriteSignatureAsync(RmsIncidentReport report, RmsRevision revision, string userId, string statementVersion, string ipAddress, DateTime now, CancellationToken cancellationToken) + { + var profile = await _profiles.GetProfileByUserIdAsync(userId, false); + var roles = await _roles.GetRolesForUserAsync(userId, report.DepartmentId); + await _signatures.InsertAsync(new RmsSignature + { + RmsSignatureId = Guid.NewGuid().ToString(), + DepartmentId = report.DepartmentId, + ProtectionId = Guid.NewGuid().ToString(), + RecordId = report.RmsIncidentReportId, + RecordKind = (int)RmsRecordKind.IncidentReport, + RevisionId = revision.RmsRevisionId, + SignerUserId = userId, + SignerNameSnapshot = profile == null ? null : $"{profile.FirstName} {profile.LastName}".Trim(), + SignerRoleSnapshot = roles == null || roles.Count == 0 ? null : string.Join(", ", roles.Select(r => r.Name).Where(n => !string.IsNullOrWhiteSpace(n))), + Intent = (int)RmsSignatureIntent.Attestation, + StatementVersion = statementVersion, + StatementText = AttestationStatement(statementVersion), + Method = (int)RmsSignatureMethod.WebAttestation, + SignedOn = now, + IpAddress = ipAddress, + ArtifactChecksum = revision.Checksum, + CreatedOn = now, + RowVersion = 1 + }, cancellationToken, true); + } + + public static string AttestationStatement(string version) + { + return "I attest that this incident report is accurate and complete to the best of my knowledge."; + } + + private async Task AllocateRecordNumberAsync(RmsIncidentReport report, CancellationToken cancellationToken) + { + var config = await _settings.GetRecordsNumberingConfigAsync(report.DepartmentId); + var year = (report.CallCreatedOn ?? DateTime.UtcNow).Year; + var prefix = NumberPrefix + "-"; + if (config.PerGroupSequence && report.StationGroupId.HasValue) + prefix += "G" + report.StationGroupId.Value + "-"; + if (config.IncludeYear) + prefix += year + "-"; + var width = Math.Max(3, Math.Min(8, config.SequenceWidth <= 0 ? 4 : config.SequenceWidth)); + var sequence = await _reports.GetMaxRecordNumberSequenceAsync(report.DepartmentId, prefix) + 1; + return prefix + sequence.ToString("D" + width); + } + + /// Scoped idempotency key (plan 5.3): stable for a revision, new for every new revision. + public static string IdempotencyKey(int departmentId, string reportId, string revisionId) + { + using var sha = SHA256.Create(); + return Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes($"neris:{departmentId}:{reportId}:{revisionId}"))).ToLowerInvariant(); + } + + #endregion + + #region Hydration / projection / scope / events + + private async Task HydrateAsync(RmsIncidentReport report, string revisionId, bool includeHistory) + { + var dept = report.DepartmentId; + var id = report.RmsIncidentReportId; + var aggregate = new IncidentReportAggregate + { + Report = report, + Location = (await _locations.GetForRecordAsync(dept, id, revisionId))?.FirstOrDefault(), + Types = (await _types.GetForRecordAsync(dept, id, revisionId))?.ToList() ?? new List(), + Units = (await _units.GetForRecordAsync(dept, id, revisionId))?.ToList() ?? new List(), + Aids = (await _aids.GetForRecordAsync(dept, id, revisionId))?.ToList() ?? new List(), + Tactics = (await _tactics.GetForRecordAsync(dept, id, revisionId))?.ToList() ?? new List(), + Narrative = (await _narratives.GetForRecordAsync(dept, id, revisionId))?.FirstOrDefault(), + Facts = (await _facts.GetForRecordAsync(dept, id, revisionId))?.ToList() ?? new List(), + Issues = (await _issues.GetForRecordAsync(dept, id))?.ToList() ?? new List(), + GroupScope = (await _scopes.GetForRecordAsync(dept, id))?.ToList() ?? new List() + }; + if (includeHistory) + { + aggregate.Submissions = (await _submissions.GetForRecordAsync(dept, id))?.ToList() ?? new List(); + aggregate.Signatures = (await _signatures.GetForRecordAsync(dept, id))?.ToList() ?? new List(); + aggregate.Revisions = (await _revisions.GetForRecordAsync(dept, id))?.ToList() ?? new List(); + } + return aggregate; + } + + private async Task RefreshProjectionAsync(RmsIncidentReport report, CancellationToken cancellationToken) + { + await UpsertProjectionAsync(await HydrateAsync(report, null, false), cancellationToken); + } + + private async Task UpsertProjectionAsync(IncidentReportAggregate aggregate, CancellationToken cancellationToken) + { + var report = aggregate.Report; + var existing = await _projections.GetByRecordIdAsync(report.DepartmentId, report.RmsIncidentReportId); + var projection = existing ?? new RmsRecordSearchProjection + { + RmsRecordSearchProjectionId = report.RmsIncidentReportId, + DepartmentId = report.DepartmentId, + ProtectionId = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + RowVersion = 0 + }; + + projection.SourceType = (int)RmsSearchSourceType.Record; + projection.SourceId = report.RmsIncidentReportId; + projection.RecordKind = (int)RmsRecordKind.IncidentReport; + projection.RecordNumber = report.RecordNumber; + projection.DraftReference = report.DraftReference; + projection.DefinitionKey = report.DefinitionKey; + projection.DefinitionVersion = report.DefinitionVersion; + projection.RecordType = null; + projection.State = report.State; + projection.OccurredOn = report.CallCreatedOn; + projection.RecordCreatedOn = report.CreatedOn; + projection.FinalizedOn = report.FinalizedOn; + projection.StationGroupId = report.StationGroupId; + projection.CallId = report.CallId; + projection.CallNumber = report.IncidentNumber; + projection.AuthorUserId = report.AuthorUserId; + projection.OwnerUserId = report.OwnerUserId; + projection.ReviewerUserId = report.ReviewerUserId; + projection.ParticipantUserIds = string.Empty; + projection.UnitIds = string.Join(",", aggregate.Units.Where(u => u.UnitId.HasValue).Select(u => u.UnitId.Value).Distinct()); + projection.GroupScopeIds = string.Join(",", (aggregate.GroupScope ?? new List()).Select(s => s.DepartmentGroupId).Distinct()); + projection.DisplaySummary = report.DisplaySummary; + // Safe fields only (plan 5.10): numbers, summary, incident number, type codes; never narrative or address detail. + projection.SearchText = string.Join(" ", new[] { report.RecordNumber, report.DraftReference, report.DisplaySummary, report.IncidentNumber, report.NerisIncidentId }.Concat(aggregate.Types.Select(t => t.TypeCode)).Where(s => !string.IsNullOrWhiteSpace(s))); + projection.IsLegacy = false; + projection.ProjectionVersion = RmsRecordSearchProjection.CurrentProjectionVersion; + projection.ProtectedCatalogVersion = await SafeCatalogVersionAsync(report.DepartmentId); + projection.PolicyEpoch = await SafePolicyEpochAsync(report.DepartmentId); + projection.ModifiedOn = DateTime.UtcNow; + projection.RowVersion += 1; + projection.DeletedOn = report.DeletedOn; + + if (existing == null) + await _projections.InsertAsync(projection, cancellationToken, true); + else + await _projections.UpdateAsync(projection, cancellationToken, true); + } + + private async Task RecomputeGroupScopeAsync(IncidentReportAggregate aggregate, int? authorGroupId, CancellationToken cancellationToken) + { + var report = aggregate.Report; + var scopes = new List(); + var seen = new HashSet(StringComparer.Ordinal); + void add(int? groupId, RmsGroupScopeAnchorType anchor) + { + if (!groupId.HasValue || !seen.Add(groupId.Value + ":" + (int)anchor)) + return; + scopes.Add(new RmsRecordGroupScope { DepartmentId = report.DepartmentId, RecordId = report.RmsIncidentReportId, DepartmentGroupId = groupId.Value, AnchorType = (int)anchor, CreatedOn = DateTime.UtcNow }); + } + add(report.StationGroupId, RmsGroupScopeAnchorType.RecordGroup); + add(authorGroupId ?? (await _groups.GetGroupForUserAsync(report.AuthorUserId, report.DepartmentId))?.DepartmentGroupId, RmsGroupScopeAnchorType.Author); + foreach (var unit in aggregate.Units) + add(unit.StationGroupIdSnapshot, RmsGroupScopeAnchorType.Unit); + var now = DateTime.UtcNow; + foreach (var share in (await _shares.GetForRecordAsync(report.DepartmentId, report.RmsIncidentReportId) ?? Enumerable.Empty()).Where(s => s.IsEffective(now))) + add(share.DepartmentGroupId, RmsGroupScopeAnchorType.Share); + await _scopes.ReplaceForRecordAsync(report.DepartmentId, report.RmsIncidentReportId, scopes, cancellationToken); + aggregate.GroupScope = scopes; + } + + private async Task EnqueueLifecycleEventAsync(RmsIncidentReport report, RmsRevision revision, WorkflowTriggerEventType trigger, RmsRecordState from, RmsRecordState to, string reasonCode, object submission, CancellationToken cancellationToken, object extra = null) + { + var payload = new Dictionary + { + ["record"] = RecordBlock(report, revision, to), + ["record_change"] = new + { + previous_state = from.ToString(), + current_state = to.ToString(), + prior_revision_id = revision?.PriorRevisionId, + current_revision_id = revision?.RmsRevisionId ?? report.CurrentRevisionId, + reason_code = reasonCode + } + }; + if (submission != null) + payload["submission"] = submission; + if (extra != null) + payload["extra"] = extra; + + return await _outbox.EnqueueAsync(report.DepartmentId, DomainEventProducers.Records, new DomainEventEnvelope + { + EventName = trigger.ToString(), + SchemaVersion = 1, + AggregateType = IncidentAggregate, + AggregateId = report.RmsIncidentReportId, + AggregateVersion = revision?.RevisionNumber ?? report.RevisionCount, + Trigger = trigger, + Payload = payload, + CorrelationId = report.RmsIncidentReportId, + OriginClient = (RmsOriginClient)report.OriginClient + }, cancellationToken); + } + + /// The record.* block for incident reports; kind and the NERIS ID distinguish it from operational records. + public static object RecordBlock(RmsIncidentReport report, RmsRevision revision, RmsRecordState state) + { + return new + { + id = report.RmsIncidentReportId, + kind = "IncidentReport", + record_number = report.RecordNumber, + draft_reference = report.DraftReference, + definition_key = report.DefinitionKey, + definition_version = report.DefinitionVersion, + type_key = "NerisIncident", + state = state.ToString(), + lifecycle_preset = ((RmsLifecyclePreset)report.LifecyclePreset).ToString(), + department_id = report.DepartmentId, + station_group_id = report.StationGroupId, + call_id = report.CallId, + external_id = report.NerisIncidentId, + author_user_id = report.AuthorUserId, + owner_user_id = report.OwnerUserId, + started_on = report.CallCreatedOn, + ended_on = report.IncidentClearedOn, + created_on = report.CreatedOn, + finalized_on = report.FinalizedOn, + revision_id = revision?.RmsRevisionId ?? report.CurrentRevisionId, + revision_number = revision?.RevisionNumber ?? report.RevisionCount, + checksum = revision?.Checksum, + summary = report.DisplaySummary, + incident_number = report.IncidentNumber, + neris_incident_id = report.NerisIncidentId + }; + } + + /// The sanitized submission.* block (plan 5.6): state, external status, attempts, codes and paths; never payload or response bodies. + public static object SubmissionBlock(RmsSubmission submission) + { + return new + { + id = submission.RmsSubmissionId, + destination = submission.Destination, + destination_version = submission.DestinationVersion, + state = ((RmsSubmissionState)submission.State).ToString(), + external_id = submission.ExternalId, + external_status = submission.ExternalStatus, + attempts = submission.Attempts, + max_attempts = submission.MaxAttempts, + error_summary = submission.ErrorSummary, + queued_on = submission.QueuedOn, + sent_on = submission.SentOn, + completed_on = submission.CompletedOn + }; + } + + #endregion + + #region Small helpers + + private async Task LoadAsync(int departmentId, string reportId) + { + var report = string.IsNullOrWhiteSpace(reportId) ? null : await _reports.GetByIdForDepartmentAsync(departmentId, reportId); + if (report == null || report.DeletedOn.HasValue) + throw new ArgumentException($"Incident report {reportId} was not found.", nameof(reportId)); + return report; + } + + private static void RequireEditable(RmsIncidentReport report) + { + var state = (RmsRecordState)report.State; + if (RmsLifecycle.IsEditable(state) || state == RmsRecordState.Rejected || report.AmendsRevisionId != null) + return; + throw new RecordTransitionException(report.RmsIncidentReportId, state, state, "the report is not editable in its current state"); + } + + private static void RequireTransition(RmsIncidentReport report, RmsRecordState from, RmsRecordState to) + { + if (!RmsLifecycle.CanTransition((RmsLifecyclePreset)report.LifecyclePreset, from, to)) + throw new RecordTransitionException(report.RmsIncidentReportId, from, to); + } + + private async Task GuardVersionAsync(RmsIncidentReport report, long expectedRowVersion, CancellationToken cancellationToken) + { + if (!await _reports.TryBumpRowVersionAsync(report.DepartmentId, report.RmsIncidentReportId, expectedRowVersion, cancellationToken)) + { + var current = await _reports.GetByIdForDepartmentAsync(report.DepartmentId, report.RmsIncidentReportId); + throw new RecordConcurrencyException(report.RmsIncidentReportId, expectedRowVersion, current?.RowVersion ?? -1); + } + report.RowVersion = expectedRowVersion + 1; + } + + private async Task InTransactionAsync(Func work) + { + _unitOfWork.CreateOrGetConnection(); + try + { + await work(); + _unitOfWork.CommitChanges(); + } + catch + { + _unitOfWork.DiscardChanges(); + throw; + } + } + + private Task AuditAsync(int departmentId, string userId, string recordId, string revisionId, RmsAccessAuditAction action, string purpose, RmsOriginClient origin, CancellationToken cancellationToken, object detail = null, string ipAddress = null) + { + return _audits.InsertAsync(new RmsAccessAudit + { + DepartmentId = departmentId, + RecordId = recordId, + RevisionId = revisionId, + Action = (int)action, + ActorUserId = userId, + Purpose = purpose, + OriginClient = (int)origin, + IpAddress = ipAddress, + Successful = true, + OccurredOn = DateTime.UtcNow, + DetailJson = detail == null ? null : JsonConvert.SerializeObject(detail) + }, cancellationToken, true); + } + + private async Task SafeCatalogVersionAsync(int departmentId) + { + try { return await _dataProtection.GetPinnedCatalogVersionAsync(departmentId); } + catch (Exception ex) { Logging.LogException(ex); return 0; } + } + + private async Task SafePolicyEpochAsync(int departmentId) + { + try { return (await _dataProtection.GetPolicyByDepartmentIdAsync(departmentId))?.PolicyEpoch ?? 0; } + catch (Exception ex) { Logging.LogException(ex); return 0; } + } + + private static string BuildSummary(RmsIncidentReport report, List types, string callName) + { + var primary = types?.FirstOrDefault(t => t.IsPrimary)?.TypeCode ?? types?.FirstOrDefault()?.TypeCode; + var label = primary == null ? (string.IsNullOrWhiteSpace(report.DispatchIncidentCode) ? "Incident" : report.DispatchIncidentCode) : primary.Split(new[] { "||" }, StringSplitOptions.None).Last().Replace('_', ' '); + var number = report.IncidentNumber ?? report.DraftReference; + var text = $"{label} - {number}"; + if (!string.IsNullOrWhiteSpace(callName)) + text += " - " + callName.Trim(); + return text.Length > 400 ? text.Substring(0, 400) : text; + } + + private static T Copy(T source, Action assignId, string revisionId, DateTime now) where T : class + { + var copy = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(source)); + assignId(copy); + var type = typeof(T); + type.GetProperty("RevisionId")?.SetValue(copy, revisionId); + type.GetProperty("CreatedOn")?.SetValue(copy, now); + type.GetProperty("ModifiedOn")?.SetValue(copy, now); + type.GetProperty("RowVersion")?.SetValue(copy, 1L); + return copy; + } + + private static string SerializeSnapshot(IncidentReportAggregate aggregate) + { + var snapshot = new + { + SnapshotVersion = 1, + aggregate.Report, + aggregate.Location, + aggregate.Types, + aggregate.Units, + aggregate.Aids, + aggregate.Tactics, + aggregate.Narrative, + aggregate.Facts + }; + return JsonConvert.SerializeObject(snapshot, Formatting.None); + } + + private static List SplitCsv(string csv) + { + return string.IsNullOrWhiteSpace(csv) ? new List() : csv.Split(',', StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).Where(s => s.Length > 0).Distinct().ToList(); + } + + private static string Trim(string value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + + private static string NewDraftReference() + { + const string alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + var bytes = new byte[5]; + using (var rng = RandomNumberGenerator.Create()) + rng.GetBytes(bytes); + var chars = new char[5]; + for (var i = 0; i < 5; i++) + chars[i] = alphabet[bytes[i] % alphabet.Length]; + return "I-" + new string(chars); + } + + #endregion + } +} diff --git a/Core/Resgrid.Services/Records/RecordSnapshotSerializer.cs b/Core/Resgrid.Services/Records/RecordSnapshotSerializer.cs index c8cd6c7e..ae433fe8 100644 --- a/Core/Resgrid.Services/Records/RecordSnapshotSerializer.cs +++ b/Core/Resgrid.Services/Records/RecordSnapshotSerializer.cs @@ -37,7 +37,7 @@ public static class RecordSnapshotSerializer nameof(RmsOperationalRecordDetail.Destination) }; - private static readonly string[] DetailFieldOrder = + public static readonly string[] DetailFieldOrder = { nameof(RmsOperationalRecordDetail.Narrative), nameof(RmsOperationalRecordDetail.InitialReport), nameof(RmsOperationalRecordDetail.Type), nameof(RmsOperationalRecordDetail.Course), nameof(RmsOperationalRecordDetail.CourseCode), nameof(RmsOperationalRecordDetail.Instructors), diff --git a/Core/Resgrid.Services/Records/RecordsApiSupport.cs b/Core/Resgrid.Services/Records/RecordsApiSupport.cs new file mode 100644 index 00000000..62dc91f3 --- /dev/null +++ b/Core/Resgrid.Services/Records/RecordsApiSupport.cs @@ -0,0 +1,366 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Resgrid.Framework; +using Resgrid.Model; +using Resgrid.Model.Providers; +using Resgrid.Model.Services; + +namespace Resgrid.Services.Records +{ + /// + /// Keyed short-lived state for the v4 Records contract. Redis (through ICacheProvider string entries) when caching + /// is on and connected; a process-local dictionary otherwise, which is single-node only and says so in the log. + /// + public class RecordsApiStateStore : IRecordsApiStateStore + { + private static readonly ConcurrentDictionary Local = new ConcurrentDictionary(StringComparer.Ordinal); + private static int _localWarned; + private readonly ICacheProvider _cache; + + public RecordsApiStateStore(ICacheProvider cache) + { + _cache = cache; + } + + private bool UseCache => Config.SystemBehaviorConfig.CacheEnabled && _cache != null && SafeConnected(); + + private bool SafeConnected() + { + try { return _cache.IsConnected(); } + catch { return false; } + } + + public async Task GetAsync(string key) + { + if (UseCache) + return await _cache.GetStringAsync(key); + + Sweep(); + return Local.TryGetValue(key, out var entry) && entry.ExpiresOn > DateTime.UtcNow ? entry.Value : null; + } + + public async Task SetAsync(string key, string value, TimeSpan timeToLive) + { + if (UseCache) + { + await _cache.SetStringAsync(key, value, timeToLive); + return; + } + + if (Interlocked.Exchange(ref _localWarned, 1) == 0) + Logging.LogInfo("Records API state (upload sessions, idempotency) is process-local because caching is off; resumable uploads must reach the same node."); + Sweep(); + Local[key] = (value, DateTime.UtcNow.Add(timeToLive)); + } + + public async Task RemoveAsync(string key) + { + if (UseCache) + { + await _cache.RemoveAsync(key); + return; + } + Local.TryRemove(key, out _); + } + + private static void Sweep() + { + if (Local.Count < 512) + return; + var now = DateTime.UtcNow; + foreach (var kv in Local.Where(kv => kv.Value.ExpiresOn <= now).ToList()) + Local.TryRemove(kv.Key, out _); + } + + /// Test seam: clears the process-local store. + public static void ResetLocal() => Local.Clear(); + } + + /// Scoped command idempotency (plan section 5.3): remembered for a day, keyed by department, user and the client's key. + public class RecordsApiIdempotencyService : IRecordsApiIdempotencyService + { + public static readonly TimeSpan Retention = TimeSpan.FromHours(24); + private readonly IRecordsApiStateStore _store; + + public RecordsApiIdempotencyService(IRecordsApiStateStore store) + { + _store = store; + } + + public static string Key(int departmentId, string userId, string idempotencyKey) => $"RecordsApiCmd_{departmentId}_{userId}_{idempotencyKey}"; + + public Task TryGetRecordIdAsync(int departmentId, string userId, string idempotencyKey) + { + if (string.IsNullOrWhiteSpace(idempotencyKey) || string.IsNullOrWhiteSpace(userId)) + return Task.FromResult(null); + return _store.GetAsync(Key(departmentId, userId, idempotencyKey.Trim())); + } + + public Task RememberAsync(int departmentId, string userId, string idempotencyKey, string recordId) + { + if (string.IsNullOrWhiteSpace(idempotencyKey) || string.IsNullOrWhiteSpace(userId) || string.IsNullOrWhiteSpace(recordId)) + return Task.CompletedTask; + return _store.SetAsync(Key(departmentId, userId, idempotencyKey.Trim()), recordId, Retention); + } + } + + /// + /// Resumable attachment upload sessions (plan section 5.3). The client declares size and SHA-256, sends ordered + /// chunks (each resumable from the reported ReceivedBytes), and completes; completion verifies the checksum and + /// then stores through RecordsService.AddAttachmentAsync, so hygiene, scanning and authorization apply exactly as + /// they do to a direct upload. Sessions expire after a day and never outlive the record's editability. + /// + public class RecordAttachmentUploadService : IRecordAttachmentUploadService + { + public const int DefaultChunkSize = 512 * 1024; + public static readonly TimeSpan SessionLifetime = TimeSpan.FromHours(24); + + private readonly IRecordsApiStateStore _store; + private readonly IRecordsService _records; + + public RecordAttachmentUploadService(IRecordsApiStateStore store, IRecordsService records) + { + _store = store; + _records = records; + } + + public int ChunkSize => DefaultChunkSize; + + public static string SessionKey(int departmentId, string uploadId) => $"RecordsApiUpload_{departmentId}_{uploadId}"; + public static string ChunkKey(int departmentId, string uploadId, int index) => $"RecordsApiUploadChunk_{departmentId}_{uploadId}_{index}"; + + public async Task BeginAsync(int departmentId, string userId, string recordId, string fileName, string contentType, long declaredSize, string sha256) + { + if (string.IsNullOrWhiteSpace(recordId)) throw new ArgumentException("A record is required.", nameof(recordId)); + if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("A file name is required.", nameof(fileName)); + if (declaredSize <= 0) throw new RecordUploadSessionException("too_large", "The declared size must be positive."); + if (declaredSize > RecordAttachmentHygiene.MaxBytes) throw new RecordUploadSessionException("too_large", $"Attachments are limited to {RecordAttachmentHygiene.MaxBytes / (1024 * 1024)} MB."); + if (!IsSha256(sha256)) throw new RecordUploadSessionException("checksum_mismatch", "A lower-case hex SHA-256 of the file is required."); + + var aggregate = await _records.GetAsync(departmentId, recordId); + if (aggregate == null) + throw new ArgumentException($"Record {recordId} was not found.", nameof(recordId)); + var state = (RmsRecordState)aggregate.Record.State; + if (!RmsLifecycle.IsEditable(state) && aggregate.Record.AmendsRevisionId == null) + throw new RecordTransitionException(recordId, state, state, "attachments can only be added to an editable draft"); + + var now = DateTime.UtcNow; + var session = new RecordAttachmentUploadSession + { + UploadId = Guid.NewGuid().ToString("N"), + DepartmentId = departmentId, + RecordId = recordId, + UserId = userId, + FileName = System.IO.Path.GetFileName(fileName.Trim()), + ContentType = string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType.Trim(), + DeclaredSize = declaredSize, + Sha256 = sha256.Trim().ToLowerInvariant(), + ChunkSize = ChunkSize, + ChunkCount = (int)((declaredSize + ChunkSize - 1) / ChunkSize), + CreatedOn = now, + ExpiresOn = now.Add(SessionLifetime) + }; + await SaveAsync(session); + return session; + } + + public async Task GetAsync(int departmentId, string userId, string uploadId) + { + var session = await LoadAsync(departmentId, uploadId); + if (session == null) + return null; + if (!string.Equals(session.UserId, userId, StringComparison.OrdinalIgnoreCase)) + return null; + return session; + } + + public async Task AppendAsync(int departmentId, string userId, string uploadId, long offset, byte[] data) + { + var session = await RequireOpenAsync(departmentId, userId, uploadId); + if (data == null || data.Length == 0) + throw new RecordUploadSessionException("bad_offset", "A chunk must carry data."); + if (offset != session.ReceivedBytes) + throw new RecordUploadSessionException("bad_offset", $"Chunks must be sent in order; the next offset is {session.ReceivedBytes}."); + if (offset % session.ChunkSize != 0) + throw new RecordUploadSessionException("bad_offset", $"Offsets must be multiples of {session.ChunkSize}."); + if (data.Length > session.ChunkSize) + throw new RecordUploadSessionException("bad_offset", $"A chunk may carry at most {session.ChunkSize} bytes."); + if (session.ReceivedBytes + data.Length > session.DeclaredSize) + throw new RecordUploadSessionException("too_large", "The upload exceeds its declared size."); + var index = (int)(offset / session.ChunkSize); + if (data.Length < session.ChunkSize && index != session.ChunkCount - 1) + throw new RecordUploadSessionException("bad_offset", "Only the last chunk may be shorter than the chunk size."); + + await _store.SetAsync(ChunkKey(departmentId, uploadId, index), Convert.ToBase64String(data), SessionLifetime); + session.ReceivedBytes += data.Length; + await SaveAsync(session); + return session; + } + + public async Task CompleteAsync(int departmentId, string userId, string uploadId, string description, CancellationToken cancellationToken = default) + { + var session = await RequireOpenAsync(departmentId, userId, uploadId); + if (!session.IsComplete) + throw new RecordUploadSessionException("incomplete", $"{session.ReceivedBytes} of {session.DeclaredSize} bytes received."); + + var buffer = new byte[session.DeclaredSize]; + long position = 0; + for (var index = 0; index < session.ChunkCount; index++) + { + var chunk = await _store.GetAsync(ChunkKey(departmentId, uploadId, index)); + if (chunk == null) + throw new RecordUploadSessionException("expired", $"Chunk {index} is no longer available; restart the upload."); + var bytes = Convert.FromBase64String(chunk); + Buffer.BlockCopy(bytes, 0, buffer, (int)position, bytes.Length); + position += bytes.Length; + } + if (position != session.DeclaredSize) + throw new RecordUploadSessionException("incomplete", "The assembled file does not match the declared size."); + + var actual = RecordSnapshotSerializer.Checksum(buffer); + if (!string.Equals(actual, session.Sha256, StringComparison.Ordinal)) + throw new RecordUploadSessionException("checksum_mismatch", "The uploaded bytes do not match the declared SHA-256; restart the upload."); + + RmsRecordAttachment attachment; + try + { + attachment = await _records.AddAttachmentAsync(departmentId, userId, session.RecordId, session.FileName, session.ContentType, buffer, description, cancellationToken); + } + catch (RecordAttachmentRejectedException ex) + { + throw new RecordUploadSessionException("rejected", ex.Message); + } + + session.State = RecordUploadSessionState.Completed; + session.AttachmentId = attachment.RmsRecordAttachmentId; + await SaveAsync(session); + await RemoveChunksAsync(session); + return attachment; + } + + public async Task AbortAsync(int departmentId, string userId, string uploadId) + { + var session = await GetAsync(departmentId, userId, uploadId); + if (session == null) + return false; + session.State = RecordUploadSessionState.Aborted; + await SaveAsync(session); + await RemoveChunksAsync(session); + return true; + } + + private async Task RequireOpenAsync(int departmentId, string userId, string uploadId) + { + var session = await GetAsync(departmentId, userId, uploadId); + if (session == null) + throw new RecordUploadSessionException("not_found", "The upload session was not found."); + if (session.ExpiresOn <= DateTime.UtcNow) + throw new RecordUploadSessionException("expired", "The upload session has expired; restart the upload."); + if (session.State != RecordUploadSessionState.Open) + throw new RecordUploadSessionException("closed", $"The upload session is {session.State}."); + return session; + } + + private async Task LoadAsync(int departmentId, string uploadId) + { + if (string.IsNullOrWhiteSpace(uploadId)) + return null; + var json = await _store.GetAsync(SessionKey(departmentId, uploadId)); + return json == null ? null : JsonConvert.DeserializeObject(json); + } + + private Task SaveAsync(RecordAttachmentUploadSession session) + { + return _store.SetAsync(SessionKey(session.DepartmentId, session.UploadId), JsonConvert.SerializeObject(session), SessionLifetime); + } + + private async Task RemoveChunksAsync(RecordAttachmentUploadSession session) + { + for (var index = 0; index < session.ChunkCount; index++) + await _store.RemoveAsync(ChunkKey(session.DepartmentId, session.UploadId, index)); + } + + public static bool IsSha256(string value) + { + return !string.IsNullOrWhiteSpace(value) && value.Trim().Length == 64 && value.Trim().All(c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')); + } + + public static string Sha256Hex(byte[] data) + { + using var sha = SHA256.Create(); + return string.Concat(sha.ComputeHash(data ?? Array.Empty()).Select(b => b.ToString("x2"))); + } + } + + /// Names the field paths a stale draft save would have changed, so a client reconciles instead of overwriting (plan section 5.3). + public static class RecordDraftConflictResolver + { + public static RecordDraftConflict Describe(RecordDraftInput input, RecordAggregate current, long expectedRowVersion) + { + var record = current.Record; + var conflict = new RecordDraftConflict + { + RecordId = record.RmsOperationalRecordId, + ExpectedRowVersion = expectedRowVersion, + CurrentRowVersion = record.RowVersion, + CurrentState = (RmsRecordState)record.State, + CurrentRevisionId = record.CurrentRevisionId + }; + if (input == null) + return conflict; + + void Compare(string path, string mine, string theirs) + { + if (!string.Equals(mine ?? string.Empty, theirs ?? string.Empty, StringComparison.Ordinal)) + conflict.ChangedFieldPaths.Add(path); + } + + Compare("StartedOn", Iso(input.StartedOn), Iso(record.StartedOn)); + Compare("EndedOn", Iso(input.EndedOn), Iso(record.EndedOn)); + Compare("StationGroupId", input.StationGroupId?.ToString(), record.StationGroupId?.ToString()); + Compare("CallId", input.CallId?.ToString(), record.CallId?.ToString()); + Compare("ExternalId", input.ExternalId, record.ExternalId); + + var mineDetails = input.Details ?? new RmsOperationalRecordDetail(); + var theirDetails = current.Details ?? new RmsOperationalRecordDetail(); + foreach (var field in RecordSnapshotSerializer.DetailFieldOrder) + { + var property = typeof(RmsOperationalRecordDetail).GetProperty(field); + if (property == null) + continue; + Compare("Details." + field, Stringify(property.GetValue(mineDetails)), Stringify(property.GetValue(theirDetails))); + } + + var mineParticipants = new HashSet((input.Participants ?? new List()).Select(p => p.UserId ?? string.Empty), StringComparer.OrdinalIgnoreCase); + var theirParticipants = new HashSet(current.Participants.Select(p => p.UserId ?? string.Empty), StringComparer.OrdinalIgnoreCase); + if (!mineParticipants.SetEquals(theirParticipants)) + conflict.ChangedFieldPaths.Add("Participants"); + + var mineUnits = new HashSet((input.Units ?? new List()).Select(u => $"{u.UnitId}|{Iso(u.Dispatched)}|{Iso(u.Enroute)}|{Iso(u.OnScene)}|{Iso(u.Released)}|{Iso(u.InQuarters)}"), StringComparer.Ordinal); + var theirUnits = new HashSet(current.Units.Select(u => $"{u.UnitId}|{Iso(u.Dispatched)}|{Iso(u.Enroute)}|{Iso(u.OnScene)}|{Iso(u.Released)}|{Iso(u.InQuarters)}"), StringComparer.Ordinal); + if (!mineUnits.SetEquals(theirUnits)) + conflict.ChangedFieldPaths.Add("Units"); + + return conflict; + } + + private static string Iso(DateTime? value) => value?.ToUniversalTime().ToString("O"); + + private static string Stringify(object value) + { + switch (value) + { + case null: return null; + case DateTime dt: return dt.ToUniversalTime().ToString("O"); + case bool b: return b ? "true" : "false"; + default: return Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture); + } + } + } +} diff --git a/Core/Resgrid.Services/Records/RecordsAuthorizationService.cs b/Core/Resgrid.Services/Records/RecordsAuthorizationService.cs index 3f77342e..e1ce7fa1 100644 --- a/Core/Resgrid.Services/Records/RecordsAuthorizationService.cs +++ b/Core/Resgrid.Services/Records/RecordsAuthorizationService.cs @@ -32,13 +32,15 @@ public class RecordsAuthorizationService : IRecordsAuthorizationService private readonly IRmsRecordParticipantsRepository _participantsRepository; private readonly ICacheProvider _cacheProvider; private readonly IRmsLegacyStatsRepository _legacyStats; + private readonly IRmsIncidentReportsRepository _incidentReports; public RecordsAuthorizationService(IPermissionsService permissionsService, IDepartmentsService departmentsService, IDepartmentGroupsService departmentGroupsService, IPersonnelRolesService personnelRolesService, IDepartmentSettingsService departmentSettingsService, IRmsOperationalRecordsRepository recordsRepository, IRmsRecordGroupScopesRepository groupScopesRepository, - IRmsRecordParticipantsRepository participantsRepository, ICacheProvider cacheProvider, IRmsLegacyStatsRepository legacyStats) + IRmsRecordParticipantsRepository participantsRepository, ICacheProvider cacheProvider, IRmsLegacyStatsRepository legacyStats, IRmsIncidentReportsRepository incidentReports) { _legacyStats = legacyStats; + _incidentReports = incidentReports; _permissionsService = permissionsService; _departmentsService = departmentsService; _departmentGroupsService = departmentGroupsService; @@ -111,24 +113,40 @@ public async Task CanUserViewRecordAsync(string userId, string recordId, i { try { + // Both aggregates share the id space, the group-scope table and this rule (RMS-2 incident reports have no participants). + string author, owner, reviewer, approver; + var isOperational = true; var record = await _recordsRepository.GetByIdForDepartmentAsync(departmentId, recordId); - if (record == null || record.DeletedOn.HasValue) - return false; + if (record != null && !record.DeletedOn.HasValue) + { + author = record.AuthorUserId; owner = record.OwnerUserId; reviewer = record.ReviewerUserId; approver = record.ApproverUserId; + } + else + { + var report = await _incidentReports.GetByIdForDepartmentAsync(departmentId, recordId); + if (report == null || report.DeletedOn.HasValue) + return false; + isOperational = false; + author = report.AuthorUserId; owner = report.OwnerUserId; reviewer = report.ReviewerUserId; approver = null; + } // Always-visible cases resolve before any group evaluation. - if (string.Equals(record.AuthorUserId, userId, StringComparison.Ordinal) || - string.Equals(record.OwnerUserId, userId, StringComparison.Ordinal) || - string.Equals(record.ReviewerUserId, userId, StringComparison.Ordinal) || - string.Equals(record.ApproverUserId, userId, StringComparison.Ordinal)) + if (string.Equals(author, userId, StringComparison.Ordinal) || + string.Equals(owner, userId, StringComparison.Ordinal) || + string.Equals(reviewer, userId, StringComparison.Ordinal) || + string.Equals(approver, userId, StringComparison.Ordinal)) return true; var visible = await GetVisibleGroupIdsAsync(userId, departmentId); if (visible == null) return true; - var participants = await _participantsRepository.GetForRecordAsync(departmentId, recordId, null); - if (participants != null && participants.Any(p => string.Equals(p.UserId, userId, StringComparison.Ordinal))) - return true; + if (isOperational) + { + var participants = await _participantsRepository.GetForRecordAsync(departmentId, recordId, null); + if (participants != null && participants.Any(p => string.Equals(p.UserId, userId, StringComparison.Ordinal))) + return true; + } var scope = await _groupScopesRepository.GetForRecordAsync(departmentId, recordId); return scope != null && scope.Any(s => visible.Contains(s.DepartmentGroupId)); diff --git a/Core/Resgrid.Services/Records/RecordsCutoverService.cs b/Core/Resgrid.Services/Records/RecordsCutoverService.cs index 68a54853..f17211a5 100644 --- a/Core/Resgrid.Services/Records/RecordsCutoverService.cs +++ b/Core/Resgrid.Services/Records/RecordsCutoverService.cs @@ -151,7 +151,7 @@ public async Task GetActivationPreviewAsync(int depart return preview; } - public async Task ActivateAsync(int departmentId, string userId, string reason, bool viewGroupRecordsLockToGroup, CancellationToken cancellationToken = default) + public async Task ActivateAsync(int departmentId, string userId, string reason, bool viewGroupRecordsLockToGroup, string ipAddress = null, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(userId)) return RecordsActivationResult.Failed("An acting user is required."); @@ -237,6 +237,7 @@ await _accessAuditsRepository.InsertAsync(new RmsAccessAudit Action = (int)RmsAccessAuditAction.Activation, ActorUserId = userId, Purpose = "Records activation", + IpAddress = ipAddress, Successful = true, OccurredOn = now, OriginClient = (int)RmsOriginClient.Web, @@ -270,7 +271,7 @@ public async Task GetRollbackOutcomeAsync(int department : RecordsRollbackOutcome.CleanRevert; } - public async Task RevertAsync(int departmentId, string userId, string reason, CancellationToken cancellationToken = default) + public async Task RevertAsync(int departmentId, string userId, string reason, string ipAddress = null, CancellationToken cancellationToken = default) { var cutover = await _cutoversRepository.GetByDepartmentIdAsync(departmentId); if (cutover == null || !cutover.IsActive) @@ -310,6 +311,7 @@ await _accessAuditsRepository.InsertAsync(new RmsAccessAudit Action = (int)RmsAccessAuditAction.Activation, ActorUserId = userId, Purpose = "Records clean revert", + IpAddress = ipAddress, Successful = true, OccurredOn = now, OriginClient = (int)RmsOriginClient.Web, diff --git a/Core/Resgrid.Services/Records/RecordsNotificationService.cs b/Core/Resgrid.Services/Records/RecordsNotificationService.cs index a007ebdb..fbaa4637 100644 --- a/Core/Resgrid.Services/Records/RecordsNotificationService.cs +++ b/Core/Resgrid.Services/Records/RecordsNotificationService.cs @@ -17,18 +17,21 @@ namespace Resgrid.Services.Records public class RecordsNotificationService : IRecordsNotificationService { public const string ReturnedForCorrectionTitle = "Record returned for correction"; + public const string SubmissionRejectedTitle = "Incident report rejected by NERIS"; private const int MaxReasonTextLength = 200; private readonly IRmsOperationalRecordsRepository _records; + private readonly IRmsIncidentReportsRepository _incidentReports; private readonly ICommunicationService _communication; private readonly IDepartmentsService _departments; private readonly IDepartmentSettingsService _departmentSettings; private readonly IUserProfileService _profiles; public RecordsNotificationService(IRmsOperationalRecordsRepository records, ICommunicationService communication, - IDepartmentsService departments, IDepartmentSettingsService departmentSettings, IUserProfileService profiles) + IDepartmentsService departments, IDepartmentSettingsService departmentSettings, IUserProfileService profiles, IRmsIncidentReportsRepository incidentReports) { _records = records; + _incidentReports = incidentReports; _communication = communication; _departments = departments; _departmentSettings = departmentSettings; @@ -65,6 +68,58 @@ public async Task NotifyReturnedForCorrectionAsync(int departmentId, strin } } + public async Task NotifySubmissionRejectedAsync(int departmentId, string reportId, CancellationToken cancellationToken = default) + { + if (departmentId <= 0 || string.IsNullOrWhiteSpace(reportId)) + return false; + + var report = await _incidentReports.GetByIdForDepartmentAsync(departmentId, reportId); + if (report == null || report.State != (int)RmsRecordState.Rejected || string.IsNullOrWhiteSpace(report.AuthorUserId)) + return false; + + cancellationToken.ThrowIfCancellationRequested(); + var department = await _departments.GetDepartmentByIdAsync(departmentId, false); + var departmentNumber = await _departmentSettings.GetTextToCallNumberForDepartmentAsync(departmentId); + var author = await _profiles.GetProfileByUserIdAsync(report.AuthorUserId, false); + var message = BuildSubmissionRejectedMessage(report); + + try + { + return await _communication.SendNotificationAsync(report.AuthorUserId, departmentId, message, departmentNumber, department, SubmissionRejectedTitle, author); + } + catch (Exception ex) + { + Logging.LogException(ex, $"Submission-rejected notification for report {reportId} could not be sent."); + return false; + } + } + + /// Header, normalized rejection summary (codes and field paths, never destination payloads) and a link. + public static string BuildSubmissionRejectedMessage(RmsIncidentReport report) + { + if (report == null) + throw new ArgumentNullException(nameof(report)); + + var reference = string.IsNullOrWhiteSpace(report.RecordNumber) ? report.DraftReference : report.RecordNumber; + var builder = new StringBuilder(); + builder.Append("Incident report ").Append(reference); + if (!string.IsNullOrWhiteSpace(report.IncidentNumber)) + builder.Append(" (incident ").Append(report.IncidentNumber).Append(')'); + builder.Append(" was rejected by NERIS."); + + if (!string.IsNullOrWhiteSpace(report.RejectionSummary)) + { + var summary = report.RejectionSummary.Trim(); + if (summary.Length > MaxReasonTextLength) + summary = summary.Substring(0, MaxReasonTextLength) + "…"; + builder.Append(" Issues: ").Append(summary); + } + + var baseUrl = (Config.SystemBehaviorConfig.ResgridBaseUrl ?? string.Empty).TrimEnd('/'); + builder.Append(' ').Append(baseUrl).Append("/User/IncidentReports/Details/").Append(report.RmsIncidentReportId); + return builder.ToString(); + } + public static string BuildReturnedForCorrectionMessage(RmsOperationalRecord record, string reviewerName) { if (record == null) diff --git a/Core/Resgrid.Services/Records/RecordsService.cs b/Core/Resgrid.Services/Records/RecordsService.cs index 55c825f3..49ed6953 100644 --- a/Core/Resgrid.Services/Records/RecordsService.cs +++ b/Core/Resgrid.Services/Records/RecordsService.cs @@ -584,6 +584,12 @@ public async Task> GetYearsAsync(int departmentId) return (await _projections.GetYearsAsync(departmentId))?.ToList() ?? new List(); } + public async Task> GetChangesSinceAsync(int departmentId, DateTime? since, int take) + { + var bounded = Math.Max(1, Math.Min(500, take)); + return (await _projections.GetModifiedSinceAsync(departmentId, since, bounded))?.OrderBy(p => p.ModifiedOn).ThenBy(p => p.RmsRecordSearchProjectionId, StringComparer.Ordinal).ToList() ?? new List(); + } + public async Task> GetRevisionsAsync(int departmentId, string recordId) { return (await _revisions.GetForRecordAsync(departmentId, recordId))?.ToList() ?? new List(); diff --git a/Core/Resgrid.Services/Records/RecordsSubmissionService.cs b/Core/Resgrid.Services/Records/RecordsSubmissionService.cs new file mode 100644 index 00000000..832814ba --- /dev/null +++ b/Core/Resgrid.Services/Records/RecordsSubmissionService.cs @@ -0,0 +1,361 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Resgrid.Config; +using Resgrid.Framework; +using Resgrid.Model; +using Resgrid.Model.Events; +using Resgrid.Model.Providers; +using Resgrid.Model.Queue; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Model.Services; + +namespace Resgrid.Services.Records +{ + /// + /// Worker 41 (RmsSubmissionCommand) logic: claim due submissions with a lease, talk to the destination with no + /// database transaction open (plan 5.3), then persist the immutable response artifact and the outcome in one + /// short transaction. Outcomes move the report through Submitted / Accepted / Rejected, retry transient + /// failures with backoff up to MaxAttempts, and raise triggers 109–111 plus the author-targeted notification + /// 33 on rejection. Trigger 108 is raised when the submission is queued, not here. + /// + public class RecordsSubmissionService : IRecordsSubmissionService + { + public const string RejectionNotificationPurpose = "Submission rejected"; + + private readonly IRmsSubmissionsRepository _submissions; + private readonly IRmsIncidentReportsRepository _reports; + private readonly IRmsRecordSearchProjectionsRepository _projections; + private readonly IRmsAccessAuditsRepository _audits; + private readonly INerisProfileService _profiles; + private readonly INerisSubmissionService _delivery; + private readonly IDomainEventOutboxService _outbox; + private readonly IOutboundQueueProvider _outboundQueue; + private readonly IUnitOfWork _unitOfWork; + + public RecordsSubmissionService(IRmsSubmissionsRepository submissions, IRmsIncidentReportsRepository reports, IRmsRecordSearchProjectionsRepository projections, + IRmsAccessAuditsRepository audits, INerisProfileService profiles, INerisSubmissionService delivery, IDomainEventOutboxService outbox, + IOutboundQueueProvider outboundQueue, IUnitOfWork unitOfWork) + { + _submissions = submissions; + _reports = reports; + _projections = projections; + _audits = audits; + _profiles = profiles; + _delivery = delivery; + _outbox = outbox; + _outboundQueue = outboundQueue; + _unitOfWork = unitOfWork; + } + + public async Task SweepAsync(CancellationToken cancellationToken = default) + { + var result = new RecordsSubmissionSweepResult(); + if (!NerisConfig.Enabled) + { + result.Message = "NERIS submission disabled; nothing to do."; + return result; + } + + var owner = $"{Environment.MachineName}:{Guid.NewGuid():N}"; + var claimed = (await _submissions.ClaimDueBatchAsync(owner, TimeSpan.FromSeconds(Math.Max(30, NerisConfig.LeaseSeconds)), Math.Max(1, NerisConfig.BatchSize), DateTime.UtcNow, cancellationToken))?.ToList() ?? new List(); + result.Claimed = claimed.Count; + + foreach (var submission in claimed) + { + cancellationToken.ThrowIfCancellationRequested(); + try + { + var processed = await ProcessAsync(submission, cancellationToken); + switch ((RmsSubmissionState)processed.State) + { + case RmsSubmissionState.Accepted: result.Accepted++; break; + case RmsSubmissionState.Rejected: result.Rejected++; break; + case RmsSubmissionState.Failed: result.Failed++; break; + case RmsSubmissionState.AwaitingDestination: result.Delivered++; break; + default: result.Deferred++; break; + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + result.Errors++; + Logging.LogException(ex, $"Submission {submission.RmsSubmissionId} could not be processed."); + } + } + + result.Message = $"claimed {result.Claimed}, delivered {result.Delivered}, accepted {result.Accepted}, rejected {result.Rejected}, deferred {result.Deferred}, failed {result.Failed}, errors {result.Errors}"; + return result; + } + + public async Task ProcessAsync(RmsSubmission submission, CancellationToken cancellationToken = default) + { + if (submission == null) throw new ArgumentNullException(nameof(submission)); + var now = DateTime.UtcNow; + + var report = await _reports.GetByIdForDepartmentAsync(submission.DepartmentId, submission.RecordId); + if (report == null || report.DeletedOn.HasValue) + return await PersistAsync(submission, report, Fatal("The report no longer exists."), now, cancellationToken); + + // A superseding revision or a void may have arrived while this row waited; never deliver stale content. + if (submission.State == (int)RmsSubmissionState.Superseded || RmsLifecycle.IsTerminal((RmsRecordState)report.State)) + { + submission.State = (int)RmsSubmissionState.Superseded; + submission.CompletedOn = now; + return await ReleaseAsync(submission, now, cancellationToken); + } + + var profile = await _profiles.GetProfileAsync(submission.DepartmentId); + if (!await _profiles.IsSubmissionEnabledAsync(submission.DepartmentId)) + { + submission.NextAttemptOn = now.AddMinutes(Math.Max(1, NerisConfig.StatusPollMinutes)); + return await ReleaseAsync(submission, now, cancellationToken); + } + + NerisSubmissionOutcome outcome; + if (submission.State == (int)RmsSubmissionState.AwaitingDestination) + { + var nerisId = submission.ExternalId ?? report.NerisIncidentId; + outcome = string.IsNullOrWhiteSpace(nerisId) + ? Fatal("The submission is awaiting the destination but carries no incident ID.") + : await _delivery.CheckStatusAsync(profile, nerisId, cancellationToken); + } + else + { + submission.Attempts += 1; + submission.SentOn = now; + outcome = await _delivery.DeliverAsync(profile, submission, report.NerisIncidentId, cancellationToken); + } + + return await PersistAsync(submission, report, outcome, now, cancellationToken); + } + + private async Task PersistAsync(RmsSubmission submission, RmsIncidentReport report, NerisSubmissionOutcome outcome, DateTime now, CancellationToken cancellationToken) + { + var outboxIds = new List(); + NotificationItem notification = null; + + await InTransactionAsync(async () => + { + if (outcome.ResponseJson != null) + { + submission.ResponseJson = outcome.ResponseJson; + submission.ResponseChecksum = RecordSnapshotSerializer.Checksum(outcome.ResponseJson); + } + submission.ResponseStatusCode = outcome.StatusCode; + if (!string.IsNullOrWhiteSpace(outcome.ExternalId)) + submission.ExternalId = outcome.ExternalId; + if (!string.IsNullOrWhiteSpace(outcome.ExternalStatus)) + submission.ExternalStatus = outcome.ExternalStatus; + + WorkflowTriggerEventType? trigger = null; + RmsRecordState? reportState = null; + string auditPurpose; + + switch (outcome.Kind) + { + case NerisOutcomeKind.Created: + case NerisOutcomeKind.Updated: + case NerisOutcomeKind.Pending: + submission.State = (int)RmsSubmissionState.AwaitingDestination; + submission.NextAttemptOn = now.AddMinutes(Math.Max(1, NerisConfig.StatusPollMinutes)); + submission.ErrorSummary = null; + reportState = ReportStateAfterDelivery(report); + auditPurpose = outcome.Kind == NerisOutcomeKind.Pending ? "Submission status pending" : "Submission delivered"; + break; + + case NerisOutcomeKind.Accepted: + submission.State = (int)RmsSubmissionState.Accepted; + submission.CompletedOn = now; + submission.NextAttemptOn = null; + submission.ErrorSummary = null; + reportState = RmsRecordState.Accepted; + trigger = WorkflowTriggerEventType.RecordSubmissionAccepted; + auditPurpose = "Submission accepted"; + break; + + case NerisOutcomeKind.Rejected: + submission.State = (int)RmsSubmissionState.Rejected; + submission.CompletedOn = now; + submission.NextAttemptOn = null; + submission.ErrorSummary = Summarize(outcome); + reportState = RmsRecordState.Rejected; + trigger = WorkflowTriggerEventType.RecordSubmissionRejected; + auditPurpose = "Submission rejected"; + break; + + case NerisOutcomeKind.Transient: + if (submission.Attempts >= Math.Max(1, submission.MaxAttempts)) + { + submission.State = (int)RmsSubmissionState.Failed; + submission.CompletedOn = now; + submission.NextAttemptOn = null; + submission.ErrorSummary = "Delivery exhausted its retries: " + (outcome.Message ?? "destination unavailable"); + trigger = WorkflowTriggerEventType.RecordSubmissionFailed; + auditPurpose = "Submission failed (retries exhausted)"; + } + else + { + if (submission.State != (int)RmsSubmissionState.AwaitingDestination) + submission.State = (int)RmsSubmissionState.Queued; + submission.NextAttemptOn = now.AddMinutes(Backoff(submission.Attempts)); + submission.ErrorSummary = outcome.Message; + auditPurpose = "Submission deferred"; + } + break; + + default: + submission.State = (int)RmsSubmissionState.Failed; + submission.CompletedOn = now; + submission.NextAttemptOn = null; + submission.ErrorSummary = outcome.Message ?? "Delivery needs operator attention."; + trigger = WorkflowTriggerEventType.RecordSubmissionFailed; + auditPurpose = "Submission failed"; + break; + } + + submission.LeaseOwner = null; + submission.LeaseExpiresOn = null; + submission.ModifiedOn = now; + submission.RowVersion += 1; + await _submissions.UpdateAsync(submission, cancellationToken, true); + + if (report != null) + { + if (!string.IsNullOrWhiteSpace(submission.ExternalId)) + report.NerisIncidentId = submission.ExternalId; + report.LastSubmissionId = submission.RmsSubmissionId; + report.LastSubmissionState = submission.State; + var from = (RmsRecordState)report.State; + if (reportState.HasValue && reportState.Value != from) + { + report.State = (int)reportState.Value; + if (reportState == RmsRecordState.Accepted) report.AcceptedOn = now; + if (reportState == RmsRecordState.Rejected) { report.RejectedOn = now; report.RejectionSummary = submission.ErrorSummary; } + } + report.ModifiedOn = now; + report.RowVersion += 1; + await _reports.UpdateAsync(report, cancellationToken, true); + await UpdateProjectionStateAsync(report, cancellationToken); + + if (trigger.HasValue) + { + var entry = await _outbox.EnqueueAsync(report.DepartmentId, DomainEventProducers.Records, new DomainEventEnvelope + { + EventName = trigger.Value.ToString(), + SchemaVersion = 1, + AggregateType = IncidentReportsService.IncidentAggregate, + AggregateId = report.RmsIncidentReportId, + AggregateVersion = report.RevisionCount, + Trigger = trigger.Value, + Payload = new Dictionary + { + ["record"] = IncidentReportsService.RecordBlock(report, null, (RmsRecordState)report.State), + ["record_change"] = new { previous_state = from.ToString(), current_state = ((RmsRecordState)report.State).ToString(), prior_revision_id = (string)null, current_revision_id = report.CurrentRevisionId, reason_code = (string)null }, + ["submission"] = IncidentReportsService.SubmissionBlock(submission) + }, + CorrelationId = report.RmsIncidentReportId, + OriginClient = RmsOriginClient.System + }, cancellationToken); + outboxIds.Add(entry.DomainEventOutboxId); + } + + if (outcome.Kind == NerisOutcomeKind.Rejected && !string.IsNullOrWhiteSpace(report.AuthorUserId)) + notification = new NotificationItem { DepartmentId = report.DepartmentId, Type = (int)EventTypes.RecordSubmissionRejected, Value = report.RmsIncidentReportId, UserId = report.AuthorUserId }; + + await _audits.InsertAsync(new RmsAccessAudit + { + DepartmentId = report.DepartmentId, + RecordId = report.RmsIncidentReportId, + RevisionId = submission.RevisionId, + Action = (int)RmsAccessAuditAction.Submit, + ActorUserId = null, + Purpose = auditPurpose, + OriginClient = (int)RmsOriginClient.System, + Successful = outcome.Kind != NerisOutcomeKind.Fatal, + OccurredOn = now, + DetailJson = JsonConvert.SerializeObject(new { submission.RmsSubmissionId, submission.Attempts, outcome.Kind, outcome.StatusCode, submission.ExternalId, submission.ExternalStatus, submission.ResponseChecksum }) + }, cancellationToken, true); + } + }); + + await _outbox.DispatchAfterCommitAsync(outboxIds, cancellationToken); + if (notification != null) + { + try { await _outboundQueue.EnqueueNotification(notification); } + catch (Exception ex) { Logging.LogException(ex, $"Rejection notification for report {report?.RmsIncidentReportId} could not be queued."); } + } + + return submission; + } + + private static RmsRecordState? ReportStateAfterDelivery(RmsIncidentReport report) + { + var state = (RmsRecordState)report.State; + return state == RmsRecordState.Finalized || state == RmsRecordState.Amended || state == RmsRecordState.Corrected || state == RmsRecordState.Rejected + ? RmsRecordState.Submitted + : (RmsRecordState?)null; + } + + /// Exponential backoff in minutes: base, 2x, 4x ... capped at one day. + public static int Backoff(int attempts) + { + var minutes = Math.Max(1, NerisConfig.RetryBackoffMinutes) * Math.Pow(2, Math.Max(0, attempts - 1)); + return (int)Math.Min(minutes, 24 * 60); + } + + /// Codes and field paths only: what workflows, notifications and the queue may see (plan 5.6). + public static string Summarize(NerisSubmissionOutcome outcome) + { + if (outcome.Errors == null || outcome.Errors.Count == 0) + return outcome.Message ?? "Rejected by the destination."; + return string.Join("; ", outcome.Errors.Take(20).Select(e => string.IsNullOrWhiteSpace(e.FieldPath) ? e.Code : $"{e.FieldPath} ({e.Code})")); + } + + private async Task ReleaseAsync(RmsSubmission submission, DateTime now, CancellationToken cancellationToken) + { + submission.LeaseOwner = null; + submission.LeaseExpiresOn = null; + submission.ModifiedOn = now; + submission.RowVersion += 1; + await _submissions.UpdateAsync(submission, cancellationToken, true); + return submission; + } + + private async Task UpdateProjectionStateAsync(RmsIncidentReport report, CancellationToken cancellationToken) + { + var projection = await _projections.GetByRecordIdAsync(report.DepartmentId, report.RmsIncidentReportId); + if (projection == null) + return; + projection.State = report.State; + projection.SearchText = string.Join(" ", new[] { report.RecordNumber, report.DraftReference, report.DisplaySummary, report.IncidentNumber, report.NerisIncidentId }.Where(s => !string.IsNullOrWhiteSpace(s))); + projection.ModifiedOn = DateTime.UtcNow; + projection.RowVersion += 1; + await _projections.UpdateAsync(projection, cancellationToken, true); + } + + private static NerisSubmissionOutcome Fatal(string message) => new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Fatal, Message = message }; + + private async Task InTransactionAsync(Func work) + { + _unitOfWork.CreateOrGetConnection(); + try + { + await work(); + _unitOfWork.CommitChanges(); + } + catch + { + _unitOfWork.DiscardChanges(); + throw; + } + } + } +} diff --git a/Core/Resgrid.Services/ServicesModule.cs b/Core/Resgrid.Services/ServicesModule.cs index ab016add..78883f88 100644 --- a/Core/Resgrid.Services/ServicesModule.cs +++ b/Core/Resgrid.Services/ServicesModule.cs @@ -242,6 +242,13 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + // RMS-2: NERIS incident reports and the submission worker logic + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + // RMS-1B v4 Records API support: keyed short-lived state, command idempotency, resumable attachment sessions + 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/Core/Resgrid.Services/WorkflowSampleDataGenerator.cs b/Core/Resgrid.Services/WorkflowSampleDataGenerator.cs index 28b5a0af..21e888ce 100644 --- a/Core/Resgrid.Services/WorkflowSampleDataGenerator.cs +++ b/Core/Resgrid.Services/WorkflowSampleDataGenerator.cs @@ -623,6 +623,27 @@ private static void AddRecordsSamples(ScriptObject obj, WorkflowTriggerEventType c["number_disposition"] = "none"; obj["record_change"] = c; + if (eventType >= WorkflowTriggerEventType.RecordSubmissionQueued && eventType <= WorkflowTriggerEventType.RecordSubmissionFailed) + { + r["kind"] = "IncidentReport"; + r["incident_number"] = "2026-000123"; + r["neris_incident_id"] = eventType == WorkflowTriggerEventType.RecordSubmissionQueued ? "" : "FD24027000I2026000123"; + var s = new ScriptObject(); + s["id"] = "7c1e2a9b-3d4f-4e5a-8b6c-1d2e3f4a5b6c"; + s["destination"] = "NERIS"; + s["destination_version"] = "1.4.78"; + s["state"] = eventType == WorkflowTriggerEventType.RecordSubmissionQueued ? "Queued" : eventType == WorkflowTriggerEventType.RecordSubmissionAccepted ? "Accepted" : eventType == WorkflowTriggerEventType.RecordSubmissionRejected ? "Rejected" : "Failed"; + s["external_id"] = eventType == WorkflowTriggerEventType.RecordSubmissionQueued ? "" : "FD24027000I2026000123"; + s["external_status"] = eventType == WorkflowTriggerEventType.RecordSubmissionAccepted ? "APPROVED" : eventType == WorkflowTriggerEventType.RecordSubmissionRejected ? "REJECTED" : ""; + s["attempts"] = eventType == WorkflowTriggerEventType.RecordSubmissionFailed ? 5 : 1; + s["max_attempts"] = 5; + s["error_summary"] = eventType == WorkflowTriggerEventType.RecordSubmissionRejected ? "dispatch.call_answered (missing)" : eventType == WorkflowTriggerEventType.RecordSubmissionFailed ? "Delivery exhausted its retries: NERIS returned 503." : ""; + s["queued_on"] = DateTime.Now.AddMinutes(-30); + s["sent_on"] = DateTime.Now.AddMinutes(-1); + s["completed_on"] = eventType == WorkflowTriggerEventType.RecordSubmissionQueued ? (DateTime?)null : DateTime.Now; + obj["submission"] = s; + } + if (eventType == WorkflowTriggerEventType.RecordSubmittedForReview || eventType == WorkflowTriggerEventType.RecordReturnedForCorrection) { var returned = eventType == WorkflowTriggerEventType.RecordReturnedForCorrection; diff --git a/Core/Resgrid.Services/WorkflowTemplateContextBuilder.cs b/Core/Resgrid.Services/WorkflowTemplateContextBuilder.cs index 3dd79a5c..d05ef114 100644 --- a/Core/Resgrid.Services/WorkflowTemplateContextBuilder.cs +++ b/Core/Resgrid.Services/WorkflowTemplateContextBuilder.cs @@ -407,6 +407,10 @@ public async Task BuildContextAsync( case WorkflowTriggerEventType.RecordAmended: case WorkflowTriggerEventType.RecordVoided: case WorkflowTriggerEventType.RecordCancelled: + case WorkflowTriggerEventType.RecordSubmissionQueued: + case WorkflowTriggerEventType.RecordSubmissionAccepted: + case WorkflowTriggerEventType.RecordSubmissionRejected: + case WorkflowTriggerEventType.RecordSubmissionFailed: { // Records (RMS): the payload is the outbox snapshot carried by RecordsWorkflowEvent; it is never // rehydrated from current record state, so a retry sees exactly what the original run saw. @@ -1178,9 +1182,10 @@ private static string MapRecordsEventVariables(ScriptObject obj, RecordsWorkflow var recordToken = payload["record"] as JObject; var record = ToScriptObject(recordToken); var recordId = recordToken?["id"]?.Type == JTokenType.String ? (string)recordToken["id"] : null; + var isIncident = recordToken?["kind"]?.Type == JTokenType.String && string.Equals((string)recordToken["kind"], "IncidentReport", StringComparison.Ordinal); record["url"] = string.IsNullOrWhiteSpace(recordId) ? string.Empty - : $"{(Resgrid.Config.SystemBehaviorConfig.ResgridBaseUrl ?? string.Empty).TrimEnd('/')}/User/Records/Details/{recordId}"; + : $"{(Resgrid.Config.SystemBehaviorConfig.ResgridBaseUrl ?? string.Empty).TrimEnd('/')}/User/{(isIncident ? "IncidentReports" : "Records")}/Details/{recordId}"; obj["record"] = record; var change = ToScriptObject(payload["record_change"] as JObject); @@ -1195,6 +1200,9 @@ private static string MapRecordsEventVariables(ScriptObject obj, RecordsWorkflow if (payload["review"] is JObject review) obj["review"] = ToScriptObject(review); + if (payload["submission"] is JObject submission) + obj["submission"] = ToScriptObject(submission); + return recordToken?["author_user_id"]?.Type == JTokenType.String ? (string)recordToken["author_user_id"] : null; } diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0164_AddRmsIncidentReportCore.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0164_AddRmsIncidentReportCore.cs new file mode 100644 index 00000000..0c846641 --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0164_AddRmsIncidentReportCore.cs @@ -0,0 +1,304 @@ +using FluentMigrator; + +namespace Resgrid.Providers.Migrations.Migrations +{ + /// + /// NERIS incident report core (RMS plan sections 4.2/5.2, registry M0164, package RMS-2): RmsIncidentReports + /// (aggregate root, SingleAuthoritative per (DepartmentId, CallId, ReportingEntityId, DefinitionKey)), + /// RmsSourceFacts (provenance ledger for every prefilled value), RmsUnitResponses, RmsIncidentTypes, + /// RmsActionTactics, RmsAids, RmsLocations, RmsNarratives (protected-candidate holder with the inert + /// envelope) and RmsValidationIssues. Every table carries DepartmentId and an immutable ProtectionId; child + /// rows are a working draft (RevisionId NULL) plus immutable revision copies. Existence-guarded for safe retry. + /// + [Migration(164)] + public class M0164_AddRmsIncidentReportCore : Migration + { + public override void Up() + { + if (!Schema.Table("RmsIncidentReports").Exists()) + { + Create.Table("RmsIncidentReports") + .WithColumn("RmsIncidentReportId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("CallId").AsInt32().NotNullable() + .WithColumn("ReportingEntityId").AsString(100).NotNullable() + .WithColumn("DefinitionKey").AsString(100).NotNullable() + .WithColumn("DefinitionVersion").AsInt32().NotNullable() + .WithColumn("ProfileVersion").AsString(20).Nullable() + .WithColumn("LifecyclePreset").AsInt32().NotNullable() + .WithColumn("State").AsInt32().NotNullable() + .WithColumn("RecordNumber").AsString(50).Nullable() + .WithColumn("DraftReference").AsString(50).Nullable() + .WithColumn("IncidentNumber").AsString(100).Nullable() + .WithColumn("DisplaySummary").AsString(400).Nullable() + .WithColumn("StationGroupId").AsInt32().Nullable() + .WithColumn("AuthorUserId").AsString(128).NotNullable() + .WithColumn("OwnerUserId").AsString(128).Nullable() + .WithColumn("ReviewerUserId").AsString(128).Nullable() + .WithColumn("ApproverUserId").AsString(128).Nullable() + .WithColumn("ReviewDueOn").AsDateTime2().Nullable() + .WithColumn("SubmittedForReviewOn").AsDateTime2().Nullable() + .WithColumn("ReturnedOn").AsDateTime2().Nullable() + .WithColumn("ReturnReasonCode").AsString(50).Nullable() + .WithColumn("ReturnReasonText").AsString(int.MaxValue).Nullable() + .WithColumn("ReturnCount").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("ApprovedOn").AsDateTime2().Nullable() + .WithColumn("FinalizedOn").AsDateTime2().Nullable() + .WithColumn("FinalizedByUserId").AsString(128).Nullable() + .WithColumn("CurrentRevisionId").AsString(36).Nullable() + .WithColumn("RevisionCount").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("AmendsRevisionId").AsString(36).Nullable() + .WithColumn("VoidedOn").AsDateTime2().Nullable() + .WithColumn("VoidedByUserId").AsString(128).Nullable() + .WithColumn("VoidReasonCode").AsString(50).Nullable() + .WithColumn("VoidReasonText").AsString(int.MaxValue).Nullable() + .WithColumn("CancelledOn").AsDateTime2().Nullable() + .WithColumn("CancelledByUserId").AsString(128).Nullable() + .WithColumn("NerisIncidentId").AsString(100).Nullable() + .WithColumn("LastSubmissionId").AsString(36).Nullable() + .WithColumn("LastSubmissionState").AsInt32().Nullable() + .WithColumn("LastSubmittedOn").AsDateTime2().Nullable() + .WithColumn("AcceptedOn").AsDateTime2().Nullable() + .WithColumn("RejectedOn").AsDateTime2().Nullable() + .WithColumn("RejectionSummary").AsString(int.MaxValue).Nullable() + .WithColumn("CallCreatedOn").AsDateTime2().Nullable() + .WithColumn("CallAnsweredOn").AsDateTime2().Nullable() + .WithColumn("CallArrivalOn").AsDateTime2().Nullable() + .WithColumn("IncidentClearedOn").AsDateTime2().Nullable() + .WithColumn("DispatchCenterId").AsString(100).Nullable() + .WithColumn("DeterminantCode").AsString(100).Nullable() + .WithColumn("DispatchIncidentCode").AsString(200).Nullable() + .WithColumn("Disposition").AsString(200).Nullable() + .WithColumn("PeoplePresent").AsBoolean().Nullable() + .WithColumn("DisplacementCount").AsInt32().Nullable() + .WithColumn("AnimalsRescued").AsInt32().Nullable() + .WithColumn("SpecialModifiersCsv").AsString(400).Nullable() + .WithColumn("IdempotencyKey").AsString(64).Nullable() + .WithColumn("OriginClient").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("CreatedByUserId").AsString(128).Nullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedByUserId").AsString(128).Nullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L) + .WithColumn("DeletedOn").AsDateTime2().Nullable(); + + Create.Index("IX_RmsIncidentReports_Department_State_Created").OnTable("RmsIncidentReports") + .OnColumn("DepartmentId").Ascending().OnColumn("State").Ascending().OnColumn("CreatedOn").Descending(); + Create.Index("IX_RmsIncidentReports_Department_Call").OnTable("RmsIncidentReports") + .OnColumn("DepartmentId").Ascending().OnColumn("CallId").Ascending(); + Create.Index("IX_RmsIncidentReports_Department_Owner").OnTable("RmsIncidentReports") + .OnColumn("DepartmentId").Ascending().OnColumn("OwnerUserId").Ascending(); + // SingleAuthoritative cardinality (plan 5.2.1): one report per responding entity per Call. + Execute.Sql("CREATE UNIQUE NONCLUSTERED INDEX UX_RmsIncidentReports_Call_Entity ON RmsIncidentReports (DepartmentId, CallId, ReportingEntityId, DefinitionKey) WHERE DeletedOn IS NULL;"); + Execute.Sql("CREATE UNIQUE NONCLUSTERED INDEX UX_RmsIncidentReports_Idempotency ON RmsIncidentReports (DepartmentId, IdempotencyKey) WHERE IdempotencyKey IS NOT NULL;"); + } + + if (!Schema.Table("RmsSourceFacts").Exists()) + { + Create.Table("RmsSourceFacts") + .WithColumn("RmsSourceFactId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RevisionId").AsString(36).Nullable() + .WithColumn("FactKey").AsString(150).NotNullable() + .WithColumn("SourceKind").AsInt32().NotNullable() + .WithColumn("SourceSystem").AsString(50).Nullable() + .WithColumn("SourceEntityType").AsString(100).Nullable() + .WithColumn("SourceEntityId").AsString(100).Nullable() + .WithColumn("SourceValue").AsString(int.MaxValue).Nullable() + .WithColumn("CurrentValue").AsString(int.MaxValue).Nullable() + .WithColumn("SourceTime").AsDateTime2().Nullable() + .WithColumn("ImportedOn").AsDateTime2().NotNullable() + .WithColumn("CorrectedOn").AsDateTime2().Nullable() + .WithColumn("CorrectedByUserId").AsString(128).Nullable() + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + Create.Index("IX_RmsSourceFacts_Department_Record_Revision").OnTable("RmsSourceFacts") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending().OnColumn("RevisionId").Ascending(); + } + + if (!Schema.Table("RmsUnitResponses").Exists()) + { + Create.Table("RmsUnitResponses") + .WithColumn("RmsUnitResponseId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RevisionId").AsString(36).Nullable() + .WithColumn("UnitId").AsInt32().Nullable() + .WithColumn("UnitNameSnapshot").AsString(200).Nullable() + .WithColumn("UnitTypeSnapshot").AsString(100).Nullable() + .WithColumn("UnitNerisId").AsString(50).Nullable() + .WithColumn("StationGroupIdSnapshot").AsInt32().Nullable() + .WithColumn("Staffing").AsInt32().Nullable() + .WithColumn("UnableToDispatch").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("DispatchedOn").AsDateTime2().Nullable() + .WithColumn("EnrouteOn").AsDateTime2().Nullable() + .WithColumn("OnSceneOn").AsDateTime2().Nullable() + .WithColumn("CanceledEnrouteOn").AsDateTime2().Nullable() + .WithColumn("StagingOn").AsDateTime2().Nullable() + .WithColumn("ClearedOn").AsDateTime2().Nullable() + .WithColumn("ResponseMode").AsString(20).Nullable() + .WithColumn("TransportMode").AsString(20).Nullable() + .WithColumn("TimesSourceKind").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("Disposition").AsString(100).Nullable() + .WithColumn("Ordinal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + Create.Index("IX_RmsUnitResponses_Department_Record_Revision").OnTable("RmsUnitResponses") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending().OnColumn("RevisionId").Ascending(); + } + + if (!Schema.Table("RmsIncidentTypes").Exists()) + { + Create.Table("RmsIncidentTypes") + .WithColumn("RmsIncidentTypeId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RevisionId").AsString(36).Nullable() + .WithColumn("TypeCode").AsString(200).NotNullable() + .WithColumn("IsPrimary").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("LocalCode").AsString(200).Nullable() + .WithColumn("ValueSetVersion").AsString(20).Nullable() + .WithColumn("Ordinal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + Create.Index("IX_RmsIncidentTypes_Department_Record_Revision").OnTable("RmsIncidentTypes") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending().OnColumn("RevisionId").Ascending(); + } + + if (!Schema.Table("RmsActionTactics").Exists()) + { + Create.Table("RmsActionTactics") + .WithColumn("RmsActionTacticId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RevisionId").AsString(36).Nullable() + .WithColumn("TacticCode").AsString(200).NotNullable() + .WithColumn("ActorUnitId").AsInt32().Nullable() + .WithColumn("OccurredOn").AsDateTime2().Nullable() + .WithColumn("SourceKind").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("Outcome").AsString(200).Nullable() + .WithColumn("Ordinal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + Create.Index("IX_RmsActionTactics_Department_Record_Revision").OnTable("RmsActionTactics") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending().OnColumn("RevisionId").Ascending(); + } + + if (!Schema.Table("RmsAids").Exists()) + { + Create.Table("RmsAids") + .WithColumn("RmsAidId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RevisionId").AsString(36).Nullable() + .WithColumn("Direction").AsString(10).NotNullable() + .WithColumn("AidType").AsString(30).NotNullable() + .WithColumn("CounterpartNerisId").AsString(50).Nullable() + .WithColumn("CounterpartName").AsString(200).Nullable() + .WithColumn("IsNonFireDepartment").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("NonFdType").AsString(50).Nullable() + .WithColumn("Ordinal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + Create.Index("IX_RmsAids_Department_Record_Revision").OnTable("RmsAids") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending().OnColumn("RevisionId").Ascending(); + } + + if (!Schema.Table("RmsLocations").Exists()) + { + Create.Table("RmsLocations") + .WithColumn("RmsLocationId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RevisionId").AsString(36).Nullable() + .WithColumn("AddressText").AsString(int.MaxValue).Nullable() + .WithColumn("Number").AsString(20).Nullable() + .WithColumn("NumberPrefix").AsString(20).Nullable() + .WithColumn("NumberSuffix").AsString(20).Nullable() + .WithColumn("Street").AsString(200).Nullable() + .WithColumn("UnitValue").AsString(50).Nullable() + .WithColumn("Municipality").AsString(200).Nullable() + .WithColumn("County").AsString(200).Nullable() + .WithColumn("State").AsString(10).Nullable() + .WithColumn("PostalCode").AsString(20).Nullable() + .WithColumn("Country").AsString(2).Nullable() + .WithColumn("PlaceType").AsString(100).Nullable() + .WithColumn("LocationUse").AsString(100).Nullable() + .WithColumn("CrossStreet1").AsString(200).Nullable() + .WithColumn("CrossStreet2").AsString(200).Nullable() + .WithColumn("Latitude").AsDecimal(12, 8).Nullable() + .WithColumn("Longitude").AsDecimal(12, 8).Nullable() + .WithColumn("Jurisdiction").AsString(200).Nullable() + .WithColumn("SourceKind").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + Create.Index("IX_RmsLocations_Department_Record_Revision").OnTable("RmsLocations") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending().OnColumn("RevisionId").Ascending(); + } + + if (!Schema.Table("RmsNarratives").Exists()) + { + Create.Table("RmsNarratives") + .WithColumn("RmsNarrativeId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RevisionId").AsString(36).Nullable() + .WithColumn("Narrative").AsString(int.MaxValue).Nullable() + .WithColumn("ImpedimentNarrative").AsString(int.MaxValue).Nullable() + .WithColumn("OutcomeNarrative").AsString(int.MaxValue).Nullable() + .WithColumn("SupplementalJson").AsString(int.MaxValue).Nullable() + .WithColumn("IsProtected").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("ProtectedEnvelope").AsString(int.MaxValue).Nullable() + .WithColumn("ProtectedCatalogVersion").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + Create.Index("IX_RmsNarratives_Department_Record_Revision").OnTable("RmsNarratives") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending().OnColumn("RevisionId").Ascending(); + } + + if (!Schema.Table("RmsValidationIssues").Exists()) + { + Create.Table("RmsValidationIssues") + .WithColumn("RmsValidationIssueId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RevisionId").AsString(36).Nullable() + .WithColumn("ProfileVersion").AsString(20).Nullable() + .WithColumn("RuleKey").AsString(100).NotNullable() + .WithColumn("Severity").AsInt32().NotNullable() + .WithColumn("FieldPath").AsString(200).Nullable() + .WithColumn("Message").AsString(int.MaxValue).Nullable() + .WithColumn("Source").AsInt32().NotNullable() + .WithColumn("ResolvedOn").AsDateTime2().Nullable() + .WithColumn("CreatedOn").AsDateTime2().NotNullable(); + Create.Index("IX_RmsValidationIssues_Department_Record").OnTable("RmsValidationIssues") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending(); + } + } + + public override void Down() + { + foreach (var table in new[] { "RmsValidationIssues", "RmsNarratives", "RmsLocations", "RmsAids", "RmsActionTactics", "RmsIncidentTypes", "RmsUnitResponses", "RmsSourceFacts", "RmsIncidentReports" }) + { + if (Schema.Table(table).Exists()) + Delete.Table(table); + } + } + } +} diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0165_AddRmsSubmissionsAndSignatures.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0165_AddRmsSubmissionsAndSignatures.cs new file mode 100644 index 00000000..c1b91d76 --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0165_AddRmsSubmissionsAndSignatures.cs @@ -0,0 +1,94 @@ +using FluentMigrator; + +namespace Resgrid.Providers.Migrations.Migrations +{ + /// + /// Reporting-destination submissions and officer attestation (RMS plan sections 5.3/5.5, registry M0165, + /// RMS-2). RmsSubmissions: one row per exchange with a destination, immutable payload/response artifacts with + /// checksums, scoped idempotency key (unique), lease columns for worker 41. RmsSignatures: signer, role, + /// intent, statement version, method, time and the revision checksum the signature binds to. + /// + [Migration(165)] + public class M0165_AddRmsSubmissionsAndSignatures : Migration + { + public override void Up() + { + if (!Schema.Table("RmsSubmissions").Exists()) + { + Create.Table("RmsSubmissions") + .WithColumn("RmsSubmissionId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RecordKind").AsInt32().NotNullable() + .WithColumn("RevisionId").AsString(36).Nullable() + .WithColumn("Destination").AsString(50).NotNullable() + .WithColumn("DestinationVersion").AsString(20).Nullable() + .WithColumn("IdempotencyKey").AsString(64).NotNullable() + .WithColumn("State").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("Attempts").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("MaxAttempts").AsInt32().NotNullable().WithDefaultValue(5) + .WithColumn("NextAttemptOn").AsDateTime2().Nullable() + .WithColumn("LeaseOwner").AsString(100).Nullable() + .WithColumn("LeaseExpiresOn").AsDateTime2().Nullable() + .WithColumn("PayloadJson").AsString(int.MaxValue).Nullable() + .WithColumn("PayloadChecksum").AsString(64).Nullable() + .WithColumn("ResponseJson").AsString(int.MaxValue).Nullable() + .WithColumn("ResponseChecksum").AsString(64).Nullable() + .WithColumn("ResponseStatusCode").AsInt32().Nullable() + .WithColumn("ExternalId").AsString(100).Nullable() + .WithColumn("ExternalStatus").AsString(50).Nullable() + .WithColumn("ErrorSummary").AsString(int.MaxValue).Nullable() + .WithColumn("QueuedOn").AsDateTime2().NotNullable() + .WithColumn("SentOn").AsDateTime2().Nullable() + .WithColumn("CompletedOn").AsDateTime2().Nullable() + .WithColumn("CreatedByUserId").AsString(128).Nullable() + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + + Execute.Sql("CREATE UNIQUE NONCLUSTERED INDEX UX_RmsSubmissions_IdempotencyKey ON RmsSubmissions (IdempotencyKey);"); + Create.Index("IX_RmsSubmissions_State_NextAttempt").OnTable("RmsSubmissions") + .OnColumn("State").Ascending().OnColumn("NextAttemptOn").Ascending(); + Create.Index("IX_RmsSubmissions_Department_Record").OnTable("RmsSubmissions") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending(); + } + + if (!Schema.Table("RmsSignatures").Exists()) + { + Create.Table("RmsSignatures") + .WithColumn("RmsSignatureId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("RecordId").AsString(36).NotNullable() + .WithColumn("RecordKind").AsInt32().NotNullable() + .WithColumn("RevisionId").AsString(36).NotNullable() + .WithColumn("SignerUserId").AsString(128).NotNullable() + .WithColumn("SignerNameSnapshot").AsString(200).Nullable() + .WithColumn("SignerRoleSnapshot").AsString(200).Nullable() + .WithColumn("Intent").AsInt32().NotNullable() + .WithColumn("StatementVersion").AsString(20).Nullable() + .WithColumn("StatementText").AsString(int.MaxValue).Nullable() + .WithColumn("Method").AsInt32().NotNullable() + .WithColumn("SignedOn").AsDateTime2().NotNullable() + .WithColumn("IpAddress").AsString(64).Nullable() + .WithColumn("ArtifactChecksum").AsString(64).Nullable() + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + + Create.Index("IX_RmsSignatures_Department_Record").OnTable("RmsSignatures") + .OnColumn("DepartmentId").Ascending().OnColumn("RecordId").Ascending(); + Create.Index("IX_RmsSignatures_Department_Revision").OnTable("RmsSignatures") + .OnColumn("DepartmentId").Ascending().OnColumn("RevisionId").Ascending(); + } + } + + public override void Down() + { + if (Schema.Table("RmsSignatures").Exists()) + Delete.Table("RmsSignatures"); + if (Schema.Table("RmsSubmissions").Exists()) + Delete.Table("RmsSubmissions"); + } + } +} diff --git a/Providers/Resgrid.Providers.Migrations/Migrations/M0166_AddRmsNerisProfilesAndValueSets.cs b/Providers/Resgrid.Providers.Migrations/Migrations/M0166_AddRmsNerisProfilesAndValueSets.cs new file mode 100644 index 00000000..c196939d --- /dev/null +++ b/Providers/Resgrid.Providers.Migrations/Migrations/M0166_AddRmsNerisProfilesAndValueSets.cs @@ -0,0 +1,92 @@ +using FluentMigrator; + +namespace Resgrid.Providers.Migrations.Migrations +{ + /// + /// NERIS profiles, value sets and crosswalks (RMS plan section 5.5, registry M0166, RMS-2). RmsNerisProfiles: + /// one per department — entity ID, environment, department-encrypted credential, pinned contract version. + /// RmsNerisValueSets: global reference data seeded from the pinned snapshot, keyed by contract version. + /// RmsNerisCrosswalks: department-owned Resgrid/CAD code to NERIS value mapping; the original code is always + /// kept beside the mapped one. + /// + [Migration(166)] + public class M0166_AddRmsNerisProfilesAndValueSets : Migration + { + public override void Up() + { + if (!Schema.Table("RmsNerisProfiles").Exists()) + { + Create.Table("RmsNerisProfiles") + .WithColumn("RmsNerisProfileId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("NerisEntityId").AsString(50).Nullable() + .WithColumn("EntityName").AsString(200).Nullable() + .WithColumn("Environment").AsString(20).NotNullable().WithDefaultValue("production") + .WithColumn("BaseUrlOverride").AsString(400).Nullable() + .WithColumn("GrantType").AsString(30).NotNullable().WithDefaultValue("client_credentials") + .WithColumn("EncryptedCredentialJson").AsString(int.MaxValue).Nullable() + .WithColumn("ContractVersion").AsString(20).Nullable() + .WithColumn("AutoSubmitOnFinalize").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("IsEnabled").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("LastTokenIssuedOn").AsDateTime2().Nullable() + .WithColumn("LastSuccessfulCallOn").AsDateTime2().Nullable() + .WithColumn("LastError").AsString(int.MaxValue).Nullable() + .WithColumn("UpdatedByUserId").AsString(128).Nullable() + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L); + + Execute.Sql("CREATE UNIQUE NONCLUSTERED INDEX UX_RmsNerisProfiles_Department ON RmsNerisProfiles (DepartmentId);"); + } + + if (!Schema.Table("RmsNerisValueSets").Exists()) + { + Create.Table("RmsNerisValueSets") + .WithColumn("RmsNerisValueSetEntryId").AsInt32().NotNullable().PrimaryKey().Identity() + .WithColumn("ContractVersion").AsString(20).NotNullable() + .WithColumn("SetKey").AsString(60).NotNullable() + .WithColumn("Code").AsString(300).NotNullable() + .WithColumn("Label").AsString(300).Nullable() + .WithColumn("ParentCode").AsString(300).Nullable() + .WithColumn("SortOrder").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("IsRetired").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("CreatedOn").AsDateTime2().NotNullable(); + + Execute.Sql("CREATE UNIQUE NONCLUSTERED INDEX UX_RmsNerisValueSets_Version_Set_Code ON RmsNerisValueSets (ContractVersion, SetKey, Code);"); + } + + if (!Schema.Table("RmsNerisCrosswalks").Exists()) + { + Create.Table("RmsNerisCrosswalks") + .WithColumn("RmsNerisCrosswalkId").AsString(36).NotNullable().PrimaryKey() + .WithColumn("DepartmentId").AsInt32().NotNullable() + .WithColumn("ProtectionId").AsString(36).NotNullable() + .WithColumn("ContractVersion").AsString(20).NotNullable() + .WithColumn("SetKey").AsString(60).NotNullable() + .WithColumn("LocalSource").AsString(50).NotNullable() + .WithColumn("LocalCode").AsString(200).NotNullable() + .WithColumn("NerisCode").AsString(300).NotNullable() + .WithColumn("IsDefault").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("CreatedByUserId").AsString(128).Nullable() + .WithColumn("CreatedOn").AsDateTime2().NotNullable() + .WithColumn("ModifiedOn").AsDateTime2().NotNullable() + .WithColumn("RowVersion").AsInt64().NotNullable().WithDefaultValue(1L) + .WithColumn("DeletedOn").AsDateTime2().Nullable(); + + Create.Index("IX_RmsNerisCrosswalks_Department_Version").OnTable("RmsNerisCrosswalks") + .OnColumn("DepartmentId").Ascending().OnColumn("ContractVersion").Ascending(); + Execute.Sql("CREATE UNIQUE NONCLUSTERED INDEX UX_RmsNerisCrosswalks_Local ON RmsNerisCrosswalks (DepartmentId, ContractVersion, SetKey, LocalSource, LocalCode) WHERE DeletedOn IS NULL;"); + } + } + + public override void Down() + { + foreach (var table in new[] { "RmsNerisCrosswalks", "RmsNerisValueSets", "RmsNerisProfiles" }) + { + if (Schema.Table(table).Exists()) + Delete.Table(table); + } + } + } +} diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0164_AddRmsIncidentReportCorePg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0164_AddRmsIncidentReportCorePg.cs new file mode 100644 index 00000000..75b288b2 --- /dev/null +++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0164_AddRmsIncidentReportCorePg.cs @@ -0,0 +1,289 @@ +using FluentMigrator; + +namespace Resgrid.Providers.MigrationsPg.Migrations +{ + /// + /// PostgreSQL twin of M0164 (registry M0164, RMS-2): rmsincidentreports, rmssourcefacts, rmsunitresponses, + /// rmsincidenttypes, rmsactiontactics, rmsaids, rmslocations, rmsnarratives, rmsvalidationissues. Lowercase + /// unquoted identifiers, citext for text, partial unique indexes for the SingleAuthoritative and idempotency rules. + /// + [Migration(164)] + public class M0164_AddRmsIncidentReportCorePg : Migration + { + public override void Up() + { + if (!Schema.Table("rmsincidentreports").Exists()) + { + Create.Table("rmsincidentreports") + .WithColumn("rmsincidentreportid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("callid").AsInt32().NotNullable() + .WithColumn("reportingentityid").AsCustom("citext").NotNullable() + .WithColumn("definitionkey").AsCustom("citext").NotNullable() + .WithColumn("definitionversion").AsInt32().NotNullable() + .WithColumn("profileversion").AsCustom("citext").Nullable() + .WithColumn("lifecyclepreset").AsInt32().NotNullable() + .WithColumn("state").AsInt32().NotNullable() + .WithColumn("recordnumber").AsCustom("citext").Nullable() + .WithColumn("draftreference").AsCustom("citext").Nullable() + .WithColumn("incidentnumber").AsCustom("citext").Nullable() + .WithColumn("displaysummary").AsCustom("citext").Nullable() + .WithColumn("stationgroupid").AsInt32().Nullable() + .WithColumn("authoruserid").AsString(128).NotNullable() + .WithColumn("owneruserid").AsString(128).Nullable() + .WithColumn("revieweruserid").AsString(128).Nullable() + .WithColumn("approveruserid").AsString(128).Nullable() + .WithColumn("reviewdueon").AsDateTime2().Nullable() + .WithColumn("submittedforreviewon").AsDateTime2().Nullable() + .WithColumn("returnedon").AsDateTime2().Nullable() + .WithColumn("returnreasoncode").AsCustom("citext").Nullable() + .WithColumn("returnreasontext").AsCustom("citext").Nullable() + .WithColumn("returncount").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("approvedon").AsDateTime2().Nullable() + .WithColumn("finalizedon").AsDateTime2().Nullable() + .WithColumn("finalizedbyuserid").AsString(128).Nullable() + .WithColumn("currentrevisionid").AsString(36).Nullable() + .WithColumn("revisioncount").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("amendsrevisionid").AsString(36).Nullable() + .WithColumn("voidedon").AsDateTime2().Nullable() + .WithColumn("voidedbyuserid").AsString(128).Nullable() + .WithColumn("voidreasoncode").AsCustom("citext").Nullable() + .WithColumn("voidreasontext").AsCustom("citext").Nullable() + .WithColumn("cancelledon").AsDateTime2().Nullable() + .WithColumn("cancelledbyuserid").AsString(128).Nullable() + .WithColumn("nerisincidentid").AsCustom("citext").Nullable() + .WithColumn("lastsubmissionid").AsString(36).Nullable() + .WithColumn("lastsubmissionstate").AsInt32().Nullable() + .WithColumn("lastsubmittedon").AsDateTime2().Nullable() + .WithColumn("acceptedon").AsDateTime2().Nullable() + .WithColumn("rejectedon").AsDateTime2().Nullable() + .WithColumn("rejectionsummary").AsCustom("citext").Nullable() + .WithColumn("callcreatedon").AsDateTime2().Nullable() + .WithColumn("callansweredon").AsDateTime2().Nullable() + .WithColumn("callarrivalon").AsDateTime2().Nullable() + .WithColumn("incidentclearedon").AsDateTime2().Nullable() + .WithColumn("dispatchcenterid").AsCustom("citext").Nullable() + .WithColumn("determinantcode").AsCustom("citext").Nullable() + .WithColumn("dispatchincidentcode").AsCustom("citext").Nullable() + .WithColumn("disposition").AsCustom("citext").Nullable() + .WithColumn("peoplepresent").AsBoolean().Nullable() + .WithColumn("displacementcount").AsInt32().Nullable() + .WithColumn("animalsrescued").AsInt32().Nullable() + .WithColumn("specialmodifierscsv").AsCustom("citext").Nullable() + .WithColumn("idempotencykey").AsCustom("citext").Nullable() + .WithColumn("originclient").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("createdbyuserid").AsString(128).Nullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("modifiedbyuserid").AsString(128).Nullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L) + .WithColumn("deletedon").AsDateTime2().Nullable(); + + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsincidentreports_department_state_created ON rmsincidentreports (departmentid, state, createdon DESC);"); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsincidentreports_department_call ON rmsincidentreports (departmentid, callid);"); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsincidentreports_department_owner ON rmsincidentreports (departmentid, owneruserid);"); + Execute.Sql("CREATE UNIQUE INDEX IF NOT EXISTS ux_rmsincidentreports_call_entity ON rmsincidentreports (departmentid, callid, reportingentityid, definitionkey) WHERE deletedon IS NULL;"); + Execute.Sql("CREATE UNIQUE INDEX IF NOT EXISTS ux_rmsincidentreports_idempotency ON rmsincidentreports (departmentid, idempotencykey) WHERE idempotencykey IS NOT NULL;"); + } + + if (!Schema.Table("rmssourcefacts").Exists()) + { + Create.Table("rmssourcefacts") + .WithColumn("rmssourcefactid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("revisionid").AsString(36).Nullable() + .WithColumn("factkey").AsCustom("citext").NotNullable() + .WithColumn("sourcekind").AsInt32().NotNullable() + .WithColumn("sourcesystem").AsCustom("citext").Nullable() + .WithColumn("sourceentitytype").AsCustom("citext").Nullable() + .WithColumn("sourceentityid").AsCustom("citext").Nullable() + .WithColumn("sourcevalue").AsCustom("citext").Nullable() + .WithColumn("currentvalue").AsCustom("citext").Nullable() + .WithColumn("sourcetime").AsDateTime2().Nullable() + .WithColumn("importedon").AsDateTime2().NotNullable() + .WithColumn("correctedon").AsDateTime2().Nullable() + .WithColumn("correctedbyuserid").AsString(128).Nullable() + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmssourcefacts_department_record_revision ON rmssourcefacts (departmentid, recordid, revisionid);"); + } + + if (!Schema.Table("rmsunitresponses").Exists()) + { + Create.Table("rmsunitresponses") + .WithColumn("rmsunitresponseid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("revisionid").AsString(36).Nullable() + .WithColumn("unitid").AsInt32().Nullable() + .WithColumn("unitnamesnapshot").AsCustom("citext").Nullable() + .WithColumn("unittypesnapshot").AsCustom("citext").Nullable() + .WithColumn("unitnerisid").AsCustom("citext").Nullable() + .WithColumn("stationgroupidsnapshot").AsInt32().Nullable() + .WithColumn("staffing").AsInt32().Nullable() + .WithColumn("unabletodispatch").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("dispatchedon").AsDateTime2().Nullable() + .WithColumn("enrouteon").AsDateTime2().Nullable() + .WithColumn("onsceneon").AsDateTime2().Nullable() + .WithColumn("canceledenrouteon").AsDateTime2().Nullable() + .WithColumn("stagingon").AsDateTime2().Nullable() + .WithColumn("clearedon").AsDateTime2().Nullable() + .WithColumn("responsemode").AsCustom("citext").Nullable() + .WithColumn("transportmode").AsCustom("citext").Nullable() + .WithColumn("timessourcekind").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("disposition").AsCustom("citext").Nullable() + .WithColumn("ordinal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsunitresponses_department_record_revision ON rmsunitresponses (departmentid, recordid, revisionid);"); + } + + if (!Schema.Table("rmsincidenttypes").Exists()) + { + Create.Table("rmsincidenttypes") + .WithColumn("rmsincidenttypeid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("revisionid").AsString(36).Nullable() + .WithColumn("typecode").AsCustom("citext").NotNullable() + .WithColumn("isprimary").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("localcode").AsCustom("citext").Nullable() + .WithColumn("valuesetversion").AsCustom("citext").Nullable() + .WithColumn("ordinal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsincidenttypes_department_record_revision ON rmsincidenttypes (departmentid, recordid, revisionid);"); + } + + if (!Schema.Table("rmsactiontactics").Exists()) + { + Create.Table("rmsactiontactics") + .WithColumn("rmsactiontacticid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("revisionid").AsString(36).Nullable() + .WithColumn("tacticcode").AsCustom("citext").NotNullable() + .WithColumn("actorunitid").AsInt32().Nullable() + .WithColumn("occurredon").AsDateTime2().Nullable() + .WithColumn("sourcekind").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("outcome").AsCustom("citext").Nullable() + .WithColumn("ordinal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsactiontactics_department_record_revision ON rmsactiontactics (departmentid, recordid, revisionid);"); + } + + if (!Schema.Table("rmsaids").Exists()) + { + Create.Table("rmsaids") + .WithColumn("rmsaidid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("revisionid").AsString(36).Nullable() + .WithColumn("direction").AsCustom("citext").NotNullable() + .WithColumn("aidtype").AsCustom("citext").NotNullable() + .WithColumn("counterpartnerisid").AsCustom("citext").Nullable() + .WithColumn("counterpartname").AsCustom("citext").Nullable() + .WithColumn("isnonfiredepartment").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("nonfdtype").AsCustom("citext").Nullable() + .WithColumn("ordinal").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsaids_department_record_revision ON rmsaids (departmentid, recordid, revisionid);"); + } + + if (!Schema.Table("rmslocations").Exists()) + { + Create.Table("rmslocations") + .WithColumn("rmslocationid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("revisionid").AsString(36).Nullable() + .WithColumn("addresstext").AsCustom("citext").Nullable() + .WithColumn("number").AsCustom("citext").Nullable() + .WithColumn("numberprefix").AsCustom("citext").Nullable() + .WithColumn("numbersuffix").AsCustom("citext").Nullable() + .WithColumn("street").AsCustom("citext").Nullable() + .WithColumn("unitvalue").AsCustom("citext").Nullable() + .WithColumn("municipality").AsCustom("citext").Nullable() + .WithColumn("county").AsCustom("citext").Nullable() + .WithColumn("state").AsCustom("citext").Nullable() + .WithColumn("postalcode").AsCustom("citext").Nullable() + .WithColumn("country").AsCustom("citext").Nullable() + .WithColumn("placetype").AsCustom("citext").Nullable() + .WithColumn("locationuse").AsCustom("citext").Nullable() + .WithColumn("crossstreet1").AsCustom("citext").Nullable() + .WithColumn("crossstreet2").AsCustom("citext").Nullable() + .WithColumn("latitude").AsDecimal(12, 8).Nullable() + .WithColumn("longitude").AsDecimal(12, 8).Nullable() + .WithColumn("jurisdiction").AsCustom("citext").Nullable() + .WithColumn("sourcekind").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmslocations_department_record_revision ON rmslocations (departmentid, recordid, revisionid);"); + } + + if (!Schema.Table("rmsnarratives").Exists()) + { + Create.Table("rmsnarratives") + .WithColumn("rmsnarrativeid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("revisionid").AsString(36).Nullable() + .WithColumn("narrative").AsCustom("citext").Nullable() + .WithColumn("impedimentnarrative").AsCustom("citext").Nullable() + .WithColumn("outcomenarrative").AsCustom("citext").Nullable() + .WithColumn("supplementaljson").AsCustom("citext").Nullable() + .WithColumn("isprotected").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("protectedenvelope").AsString(int.MaxValue).Nullable() + .WithColumn("protectedcatalogversion").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsnarratives_department_record_revision ON rmsnarratives (departmentid, recordid, revisionid);"); + } + + if (!Schema.Table("rmsvalidationissues").Exists()) + { + Create.Table("rmsvalidationissues") + .WithColumn("rmsvalidationissueid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("revisionid").AsString(36).Nullable() + .WithColumn("profileversion").AsCustom("citext").Nullable() + .WithColumn("rulekey").AsCustom("citext").NotNullable() + .WithColumn("severity").AsInt32().NotNullable() + .WithColumn("fieldpath").AsCustom("citext").Nullable() + .WithColumn("message").AsCustom("citext").Nullable() + .WithColumn("source").AsInt32().NotNullable() + .WithColumn("resolvedon").AsDateTime2().Nullable() + .WithColumn("createdon").AsDateTime2().NotNullable(); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsvalidationissues_department_record ON rmsvalidationissues (departmentid, recordid);"); + } + } + + public override void Down() + { + foreach (var table in new[] { "rmsvalidationissues", "rmsnarratives", "rmslocations", "rmsaids", "rmsactiontactics", "rmsincidenttypes", "rmsunitresponses", "rmssourcefacts", "rmsincidentreports" }) + { + if (Schema.Table(table).Exists()) + Delete.Table(table); + } + } + } +} diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0165_AddRmsSubmissionsAndSignaturesPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0165_AddRmsSubmissionsAndSignaturesPg.cs new file mode 100644 index 00000000..ab1a0d41 --- /dev/null +++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0165_AddRmsSubmissionsAndSignaturesPg.cs @@ -0,0 +1,85 @@ +using FluentMigrator; + +namespace Resgrid.Providers.MigrationsPg.Migrations +{ + /// PostgreSQL twin of M0165 (registry M0165, RMS-2): rmssubmissions and rmssignatures. + [Migration(165)] + public class M0165_AddRmsSubmissionsAndSignaturesPg : Migration + { + public override void Up() + { + if (!Schema.Table("rmssubmissions").Exists()) + { + Create.Table("rmssubmissions") + .WithColumn("rmssubmissionid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("recordkind").AsInt32().NotNullable() + .WithColumn("revisionid").AsString(36).Nullable() + .WithColumn("destination").AsCustom("citext").NotNullable() + .WithColumn("destinationversion").AsCustom("citext").Nullable() + .WithColumn("idempotencykey").AsCustom("citext").NotNullable() + .WithColumn("state").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("attempts").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("maxattempts").AsInt32().NotNullable().WithDefaultValue(5) + .WithColumn("nextattempton").AsDateTime2().Nullable() + .WithColumn("leaseowner").AsCustom("citext").Nullable() + .WithColumn("leaseexpireson").AsDateTime2().Nullable() + .WithColumn("payloadjson").AsCustom("citext").Nullable() + .WithColumn("payloadchecksum").AsCustom("citext").Nullable() + .WithColumn("responsejson").AsCustom("citext").Nullable() + .WithColumn("responsechecksum").AsCustom("citext").Nullable() + .WithColumn("responsestatuscode").AsInt32().Nullable() + .WithColumn("externalid").AsCustom("citext").Nullable() + .WithColumn("externalstatus").AsCustom("citext").Nullable() + .WithColumn("errorsummary").AsCustom("citext").Nullable() + .WithColumn("queuedon").AsDateTime2().NotNullable() + .WithColumn("senton").AsDateTime2().Nullable() + .WithColumn("completedon").AsDateTime2().Nullable() + .WithColumn("createdbyuserid").AsString(128).Nullable() + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + + Execute.Sql("CREATE UNIQUE INDEX IF NOT EXISTS ux_rmssubmissions_idempotencykey ON rmssubmissions (idempotencykey);"); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmssubmissions_state_nextattempt ON rmssubmissions (state, nextattempton);"); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmssubmissions_department_record ON rmssubmissions (departmentid, recordid);"); + } + + if (!Schema.Table("rmssignatures").Exists()) + { + Create.Table("rmssignatures") + .WithColumn("rmssignatureid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("recordid").AsString(36).NotNullable() + .WithColumn("recordkind").AsInt32().NotNullable() + .WithColumn("revisionid").AsString(36).NotNullable() + .WithColumn("signeruserid").AsString(128).NotNullable() + .WithColumn("signernamesnapshot").AsCustom("citext").Nullable() + .WithColumn("signerrolesnapshot").AsCustom("citext").Nullable() + .WithColumn("intent").AsInt32().NotNullable() + .WithColumn("statementversion").AsCustom("citext").Nullable() + .WithColumn("statementtext").AsCustom("citext").Nullable() + .WithColumn("method").AsInt32().NotNullable() + .WithColumn("signedon").AsDateTime2().NotNullable() + .WithColumn("ipaddress").AsCustom("citext").Nullable() + .WithColumn("artifactchecksum").AsCustom("citext").Nullable() + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmssignatures_department_record ON rmssignatures (departmentid, recordid);"); + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmssignatures_department_revision ON rmssignatures (departmentid, revisionid);"); + } + } + + public override void Down() + { + if (Schema.Table("rmssignatures").Exists()) + Delete.Table("rmssignatures"); + if (Schema.Table("rmssubmissions").Exists()) + Delete.Table("rmssubmissions"); + } + } +} diff --git a/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0166_AddRmsNerisProfilesAndValueSetsPg.cs b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0166_AddRmsNerisProfilesAndValueSetsPg.cs new file mode 100644 index 00000000..01bc6444 --- /dev/null +++ b/Providers/Resgrid.Providers.MigrationsPg/Migrations/M0166_AddRmsNerisProfilesAndValueSetsPg.cs @@ -0,0 +1,85 @@ +using FluentMigrator; + +namespace Resgrid.Providers.MigrationsPg.Migrations +{ + /// PostgreSQL twin of M0166 (registry M0166, RMS-2): rmsnerisprofiles, rmsnerisvaluesets, rmsneriscrosswalks. + [Migration(166)] + public class M0166_AddRmsNerisProfilesAndValueSetsPg : Migration + { + public override void Up() + { + if (!Schema.Table("rmsnerisprofiles").Exists()) + { + Create.Table("rmsnerisprofiles") + .WithColumn("rmsnerisprofileid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("nerisentityid").AsCustom("citext").Nullable() + .WithColumn("entityname").AsCustom("citext").Nullable() + .WithColumn("environment").AsCustom("citext").NotNullable().WithDefaultValue("production") + .WithColumn("baseurloverride").AsCustom("citext").Nullable() + .WithColumn("granttype").AsCustom("citext").NotNullable().WithDefaultValue("client_credentials") + .WithColumn("encryptedcredentialjson").AsString(int.MaxValue).Nullable() + .WithColumn("contractversion").AsCustom("citext").Nullable() + .WithColumn("autosubmitonfinalize").AsBoolean().NotNullable().WithDefaultValue(true) + .WithColumn("isenabled").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("lasttokenissuedon").AsDateTime2().Nullable() + .WithColumn("lastsuccessfulcallon").AsDateTime2().Nullable() + .WithColumn("lasterror").AsCustom("citext").Nullable() + .WithColumn("updatedbyuserid").AsString(128).Nullable() + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L); + + Execute.Sql("CREATE UNIQUE INDEX IF NOT EXISTS ux_rmsnerisprofiles_department ON rmsnerisprofiles (departmentid);"); + } + + if (!Schema.Table("rmsnerisvaluesets").Exists()) + { + Create.Table("rmsnerisvaluesets") + .WithColumn("rmsnerisvaluesetentryid").AsInt32().NotNullable().PrimaryKey().Identity() + .WithColumn("contractversion").AsCustom("citext").NotNullable() + .WithColumn("setkey").AsCustom("citext").NotNullable() + .WithColumn("code").AsCustom("citext").NotNullable() + .WithColumn("label").AsCustom("citext").Nullable() + .WithColumn("parentcode").AsCustom("citext").Nullable() + .WithColumn("sortorder").AsInt32().NotNullable().WithDefaultValue(0) + .WithColumn("isretired").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("createdon").AsDateTime2().NotNullable(); + + Execute.Sql("CREATE UNIQUE INDEX IF NOT EXISTS ux_rmsnerisvaluesets_version_set_code ON rmsnerisvaluesets (contractversion, setkey, code);"); + } + + if (!Schema.Table("rmsneriscrosswalks").Exists()) + { + Create.Table("rmsneriscrosswalks") + .WithColumn("rmsneriscrosswalkid").AsString(36).NotNullable().PrimaryKey() + .WithColumn("departmentid").AsInt32().NotNullable() + .WithColumn("protectionid").AsString(36).NotNullable() + .WithColumn("contractversion").AsCustom("citext").NotNullable() + .WithColumn("setkey").AsCustom("citext").NotNullable() + .WithColumn("localsource").AsCustom("citext").NotNullable() + .WithColumn("localcode").AsCustom("citext").NotNullable() + .WithColumn("neriscode").AsCustom("citext").NotNullable() + .WithColumn("isdefault").AsBoolean().NotNullable().WithDefaultValue(false) + .WithColumn("createdbyuserid").AsString(128).Nullable() + .WithColumn("createdon").AsDateTime2().NotNullable() + .WithColumn("modifiedon").AsDateTime2().NotNullable() + .WithColumn("rowversion").AsInt64().NotNullable().WithDefaultValue(1L) + .WithColumn("deletedon").AsDateTime2().Nullable(); + + Execute.Sql("CREATE INDEX IF NOT EXISTS ix_rmsneriscrosswalks_department_version ON rmsneriscrosswalks (departmentid, contractversion);"); + Execute.Sql("CREATE UNIQUE INDEX IF NOT EXISTS ux_rmsneriscrosswalks_local ON rmsneriscrosswalks (departmentid, contractversion, setkey, localsource, localcode) WHERE deletedon IS NULL;"); + } + } + + public override void Down() + { + foreach (var table in new[] { "rmsneriscrosswalks", "rmsnerisvaluesets", "rmsnerisprofiles" }) + { + if (Schema.Table(table).Exists()) + Delete.Table(table); + } + } + } +} diff --git a/Providers/Resgrid.Providers.Neris/Contract/neris-openapi-v1.4.78-2026-09-03.json b/Providers/Resgrid.Providers.Neris/Contract/neris-openapi-v1.4.78-2026-09-03.json new file mode 100644 index 00000000..ed78a0d4 --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/Contract/neris-openapi-v1.4.78-2026-09-03.json @@ -0,0 +1 @@ +{"openapi":"3.1.0","info":{"title":"NERIS","version":"1.4.78"},"servers":[{"url":"https://api.neris.fsri.org/v1"}],"paths":{"/health":{"get":{"summary":"Health","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"head":{"summary":"Health","operationId":"health_health_head","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/openapi.yaml":{"get":{"summary":"Openapi","operationId":"openapi_openapi_yaml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/openapi.yml":{"get":{"summary":"Openapi","operationId":"openapi_openapi_yml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/token":{"post":{"tags":["OAuth token routes."],"summary":"Issue Token","description":"Obtain tokens via Oauth2 flows","operationId":"issue_token_token_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenBody"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/AuthenticationSuccessResponse"},{"$ref":"#/components/schemas/AuthChallengeResponse"},{"$ref":"#/components/schemas/ErrorResponse"}],"title":"Response Issue Token Token Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity":{"get":{"tags":["Entities"],"summary":"List Entities","description":"List entities with optional query parameter filters.","operationId":"list_entities_entity_get","parameters":[{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":3,"maxLength":255},{"type":"null"}],"description":"Filters entities by a case-insensitive substring of the entity's name.","type":"string","minLength":3,"maxLength":255,"title":"Name"},"description":"Filters entities by a case-insensitive substring of the entity's name."},{"name":"neris_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":2,"maxLength":10},{"type":"null"}],"description":"Filters entities by a substring of the entity's NERIS ID.","type":"string","minLength":2,"maxLength":10,"title":"Neris Id"},"description":"Filters entities by a substring of the entity's NERIS ID."},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs","minLength":2,"maxLength":2},{"type":"null"}],"description":"Filters entities by the two character state or province code.","type":"string","enum":["AA","AB","AE","AGU","AK","AL","AP","AR","AS","AZ","BC","BCN","BCS","CA","CAM","CHH","CHP","CMX","CO","COA","COL","CT","DC","DE","DUR","FL","FM","GA","GRO","GU","GUA","HI","HID","IA","ID","IL","IN","JAL","KS","KY","LA","MA","MB","MD","ME","MEX","MH","MI","MIC","MN","MO","MOR","MP","MS","MT","NA","NAY","NB","NC","ND","NE","NH","NJ","NL","NLE","NM","NS","NT","NU","NV","NY","OAX","OH","OK","ON","OR","PA","PE","PR","PUE","PW","QC","QUE","RI","ROO","SC","SD","SIN","SK","SLP","SON","TAB","TAM","TLA","TN","TX","UM","UT","VA","VER","VI","VT","WA","WI","WV","WY","YT","YUC","ZAC"],"title":"State"},"description":"Filters entities by the two character state or province code."},{"name":"entity_class","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/EntityClassOptions"},{"type":"null"}],"description":"Filters entities by their class.","type":"string","enum":["FEDERAL_AGENCY","FIRE_DEPARTMENT","FIRE_MARSHAL"],"title":"Entity Class"},"description":"Filters entities by their class."},{"name":"entity_subtype","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/TypeEntityValue"},{"type":"null"}],"description":"Filters entities by their subtype.","type":"string","enum":["CONTRACT","FEDERAL","LOCAL","OTHER","PRIVATE","STATE","TRANSPORTATION","TRIBAL"],"title":"Entity Subtype"},"description":"Filters entities by their subtype."},{"name":"last_modified","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(before|after)\\(((\\d{1,3}[yMdhms])|((19|20)\\d{2}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?((\\+|\\-)\\d{2}:\\d{2})?))\\)$","description":"A string expression for a time period relative to and prior to either the current time or an absolute time.","examples":["before(24h) # 24 hours ago and before","after(2M) # Two months ago until current time","before(1y) # One year ago and before","before(2024-03-20T11:24:15.256+00:00) # Before 2024-03-20T11:24:15.256+00:00","after(2024-03-20T11:24:15.256+00:00) # After 2024-03-20T11:24:15.256+00:00"]},{"type":"null"}],"description":"A string expression for a time period relative to either the current time or an absolute time.","title":"Last Modified"},"description":"A string expression for a time period relative to either the current time or an absolute time.","examples":{"Before 24 hours ago":{"summary":"Before 24 hours ago","description":"Filter for entities modified 24 hours ago or prior","value":"before(24h)"},"After two months ago":{"summary":"After two months ago","description":"Filter for entities modified two months ago or later","value":"after(2M)"},"Before one year ago":{"summary":"Before one year ago","description":"Filter for entities modified one year ago or prior","value":"before(1y)"},"Before datetime":{"summary":"Before a specific datetime","description":"Filter for entities modified prior to the supplied datetime","value":"before(2024-03-20T11:24:13.275-05:00)"},"After datetime":{"summary":"After a specific datetime","description":"Filter for entities modified after the supplied datetime","value":"after(2024-03-20T11:24:13.275-05:00)"}}},{"name":"page_number","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"type":"integer","minimum":1,"default":1,"title":"Page Number"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Page Size"}},{"name":"sort_by","in":"query","required":false,"schema":{"$ref":"#/components/schemas/DeptSortBy","description":"Attribute to sort by.","type":"string","enum":["name","neris_id","address_line_1","city","state","zip_code","department_type","website","last_modified"],"default":"neris_id"},"description":"Attribute to sort by."},{"name":"sort_direction","in":"query","required":false,"schema":{"$ref":"#/components/schemas/SortDirection","description":"Order of sorting.","type":"string","enum":["ASCENDING","DESCENDING"],"default":"ASCENDING"},"description":"Order of sorting."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEntitiesSummaryInfoResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["Entities"],"summary":"Create Entity","operationId":"create_entity_entity_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/CreateDepartmentPayload"},{"$ref":"#/components/schemas/CreateFireMarshalPayload"},{"$ref":"#/components/schemas/CreateFederalAgencyPayload"}],"description":"Payload for the entity","title":"Payload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/DepartmentCreatedModifiedResponse"},{"$ref":"#/components/schemas/EntityCreatedModifiedResponse"}],"title":"Response Create Entity Entity Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}":{"get":{"tags":["Entities"],"summary":"Get Entity","operationId":"get_entity_entity__neris_id_entity__get","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"anyOf":[{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"]},{"type":"null"}],"title":"NERIS ID of the entity"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/DepartmentWithFeatureFlagsResponse"},{"$ref":"#/components/schemas/FireMarshalResponse"},{"$ref":"#/components/schemas/FederalAgencyResponse"}],"title":"Response Get Entity Entity Neris Id Entity Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Entities"],"summary":"Put Entity","operationId":"put_entity_entity__neris_id_entity__put","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|FM|FA)\\d{8}$","examples":["FD24027334","FM24001001","FA24001001"],"title":"Neris Id Entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"description":"Entity payload - use DepartmentPayload for Department entities and FireMarshalPayload for Fire Marshal entities","title":"Body","oneOf":[{"$ref":"#/components/schemas/DepartmentPayload"},{"$ref":"#/components/schemas/FireMarshalPayload"}]}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/DepartmentCreatedModifiedResponse"},{"$ref":"#/components/schemas/EntityCreatedModifiedResponse"}],"title":"Response Put Entity Entity Neris Id Entity Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Entities"],"summary":"Patch Entity","description":"Patch an entity","operationId":"patch_entity_entity__neris_id_entity__patch","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|FM|FA)\\d{8}$","examples":["FD24027334","FM24001001","FA24001001"],"title":"Neris Id Entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"description":"Entity patch payload - only include fields to update (all fields optional).","title":"Body","oneOf":[{"$ref":"#/components/schemas/PatchDepartmentPayload"},{"$ref":"#/components/schemas/PatchFireMarshalPayload"}]}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/DepartmentCreatedModifiedResponse"},{"$ref":"#/components/schemas/EntityCreatedModifiedResponse"}],"title":"Response Patch Entity Entity Neris Id Entity Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}/metrics":{"get":{"tags":["Entities"],"summary":"Get Entity Metrics","operationId":"get_entity_metrics_entity__neris_id_entity__metrics_get","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"anyOf":[{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"]},{"type":"null"}],"title":"NERIS ID of the entity"}},{"name":"time_zone","in":"query","required":false,"schema":{"$ref":"#/components/schemas/TimeZones","description":"Desired IANA time zone","default":"UTC"},"description":"Desired IANA time zone"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentMetricsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}/entity_activation":{"put":{"tags":["Entities"],"summary":"Put Entity Activation","operationId":"put_entity_activation_entity__neris_id_entity__entity_activation_put","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|FM|FA)\\d{8}$","examples":["FD24027334","FM24001001","FA24001001"],"title":"The entity's NERIS ID"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEntityActivationPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}/entity_feature_flags":{"patch":{"tags":["Entities"],"summary":"Patch Entity Feature Flags","description":"Update entity feature flags","operationId":"patch_entity_feature_flags_entity__neris_id_entity__entity_feature_flags_patch","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityFeatureFlagsPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntityFeatureFlagsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}/station":{"post":{"tags":["Entities"],"summary":"Create Station","description":"Create a station","operationId":"create_station_entity__neris_id_entity__station_post","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStationPayload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StationCreatedModifiedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}/station/{neris_id_station}":{"delete":{"tags":["Entities"],"summary":"Delete Station","description":"Remove a station and associated units","operationId":"delete_station_entity__neris_id_entity__station__neris_id_station__delete","parameters":[{"name":"neris_id_station","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}$","examples":["FD24027334S001","FD24160543S010"],"title":"The station's NERIS ID"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Entities"],"summary":"Patch Station","description":"Patch a station","operationId":"patch_station_entity__neris_id_entity__station__neris_id_station__patch","parameters":[{"name":"neris_id_station","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}$","examples":["FD24027334S001","FD24160543S010"],"title":"The station's NERIS ID"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchStationPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StationCreatedModifiedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}/station/{neris_id_station}/unit":{"post":{"tags":["Entities"],"summary":"Create Unit","description":"Create a unit","operationId":"create_unit_entity__neris_id_entity__station__neris_id_station__unit_post","parameters":[{"name":"neris_id_station","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}$","examples":["FD24027334S001","FD24160543S010"],"title":"The station's NERIS ID"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUnitPayload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitCreatedModifiedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}/station/{neris_id_station}/unit/{neris_id_unit}":{"delete":{"tags":["Entities"],"summary":"Delete Unit","description":"Remove a unit","operationId":"delete_unit_entity__neris_id_entity__station__neris_id_station__unit__neris_id_unit__delete","parameters":[{"name":"neris_id_station","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}$","examples":["FD24027334S001","FD24160543S010"],"title":"The station's NERIS ID"}},{"name":"neris_id_unit","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}U[A-Z\\d]{3}$","examples":["FD24027334S001U003","FD24160543S010U001"],"title":"The unit's NERIS ID"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Entities"],"summary":"Patch Unit","description":"Patch a unit","operationId":"patch_unit_entity__neris_id_entity__station__neris_id_station__unit__neris_id_unit__patch","parameters":[{"name":"neris_id_station","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}$","examples":["FD24027334S001","FD24160543S010"],"title":"The station's NERIS ID"}},{"name":"neris_id_unit","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}U[A-Z\\d]{3}$","examples":["FD24027334S001U003","FD24160543S010U001"],"title":"The unit's NERIS ID"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchUnitPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UnitCreatedModifiedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}/user_entity_membership":{"get":{"tags":["Entities"],"summary":"List User Entity Memberships","description":"List user entity memberships for the entity","operationId":"list_user_entity_memberships_entity__neris_id_entity__user_entity_membership_get","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}},{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":24},{"type":"null"}],"description":"Filters users by given, family, or user names.","type":"string","minLength":1,"maxLength":24,"title":"Name"},"description":"Filters users by given, family, or user names."},{"name":"role","in":"query","required":false,"schema":{"anyOf":[{"anyOf":[{"type":"string"},{"type":"string","format":"uuid"}],"minLength":1,"maxLength":36},{"type":"null"}],"description":"Filters users attached to a specific role for this entity. Query by a substring of role name or nuid.","type":"string","minLength":1,"maxLength":36,"title":"Role"},"description":"Filters users attached to a specific role for this entity. Query by a substring of role name or nuid."},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/UserStatus"},{"type":"null"}],"description":"Filters users by user status.","type":"string","enum":["ARCHIVED","COMPROMISED","CONFIRMED","EXTERNAL_PROVIDER","FORCE_CHANGE_PASSWORD","RESET_REQUIRED","UNCONFIRMED","UNKNOWN"],"title":"Status"},"description":"Filters users by user status."},{"name":"email_verified","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Filters users by email verification status.","type":"boolean","title":"Email Verified"},"description":"Filters users by email verification status."},{"name":"page_number","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"type":"integer","minimum":1,"default":1,"title":"Page Number"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListUserInfoResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/entity/{neris_id_entity}/logo":{"put":{"tags":["Entities"],"summary":"Upsert Logo","description":"Upsert entity logo","operationId":"upsert_logo_entity__neris_id_entity__logo_put","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"anyOf":[{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"]},{"type":"null"}],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upsert_logo_entity__neris_id_entity__logo_put"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"string","title":"Response Upsert Logo Entity Neris Id Entity Logo Put"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/forgot_password":{"put":{"tags":["User management"],"summary":"Finalize Forgot Password","description":"Initiate forgot password flow","operationId":"finalize_forgot_password_user_forgot_password_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinalizeForgotPasswordPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["User management"],"summary":"Initiate Forgot Password","description":"Initiate forgot password flow","operationId":"initiate_forgot_password_user_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InitiateForgotPasswordPayload"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/{sub}/sync":{"put":{"tags":["User management"],"summary":"Sync User","description":"Sync a user's IDP data","operationId":"sync_user_user__sub__sync_put","parameters":[{"name":"sub","in":"path","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"title":"The user's unique ID or email address"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/{sub}":{"get":{"tags":["User management"],"summary":"Get User","description":"Get a user's attribute by their unique ID","operationId":"get_user_user__sub__get","parameters":[{"name":"sub","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"The user's sub (UUID)"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/neris__api__user__models__UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["User management"],"summary":"Delete User","description":"Delete a user by their unique ID","operationId":"delete_user_user__sub__delete","parameters":[{"name":"sub","in":"path","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"title":"The user's sub (UUID) or email address"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["User management"],"summary":"Update User","description":"Update a user's attributes","operationId":"update_user_user__sub__put","parameters":[{"name":"sub","in":"path","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"title":"The user's sub (UUID) or email address"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/neris__api__user__models__UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user":{"post":{"tags":["User management"],"summary":"Create User","description":"Create a new user","operationId":"create_user_user_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserPayload"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/neris__api__user__models__UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/{sub}/user_password_reset":{"put":{"tags":["User management"],"summary":"User Password Reset","description":"Reset a user's password to a temporary password","operationId":"user_password_reset_user__sub__user_password_reset_put","parameters":[{"name":"sub","in":"path","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"title":"The user's sub (UUID) or email address"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/{sub}/user_activation":{"put":{"tags":["User management"],"summary":"Update User Activation","description":"Set a user's activation status","operationId":"update_user_activation_user__sub__user_activation_put","parameters":[{"name":"sub","in":"path","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"title":"The user's sub (UUID) or email address"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserActivationPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/{sub}/user_entity_membership/{neris_id_entity}":{"post":{"tags":["User management"],"summary":"Create User Entity Membership","description":"Create a USER_ENTITY_MEMBERSHIP.","operationId":"create_user_entity_membership_user__sub__user_entity_membership__neris_id_entity__post","parameters":[{"name":"sub","in":"path","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"title":"The user's unique ID or email address"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["User management"],"summary":"Delete User Entity Membership","description":"Delete a USER_ENTITY_MEMBERSHIP.","operationId":"delete_user_entity_membership_user__sub__user_entity_membership__neris_id_entity__delete","parameters":[{"name":"sub","in":"path","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"title":"The user's sub (UUID) or email address"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/{sub}/user_entity_membership":{"get":{"tags":["User management"],"summary":"List User Entity Memberships","description":"List a user's USER_ENTITY_MEMBERSHIPs","operationId":"list_user_entity_memberships_user__sub__user_entity_membership_get","parameters":[{"name":"sub","in":"path","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"title":"The user's unique ID or email address"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserEntitySummaryInfoResponse"},"title":"Response List User Entity Memberships User Sub User Entity Membership Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/{sub}/user_entity_activation/{neris_id_entity}":{"put":{"tags":["User management"],"summary":"Update User Entity Activation","description":"Set a user's activation status for an entity.","operationId":"update_user_entity_activation_user__sub__user_entity_activation__neris_id_entity__put","parameters":[{"name":"sub","in":"path","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"title":"The user's sub (UUID) or email address"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserActivationPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/role":{"post":{"tags":["Authorization management"],"summary":"Create Role","operationId":"create_role_auth_role_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RolePayload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Authorization management"],"summary":"List Roles","description":"List ROLEs for the system or per entity","operationId":"list_roles_auth_role_get","parameters":[{"name":"neris_id_entity","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"]},{"type":"null"}],"title":"NERIS ID of the entity"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleResponse"},"title":"Response List Roles Auth Role Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/role/{nuid}":{"put":{"tags":["Authorization management"],"summary":"Update Role","operationId":"update_role_auth_role__nuid__put","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Unique identifier for the role"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RolePayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Authorization management"],"summary":"Delete Role","description":"Delete a role","operationId":"delete_role_auth_role__nuid__delete","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Unique identifier for the role"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/entity_set":{"post":{"tags":["Authorization management"],"summary":"Create Entity Set","operationId":"create_entity_set_auth_entity_set_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntitySetPayload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntitySetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["Authorization management"],"summary":"List Entity Sets","description":"List entity sets and their members.","operationId":"list_entity_sets_auth_entity_set_get","parameters":[{"name":"neris_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"]},{"type":"null"}],"description":"Filter by entity sets that have a particular entity as a member.","title":"Neris Id"},"description":"Filter by entity sets that have a particular entity as a member."},{"name":"type","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/TypeEntitySetValue"},{"type":"null"}],"description":"Filter by entity set type.","title":"Type"},"description":"Filter by entity set type."},{"name":"page_number","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"type":"integer","minimum":1,"default":1,"title":"Page Number"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEntitySetsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/entity_set/{nuid}":{"get":{"tags":["Authorization management"],"summary":"Get Entity Set","description":"Get an entity set by its unique identifier.","operationId":"get_entity_set_auth_entity_set__nuid__get","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Unique identifier for the entity set"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntitySetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Authorization management"],"summary":"Update Entity Set","operationId":"update_entity_set_auth_entity_set__nuid__put","parameters":[{"name":"nuid","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Unique identifier for the entity set"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntitySetPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EntitySetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/user_role_entity_set_attachment":{"post":{"tags":["Authorization management"],"summary":"Create User Role Entity Set Attachment","description":"Create a USER_ROLE_ENTITY_SET_ATTACHMENT.","operationId":"create_user_role_entity_set_attachment_auth_user_role_entity_set_attachment_post","parameters":[{"name":"sub_user","in":"query","required":true,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"string","format":"email"}],"description":"The user's unique ID or email address","title":"Sub User"},"description":"The user's unique ID or email address"},{"name":"nuid_role","in":"query","required":true,"schema":{"type":"string","format":"uuid","description":"The role's unique ID","title":"Nuid Role"},"description":"The role's unique ID"},{"name":"nuid_entity_set","in":"query","required":true,"schema":{"type":"string","format":"uuid","description":"The entity set's unique ID","title":"Nuid Entity Set"},"description":"The entity set's unique ID"}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRoleEntitySetAttachmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Authorization management"],"summary":"Delete User Role Entity Set Attachments","description":"Delete a single USER_ROLE_ENTITY_SET_ATTACHMENT.","operationId":"delete_user_role_entity_set_attachments_auth_user_role_entity_set_attachment_delete","parameters":[{"name":"sub_user","in":"query","required":true,"schema":{"type":"string","format":"uuid","description":"The user's unique ID","title":"Sub User"},"description":"The user's unique ID"},{"name":"nuid_role","in":"query","required":true,"schema":{"type":"string","format":"uuid","description":"The role's unique ID","title":"Nuid Role"},"description":"The role's unique ID"},{"name":"nuid_entity_set","in":"query","required":true,"schema":{"type":"string","format":"uuid","description":"The entity set's unique ID","title":"Nuid Entity Set"},"description":"The entity set's unique ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/user_role_entity_set_attachment/{kind}/{id}":{"get":{"tags":["Authorization management"],"summary":"List User Role Entity Set Attachments","description":"List USER_ROLE_ENTITY_SET_ATTACHMENTs by sub_user, nuid_entity_set, or nuid_role.","operationId":"list_user_role_entity_set_attachments_auth_user_role_entity_set_attachment__kind___id__get","parameters":[{"name":"kind","in":"path","required":true,"schema":{"$ref":"#/components/schemas/UserRoleEntitySetAttachmentPathParameter"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserRoleEntitySetAttachmentResponse"},"title":"Response List User Role Entity Set Attachments Auth User Role Entity Set Attachment Kind Id Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/user_permissions/{sub}":{"get":{"tags":["Authorization management"],"summary":"Get User Permissions","description":"Returns an object representing the user's permissions.","operationId":"get_user_permissions_auth_user_permissions__sub__get","parameters":[{"name":"sub","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Sub"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserPermissionsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/entity_role_entity_set_attachment/{neris_id_entity}":{"get":{"tags":["Authorization management"],"summary":"Get Entity Role Entity Set Attachment","description":"Get an entity role entity set attachment by it entity NERIS ID.","operationId":"get_entity_role_entity_set_attachment_auth_entity_role_entity_set_attachment__neris_id_entity__get","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|FM|FA)\\d{8}$","examples":["FD24027334","FM24001001","FA24001001"],"title":"Neris Id Entity"}},{"name":"type_entity_set","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/TypeEntityValue"},{"type":"null"}],"description":"Filter by entity type.","title":"Type Entity Set"},"description":"Filter by entity type."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EntityRoleEntitySetAttachmentResponse"},"title":"Response Get Entity Role Entity Set Attachment Auth Entity Role Entity Set Attachment Neris Id Entity Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident/{neris_id_entity}/{neris_id_incident}":{"get":{"tags":["Incidents"],"summary":"Get Incident","description":"Retrieve an individual incident.","operationId":"get_incident_incident__neris_id_entity___neris_id_incident__get","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|FM|FA)\\d{8}$","examples":["FD24027334","FM24001001","FA24001001"],"title":"Neris Id Entity"}},{"name":"neris_id_incident","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"],"title":"Neris Id Incident"}},{"name":"geo_format","in":"query","required":false,"schema":{"enum":["url","geojson"],"type":"string","description":"Whether point/polygon fields are pygeoapi URLs or inline GeoJSON.","default":"url","title":"Geo Format"},"description":"Whether point/polygon fields are pygeoapi URLs or inline GeoJSON."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["Incidents"],"summary":"Patch Incident","description":"Patch an incident","operationId":"patch_incident_incident__neris_id_entity___neris_id_incident__patch","parameters":[{"name":"neris_id_incident","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"],"title":"Neris Id Incident"},"description":"The incident's NERIS ID"},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchIncidentAction"},"examples":{"valid":{"summary":"A valid patch incident payload","value":{"neris_id":"FD24027214|2025-10-13:04|1753833600","action":"patch","properties":{"actions_tactics":{"action":"set","value":{"action_noaction":{"type":"ACTION","actions":["HAZARDOUS_SITUATION_MITIGATION||REMOVE_HAZARD","PERSONNEL_CONTAMINATION_REDUCTION||CLEAN_CAB_TRANSPORT"]}}},"special_modifiers":{"action":"set","value":["URBAN_CONFLAGRATION","VIOLENCE_AGAINST_RESPONDER"]},"aids":[{"action":"append","value":{"department_neris_id":"FD24027000","aid_type":"SUPPORT_AID","aid_direction":"GIVEN"}}],"base":{"action":"patch","neris_uid":999,"properties":{"point":{"action":"set","value":{"crs":4326,"source":"API_SUBMISSION","geometry":{"coordinates":[-122.4758621,37.7632405]}}}}},"smoke_alarm":{"action":"patch","neris_uid":999,"properties":{"presence":{"action":"patch","neris_uid":999,"properties":{"alarm_types":{"action":"set","value":["HARDWIRED"]}}}}},"fire_alarm":{"action":"set","value":{"presence":{"type":"NOT_PRESENT"}}},"electric_hazards":[{"action":"remove","neris_uid":999}],"medical_oxygen_hazard":{"action":"set","value":{"presence":{"type":"NOT_PRESENT"}}},"dispatch":{"action":"patch","neris_uid":999,"properties":{"center_id":{"action":"set","value":"1235"},"incident_number":{"action":"set","value":"1725295159"},"determinant_code":{"action":"set","value":"A1234"},"incident_code":{"action":"unset"},"disposition":{"action":"set","value":"Structure fire suppressed on first alarm, 2 occupants rescued, occupants refused treatment."},"incident_clear":{"action":"unset"},"automatic_alarm":{"action":"unset"},"tactic_timestamps":{"action":"patch","neris_uid":999,"properties":{"command_established":{"action":"set","value":"2025-05-03T18:50:24+00:00"}}},"unit_responses":[{"action":"patch","neris_uid":999,"properties":{"unit_neris_id":{"action":"set","value":"FD24027214S000U002"},"reported_unit_id":{"action":"set","value":"E84"},"point":{"action":"set","value":{"crs":4326,"source":"API_SUBMISSION","geometry":{"type":"Point","coordinates":[-76.820114,39.186229]}}},"dispatch":{"action":"set","value":"2025-05-03T18:50:24+00:00"},"enroute_to_scene":{"action":"set","value":"2025-05-03T18:51:13+00:00"},"on_scene":{"action":"unset"},"unit_clear":{"action":"unset"},"med_responses":[{"action":"append","value":{"at_patient":"2025-05-03T18:52:44+00:00","enroute_to_hospital":"2025-05-03T18:58:19+00:00","arrived_at_hospital":"2025-05-03T19:07:24+00:00","hospital_destination":"University Regional Health Center"}}]}}]}},"powergen_hazards":[{"action":"append","value":{"type":"PHOTOVOLTAICS","source_or_target":"SOURCE","pv_type":"THIN_FILM_POWER_GENERATION"}},{"action":"patch","neris_uid":999,"properties":{"type":{"action":"set","value":"NOT_APPLICABLE"}}},{"action":"remove","neris_uid":1000}]}}}}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentUpdatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"put":{"tags":["Incidents"],"summary":"Put Incident","description":"Update an incident","operationId":"put_incident_incident__neris_id_entity___neris_id_incident__put","parameters":[{"name":"neris_id_incident","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"],"title":"Neris Id Incident"},"description":"The incident's NERIS ID"},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutIncidentPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentUpdatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident/{neris_id_dept}/{neris_id_incident}/history":{"get":{"tags":["Incidents"],"summary":"Get Incident History","description":"Chronogolical history of the incident","operationId":"get_incident_history_incident__neris_id_dept___neris_id_incident__history_get","parameters":[{"name":"neris_id_dept","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}$","examples":["FD24027334","FD24160543"],"title":"Neris Id Dept"}},{"name":"neris_id_incident","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"],"title":"Neris Id Incident"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentHistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident/{neris_id_entity}/{neris_id_incident}/report":{"get":{"tags":["Incidents"],"summary":"Get Incident Report","description":"Retrieve the printable report for an individual incident.","operationId":"get_incident_report_incident__neris_id_entity___neris_id_incident__report_get","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|FM|FA)\\d{8}$","examples":["FD24027334","FM24001001","FA24001001"],"title":"Neris Id Entity"}},{"name":"neris_id_incident","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"],"title":"Neris Id Incident"}}],"responses":{"200":{"description":"Returns the incident report.","content":{"application/pdf":{}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident/{neris_id_dept}/{neris_id_incident}":{"delete":{"tags":["Incidents"],"summary":"Delete Incident","description":"Delete an incident.","operationId":"delete_incident_incident__neris_id_dept___neris_id_incident__delete","parameters":[{"name":"neris_id_dept","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}$","examples":["FD24027334","FD24160543"],"title":"Neris Id Dept"}},{"name":"neris_id_incident","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"],"title":"Neris Id Incident"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident/{neris_id_entity}/validate":{"post":{"tags":["Incidents"],"summary":"Validate Incident","description":"Validate an incident payload.","operationId":"validate_incident_incident__neris_id_entity__validate_post","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentPayload"}}}},"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident/{neris_id_entity}":{"post":{"tags":["Incidents"],"summary":"Create Incident","description":"Create a new incident for an entity.","operationId":"create_incident_incident__neris_id_entity__post","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentPayload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentCreatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident":{"get":{"tags":["Incidents"],"summary":"List Incidents","description":"List incidents","operationId":"list_incidents_incident_get","parameters":[{"name":"neris_id_entity","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"]},{"type":"null"}],"title":"NERIS ID of the entity"}},{"name":"nuid_entity_set","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"An entity set's unique identifier","title":"Nuid Entity Set"},"description":"An entity set's unique identifier"},{"name":"incident_types","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/IncidentFilterType"}},{"type":"null"}],"description":"One or more incident type or subtype","title":"Incident Types"},"description":"One or more incident type or subtype"},{"name":"incident_types_set_operation","in":"query","required":false,"schema":{"$ref":"#/components/schemas/SetOperation","description":"Whether to return the union or intersection of incident types listed in the `incident_types` parameter","default":"UNION"},"description":"Whether to return the union or intersection of incident types listed in the `incident_types` parameter"},{"name":"call_create_start","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"description":"Earliest time an incident's dispatch call could be opened","title":"Call Create Start"},"description":"Earliest time an incident's dispatch call could be opened"},{"name":"call_create_end","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"description":"Latest time an incident's dispatch call could be opened","title":"Call Create End"},"description":"Latest time an incident's dispatch call could be opened"},{"name":"incident_number","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case insensitive substring to match against `incident_number`","title":"Incident Number"},"description":"Case insensitive substring to match against `incident_number`"},{"name":"dispatch_incident_number","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case insensitive substring to match against `dispatch.incident_number`","title":"Dispatch Incident Number"},"description":"Case insensitive substring to match against `dispatch.incident_number`"},{"name":"include_aid","in":"query","required":false,"schema":{"$ref":"#/components/schemas/AidDirection","description":"Include incidents linked to the department in `neris_id_entity` via aid"},"description":"Include incidents linked to the department in `neris_id_entity` via aid"},{"name":"aid","in":"query","required":false,"schema":{"$ref":"#/components/schemas/AidDirection","description":"Only incidents linked to the department in `neris_id_entity` via aid"},"description":"Only incidents linked to the department in `neris_id_entity` via aid"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"$ref":"#/components/schemas/TypeIncidentStatusValue"}},{"type":"null"}],"description":"Status of the incident","title":"Status"},"description":"Status of the incident"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs","minLength":2,"maxLength":2},{"type":"null"}],"description":"Filters incidents by the two character state or province code.","title":"State"},"description":"Filters incidents by the two character state or province code."},{"name":"last_modified","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(before|after)\\(((\\d{1,3}[yMdhms])|((19|20)\\d{2}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?((\\+|\\-)\\d{2}:\\d{2})?))\\)$","description":"A string expression for a time period relative to and prior to either the current time or an absolute time.","examples":["before(24h) # 24 hours ago and before","after(2M) # Two months ago until current time","before(1y) # One year ago and before","before(2024-03-20T11:24:15.256+00:00) # Before 2024-03-20T11:24:15.256+00:00","after(2024-03-20T11:24:15.256+00:00) # After 2024-03-20T11:24:15.256+00:00"]},{"type":"null"}],"description":"A string expression for a time period relative to either the current time or an absolute time.","title":"Last Modified"},"description":"A string expression for a time period relative to either the current time or an absolute time.","examples":{"Before 24 hours ago":{"summary":"Before 24 hours ago","description":"Filter for incidents modified 24 hours ago or prior","value":"before(24h)"},"After two months ago":{"summary":"After two months ago","description":"Filter for incidents modified two months ago or later","value":"after(2M)"},"Before one year ago":{"summary":"Before one year ago","description":"Filter for incidents modified one year ago or prior","value":"before(1y)"},"Before datetime":{"summary":"Before a specific datetime","description":"Filter for incidents modified prior to the supplied datetime","value":"before(2024-03-20T11:24:13.275-05:00)"},"After datetime":{"summary":"After a specific datetime","description":"Filter for incidents modified after the supplied datetime","value":"after(2024-03-20T11:24:13.275-05:00)"}}},{"name":"sort_by","in":"query","required":false,"schema":{"$ref":"#/components/schemas/IncidentSortBy","description":"Which field to sort incidents by","default":"call_create"},"description":"Which field to sort incidents by"},{"name":"sort_direction","in":"query","required":false,"schema":{"$ref":"#/components/schemas/SortDirection","description":"The direction of sorting","default":"DESCENDING"},"description":"The direction of sorting"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Page Size"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The pagination cursor","title":"Cursor"},"description":"The pagination cursor"},{"name":"geo_format","in":"query","required":false,"schema":{"enum":["url","geojson"],"type":"string","description":"Whether point/polygon fields are pygeoapi URLs or inline GeoJSON.","default":"url","title":"Geo Format"},"description":"Whether point/polygon fields are pygeoapi URLs or inline GeoJSON."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListIncidentsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident/{neris_id_entity}/{neris_id_incident}/incident_status":{"put":{"tags":["Incidents"],"summary":"Put Incident Status","description":"Update the status of an incident","operationId":"put_incident_status_incident__neris_id_entity___neris_id_incident__incident_status_put","parameters":[{"name":"neris_id_incident","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"],"title":"Neris Id Incident"},"description":"The incident's NERIS ID"},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateIncidentStatusPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident_analysis/{neris_id_entity}":{"get":{"tags":["Incident Analysis"],"summary":"Get Incident Analysis","operationId":"get_incident_analysis_incident_analysis__neris_id_entity__get","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|FM|FA)\\d{8}$","examples":["FD24027334","FM24001001","FA24001001"],"title":"Neris Id Entity"}},{"name":"neris_id_ia","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^IA\\|FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["IA|FD12345678|abc123xyz|1729023498","IA|FD24027334|incident42:f-tiw|1729023498","IA|FD12345678|0457812893477|1729023498"]},{"type":"null"}],"title":"Neris Id Ia"}},{"name":"neris_id_incident","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"]},{"type":"null"}],"title":"Neris Id Incident"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentAnalysisResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident_analysis/{neris_id_entity}/report":{"get":{"tags":["Incident Analysis"],"summary":"Get Incident Analysis Report","description":"Retrieve the printable report for an individual incident analysis.","operationId":"get_incident_analysis_report_incident_analysis__neris_id_entity__report_get","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|FM|FA)\\d{8}$","examples":["FD24027334","FM24001001","FA24001001"],"title":"Neris Id Entity"}},{"name":"neris_id_ia","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^IA\\|FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["IA|FD12345678|abc123xyz|1729023498","IA|FD24027334|incident42:f-tiw|1729023498","IA|FD12345678|0457812893477|1729023498"]},{"type":"null"}],"title":"Neris Id Ia"}},{"name":"neris_id_incident","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"]},{"type":"null"}],"title":"Neris Id Incident"}}],"responses":{"200":{"description":"Returns the incident analysis report.","content":{"application/pdf":{}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident_analysis/{neris_id_incident}":{"post":{"tags":["Incident Analysis"],"summary":"Create Incident Analysis","description":"Create a new incident for an entity.","operationId":"create_incident_analysis_incident_analysis__neris_id_incident__post","parameters":[{"name":"neris_id_incident","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"],"title":"Neris Id Incident"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentAnalysisPayload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentAnalysisCreatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident_analysis/{neris_id_entity}/{neris_id_ia}":{"put":{"tags":["Incident Analysis"],"summary":"Put Incident Analysis","description":"Update an incident analysis","operationId":"put_incident_analysis_incident_analysis__neris_id_entity___neris_id_ia__put","parameters":[{"name":"neris_id_ia","in":"path","required":true,"schema":{"type":"string","pattern":"^IA\\|FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["IA|FD12345678|abc123xyz|1729023498","IA|FD24027334|incident42:f-tiw|1729023498","IA|FD12345678|0457812893477|1729023498"],"title":"Neris Id Ia"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutIncidentAnalysisPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentAnalysisUpdatedResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Incident Analysis"],"summary":"Delete Incident Analysis","description":"Delete an incident.","operationId":"delete_incident_analysis_incident_analysis__neris_id_entity___neris_id_ia__delete","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^FD\\d{8}$","examples":["FD24027334","FD24160543"],"title":"Neris Id Entity"}},{"name":"neris_id_ia","in":"path","required":true,"schema":{"type":"string","pattern":"^IA\\|FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["IA|FD12345678|abc123xyz|1729023498","IA|FD24027334|incident42:f-tiw|1729023498","IA|FD12345678|0457812893477|1729023498"],"title":"Neris Id Ia"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Response"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident_analysis/{neris_id_entity}/{neris_id_ia}/incident_analysis_status":{"put":{"tags":["Incident Analysis"],"summary":"Put Incident Analysis Status","description":"Update the status of an incident analysis","operationId":"put_incident_analysis_status_incident_analysis__neris_id_entity___neris_id_ia__incident_analysis_status_put","parameters":[{"name":"neris_id_ia","in":"path","required":true,"schema":{"type":"string","pattern":"^IA\\|FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["IA|FD12345678|abc123xyz|1729023498","IA|FD24027334|incident42:f-tiw|1729023498","IA|FD12345678|0457812893477|1729023498"],"title":"Neris Id Ia"}},{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentAnalysisStatusPayload"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentAnalysisStatusResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/incident_analysis":{"get":{"tags":["Incident Analysis"],"summary":"List Incident Analyses","description":"List incident analyses","operationId":"list_incident_analyses_incident_analysis_get","parameters":[{"name":"neris_id_entity","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"]},{"type":"null"}],"title":"NERIS ID of the entity"}},{"name":"last_modified","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^(before|after)\\(((\\d{1,3}[yMdhms])|((19|20)\\d{2}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?((\\+|\\-)\\d{2}:\\d{2})?))\\)$","description":"A string expression for a time period relative to and prior to either the current time or an absolute time.","examples":["before(24h) # 24 hours ago and before","after(2M) # Two months ago until current time","before(1y) # One year ago and before","before(2024-03-20T11:24:15.256+00:00) # Before 2024-03-20T11:24:15.256+00:00","after(2024-03-20T11:24:15.256+00:00) # After 2024-03-20T11:24:15.256+00:00"]},{"type":"null"}],"description":"A string expression for a time period relative to either the current time or an absolute time.","title":"Last Modified"},"description":"A string expression for a time period relative to either the current time or an absolute time.","examples":{"Before 24 hours ago":{"summary":"Before 24 hours ago","description":"Filter for incidents modified 24 hours ago or prior","value":"before(24h)"},"After two months ago":{"summary":"After two months ago","description":"Filter for incidents modified two months ago or later","value":"after(2M)"},"Before one year ago":{"summary":"Before one year ago","description":"Filter for incidents modified one year ago or prior","value":"before(1y)"},"Before datetime":{"summary":"Before a specific datetime","description":"Filter for incidents modified prior to the supplied datetime","value":"before(2024-03-20T11:24:13.275-05:00)"},"After datetime":{"summary":"After a specific datetime","description":"Filter for incidents modified after the supplied datetime","value":"after(2024-03-20T11:24:13.275-05:00)"}}},{"name":"sort_by","in":"query","required":false,"schema":{"$ref":"#/components/schemas/IncidentAnalysisSortBy","description":"Which field to sort incident analyses by","default":"neris_id_entity"},"description":"Which field to sort incident analyses by"},{"name":"sort_direction","in":"query","required":false,"schema":{"$ref":"#/components/schemas/SortDirection","description":"The direction of sorting","default":"DESCENDING"},"description":"The direction of sorting"},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Page Size"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The pagination cursor","title":"Cursor"},"description":"The pagination cursor"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListIncidentAnalysesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/integration/{entity_id}":{"post":{"tags":["Account management"],"summary":"Create an API integration.","description":"Create an API integration for an entity.","operationId":"create_integration_account_integration__entity_id__post","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIntegrationAccountPayload"},"examples":{"example":{"title":"ACME Records Management","type":"RMS"}}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIntegrationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/integration/{client_id}":{"delete":{"tags":["Account management"],"summary":"Delete an API integration.","description":"Delete an API integration for an entity.","operationId":"delete_integration_account_integration__client_id__delete","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Client Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/credential/{client_id}":{"post":{"tags":["Account management"],"summary":"Create a new client secret.","description":"Create a new credential (client secret) for an integration account.","operationId":"create_integration_credential_account_credential__client_id__post","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Client Id"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateIntegrationCredentialResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/credential/{client_id}/{credential_id}":{"delete":{"tags":["Account management"],"summary":"Delete Integration Credential","description":"Delete an integration credential","operationId":"delete_integration_credential_account_credential__client_id___credential_id__delete","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Client Id"}},{"name":"credential_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","descriptino":"The credential id","title":"Credential Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/integration/{entity_id}/list":{"get":{"tags":["Account management"],"summary":"List API integrations.","description":"List API integrations for an entity.","operationId":"list_integrations_by_entity_account_integration__entity_id__list_get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListIntegrationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/enrollment/{entity_id}/{client_id}":{"post":{"tags":["Account management"],"summary":"Enroll Integration","description":"Grants permission to a api integration to the entity's data.","operationId":"enroll_integration_account_enrollment__entity_id___client_id__post","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"The client_id of the api integration to enroll","title":"Client Id"},"description":"The client_id of the api integration to enroll"},{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationEnrollmentResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["Account management"],"summary":"Delete Integration Enrollment","description":"Removes the permission of an API integration to access the entity's data.","operationId":"delete_integration_enrollment_account_enrollment__entity_id___client_id__delete","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"The client_id of the API integration to remove","title":"Client Id"},"description":"The client_id of the API integration to remove"},{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/enrollment/{client_id}":{"get":{"tags":["Account management"],"summary":"List integration enrollments.","description":"Lists integration enrollments by integration.","operationId":"list_enrollments_by_integration_account_enrollment__client_id__get","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Client Id"}},{"name":"title","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":36},{"type":"null"}],"description":"Substring of the integration title","type":"string","minLength":1,"maxLength":36,"title":"Title"},"description":"Substring of the integration title"},{"name":"entity_name","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":36},{"type":"null"}],"description":"Substring of the entity name","type":"string","minLength":1,"maxLength":36,"title":"Entity Name"},"description":"Substring of the entity name"},{"name":"page_number","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"type":"integer","minimum":1,"default":1,"title":"Page Number"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListIntegrationEnrollmentsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/account/enrollment/{entity_id}":{"get":{"tags":["Account management"],"summary":"List integration enrollments.","description":"Lists integration enrollments by entity.","operationId":"list_enrollments_by_entity_account_enrollment__entity_id__get","parameters":[{"name":"entity_id","in":"path","required":true,"schema":{"type":"string","title":"Entity Id"}},{"name":"title","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":1,"maxLength":36},{"type":"null"}],"description":"Substring of the integration title","type":"string","minLength":1,"maxLength":36,"title":"Title"},"description":"Substring of the integration title"},{"name":"page_number","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"type":"integer","minimum":1,"default":1,"title":"Page Number"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":10,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListIntegrationEnrollmentsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/no_activity_report/{neris_uid}":{"get":{"tags":["No activity report"],"summary":"Get No Activity Report","description":"Retrieve a no activity report by unique ID","operationId":"get_no_activity_report_no_activity_report__neris_uid__get","parameters":[{"name":"neris_uid","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"The unique ID","title":"Neris Uid"},"description":"The unique ID"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoActivityReportResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["No activity report"],"summary":"Delete No Activity Report","description":"Delete a no activity report by unique ID","operationId":"delete_no_activity_report_no_activity_report__neris_uid__delete","parameters":[{"name":"neris_uid","in":"path","required":true,"schema":{"type":"string","format":"uuid","description":"The unique ID","title":"Neris Uid"},"description":"The unique ID"}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/no_activity_report":{"get":{"tags":["No activity report"],"summary":"List No Activity Reports","description":"List no activity reports","operationId":"list_no_activity_reports_no_activity_report_get","parameters":[{"name":"neris_id_entity","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}$","examples":["FD24027334","FD24160543"]},{"type":"null"}],"description":"NERIS ID of the entity","title":"Neris Id Entity"},"description":"NERIS ID of the entity"},{"name":"start_month_year","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^[0-1][0-9]/20[2-9][0-9]$","examples":["12/2025","06/2026","01/2025"]},{"type":"null"}],"description":"Beginning month/year for filtering, inclusive","title":"Start Month Year"},"description":"Beginning month/year for filtering, inclusive"},{"name":"end_month_year","in":"query","required":false,"schema":{"anyOf":[{"type":"string","pattern":"^[0-1][0-9]/20[2-9][0-9]$","examples":["12/2025","06/2026","01/2025"]},{"type":"null"}],"description":"Ending month/year for filtering, inclusive","title":"End Month Year"},"description":"Ending month/year for filtering, inclusive"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListNoActivityReportResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/no_activity_report/{neris_id_entity}":{"post":{"tags":["No activity report"],"summary":"Create No Activity Report","description":"Create a no activity report","operationId":"create_no_activity_report_no_activity_report__neris_id_entity__post","parameters":[{"name":"neris_id_entity","in":"path","required":true,"schema":{"type":"string","pattern":"^(FD|VN|FM|FA)\\d{8}$","examples":["VN24027334","FD24160543"],"title":"NERIS ID of the entity"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoActivityReportPayload"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoActivityReportResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"ActionPayload":{"properties":{"type":{"type":"string","const":"ACTION","title":"Type","description":"`ACTION` indicates action was taken","default":"ACTION"},"actions":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeActionTacticValue"},"type":"array"},{"type":"null"}],"title":"Actions","description":"Actions taken by the fire department on the scene of the incident.","x-ui-label":"Actions and Tactics Concat"}},"additionalProperties":false,"type":"object","title":"ActionPayload"},"ActionResponse":{"properties":{"type":{"type":"string","const":"ACTION","title":"Type","description":"`ACTION` indicates action was taken","default":"ACTION"},"actions":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Actions","description":"Actions taken by the fire department on the scene of the incident."}},"type":"object","title":"ActionResponse"},"ActionTacticPayload":{"properties":{"action_noaction":{"oneOf":[{"$ref":"#/components/schemas/ActionPayload"},{"$ref":"#/components/schemas/NoactionPayload"}],"title":"Action Noaction","discriminator":{"propertyName":"type","mapping":{"ACTION":"#/components/schemas/ActionPayload","NOACTION":"#/components/schemas/NoactionPayload"}}}},"additionalProperties":false,"type":"object","required":["action_noaction"],"title":"ActionTacticPayload"},"ActionTacticResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"action_noaction":{"oneOf":[{"$ref":"#/components/schemas/ActionResponse"},{"$ref":"#/components/schemas/NoactionResponse"}],"title":"Action Noaction","discriminator":{"propertyName":"type","mapping":{"ACTION":"#/components/schemas/ActionResponse","NOACTION":"#/components/schemas/NoactionResponse"}}}},"additionalProperties":false,"type":"object","required":["last_modified","action_noaction"],"title":"ActionTacticResponse"},"AidAgreementPayload":{"properties":{"neris_id_dept":{"type":"string","pattern":"^FD\\d{8}$","title":"Neris Id Dept","examples":["FD24027334","FD24160543"]},"type":{"$ref":"#/components/schemas/TypeAidAgreementValue"}},"additionalProperties":false,"type":"object","required":["neris_id_dept","type"],"title":"AidAgreementPayload"},"AidAgreementResponse":{"properties":{"neris_id_dept":{"type":"string","pattern":"^FD\\d{8}$","title":"Neris Id Dept","examples":["FD24027334","FD24160543"]},"type":{"type":"string","title":"Type"},"confirmed":{"type":"boolean","title":"Confirmed"}},"type":"object","required":["neris_id_dept","type","confirmed"],"title":"AidAgreementResponse"},"AidDirection":{"type":"string","enum":["ALL","GIVEN","RECEIVED"],"title":"AidDirection"},"AidPayload":{"properties":{"department_neris_id":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}$","examples":["FD24027334","FD24160543"]},{"type":"string","pattern":"^FM\\d{8}$","examples":["FM24000000","FM24001001"]}],"title":"Department Neris Id","description":"NERIS ID of the fire department or marshal to which aid was given/received.","examples":["FD24027000","FM24000000"],"x-ui-label":"NERIS ID of fire department for which aid was provided.","x-ui-hint":"Search for NERIS ID"},"aid_type":{"$ref":"#/components/schemas/TypeAidValue","description":"The type of aid provided","x-ui-label":"Describe the nature of the aid provided.","x-ui-hint":"Select one."},"aid_direction":{"$ref":"#/components/schemas/TypeAidDirectionValue","description":"The direction aid was provided","x-ui-label":"Was aid provided by or given to another agency on the incident?","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["department_neris_id","aid_type","aid_direction"],"title":"AidPayload"},"AidResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"department_neris_id":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}$","examples":["FD24027334","FD24160543"]},{"type":"string","pattern":"^FM\\d{8}$","examples":["FM24000000","FM24001001"]}],"title":"Department Neris Id","description":"NERIS ID of the fire department or marshal to which aid was given/received.","examples":["FD24027000","FM24000000"]},"aid_type":{"type":"string","title":"Aid Type","description":"The type of aid provided"},"aid_direction":{"type":"string","title":"Aid Direction","description":"The direction aid was provided"}},"type":"object","required":["last_modified","department_neris_id","aid_type","aid_direction"],"title":"AidResponse"},"AppendAidAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/AidPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendAidAction"},"AppendCasualtyRescueAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/CasualtyRescuePayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendCasualtyRescueAction"},"AppendChemicalAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/ChemicalPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendChemicalAction"},"AppendCommentAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/CommentPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendCommentAction"},"AppendCrossStreetAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/CrossStreetPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendCrossStreetAction"},"AppendDispatchUnitResponseAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/DispatchUnitResponsePayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendDispatchUnitResponseAction"},"AppendElectricHazardAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/ElectricHazardPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendElectricHazardAction"},"AppendExposureAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/ExposurePayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendExposureAction"},"AppendIncidentTypeAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/IncidentTypePayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendIncidentTypeAction"},"AppendIncidentUnitResponseAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/IncidentUnitResponsePayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendIncidentUnitResponseAction"},"AppendLocationAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/LocationPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendLocationAction"},"AppendMedResponseAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/MedResponsePayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendMedResponseAction"},"AppendMedicalAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/MedicalPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendMedicalAction"},"AppendOtherPowergenHazardAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/OtherPowergenHazardPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendOtherPowergenHazardAction"},"AppendPvPowergenHazardAction":{"properties":{"action":{"type":"string","const":"append","title":"Action","description":"Append an element to the array"},"value":{"$ref":"#/components/schemas/PvPowergenHazardPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"AppendPvPowergenHazardAction"},"AssessmentInfo":{"properties":{"primary_year_built":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Primary Year Built","description":"The year the primary structure on the property was built"},"primary_zoning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Primary Zoning","description":"Actual city zoning, unique to each incorporated area as reported to the County. Please note that it is not always current, nor reported","examples":["RP","NT"]},"primary_use":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Primary Use","description":"Description of the Standardized Land Use Code","examples":["OFFICE_BUILDING"]},"primary_owner_occupied":{"anyOf":[{"$ref":"#/components/schemas/YesOrNo"},{"type":"null"}],"description":"Indicates if the property is occupied by the owner"},"primary_assessment_land":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Primary Assessment Land","description":"The assessed land value in dollars"},"computed_loss":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Computed Loss","description":"The estimated, computed value lost due to structure fire based on damage type. Value is in dollars."}},"type":"object","title":"AssessmentInfo","description":"Assessment information for a parcel."},"AssessmentPayload":{"properties":{"iso_rating":{"anyOf":[{"type":"integer","exclusiveMaximum":11.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Iso Rating"},"cpse_accredited":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Cpse Accredited"},"caas_accredited":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Caas Accredited"}},"additionalProperties":false,"type":"object","title":"AssessmentPayload"},"AssessmentResponse":{"properties":{"iso_rating":{"anyOf":[{"type":"integer","exclusiveMaximum":11.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Iso Rating"},"cpse_accredited":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Cpse Accredited"},"caas_accredited":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Caas Accredited"}},"type":"object","title":"AssessmentResponse"},"AuthChallengeResponse":{"properties":{"challenge_name":{"type":"string","enum":["email_otp","software_token_mfa","new_password_required"],"title":"Challenge Name"},"session":{"type":"string","title":"Session"}},"type":"object","required":["challenge_name","session"],"title":"AuthChallengeResponse"},"AuthError":{"type":"string","enum":["invalid_request","invalid_grant","invalid_client","unauthorized_client","unsupported_grant_type","invalid_scope"],"title":"AuthError"},"AuthenticationSuccessResponse":{"properties":{"access_token":{"type":"string","title":"Access Token","description":"The access token issued by the authorization server."},"refresh_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Refresh Token","description":"The refresh token. The server automatically rotates refresh tokens. Clients should always use the latest refresh token."},"expires_in":{"type":"integer","title":"Expires In","description":"The lifetime in seconds of the access token."},"token_type":{"type":"string","const":"bearer","title":"Token Type","description":"The type of the token. The only value supported by the Neris authorization server is \"bearer\".","default":"bearer"}},"type":"object","required":["access_token","expires_in"],"title":"AuthenticationSuccessResponse"},"BatteryPayload":{"properties":{"original_battery":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Original Battery","description":"Whether the battery was the product's original battery.","x-ui-label":"Was this the product's original battery?"},"safety_listed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Safety Listed","description":"Whether the product was listed by an independent safety testing company.","x-ui-label":"Was the product listed by an independent safety testing company?"},"voltage":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Voltage","description":"The voltage of the battery.","x-ui-label":"Describe the voltage of the battery."},"watt_hour":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Watt Hour","description":"The watt hour of the battery.","x-ui-label":"Describe the watt-hour size of the battery."},"amp_hour":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Amp Hour","description":"The amp hour of the battery.","x-ui-label":"Describe the amp-hour size of the battery."},"charging":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Charging","description":"Whether the battery was plugged in/charging at the time of the incident.","x-ui-label":"Was the battery plugged in / charging at the time of the incident?"},"installed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Installed","description":"Whether the battery was permanently installed.","x-ui-label":"Was the battery permanently installed?","x-ui-hint":"e.g., a home battery attached to a wall."},"direct_sunlight":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Direct Sunlight","description":"Whether the battery was exposed to direct sunlight at the time of the incident.","x-ui-label":"Was the battery exposed to direct sunlight at the time of the incident?"},"impacted_by_source":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Impacted By Source","description":"Whether the battery was impacted by the ignition of another source.","x-ui-label":"Was the battery impacted due to the ignition of another source?"},"thermal_runaway":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Thermal Runaway","description":"Whether the battery displayed signs of thermal runaway.","x-ui-label":"Did the battery display signs of thermal runaway?"},"product_type":{"$ref":"#/components/schemas/TypeEmerghazElecValue","description":"The battery-powered product involved in the incident.","x-ui-label":"Select the battery-powered product involved in the incident."},"cell":{"anyOf":[{"$ref":"#/components/schemas/TypeBatteryCellValue"},{"type":"null"}],"description":"The type of battery cell.","x-ui-label":"Describe the type of battery cell."},"chemistry":{"anyOf":[{"$ref":"#/components/schemas/TypeBatteryChemistryValue"},{"type":"null"}],"description":"The chemistry of the battery.","x-ui-label":"Describe the chemistry of the battery."},"storage":{"anyOf":[{"$ref":"#/components/schemas/TypeIndoorOutdoorValue"},{"type":"null"}],"description":"Whether the battery was primarily stored indoors or outdoors.","x-ui-label":"Was the battery primarily stored indoors or outdoors?"},"indoor_or_outdoor":{"anyOf":[{"$ref":"#/components/schemas/TypeIndoorOutdoorValue"},{"type":"null"}],"description":"Whether the battery was located indoors or outdoors at the time of the incident.","x-ui-label":"Was the battery located indoors or outdoors at the time of the incident?"},"suppression_efforts":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeEmerghazSuppressionValue"},"type":"array"},{"type":"null"}],"title":"Suppression Efforts","description":"The suppression/mitigation efforts used to suppress the fire.","x-ui-label":"Select the type of suppression / mitigation efforts employed for suppression.","x-ui-hint":"Select all that apply."}},"additionalProperties":false,"type":"object","required":["product_type"],"title":"BatteryPayload"},"BatteryResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"original_battery":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Original Battery","description":"Whether the battery was the product's original battery."},"safety_listed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Safety Listed","description":"Whether the product was listed by an independent safety testing company."},"voltage":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Voltage","description":"The voltage of the battery."},"watt_hour":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Watt Hour","description":"The watt hour of the battery."},"amp_hour":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Amp Hour","description":"The amp hour of the battery."},"charging":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Charging","description":"Whether the battery was plugged in/charging at the time of the incident."},"installed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Installed","description":"Whether the battery was permanently installed."},"direct_sunlight":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Direct Sunlight","description":"Whether the battery was exposed to direct sunlight at the time of the incident."},"impacted_by_source":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Impacted By Source","description":"Whether the battery was impacted by the ignition of another source."},"thermal_runaway":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Thermal Runaway","description":"Whether the battery displayed signs of thermal runaway."},"product_type":{"type":"string","title":"Product Type","description":"The battery-powered product involved in the incident."},"cell":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cell","description":"The type of battery cell."},"chemistry":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Chemistry","description":"The chemistry of the battery."},"storage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Storage","description":"Whether the battery was primarily stored indoors or outdoors."},"indoor_or_outdoor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Indoor Or Outdoor","description":"Whether the battery was located indoors or outdoors at the time of the incident."},"suppression_efforts":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Suppression Efforts","description":"The suppression/mitigation efforts used to suppress the fire."}},"type":"object","required":["last_modified","product_type"],"title":"BatteryResponse"},"Body_upsert_logo_entity__neris_id_entity__logo_put":{"properties":{"upload_logo":{"type":"string","format":"binary","title":"Upload Logo","description":"Logo image that is under 10MB"}},"type":"object","required":["upload_logo"],"title":"Body_upsert_logo_entity__neris_id_entity__logo_put"},"BuildingInfo":{"properties":{"primary_building_area":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Primary Building Area","description":"The primary building footprint area in square feet"},"primary_building_height_avg":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Primary Building Height Avg","description":"The primary building average height in feet"},"secondary_building_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Secondary Building Count","description":"The number of secondary buildings"}},"type":"object","title":"BuildingInfo","description":"Building information for a parcel."},"CasualtyPayload":{"properties":{"injury_or_noninjury":{"oneOf":[{"$ref":"#/components/schemas/InjuryPayload"},{"$ref":"#/components/schemas/NoinjuryPayload"}],"title":"Injury Or Noninjury","discriminator":{"propertyName":"type","mapping":{"INJURED_FATAL":"#/components/schemas/InjuryPayload","INJURED_NONFATAL":"#/components/schemas/InjuryPayload","UNINJURED":"#/components/schemas/NoinjuryPayload"}}}},"additionalProperties":false,"type":"object","required":["injury_or_noninjury"],"title":"CasualtyPayload"},"CasualtyRescuePayload":{"properties":{"type":{"type":"string","enum":["FF","NONFF"],"title":"Type","description":"Whether the person _rescued/sustaining a casualty_ is a firefighter or nonfirefighter."},"rank":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Rank","description":"Rank of firefighter.","examples":["Captain"],"x-ui-label":"Rank of firefighter."},"years_of_service":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Years Of Service","description":"Years of service of firefighter.","examples":["2"],"x-ui-label":"Years of service."},"casualty":{"anyOf":[{"$ref":"#/components/schemas/CasualtyPayload"},{"type":"null"}]},"rescue":{"anyOf":[{"$ref":"#/components/schemas/RescuePayload"},{"type":"null"}]},"birth_month_year":{"anyOf":[{"type":"string","maxLength":7,"minLength":7},{"type":"null"}],"title":"Birth Month Year","description":"Month and year of birth of the person(s) injured or rescued.\n\n
Validation Rules\n\n- `birth_month_year` must be in `MM/YYYY` format.\n\n
","examples":["06/1972"],"x-ui-label":"The birth month and year of the person injured or rescued."},"gender":{"anyOf":[{"$ref":"#/components/schemas/TypeGenderValue"},{"type":"null"}],"description":"Gender of the person(s) injured or rescued.","x-ui-label":"The gender of the person injured or rescued.","x-ui-hint":"Select one."},"race":{"anyOf":[{"$ref":"#/components/schemas/TypeRaceValue"},{"type":"null"}],"description":"Race of the person(s) injured or rescued.","x-ui-label":"The race of the person injured or rescued.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"CasualtyRescuePayload","description":"\n\n
Validation Rules\n\n- `rescue.presence_known` only possible if `type` is `NONFF`.\n- `mayday` only possible if `type` is `FF`.\n- `casualty.injury_or_noninjury.ff_injury_details` only possible if `type` is `FF`.\n- `rank` and `years_of_service` only possible if `type` is `FF`.\n\n
"},"CasualtyRescueResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","enum":["FF","NONFF"],"title":"Type","description":"Whether the person _rescued/sustaining a casualty_ is a firefighter or nonfirefighter."},"rank":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Rank","description":"Rank of firefighter.","examples":["Captain"]},"years_of_service":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Years Of Service","description":"Years of service of firefighter.","examples":["2"]},"casualty":{"anyOf":[{"$ref":"#/components/schemas/CasualtyResponse"},{"type":"null"}]},"rescue":{"anyOf":[{"$ref":"#/components/schemas/RescueResponse"},{"type":"null"}]},"birth_month_year":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Birth Month Year","description":"Age range","examples":["75-79","10-14"]},"gender":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gender","description":"Gender of the person(s) injured or rescued."},"race":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Race","description":"Race of the person(s) injured or rescued."}},"type":"object","required":["last_modified","type"],"title":"CasualtyRescueResponse"},"CasualtyResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"injury_or_noninjury":{"anyOf":[{"$ref":"#/components/schemas/InjuryResponse"},{"$ref":"#/components/schemas/NoinjuryResponse"}],"title":"Injury Or Noninjury"}},"type":"object","required":["last_modified","injury_or_noninjury"],"title":"CasualtyResponse"},"CensusTractResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"incident_neris_id":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Incident Neris Id","description":"NERIS ID of the incident. This is comprised of the department NERIS ID concatenated with `dispatch.internal_id` and the epoch time of `dispatch.call_create` in the record as initially submitted. An incident's NERIS_ID is immutable, and so should not be relied upon for extraction of `internal_id` or `call_create`, as these attributes could have been updated.","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"]},"fips_code":{"type":"string","title":"Fips Code","description":"FIPS code"},"tract_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tract Id","description":"Census tract ID"},"block_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Block Id","description":"Census block ID"},"area":{"type":"number","title":"Area","description":"Area in square miles"},"population_density":{"type":"number","title":"Population Density","description":"Population per square mile"}},"type":"object","required":["last_modified","incident_neris_id","fips_code","area","population_density"],"title":"CensusTractResponse"},"ChemicalPayload":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Name of the chemical/material involved in the incident.","x-ui-label":"Enter the name of the material released."},"release_occurred":{"type":"boolean","title":"Release Occurred","description":"Whether the chemical was released.","x-ui-label":"Whether the chemical was released"},"release":{"anyOf":[{"$ref":"#/components/schemas/ReleasePayload"},{"type":"null"}],"description":"Details on a chemical release."},"dot_class":{"$ref":"#/components/schemas/TypeHazardDotValue","description":"Department of Transportation Hazard Classification.","x-ui-label":"Select the DOT class of the material.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["name","release_occurred","dot_class"],"title":"ChemicalPayload","description":"\n\n
Validation Rules\n\n- `release` only possible if `release_occurred` is `true`.\n\n
"},"ChemicalResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Name of the chemical/material involved in the incident."},"release_occurred":{"type":"boolean","title":"Release Occurred","description":"Whether the chemical was released."},"release":{"anyOf":[{"$ref":"#/components/schemas/ReleaseResponse"},{"type":"null"}]},"dot_class":{"type":"string","title":"Dot Class","description":"Department of Transportation Hazard Classification."}},"type":"object","required":["last_modified","name","release_occurred","dot_class"],"title":"ChemicalResponse"},"ChemicalWithReleaseFactorsPayload":{"properties":{"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Name of the chemical/material involved in the incident."},"release_occurred":{"type":"boolean","title":"Release Occurred","description":"Whether the chemical was released."},"release":{"anyOf":[{"$ref":"#/components/schemas/ReleaseWithReleaseFactorsPayload"},{"type":"null"}],"description":"Details on a chemical release."},"dot_class":{"$ref":"#/components/schemas/TypeHazardDotValue","description":"Department of Transportation Hazard Classification."},"hazards":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeHazardValue"},"type":"array"},{"type":"null"}],"enum":["ENVIRONMENTAL_DAMAGE","EXPLOSION","FIRE","LEAK","MATERIAL_ENTERED_WATERWAY","SOLID_DISPERSION","SPILL","VAPOR_GAS_DISPERSION"],"title":"Hazards","description":"Types of hazard(s) involved in the incident."}},"additionalProperties":false,"type":"object","required":["name","release_occurred","dot_class"],"title":"ChemicalWithReleaseFactorsPayload","description":"\n\n
Validation Rules\n\n- `release` only possible if `release_occurred` is `true`.\n- `release_factors` only possible if `release_occurred` is `true`.\n\n
"},"ChemicalWithReleaseFactorsResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"name":{"type":"string","maxLength":255,"minLength":1,"title":"Name","description":"Name of the chemical/material involved in the incident."},"release_occurred":{"type":"boolean","title":"Release Occurred","description":"Whether the chemical was released."},"release":{"anyOf":[{"$ref":"#/components/schemas/ReleaseWithReleaseFactorsResponse"},{"type":"null"}]},"dot_class":{"type":"string","title":"Dot Class","description":"Department of Transportation Hazard Classification."},"type":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Type","description":"Types of hazard(s) involved in the incident."}},"type":"object","required":["last_modified","name","release_occurred","dot_class"],"title":"ChemicalWithReleaseFactorsResponse"},"CommentPayload":{"properties":{"timestamp":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Timestamp","description":"Timestamp of the comment."},"comment":{"type":"string","maxLength":100000,"minLength":1,"title":"Comment","description":"Free text or canned remark comments from dispatcher throughout duration of call.","examples":["Dispatch unit 35E to 123 Main Street for possible structure fire"]}},"additionalProperties":false,"type":"object","required":["comment"],"title":"CommentPayload"},"CommentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"timestamp":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Timestamp","description":"Timestamp of the comment."},"comment":{"type":"string","maxLength":101000,"minLength":1,"title":"Comment","description":"Free text or canned remark comments from dispatcher throughout duration of call.","examples":["Dispatch unit 35E to 123 Main Street for possible structure fire"]}},"type":"object","required":["last_modified","comment"],"title":"CommentResponse"},"CookingFireSuppressionNotPresentPayload":{"properties":{"type":{"type":"string","enum":["NOT_PRESENT","NOT_APPLICABLE"],"title":"Type"}},"additionalProperties":false,"type":"object","required":["type"],"title":"CookingFireSuppressionNotPresentPayload"},"CookingFireSuppressionNotPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"}},"type":"object","required":["last_modified","type"],"title":"CookingFireSuppressionNotPresentResponse"},"CookingFireSuppressionPayload":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/CookingFireSuppressionPresentPayload"},{"$ref":"#/components/schemas/CookingFireSuppressionNotPresentPayload"}],"title":"Presence","discriminator":{"propertyName":"type","mapping":{"NOT_APPLICABLE":"#/components/schemas/CookingFireSuppressionNotPresentPayload","NOT_PRESENT":"#/components/schemas/CookingFireSuppressionNotPresentPayload","PRESENT":"#/components/schemas/CookingFireSuppressionPresentPayload"}}}},"additionalProperties":false,"type":"object","required":["presence"],"title":"CookingFireSuppressionPayload"},"CookingFireSuppressionPresentPayload":{"properties":{"type":{"type":"string","const":"PRESENT","title":"Type","x-ui-label":"Describe whether there was any type of fire protection system or fire prevention technology focused on reducing damage from cooking fires installed above or in proximity of the cooking appliance.","x-ui-hint":"Select one."},"suppression_types":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeSuppressCookingValue"},"type":"array"},{"type":"null"}],"title":"Suppression Types","x-ui-label":"Describe the type(s) of system present.","x-ui-hint":"Select all that apply."},"operation_type":{"anyOf":[{"$ref":"#/components/schemas/TypeSuppressOperationValue"},{"type":"null"}],"x-ui-label":"Describe the operation of the suppression system.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"CookingFireSuppressionPresentPayload"},"CookingFireSuppressionPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"},"suppression_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Suppression Types"},"operation_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operation Type"}},"type":"object","required":["last_modified","type"],"title":"CookingFireSuppressionPresentResponse"},"CookingFireSuppressionResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"presence":{"oneOf":[{"$ref":"#/components/schemas/CookingFireSuppressionPresentResponse"},{"$ref":"#/components/schemas/CookingFireSuppressionNotPresentResponse"}],"title":"Presence"}},"type":"object","required":["last_modified","presence"],"title":"CookingFireSuppressionResponse"},"CreateDepartmentPayload":{"properties":{"mail_address_line_1":{"anyOf":[{"type":"string","maxLength":255,"minLength":2},{"type":"null"}],"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}]},"mail_zip_code":{"anyOf":[{"type":"string","maxLength":10,"minLength":5},{"type":"null"}],"title":"Mail Zip Code"},"address_line_1":{"type":"string","maxLength":255,"minLength":2,"title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Address Line 2"},"city":{"type":"string","maxLength":255,"minLength":1,"title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","maxLength":10,"minLength":5,"title":"Zip Code"},"name":{"type":"string","title":"Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"continue_edu":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Continue Edu"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Internal Id"},"rms_software":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rms Software"},"time_zone":{"$ref":"#/components/schemas/TimeZones"},"department_type":{"anyOf":[{"$ref":"#/components/schemas/TypeDeptValue"},{"type":"null"}]},"entity_type":{"anyOf":[{"$ref":"#/components/schemas/TypeEntityValue"},{"type":"null"}]},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationPayload"},{"type":"null"}]},"dispatch":{"anyOf":[{"$ref":"#/components/schemas/DepartmentDispatchPayload"},{"type":"null"}]},"staffing":{"anyOf":[{"$ref":"#/components/schemas/StaffingPayload"},{"type":"null"}]},"website":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Website"},"assessment":{"anyOf":[{"$ref":"#/components/schemas/AssessmentPayload"},{"type":"null"}]},"shift":{"anyOf":[{"$ref":"#/components/schemas/ShiftPayload"},{"type":"null"}]},"fire_services":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeServFdValue"},"type":"array"},{"type":"null"}],"title":"Fire Services"},"ems_services":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeServEmsValue"},"type":"array"},{"type":"null"}],"title":"Ems Services"},"investigation_services":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeServInvestValue"},"type":"array"},{"type":"null"}],"title":"Investigation Services"},"stations":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateStationPayload"},"type":"array"},{"type":"null"}],"title":"Stations"},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]},"region_sets":{"anyOf":[{"items":{"$ref":"#/components/schemas/RegionSetPayload"},"type":"array"},{"type":"null"}],"title":"Region Sets"},"aid_agreements":{"anyOf":[{"items":{"$ref":"#/components/schemas/AidAgreementPayload"},"type":"array"},{"type":"null"}],"title":"Aid Agreements"},"onboarding_status":{"type":"string","const":"UNCONFIRMED","title":"Onboarding Status","default":"UNCONFIRMED"},"fips_code":{"type":"string","title":"Fips Code","description":"FIPS code for NERIS ID generation"}},"additionalProperties":false,"type":"object","required":["address_line_1","city","state","zip_code","name","time_zone"],"title":"CreateDepartmentPayload","description":"\n\n
Validation Rules\n\n- The concatenated `address_line_1` + `address_line_2` must be at least 6 characters long.\n\n
"},"CreateFederalAgencyPayload":{"properties":{"mail_address_line_1":{"anyOf":[{"type":"string","maxLength":255,"minLength":2},{"type":"null"}],"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}]},"mail_zip_code":{"anyOf":[{"type":"string","maxLength":10,"minLength":5},{"type":"null"}],"title":"Mail Zip Code"},"address_line_1":{"type":"string","maxLength":255,"minLength":2,"title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Address Line 2"},"city":{"type":"string","maxLength":255,"minLength":1,"title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","maxLength":10,"minLength":5,"title":"Zip Code"},"name":{"type":"string","title":"Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"},"jurisdiction_type":{"$ref":"#/components/schemas/TypeFaJurisdictionValue"},"website":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Website"},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["address_line_1","city","state","zip_code","name","jurisdiction_type"],"title":"CreateFederalAgencyPayload","description":"\n\n
Validation Rules\n\n- The concatenated `address_line_1` + `address_line_2` must be at least 6 characters long.\n\n
"},"CreateFireMarshalPayload":{"properties":{"mail_address_line_1":{"anyOf":[{"type":"string","maxLength":255,"minLength":2},{"type":"null"}],"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}]},"mail_zip_code":{"anyOf":[{"type":"string","maxLength":10,"minLength":5},{"type":"null"}],"title":"Mail Zip Code"},"address_line_1":{"type":"string","maxLength":255,"minLength":2,"title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Address Line 2"},"city":{"type":"string","maxLength":255,"minLength":1,"title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","maxLength":10,"minLength":5,"title":"Zip Code"},"name":{"type":"string","title":"Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"},"website":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Website"},"jurisdiction_type":{"$ref":"#/components/schemas/TypeFmJurisdictionValue"},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["address_line_1","city","state","zip_code","name","jurisdiction_type"],"title":"CreateFireMarshalPayload","description":"\n\n
Validation Rules\n\n- The concatenated `address_line_1` + `address_line_2` must be at least 6 characters long.\n\n
"},"CreateIntegrationAccountPayload":{"properties":{"title":{"type":"string","title":"Title"},"type":{"$ref":"#/components/schemas/TypeIntegrationValue","description":"The type of integration","default":"RMS"}},"additionalProperties":false,"type":"object","required":["title"],"title":"CreateIntegrationAccountPayload"},"CreateIntegrationCredentialResponse":{"properties":{"client_id":{"type":"string","format":"uuid","title":"Client Id","description":"The client id of the integration"},"client_secret":{"type":"string","title":"Client Secret","description":"The secret of the integration. This cannot be retrieved again."}},"additionalProperties":false,"type":"object","required":["client_id","client_secret"],"title":"CreateIntegrationCredentialResponse"},"CreateIntegrationResponse":{"properties":{"title":{"type":"string","title":"Title","description":"The name of the integration"},"type":{"$ref":"#/components/schemas/TypeIntegrationValue"},"client_id":{"type":"string","format":"uuid","title":"Client Id","description":"The client id of the integration"},"client_secret":{"type":"string","title":"Client Secret","description":"The secret of the integration. This cannot be retrieved again."}},"additionalProperties":false,"type":"object","required":["title","type","client_id","client_secret"],"title":"CreateIntegrationResponse"},"CreateStationPayload":{"properties":{"address_line_1":{"type":"string","maxLength":255,"minLength":2,"title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Address Line 2"},"city":{"type":"string","maxLength":255,"minLength":1,"title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","maxLength":10,"minLength":5,"title":"Zip Code"},"station_id":{"type":"string","title":"Station Id"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Internal Id"},"staffing":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Staffing"},"units":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateUnitPayload"},"type":"array"},{"type":"null"}],"title":"Units"},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["address_line_1","city","state","zip_code","station_id"],"title":"CreateStationPayload","description":"\n\n
Validation Rules\n\n- The concatenated `address_line_1` + `address_line_2` must be at least 6 characters long.\n\n
"},"CreateUnitPayload":{"properties":{"staffing":{"type":"integer","minimum":0.0,"title":"Staffing","description":"The minimum staffing required for the unit to be dispatched to an incident."},"dedicated_staffing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Dedicated Staffing","description":"Whether the unit has dedicated staffing."},"type":{"$ref":"#/components/schemas/TypeUnitValue","description":"The type of unit housed at the station, and its associated capability."},"cad_designation_1":{"type":"string","pattern":"^[\\w\\(\\)\\#\\- ]{1,64}$","title":"Cad Designation 1","description":"A unit's designation in the computer-aided dispatch (CAD) system.","examples":["CAR15","Quint-42","HAZMAT #08"]},"cad_designation_2":{"anyOf":[{"type":"string","pattern":"^[\\w\\(\\)\\#\\- ]{1,64}$","examples":["CAR15","Quint-42","HAZMAT #08"]},{"type":"null"}],"title":"Cad Designation 2"}},"additionalProperties":false,"type":"object","required":["staffing","type","cad_designation_1"],"title":"CreateUnitPayload","description":"\n\n
Validation Rules\n\n- `dedicated_staffing` must not be `true` if `staffing` is `0`.\n\n
"},"CreateUserPayload":{"properties":{"email":{"type":"string","format":"email","title":"Email","description":"The new user's email address"},"given_name":{"type":"string","title":"Given Name"},"family_name":{"type":"string","title":"Family Name"},"phone_number":{"anyOf":[{"type":"string","pattern":"^(\\+?[1|0])?( )*([ \\[\\(])?\\d{3}([\\)\\]\\. \\-])?( )*\\d{3}([\\. \\-])?( )*\\d{4}$","examples":["+15555555555","(555)555-55555","555.555.5555","(555) 555-5555","15555555555","[555]555-5555"]},{"type":"null"}],"title":"Phone Number"}},"additionalProperties":false,"type":"object","required":["email","given_name","family_name"],"title":"CreateUserPayload"},"CrossStreetPayload":{"properties":{"number_prefix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Prefix","description":"Identifier in the portion of the complete address number that precedes the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"A\" in \"A19 Calle 117, Toa Alta, PR\""]},"number_suffix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Suffix","description":"Identifier in the portion of the complete address number that follows the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"1\"2\" in \"194-03 1\"2 50th Avenue, New York, NY 11365\" "]},"complete_number":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Complete Number","description":"Address Number Complete includes the Address Number Prefix (if any), the Address Number, Address Number Suffix (if any), and any formatting or separator characters needed to display the official version of the complete address number. The Address Number Complete precedes the complete street name to identify a location along a thoroughfare or within a defined area. ","examples":["\"A19\" in \"A19 Calle 117, Toa Alta, PR\""]},"distance_marker":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Distance Marker","description":"Distance travelled along a route such as a road or highway, indicated by a distance marker sign, typically a post or other marker indicating the distance in miles/kilometers from or to a given point. ","examples":["\"Milepost 1303\" in \"Milepost 1303, Alaska Highway\""]},"number":{"anyOf":[{"type":"integer","maximum":1000000.0,"minimum":0.0},{"type":"null"}],"title":"Number","description":"Integer identifier of a location along a thoroughfare or within a defined community.","examples":["\"123\" in \"123 Main Street\""]},"street_prefix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Modifier","description":"Word or phrase that precedes and modifies the Street Name element but is separated from it by a Street Name Pre Type or a Street Name Pre Directional or both.","examples":["\"Old\" in \"Old North First Street\" (\"Old\" is a Street Name Pre Modifier because the Street Name Pre Directional \"North\" separates \"Old\" from the Street Name \"First\" and the Street Name Post Type \"Street\".)"]},"street_prefix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Direction","description":"Word preceding the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"North\" in \"North Fairfax Drive\""]},"street":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street","description":"Element of the complete street name that identifies the particular street (as opposed to any street types, directionals, and modifiers).","examples":["\"Fairfax\" in \"North Fairfax Avenue\""]},"street_postfix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Direction","description":"Word following the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"East\" in \"Seventh Street East\""]},"street_postfix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Modifier","description":"Word or phrase that follows and modifies the Street Name element and is either separated from it by a Street Name Post Type and/or a Street Name Post Directional.","examples":["\"Extension\" in \"Market Street North Extension\" (because \"North\" separates \"Extension\" from the Street Name Post Type."]},"street_prefix":{"anyOf":[{"$ref":"#/components/schemas/TypeLocSnPrePostValue"},{"type":"null"}],"description":"Word or phrase that precedes the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"Avenue A\""]},"street_preposition_type_separator":{"anyOf":[{"$ref":"#/components/schemas/TypeLocSnPreSepValue"},{"type":"null"}],"description":"Preposition or prepositional phrase between the Street Name Pre Type and the Street Name.","examples":["\"of the\" in \"Avenue of the Americas\""]},"street_postfix":{"anyOf":[{"$ref":"#/components/schemas/TypeLocSnPrePostValue"},{"type":"null"}],"description":"Word or phrase that follows the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"North Fairfax Avenue\""]},"direction_of_travel":{"anyOf":[{"$ref":"#/components/schemas/TypeLocSnDirectionValue"},{"type":"null"}],"description":"Word which follows all other street name elements and is used only as needed to indicate direction of travel on a divided roadway and associated frontage roads.","examples":["\"northbound\" in \"Interstate Highway 5 North northbound\""]},"cross_street_modifier":{"anyOf":[{"$ref":"#/components/schemas/TypeLocationCrossStreetValue"},{"type":"null"}],"description":"Modifier of the cross street, such as 'nearest' or 'second nearest'"}},"additionalProperties":false,"type":"object","title":"CrossStreetPayload"},"CrossStreetResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"number_prefix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Prefix","description":"Identifier in the portion of the complete address number that precedes the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"A\" in \"A19 Calle 117, Toa Alta, PR\""]},"number_suffix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Suffix","description":"Identifier in the portion of the complete address number that follows the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"1\"2\" in \"194-03 1\"2 50th Avenue, New York, NY 11365\" "]},"complete_number":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Complete Number","description":"Address Number Complete includes the Address Number Prefix (if any), the Address Number, Address Number Suffix (if any), and any formatting or separator characters needed to display the official version of the complete address number. The Address Number Complete precedes the complete street name to identify a location along a thoroughfare or within a defined area. ","examples":["\"A19\" in \"A19 Calle 117, Toa Alta, PR\""]},"distance_marker":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Distance Marker","description":"Distance travelled along a route such as a road or highway, indicated by a distance marker sign, typically a post or other marker indicating the distance in miles/kilometers from or to a given point. ","examples":["\"Milepost 1303\" in \"Milepost 1303, Alaska Highway\""]},"number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Number","description":"Integer identifier of a location along a thoroughfare or within a defined community.","examples":["\"123\" in \"123 Main Street\""]},"street_prefix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Modifier","description":"Word or phrase that precedes and modifies the Street Name element but is separated from it by a Street Name Pre Type or a Street Name Pre Directional or both.","examples":["\"Old\" in \"Old North First Street\" (\"Old\" is a Street Name Pre Modifier because the Street Name Pre Directional \"North\" separates \"Old\" from the Street Name \"First\" and the Street Name Post Type \"Street\".)"]},"street_prefix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Direction","description":"Word preceding the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"North\" in \"North Fairfax Drive\""]},"street":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street","description":"Element of the complete street name that identifies the particular street (as opposed to any street types, directionals, and modifiers).","examples":["\"Fairfax\" in \"North Fairfax Avenue\""]},"street_postfix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Direction","description":"Word following the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"East\" in \"Seventh Street East\""]},"street_postfix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Modifier","description":"Word or phrase that follows and modifies the Street Name element and is either separated from it by a Street Name Post Type and/or a Street Name Post Directional.","examples":["\"Extension\" in \"Market Street North Extension\" (because \"North\" separates \"Extension\" from the Street Name Post Type."]},"street_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Street Prefix","description":"Word or phrase that precedes the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"Avenue A\""]},"street_preposition_type_separator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Street Preposition Type Separator","description":"Preposition or prepositional phrase between the Street Name Pre Type and the Street Name.","examples":["\"of the\" in \"Avenue of the Americas\""]},"street_postfix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Street Postfix","description":"Word or phrase that follows the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"North Fairfax Avenue\""]},"direction_of_travel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Direction Of Travel","description":"Word which follows all other street name elements and is used only as needed to indicate direction of travel on a divided roadway and associated frontage roads.","examples":["\"northbound\" in \"Interstate Highway 5 North northbound\""]},"cross_street_modifier":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cross Street Modifier","description":"Modifier of the cross street, such as 'nearest' or 'second nearest'"}},"type":"object","required":["last_modified"],"title":"CrossStreetResponse"},"CsstHazardPayload":{"properties":{"ignition_source":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Ignition Source","description":"Whether corrugated stainless steel tubing was a suspected ignition source.","x-ui-label":"Whether corrugated stainless steel tubing was a suspected ignition source.","x-ui-hint":"Select one."},"lightning_suspected":{"anyOf":[{"$ref":"#/components/schemas/TypeYesNoUnknownValue"},{"type":"null"}],"description":"Whether lightning was suspected as cause of ignition.","x-ui-label":"Whether the corrugated stainless steel tubing was grounded.","x-ui-hint":"Select one."},"grounded":{"anyOf":[{"$ref":"#/components/schemas/TypeYesNoUnknownValue"},{"type":"null"}],"description":"The determination of whether the corrugated stainless steel tubing (CSST) was grounded.","x-ui-label":"Whether lightning was suspected as cause of ignition.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","title":"CsstHazardPayload"},"CsstHazardResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"ignition_source":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Ignition Source","description":"Whether corrugated stainless steel tubing was a suspected ignition source."},"lightning_suspected":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lightning Suspected","description":"Whether lightning was suspected as cause of ignition."},"grounded":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grounded","description":"The determination of whether the corrugated stainless steel tubing (CSST) was grounded."}},"type":"object","required":["last_modified"],"title":"CsstHazardResponse"},"DecodeVinResultResponse":{"properties":{"abs":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Abs"},"active_safety_sys_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Active Safety Sys Note"},"adaptive_cruise_control":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Adaptive Cruise Control"},"adaptive_driving_beam":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Adaptive Driving Beam"},"adaptive_headlights":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Adaptive Headlights"},"additional_error_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Additional Error Text"},"air_bag_loc_curtain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Air Bag Loc Curtain"},"air_bag_loc_front":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Air Bag Loc Front"},"air_bag_loc_knee":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Air Bag Loc Knee"},"air_bag_loc_seat_cushion":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Air Bag Loc Seat Cushion"},"air_bag_loc_side":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Air Bag Loc Side"},"auto_reverse_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Auto Reverse System"},"automatic_pedestrian_alerting_sound":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Automatic Pedestrian Alerting Sound"},"axle_configuration":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Axle Configuration"},"axles":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Axles"},"base_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Base Price"},"battery_amps_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery Amps Min"},"battery_amps_max":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery Amps Max"},"battery_cells":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery Cells"},"battery_info":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Battery Info"},"battery_kwh_min":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kwh Min"},"battery_kwh_max":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Kwh Max"},"battery_modules":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery Modules"},"battery_packs":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Battery Packs"},"battery_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Battery Type"},"battery_volts_min":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Volts Min"},"battery_volts_max":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Battery Volts Max"},"bed_length_in":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Bed Length In"},"bed_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bed Type"},"blind_spot_intervention":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blind Spot Intervention"},"blind_spot_mon":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Blind Spot Mon"},"body_cab_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body Cab Type"},"body_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body Class"},"brake_system_desc":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brake System Desc"},"brake_system_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brake System Type"},"bus_floor_config_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bus Floor Config Type"},"bus_length":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Bus Length"},"bus_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bus Type"},"can_aacn":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Can Aacn"},"cib":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cib"},"cash_for_clunkers":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cash For Clunkers"},"charger_level":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Charger Level"},"charger_power_kw":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Charger Power Kw"},"combined_braking_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Combined Braking System"},"cooling_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cooling Type"},"curb_weight_lb":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Curb Weight Lb"},"custom_motorcycle_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Custom Motorcycle Type"},"daytime_running_light":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Daytime Running Light"},"destination_market":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination Market"},"displacement_cc":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Displacement Cc"},"displacement_ci":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Displacement Ci"},"displacement_l":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Displacement L"},"doors":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Doors"},"drive_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Drive Type"},"driver_assist":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Driver Assist"},"dynamic_brake_support":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dynamic Brake Support"},"edr":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Edr"},"esc":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Esc"},"ev_drive_unit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ev Drive Unit"},"electrification_level":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Electrification Level"},"engine_configuration":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Engine Configuration"},"engine_cycles":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Engine Cycles"},"engine_cylinders":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Engine Cylinders"},"engine_hp_min":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Engine Hp Min"},"engine_hp_max":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Engine Hp Max"},"engine_power_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Engine Power Kw"},"engine_manufacturer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Engine Manufacturer"},"engine_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Engine Model"},"entertainment_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entertainment System"},"error_code":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Error Code"},"error_text":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Error Text"},"forward_collision_warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Forward Collision Warning"},"fuel_injection_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fuel Injection Type"},"fuel_tank_material":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fuel Tank Material"},"fuel_tank_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fuel Tank Type"},"fuel_type_primary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fuel Type Primary"},"fuel_type_secondary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fuel Type Secondary"},"gcwr_min":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gcwr Min"},"gcwr_max":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gcwr Max"},"gvwr_min":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gvwr Min"},"gvwr_max":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gvwr Max"},"keyless_ignition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Keyless Ignition"},"lane_centering_assistance":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lane Centering Assistance"},"lane_departure_warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lane Departure Warning"},"lane_keep_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lane Keep System"},"lower_beam_headlamp_light_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lower Beam Headlamp Light Source"},"make":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Make"},"make_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Make Id"},"manufacturer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manufacturer"},"manufacturer_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Manufacturer Id"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"model_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Model Id"},"model_year":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Model Year"},"motorcycle_chassis_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Motorcycle Chassis Type"},"motorcycle_suspension_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Motorcycle Suspension Type"},"ncsa_body_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ncsa Body Type"},"ncsa_make":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ncsa Make"},"ncsa_map_exc_approved_by":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ncsa Map Exc Approved By"},"ncsa_map_exc_approved_on":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ncsa Map Exc Approved On"},"ncsa_mapping_exception":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ncsa Mapping Exception"},"ncsa_model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ncsa Model"},"ncsa_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ncsa Note"},"non_land_use":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Non Land Use"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"other_bus_info":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Other Bus Info"},"other_engine_info":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Other Engine Info"},"other_motorcycle_info":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Other Motorcycle Info"},"other_restraint_system_info":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Other Restraint System Info"},"other_trailer_info":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Other Trailer Info"},"park_assist":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Park Assist"},"pedestrian_automatic_emergency_braking":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pedestrian Automatic Emergency Braking"},"plant_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plant City"},"plant_company_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plant Company Name"},"plant_country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plant Country"},"plant_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plant State"},"possible_values":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Possible Values"},"pretensioner":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pretensioner"},"rear_automatic_emergency_braking":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rear Automatic Emergency Braking"},"rear_cross_traffic_alert":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rear Cross Traffic Alert"},"rear_visibility_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rear Visibility System"},"sae_automation_level_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sae Automation Level Min"},"sae_automation_level_max":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sae Automation Level Max"},"seat_belts_all":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Seat Belts All"},"seat_rows":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seat Rows"},"seats":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Seats"},"semiautomatic_headlamp_beam_switching":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Semiautomatic Headlamp Beam Switching"},"series":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Series"},"series_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Series 2"},"steering_location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Steering Location"},"suggested_vin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Suggested Vin"},"tpms":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tpms"},"top_speed_mph":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Top Speed Mph"},"track_width":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Track Width"},"traction_control":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Traction Control"},"trailer_body_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trailer Body Type"},"trailer_length":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Trailer Length"},"trailer_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trailer Type"},"transmission_speeds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Transmission Speeds"},"transmission_style":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Transmission Style"},"trim":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trim"},"trim_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trim 2"},"turbo":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Turbo"},"vin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vin"},"valve_train_design":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Valve Train Design"},"vehicle_descriptor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vehicle Descriptor"},"vehicle_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vehicle Type"},"wheel_base_long":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Wheel Base Long"},"wheel_base_short":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Wheel Base Short"},"wheel_base_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wheel Base Type"},"wheel_size_front":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Wheel Size Front"},"wheel_size_rear":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Wheel Size Rear"},"wheelie_mitigation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Wheelie Mitigation"},"wheels":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Wheels"},"windows":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Windows"}},"type":"object","title":"DecodeVinResultResponse"},"DepartmentCreatedModifiedResponse":{"properties":{"neris_id":{"type":"string","title":"Neris Id"},"version":{"type":"integer","title":"Version"},"valid_start":{"type":"string","format":"date-time","title":"Valid Start"},"stations":{"items":{"$ref":"#/components/schemas/StationCreatedModifiedResponse"},"type":"array","title":"Stations"}},"type":"object","required":["neris_id","version","valid_start"],"title":"DepartmentCreatedModifiedResponse"},"DepartmentDispatchPayload":{"properties":{"avl_usage":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Avl Usage"},"center_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Center Id","description":"\n\n
Validation Rules\n\n- `center_id` must be exactly four digits.\n\n
"},"cad_software":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cad Software"},"psap_type":{"anyOf":[{"$ref":"#/components/schemas/TypePsapValue"},{"type":"null"}]},"psap_capability":{"anyOf":[{"$ref":"#/components/schemas/TypePsapCapaValue"},{"type":"null"}]},"psap_discipline":{"anyOf":[{"$ref":"#/components/schemas/TypePsapDiscValue"},{"type":"null"}]},"psap_jurisdiction":{"anyOf":[{"$ref":"#/components/schemas/TypePsapJurisValue"},{"type":"null"}]},"protocol_fire":{"anyOf":[{"$ref":"#/components/schemas/TypeDispProtoFireValue"},{"type":"null"}]},"protocol_med":{"anyOf":[{"$ref":"#/components/schemas/TypeDispProtoMedValue"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"DepartmentDispatchPayload"},"DepartmentDispatchResponse":{"properties":{"avl_usage":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Avl Usage"},"center_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Center Id"},"cad_software":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cad Software"},"psap_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Psap Type"},"psap_capability":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Psap Capability"},"psap_discipline":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Psap Discipline"},"psap_jurisdiction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Psap Jurisdiction"},"protocol_fire":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Protocol Fire"},"protocol_med":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Protocol Med"}},"type":"object","title":"DepartmentDispatchResponse"},"DepartmentMetricsResponse":{"properties":{"incident_count":{"$ref":"#/components/schemas/IncidentCountMetricsResponse"}},"type":"object","required":["incident_count"],"title":"DepartmentMetricsResponse"},"DepartmentWithFeatureFlagsResponse":{"properties":{"feature_flags":{"anyOf":[{"$ref":"#/components/schemas/EntityFeatureFlagsResponse"},{"type":"null"}]},"mail_address_line_1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}]},"mail_zip_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Zip Code"},"address_line_1":{"type":"string","title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"type":"string","title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","title":"Zip Code"},"name":{"type":"string","title":"Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"continue_edu":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Continue Edu"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Internal Id"},"rms_software":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rms Software"},"time_zone":{"$ref":"#/components/schemas/TimeZones"},"neris_id":{"type":"string","title":"Neris Id"},"version":{"type":"integer","title":"Version"},"last_modified":{"type":"string","format":"date-time","title":"Last Modified"},"onboarding_status":{"$ref":"#/components/schemas/DeptOnboardingStatus"},"department_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department Type"},"entity_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Type"},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationResponse"},{"type":"null"}]},"dispatch":{"anyOf":[{"$ref":"#/components/schemas/DepartmentDispatchResponse"},{"type":"null"}]},"staffing":{"anyOf":[{"$ref":"#/components/schemas/StaffingResponse"},{"type":"null"}]},"assessment":{"anyOf":[{"$ref":"#/components/schemas/AssessmentResponse"},{"type":"null"}]},"website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website"},"shift":{"anyOf":[{"$ref":"#/components/schemas/ShiftResponse"},{"type":"null"}]},"fire_services":{"items":{"type":"string"},"type":"array","title":"Fire Services"},"ems_services":{"items":{"type":"string"},"type":"array","title":"Ems Services"},"investigation_services":{"items":{"type":"string"},"type":"array","title":"Investigation Services"},"stations":{"items":{"$ref":"#/components/schemas/StationResponse"},"type":"array","title":"Stations"},"location":{"anyOf":[{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."},{"type":"null"}],"title":"Location"},"region_sets":{"items":{"$ref":"#/components/schemas/RegionSetResponse"},"type":"array","title":"Region Sets"},"aid_agreements":{"items":{"$ref":"#/components/schemas/AidAgreementResponse"},"type":"array","title":"Aid Agreements"}},"type":"object","required":["address_line_1","city","state","zip_code","name","time_zone","neris_id","version","last_modified","onboarding_status"],"title":"DepartmentWithFeatureFlagsResponse"},"DeptOnboardingStatus":{"type":"string","enum":["CONFIRMED","UNCONFIRMED"],"title":"DeptOnboardingStatus"},"DeptSortBy":{"type":"string","enum":["name","neris_id","address_line_1","city","state","zip_code","department_type","website","last_modified"],"title":"DeptSortBy"},"DispatchPayload":{"properties":{"center_id":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Center Id","description":"This 4 digit ID is a unique identifier for each PSAP dispatch center.\n\n
Validation Rules\n\n- `center_id` must be exactly four digits.\n\n
","examples":["1234"]},"incident_number":{"type":"string","pattern":"[\\w\\-\\:]+","title":"Incident Number","description":"Unique identifier for the event that requires dispatch of department resources.","examples":["FDID_20230814000001","FDID_20230814000002"],"x-ui-label":"Incident Number","x-ui-hint":"This incident number is automatically generated by NERIS. Please replace with local incident number if desired."},"determinant_code":{"anyOf":[{"type":"string","maxLength":8,"minLength":1},{"type":"null"}],"title":"Determinant Code","description":"Output code from dispatch protocol based on the call specifics.","examples":["17-D-5"],"x-ui-label":"Department-specific incident code derived from dispatch."},"incident_code":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Incident Code","description":"Department-specific incident code pulled from CAD; levels up into incident_type.","examples":["STRF1","ALS2"]},"automatic_alarm":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Automatic Alarm","description":"Identifies if the call is an automatic alarm."},"incident_clear":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Incident Clear","description":"Timestamp when dispatch closes incident.","x-ui-label":"Time at which dispatch closed incident.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"call_arrival":{"type":"string","format":"date-time","title":"Call Arrival","description":"Timestamp at which call arrives at PSAP or department dispatch center.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442],"x-ui-label":"Time at which call arrived at PSAP or dispatch center.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"call_answered":{"type":"string","format":"date-time","title":"Call Answered","description":"Timestamp at which call is answered at PSAP or department dispatch center.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442],"x-ui-label":"Time at which call was answered at PSAP or dispatch center.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"call_create":{"type":"string","format":"date-time","title":"Call Create","description":"Timestamp at which call processing begins.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442],"x-ui-label":"Time at which call was created.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"disposition":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Disposition","description":"Closing disposition of call set by dispatcher if call changes from initial dispatch.","examples":["No Patient Found","Building Secure"]},"location":{"$ref":"#/components/schemas/LocationPayload"},"point":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]},"comments":{"anyOf":[{"items":{"$ref":"#/components/schemas/CommentPayload"},"type":"array"},{"type":"null"}],"title":"Comments"},"unit_responses":{"items":{"$ref":"#/components/schemas/DispatchUnitResponsePayload"},"type":"array","title":"Unit Responses"},"tactic_timestamps":{"anyOf":[{"$ref":"#/components/schemas/DispatchTacticTimestampsPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["incident_number","call_arrival","call_answered","call_create","location","unit_responses"],"title":"DispatchPayload","description":"\n\n
Validation Rules\n\n- `call_arrival` must not be after `call_answered`.\n- `call_arrival` must not be after `call_create`.\n- `call_answered` must not be after `call_create`.\n\n
"},"DispatchResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"center_id":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Center Id","description":"This 4 digit ID is a unique identifier for each PSAP dispatch center.","examples":["1234"]},"incident_number":{"type":"string","pattern":"[\\w\\-\\:]+","title":"Incident Number","description":"Unique identifier for the event that requires dispatch of department resources.","examples":["FDID_20230814000001","FDID_20230814000002"]},"determinant_code":{"anyOf":[{"type":"string","maxLength":8,"minLength":1},{"type":"null"}],"title":"Determinant Code","description":"Output code from dispatch protocol based on the call specifics.","examples":["17-D-5"]},"incident_code":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Incident Code","description":"Department-specific incident code pulled from CAD; levels up into incident_type.","examples":["STRF1","ALS2"]},"automatic_alarm":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Automatic Alarm","description":"Identifies if the call is an automatic alarm."},"incident_clear":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Incident Clear","description":"Timestamp when dispatch closes incident."},"call_arrival":{"type":"string","format":"date-time","title":"Call Arrival","description":"Timestamp at which call arrives at PSAP or department dispatch center.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"call_answered":{"type":"string","format":"date-time","title":"Call Answered","description":"Timestamp at which call is answered at PSAP or department dispatch center.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"call_create":{"type":"string","format":"date-time","title":"Call Create","description":"Timestamp at which call processing begins.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"disposition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disposition","description":"Closing disposition of call set by dispatcher if call changes from initial dispatch.","examples":["No Patient Found","Building Secure"]},"location":{"$ref":"#/components/schemas/LocationResponse"},"comments":{"anyOf":[{"items":{"$ref":"#/components/schemas/CommentResponse"},"type":"array"},{"type":"null"}],"title":"Comments"},"tactic_timestamps":{"anyOf":[{"$ref":"#/components/schemas/DispatchTacticTimestampsResponse"},{"type":"null"}]},"unit_responses":{"items":{"$ref":"#/components/schemas/UnitResponseResponse"},"type":"array","title":"Unit Responses"},"point":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Point"}},"type":"object","required":["last_modified","incident_number","call_arrival","call_answered","call_create","location","tactic_timestamps","unit_responses"],"title":"DispatchResponse"},"DispatchTacticTimestampsPayload":{"properties":{"command_established":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Command Established","description":"Timestamp at which incident command is established.","x-ui-label":"Time at which incident command was established.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"completed_sizeup":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Completed Sizeup","description":"Timestamp at which incident sizeup is complete.","x-ui-label":"Time at which incident sizeup was completed.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"suppression_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Suppression Complete","description":"Timestamp at which suppression is complete.","x-ui-label":"Time at which suppression was complete.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"primary_search_begin":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Primary Search Begin","description":"Timestamp at which primary search operations begin.","x-ui-label":"Time at which primary search operations began.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"primary_search_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Primary Search Complete","description":"Timestamp at which primary search operations are complete.","x-ui-label":"Time at which primary search operations were complete.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"water_on_fire":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Water On Fire","description":"Timestamp at which water is first applied to fire.","x-ui-label":"Time at which water is first applied to fire.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"fire_under_control":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Fire Under Control","description":"Timestamp at which fire is considered contained (e.g. in a wildfire), but not yet extinguished.","x-ui-label":"Time at which fire is considered contained (e.g. in a wildfire), but not yet extinguished.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"fire_knocked_down":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Fire Knocked Down","description":"Timestamp at which fire is has been knocked down.","x-ui-label":"Time at which fire was knocked down.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"extrication_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Extrication Complete","description":"Timestamp at which extrication (motor vehicle, technical rescue) has been completed."}},"additionalProperties":false,"type":"object","title":"DispatchTacticTimestampsPayload"},"DispatchTacticTimestampsResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"command_established":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Command Established","description":"Timestamp at which incident command is established."},"completed_sizeup":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Completed Sizeup","description":"Timestamp at which incident sizeup is complete."},"suppression_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Suppression Complete","description":"Timestamp at which suppression is complete."},"primary_search_begin":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Primary Search Begin","description":"Timestamp at which primary search operations begin."},"primary_search_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Primary Search Complete","description":"Timestamp at which primary search operations are complete."},"water_on_fire":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Water On Fire","description":"Timestamp at which water is first applied to fire."},"fire_under_control":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Fire Under Control","description":"Timestamp at which fire is considered contained (e.g. in a wildfire), but not yet extinguished."},"fire_knocked_down":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Fire Knocked Down","description":"Timestamp at which fire is has been knocked down."},"extrication_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Extrication Complete","description":"Timestamp at which extrication (motor vehicle, technical rescue) has been completed."}},"type":"object","required":["last_modified"],"title":"DispatchTacticTimestampsResponse"},"DispatchUnitResponsePayload":{"properties":{"unit_neris_id":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}U[A-Z\\d]{3}$","examples":["FD24027334S001U003","FD24160543S010U001"]},{"type":"null"}],"title":"Unit Neris Id","description":"NERIS ID of the unit responding to the incident.","examples":["FD24027000S000U000"],"x-ui-label":"Unit assigned to the incident."},"reported_unit_id":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Reported Unit Id","description":"Name or ID of the unit responding to the incident if not yet in Entity Spec (or cannot be furnished).","examples":["A702"],"x-ui-label":"If unit not in drop down, enter CAD designation here."},"staffing":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Staffing","description":"On-scene staffing of unit.","examples":[4],"x-ui-label":"Incident staffing of unit."},"unable_to_dispatch":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Unable To Dispatch","description":"Set to TRUE if unit is unable to dispatch.","x-ui-label":"Unit was able to dispatch.","x-ui-hint":"Select No if unit could not dispatch to incident."},"dispatch":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Dispatch","description":"Timestamp that the unit is dispatched.","x-ui-label":"Time at which the unit was dispatched.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"enroute_to_scene":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Enroute To Scene","description":"Timestamp that the unit goes enroute to scene of the incident.","x-ui-label":"Time at which the unit went enroute to scene of the incident.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"on_scene":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"On Scene","description":"Timestamp that the unit arrived on scene.","x-ui-label":"Time at which the unit arrived on scene.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"canceled_enroute":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Canceled Enroute","description":"Timestamp that the unit was canceled prior to arrival.","x-ui-label":"Time at which unit response was canceled.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"staging":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Staging","description":"Timestamp that the unit stages on scene.","x-ui-label":"If applicable, time at which the unit staged on scene.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"unit_clear":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Unit Clear","description":"Timestamp that the unit clears the incident.","x-ui-label":"Time at which the unit cleared the incident.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"med_responses":{"anyOf":[{"items":{"$ref":"#/components/schemas/MedResponsePayload"},"type":"array"},{"type":"null"}],"title":"Med Responses"},"point":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]},"response_mode":{"anyOf":[{"$ref":"#/components/schemas/TypeResponseModeValue"},{"type":"null"}],"description":"Response mode of unit as part of the unit's response to an incident.","x-ui-label":"Response mode of unit as part of the unit's response to an incident."},"transport_mode":{"anyOf":[{"$ref":"#/components/schemas/TypeResponseModeValue"},{"type":"null"}],"description":"Transport mode of unit as part of the unit's response to a hospital.","x-ui-label":"Response mode of unit as part of the unit's transport to a hospital."}},"additionalProperties":false,"type":"object","title":"DispatchUnitResponsePayload","description":"\n\n
Validation Rules\n\n- `neris_id_unit`, `reported_id_unit`, or both must be provided.\n\n
"},"DistancePayload":{"properties":{"distance_nearest_structure":{"type":"number","title":"Distance Nearest Structure","description":"The distance to the next nearest structure.","x-ui-label":"What is the distance to the nearest structure?","x-ui-hint":"You will be able to select the unit of measurement next (miles, feet)"},"distance_unit":{"$ref":"#/components/schemas/TypeDistanceUnitValue","description":"The units of distance from the structure to the next nearest structure.","x-ui-label":"Select the units for the distance to nearest structure."}},"additionalProperties":false,"type":"object","required":["distance_nearest_structure","distance_unit"],"title":"DistancePayload"},"DistanceResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"distance_nearest_structure":{"type":"number","title":"Distance Nearest Structure","description":"The distance to the next nearest structure."},"type_distance_unit":{"type":"string","title":"Type Distance Unit","description":"The units of distance from the structure to the next nearest structure."}},"type":"object","required":["distance_nearest_structure","type_distance_unit"],"title":"DistanceResponse"},"ElectricHazardFirePayload":{"properties":{"reignition":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Reignition","description":"Whether there was a re-ignition.","x-ui-label":"Whether there was a re-ignition.","x-ui-hint":"Select one."},"suppression_types":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeEmerghazSuppressionValue"},"type":"array"},{"type":"null"}],"title":"Suppression Types","description":"Suppression methods used.","x-ui-label":"Describe suppression approach.","x-ui-hint":"Select all that apply."}},"additionalProperties":false,"type":"object","title":"ElectricHazardFirePayload"},"ElectricHazardFireResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"reignition":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Reignition","description":"Whether there was a re-ignition."},"suppression_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Suppression Types","description":"Suppression methods used."}},"type":"object","required":["last_modified"],"title":"ElectricHazardFireResponse"},"ElectricHazardPayload":{"properties":{"fire_details":{"anyOf":[{"$ref":"#/components/schemas/ElectricHazardFirePayload"},{"type":"null"}],"description":"Fire details for the electric emerging hazard."},"type":{"$ref":"#/components/schemas/TypeEmerghazElecValue","description":"Additional specificity on the battery powered / stored energy emerging hazard.","x-ui-label":"The specific type of electrification hazard.","x-ui-hint":"Select one."},"source_or_target":{"anyOf":[{"$ref":"#/components/schemas/TypeSourceTargetValue"},{"type":"null"}],"description":"Whether the battery was the source or target.","x-ui-label":"Whether the battery/ESS was the source or target.","x-ui-hint":"Select one."},"involved_in_crash":{"anyOf":[{"type":"boolean"},{"type":"null"}],"type":"boolean","title":"Involved In Crash","description":"Whether the electric vehicle was involved in crash. Only applicable to `ELECTRIC_VEHICLE` types","x-ui-label":"Whether the electric vehicle was involved in crash.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"ElectricHazardPayload","description":"\n\n
Validation Rules\n\n- `involved_in_crash` only applicable to `ELECTRIC_VEHICLE` types.\n\n
"},"ElectricHazardResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"fire_details":{"anyOf":[{"$ref":"#/components/schemas/ElectricHazardFireResponse"},{"type":"null"}]},"type":{"type":"string","title":"Type","description":"Additional specificity on the battery powered / stored energy emerging hazard."},"source_or_target":{"anyOf":[{"$ref":"#/components/schemas/TypeSourceTargetValue"},{"type":"null"}],"description":"Whether the battery was the source or target."},"involved_in_crash":{"anyOf":[{"type":"boolean"},{"type":"null"}],"type":"boolean","title":"Involved In Crash","description":"Whether the electric vehicle was involved in crash. Only applicable to `ELECTRIC_VEHICLE` types"}},"type":"object","required":["last_modified","type"],"title":"ElectricHazardResponse"},"EntityClassOptions":{"type":"string","enum":["FIRE_DEPARTMENT","FIRE_MARSHAL","FEDERAL_AGENCY"],"title":"EntityClassOptions"},"EntityCreatedModifiedResponse":{"properties":{"neris_id":{"type":"string","title":"Neris Id"},"version":{"type":"integer","title":"Version"},"valid_start":{"type":"string","format":"date-time","title":"Valid Start"}},"type":"object","required":["neris_id","version","valid_start"],"title":"EntityCreatedModifiedResponse"},"EntityFeatureFlagsPayload":{"properties":{"allow_ui_incident_creation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Ui Incident Creation"},"allow_ui_no_activity_report_creation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Ui No Activity Report Creation"},"allow_incident_analysis_creation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Incident Analysis Creation"},"allow_ui_incident_creation_v2":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Ui Incident Creation V2"},"non_reporting":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Non Reporting"}},"additionalProperties":false,"type":"object","title":"EntityFeatureFlagsPayload"},"EntityFeatureFlagsResponse":{"properties":{"allow_ui_incident_creation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Ui Incident Creation"},"allow_ui_no_activity_report_creation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Ui No Activity Report Creation"},"allow_incident_analysis_creation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Incident Analysis Creation"},"allow_ui_incident_creation_v2":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Allow Ui Incident Creation V2"},"non_reporting":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Non Reporting"}},"type":"object","title":"EntityFeatureFlagsResponse"},"EntityRoleEntitySetAttachmentResponse":{"properties":{"neris_id_entity":{"type":"string","pattern":"^(FD|FM|FA)\\d{8}$","title":"Neris Id Entity","examples":["FD24027334","FM24001001","FA24001001"]},"nuid_role":{"type":"string","format":"uuid","title":"Nuid Role"},"nuid_entity_set":{"type":"string","format":"uuid","title":"Nuid Entity Set"}},"additionalProperties":false,"type":"object","required":["neris_id_entity","nuid_role","nuid_entity_set"],"title":"EntityRoleEntitySetAttachmentResponse"},"EntitySetMembershipPayload":{"properties":{"neris_id":{"type":"string","title":"Neris Id","description":"The entity's NERIS ID"}},"additionalProperties":false,"type":"object","required":["neris_id"],"title":"EntitySetMembershipPayload"},"EntitySetPayload":{"properties":{"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"type":{"$ref":"#/components/schemas/TypeEntitySetValue","description":"The type of entity set."},"members":{"anyOf":[{"items":{"$ref":"#/components/schemas/EntitySetMembershipPayload"},"type":"array"},{"type":"null"}],"title":"Members"}},"additionalProperties":false,"type":"object","required":["name","type"],"title":"EntitySetPayload"},"EntitySetResponse":{"properties":{"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"type":{"type":"string","title":"Type","description":"The type of entity set."},"members":{"anyOf":[{"items":{"$ref":"#/components/schemas/EntitySetMembershipPayload"},"type":"array"},{"type":"null"}],"title":"Members"},"nuid":{"type":"string","format":"uuid","title":"Nuid","description":"The unique identifier for the entity set"}},"additionalProperties":false,"type":"object","required":["name","type","nuid"],"title":"EntitySetResponse"},"EntitySummaryInfoResponse":{"properties":{"name":{"type":"string","title":"Name"},"neris_id":{"type":"string","title":"Neris Id"},"address_line_1":{"type":"string","title":"Address Line 1"},"city":{"type":"string","title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","title":"Zip Code"},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"department_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Department Type"},"website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website"},"time_zone":{"anyOf":[{"$ref":"#/components/schemas/TimeZones"},{"type":"null"}]},"location":{"anyOf":[{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."},{"type":"null"}],"title":"Location"},"region_sets":{"anyOf":[{"items":{"$ref":"#/components/schemas/RegionSetResponse"},"type":"array"},{"type":"null"}],"title":"Region Sets"},"stations":{"anyOf":[{"items":{"$ref":"#/components/schemas/StationSummaryInfoResponse"},"type":"array"},{"type":"null"}],"title":"Stations"}},"type":"object","required":["name","neris_id","address_line_1","city","state","zip_code","last_modified"],"title":"EntitySummaryInfoResponse"},"ErrorResponse":{"properties":{"error":{"$ref":"#/components/schemas/AuthError","description":"The error code. Error messages respect rfc6749."}},"type":"object","required":["error"],"title":"ErrorResponse"},"ExposurePayload":{"properties":{"people_present":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"People Present","description":"Whether people were present at the time of the incident."},"displacement_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Displacement Count","description":" Number of people/businesses displaced.","x-ui-label":"Total number of occupants/businesses that were displaced from the exposure."},"location_detail":{"oneOf":[{"$ref":"#/components/schemas/ExternalExposurePayload"},{"$ref":"#/components/schemas/InternalExposurePayload"}],"title":"Location Detail","discriminator":{"propertyName":"type","mapping":{"EXTERNAL_EXPOSURE":"#/components/schemas/ExternalExposurePayload","INTERNAL_EXPOSURE":"#/components/schemas/InternalExposurePayload"}}},"location":{"$ref":"#/components/schemas/LocationPayload"},"location_use":{"anyOf":[{"$ref":"#/components/schemas/LocationUsePayload"},{"type":"null"}]},"point":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]},"polygon":{"anyOf":[{"$ref":"#/components/schemas/HighPrecisionGeoMultipolygon"},{"type":"null"}]},"damage_type":{"$ref":"#/components/schemas/TypeExposureDamageValue","description":"Rating of damage to the exposure.","x-ui-label":"Rating of damage to the exposure.","x-ui-hint":"Select one."},"displacement_causes":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeDisplaceCauseValueRelExposure"},"type":"array"},{"type":"null"}],"title":"Displacement Causes","description":"Causes of displacement.","x-ui-label":"Apparent cause of the displacement(s)."}},"additionalProperties":false,"type":"object","required":["location_detail","location","damage_type"],"title":"ExposurePayload"},"ExposureResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"people_present":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"People Present","description":"Whether people were present at the time of the incident."},"displacement_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Displacement Count","description":" Number of people/businesses displaced."},"location_detail":{"anyOf":[{"$ref":"#/components/schemas/ExternalExposureResponse"},{"$ref":"#/components/schemas/InternalExposureResponse"}],"title":"Location Detail"},"location":{"$ref":"#/components/schemas/LocationResponse"},"location_use":{"anyOf":[{"$ref":"#/components/schemas/LocationUseResponse"},{"type":"null"}]},"damage_type":{"type":"string","title":"Damage Type"},"point":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Point"},"polygon":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Polygon"},"displacement_causes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Displacement Causes","description":"Causes of displacement."},"parcel":{"anyOf":[{"$ref":"#/components/schemas/ParcelResponse"},{"type":"null"}],"description":"Parcel information for the exposure coordinates, if available"}},"type":"object","required":["last_modified","location_detail","location","damage_type"],"title":"ExposureResponse"},"ExternalExposurePayload":{"properties":{"type":{"type":"string","const":"EXTERNAL_EXPOSURE","title":"Type","x-ui-label":"The type exposure being reported."},"item_type":{"$ref":"#/components/schemas/TypeExposureItemValue","description":"Type of property damaged or destroyed from the originating source of the hazard.","x-ui-label":"The type of property damaged or destroyed secondary from the originating source of the hazard."}},"additionalProperties":false,"type":"object","required":["type","item_type"],"title":"ExternalExposurePayload"},"ExternalExposureResponse":{"properties":{"type":{"type":"string","title":"Type"},"item_type":{"type":"string","title":"Item Type","description":"Type of property damaged or destroyed from the originating source of the hazard."}},"type":"object","required":["type","item_type"],"title":"ExternalExposureResponse"},"FederalAgencyResponse":{"properties":{"mail_address_line_1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}]},"mail_zip_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Zip Code"},"address_line_1":{"type":"string","title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"type":"string","title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","title":"Zip Code"},"name":{"type":"string","title":"Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"},"neris_id":{"type":"string","title":"Neris Id"},"version":{"type":"integer","title":"Version"},"active":{"type":"boolean","title":"Active"},"website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website"},"jurisdiction_type":{"type":"string","title":"Jurisdiction Type"},"location":{"anyOf":[{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."},{"type":"null"}],"title":"Location"},"last_modified":{"type":"string","format":"date-time","title":"Last Modified"},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationResponse"},{"type":"null"}]}},"type":"object","required":["address_line_1","city","state","zip_code","name","neris_id","version","active","jurisdiction_type","last_modified"],"title":"FederalAgencyResponse"},"FfInjuryDetailsPayload":{"properties":{"unit_neris_id":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}U[A-Z\\d]{3}$","examples":["FD24027334S001U003","FD24160543S010U001"]},{"type":"null"}],"title":"Unit Neris Id","description":"NERIS ID of the unit responding to the incident in FD Spec.","examples":["FD24027442S000U000","FD24027442S000U001"],"x-ui-label":"Unit assigned to the incident."},"reported_unit_id":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Reported Unit Id","description":"Name of the unit responding to the incident if neris id not yet in FD Spec.","examples":["A702"],"x-ui-label":"If unit not in drop down, enter CAD designation here."},"unit_continuity":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Unit Continuity","description":"Continuity of company at the time of incident.","x-ui-label":"Describe whether there was continuity of company at the time of incident."},"incident_command":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Incident Command","description":"Whether an incident command structure was in place during incident.","x-ui-label":"Whether incident command structure was in place during incident."},"job_classification":{"anyOf":[{"$ref":"#/components/schemas/TypeJobClassificationValue"},{"type":"null"}],"description":"Job classification of firefighter.","x-ui-label":"Job classification of firefighter.","x-ui-hint":"Select one."},"duty_type":{"anyOf":[{"$ref":"#/components/schemas/TypeDutyValue"},{"type":"null"}],"description":"Duty state of the firefighter at the time of the incident.","x-ui-label":"The duty state of the firefighter at the time of the incident.","x-ui-hint":"Select one."},"action_type":{"anyOf":[{"$ref":"#/components/schemas/TypeCasualtyActionValue"},{"type":"null"}],"description":"Actions that occurred during the time of the incident.","x-ui-label":"Firefighter duties at the time of the incident."},"incident_stage":{"anyOf":[{"$ref":"#/components/schemas/TypeCasualtyTimelineValue"},{"type":"null"}],"description":"Stage of the incident when the injury occurred.","x-ui-label":"Stage of the incident when the injury occurred.","x-ui-hint":"Select one."},"ppe_items":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeCasualtyPpeValue"},"type":"array"},{"type":"null"}],"title":"Ppe Items","description":"PPE worn during time of incident.","x-ui-label":"The PPE worn during time of incident.","x-ui-hint":"Select all that apply."}},"additionalProperties":false,"type":"object","title":"FfInjuryDetailsPayload","description":"\n\n
Validation Rules\n\n- If `NONE` is included in `ppe_items` it must be the only item in the array.\n\n
"},"FfInjuryDetailsResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"unit_neris_id":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}U[A-Z\\d]{3}$","examples":["FD24027334S001U003","FD24160543S010U001"]},{"type":"null"}],"title":"Unit Neris Id","description":"NERIS ID of the unit responding to the incident in FD Spec.","examples":["FD24027442S000U000","FD24027442S000U001"]},"reported_unit_id":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Reported Unit Id","description":"Name of the unit responding to the incident if neris id not yet in FD Spec.","examples":["A702"]},"unit_continuity":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Unit Continuity","description":"Continuity of company at the time of incident."},"incident_command":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Incident Command","description":"Whether an incident command structure was in place during incident."},"job_classification":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Classification","description":"Job classification of firefighter."},"duty_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Duty Type","description":"Duty state of the firefighter at the time of the incident."},"action_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action Type","description":"Actions that occurred during the time of the incident."},"incident_stage":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Incident Stage","description":"Stage of the incident when the injury occurred."},"ppe_items":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Ppe Items","description":"PPE worn during time of incident."}},"type":"object","required":["last_modified"],"title":"FfInjuryDetailsResponse"},"FfRescuePayload":{"properties":{"actions":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeRescueActionValue"},"type":"array"},{"type":"null"}],"title":"Actions","description":"Action taken to support the rescue.","x-ui-label":"Describe actions taken to support the rescue.","x-ui-hint":"Select all that apply."},"removal_or_nonremoval":{"oneOf":[{"$ref":"#/components/schemas/RemovalPayload"},{"$ref":"#/components/schemas/NonremovalPayload"}],"title":"Removal Or Nonremoval","discriminator":{"propertyName":"type","mapping":{"DISENTANGLEMENT":"#/components/schemas/NonremovalPayload","EXTRICATION":"#/components/schemas/NonremovalPayload","OTHER":"#/components/schemas/NonremovalPayload","RECOVERY":"#/components/schemas/NonremovalPayload","REMOVAL_FROM_STRUCTURE":"#/components/schemas/RemovalPayload"}}},"type":{"type":"string","enum":["RESCUED_BY_FIREFIGHTER","RESCUED_BY_FF_RIT","EVAC_ASSISTED_BY_FIREFIGHTER"],"title":"Type","x-ui-label":"Describe whether the person was rescued, evacuated, or if there was no rescue.","x-ui-hint":"Select one."},"impediments":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeRescueImpedimentValue"},"type":"array"},{"type":"null"}],"title":"Impediments","description":"Whether conditions impacted the ability of rescue.","x-ui-label":"Describe any conditions impacted the rescue.","x-ui-hint":"Select all that apply."}},"additionalProperties":false,"type":"object","required":["removal_or_nonremoval","type"],"title":"FfRescuePayload","description":"\n\n
Validation Rules\n\n- If `NONE` is included in `actions` it must be the only item in the array.\n- If `NONE` is included in `impediments` it must be the only item in the array.\n\n
"},"FfRescueResponse":{"properties":{"actions":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeRescueActionValue"},"type":"array"},{"type":"null"}],"title":"Actions","description":"Action taken to support the rescue."},"removal_or_nonremoval":{"oneOf":[{"$ref":"#/components/schemas/RemovalResponse"},{"$ref":"#/components/schemas/NonremovalResponse"}],"title":"Removal Or Nonremoval"},"type":{"type":"string","title":"Type"},"impediments":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Impediments","description":"Whether conditions impacted the ability of rescue."}},"type":"object","required":["removal_or_nonremoval","type"],"title":"FfRescueResponse"},"FinalizeForgotPasswordPayload":{"properties":{"code":{"type":"string","title":"Code","description":"Authorization code for the request"},"account_id":{"type":"string","format":"uuid","title":"Account Id","description":"User's account ID"},"password":{"type":"string","title":"Password","description":"The new password"}},"additionalProperties":false,"type":"object","required":["code","account_id","password"],"title":"FinalizeForgotPasswordPayload"},"FireAlarmNotPresentPayload":{"properties":{"type":{"type":"string","enum":["NOT_PRESENT","NOT_APPLICABLE"],"title":"Type","x-ui-label":"Describe whether the building was fitted with a whole building fire alarm system that includes fire detection devices, alarm pull stations, and alarm notification devices.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"FireAlarmNotPresentPayload"},"FireAlarmNotPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"}},"type":"object","required":["last_modified","type"],"title":"FireAlarmNotPresentResponse"},"FireAlarmPayload":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/FireAlarmPresentPayload"},{"$ref":"#/components/schemas/FireAlarmNotPresentPayload"}],"title":"Presence","discriminator":{"propertyName":"type","mapping":{"NOT_APPLICABLE":"#/components/schemas/FireAlarmNotPresentPayload","NOT_PRESENT":"#/components/schemas/FireAlarmNotPresentPayload","PRESENT":"#/components/schemas/FireAlarmPresentPayload"}}}},"additionalProperties":false,"type":"object","required":["presence"],"title":"FireAlarmPayload"},"FireAlarmPresentPayload":{"properties":{"type":{"type":"string","const":"PRESENT","title":"Type","x-ui-label":"Describe whether the building was fitted with a whole building fire alarm system that includes fire detection devices, alarm pull stations, and alarm notification devices.","x-ui-hint":"Select one."},"alarm_types":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeAlarmFireValue"},"type":"array"},{"type":"null"}],"title":"Alarm Types","x-ui-label":"Describe the type of fire alarm system present.","x-ui-hint":"Select one."},"operation_type":{"anyOf":[{"$ref":"#/components/schemas/TypeAlarmOperationValue"},{"type":"null"}],"x-ui-label":"Describe whether the fire alarm operated as intended during the incident.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"FireAlarmPresentPayload"},"FireAlarmPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"},"alarm_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Alarm Types"},"operation_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operation Type"}},"type":"object","required":["last_modified","type"],"title":"FireAlarmPresentResponse"},"FireAlarmResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"presence":{"oneOf":[{"$ref":"#/components/schemas/FireAlarmPresentResponse"},{"$ref":"#/components/schemas/FireAlarmNotPresentResponse"}],"title":"Presence"}},"type":"object","required":["last_modified","presence"],"title":"FireAlarmResponse"},"FireMarshalResponse":{"properties":{"mail_address_line_1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}]},"mail_zip_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mail Zip Code"},"address_line_1":{"type":"string","title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"type":"string","title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","title":"Zip Code"},"name":{"type":"string","title":"Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"},"neris_id":{"type":"string","title":"Neris Id"},"version":{"type":"integer","title":"Version"},"active":{"type":"boolean","title":"Active"},"website":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Website"},"jurisdiction_type":{"type":"string","title":"Jurisdiction Type"},"location":{"anyOf":[{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."},{"type":"null"}],"title":"Location"},"last_modified":{"type":"string","format":"date-time","title":"Last Modified"},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationResponse"},{"type":"null"}]}},"type":"object","required":["address_line_1","city","state","zip_code","name","neris_id","version","active","jurisdiction_type","last_modified"],"title":"FireMarshalResponse"},"FirePayload":{"properties":{"location_detail":{"oneOf":[{"$ref":"#/components/schemas/StructureFireLocationDetailPayload"},{"$ref":"#/components/schemas/OutsideFireLocationDetailPayload"}],"title":"Location Detail","discriminator":{"propertyName":"type","mapping":{"OUTSIDE":"#/components/schemas/OutsideFireLocationDetailPayload","STRUCTURE":"#/components/schemas/StructureFireLocationDetailPayload"}}},"water_supply":{"$ref":"#/components/schemas/TypeWaterSupplyValue","description":"Describes the type of water supply utilized for the incident.","x-ui-label":"Describes the type of water supply utilized for the incident.","x-ui-hint":"Select one."},"investigation_needed":{"$ref":"#/components/schemas/TypeFireInvestNeedValue","description":"Assessment by incident commander and/or officer in charge of the incident as to whether the fire necessitated a formal fire investigation.","x-ui-label":"Assessment by incident commander and/or officer in charge of the incident as to whether the fire necessitated a formal fire investigation.","x-ui-hint":"Select one."},"investigation_types":{"items":{"$ref":"#/components/schemas/TypeFireInvestValue"},"type":"array","title":"Investigation Types","description":"Categorizes the general type of investigation completed at the structure fire incident.","x-ui-label":"Categorizes the general type of investigation completed at the structure fire incident.","x-ui-hint":"Select all that apply."},"suppression_appliances":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeSuppressApplianceValue"},"type":"array"},{"type":"null"}],"title":"Suppression Appliances","description":"Describes the appliances used for suppression.","x-ui-label":"Describe the extinguishment tools used for fire suppression.","x-ui-hint":"Select all that apply."}},"additionalProperties":false,"type":"object","required":["location_detail","water_supply","investigation_needed","investigation_types"],"title":"FirePayload","description":"\n\n
Validation Rules\n\n- If `NONE` is included in `suppression_appliances` it must be the only item in the array.\n- If `NONE` is included in `investigation_types` it must be the only item in the array.\n\n
"},"FireRemovalPayload":{"properties":{"relative_suppression_time":{"anyOf":[{"$ref":"#/components/schemas/TypeSuppressTimeValue"},{"type":"null"}],"description":"The time, relative to suppression, when the person was rescued.","x-ui-label":"Relative to suppression, when was the person removed from the structure?","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","title":"FireRemovalPayload"},"FireRemovalResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"relative_suppression_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relative Suppression Time","description":"The time, relative to suppression, when the person was rescued."}},"type":"object","required":["last_modified"],"title":"FireRemovalResponse"},"FireResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"location_detail":{"anyOf":[{"$ref":"#/components/schemas/StructureFireLocationDetailResponse"},{"$ref":"#/components/schemas/OutsideFireLocationDetailResponse"}],"title":"Location Detail"},"water_supply":{"type":"string","title":"Water Supply","description":"Describes the type of water supply utilized for the incident."},"investigation_needed":{"type":"string","title":"Investigation Needed","description":"Assessment by incident commander and/or officer in charge of the incident as to whether the fire necessitated a formal fire investigation."},"investigation_types":{"items":{"type":"string"},"type":"array","title":"Investigation Types","description":"Categorizes the general type of investigation completed at the structure fire incident."},"suppression_appliances":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Suppression Appliances","description":"Describes the appliances used for suppression."}},"type":"object","required":["last_modified","location_detail","water_supply","investigation_needed","investigation_types"],"title":"FireResponse"},"FireSuppressionEffectivePayload":{"properties":{"sprinklers_activated":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sprinklers Activated","description":"Number of sprinkler heads activated.","examples":[1],"x-ui-label":"Describe the number of sprinkler heads activated."},"type":{"type":"string","const":"OPERATED_EFFECTIVE","title":"Type","x-ui-label":"Describe the operation of the fire suppression system.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"FireSuppressionEffectivePayload"},"FireSuppressionEffectiveResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"sprinklers_activated":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sprinklers Activated","description":"Number of sprinkler heads activated.","examples":[1]},"type":{"type":"string","title":"Type"}},"type":"object","required":["last_modified","type"],"title":"FireSuppressionEffectiveResponse"},"FireSuppressionFailedPayload":{"properties":{"type":{"type":"string","const":"NO_OPERATION","title":"Type","x-ui-label":"Describe the operation of the fire suppression system.","x-ui-hint":"Select one."},"failure_reason":{"anyOf":[{"$ref":"#/components/schemas/TypeSuppressNoOperationValue"},{"type":"null"}],"description":"Reason for suppression system failure.","x-ui-label":"Describe the reason for the failure to operate effectively.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"FireSuppressionFailedPayload"},"FireSuppressionFailedResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"},"failure_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Reason","description":"Reason for suppression system failure."}},"type":"object","required":["last_modified","type"],"title":"FireSuppressionFailedResponse"},"FireSuppressionIneffectivePayload":{"properties":{"sprinklers_activated":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sprinklers Activated","description":"Number of sprinkler heads activated.","examples":[1],"x-ui-label":"Describe the number of sprinkler heads activated."},"type":{"type":"string","const":"OPERATED_NOT_EFFECTIVE","title":"Type","x-ui-label":"Describe the operation of the fire suppression system.","x-ui-hint":"Select one."},"failure_reason":{"anyOf":[{"$ref":"#/components/schemas/TypeSuppressNoOperationValue"},{"type":"null"}],"description":"Reason for suppression system failure.","x-ui-label":"Describe the reason for the failure to operate effectively.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"FireSuppressionIneffectivePayload"},"FireSuppressionIneffectiveResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"sprinklers_activated":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sprinklers Activated","description":"Number of sprinkler heads activated.","examples":[1]},"type":{"type":"string","title":"Type"},"failure_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Reason","description":"Reason for suppression system failure."}},"type":"object","required":["last_modified","type"],"title":"FireSuppressionIneffectiveResponse"},"FireSuppressionNotPresentPayload":{"properties":{"type":{"type":"string","enum":["NOT_PRESENT","NOT_APPLICABLE"],"title":"Type","x-ui-label":"Describe whether there was a fire suppression (sprinkler) system in the building.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"FireSuppressionNotPresentPayload"},"FireSuppressionNotPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"}},"type":"object","required":["last_modified","type"],"title":"FireSuppressionNotPresentResponse"},"FireSuppressionOperationPayload":{"properties":{"effectiveness":{"oneOf":[{"$ref":"#/components/schemas/FireSuppressionEffectivePayload"},{"$ref":"#/components/schemas/FireSuppressionIneffectivePayload"},{"$ref":"#/components/schemas/FireSuppressionFailedPayload"}],"title":"Effectiveness","discriminator":{"propertyName":"type","mapping":{"NO_OPERATION":"#/components/schemas/FireSuppressionFailedPayload","OPERATED_EFFECTIVE":"#/components/schemas/FireSuppressionEffectivePayload","OPERATED_NOT_EFFECTIVE":"#/components/schemas/FireSuppressionIneffectivePayload"}}}},"additionalProperties":false,"type":"object","required":["effectiveness"],"title":"FireSuppressionOperationPayload"},"FireSuppressionOperationResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"effectiveness":{"oneOf":[{"$ref":"#/components/schemas/FireSuppressionEffectiveResponse"},{"$ref":"#/components/schemas/FireSuppressionIneffectiveResponse"},{"$ref":"#/components/schemas/FireSuppressionFailedResponse"}],"title":"Effectiveness"}},"type":"object","required":["last_modified","effectiveness"],"title":"FireSuppressionOperationResponse"},"FireSuppressionPayload":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/FireSuppressionPresentPayload"},{"$ref":"#/components/schemas/FireSuppressionNotPresentPayload"}],"title":"Presence","discriminator":{"propertyName":"type","mapping":{"NOT_APPLICABLE":"#/components/schemas/FireSuppressionNotPresentPayload","NOT_PRESENT":"#/components/schemas/FireSuppressionNotPresentPayload","PRESENT":"#/components/schemas/FireSuppressionPresentPayload"}}}},"additionalProperties":false,"type":"object","required":["presence"],"title":"FireSuppressionPayload"},"FireSuppressionPresentPayload":{"properties":{"type":{"type":"string","const":"PRESENT","title":"Type","x-ui-label":"Describe whether there was a fire suppression (sprinkler) system in the building.","x-ui-hint":"Select one."},"suppression_types":{"anyOf":[{"items":{"$ref":"#/components/schemas/FireSuppressionTypePayload"},"type":"array"},{"type":"null"}],"title":"Suppression Types"},"operation_type":{"anyOf":[{"$ref":"#/components/schemas/FireSuppressionOperationPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["type"],"title":"FireSuppressionPresentPayload"},"FireSuppressionPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"},"suppression_types":{"anyOf":[{"items":{"$ref":"#/components/schemas/FireSuppressionTypeResponse"},"type":"array"},{"type":"null"}],"title":"Suppression Types"},"operation_type":{"anyOf":[{"$ref":"#/components/schemas/FireSuppressionOperationResponse"},{"type":"null"}]}},"type":"object","required":["last_modified","type"],"title":"FireSuppressionPresentResponse"},"FireSuppressionResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"presence":{"oneOf":[{"$ref":"#/components/schemas/FireSuppressionPresentResponse"},{"$ref":"#/components/schemas/FireSuppressionNotPresentResponse"}],"title":"Presence"}},"type":"object","required":["last_modified","presence"],"title":"FireSuppressionResponse"},"FireSuppressionTypePayload":{"properties":{"type":{"$ref":"#/components/schemas/TypeSuppressFireValue","description":"Type of fire suppression system.","x-ui-label":"Describe the type of system present.","x-ui-hint":"Select one."},"full_partial":{"anyOf":[{"$ref":"#/components/schemas/TypeFullPartialValue"},{"type":"null"}],"description":"Whether the fire suppression system was full or partial.","x-ui-label":"Describe the coverage extent of the system.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"FireSuppressionTypePayload"},"FireSuppressionTypeResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type","description":"Type of fire suppression system."},"full_partial":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Partial","description":"Whether the fire suppression system was full or partial."}},"type":"object","required":["last_modified","type"],"title":"FireSuppressionTypeResponse"},"GeoJsonMultiPolygon":{"properties":{"type":{"type":"string","const":"MultiPolygon","title":"Type","description":"A literal type indicating the GeoJSON object type. Always \"MultiPolygon\" for this class.","default":"MultiPolygon","examples":["MultiPolygon"]},"coordinates":{"items":{"items":{"items":{"items":{"type":"number"},"type":"array","maxItems":2,"minItems":2},"type":"array","minItems":4},"type":"array"},"type":"array","title":"Coordinates"}},"additionalProperties":false,"type":"object","required":["coordinates"],"title":"GeoJsonMultiPolygon"},"GeoJsonPoint":{"properties":{"type":{"type":"string","const":"Point","title":"Type","description":"A literal type indicating the GeoJSON object type. Always \"Point\" for this class.","default":"Point","examples":["Point"]},"coordinates":{"items":{"type":"number"},"type":"array","maxItems":2,"minItems":2,"title":"Coordinates"}},"additionalProperties":false,"type":"object","required":["coordinates"],"title":"GeoJsonPoint"},"GeoMultipolygon":{"properties":{"crs":{"anyOf":[{"type":"integer"},{"type":"string"}],"title":"Crs","description":"EPSG code for the coordinate reference system","default":4326},"geometry":{"$ref":"#/components/schemas/GeoJsonMultiPolygon"}},"type":"object","required":["geometry"],"title":"GeoMultipolygon"},"GeoPoint":{"properties":{"crs":{"anyOf":[{"type":"integer"},{"type":"string"}],"title":"Crs","description":"EPSG code for the coordinate reference system","default":4326},"geometry":{"$ref":"#/components/schemas/GeoJsonPoint"}},"type":"object","required":["geometry"],"title":"GeoPoint"},"GeocodedLocationResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"place_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Place Type","description":"Type of feature identified by the address.","examples":["AIRPORT"]},"postal_community":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Postal Community","description":"City name for the ZIP Code of an address, as given in the USPS City State file.","examples":["Stanton (a post office name in KY)"]},"neighborhood_community":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Neighborhood Community","description":"Name of an unincorporated neighborhood, subdivision or area, either within an incorporated municipality, or in an unincorporated portion of a county or both, where the address is located. ","examples":["Cypress Meadows Subdivision"]},"unincorporated_community":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Unincorporated Community","description":"Name of an unincorporated community, either within an incorporated municipality or in an unincorporated portion of a county, or both, where the address is located. ","examples":["14th Ward (a ward in the incorporated municipality of Pittsburgh, PA) "]},"incorporated_municipality":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Incorporated Municipality","description":"Name of the incorporated municipality or other general-purpose local governmental unit (if any) where the address is located.","examples":["Haleyville"]},"county":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"County","description":"Name of the county or county-equivalent where the address is located. A county (or its equivalent) is the primary legal division of a state or territory.","examples":["Winston County"]},"state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"description":"Name of a state or state equivalent, represented by the two- letter UPPER CASE abbreviation given in USPS Publication 28 [15], Appendix B. A state is a primary governmental division of the United States.","examples":["VA"]},"postal_code":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Postal Code","description":"System of 5-digit codes that identifies the individual USPS Post Office or metropolitan area delivery station associated with an address.","examples":["02109 (one of many ZIP Codes for Boston, MA)"]},"postal_code_extension":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Postal Code Extension","description":"System of 4-digit codes that are used after the 5-digit ZIP Code to specify a range of USPS delivery addresses.","examples":["\"0001\" in \"02109-0001\" (portion of a 02109 ZIP Code)."]},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country","description":"Name of a country represented by its two-letter ISO 3166-1[14] English country alpha-2 code elements in UPPER CASE letters.","examples":["US"]},"street_prefix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Modifier","description":"Word or phrase that precedes and modifies the Street Name element but is separated from it by a Street Name Pre Type or a Street Name Pre Directional or both.","examples":["\"Old\" in \"Old North First Street\" (\"Old\" is a Street Name Pre Modifier because the Street Name Pre Directional \"North\" separates \"Old\" from the Street Name \"First\" and the Street Name Post Type \"Street\".)"]},"street_prefix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Direction","description":"Word preceding the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"North\" in \"North Fairfax Drive\""]},"street":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street","description":"Element of the complete street name that identifies the particular street (as opposed to any street types, directionals, and modifiers).","examples":["\"Fairfax\" in \"North Fairfax Avenue\""]},"street_postfix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Direction","description":"Word following the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"East\" in \"Seventh Street East\""]},"street_postfix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Modifier","description":"Word or phrase that follows and modifies the Street Name element and is either separated from it by a Street Name Post Type and/or a Street Name Post Directional.","examples":["\"Extension\" in \"Market Street North Extension\" (because \"North\" separates \"Extension\" from the Street Name Post Type."]},"street_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Street Prefix","description":"Word or phrase that precedes the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"Avenue A\""]},"street_preposition_type_separator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Street Preposition Type Separator","description":"Preposition or prepositional phrase between the Street Name Pre Type and the Street Name.","examples":["\"of the\" in \"Avenue of the Americas\""]},"street_postfix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Street Postfix","description":"Word or phrase that follows the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"North Fairfax Avenue\""]},"direction_of_travel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Direction Of Travel","description":"Word which follows all other street name elements and is used only as needed to indicate direction of travel on a divided roadway and associated frontage roads.","examples":["\"northbound\" in \"Interstate Highway 5 North northbound\""]},"number_prefix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Prefix","description":"Identifier in the portion of the complete address number that precedes the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"A\" in \"A19 Calle 117, Toa Alta, PR\""]},"number_suffix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Suffix","description":"Identifier in the portion of the complete address number that follows the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"1\"2\" in \"194-03 1\"2 50th Avenue, New York, NY 11365\" "]},"complete_number":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Complete Number","description":"Address Number Complete includes the Address Number Prefix (if any), the Address Number, Address Number Suffix (if any), and any formatting or separator characters needed to display the official version of the complete address number. The Address Number Complete precedes the complete street name to identify a location along a thoroughfare or within a defined area. ","examples":["\"A19\" in \"A19 Calle 117, Toa Alta, PR\""]},"distance_marker":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Distance Marker","description":"Distance travelled along a route such as a road or highway, indicated by a distance marker sign, typically a post or other marker indicating the distance in miles/kilometers from or to a given point. ","examples":["\"Milepost 1303\" in \"Milepost 1303, Alaska Highway\""]},"number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Number","description":"Integer identifier of a location along a thoroughfare or within a defined community.","examples":["\"123\" in \"123 Main Street\""]},"structure":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Structure","description":"A built feature which has a vertical dimension, including both conventional buildings which have walls, doors, and a roof, and other kinds of infrastructure such as cell towers, transformer stations, fuel tanks, and so on. ","examples":["\"Fuel Storage Shed\" in \"100 Cantwell Avenue, Fuel Storage Shed\""]},"subsite":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Subsite","description":"Name of a sub-area within a larger area specified either by site name, by a thoroughfare address, or both.","examples":["\"Buckeye Village\" in \"The Ohio State University, Buckeye Village\""]},"site":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Site","description":"Name of an exterior area which is publicly known and unique within a given place. A site may contain one or more structures and/or sub-sites.","examples":["\"Winslow Park and Campground\" in \"Winslow Park and Campground, Freeport, ME\" "]},"wing":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Wing","description":"Designated part of a structure which spans one or many floors, typically including more than one unit or room and representing a significant portion of the structure floor area. ","examples":["\"Concourse A\" in \"Pittsburgh International Airport, Airside Terminal, Concourse A\" "]},"floor":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Floor","description":"Standardized identifier for a story or level within a structure, wing, or unit.","examples":["\"5\" in \"800 Jefferson Street, 5th Floor\" "]},"unit_prefix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Unit Prefix","description":"Part of the complete unit identifier that precedes the Unit Value and indicates the kind of unit.","examples":["\"Apartment\" in \"422 Via Casitas, Apartment 12\" "]},"unit_value":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Unit Value","description":"Part of the complete unit identifier that uniquely identifies a particular unit.","examples":["\"12\" in \"422 Via Casitas, Apartment 12\" "]},"room":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Room","description":"Single, distinctly identified, enclosed space within a structure. ","examples":["\"E427\" in \"Gilman Memorial Hospital, Room E247\" "]},"section":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Section","description":"Identified, unenclosed area within a structure, wing, unit, or room. ","examples":["\"Customer Seating\" in \"Coastland Mall, Food Court, Customer Seating\" "]},"row":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Row","description":"Identified linear feature, such as a linear arrangement of seats, workstations, equipment, or storage, within a structure, wing, unit, or room. ","examples":["\"Aisle 4\" in \"Warehouse 3, Aisle 4\""]},"seat":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Seat","description":"Identified seat, desk, workstation, or similar precise location within a structure, wing, unit, room, section, or row.","examples":["\"Cubicle A-7\" in \"1 Ashburton Place, Room 819, Cubicle A-7\""]},"additional_info":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Additional Info","description":"Information that relates to location but does not meet the definition of any other named location elements.","examples":["Main Loading Dock"]},"marker":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Marker","description":"Uniquely identified and indivisible infrastructure component, smaller than a structure, which exists either within a structure or exterior to any structure, such as an alarm box, a utility pole, a callbox, or other similar feature. ","examples":["Call Box CC-680-118 (an emergency call box on Interstate 680 in Contra Costa County, CA)"]},"score":{"type":"number","title":"Score","description":"Measure of confidence in the geocoded result"},"point":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"title":"Point","description":"The geometry data from pygeoapi."}},"type":"object","required":["score","point"],"title":"GeocodedLocationResponse"},"GrantType":{"type":"string","enum":["client_credentials","refresh_token","password","email_otp","software_token_mfa","new_password_required"],"title":"GrantType"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HazsitPayload":{"properties":{"evacuated":{"type":"integer","title":"Evacuated","description":"Number of occupants/businesses evacuated during the incident response.","examples":[2],"x-ui-label":"Estimated number of occupants/businesses evacuated as part of this incident response?"},"chemicals":{"anyOf":[{"items":{"$ref":"#/components/schemas/ChemicalPayload"},"type":"array"},{"type":"null"}],"title":"Chemicals"},"disposition":{"$ref":"#/components/schemas/TypeHazardDispositionValue","description":"Outcome of the hazmat incident.","x-ui-label":"The outcome of the hazardous situation incident."}},"additionalProperties":false,"type":"object","required":["evacuated","disposition"],"title":"HazsitPayload"},"HazsitResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"evacuated":{"type":"integer","title":"Evacuated","description":"Number of occupants/businesses evacuated during the incident response.","examples":[2]},"chemicals":{"anyOf":[{"items":{"$ref":"#/components/schemas/ChemicalResponse"},"type":"array"},{"type":"null"}],"title":"Chemicals"},"disposition":{"type":"string","title":"Disposition","description":"Outcome of the hazmat incident."}},"type":"object","required":["last_modified","evacuated","disposition"],"title":"HazsitResponse"},"HazsitWithReleaseFactorsPayload":{"properties":{"evacuated":{"type":"integer","title":"Evacuated","description":"Number of occupants/businesses evacuated during the incident response.","examples":[2],"x-ui-label":"Number of occupants/businesses evacuated during the incident.","x-ui-hint":"Enter 0 if none."},"chemicals":{"anyOf":[{"items":{"$ref":"#/components/schemas/ChemicalWithReleaseFactorsPayload"},"type":"array"},{"type":"null"}],"title":"Chemicals"},"disposition":{"$ref":"#/components/schemas/TypeHazardDispositionValue","description":"Outcome of the hazmat incident.","x-ui-label":"Select the outcome of the incident."}},"additionalProperties":false,"type":"object","required":["evacuated","disposition"],"title":"HazsitWithReleaseFactorsPayload"},"HazsitWithReleaseFactorsResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"evacuated":{"type":"integer","title":"Evacuated","description":"Number of occupants/businesses evacuated during the incident response.","examples":[2]},"chemicals":{"anyOf":[{"items":{"$ref":"#/components/schemas/ChemicalWithReleaseFactorsResponse"},"type":"array"},{"type":"null"}],"title":"Chemicals"},"disposition":{"type":"string","title":"Disposition","description":"Outcome of the hazmat incident."}},"type":"object","required":["last_modified","evacuated","disposition"],"title":"HazsitWithReleaseFactorsResponse"},"HighPrecisionGeoJsonMultiPolygon":{"properties":{"type":{"type":"string","const":"MultiPolygon","title":"Type","description":"A literal type indicating the GeoJSON object type. Always \"MultiPolygon\" for this class.","default":"MultiPolygon","examples":["MultiPolygon"]},"coordinates":{"items":{"items":{"items":{"items":{"type":"number"},"type":"array","maxItems":2,"minItems":2},"type":"array","minItems":4},"type":"array"},"type":"array","title":"Coordinates"}},"additionalProperties":false,"type":"object","required":["coordinates"],"title":"HighPrecisionGeoJsonMultiPolygon"},"HighPrecisionGeoMultipolygon":{"properties":{"crs":{"anyOf":[{"type":"integer"},{"type":"string"}],"title":"Crs","description":"EPSG code for the coordinate reference system","default":4326},"geometry":{"$ref":"#/components/schemas/HighPrecisionGeoJsonMultiPolygon"}},"type":"object","required":["geometry"],"title":"HighPrecisionGeoMultipolygon"},"IncidentAnalysisBasePayload":{"properties":{"neris_id_incident":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Neris Id Incident","description":"The NERIS ID for the incident this incident analysis is associated with.","examples":["FD24027000|abc123xyz|1729023498"],"x-ui-label":"Incident Number","x-ui-hint":"Replace if the incident already has an identification number associated with it."},"incident_number":{"type":"string","pattern":"[\\w\\-\\:]+","title":"Incident Number","description":"The incident's internal ID for the incident analysis.","examples":["INA20260703TV"]},"narrative":{"anyOf":[{"type":"string","maxLength":100000,"minLength":1},{"type":"null"}],"title":"Narrative","description":"The narrative of the incident analysis.","x-ui-label":"Please provide a description of findings and any other information pertinent to the analysis.","x-ui-hint":"Please take care to avoid entering any personal identifying information."},"medical_response":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Medical Response","description":"Whether the incident required a medical response or intervention.","x-ui-label":"Did the incident require a medical response or intervention?"},"transport_impacted":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Transport Impacted","description":"Whether a major transportation artery or facility was closed in response to the incident.","x-ui-label":"Describe whether a major transportation artery or facility was closed in response to the incident."},"human_factors":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeHumanFactorsValue"},"type":"array"},{"type":"null"}],"title":"Human Factors","description":"The human factors that contributed to the incident.","x-ui-label":"Select any human factors that may have contributed to the incident."}},"additionalProperties":false,"type":"object","required":["neris_id_incident","incident_number"],"title":"IncidentAnalysisBasePayload"},"IncidentAnalysisBaseResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"neris_id_incident":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Neris Id Incident","description":"The NERIS ID for the incident this incident analysis is associated with.","examples":["FD24027000|abc123xyz|1729023498"]},"incident_number":{"type":"string","pattern":"[\\w\\-\\:]+","title":"Incident Number","description":"The incident's internal ID for the incident analysis.","examples":["INA20260703TV"]},"narrative":{"anyOf":[{"type":"string","maxLength":100000,"minLength":1},{"type":"null"}],"title":"Narrative","description":"The narrative of the incident analysis."},"medical_response":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Medical Response","description":"Whether the incident required a medical response or intervention."},"transport_impacted":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Transport Impacted","description":"Whether a major transportation artery or facility was closed in response to the incident."},"human_factors":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Human Factors","description":"The human factors that contributed to the incident."}},"type":"object","required":["last_modified","neris_id_incident","incident_number"],"title":"IncidentAnalysisBaseResponse"},"IncidentAnalysisCreatedResponse":{"properties":{"neris_id":{"type":"string","pattern":"^IA\\|FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Neris Id","description":"NERIS ID of the incident analysis. This is comprised of \"IA\", the department NERIS ID, the incident's internal ID, and the epoch time of when the record was created.","examples":["IA|FD12345678|abc123xyz|1729023498","IA|FD24027334|incident42:f-tiw|1729023498","IA|FD12345678|0457812893477|1729023498"]},"incident_analysis_status":{"$ref":"#/components/schemas/IncidentAnalysisStatusResponse"}},"type":"object","required":["neris_id","incident_analysis_status"],"title":"IncidentAnalysisCreatedResponse","description":"For when an incident analysis is created in the landing zone"},"IncidentAnalysisPayload":{"properties":{"base":{"$ref":"#/components/schemas/IncidentAnalysisBasePayload"},"hazsit":{"anyOf":[{"$ref":"#/components/schemas/HazsitWithReleaseFactorsPayload"},{"type":"null"}],"description":"The hazmat/hazsit analysis for the incident."},"products":{"anyOf":[{"items":{"$ref":"#/components/schemas/ProductPayload"},"type":"array"},{"type":"null"}],"title":"Products","description":"The products involved in the incident."},"batteries":{"anyOf":[{"items":{"$ref":"#/components/schemas/BatteryPayload"},"type":"array"},{"type":"null"}],"title":"Batteries","description":"The batteries involved in the incident."},"outside_fire":{"anyOf":[{"$ref":"#/components/schemas/OutsideFirePayload","description":"Analysis for how the fire affected outdoor spaces."},{"type":"null"}]},"structure_fire_origin":{"anyOf":[{"$ref":"#/components/schemas/StructureFireOriginPayload","description":"Analysis for how the fire started if it started in a structure."},{"type":"null"}]},"properties":{"anyOf":[{"items":{"$ref":"#/components/schemas/PropertyAnalysisPayload"},"type":"array"},{"type":"null"}],"title":"Properties","description":"Analysis for the property/structures that were affected by the incident. A property must have a `parcel_id`, `location`, or `point` so that the property can be placed on a map."},"vehicles":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/VehicleAutomobilePayload"},{"$ref":"#/components/schemas/VehicleOtherPayload"}],"discriminator":{"propertyName":"type","mapping":{"AERIAL_LIFT_PLATFORM":"#/components/schemas/VehicleOtherPayload","AERIAL_OTHER":"#/components/schemas/VehicleOtherPayload","AIRPLANE":"#/components/schemas/VehicleOtherPayload","ATV":"#/components/schemas/VehicleOtherPayload","AUTOMOBILE":"#/components/schemas/VehicleAutomobilePayload","BICYCLE":"#/components/schemas/VehicleOtherPayload","BOAT_SHIP":"#/components/schemas/VehicleOtherPayload","BUS":"#/components/schemas/VehicleOtherPayload","CARGO":"#/components/schemas/VehicleOtherPayload","CONSTRUCTION":"#/components/schemas/VehicleOtherPayload","FARM":"#/components/schemas/VehicleOtherPayload","FOOD_TRUCK":"#/components/schemas/VehicleOtherPayload","FREIGHT":"#/components/schemas/VehicleOtherPayload","HELICOPTER":"#/components/schemas/VehicleOtherPayload","JET_SKI":"#/components/schemas/VehicleOtherPayload","MILITARY_OTHER":"#/components/schemas/VehicleOtherPayload","MOTORCYCLE":"#/components/schemas/VehicleOtherPayload","MOTORIZED_HOME":"#/components/schemas/VehicleOtherPayload","OTHER":"#/components/schemas/VehicleOtherPayload","RAIL":"#/components/schemas/VehicleOtherPayload","SCOOTER_MOPED":"#/components/schemas/VehicleOtherPayload","SPORT_UTILITY":"#/components/schemas/VehicleOtherPayload","TOW_TRUCK":"#/components/schemas/VehicleOtherPayload","TRAILER":"#/components/schemas/VehicleOtherPayload","UTILITY_VEHICLE":"#/components/schemas/VehicleOtherPayload","VAN":"#/components/schemas/VehicleOtherPayload"}}},"type":"array"},{"type":"null"}],"title":"Vehicles","description":"The vehicles involved in the incident."}},"additionalProperties":false,"type":"object","required":["base"],"title":"IncidentAnalysisPayload","description":"\n\n
Validation Rules\n\n- At least one of `structure_fire_origin`, `outside_fire`, `hazsit`, `products`, `batteries`, `properties`, or `vehicles` must be provided.\n- If one structure is the ignition source, `structure_fire_origin` must also be provided.\n- `structure_fire_origin` must have exactly one structure with `ignition_source` set.\n- If `outside_fire.ignition_point` is set, the fire cannot have started in a structure.\n- Only one product can have `item_first_ignited` set.\n\n
"},"IncidentAnalysisResponse":{"properties":{"neris_id":{"type":"string","pattern":"^IA\\|FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Neris Id","description":"NERIS ID of the incident analysis. This is comprised of \"IA\", the department NERIS ID, the incident's internal ID, and the epoch time of when the record was created.","examples":["IA|FD12345678|abc123xyz|1729023498","IA|FD24027334|incident42:f-tiw|1729023498","IA|FD12345678|0457812893477|1729023498"]},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"department":{"anyOf":[{"$ref":"#/components/schemas/neris__api__incident_analysis__models__DepartmentTimeZoneResponse"},{"type":"null"}]},"base":{"$ref":"#/components/schemas/IncidentAnalysisBaseResponse"},"incident_analysis_status":{"$ref":"#/components/schemas/IncidentAnalysisStatusResponse"},"hazsit":{"anyOf":[{"$ref":"#/components/schemas/HazsitWithReleaseFactorsResponse"},{"type":"null"}],"description":"The hazmat/hazsit analysis for the incident."},"products":{"anyOf":[{"items":{"$ref":"#/components/schemas/ProductResponse"},"type":"array"},{"type":"null"}],"title":"Products","description":"The products involved in the incident."},"batteries":{"anyOf":[{"items":{"$ref":"#/components/schemas/BatteryResponse"},"type":"array"},{"type":"null"}],"title":"Batteries","description":"The batteries involved in the incident."},"outside_fire":{"anyOf":[{"$ref":"#/components/schemas/OutsideFireResponse","description":"Analysis for how the fire affected outdoor spaces."},{"type":"null"}]},"structure_fire_origin":{"anyOf":[{"$ref":"#/components/schemas/StructureFireOriginResponse","description":"Analysis for how the fire started if it started in a structure."},{"type":"null"}]},"properties":{"anyOf":[{"items":{"$ref":"#/components/schemas/PropertyAnalysisResponse"},"type":"array"},{"type":"null"}],"title":"Properties","description":"Analysis for the property/structures that were affected by the incident."},"vehicles":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/VehicleAutomobileResponse"},{"$ref":"#/components/schemas/VehicleOtherResponse"}],"discriminator":{"propertyName":"type","mapping":{"AERIAL_LIFT_PLATFORM":"#/components/schemas/VehicleOtherResponse","AERIAL_OTHER":"#/components/schemas/VehicleOtherResponse","AIRPLANE":"#/components/schemas/VehicleOtherResponse","ATV":"#/components/schemas/VehicleOtherResponse","AUTOMOBILE":"#/components/schemas/VehicleAutomobileResponse","BICYCLE":"#/components/schemas/VehicleOtherResponse","BOAT_SHIP":"#/components/schemas/VehicleOtherResponse","BUS":"#/components/schemas/VehicleOtherResponse","CARGO":"#/components/schemas/VehicleOtherResponse","CONSTRUCTION":"#/components/schemas/VehicleOtherResponse","FARM":"#/components/schemas/VehicleOtherResponse","FOOD_TRUCK":"#/components/schemas/VehicleOtherResponse","FREIGHT":"#/components/schemas/VehicleOtherResponse","HELICOPTER":"#/components/schemas/VehicleOtherResponse","JET_SKI":"#/components/schemas/VehicleOtherResponse","MILITARY_OTHER":"#/components/schemas/VehicleOtherResponse","MOTORCYCLE":"#/components/schemas/VehicleOtherResponse","MOTORIZED_HOME":"#/components/schemas/VehicleOtherResponse","OTHER":"#/components/schemas/VehicleOtherResponse","RAIL":"#/components/schemas/VehicleOtherResponse","SCOOTER_MOPED":"#/components/schemas/VehicleOtherResponse","SPORT_UTILITY":"#/components/schemas/VehicleOtherResponse","TOW_TRUCK":"#/components/schemas/VehicleOtherResponse","TRAILER":"#/components/schemas/VehicleOtherResponse","UTILITY_VEHICLE":"#/components/schemas/VehicleOtherResponse","VAN":"#/components/schemas/VehicleOtherResponse"}}},"type":"array"},{"type":"null"}],"title":"Vehicles","description":"The vehicles involved in the incident."}},"type":"object","required":["neris_id","last_modified","base","incident_analysis_status"],"title":"IncidentAnalysisResponse"},"IncidentAnalysisSortBy":{"type":"string","enum":["neris_id_entity","last_modified"],"title":"IncidentAnalysisSortBy"},"IncidentAnalysisStatusPayload":{"properties":{"status":{"$ref":"#/components/schemas/TypeIncidentAnalysisStatusPayloadValue","description":"Status of the incident analysis in the administrative approval process."}},"additionalProperties":false,"type":"object","required":["status"],"title":"IncidentAnalysisStatusPayload"},"IncidentAnalysisStatusResponse":{"properties":{"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"status":{"type":"string","enum":["APPROVED","DELETED","FAILED","PENDING_APPROVAL","REJECTED","SUBMITTED"],"title":"Status","description":"Status of the incident analysis in the administrative approval process."},"created_by":{"type":"string","format":"uuid","title":"Created By","description":"That account that created or last modified the incident analysis record."}},"type":"object","required":["last_modified","status","created_by"],"title":"IncidentAnalysisStatusResponse"},"IncidentAnalysisUpdatedResponse":{"properties":{"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]}},"type":"object","required":["last_modified"],"title":"IncidentAnalysisUpdatedResponse"},"IncidentBasePayload":{"properties":{"people_present":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"People Present","description":"Whether people were present at the time of the incident.","x-ui-label":"Describe whether there were people present."},"animals_rescued":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Animals Rescued","description":"Total number of animals rescued.","examples":[1],"x-ui-label":"Number of animals rescued during the incident."},"displacement_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Displacement Count","description":" Number of people/businesses displaced.","x-ui-label":"Total number of occupants/businesses that were displaced from the primary incident location."},"department_neris_id":{"type":"string","pattern":"^FD\\d{8}$","title":"Department Neris Id","description":"NERIS ID of the reporting Fire Department.","examples":["FD24027000"],"x-ui-label":"NERIS ID"},"incident_number":{"type":"string","pattern":"[\\w\\-\\:]+","title":"Incident Number","description":"The department's internal ID for the incident.","examples":["IN20240703HAWV"]},"displacement_causes":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeDisplaceCauseValueRelIncident"},"type":"array"},{"type":"null"}],"title":"Displacement Causes","description":"Causes of displacement.","x-ui-label":"Apparent cause of the displacement(s)."},"impediment_narrative":{"anyOf":[{"type":"string","maxLength":100000,"minLength":1},{"type":"null"}],"title":"Impediment Narrative","description":"Description of any obstacles that impacted the incident.","x-ui-label":"Please provide a description of any obstacles that impacted the incident.","x-ui-hint":"Please take care to avoid entering any personal identifying information."},"outcome_narrative":{"anyOf":[{"type":"string","maxLength":100000,"minLength":1},{"type":"null"}],"title":"Outcome Narrative","description":"Description of the final disposition of the incident.","x-ui-label":"Please provide a description of the final disposition of the incident.","x-ui-hint":"Please take care to avoid entering any personal identifying information."},"point":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]},"polygon":{"anyOf":[{"$ref":"#/components/schemas/HighPrecisionGeoMultipolygon"},{"type":"null"}]},"location":{"$ref":"#/components/schemas/LocationPayload"},"location_use":{"anyOf":[{"$ref":"#/components/schemas/LocationUsePayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["department_neris_id","incident_number","location"],"title":"IncidentBasePayload"},"IncidentBaseResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"people_present":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"People Present","description":"Whether people were present at the time of the incident."},"animals_rescued":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Animals Rescued","description":"Total number of animals rescued.","examples":[1]},"displacement_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Displacement Count","description":" Number of people/businesses displaced."},"department_neris_id":{"type":"string","pattern":"^FD\\d{8}$","title":"Department Neris Id","description":"NERIS ID of the reporting Fire Department.","examples":["FD24027000"]},"incident_number":{"type":"string","pattern":"[\\w\\-\\:]+","title":"Incident Number","description":"The department's internal ID for the incident.","examples":["IN20240703HAWV"]},"displacement_causes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Displacement Causes","description":"Causes of displacement."},"impediment_narrative":{"anyOf":[{"type":"string","maxLength":101000,"minLength":1},{"type":"null"}],"title":"Impediment Narrative","description":"Description of any obstacles that impacted the incident."},"outcome_narrative":{"anyOf":[{"type":"string","maxLength":101000,"minLength":1},{"type":"null"}],"title":"Outcome Narrative","description":"Description of the final disposition of the incident."},"location":{"$ref":"#/components/schemas/LocationResponse"},"location_use":{"anyOf":[{"$ref":"#/components/schemas/LocationUseResponse"},{"type":"null"}]},"point":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Point"},"polygon":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Polygon"}},"type":"object","required":["last_modified","department_neris_id","incident_number","location"],"title":"IncidentBaseResponse"},"IncidentCountMetricsResponse":{"properties":{"year_to_date":{"type":"integer","minimum":0.0,"title":"Year To Date","description":"Number of incidents year-to-date"},"month_to_date":{"type":"integer","minimum":0.0,"title":"Month To Date","description":"Number of incidents month-to-date"},"previous_24_hrs":{"type":"integer","minimum":0.0,"title":"Previous 24 Hrs","description":"Number of incidents in the past 24 hours"},"total":{"type":"integer","minimum":0.0,"title":"Total","description":"Total number of incidents in NERIS"}},"type":"object","required":["year_to_date","month_to_date","previous_24_hrs","total"],"title":"IncidentCountMetricsResponse"},"IncidentCreatedResponse":{"properties":{"neris_id":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Neris Id","description":"NERIS ID of the incident. This is comprised of the department NERIS ID concatenated with `dispatch.incident_number` and the epoch time of `dispatch.call_create` in the record as initially submitted. An incident's NERIS_ID is immutable, and so should not be relied upon for extraction of `incident_number` or `call_create`, as these attributes could have been updated.","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"]},"incident_status":{"$ref":"#/components/schemas/IncidentStatusResponse"}},"type":"object","required":["neris_id","incident_status"],"title":"IncidentCreatedResponse","description":"For when an incident is created in the landing zzone"},"IncidentFilterType":{"type":"string","enum":["FIRE||OUTSIDE_FIRE||CONSTRUCTION_WASTE","FIRE||OUTSIDE_FIRE||DUMPSTER_OUTDOOR_CONTAINER_FIRE","FIRE||OUTSIDE_FIRE||OTHER_OUTSIDE_FIRE","FIRE||OUTSIDE_FIRE||OUTSIDE_TANK_FIRE","FIRE||OUTSIDE_FIRE||TRASH_RUBBISH_FIRE","FIRE||OUTSIDE_FIRE||UTILITY_INFRASTRUCTURE_FIRE","FIRE||OUTSIDE_FIRE||VEGETATION_GRASS_FIRE","FIRE||OUTSIDE_FIRE||WILDFIRE_URBAN_INTERFACE","FIRE||OUTSIDE_FIRE||WILDFIRE_WILDLAND","FIRE||SPECIAL_FIRE||ESS_FIRE","FIRE||SPECIAL_FIRE||EXPLOSION","FIRE||SPECIAL_FIRE||INFRASTRUCTURE_FIRE","FIRE||STRUCTURE_FIRE||CHIMNEY_FIRE","FIRE||STRUCTURE_FIRE||CONFINED_COOKING_APPLIANCE_FIRE","FIRE||STRUCTURE_FIRE||ROOM_AND_CONTENTS_FIRE","FIRE||STRUCTURE_FIRE||STRUCTURAL_INVOLVEMENT_FIRE","FIRE||TRANSPORTATION_FIRE||AIRCRAFT_FIRE","FIRE||TRANSPORTATION_FIRE||BOAT_PERSONAL_WATERCRAFT_BARGE_FIRE","FIRE||TRANSPORTATION_FIRE||POWERED_MOBILITY_DEVICE_FIRE","FIRE||TRANSPORTATION_FIRE||TRAIN_RAIL_FIRE","FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_COMMERCIAL","FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_FOOD_TRUCK","FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_PASSENGER","FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_RV","HAZSIT||HAZARDOUS_MATERIALS||BIOLOGICAL_RELEASE_INCIDENT","HAZSIT||HAZARDOUS_MATERIALS||CARBON_MONOXIDE_RELEASE","HAZSIT||HAZARDOUS_MATERIALS||FUEL_SPILL_ODOR","HAZSIT||HAZARDOUS_MATERIALS||GAS_LEAK_ODOR","HAZSIT||HAZARDOUS_MATERIALS||HAZMAT_RELEASE_FACILITY","HAZSIT||HAZARDOUS_MATERIALS||HAZMAT_RELEASE_TRANSPORT","HAZSIT||HAZARDOUS_MATERIALS||RADIOACTIVE_RELEASE_INCIDENT","HAZSIT||HAZARD_NONCHEM||BOMB_THREAT_RESPONSE_SUSPICIOUS_PACKAGE","HAZSIT||HAZARD_NONCHEM||ELEC_HAZARD_SHORT_CIRCUIT","HAZSIT||HAZARD_NONCHEM||ELEC_POWER_LINE_DOWN_ARCHING_MALFUNC","HAZSIT||HAZARD_NONCHEM||MOTOR_VEHICLE_COLLISION","HAZSIT||INVESTIGATION||ODOR","HAZSIT||INVESTIGATION||SMOKE_INVESTIGATION","HAZSIT||OVERPRESSURE||NO_RUPTURE","HAZSIT||OVERPRESSURE||RUPTURE_WITHOUT_FIRE","LAWENFORCE","MEDICAL||ILLNESS","MEDICAL||ILLNESS||ABDOMINAL_PAIN","MEDICAL||ILLNESS||ALLERGIC_REACTION_STINGS","MEDICAL||ILLNESS||ALTERED_MENTAL_STATUS","MEDICAL||ILLNESS||BACK_PAIN_NON_TRAUMA","MEDICAL||ILLNESS||BREATHING_PROBLEMS","MEDICAL||ILLNESS||CARDIAC_ARREST","MEDICAL||ILLNESS||CHEST_PAIN_NON_TRAUMA","MEDICAL||ILLNESS||CONVULSIONS_SEIZURES","MEDICAL||ILLNESS||DIABETIC_PROBLEMS","MEDICAL||ILLNESS||HEADACHE","MEDICAL||ILLNESS||HEART_PROBLEMS","MEDICAL||ILLNESS||NAUSEA_VOMITING","MEDICAL||ILLNESS||NO_APPROPRIATE_CHOICE","MEDICAL||ILLNESS||OVERDOSE","MEDICAL||ILLNESS||PANDEMIC_EPIDEMIC_OUTBREAK","MEDICAL||ILLNESS||PREGNANCY_CHILDBIRTH","MEDICAL||ILLNESS||PSYCHOLOGICAL_BEHAVIOR_ISSUES","MEDICAL||ILLNESS||SICK_CASE","MEDICAL||ILLNESS||STROKE_CVA","MEDICAL||ILLNESS||UNCONSCIOUS_VICTIM","MEDICAL||ILLNESS||UNKNOWN_PROBLEM","MEDICAL||ILLNESS||WELL_PERSON_CHECK","MEDICAL||INJURY","MEDICAL||INJURY||ANIMAL_BITES","MEDICAL||INJURY||ASSAULT","MEDICAL||INJURY||BURNS_EXPLOSION","MEDICAL||INJURY||CARBON_MONOXIDE_OTHER_INHALATION_INJURY","MEDICAL||INJURY||CHOKING","MEDICAL||INJURY||DROWNING_DIVING_SCUBA_ACCIDENT","MEDICAL||INJURY||ELECTROCUTION","MEDICAL||INJURY||EYE_TRAUMA","MEDICAL||INJURY||FALL","MEDICAL||INJURY||GUNSHOT_WOUND","MEDICAL||INJURY||HEAT_COLD_EXPOSURE","MEDICAL||INJURY||HEMORRHAGE_LACERATION","MEDICAL||INJURY||INDUSTRIAL_INACCESSIBLE_ENTRAPMENT","MEDICAL||INJURY||MOTOR_VEHICLE_COLLISION","MEDICAL||INJURY||OTHER_TRAUMATIC_INJURY","MEDICAL||INJURY||POISONING","MEDICAL||INJURY||STAB_PENETRATING_TRAUMA","MEDICAL||OTHER||AIRMEDICAL_TRANSPORT","MEDICAL||OTHER||COMMUNITY_PUBLIC_HEALTH","MEDICAL||OTHER||HEALTHCARE_PROFESSIONAL_ADMISSION","MEDICAL||OTHER||INTERCEPT_OTHER_UNIT","MEDICAL||OTHER||MEDICAL_ALARM","MEDICAL||OTHER||STANDBY_REQUEST","MEDICAL||OTHER||TRANSFER_INTERFACILITY","NOEMERG||CANCELLED","NOEMERG||FALSE_ALARM||ACCIDENTAL_ALARM","NOEMERG||FALSE_ALARM||BOMB_SCARE","NOEMERG||FALSE_ALARM||INTENTIONAL_FALSE_ALARM","NOEMERG||FALSE_ALARM||MALFUNCTIONING_ALARM","NOEMERG||FALSE_ALARM||OTHER_FALSE_CALL","NOEMERG||GOOD_INTENT||CONTROLLED_BURNING_AUTHORIZED","NOEMERG||GOOD_INTENT||INVESTIGATE_HAZARDOUS_RELEASE","NOEMERG||GOOD_INTENT||NO_INCIDENT_FOUND_LOCATION_ERROR","NOEMERG||GOOD_INTENT||SMOKE_FROM_NONHOSTILE_SOURCE","PUBSERV||ALARMS_NONMED||CO_ALARM","PUBSERV||ALARMS_NONMED||FIRE_ALARM","PUBSERV||ALARMS_NONMED||GAS_ALARM","PUBSERV||ALARMS_NONMED||OTHER_ALARM","PUBSERV||CITIZEN_ASSIST||CITIZEN_ASSIST_SERVICE_CALL","PUBSERV||CITIZEN_ASSIST||LIFT_ASSIST","PUBSERV||CITIZEN_ASSIST||LOST_PERSON","PUBSERV||CITIZEN_ASSIST||PERSON_IN_DISTRESS","PUBSERV||DISASTER_WEATHER||DAMAGE_ASSESSMENT","PUBSERV||DISASTER_WEATHER||WEATHER_RESPONSE","PUBSERV||OTHER||DAMAGED_HYDRANT","PUBSERV||OTHER||MOVE_UP","PUBSERV||OTHER||STANDBY","RESCUE||OUTSIDE||BACKCOUNTRY_RESCUE","RESCUE||OUTSIDE||CONFINED_SPACE_RESCUE","RESCUE||OUTSIDE||EXTRICATION_ENTRAPPED","RESCUE||OUTSIDE||HIGH_ANGLE_RESCUE","RESCUE||OUTSIDE||LIMITED_NO_ACCESS","RESCUE||OUTSIDE||LOW_ANGLE_RESCUE","RESCUE||OUTSIDE||STEEP_ANGLE_RESCUE","RESCUE||OUTSIDE||TRENCH","RESCUE||STRUCTURE||BUILDING_STRUCTURE_COLLAPSE","RESCUE||STRUCTURE||CONFINED_SPACE_RESCUE","RESCUE||STRUCTURE||ELEVATOR_ESCALATOR_RESCUE","RESCUE||STRUCTURE||EXTRICATION_ENTRAPPED","RESCUE||TRANSPORTATION||AVIATION_COLLISION_CRASH","RESCUE||TRANSPORTATION||AVIATION_STANDBY","RESCUE||TRANSPORTATION||MOTOR_VEHICLE_EXTRICATION_ENTRAPPED","RESCUE||TRANSPORTATION||TRAIN_RAIL_COLLISION_DERAILMENT","RESCUE||WATER||PERSON_IN_WATER_STANDING","RESCUE||WATER||PERSON_IN_WATER_SWIFTWATER","RESCUE||WATER||WATERCRAFT_IN_DISTRESS","FIRE","OUTSIDE_FIRE","CONSTRUCTION_WASTE","DUMPSTER_OUTDOOR_CONTAINER_FIRE","OTHER_OUTSIDE_FIRE","OUTSIDE_TANK_FIRE","TRASH_RUBBISH_FIRE","UTILITY_INFRASTRUCTURE_FIRE","VEGETATION_GRASS_FIRE","WILDFIRE_URBAN_INTERFACE","WILDFIRE_WILDLAND","SPECIAL_FIRE","ESS_FIRE","EXPLOSION","INFRASTRUCTURE_FIRE","STRUCTURE_FIRE","CHIMNEY_FIRE","CONFINED_COOKING_APPLIANCE_FIRE","ROOM_AND_CONTENTS_FIRE","STRUCTURAL_INVOLVEMENT_FIRE","TRANSPORTATION_FIRE","AIRCRAFT_FIRE","BOAT_PERSONAL_WATERCRAFT_BARGE_FIRE","POWERED_MOBILITY_DEVICE_FIRE","TRAIN_RAIL_FIRE","VEHICLE_FIRE_COMMERCIAL","VEHICLE_FIRE_FOOD_TRUCK","VEHICLE_FIRE_PASSENGER","VEHICLE_FIRE_RV","HAZSIT","HAZARDOUS_MATERIALS","BIOLOGICAL_RELEASE_INCIDENT","CARBON_MONOXIDE_RELEASE","FUEL_SPILL_ODOR","GAS_LEAK_ODOR","HAZMAT_RELEASE_FACILITY","HAZMAT_RELEASE_TRANSPORT","RADIOACTIVE_RELEASE_INCIDENT","HAZARD_NONCHEM","BOMB_THREAT_RESPONSE_SUSPICIOUS_PACKAGE","ELEC_HAZARD_SHORT_CIRCUIT","ELEC_POWER_LINE_DOWN_ARCHING_MALFUNC","MOTOR_VEHICLE_COLLISION","INVESTIGATION","ODOR","SMOKE_INVESTIGATION","OVERPRESSURE","NO_RUPTURE","RUPTURE_WITHOUT_FIRE","MEDICAL","ILLNESS","ABDOMINAL_PAIN","ALLERGIC_REACTION_STINGS","ALTERED_MENTAL_STATUS","BACK_PAIN_NON_TRAUMA","BREATHING_PROBLEMS","CARDIAC_ARREST","CHEST_PAIN_NON_TRAUMA","CONVULSIONS_SEIZURES","DIABETIC_PROBLEMS","HEADACHE","HEART_PROBLEMS","NAUSEA_VOMITING","NO_APPROPRIATE_CHOICE","OVERDOSE","PANDEMIC_EPIDEMIC_OUTBREAK","PREGNANCY_CHILDBIRTH","PSYCHOLOGICAL_BEHAVIOR_ISSUES","SICK_CASE","STROKE_CVA","UNCONSCIOUS_VICTIM","UNKNOWN_PROBLEM","WELL_PERSON_CHECK","INJURY","ANIMAL_BITES","ASSAULT","BURNS_EXPLOSION","CARBON_MONOXIDE_OTHER_INHALATION_INJURY","CHOKING","DROWNING_DIVING_SCUBA_ACCIDENT","ELECTROCUTION","EYE_TRAUMA","FALL","GUNSHOT_WOUND","HEAT_COLD_EXPOSURE","HEMORRHAGE_LACERATION","INDUSTRIAL_INACCESSIBLE_ENTRAPMENT","OTHER_TRAUMATIC_INJURY","POISONING","STAB_PENETRATING_TRAUMA","OTHER","AIRMEDICAL_TRANSPORT","COMMUNITY_PUBLIC_HEALTH","HEALTHCARE_PROFESSIONAL_ADMISSION","INTERCEPT_OTHER_UNIT","MEDICAL_ALARM","STANDBY_REQUEST","TRANSFER_INTERFACILITY","NOEMERG","CANCELLED","FALSE_ALARM","ACCIDENTAL_ALARM","BOMB_SCARE","INTENTIONAL_FALSE_ALARM","MALFUNCTIONING_ALARM","OTHER_FALSE_CALL","GOOD_INTENT","CONTROLLED_BURNING_AUTHORIZED","INVESTIGATE_HAZARDOUS_RELEASE","NO_INCIDENT_FOUND_LOCATION_ERROR","SMOKE_FROM_NONHOSTILE_SOURCE","PUBSERV","ALARMS_NONMED","CO_ALARM","FIRE_ALARM","GAS_ALARM","OTHER_ALARM","CITIZEN_ASSIST","CITIZEN_ASSIST_SERVICE_CALL","LIFT_ASSIST","LOST_PERSON","PERSON_IN_DISTRESS","DISASTER_WEATHER","DAMAGE_ASSESSMENT","WEATHER_RESPONSE","DAMAGED_HYDRANT","MOVE_UP","STANDBY","RESCUE","OUTSIDE","BACKCOUNTRY_RESCUE","CONFINED_SPACE_RESCUE","EXTRICATION_ENTRAPPED","HIGH_ANGLE_RESCUE","LIMITED_NO_ACCESS","LOW_ANGLE_RESCUE","STEEP_ANGLE_RESCUE","TRENCH","STRUCTURE","BUILDING_STRUCTURE_COLLAPSE","ELEVATOR_ESCALATOR_RESCUE","TRANSPORTATION","AVIATION_COLLISION_CRASH","AVIATION_STANDBY","MOTOR_VEHICLE_EXTRICATION_ENTRAPPED","TRAIN_RAIL_COLLISION_DERAILMENT","WATER","PERSON_IN_WATER_STANDING","PERSON_IN_WATER_SWIFTWATER","WATERCRAFT_IN_DISTRESS","UNDETERMINED"],"title":"IncidentFilterType"},"IncidentHistoryItemResponse":{"properties":{"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"current":{"type":"boolean","title":"Current","description":"Whether the status is the current status"},"account_id":{"type":"string","format":"uuid","title":"Account Id","description":"The account id of the principal responsible for the incident status"},"sub":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Sub","description":"The unique id of the user if the account was a user account"},"client_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Client Id","description":"The client id of the integration if the account was an integration account"},"status":{"type":"string","enum":["APPROVED","DELETED","FAILED","PENDING_APPROVAL","PENDING_INCIDENT_DATA","REJECTED","SUBMITTED"],"title":"Status","description":"Status of the incident in the administrativie approval process."}},"type":"object","required":["last_modified","current","account_id","status"],"title":"IncidentHistoryItemResponse"},"IncidentHistoryResponse":{"properties":{"history":{"items":{"$ref":"#/components/schemas/IncidentHistoryItemResponse"},"type":"array","title":"History","description":"Chronologically ordered list of statuses for the incident"}},"type":"object","required":["history"],"title":"IncidentHistoryResponse"},"IncidentPayload":{"properties":{"base":{"$ref":"#/components/schemas/IncidentBasePayload"},"incident_types":{"anyOf":[{"items":{"$ref":"#/components/schemas/IncidentTypePayload"},"type":"array","maxItems":3,"minItems":1},{"items":{"$ref":"#/components/schemas/IncidentTypeCadPayload"},"type":"array","maxItems":1,"minItems":1}],"title":"Incident Types"},"special_modifiers":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeSpecialModifierValue"},"type":"array"},{"type":"null"}],"title":"Special Modifiers"},"aids":{"anyOf":[{"items":{"$ref":"#/components/schemas/AidPayload"},"type":"array"},{"type":"null"}],"title":"Aids"},"nonfd_aids":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeAidNonfdValue"},"type":"array"},{"type":"null"}],"title":"Nonfd Aids"},"actions_tactics":{"anyOf":[{"$ref":"#/components/schemas/ActionTacticPayload"},{"type":"null"}]},"dispatch":{"$ref":"#/components/schemas/DispatchPayload"},"tactic_timestamps":{"anyOf":[{"$ref":"#/components/schemas/IncidentTacticTimestampsPayload"},{"type":"null"}]},"unit_responses":{"anyOf":[{"items":{"$ref":"#/components/schemas/IncidentUnitResponsePayload"},"type":"array"},{"type":"null"}],"title":"Unit Responses"},"exposures":{"anyOf":[{"items":{"$ref":"#/components/schemas/ExposurePayload"},"type":"array"},{"type":"null"}],"title":"Exposures"},"casualty_rescues":{"anyOf":[{"items":{"$ref":"#/components/schemas/CasualtyRescuePayload"},"type":"array"},{"type":"null"}],"title":"Casualty Rescues"},"fire_detail":{"anyOf":[{"$ref":"#/components/schemas/FirePayload"},{"type":"null"}],"description":"Fire module."},"hazsit_detail":{"anyOf":[{"$ref":"#/components/schemas/HazsitPayload"},{"type":"null"}],"description":"Hazsit module."},"medical_details":{"anyOf":[{"items":{"$ref":"#/components/schemas/MedicalPayload"},"type":"array"},{"type":"null"}],"title":"Medical Details","description":"Medical module(s)."},"smoke_alarm":{"anyOf":[{"$ref":"#/components/schemas/SmokeAlarmPayload"},{"type":"null"}]},"fire_alarm":{"anyOf":[{"$ref":"#/components/schemas/FireAlarmPayload"},{"type":"null"}]},"other_alarm":{"anyOf":[{"$ref":"#/components/schemas/OtherAlarmPayload"},{"type":"null"}]},"fire_suppression":{"anyOf":[{"$ref":"#/components/schemas/FireSuppressionPayload"},{"type":"null"}]},"cooking_fire_suppression":{"anyOf":[{"$ref":"#/components/schemas/CookingFireSuppressionPayload"},{"type":"null"}]},"electric_hazards":{"anyOf":[{"items":{"$ref":"#/components/schemas/ElectricHazardPayload"},"type":"array"},{"type":"null"}],"title":"Electric Hazards"},"powergen_hazards":{"anyOf":[{"items":{"$ref":"#/components/schemas/PowergenHazardPayload"},"type":"array"},{"type":"null"}],"title":"Powergen Hazards"},"csst_hazard":{"anyOf":[{"$ref":"#/components/schemas/CsstHazardPayload"},{"type":"null"}]},"medical_oxygen_hazard":{"anyOf":[{"$ref":"#/components/schemas/MedicalOxygenHazardPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["base","incident_types","dispatch"],"title":"IncidentPayload","description":"\n\n
Validation Rules\n\n- `fire_details` only possible if at least one `FIRE` incident type is present.\n- `hazsit_details` only possible if at least one `HAZSIT` incident type is present.\n- `medical_details` only possible if at least one `MEDICAL` incident type is present.\n- Incidents with a `FIRE||STRUCTURE_FIRE` type require fire suppression and alarm modules: `smoke_alarm`, `fire_alarm`, `other_alarm`, `fire_suppression`, unless all aids are `SUPPORT_AID` that was `GIVEN` to another department.\n- Incidents with a `FIRE||STRUCTURE_FIRE||CONFINED_COOKING_APPLIANCE_FIRE` type require the `cooking_fire_suppression` module, unless all aids are `SUPPORT_AID` that was `GIVEN` to another department.\n- Only one `incident_type` can be marked with `primary` as `true`.\n- `aids.department_neris_id` cannot be the same as `base.department_neris_id`.\n- `aids.department_neris_id` values must be unique.\n- `incident_types.type` values must be unique.\n\n
","examples":[{"actions_tactics":{"action_noaction":{"actions":["EMERGENCY_MEDICAL_CARE||PATIENT_ASSESSMENT","EMERGENCY_MEDICAL_CARE||PROVIDE_ADVANCED_LIFE_SUPPORT","EMERGENCY_MEDICAL_CARE||PROVIDE_TRANSPORT","COMMAND_AND_CONTROL||ESTABLISH_INCIDENT_COMMAND","COMMAND_AND_CONTROL||INCIDENT_ASSESSMENT_COMPLETED","SUPPRESSION||STRUCTURAL_FIRE_SUPPRESSION||INTERIOR","VENTILATION||HORIZONTAL||DURING_SUPPRESSION","VENTILATION||HYDRAULIC||POST_SUPPRESSION","SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH||PRIOR_TO_SUPPRESSION","SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH||DURING_SUPPRESSION","SALVAGE_AND_OVERHAUL","PERSONNEL_CONTAMINATION_REDUCTION||ON_SCENE_CONTAMINATION_REDUCTION"],"type":"ACTION"}},"aids":[{"aid_direction":"GIVEN","aid_type":"SUPPORT_AID","department_neris_id":"FD48085117"}],"base":{"animals_rescued":0,"department_neris_id":"FD24027077","displacement_causes":["FIRE","SMOKE"],"displacement_count":4,"impediment_narrative":"Enhance.","incident_number":"1725295157","location":{"additional_attributes":{},"country":"US","county":"Howard County","cross_streets":[],"floor":"1","location_aliases":[{"cross_streets":[],"location_aliases":[]}],"number":6200,"place_type":"OFFICE","postal_code":"21045","postal_code_extension":"5856","postal_community":"Columbia","site":"Columbia Crossing","state":"MD","street":"Dobbin","street_postfix":"LANE","street_prefix_modifier":"Old","subsite":"Fire Safety Research Institute","unit_prefix":"Suite","unit_value":"150"},"location_use":{"in_use":{"in_use":true},"secondary_use":"RESIDENTIAL||UNHOUSED_TEMPORARY_SHELTER","use_type":"COMMERCIAL||OFFICE_OTHER_TECHNICAL_SERVICES","vacancy_cause":"NEW_CONSTRUCTION_REMODEL"},"outcome_narrative":"Final outcome narrative.","people_present":true,"point":{"crs":4326,"geometry":{"coordinates":[-76.815616,39.201414],"type":"Point"}}},"casualty_rescues":[{"birth_month_year":"06/1972","casualty":{"injury_or_noninjury":{"cause":"STRUCK_CONTACT_WITH_OBJECT","type":"INJURED_NONFATAL"}},"gender":"MALE","race":"WHITE","rank":"Captain","rescue":{"ffrescue_or_nonffrescue":{"impediments":["HOARDING_CONDITIONS","ACCESS_LIMITATIONS"],"removal_or_nonremoval":{"gas_isolation":false,"rescue_path_type":"REMOVAL_ALONG_ALT_PATH","room_type":"BEDROOM","type":"REMOVAL_FROM_STRUCTURE"},"type":"RESCUED_BY_FIREFIGHTER"},"mayday":{"mayday":true,"relative_suppression_time":"PRE_SUPPRESSION","rit_activated":true}},"type":"FF","years_of_service":12},{"birth_month_year":"01/1995","casualty":{"injury_or_noninjury":{"type":"UNINJURED"}},"rescue":{"ffrescue_or_nonffrescue":{"actions":[],"impediments":["HOARDING_CONDITIONS","ACCESS_LIMITATIONS"],"removal_or_nonremoval":{"elevation_type":"ON_BED","fire_removal":{"relative_suppression_time":"PRE_SUPPRESSION"},"gas_isolation":true,"room_type":"BEDROOM","type":"REMOVAL_FROM_STRUCTURE"},"type":"RESCUED_BY_FIREFIGHTER"}},"type":"NONFF"}],"cooking_fire_suppression":{"presence":{"type":"NOT_PRESENT"}},"csst_hazard":{"grounded":"UNKNOWN","ignition_source":true,"lightning_suspected":"NO"},"dispatch":{"automatic_alarm":false,"call_answered":"2025-05-03T18:49:47+00:00","call_arrival":"2025-05-03T18:49:23+00:00","call_create":"2025-05-03T18:50:02+00:00","center_id":"1234","comments":[{"comment":"Dispatch for gas leak/CO with potential working fire.","timestamp":"2025-05-03T18:50:12Z"}],"disposition":"Structure fire suppressed on first alarm, 1 occupant rescued, occupant refused treatment.","incident_clear":"2025-05-03T19:36:33+00:00","incident_code":"STRF1","incident_number":"1725295157","location":{"additional_attributes":{},"country":"US","county":"Howard County","cross_streets":[],"floor":"1","location_aliases":[{"cross_streets":[],"location_aliases":[]}],"number":6200,"place_type":"OFFICE","postal_code":"21045","postal_code_extension":"5856","postal_community":"Columbia","site":"Columbia Crossing","state":"MD","street":"Dobbin","street_postfix":"LANE","street_prefix_modifier":"Old","subsite":"Fire Safety Research Institute","unit_prefix":"Suite","unit_value":"150"},"tactic_timestamps":{"extrication_complete":"2025-05-03T19:01:00+00:00","suppression_complete":"2025-05-03T19:01:00+00:00","water_on_fire":"2025-05-03T18:59:00+00:00"},"unit_responses":[{"dispatch":"2025-05-03T18:50:12+00:00","enroute_to_scene":"2025-05-03T18:51:13+00:00","med_responses":[{}],"on_scene":"2025-05-03T18:53:54+00:00","point":{"crs":4326,"geometry":{"coordinates":[-76.820114,39.186229],"type":"Point"}},"reported_unit_id":"E84","response_mode":"EMERGENT","transport_mode":"NON_EMERGENT","unable_to_dispatch":false,"unit_clear":"2025-05-03T19:36:33+00:00","unit_neris_id":"FD24027077S000U001"},{"dispatch":"2025-05-03T18:50:12+00:00","enroute_to_scene":"2025-05-03T18:50:56+00:00","med_responses":[{}],"on_scene":"2025-05-03T18:52:47+00:00","point":{"crs":4326,"geometry":{"coordinates":[-76.820114,39.186229],"type":"Point"}},"reported_unit_id":"MED2","response_mode":"EMERGENT","unable_to_dispatch":false,"unit_clear":"2025-05-03T19:36:33+00:00","unit_neris_id":"FD24027077S000U011"},{"dispatch":"2025-05-03T18:50:12+00:00","enroute_to_scene":"2025-05-03T18:51:31+00:00","med_responses":[{}],"on_scene":"2025-05-03T18:54:12+00:00","point":{"crs":4326,"geometry":{"coordinates":[-76.820114,39.186229],"type":"Point"}},"reported_unit_id":"BC1","response_mode":"NON_EMERGENT","unable_to_dispatch":false,"unit_clear":"2025-05-03T19:36:33+00:00","unit_neris_id":"FD24027077S000U022"}]},"electric_hazards":[{"fire_details":{"reignition":false,"suppression_types":["SUPPRESSION_WATER_ADDITIVE","FIRE_BLANKET"]},"source_or_target":"SOURCE","type":"CONSUMER_PRODUCTS||TOY"}],"exposures":[{"damage_type":"MODERATE_DAMAGE","displacement_causes":["FIRE","SMOKE"],"displacement_count":2,"location":{"unit_prefix":"Suite","unit_value":"180"},"location_detail":{"type":"INTERNAL_EXPOSURE"},"people_present":true,"point":{"crs":4326,"geometry":{"coordinates":[-76.820114,39.186229],"type":"Point"}}}],"fire_alarm":{"presence":{"alarm_types":["MANUAL_AND_AUTOMATIC"],"operation_type":"OPERATED_ALERTED_OCCUPANT","type":"PRESENT"}},"fire_detail":{"investigation_needed":"YES","investigation_types":["INVESTIGATED_ON_SCENE_RESOURCE"],"location_detail":{"arrival_condition":"SMOKE_SHOWING","cause":"OPERATING_EQUIPMENT","damage_type":"MODERATE_DAMAGE","floor_of_origin":1,"progression_evident":true,"room_of_origin_type":"UTILITY_ROOM","type":"STRUCTURE"},"suppression_appliances":["MEDIUM_DIAMETER_FIRE_HOSE","BOOSTER_FIRE_HOSE"],"water_supply":"TANK_WATER"},"fire_suppression":{"presence":{"operation_type":{"effectiveness":{"sprinklers_activated":33,"type":"OPERATED_EFFECTIVE"}},"suppression_types":[{"full_partial":"FULL","type":"WET_PIPE_SPRINKLER_SYSTEM"},{"full_partial":"PARTIAL","type":"DRY_PIPE_SPRINKLER_SYSTEM"},{"type":"PRE_ACTION_SYSTEM"}],"type":"PRESENT"}},"hazsit_detail":{"chemicals":[{"dot_class":"GASES","name":"Carbon Monoxide","release":{"cause":"UNINTENTIONAL","physical_state":"GAS","released_into":"AIR"},"release_occurred":true},{"dot_class":"GASES","name":"HYDROGEN CYANIDE","release":{"cause":"UNINTENTIONAL","physical_state":"GAS","released_into":"AIR"},"release_occurred":true}],"disposition":"COMPLETED_WITH_FIRE_SERVICE_PRESENT","evacuated":2},"incident_types":[{"primary":true,"type":"FIRE||STRUCTURE_FIRE||ROOM_AND_CONTENTS_FIRE"},{"primary":false,"type":"HAZSIT||HAZARDOUS_MATERIALS||GAS_LEAK_ODOR"},{"type":"MEDICAL||INJURY||CARBON_MONOXIDE_OTHER_INHALATION_INJURY"}],"medical_details":[{"patient_care_evaluation":"PATIENT_SUPPORT_SERVICES_PROVIDED","patient_care_report_id":"VAE456M1-02202025","patient_status":"IMPROVED","transport_disposition":"OTHER_AGENCY_TRANSPORT"}],"medical_oxygen_hazard":{"presence":{"contributed_to_flame_spread":true,"type":"PRESENT"}},"nonfd_aids":["UTILITIES_PUBLIC_WORKS","HOUSING_SERVICES"],"other_alarm":{"presence":{"alarm_types":["HEAT_DETECTOR","CARBON_MONOXIDE"],"type":"PRESENT"}},"smoke_alarm":{"presence":{"alarm_types":["INTERCONNECTED","HARDWIRED"],"operation":{"alerted_failed_other":{"occupant_action":"EVACUATED","type":"OPERATED_ALERTED_OCCUPANT"}},"type":"PRESENT","working":true}},"special_modifiers":["URBAN_CONFLAGRATION","VIOLENCE_AGAINST_RESPONDER"],"tactic_timestamps":{"command_established":"2025-05-03T18:53:59+00:00","completed_sizeup":"2025-05-03T18:55:08+00:00","extrication_complete":"2025-05-03T19:01:00+00:00"},"unit_responses":[{"med_responses":[{}],"reported_unit_id":"E84","response_mode":"EMERGENT","staffing":4,"transport_mode":"NON_EMERGENT","unable_to_dispatch":false,"unit_neris_id":"FD24027077S000U001"},{"med_responses":[{}],"reported_unit_id":"BC1","response_mode":"EMERGENT","staffing":1,"unable_to_dispatch":false,"unit_neris_id":"FD24027077S000U022"},{"med_responses":[{"at_patient":"2025-05-03T18:53:01+00:00"}],"reported_unit_id":"MED2","response_mode":"NON_EMERGENT","staffing":2,"unable_to_dispatch":false,"unit_neris_id":"FD24027077S000U011"}]}]},"IncidentResponse":{"properties":{"neris_id":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Neris Id","description":"NERIS ID of the incident. This is comprised of the department NERIS ID concatenated with `dispatch.incident_number` and the epoch time of `dispatch.call_create` in the record as initially submitted. An incident's NERIS_ID is immutable, and so should not be relied upon for extraction of `incident_number` or `call_create`, as these attributes could have been updated.","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"]},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"department":{"anyOf":[{"$ref":"#/components/schemas/neris__api__incident__models__DepartmentTimeZoneResponse"},{"type":"null"}]},"submitter_account_type":{"$ref":"#/components/schemas/TypeAccountValue"},"base":{"$ref":"#/components/schemas/IncidentBaseResponse"},"incident_types":{"items":{"$ref":"#/components/schemas/IncidentTypeResponse"},"type":"array","title":"Incident Types"},"incident_status":{"$ref":"#/components/schemas/IncidentStatusResponse"},"special_modifiers":{"anyOf":[{"items":{"$ref":"#/components/schemas/SpecialModifierResponse"},"type":"array"},{"type":"null"}],"title":"Special Modifiers"},"aids":{"anyOf":[{"items":{"$ref":"#/components/schemas/AidResponse"},"type":"array"},{"type":"null"}],"title":"Aids"},"nonfd_aids":{"anyOf":[{"items":{"$ref":"#/components/schemas/NonfdAidResponse"},"type":"array"},{"type":"null"}],"title":"Nonfd Aids"},"actions_tactics":{"anyOf":[{"$ref":"#/components/schemas/ActionTacticResponse"},{"type":"null"}]},"dispatch":{"$ref":"#/components/schemas/DispatchResponse"},"tactic_timestamps":{"anyOf":[{"$ref":"#/components/schemas/IncidentTacticTimestampsResponse"},{"type":"null"}]},"unit_responses":{"anyOf":[{"items":{"$ref":"#/components/schemas/UnitResponseResponse"},"type":"array"},{"type":"null"}],"title":"Unit Responses"},"exposures":{"anyOf":[{"items":{"$ref":"#/components/schemas/ExposureResponse"},"type":"array"},{"type":"null"}],"title":"Exposures"},"casualty_rescues":{"anyOf":[{"items":{"$ref":"#/components/schemas/CasualtyRescueResponse"},"type":"array"},{"type":"null"}],"title":"Casualty Rescues"},"fire_detail":{"anyOf":[{"$ref":"#/components/schemas/FireResponse"},{"type":"null"}],"description":"Fire module."},"hazsit_detail":{"anyOf":[{"$ref":"#/components/schemas/HazsitResponse"},{"type":"null"}],"description":"Hazsit module."},"medical_details":{"anyOf":[{"items":{"$ref":"#/components/schemas/MedicalResponse"},"type":"array"},{"type":"null"}],"title":"Medical Details","description":"Medical module(s)."},"smoke_alarm":{"anyOf":[{"$ref":"#/components/schemas/SmokeAlarmResponse"},{"type":"null"}]},"fire_alarm":{"anyOf":[{"$ref":"#/components/schemas/FireAlarmResponse"},{"type":"null"}]},"other_alarm":{"anyOf":[{"$ref":"#/components/schemas/OtherAlarmResponse"},{"type":"null"}]},"fire_suppression":{"anyOf":[{"$ref":"#/components/schemas/FireSuppressionResponse"},{"type":"null"}]},"cooking_fire_suppression":{"anyOf":[{"$ref":"#/components/schemas/CookingFireSuppressionResponse"},{"type":"null"}]},"electric_hazards":{"anyOf":[{"items":{"$ref":"#/components/schemas/ElectricHazardResponse"},"type":"array"},{"type":"null"}],"title":"Electric Hazards"},"powergen_hazards":{"anyOf":[{"items":{"$ref":"#/components/schemas/PowergenHazardResponse"},"type":"array"},{"type":"null"}],"title":"Powergen Hazards"},"csst_hazard":{"anyOf":[{"$ref":"#/components/schemas/CsstHazardResponse"},{"type":"null"}]},"medical_oxygen_hazard":{"anyOf":[{"$ref":"#/components/schemas/MedicalOxygenHazardResponse"},{"type":"null"}]},"weather":{"anyOf":[{"$ref":"#/components/schemas/WeatherResponse"},{"type":"null"}],"description":"Weather conditions at the time of `dispatch.call_create`, if available"},"census_tract":{"anyOf":[{"$ref":"#/components/schemas/CensusTractResponse"},{"type":"null"}],"description":"Census tract information, if available"},"parcel":{"anyOf":[{"$ref":"#/components/schemas/ParcelResponse"},{"type":"null"}],"description":"Parcel information for the incident coordinates, if available"},"incident_analysis_neris_id":{"anyOf":[{"type":"string","pattern":"^IA\\|FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","examples":["IA|FD12345678|abc123xyz|1729023498","IA|FD24027334|incident42:f-tiw|1729023498","IA|FD12345678|0457812893477|1729023498"]},{"type":"null"}],"title":"Incident Analysis Neris Id","description":"Unique identifier of incident analysis, if present."}},"type":"object","required":["neris_id","last_modified","submitter_account_type","base","incident_types","incident_status","dispatch"],"title":"IncidentResponse","description":"For use in responses to get individual incidents or listing multiple incidents"},"IncidentSortBy":{"type":"string","enum":["call_create","neris_id_entity","incident_number","dispatch_incident_number","status","last_modified"],"title":"IncidentSortBy"},"IncidentStatusResponse":{"properties":{"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"created_by":{"type":"string","format":"uuid","title":"Created By","description":"That account that created or last modified the incident record."},"status":{"type":"string","enum":["APPROVED","DELETED","FAILED","PENDING_APPROVAL","PENDING_INCIDENT_DATA","REJECTED","SUBMITTED"],"title":"Status","description":"Status of the incident in the administrativie approval process."}},"type":"object","required":["last_modified","created_by","status"],"title":"IncidentStatusResponse"},"IncidentTacticTimestampsPayload":{"properties":{"command_established":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Command Established","description":"Timestamp at which incident command is established.","x-ui-label":"Time at which incident command was established.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"completed_sizeup":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Completed Sizeup","description":"Timestamp at which incident sizeup is complete.","x-ui-label":"Time at which incident sizeup was completed.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"suppression_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Suppression Complete","description":"Timestamp at which suppression is complete.","x-ui-label":"Time at which suppression was complete.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"primary_search_begin":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Primary Search Begin","description":"Timestamp at which primary search operations begin.","x-ui-label":"Time at which primary search operations began.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"primary_search_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Primary Search Complete","description":"Timestamp at which primary search operations are complete.","x-ui-label":"Time at which primary search operations were complete.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"water_on_fire":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Water On Fire","description":"Timestamp at which water is first applied to fire.","x-ui-label":"Time at which water is first applied to fire.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"fire_under_control":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Fire Under Control","description":"Timestamp at which fire is considered contained (e.g. in a wildfire), but not yet extinguished.","x-ui-label":"Time at which fire is considered contained (e.g. in a wildfire), but not yet extinguished.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"fire_knocked_down":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Fire Knocked Down","description":"Timestamp at which fire is has been knocked down.","x-ui-label":"Time at which fire was knocked down.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"extrication_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Extrication Complete","description":"Timestamp at which extrication (motor vehicle, technical rescue) has been completed."}},"additionalProperties":false,"type":"object","title":"IncidentTacticTimestampsPayload"},"IncidentTacticTimestampsResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"command_established":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Command Established","description":"Timestamp at which incident command is established."},"completed_sizeup":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Completed Sizeup","description":"Timestamp at which incident sizeup is complete."},"suppression_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Suppression Complete","description":"Timestamp at which suppression is complete."},"primary_search_begin":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Primary Search Begin","description":"Timestamp at which primary search operations begin."},"primary_search_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Primary Search Complete","description":"Timestamp at which primary search operations are complete."},"water_on_fire":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Water On Fire","description":"Timestamp at which water is first applied to fire."},"fire_under_control":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Fire Under Control","description":"Timestamp at which fire is considered contained (e.g. in a wildfire), but not yet extinguished."},"fire_knocked_down":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Fire Knocked Down","description":"Timestamp at which fire is has been knocked down."},"extrication_complete":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Extrication Complete","description":"Timestamp at which extrication (motor vehicle, technical rescue) has been completed."}},"type":"object","required":["last_modified"],"title":"IncidentTacticTimestampsResponse"},"IncidentTypeCadPayload":{"properties":{"type":{"type":"string","const":"UNDETERMINED","title":"Type","description":"Special value for CAD integrations where the incident type is not yet known."}},"additionalProperties":false,"type":"object","required":["type"],"title":"IncidentTypeCadPayload"},"IncidentTypePayload":{"properties":{"primary":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Primary","description":"Flag for the primary incident type. Only one type can be `True`."},"type":{"$ref":"#/components/schemas/TypeIncidentValue","description":"Specific incident type that details the nature of the incident; levels up into incident_category."}},"additionalProperties":false,"type":"object","required":["type"],"title":"IncidentTypePayload"},"IncidentTypeResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"primary":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Primary","description":"Flag for the primary incident type. Only one type can be `True`."},"type":{"type":"string","title":"Type","description":"Specific incident type that details the nature of the incident; levels up into incident_category."}},"type":"object","required":["last_modified","type"],"title":"IncidentTypeResponse"},"IncidentUnitResponsePayload":{"properties":{"unit_neris_id":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}U[A-Z\\d]{3}$","examples":["FD24027334S001U003","FD24160543S010U001"]},{"type":"null"}],"title":"Unit Neris Id","description":"NERIS ID of the unit responding to the incident.","examples":["FD24027000S000U000"],"x-ui-label":"Unit assigned to the incident."},"reported_unit_id":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Reported Unit Id","description":"Name or ID of the unit responding to the incident if not yet in Entity Spec (or cannot be furnished).","examples":["A702"],"x-ui-label":"If unit not in drop down, enter CAD designation here."},"staffing":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Staffing","description":"On-scene staffing of unit.","examples":[4],"x-ui-label":"Incident staffing of unit."},"unable_to_dispatch":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Unable To Dispatch","description":"Set to TRUE if unit is unable to dispatch.","x-ui-label":"Unit was able to dispatch.","x-ui-hint":"Select No if unit could not dispatch to incident."},"dispatch":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Dispatch","description":"Timestamp that the unit is dispatched.","x-ui-label":"Time at which the unit was dispatched.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"enroute_to_scene":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Enroute To Scene","description":"Timestamp that the unit goes enroute to scene of the incident.","x-ui-label":"Time at which the unit went enroute to scene of the incident.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"on_scene":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"On Scene","description":"Timestamp that the unit arrived on scene.","x-ui-label":"Time at which the unit arrived on scene.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"canceled_enroute":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Canceled Enroute","description":"Timestamp that the unit was canceled prior to arrival.","x-ui-label":"Time at which unit response was canceled.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"staging":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Staging","description":"Timestamp that the unit stages on scene.","x-ui-label":"If applicable, time at which the unit staged on scene.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"unit_clear":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Unit Clear","description":"Timestamp that the unit clears the incident.","x-ui-label":"Time at which the unit cleared the incident.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"med_responses":{"anyOf":[{"items":{"$ref":"#/components/schemas/MedResponsePayload"},"type":"array"},{"type":"null"}],"title":"Med Responses"},"point":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]},"response_mode":{"anyOf":[{"$ref":"#/components/schemas/TypeResponseModeValue"},{"type":"null"}],"description":"Response mode of unit as part of the unit's response to an incident.","x-ui-label":"Response mode of unit as part of the unit's response to an incident."},"transport_mode":{"anyOf":[{"$ref":"#/components/schemas/TypeResponseModeValue"},{"type":"null"}],"description":"Transport mode of unit as part of the unit's response to a hospital.","x-ui-label":"Response mode of unit as part of the unit's transport to a hospital."}},"additionalProperties":false,"type":"object","title":"IncidentUnitResponsePayload","description":"\n\n
Validation Rules\n\n- `neris_id_unit`, `reported_id_unit`, or both must be provided.\n\n
"},"IncidentUpdatedResponse":{"properties":{"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]}},"type":"object","required":["last_modified"],"title":"IncidentUpdatedResponse"},"InitiateForgotPasswordPayload":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"additionalProperties":false,"type":"object","required":["email"],"title":"InitiateForgotPasswordPayload"},"InjuryPayload":{"properties":{"type":{"type":"string","enum":["INJURED_NONFATAL","INJURED_FATAL"],"title":"Type","x-ui-label":"Describe the nature of the casualty.","x-ui-hint":"Select one."},"cause":{"anyOf":[{"$ref":"#/components/schemas/TypeCasualtyCauseValue"},{"type":"null"}],"description":"Apparent cause of the injury or fatality.","x-ui-label":"The apparent cause of the casualty.","x-ui-hint":"Select one."},"ff_injury_details":{"anyOf":[{"$ref":"#/components/schemas/FfInjuryDetailsPayload"},{"type":"null"}],"description":"Details of the injury sustained by a firefighter."}},"additionalProperties":false,"type":"object","required":["type"],"title":"InjuryPayload"},"InjuryResponse":{"properties":{"type":{"type":"string","title":"Type"},"cause":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cause","description":"Apparent cause of the injury or fatality."},"ff_injury_details":{"anyOf":[{"$ref":"#/components/schemas/FfInjuryDetailsResponse"},{"type":"null"}],"description":"Details of the injury sustained by a firefighter."}},"type":"object","required":["type"],"title":"InjuryResponse"},"InspectionPayload":{"properties":{"inspection_required":{"type":"boolean","title":"Inspection Required","description":"Whether the structure requires an inspection.","default":false,"x-ui-label":"Does this structure require regular inspections?"},"date_of_last_inspection":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Date Of Last Inspection","description":"The date of the last inspection.","x-ui-label":"Date of Last Inspection"},"unresolved_inspection_findings":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Unresolved Inspection Findings","description":"Whether a previous inspection found unresolved findings.","x-ui-label":"Did a previous inspection reveal any unresolved inspection findings?"}},"additionalProperties":false,"type":"object","title":"InspectionPayload"},"InspectionResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"inspection_required":{"type":"boolean","title":"Inspection Required","description":"Whether the structure requires an inspection.","default":false},"date_of_last_inspection":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Date Of Last Inspection","description":"The date of the last inspection."},"unresolved_inspection_findings":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Unresolved Inspection Findings","description":"Whether a previous inspection found unresolved findings."}},"type":"object","title":"InspectionResponse"},"IntegrationEnrollmentListItem":{"properties":{"id":{"type":"string","format":"uuid","title":"Id","description":"The unique identifier for the enrollment"},"client_id":{"type":"string","format":"uuid","title":"Client Id","description":"The client id (integration account id) for the integration"},"neris_id":{"type":"string","title":"Neris Id","description":"The NERIS ID of the entity that is enrolled in the integration"},"entity_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Name","description":"The name of the entity enrolled in the integration"},"created_by":{"type":"string","format":"uuid","title":"Created By","description":"User account ID of the account that created the enrollment"},"approved_by":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Approved By","description":"Optional user account ID of the account that approved the enrollment"},"integration_title":{"type":"string","title":"Integration Title","description":"Title of the integration"}},"additionalProperties":false,"type":"object","required":["id","client_id","neris_id","created_by","integration_title"],"title":"IntegrationEnrollmentListItem"},"IntegrationEnrollmentResponse":{"properties":{"id":{"type":"string","format":"uuid","title":"Id","description":"The unique identifier for the enrollment"},"client_id":{"type":"string","format":"uuid","title":"Client Id","description":"The client id (integration account id) for the integration"},"neris_id":{"type":"string","title":"Neris Id","description":"The NERIS ID of the entity that is enrolled in the integration"},"entity_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Name","description":"The name of the entity enrolled in the integration"},"created_by":{"type":"string","format":"uuid","title":"Created By","description":"User account ID of the account that created the enrollment"},"approved_by":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Approved By","description":"Optional user account ID of the account that approved the enrollment"}},"additionalProperties":false,"type":"object","required":["id","client_id","neris_id","created_by"],"title":"IntegrationEnrollmentResponse"},"IntegrationListItem":{"properties":{"client_id":{"type":"string","format":"uuid","title":"Client Id","description":"The integration's client id"},"title":{"type":"string","title":"Title"},"type":{"$ref":"#/components/schemas/TypeIntegrationValue"},"credential_ids":{"items":{"type":"string","format":"uuid"},"type":"array","title":"Credential Ids","description":"List of credential ids for the integration"}},"additionalProperties":false,"type":"object","required":["client_id","title","type","credential_ids"],"title":"IntegrationListItem"},"InternalExposurePayload":{"properties":{"type":{"type":"string","const":"INTERNAL_EXPOSURE","title":"Type","x-ui-label":"The type exposure being reported."}},"additionalProperties":false,"type":"object","required":["type"],"title":"InternalExposurePayload"},"InternalExposureResponse":{"properties":{"type":{"type":"string","title":"Type"}},"type":"object","required":["type"],"title":"InternalExposureResponse"},"InusePayload":{"properties":{"in_use":{"type":"boolean","title":"In Use","description":"Whether the location is in use.","x-ui-label":"Describe whether the location was in use.","x-ui-hint":"This could be a \"vacant\" structure, but that does not mean that the structure was unoccupied."},"intended":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Intended","description":"Whether the location is being used as intended.","x-ui-label":"Describe whether the location was being used as intended."}},"additionalProperties":false,"type":"object","required":["in_use"],"title":"InusePayload","description":"\n\n
Validation Rules\n\n- `intended` can only be set if `in_use` is `true`.\n\n
"},"InuseResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"in_use":{"type":"boolean","title":"In Use","description":"Whether the location is in use."},"intended":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Intended","description":"Whether the location is being used as intended."}},"type":"object","required":["last_modified","in_use"],"title":"InuseResponse"},"ListEntitiesSummaryInfoResponse":{"properties":{"page_size":{"type":"integer","title":"Page Size"},"page_count":{"type":"integer","title":"Page Count"},"page_number":{"type":"integer","title":"Page Number"},"total_count":{"type":"integer","title":"Total Count"},"entities":{"items":{"$ref":"#/components/schemas/EntitySummaryInfoResponse"},"type":"array","title":"Entities"}},"type":"object","required":["page_size","page_count","page_number","total_count","entities"],"title":"ListEntitiesSummaryInfoResponse"},"ListEntitySetsResponse":{"properties":{"page_size":{"type":"integer","title":"Page Size"},"page_count":{"type":"integer","title":"Page Count"},"page_number":{"type":"integer","title":"Page Number"},"total_count":{"type":"integer","title":"Total Count"},"entity_sets":{"items":{"$ref":"#/components/schemas/EntitySetResponse"},"type":"array","title":"Entity Sets"}},"additionalProperties":false,"type":"object","required":["page_size","page_count","page_number","total_count","entity_sets"],"title":"ListEntitySetsResponse"},"ListIncidentAnalysesResponse":{"properties":{"incident_analyses":{"items":{"$ref":"#/components/schemas/IncidentAnalysisResponse"},"type":"array","title":"Incident Analyses"},"prev_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prev Cursor"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["incident_analyses"],"title":"ListIncidentAnalysesResponse"},"ListIncidentsResponse":{"properties":{"incidents":{"items":{"$ref":"#/components/schemas/IncidentResponse"},"type":"array","title":"Incidents"},"prev_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prev Cursor"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"}},"type":"object","required":["incidents"],"title":"ListIncidentsResponse"},"ListIntegrationEnrollmentsResponse":{"properties":{"page_size":{"type":"integer","title":"Page Size"},"page_count":{"type":"integer","title":"Page Count"},"page_number":{"type":"integer","title":"Page Number"},"total_count":{"type":"integer","title":"Total Count"},"distinct_integration_types":{"items":{"$ref":"#/components/schemas/TypeIntegrationValue"},"type":"array","title":"Distinct Integration Types"},"enrollments":{"items":{"$ref":"#/components/schemas/IntegrationEnrollmentListItem"},"type":"array","title":"Enrollments","description":"List of integration enrollments for the entity."}},"additionalProperties":false,"type":"object","required":["page_size","page_count","page_number","total_count","distinct_integration_types","enrollments"],"title":"ListIntegrationEnrollmentsResponse"},"ListIntegrationResponse":{"properties":{"integrations":{"items":{"$ref":"#/components/schemas/IntegrationListItem"},"type":"array","title":"Integrations","description":"List of integration accounts for this entity. Each integration account has one client id and one or more credentials","examples":[[{"client_id":"0ec8c707-5b6c-45f0-b4fc-e1c609e06c30","credential_ids":["6025cedb-7cec-4414-a003-4079dfda7f5b","294d7e8b-308a-49f1-b762-40fa3b772b09"],"title":"ACME Records Management","type":"RMS"},{"client_id":"fa9a3ea3-f682-4ec0-ba92-9a9a8b0ccee3","credential_ids":["931fc33f-d8d2-445b-b0bf-6dcccf1758c9","3b558963-6fc2-4b89-b93e-5faa75adb45a"],"title":"ACME Analytics","type":"CAD"}]]}},"additionalProperties":false,"type":"object","required":["integrations"],"title":"ListIntegrationResponse"},"ListNoActivityReportResponse":{"properties":{"reports":{"items":{"$ref":"#/components/schemas/NoActivityReportResponse"},"type":"array","title":"Reports"}},"type":"object","required":["reports"],"title":"ListNoActivityReportResponse"},"ListUserInfoResponse":{"properties":{"page_size":{"type":"integer","title":"Page Size"},"page_count":{"type":"integer","title":"Page Count"},"page_number":{"type":"integer","title":"Page Number"},"total_count":{"type":"integer","title":"Total Count"},"users":{"items":{"$ref":"#/components/schemas/UserInfoResponse"},"type":"array","title":"Users"}},"type":"object","required":["page_size","page_count","page_number","total_count"],"title":"ListUserInfoResponse"},"LocationPayload":{"properties":{"additional_attributes":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Additional Attributes","description":"JSON string of structure address elements which the transmitter wishes to include.","examples":[{"common_name":"Eastside Walmart"}]},"place_type":{"anyOf":[{"$ref":"#/components/schemas/TypeLocPlaceValue"},{"type":"null"}],"description":"Type of feature identified by the address.","examples":["AIRPORT"]},"postal_community":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Postal Community","description":"City name for the ZIP Code of an address, as given in the USPS City State file.","examples":["Stanton (a post office name in KY)"]},"neighborhood_community":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Neighborhood Community","description":"Name of an unincorporated neighborhood, subdivision or area, either within an incorporated municipality, or in an unincorporated portion of a county or both, where the address is located. ","examples":["Cypress Meadows Subdivision"],"x-ui-label":"parcel_neighborhood"},"unincorporated_community":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Unincorporated Community","description":"Name of an unincorporated community, either within an incorporated municipality or in an unincorporated portion of a county, or both, where the address is located. ","examples":["14th Ward (a ward in the incorporated municipality of Pittsburgh, PA) "]},"incorporated_municipality":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Incorporated Municipality","description":"Name of the incorporated municipality or other general-purpose local governmental unit (if any) where the address is located.","examples":["Haleyville"],"x-ui-label":"parcel_address_city"},"county":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"County","description":"Name of the county or county-equivalent where the address is located. A county (or its equivalent) is the primary legal division of a state or territory.","examples":["Winston County"],"x-ui-label":"parcel_address_county"},"state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"description":"Name of a state or state equivalent, represented by the two- letter UPPER CASE abbreviation given in USPS Publication 28 [15], Appendix B. A state is a primary governmental division of the United States.","examples":["VA"],"x-ui-label":"parcel_address_state"},"postal_code":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Postal Code","description":"System of 5-digit codes that identifies the individual USPS Post Office or metropolitan area delivery station associated with an address.","examples":["02109 (one of many ZIP Codes for Boston, MA)"],"x-ui-label":"parcel_address_zip"},"postal_code_extension":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Postal Code Extension","description":"System of 4-digit codes that are used after the 5-digit ZIP Code to specify a range of USPS delivery addresses.","examples":["\"0001\" in \"02109-0001\" (portion of a 02109 ZIP Code)."],"x-ui-label":"parcel_post_extension"},"country":{"anyOf":[{"$ref":"#/components/schemas/TypeLocCspCountryValue"},{"type":"null"}],"description":"Name of a country represented by its two-letter ISO 3166-1[14] English country alpha-2 code elements in UPPER CASE letters.","examples":["US"]},"street_prefix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Modifier","description":"Word or phrase that precedes and modifies the Street Name element but is separated from it by a Street Name Pre Type or a Street Name Pre Directional or both.","examples":["\"Old\" in \"Old North First Street\" (\"Old\" is a Street Name Pre Modifier because the Street Name Pre Directional \"North\" separates \"Old\" from the Street Name \"First\" and the Street Name Post Type \"Street\".)"]},"street_prefix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Direction","description":"Word preceding the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"North\" in \"North Fairfax Drive\""],"x-ui-label":"exposure_StPreDir"},"street":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street","description":"Element of the complete street name that identifies the particular street (as opposed to any street types, directionals, and modifiers).","examples":["\"Fairfax\" in \"North Fairfax Avenue\""],"x-ui-label":"parcel_StName"},"street_postfix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Direction","description":"Word following the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"East\" in \"Seventh Street East\""],"x-ui-label":"exposure_StDir"},"street_postfix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Modifier","description":"Word or phrase that follows and modifies the Street Name element and is either separated from it by a Street Name Post Type and/or a Street Name Post Directional.","examples":["\"Extension\" in \"Market Street North Extension\" (because \"North\" separates \"Extension\" from the Street Name Post Type."]},"street_prefix":{"anyOf":[{"$ref":"#/components/schemas/TypeLocSnPrePostValue"},{"type":"null"}],"description":"Word or phrase that precedes the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"Avenue A\""],"x-ui-label":"exposure_StPreType"},"street_preposition_type_separator":{"anyOf":[{"$ref":"#/components/schemas/TypeLocSnPreSepValue"},{"type":"null"}],"description":"Preposition or prepositional phrase between the Street Name Pre Type and the Street Name.","examples":["\"of the\" in \"Avenue of the Americas\""]},"street_postfix":{"anyOf":[{"$ref":"#/components/schemas/TypeLocSnPrePostValue"},{"type":"null"}],"description":"Word or phrase that follows the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"North Fairfax Avenue\""],"x-ui-label":"exposure_StType"},"direction_of_travel":{"anyOf":[{"$ref":"#/components/schemas/TypeLocSnDirectionValue"},{"type":"null"}],"description":"Word which follows all other street name elements and is used only as needed to indicate direction of travel on a divided roadway and associated frontage roads.","examples":["\"northbound\" in \"Interstate Highway 5 North northbound\""]},"number_prefix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Prefix","description":"Identifier in the portion of the complete address number that precedes the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"A\" in \"A19 Calle 117, Toa Alta, PR\""]},"number_suffix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Suffix","description":"Identifier in the portion of the complete address number that follows the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"1\"2\" in \"194-03 1\"2 50th Avenue, New York, NY 11365\" "]},"complete_number":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Complete Number","description":"Address Number Complete includes the Address Number Prefix (if any), the Address Number, Address Number Suffix (if any), and any formatting or separator characters needed to display the official version of the complete address number. The Address Number Complete precedes the complete street name to identify a location along a thoroughfare or within a defined area. ","examples":["\"A19\" in \"A19 Calle 117, Toa Alta, PR\""],"x-ui-label":"parcel_address_num"},"distance_marker":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Distance Marker","description":"Distance travelled along a route such as a road or highway, indicated by a distance marker sign, typically a post or other marker indicating the distance in miles/kilometers from or to a given point. ","examples":["\"Milepost 1303\" in \"Milepost 1303, Alaska Highway\""]},"number":{"anyOf":[{"type":"integer","maximum":1000000.0,"minimum":0.0},{"type":"null"}],"title":"Number","description":"Integer identifier of a location along a thoroughfare or within a defined community.","examples":["\"123\" in \"123 Main Street\""]},"structure":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Structure","description":"A built feature which has a vertical dimension, including both conventional buildings which have walls, doors, and a roof, and other kinds of infrastructure such as cell towers, transformer stations, fuel tanks, and so on. ","examples":["\"Fuel Storage Shed\" in \"100 Cantwell Avenue, Fuel Storage Shed\""],"x-ui-label":"parcel_structure"},"subsite":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Subsite","description":"Name of a sub-area within a larger area specified either by site name, by a thoroughfare address, or both.","examples":["\"Buckeye Village\" in \"The Ohio State University, Buckeye Village\""]},"site":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Site","description":"Name of an exterior area which is publicly known and unique within a given place. A site may contain one or more structures and/or sub-sites.","examples":["\"Winslow Park and Campground\" in \"Winslow Park and Campground, Freeport, ME\" "]},"wing":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Wing","description":"Designated part of a structure which spans one or many floors, typically including more than one unit or room and representing a significant portion of the structure floor area. ","examples":["\"Concourse A\" in \"Pittsburgh International Airport, Airside Terminal, Concourse A\" "]},"floor":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Floor","description":"Standardized identifier for a story or level within a structure, wing, or unit.","examples":["\"5\" in \"800 Jefferson Street, 5th Floor\" "]},"unit_prefix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Unit Prefix","description":"Part of the complete unit identifier that precedes the Unit Value and indicates the kind of unit.","examples":["\"Apartment\" in \"422 Via Casitas, Apartment 12\" "],"x-ui-label":"Type of exposure unit."},"unit_value":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Unit Value","description":"Part of the complete unit identifier that uniquely identifies a particular unit.","examples":["\"12\" in \"422 Via Casitas, Apartment 12\" "],"x-ui-label":"Unit alphanumeric value.","x-ui-hint":"e.g. 302E, 12"},"room":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Room","description":"Single, distinctly identified, enclosed space within a structure. ","examples":["\"E427\" in \"Gilman Memorial Hospital, Room E247\" "]},"section":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Section","description":"Identified, unenclosed area within a structure, wing, unit, or room. ","examples":["\"Customer Seating\" in \"Coastland Mall, Food Court, Customer Seating\" "]},"row":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Row","description":"Identified linear feature, such as a linear arrangement of seats, workstations, equipment, or storage, within a structure, wing, unit, or room. ","examples":["\"Aisle 4\" in \"Warehouse 3, Aisle 4\""]},"seat":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Seat","description":"Identified seat, desk, workstation, or similar precise location within a structure, wing, unit, room, section, or row.","examples":["\"Cubicle A-7\" in \"1 Ashburton Place, Room 819, Cubicle A-7\""]},"additional_info":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Additional Info","description":"Information that relates to location but does not meet the definition of any other named location elements.","examples":["Main Loading Dock"]},"marker":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Marker","description":"Uniquely identified and indivisible infrastructure component, smaller than a structure, which exists either within a structure or exterior to any structure, such as an alarm box, a utility pole, a callbox, or other similar feature. ","examples":["Call Box CC-680-118 (an emergency call box on Interstate 680 in Contra Costa County, CA)"]},"cross_streets":{"anyOf":[{"items":{"$ref":"#/components/schemas/CrossStreetPayload"},"type":"array"},{"type":"null"}],"title":"Cross Streets","description":"Array of (Street name + optional address number) for the nearest intersection(s).","x-ui-label":"Cross Street Concat"},"location_aliases":{"anyOf":[{"items":{"$ref":"#/components/schemas/LocationPayload"},"type":"array"},{"type":"null"}],"title":"Location Aliases","description":"A location that is an alias for the for the top-level location"}},"additionalProperties":false,"type":"object","title":"LocationPayload"},"LocationResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"additional_attributes":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Additional Attributes","description":"JSON string of structure address elements which the transmitter wishes to include.","examples":[{"common_name":"Eastside Walmart"}]},"place_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Place Type","description":"Type of feature identified by the address.","examples":["AIRPORT"]},"postal_community":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Postal Community","description":"City name for the ZIP Code of an address, as given in the USPS City State file.","examples":["Stanton (a post office name in KY)"]},"neighborhood_community":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Neighborhood Community","description":"Name of an unincorporated neighborhood, subdivision or area, either within an incorporated municipality, or in an unincorporated portion of a county or both, where the address is located. ","examples":["Cypress Meadows Subdivision"]},"unincorporated_community":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Unincorporated Community","description":"Name of an unincorporated community, either within an incorporated municipality or in an unincorporated portion of a county, or both, where the address is located. ","examples":["14th Ward (a ward in the incorporated municipality of Pittsburgh, PA) "]},"incorporated_municipality":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Incorporated Municipality","description":"Name of the incorporated municipality or other general-purpose local governmental unit (if any) where the address is located.","examples":["Haleyville"]},"county":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"County","description":"Name of the county or county-equivalent where the address is located. A county (or its equivalent) is the primary legal division of a state or territory.","examples":["Winston County"]},"state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"description":"Name of a state or state equivalent, represented by the two- letter UPPER CASE abbreviation given in USPS Publication 28 [15], Appendix B. A state is a primary governmental division of the United States.","examples":["VA"]},"postal_code":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Postal Code","description":"System of 5-digit codes that identifies the individual USPS Post Office or metropolitan area delivery station associated with an address.","examples":["02109 (one of many ZIP Codes for Boston, MA)"]},"postal_code_extension":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Postal Code Extension","description":"System of 4-digit codes that are used after the 5-digit ZIP Code to specify a range of USPS delivery addresses.","examples":["\"0001\" in \"02109-0001\" (portion of a 02109 ZIP Code)."]},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country","description":"Name of a country represented by its two-letter ISO 3166-1[14] English country alpha-2 code elements in UPPER CASE letters.","examples":["US"]},"street_prefix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Modifier","description":"Word or phrase that precedes and modifies the Street Name element but is separated from it by a Street Name Pre Type or a Street Name Pre Directional or both.","examples":["\"Old\" in \"Old North First Street\" (\"Old\" is a Street Name Pre Modifier because the Street Name Pre Directional \"North\" separates \"Old\" from the Street Name \"First\" and the Street Name Post Type \"Street\".)"]},"street_prefix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Prefix Direction","description":"Word preceding the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"North\" in \"North Fairfax Drive\""]},"street":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street","description":"Element of the complete street name that identifies the particular street (as opposed to any street types, directionals, and modifiers).","examples":["\"Fairfax\" in \"North Fairfax Avenue\""]},"street_postfix_direction":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Direction","description":"Word following the Street Name element that indicates the direction taken by the street from an arbitrary starting point or line, or the sector where it is located. ","examples":["\"East\" in \"Seventh Street East\""]},"street_postfix_modifier":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Street Postfix Modifier","description":"Word or phrase that follows and modifies the Street Name element and is either separated from it by a Street Name Post Type and/or a Street Name Post Directional.","examples":["\"Extension\" in \"Market Street North Extension\" (because \"North\" separates \"Extension\" from the Street Name Post Type."]},"street_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Street Prefix","description":"Word or phrase that precedes the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"Avenue A\""]},"street_preposition_type_separator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Street Preposition Type Separator","description":"Preposition or prepositional phrase between the Street Name Pre Type and the Street Name.","examples":["\"of the\" in \"Avenue of the Americas\""]},"street_postfix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Street Postfix","description":"Word or phrase that follows the Street Name element and identifies a type of thoroughfare in a complete street name.","examples":["\"Avenue\" in \"North Fairfax Avenue\""]},"direction_of_travel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Direction Of Travel","description":"Word which follows all other street name elements and is used only as needed to indicate direction of travel on a divided roadway and associated frontage roads.","examples":["\"northbound\" in \"Interstate Highway 5 North northbound\""]},"number_prefix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Prefix","description":"Identifier in the portion of the complete address number that precedes the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"A\" in \"A19 Calle 117, Toa Alta, PR\""]},"number_suffix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Number Suffix","description":"Identifier in the portion of the complete address number that follows the integer Address Number in order to further specify a location along a thoroughfare or within a defined area.","examples":["\"1\"2\" in \"194-03 1\"2 50th Avenue, New York, NY 11365\" "]},"complete_number":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Complete Number","description":"Address Number Complete includes the Address Number Prefix (if any), the Address Number, Address Number Suffix (if any), and any formatting or separator characters needed to display the official version of the complete address number. The Address Number Complete precedes the complete street name to identify a location along a thoroughfare or within a defined area. ","examples":["\"A19\" in \"A19 Calle 117, Toa Alta, PR\""]},"distance_marker":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Distance Marker","description":"Distance travelled along a route such as a road or highway, indicated by a distance marker sign, typically a post or other marker indicating the distance in miles/kilometers from or to a given point. ","examples":["\"Milepost 1303\" in \"Milepost 1303, Alaska Highway\""]},"number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Number","description":"Integer identifier of a location along a thoroughfare or within a defined community.","examples":["\"123\" in \"123 Main Street\""]},"structure":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Structure","description":"A built feature which has a vertical dimension, including both conventional buildings which have walls, doors, and a roof, and other kinds of infrastructure such as cell towers, transformer stations, fuel tanks, and so on. ","examples":["\"Fuel Storage Shed\" in \"100 Cantwell Avenue, Fuel Storage Shed\""]},"subsite":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Subsite","description":"Name of a sub-area within a larger area specified either by site name, by a thoroughfare address, or both.","examples":["\"Buckeye Village\" in \"The Ohio State University, Buckeye Village\""]},"site":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Site","description":"Name of an exterior area which is publicly known and unique within a given place. A site may contain one or more structures and/or sub-sites.","examples":["\"Winslow Park and Campground\" in \"Winslow Park and Campground, Freeport, ME\" "]},"wing":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Wing","description":"Designated part of a structure which spans one or many floors, typically including more than one unit or room and representing a significant portion of the structure floor area. ","examples":["\"Concourse A\" in \"Pittsburgh International Airport, Airside Terminal, Concourse A\" "]},"floor":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Floor","description":"Standardized identifier for a story or level within a structure, wing, or unit.","examples":["\"5\" in \"800 Jefferson Street, 5th Floor\" "]},"unit_prefix":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Unit Prefix","description":"Part of the complete unit identifier that precedes the Unit Value and indicates the kind of unit.","examples":["\"Apartment\" in \"422 Via Casitas, Apartment 12\" "]},"unit_value":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Unit Value","description":"Part of the complete unit identifier that uniquely identifies a particular unit.","examples":["\"12\" in \"422 Via Casitas, Apartment 12\" "]},"room":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Room","description":"Single, distinctly identified, enclosed space within a structure. ","examples":["\"E427\" in \"Gilman Memorial Hospital, Room E247\" "]},"section":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Section","description":"Identified, unenclosed area within a structure, wing, unit, or room. ","examples":["\"Customer Seating\" in \"Coastland Mall, Food Court, Customer Seating\" "]},"row":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Row","description":"Identified linear feature, such as a linear arrangement of seats, workstations, equipment, or storage, within a structure, wing, unit, or room. ","examples":["\"Aisle 4\" in \"Warehouse 3, Aisle 4\""]},"seat":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Seat","description":"Identified seat, desk, workstation, or similar precise location within a structure, wing, unit, room, section, or row.","examples":["\"Cubicle A-7\" in \"1 Ashburton Place, Room 819, Cubicle A-7\""]},"additional_info":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Additional Info","description":"Information that relates to location but does not meet the definition of any other named location elements.","examples":["Main Loading Dock"]},"marker":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Marker","description":"Uniquely identified and indivisible infrastructure component, smaller than a structure, which exists either within a structure or exterior to any structure, such as an alarm box, a utility pole, a callbox, or other similar feature. ","examples":["Call Box CC-680-118 (an emergency call box on Interstate 680 in Contra Costa County, CA)"]},"cross_streets":{"anyOf":[{"items":{"$ref":"#/components/schemas/CrossStreetResponse"},"type":"array"},{"type":"null"}],"title":"Cross Streets","description":"Array of (Street name + optional address number) for the nearest intersection(s)."},"location_aliases":{"anyOf":[{"items":{"$ref":"#/components/schemas/LocationResponse"},"type":"array"},{"type":"null"}],"title":"Location Aliases","description":"A location that is an alias for the for the top-level location"},"geocoded_location":{"anyOf":[{"$ref":"#/components/schemas/GeocodedLocationResponse"},{"type":"null"}],"description":"A geocoded location derived from the information in the parent location."}},"type":"object","title":"LocationResponse"},"LocationUsePayload":{"properties":{"in_use":{"anyOf":[{"$ref":"#/components/schemas/InusePayload"},{"type":"null"}],"description":"If the location is being used, whether it is being used as intended."},"use_type":{"anyOf":[{"$ref":"#/components/schemas/TypeLocationUseValue"},{"type":"null"}],"description":"Location use type.","x-ui-label":"Please select the specific use of the location type selected."},"vacancy_cause":{"anyOf":[{"$ref":"#/components/schemas/TypeVacancyValue"},{"type":"null"}],"description":"Apparent reason the location is vacant.","x-ui-label":"If vacant, apparent reason for vacancy.","x-ui-hint":"Note: vacant locations can still be \"in use.\""},"secondary_use":{"anyOf":[{"$ref":"#/components/schemas/TypeLocationUseValue"},{"type":"null"}],"description":"Secondary location use type, if there was one that impacted the incident response.","x-ui-label":"Please select the specific use of the secondary location type selected."}},"additionalProperties":false,"type":"object","title":"LocationUsePayload"},"LocationUseResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"in_use":{"anyOf":[{"$ref":"#/components/schemas/InuseResponse"},{"type":"null"}],"description":"If the location is being used, whether it is being used as intended."},"use_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Type","description":"Location use type."},"vacancy_cause":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Vacancy Cause","description":"Apparent reason the location is vacant."},"secondary_use":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Secondary Use","description":"Secondary location use type, if there was one that impacted the incident response."}},"type":"object","title":"LocationUseResponse"},"MaydayPayload":{"properties":{"mayday":{"type":"boolean","const":true,"title":"Mayday","description":"Whether there was a mayday called to indicate a firefighter needed assistance during the incident.","default":true,"x-ui-label":"Describe whether a mayday called as part of the incident operations."},"rit_activated":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rit Activated","description":"Whether a RIT team was activated following a mayday declaration.","x-ui-label":"Describe whether a rapid intervention team was activated.","x-ui-hint":"Select one."},"relative_suppression_time":{"anyOf":[{"$ref":"#/components/schemas/TypeSuppressTimeValue"},{"type":"null"}],"description":"Relative to suppression, when the mayday was called.","x-ui-label":"Relative to suppression, when the was mayday called.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","title":"MaydayPayload"},"MaydayResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"mayday":{"type":"boolean","const":true,"title":"Mayday","description":"Whether there was a mayday called to indicate a firefighter needed assistance during the incident.","default":true},"rit_activated":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rit Activated","description":"Whether a RIT team was activated following a mayday declaration."},"relative_suppression_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relative Suppression Time","description":"Relative to suppression, when the mayday was called."}},"type":"object","required":["last_modified"],"title":"MaydayResponse"},"MedResponsePayload":{"properties":{"hospital_destination":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Hospital Destination","description":"Name of the hospital where the patient is transported.","examples":["University of Maryland Capital Regional Medical Center"],"x-ui-label":"Name of the hospital where the patient is transported."},"at_patient":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"At Patient","description":"Timestamp that the provider reaches the patient's side.","x-ui-label":"Time that the provider reaches the patient's side.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"enroute_to_hospital":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Enroute To Hospital","description":"Timestamp that the unit statuses enroute to the hospital.","x-ui-label":"Time that the unit statuses enroute to the hospital.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"arrived_at_hospital":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Arrived At Hospital","description":"Timestamp that the unit arrives at the hospital.","x-ui-label":"Time that the unit arrives at the hospital.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"transferred_to_agency":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Transferred To Agency","description":"Timestamp that the provider transfers care to a transporting agency.","x-ui-label":"Time that the provider transfers care to a transporting agency.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"transferred_to_facility":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Transferred To Facility","description":"Timestamp that the provider transfers care to a hospital or facility.","x-ui-label":"Time that the provider transferred care to a hospital or facility.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"},"hospital_cleared":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Hospital Cleared","description":"Timestamp that the unit clears the hospital.","x-ui-label":"Time that the unit cleared the hospital.","x-ui-hint":"Please complete day-time in the 24-hour format YYYY-MM-DD HH:MM:SS"}},"additionalProperties":false,"type":"object","title":"MedResponsePayload"},"MedResponseResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"hospital_destination":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Hospital Destination","description":"Name of the hospital where the patient is transported.","examples":["University of Maryland Capital Regional Medical Center"]},"at_patient":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"At Patient","description":"Timestamp that the provider reaches the patient's side."},"enroute_to_hospital":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Enroute To Hospital","description":"Timestamp that the unit statuses enroute to the hospital."},"arrived_at_hospital":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Arrived At Hospital","description":"Timestamp that the unit arrives at the hospital."},"transferred_to_agency":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Transferred To Agency","description":"Timestamp that the provider transfers care to a transporting agency."},"transferred_to_facility":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Transferred To Facility","description":"Timestamp that the provider transfers care to a hospital or facility."},"hospital_cleared":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Hospital Cleared","description":"Timestamp that the unit clears the hospital."}},"type":"object","required":["last_modified"],"title":"MedResponseResponse"},"MedicalOxygenHazardNotPresentPayload":{"properties":{"type":{"type":"string","enum":["NOT_PRESENT","NOT_APPLICABLE"],"title":"Type"}},"additionalProperties":false,"type":"object","required":["type"],"title":"MedicalOxygenHazardNotPresentPayload"},"MedicalOxygenHazardNotPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"}},"type":"object","required":["last_modified","type"],"title":"MedicalOxygenHazardNotPresentResponse"},"MedicalOxygenHazardPayload":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/MedicalOxygenHazardPresentPayload"},{"$ref":"#/components/schemas/MedicalOxygenHazardNotPresentPayload"}],"title":"Presence","discriminator":{"propertyName":"type","mapping":{"NOT_APPLICABLE":"#/components/schemas/MedicalOxygenHazardNotPresentPayload","NOT_PRESENT":"#/components/schemas/MedicalOxygenHazardNotPresentPayload","PRESENT":"#/components/schemas/MedicalOxygenHazardPresentPayload"}}}},"additionalProperties":false,"type":"object","required":["presence"],"title":"MedicalOxygenHazardPayload"},"MedicalOxygenHazardPresentPayload":{"properties":{"type":{"type":"string","const":"PRESENT","title":"Type"},"contributed_to_flame_spread":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Contributed To Flame Spread","description":"Whether the medical oxygen hazard contributed to flame spread."}},"additionalProperties":false,"type":"object","required":["type"],"title":"MedicalOxygenHazardPresentPayload"},"MedicalOxygenHazardPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"},"contributed_to_flame_spread":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Contributed To Flame Spread","description":"Whether the medical oxygen hazard contributed to flame spread."}},"type":"object","required":["last_modified","type"],"title":"MedicalOxygenHazardPresentResponse"},"MedicalOxygenHazardResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"presence":{"oneOf":[{"$ref":"#/components/schemas/MedicalOxygenHazardPresentResponse"},{"$ref":"#/components/schemas/MedicalOxygenHazardNotPresentResponse"}],"title":"Presence"}},"type":"object","required":["last_modified","presence"],"title":"MedicalOxygenHazardResponse"},"MedicalPayload":{"properties":{"patient_care_evaluation":{"$ref":"#/components/schemas/TypeMedicalPatientCareValue","description":"Status of patient care needs based on evaluation.","x-ui-label":"Status of patient care needs based on evaluation."},"patient_status":{"anyOf":[{"$ref":"#/components/schemas/TypeMedicalPatientStatusValue"},{"type":"null"}],"description":"Status of the patient after FD arrival and intervention.","x-ui-label":"Status of the patient after arrival and intervention."},"transport_disposition":{"anyOf":[{"$ref":"#/components/schemas/TypeMedicalTransportValue"},{"type":"null"}],"description":"Transport outcome of the incident.","x-ui-label":"The transport outcome of the incident."},"patient_care_report_id":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Patient Care Report Id","description":"Patient care report identification number.","examples":["VAE456M1-02202024"],"x-ui-label":"The patient care report identification number."}},"additionalProperties":false,"type":"object","required":["patient_care_evaluation"],"title":"MedicalPayload"},"MedicalResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"patient_care_evaluation":{"type":"string","title":"Patient Care Evaluation","description":"Status of patient care needs based on evaluation."},"patient_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Patient Status","description":"Status of the patient after FD arrival and intervention."},"transport_disposition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Transport Disposition","description":"Transport outcome of the incident."},"patient_care_report_id":{"anyOf":[{"type":"string","maxLength":280,"minLength":1},{"type":"null"}],"title":"Patient Care Report Id","description":"Patient care report identification number.","examples":["VAE456M1-02202024"]}},"additionalProperties":false,"type":"object","required":["last_modified","patient_care_evaluation"],"title":"MedicalResponse"},"NoActivityReportPayload":{"properties":{"month_year":{"type":"string","pattern":"^[0-1][0-9]/20[2-9][0-9]$","title":"Month Year","description":"MM/YYYY formatted month and year of the period of no activity.","examples":["12/2025","06/2026","01/2025"]}},"additionalProperties":false,"type":"object","required":["month_year"],"title":"NoActivityReportPayload"},"NoActivityReportResponse":{"properties":{"month_year":{"type":"string","pattern":"^[0-1][0-9]/20[2-9][0-9]$","title":"Month Year","description":"MM/YYYY formatted month and year of the period of no activity.","examples":["12/2025","06/2026","01/2025"]},"neris_uid":{"type":"string","format":"uuid","title":"Neris Uid","description":"Unique identifier of the report"},"submitted_by":{"type":"string","format":"uuid","title":"Submitted By","description":"Account id of the report submitter"},"neris_id_entity":{"type":"string","pattern":"^FD\\d{8}$","title":"Neris Id Entity","description":"Unique identifier of the entity the report was submitted on behalf of","examples":["FD24027334","FD24160543"]},"created":{"type":"string","format":"date-time","title":"Created","description":"Date and time the report was created","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]}},"type":"object","required":["month_year","neris_uid","submitted_by","neris_id_entity","created"],"title":"NoActivityReportResponse"},"NoactionPayload":{"properties":{"type":{"type":"string","const":"NOACTION","title":"Type","description":"`NOACTION` indicates no action was taken","default":"NOACTION"},"noaction_type":{"$ref":"#/components/schemas/TypeNoactionValue","description":"Reason no action taken by the fire department on the scene of the incident.","x-ui-label":"Actions that were taken as part of the incident response.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["noaction_type"],"title":"NoactionPayload"},"NoactionResponse":{"properties":{"type":{"type":"string","const":"NOACTION","title":"Type","description":"`NOACTION` indicates no action was taken","default":"NOACTION"},"noaction_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Noaction Type","description":"Reason no action taken by the fire department on the scene of the incident."}},"type":"object","title":"NoactionResponse"},"NoinjuryPayload":{"properties":{"type":{"type":"string","const":"UNINJURED","title":"Type","x-ui-label":"Describe the nature of the casualty.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"NoinjuryPayload"},"NoinjuryResponse":{"properties":{"type":{"type":"string","title":"Type"}},"type":"object","required":["type"],"title":"NoinjuryResponse"},"NonFfRescuePayload":{"properties":{"type":{"type":"string","enum":["RESCUED_BY_NONFIREFIGHTER","SELF_EVACUATION","NO_RESCUE_NEEDED"],"title":"Type","x-ui-label":"Describe whether the person was rescued, evacuated, or if there was no rescue.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"NonFfRescuePayload"},"NonFfRescueResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"}},"type":"object","required":["last_modified","type"],"title":"NonFfRescueResponse"},"NonfdAidResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type","description":"Type of non-fire department entities giving aid received."}},"type":"object","required":["last_modified","type"],"title":"NonfdAidResponse"},"NonremovalPayload":{"properties":{"type":{"type":"string","enum":["EXTRICATION","DISENTANGLEMENT","RECOVERY","OTHER"],"title":"Type","x-ui-label":"Describe the primary rescue action taken.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"NonremovalPayload"},"NonremovalResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"incident_neris_id":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Incident Neris Id","description":"NERIS ID of the incident. This is comprised of the department NERIS ID concatenated with `dispatch.internal_id` and the epoch time of `dispatch.call_create` in the record as initially submitted. An incident's NERIS_ID is immutable, and so should not be relied upon for extraction of `internal_id` or `call_create`, as these attributes could have been updated.","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"]},"type":{"type":"string","title":"Type"}},"type":"object","required":["incident_neris_id","type"],"title":"NonremovalResponse"},"OtherAlarmNotPresentPayload":{"properties":{"type":{"type":"string","enum":["NOT_PRESENT","NOT_APPLICABLE"],"title":"Type","x-ui-label":"Describe whether other alarm systems for detecting gases or other harmful conditions were installed within the building.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"OtherAlarmNotPresentPayload"},"OtherAlarmNotPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"}},"type":"object","required":["last_modified","type"],"title":"OtherAlarmNotPresentResponse"},"OtherAlarmPayload":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/OtherAlarmPresentPayload"},{"$ref":"#/components/schemas/OtherAlarmNotPresentPayload"}],"title":"Presence","discriminator":{"propertyName":"type","mapping":{"NOT_APPLICABLE":"#/components/schemas/OtherAlarmNotPresentPayload","NOT_PRESENT":"#/components/schemas/OtherAlarmNotPresentPayload","PRESENT":"#/components/schemas/OtherAlarmPresentPayload"}}}},"additionalProperties":false,"type":"object","required":["presence"],"title":"OtherAlarmPayload"},"OtherAlarmPresentPayload":{"properties":{"type":{"type":"string","const":"PRESENT","title":"Type","x-ui-label":"Describe whether other alarm systems for detecting gases or other harmful conditions were installed within the building.","x-ui-hint":"Select one."},"alarm_types":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeAlarmOtherValue"},"type":"array"},{"type":"null"}],"title":"Alarm Types","x-ui-label":"Describe the other alarm(s) present.","x-ui-hint":"Select all that apply."}},"additionalProperties":false,"type":"object","required":["type"],"title":"OtherAlarmPresentPayload"},"OtherAlarmPresentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"},"alarm_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Alarm Types"}},"type":"object","required":["last_modified","type"],"title":"OtherAlarmPresentResponse"},"OtherAlarmResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"presence":{"oneOf":[{"$ref":"#/components/schemas/OtherAlarmPresentResponse"},{"$ref":"#/components/schemas/OtherAlarmNotPresentResponse"}],"title":"Presence"}},"type":"object","required":["last_modified","presence"],"title":"OtherAlarmResponse"},"OtherPowergenHazardPayload":{"properties":{"type":{"type":"string","enum":["WIND_TURBINE","OTHER","NOT_APPLICABLE"],"title":"Type","x-ui-label":"Whether power generation hardware was involved in the incident.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"OtherPowergenHazardPayload"},"OtherPowergenHazardResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"}},"type":"object","required":["last_modified","type"],"title":"OtherPowergenHazardResponse"},"OutsideFireLocationDetailPayload":{"properties":{"type":{"type":"string","const":"OUTSIDE","title":"Type","default":"OUTSIDE"},"acres_burned":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Acres Burned","description":"Estimated number of acres burned during the incident.","examples":["0.5"],"x-ui-label":"Estimated number of acres burned during the incident."},"cause":{"$ref":"#/components/schemas/TypeFireCauseOutValue","description":"General categorization of the cause (or likely cause) of the outdoor fire.","x-ui-label":"General categorization of the cause (or likely cause) of the structure fire.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["cause"],"title":"OutsideFireLocationDetailPayload"},"OutsideFireLocationDetailResponse":{"properties":{"type":{"type":"string","const":"OUTSIDE","title":"Type","default":"OUTSIDE"},"acres_burned":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Acres Burned","description":"Estimated number of acres burned during the incident.","examples":["0.5"]},"cause":{"type":"string","title":"Cause","description":"General categorization of the cause (or likely cause) of the outdoor fire."}},"type":"object","required":["cause"],"title":"OutsideFireLocationDetailResponse"},"OutsideFirePayload":{"properties":{"incident_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Incident Name","description":"The name of the outdoor incident.","x-ui-label":"Incident Name","x-ui-hint":"The Incident Name, if applicable."},"incident_start":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Incident Start","description":"The time the entire incident began.","x-ui-label":"Time the incident began."},"incident_end":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Incident End","description":"The time the entire incident ended.","x-ui-label":"Time the incident ended."},"acres_burned":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Acres Burned","description":"The number of acres burned.","x-ui-label":"Enter an estimate of the number of acres burned."},"open_burning":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Open Burning","description":"If the fire was caused by open burning, was a permit issued?","x-ui-label":"If Open Burning was the cause, was a permit issued?"},"minor_involved":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Minor Involved","description":"Whether the fire's ignition had a minor involved.","x-ui-label":"Was there a minor involved in the ignition of the fire?"},"elevation":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Elevation","description":"The elevation of the ignition point (in feet) above the ground.","x-ui-label":"Describe the elevation of ignition (in feet)."},"flame_length":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Flame Length","description":"The length of the flame (in feet).","x-ui-label":"Describe the flame length (in feet)."},"incident_complexity":{"anyOf":[{"$ref":"#/components/schemas/TypeIncidentComplexityValue"},{"type":"null"}],"description":"The complexity of the outdoor incident.","x-ui-label":"Select the incident management type / complexity."},"general_cause":{"$ref":"#/components/schemas/TypeGeneralFireCauseValue","description":"The general cause of the outdoor fire.","x-ui-label":"Select the general cause of the fire."},"heat_sources":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeFireCauseOutValue"},"type":"array"},{"type":"null"}],"title":"Heat Sources","description":"The heat sources involved in the outdoor fire.","x-ui-label":"Select heat sources that contributed to the outdoor fire.","x-ui-hint":"Select all that apply."},"cause":{"$ref":"#/components/schemas/TypeOutdoorCauseValue","description":"The primary cause of the outdoor fire.","x-ui-label":"Select the specific cause of the fire."},"contributing_activities":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeOutdoorActivitiesValue"},"type":"array"},{"type":"null"}],"title":"Contributing Activities","description":"The activities contributing to the outdoor fire.","x-ui-label":"Select any activities that may have contributed to ignition.","x-ui-hint":"Select all that apply."},"fuel_arrangement":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeFuelArrangementValue"},"type":"array"},{"type":"null"}],"title":"Fuel Arrangement","description":"The arrangement of fuels involved.","x-ui-label":"Describe the arrangement of fuels involved.","x-ui-hint":"Select all that apply."},"fuel_size":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeFuelSizeValue"},"type":"array"},{"type":"null"}],"title":"Fuel Size","description":"The diameter (in inches) of the fuels involved.","x-ui-label":"Describe the diameter of the fuels involved (in inches).","x-ui-hint":"Select all that apply."},"fuel_distribution":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeFuelDistributionValue"},"type":"array"},{"type":"null"}],"title":"Fuel Distribution","description":"The distribution of fuels involved.","x-ui-label":"Describe the distribution of the fuels involved.","x-ui-hint":"Select all that apply."},"relative_position":{"anyOf":[{"$ref":"#/components/schemas/TypeRelativePositionValue"},{"type":"null"}],"description":"The relative position of the fire.","x-ui-label":"Describe the relative position of the fire."},"fire_aspect":{"anyOf":[{"$ref":"#/components/schemas/TypeAspectValue"},{"type":"null"}],"description":"The aspect of the fire.","x-ui-label":"Describe the aspect of the fire."},"rate_of_spread":{"anyOf":[{"$ref":"#/components/schemas/TypeRateOfSpreadValue"},{"type":"null"}],"description":"The rate of spread of the fire.","x-ui-label":"Describe the rate of spread of the fire."},"polygons":{"anyOf":[{"$ref":"#/components/schemas/HighPrecisionGeoMultipolygon"},{"type":"null"}],"description":"The polygon(s) of the area affected by the outdoor fire."},"ignition_point":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}],"description":"The ignition point for the entire fire. Between this and structure.ignition_source, only one should be set.","x-ui-label":"Indicate on the map where the inital point of ignition was."}},"additionalProperties":false,"type":"object","required":["general_cause","cause"],"title":"OutsideFirePayload"},"OutsideFireResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"incident_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Incident Name","description":"The name of the outdoor incident."},"incident_start":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Incident Start","description":"The time the entire incident began."},"incident_end":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]},{"type":"null"}],"title":"Incident End","description":"The time the entire incident ended."},"acres_burned":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Acres Burned","description":"The number of acres burned."},"open_burning":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Open Burning","description":"If the fire was caused by open burning, was a permit issued?"},"minor_involved":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Minor Involved","description":"Whether the fire's ignition had a minor involved."},"elevation":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Elevation","description":"The elevation of the ignition point (in feet) above the ground."},"flame_length":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Flame Length","description":"The length of the flame (in feet)."},"incident_complexity":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Incident Complexity","description":"The complexity of the outdoor incident."},"general_cause":{"type":"string","title":"General Cause","description":"The general cause of the outdoor fire."},"heat_sources":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Heat Sources","description":"The heat sources involved in the outdoor fire."},"cause":{"type":"string","title":"Cause","description":"The primary cause of the outdoor fire."},"contributing_activities":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Contributing Activities","description":"The activities contributing to the outdoor fire."},"fuel_arrangement":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Fuel Arrangement","description":"The arrangement of fuels involved."},"fuel_size":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Fuel Size","description":"The diameter (in inches) of the fuels involved."},"fuel_distribution":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Fuel Distribution","description":"The distribution of fuels involved."},"relative_position":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relative Position","description":"The relative position of the fire."},"fire_aspect":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fire Aspect","description":"The aspect of the fire."},"rate_of_spread":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rate Of Spread","description":"The rate of spread of the fire."},"polygons":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Polygons"},"ignition_point":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Ignition Point"}},"type":"object","required":["last_modified","general_cause","cause"],"title":"OutsideFireResponse"},"ParcelResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"summary":{"anyOf":[{"$ref":"#/components/schemas/ParcelSummary"},{"type":"null"}],"description":"Summary information for the parcel"}},"type":"object","required":["last_modified"],"title":"ParcelResponse"},"ParcelSummary":{"properties":{"building":{"anyOf":[{"$ref":"#/components/schemas/BuildingInfo"},{"type":"null"}],"description":"Building information for the parcel"},"assessment":{"anyOf":[{"$ref":"#/components/schemas/AssessmentInfo"},{"type":"null"}],"description":"Assessment for the parcel"}},"type":"object","title":"ParcelSummary","description":"Summary information for a parcel."},"PatchActionAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchActionActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchActionAction"},"PatchActionTacticAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchActionTacticActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchActionTacticAction"},"PatchAidAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchAidActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchAidAction"},"PatchCasualtyAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchCasualtyActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchCasualtyAction"},"PatchCasualtyRescueAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchCasualtyRescueActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchCasualtyRescueAction"},"PatchChemicalAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchChemicalActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchChemicalAction"},"PatchCommentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchCommentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchCommentAction"},"PatchCookingFireSuppressionAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchCookingFireSuppressionActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchCookingFireSuppressionAction"},"PatchCookingFireSuppressionNotPresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchCookingFireSuppressionNotPresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchCookingFireSuppressionNotPresentAction"},"PatchCookingFireSuppressionPresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchCookingFireSuppressionPresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchCookingFireSuppressionPresentAction"},"PatchCrossStreetAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchCrossStreetActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchCrossStreetAction"},"PatchCsstHazardAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchCsstHazardActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchCsstHazardAction"},"PatchDispatchAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchDispatchActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchDispatchAction"},"PatchDispatchTacticTimestampsAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchTacticTimestampsActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchDispatchTacticTimestampsAction"},"PatchDispatchUnitResponseAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchDispatchUnitResponseActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchDispatchUnitResponseAction"},"PatchElectricHazardAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchElectricHazardActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchElectricHazardAction"},"PatchElectricHazardFireAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchElectricHazardFireActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchElectricHazardFireAction"},"PatchExposureAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchExposureActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchExposureAction"},"PatchExternalExposureAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchExternalExposureActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchExternalExposureAction"},"PatchFfInjuryDetailsAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFfInjuryDetailsActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFfInjuryDetailsAction"},"PatchFfRescueAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFfRescueActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFfRescueAction"},"PatchFireAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireAction"},"PatchFireAlarmAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireAlarmActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireAlarmAction"},"PatchFireAlarmNotpresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireAlarmNotpresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireAlarmNotpresentAction"},"PatchFireAlarmPresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireAlarmPresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireAlarmPresentAction"},"PatchFireRemovalAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireRemovalActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireRemovalAction"},"PatchFireSuppressionAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireSuppressionActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireSuppressionAction"},"PatchFireSuppressionEffectiveAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireSuppressionEffectiveActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireSuppressionEffectiveAction"},"PatchFireSuppressionFailedAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireSuppressionFailedActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireSuppressionFailedAction"},"PatchFireSuppressionIneffectiveAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireSuppressionIneffectiveActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireSuppressionIneffectiveAction"},"PatchFireSuppressionNotPresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireSuppressionNotPresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireSuppressionNotPresentAction"},"PatchFireSuppressionOperationAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireSuppressionOperationActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireSuppressionOperationAction"},"PatchFireSuppressionPresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireSuppressionPresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireSuppressionPresentAction"},"PatchFireSuppressionTypeAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchFireSuppressionTypeActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchFireSuppressionTypeAction"},"PatchHazsitAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchHazsitActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchHazsitAction"},"PatchInUseAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchInUseActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchInUseAction"},"PatchIncidentAction":{"properties":{"neris_id":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Neris Id","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"]},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchIncidentActionProperties"}},"additionalProperties":false,"type":"object","required":["neris_id","action","properties"],"title":"PatchIncidentAction"},"PatchIncidentBaseAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchIncidentBaseActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchIncidentBaseAction"},"PatchIncidentTacticTimestampsAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchTacticTimestampsActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchIncidentTacticTimestampsAction"},"PatchIncidentTypeAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchIncidentTypeActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchIncidentTypeAction"},"PatchIncidentUnitResponseAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchIncidentUnitResponseActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchIncidentUnitResponseAction"},"PatchInjuryAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchInjuryActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchInjuryAction"},"PatchInternalExposureAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchInternalExposureActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchInternalExposureAction"},"PatchLocationAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchLocationActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchLocationAction"},"PatchLocationUseAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchLocationUseActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchLocationUseAction"},"PatchMedResponseAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchMedResponseActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchMedResponseAction"},"PatchMedicalAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchMedicalActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchMedicalAction"},"PatchMedicalOxygenHazardAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/neris__api__incident__patch_models___PatchMedicalOxygenHazardActionProperties__1"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchMedicalOxygenHazardAction"},"PatchMedicalOxygenHazardNotpresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/neris__api__incident__patch_models___PatchMedicalOxygenHazardActionProperties__2"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchMedicalOxygenHazardNotpresentAction"},"PatchMedicalOxygenHazardPresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchMedicalOxygenHazardPresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchMedicalOxygenHazardPresentAction"},"PatchNoactionAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchNoactionActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchNoactionAction"},"PatchNoinjuryAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchNoinjuryActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchNoinjuryAction"},"PatchNonFfRescueAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchNonFfRescueActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchNonFfRescueAction"},"PatchOtherAlarmAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchOtherAlarmActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchOtherAlarmAction"},"PatchOtherAlarmNotpresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchOtherAlarmNotpresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchOtherAlarmNotpresentAction"},"PatchOtherAlarmPresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchOtherAlarmPresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchOtherAlarmPresentAction"},"PatchOtherPowergenHazardAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchOtherPowergenHazardActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchOtherPowergenHazardAction"},"PatchOutsideFireLocationDetailAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchOutsideFireLocationDetailActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchOutsideFireLocationDetailAction"},"PatchPresenceKnownAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchPresenceKnownActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchPresenceKnownAction"},"PatchPvPowergenHazardAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchPvPowergenHazardActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchPvPowergenHazardAction"},"PatchReleaseAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchReleaseActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchReleaseAction"},"PatchRemovalAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchRemovalActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchRemovalAction"},"PatchRescueAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchRescueActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchRescueAction"},"PatchRescueMaydayAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchRescueMaydayActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchRescueMaydayAction"},"PatchSmokeAlarmAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchSmokeAlarmActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchSmokeAlarmAction"},"PatchSmokeAlarmNotpresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchSmokeAlarmNotpresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchSmokeAlarmNotpresentAction"},"PatchSmokeAlarmOperationAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchSmokeAlarmOperationActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchSmokeAlarmOperationAction"},"PatchSmokeAlarmOperationAlertedAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchSmokeAlarmOperationAlertedActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchSmokeAlarmOperationAlertedAction"},"PatchSmokeAlarmOperationFailedAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchSmokeAlarmOperationFailedActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchSmokeAlarmOperationFailedAction"},"PatchSmokeAlarmPresentAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchSmokeAlarmPresentActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchSmokeAlarmPresentAction"},"PatchStationPayload":{"properties":{"address_line_1":{"type":"string","maxLength":255,"minLength":2,"title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Address Line 2"},"city":{"type":"string","maxLength":255,"minLength":1,"title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","maxLength":10,"minLength":5,"title":"Zip Code"},"station_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Station Id"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Internal Id"},"staffing":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Staffing"},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"PatchStationPayload"},"PatchStructureFireLocationDetailAction":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"action":{"type":"string","const":"patch","title":"Action","description":"Patch values of a nested attribute"},"properties":{"$ref":"#/components/schemas/_PatchStructureFireLocationDetailActionProperties"}},"additionalProperties":false,"type":"object","required":["action","properties"],"title":"PatchStructureFireLocationDetailAction"},"PatchUnitPayload":{"properties":{"type":{"$ref":"#/components/schemas/TypeUnitValue","description":"The type of unit housed at the station, and its associated capability"},"staffing":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Staffing","description":"The minimum staffing required for the unit to be dispatched to an incident."},"dedicated_staffing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Dedicated Staffing","description":"Whether the unit has dedicated staffing."},"cad_designation_1":{"type":"string","pattern":"^[\\w\\(\\)\\#\\- ]{1,64}$","title":"Cad Designation 1","description":"A unit's designation in the computer-aided dispatch (CAD) system.","examples":["CAR15","Quint-42","HAZMAT #08"]},"cad_designation_2":{"anyOf":[{"type":"string","pattern":"^[\\w\\(\\)\\#\\- ]{1,64}$","examples":["CAR15","Quint-42","HAZMAT #08"]},{"type":"null"}],"title":"Cad Designation 2"}},"additionalProperties":false,"type":"object","title":"PatchUnitPayload","description":"\n\n
Validation Rules\n\n- `dedicated_staffing` must not be `true` if `staffing` is `0`.\n\n
"},"PopulationPayload":{"properties":{"protected":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Protected"},"source":{"anyOf":[{"$ref":"#/components/schemas/TypePopSourceValue"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"PopulationPayload"},"PopulationResponse":{"properties":{"protected":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Protected"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"}},"type":"object","title":"PopulationResponse"},"PowergenHazardPayload":{"properties":{"pv_other":{"oneOf":[{"$ref":"#/components/schemas/PvPowergenHazardPayload"},{"$ref":"#/components/schemas/OtherPowergenHazardPayload"}],"title":"Pv Other","discriminator":{"propertyName":"type","mapping":{"NOT_APPLICABLE":"#/components/schemas/OtherPowergenHazardPayload","OTHER":"#/components/schemas/OtherPowergenHazardPayload","PHOTOVOLTAICS":"#/components/schemas/PvPowergenHazardPayload","WIND_TURBINE":"#/components/schemas/OtherPowergenHazardPayload"}}}},"additionalProperties":false,"type":"object","required":["pv_other"],"title":"PowergenHazardPayload"},"PowergenHazardResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"pv_other":{"anyOf":[{"$ref":"#/components/schemas/PvPowergenHazardResponse"},{"$ref":"#/components/schemas/OtherPowergenHazardResponse"}],"title":"Pv Other"}},"type":"object","required":["last_modified","pv_other"],"title":"PowergenHazardResponse"},"PresenceKnownPayload":{"properties":{"presence_known_type":{"$ref":"#/components/schemas/TypeRescuePresenceKnownValue","description":" At what point the presence of an occupant was discovered during the incident duration.","x-ui-label":"Describe whether the presence of an occupant in need of rescue was known.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["presence_known_type"],"title":"PresenceKnownPayload"},"PresenceKnownResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"presence_known_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Presence Known Type","description":" At what point the presence of an occupant was discovered during the incident duration."}},"type":"object","required":["last_modified"],"title":"PresenceKnownResponse"},"ProductPayload":{"properties":{"description":{"type":"string","title":"Description","description":"The description of the product.","x-ui-label":"Enter a brief description of the product, if more details are needed."},"item_first_ignited":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Item First Ignited","description":"Whether the item was the first to ignite.","x-ui-label":"Was this the item first ignited?"},"manufacturer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manufacturer","description":"The manufacturer of the product.","x-ui-label":"Enter the manufacturer of the product involved in the incident."},"model_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Number","description":"The model number of the product.","x-ui-label":"Enter the model number of the product, if applicable."},"product_involved":{"$ref":"#/components/schemas/TypeConsumerProductValue","description":"The type of product involved in the fire.","x-ui-label":"Select the consumer product involved in the incident."},"contribution":{"anyOf":[{"$ref":"#/components/schemas/TypeProductContributionValue"},{"type":"null"}],"description":"The product's contribution to the fire.","x-ui-label":"Describe how the product contributed to the incident."}},"additionalProperties":false,"type":"object","required":["description","product_involved"],"title":"ProductPayload"},"ProductResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"description":{"type":"string","title":"Description","description":"The description of the product."},"item_first_ignited":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Item First Ignited","description":"Whether the item was the first to ignite."},"manufacturer":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Manufacturer","description":"The manufacturer of the product."},"model_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model Number","description":"The model number of the product."},"product_involved":{"type":"string","title":"Product Involved","description":"The type of product involved in the fire."},"contribution":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contribution","description":"The product's contribution to the fire."}},"type":"object","required":["last_modified","description","product_involved"],"title":"ProductResponse"},"PropertyAnalysisPayload":{"properties":{"parcel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parcel Id","description":"The ID of the parcel affected.","x-ui-label":"Parcel Identification Number"},"location":{"anyOf":[{"$ref":"#/components/schemas/LocationPayload"},{"type":"null"}],"description":"The location of the property affected."},"structures":{"anyOf":[{"items":{"$ref":"#/components/schemas/StructureAnalysisPayload"},"type":"array"},{"type":"null"}],"title":"Structures","description":"The structures on the property that were affected."},"point":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"PropertyAnalysisPayload","description":"\n\n
Validation Rules\n\n- A property must have a `parcel_id`, `location`, or `point`.\n- A property with a `location` must provide geocodable address information.\n\n
"},"PropertyAnalysisResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"parcel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parcel Id","description":"The ID of the parcel affected."},"location":{"anyOf":[{"$ref":"#/components/schemas/LocationResponse"},{"type":"null"}]},"structures":{"anyOf":[{"items":{"$ref":"#/components/schemas/StructureAnalysisResponse"},"type":"array"},{"type":"null"}],"title":"Structures","description":"The structures on the property that were affected."},"point":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Point"}},"type":"object","required":["last_modified"],"title":"PropertyAnalysisResponse"},"PutIncidentAnalysisPayload":{"properties":{"base":{"$ref":"#/components/schemas/IncidentAnalysisBasePayload"},"hazsit":{"anyOf":[{"$ref":"#/components/schemas/HazsitWithReleaseFactorsPayload"},{"type":"null"}],"description":"The hazmat/hazsit analysis for the incident."},"products":{"anyOf":[{"items":{"$ref":"#/components/schemas/ProductPayload"},"type":"array"},{"type":"null"}],"title":"Products","description":"The products involved in the incident."},"batteries":{"anyOf":[{"items":{"$ref":"#/components/schemas/BatteryPayload"},"type":"array"},{"type":"null"}],"title":"Batteries","description":"The batteries involved in the incident."},"outside_fire":{"anyOf":[{"$ref":"#/components/schemas/OutsideFirePayload","description":"Analysis for how the fire affected outdoor spaces."},{"type":"null"}]},"structure_fire_origin":{"anyOf":[{"$ref":"#/components/schemas/StructureFireOriginPayload","description":"Analysis for how the fire started if it started in a structure."},{"type":"null"}]},"properties":{"anyOf":[{"items":{"$ref":"#/components/schemas/PropertyAnalysisPayload"},"type":"array"},{"type":"null"}],"title":"Properties","description":"Analysis for the property/structures that were affected by the incident. A property must have a `parcel_id`, `location`, or `point` so that the property can be placed on a map."},"vehicles":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/VehicleAutomobilePayload"},{"$ref":"#/components/schemas/VehicleOtherPayload"}],"discriminator":{"propertyName":"type","mapping":{"AERIAL_LIFT_PLATFORM":"#/components/schemas/VehicleOtherPayload","AERIAL_OTHER":"#/components/schemas/VehicleOtherPayload","AIRPLANE":"#/components/schemas/VehicleOtherPayload","ATV":"#/components/schemas/VehicleOtherPayload","AUTOMOBILE":"#/components/schemas/VehicleAutomobilePayload","BICYCLE":"#/components/schemas/VehicleOtherPayload","BOAT_SHIP":"#/components/schemas/VehicleOtherPayload","BUS":"#/components/schemas/VehicleOtherPayload","CARGO":"#/components/schemas/VehicleOtherPayload","CONSTRUCTION":"#/components/schemas/VehicleOtherPayload","FARM":"#/components/schemas/VehicleOtherPayload","FOOD_TRUCK":"#/components/schemas/VehicleOtherPayload","FREIGHT":"#/components/schemas/VehicleOtherPayload","HELICOPTER":"#/components/schemas/VehicleOtherPayload","JET_SKI":"#/components/schemas/VehicleOtherPayload","MILITARY_OTHER":"#/components/schemas/VehicleOtherPayload","MOTORCYCLE":"#/components/schemas/VehicleOtherPayload","MOTORIZED_HOME":"#/components/schemas/VehicleOtherPayload","OTHER":"#/components/schemas/VehicleOtherPayload","RAIL":"#/components/schemas/VehicleOtherPayload","SCOOTER_MOPED":"#/components/schemas/VehicleOtherPayload","SPORT_UTILITY":"#/components/schemas/VehicleOtherPayload","TOW_TRUCK":"#/components/schemas/VehicleOtherPayload","TRAILER":"#/components/schemas/VehicleOtherPayload","UTILITY_VEHICLE":"#/components/schemas/VehicleOtherPayload","VAN":"#/components/schemas/VehicleOtherPayload"}}},"type":"array"},{"type":"null"}],"title":"Vehicles","description":"The vehicles involved in the incident."}},"additionalProperties":false,"type":"object","required":["base"],"title":"PutIncidentAnalysisPayload","description":"\n\n
Validation Rules\n\n- At least one of `structure_fire_origin`, `outside_fire`, `hazsit`, `products`, `batteries`, `properties`, or `vehicles` must be provided.\n- If one structure is the ignition source, `structure_fire_origin` must also be provided.\n- `structure_fire_origin` must have exactly one structure with `ignition_source` set.\n- If `outside_fire.ignition_point` is set, the fire cannot have started in a structure.\n- Only one product can have `item_first_ignited` set.\n\n
"},"PutIncidentPayload":{"properties":{"base":{"$ref":"#/components/schemas/IncidentBasePayload"},"incident_types":{"items":{"$ref":"#/components/schemas/IncidentTypePayload"},"type":"array","maxItems":3,"minItems":1,"title":"Incident Types"},"special_modifiers":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeSpecialModifierValue"},"type":"array"},{"type":"null"}],"title":"Special Modifiers"},"aids":{"anyOf":[{"items":{"$ref":"#/components/schemas/AidPayload"},"type":"array"},{"type":"null"}],"title":"Aids"},"nonfd_aids":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeAidNonfdValue"},"type":"array"},{"type":"null"}],"title":"Nonfd Aids"},"actions_tactics":{"anyOf":[{"$ref":"#/components/schemas/ActionTacticPayload"},{"type":"null"}]},"dispatch":{"$ref":"#/components/schemas/DispatchPayload"},"tactic_timestamps":{"anyOf":[{"$ref":"#/components/schemas/IncidentTacticTimestampsPayload"},{"type":"null"}]},"unit_responses":{"anyOf":[{"items":{"$ref":"#/components/schemas/IncidentUnitResponsePayload"},"type":"array"},{"type":"null"}],"title":"Unit Responses"},"exposures":{"anyOf":[{"items":{"$ref":"#/components/schemas/ExposurePayload"},"type":"array"},{"type":"null"}],"title":"Exposures"},"casualty_rescues":{"anyOf":[{"items":{"$ref":"#/components/schemas/CasualtyRescuePayload"},"type":"array"},{"type":"null"}],"title":"Casualty Rescues"},"fire_detail":{"anyOf":[{"$ref":"#/components/schemas/FirePayload"},{"type":"null"}],"description":"Fire module."},"hazsit_detail":{"anyOf":[{"$ref":"#/components/schemas/HazsitPayload"},{"type":"null"}],"description":"Hazsit module."},"medical_details":{"anyOf":[{"items":{"$ref":"#/components/schemas/MedicalPayload"},"type":"array"},{"type":"null"}],"title":"Medical Details","description":"Medical module(s)."},"smoke_alarm":{"anyOf":[{"$ref":"#/components/schemas/SmokeAlarmPayload"},{"type":"null"}]},"fire_alarm":{"anyOf":[{"$ref":"#/components/schemas/FireAlarmPayload"},{"type":"null"}]},"other_alarm":{"anyOf":[{"$ref":"#/components/schemas/OtherAlarmPayload"},{"type":"null"}]},"fire_suppression":{"anyOf":[{"$ref":"#/components/schemas/FireSuppressionPayload"},{"type":"null"}]},"cooking_fire_suppression":{"anyOf":[{"$ref":"#/components/schemas/CookingFireSuppressionPayload"},{"type":"null"}]},"electric_hazards":{"anyOf":[{"items":{"$ref":"#/components/schemas/ElectricHazardPayload"},"type":"array"},{"type":"null"}],"title":"Electric Hazards"},"powergen_hazards":{"anyOf":[{"items":{"$ref":"#/components/schemas/PowergenHazardPayload"},"type":"array"},{"type":"null"}],"title":"Powergen Hazards"},"csst_hazard":{"anyOf":[{"$ref":"#/components/schemas/CsstHazardPayload"},{"type":"null"}]},"medical_oxygen_hazard":{"anyOf":[{"$ref":"#/components/schemas/MedicalOxygenHazardPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["base","incident_types","dispatch"],"title":"PutIncidentPayload","description":"\n\n
Validation Rules\n\n- `fire_details` only possible if at least one `FIRE` incident type is present.\n- `hazsit_details` only possible if at least one `HAZSIT` incident type is present.\n- `medical_details` only possible if at least one `MEDICAL` incident type is present.\n- Incidents with a `FIRE||STRUCTURE_FIRE` type require fire suppression and alarm modules: `smoke_alarm`, `fire_alarm`, `other_alarm`, `fire_suppression`, unless all aids are `SUPPORT_AID` that was `GIVEN` to another department.\n- Incidents with a `FIRE||STRUCTURE_FIRE||CONFINED_COOKING_APPLIANCE_FIRE` type require the `cooking_fire_suppression` module, unless all aids are `SUPPORT_AID` that was `GIVEN` to another department.\n- Only one `incident_type` can be marked with `primary` as `true`.\n- `aids.department_neris_id` cannot be the same as `base.department_neris_id`.\n- `aids.department_neris_id` values must be unique.\n- `incident_types.type` values must be unique.\n\n
","examples":[{"actions_tactics":{"action_noaction":{"actions":["EMERGENCY_MEDICAL_CARE||PATIENT_ASSESSMENT","EMERGENCY_MEDICAL_CARE||PROVIDE_ADVANCED_LIFE_SUPPORT","EMERGENCY_MEDICAL_CARE||PROVIDE_TRANSPORT","COMMAND_AND_CONTROL||ESTABLISH_INCIDENT_COMMAND","COMMAND_AND_CONTROL||INCIDENT_ASSESSMENT_COMPLETED","SUPPRESSION||STRUCTURAL_FIRE_SUPPRESSION||INTERIOR","VENTILATION||HORIZONTAL||DURING_SUPPRESSION","VENTILATION||HYDRAULIC||POST_SUPPRESSION","SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH||PRIOR_TO_SUPPRESSION","SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH||DURING_SUPPRESSION","SALVAGE_AND_OVERHAUL","PERSONNEL_CONTAMINATION_REDUCTION||ON_SCENE_CONTAMINATION_REDUCTION"],"type":"ACTION"}},"aids":[{"aid_direction":"GIVEN","aid_type":"SUPPORT_AID","department_neris_id":"FD48085117"}],"base":{"animals_rescued":0,"department_neris_id":"FD24027077","displacement_causes":["FIRE","SMOKE"],"displacement_count":4,"impediment_narrative":"Enhance.","incident_number":"1725295157","location":{"additional_attributes":{},"country":"US","county":"Howard County","cross_streets":[],"floor":"1","location_aliases":[{"cross_streets":[],"location_aliases":[]}],"number":6200,"place_type":"OFFICE","postal_code":"21045","postal_code_extension":"5856","postal_community":"Columbia","site":"Columbia Crossing","state":"MD","street":"Dobbin","street_postfix":"LANE","street_prefix_modifier":"Old","subsite":"Fire Safety Research Institute","unit_prefix":"Suite","unit_value":"150"},"location_use":{"in_use":{"in_use":true},"secondary_use":"RESIDENTIAL||UNHOUSED_TEMPORARY_SHELTER","use_type":"COMMERCIAL||OFFICE_OTHER_TECHNICAL_SERVICES","vacancy_cause":"NEW_CONSTRUCTION_REMODEL"},"outcome_narrative":"Final outcome narrative.","people_present":true,"point":{"crs":4326,"geometry":{"coordinates":[-76.815616,39.201414],"type":"Point"}}},"casualty_rescues":[{"birth_month_year":"06/1972","casualty":{"injury_or_noninjury":{"cause":"STRUCK_CONTACT_WITH_OBJECT","type":"INJURED_NONFATAL"}},"gender":"MALE","race":"WHITE","rank":"Captain","rescue":{"ffrescue_or_nonffrescue":{"impediments":["HOARDING_CONDITIONS","ACCESS_LIMITATIONS"],"removal_or_nonremoval":{"gas_isolation":false,"rescue_path_type":"REMOVAL_ALONG_ALT_PATH","room_type":"BEDROOM","type":"REMOVAL_FROM_STRUCTURE"},"type":"RESCUED_BY_FIREFIGHTER"},"mayday":{"mayday":true,"relative_suppression_time":"PRE_SUPPRESSION","rit_activated":true}},"type":"FF","years_of_service":12},{"birth_month_year":"01/1995","casualty":{"injury_or_noninjury":{"type":"UNINJURED"}},"rescue":{"ffrescue_or_nonffrescue":{"actions":[],"impediments":["HOARDING_CONDITIONS","ACCESS_LIMITATIONS"],"removal_or_nonremoval":{"elevation_type":"ON_BED","fire_removal":{"relative_suppression_time":"PRE_SUPPRESSION"},"gas_isolation":true,"room_type":"BEDROOM","type":"REMOVAL_FROM_STRUCTURE"},"type":"RESCUED_BY_FIREFIGHTER"}},"type":"NONFF"}],"cooking_fire_suppression":{"presence":{"type":"NOT_PRESENT"}},"csst_hazard":{"grounded":"UNKNOWN","ignition_source":true,"lightning_suspected":"NO"},"dispatch":{"automatic_alarm":false,"call_answered":"2025-05-03T18:49:47+00:00","call_arrival":"2025-05-03T18:49:23+00:00","call_create":"2025-05-03T18:50:02+00:00","center_id":"1234","comments":[{"comment":"Dispatch for gas leak/CO with potential working fire.","timestamp":"2025-05-03T18:50:12Z"}],"disposition":"Structure fire suppressed on first alarm, 1 occupant rescued, occupant refused treatment.","incident_clear":"2025-05-03T19:36:33+00:00","incident_code":"STRF1","incident_number":"1725295157","location":{"additional_attributes":{},"country":"US","county":"Howard County","cross_streets":[],"floor":"1","location_aliases":[{"cross_streets":[],"location_aliases":[]}],"number":6200,"place_type":"OFFICE","postal_code":"21045","postal_code_extension":"5856","postal_community":"Columbia","site":"Columbia Crossing","state":"MD","street":"Dobbin","street_postfix":"LANE","street_prefix_modifier":"Old","subsite":"Fire Safety Research Institute","unit_prefix":"Suite","unit_value":"150"},"tactic_timestamps":{"extrication_complete":"2025-05-03T19:01:00+00:00","suppression_complete":"2025-05-03T19:01:00+00:00","water_on_fire":"2025-05-03T18:59:00+00:00"},"unit_responses":[{"dispatch":"2025-05-03T18:50:12+00:00","enroute_to_scene":"2025-05-03T18:51:13+00:00","med_responses":[{}],"on_scene":"2025-05-03T18:53:54+00:00","point":{"crs":4326,"geometry":{"coordinates":[-76.820114,39.186229],"type":"Point"}},"reported_unit_id":"E84","response_mode":"EMERGENT","transport_mode":"NON_EMERGENT","unable_to_dispatch":false,"unit_clear":"2025-05-03T19:36:33+00:00","unit_neris_id":"FD24027077S000U001"},{"dispatch":"2025-05-03T18:50:12+00:00","enroute_to_scene":"2025-05-03T18:50:56+00:00","med_responses":[{}],"on_scene":"2025-05-03T18:52:47+00:00","point":{"crs":4326,"geometry":{"coordinates":[-76.820114,39.186229],"type":"Point"}},"reported_unit_id":"MED2","response_mode":"EMERGENT","unable_to_dispatch":false,"unit_clear":"2025-05-03T19:36:33+00:00","unit_neris_id":"FD24027077S000U011"},{"dispatch":"2025-05-03T18:50:12+00:00","enroute_to_scene":"2025-05-03T18:51:31+00:00","med_responses":[{}],"on_scene":"2025-05-03T18:54:12+00:00","point":{"crs":4326,"geometry":{"coordinates":[-76.820114,39.186229],"type":"Point"}},"reported_unit_id":"BC1","response_mode":"NON_EMERGENT","unable_to_dispatch":false,"unit_clear":"2025-05-03T19:36:33+00:00","unit_neris_id":"FD24027077S000U022"}]},"electric_hazards":[{"fire_details":{"reignition":false,"suppression_types":["SUPPRESSION_WATER_ADDITIVE","FIRE_BLANKET"]},"source_or_target":"SOURCE","type":"CONSUMER_PRODUCTS||TOY"}],"exposures":[{"damage_type":"MODERATE_DAMAGE","displacement_causes":["FIRE","SMOKE"],"displacement_count":2,"location":{"unit_prefix":"Suite","unit_value":"180"},"location_detail":{"type":"INTERNAL_EXPOSURE"},"people_present":true,"point":{"crs":4326,"geometry":{"coordinates":[-76.820114,39.186229],"type":"Point"}}}],"fire_alarm":{"presence":{"alarm_types":["MANUAL_AND_AUTOMATIC"],"operation_type":"OPERATED_ALERTED_OCCUPANT","type":"PRESENT"}},"fire_detail":{"investigation_needed":"YES","investigation_types":["INVESTIGATED_ON_SCENE_RESOURCE"],"location_detail":{"arrival_condition":"SMOKE_SHOWING","cause":"OPERATING_EQUIPMENT","damage_type":"MODERATE_DAMAGE","floor_of_origin":1,"progression_evident":true,"room_of_origin_type":"UTILITY_ROOM","type":"STRUCTURE"},"suppression_appliances":["MEDIUM_DIAMETER_FIRE_HOSE","BOOSTER_FIRE_HOSE"],"water_supply":"TANK_WATER"},"fire_suppression":{"presence":{"operation_type":{"effectiveness":{"sprinklers_activated":33,"type":"OPERATED_EFFECTIVE"}},"suppression_types":[{"full_partial":"FULL","type":"WET_PIPE_SPRINKLER_SYSTEM"},{"full_partial":"PARTIAL","type":"DRY_PIPE_SPRINKLER_SYSTEM"},{"type":"PRE_ACTION_SYSTEM"}],"type":"PRESENT"}},"hazsit_detail":{"chemicals":[{"dot_class":"GASES","name":"Carbon Monoxide","release":{"cause":"UNINTENTIONAL","physical_state":"GAS","released_into":"AIR"},"release_occurred":true},{"dot_class":"GASES","name":"HYDROGEN CYANIDE","release":{"cause":"UNINTENTIONAL","physical_state":"GAS","released_into":"AIR"},"release_occurred":true}],"disposition":"COMPLETED_WITH_FIRE_SERVICE_PRESENT","evacuated":2},"incident_types":[{"primary":true,"type":"FIRE||STRUCTURE_FIRE||ROOM_AND_CONTENTS_FIRE"},{"primary":false,"type":"HAZSIT||HAZARDOUS_MATERIALS||GAS_LEAK_ODOR"},{"type":"MEDICAL||INJURY||CARBON_MONOXIDE_OTHER_INHALATION_INJURY"}],"medical_details":[{"patient_care_evaluation":"PATIENT_SUPPORT_SERVICES_PROVIDED","patient_care_report_id":"VAE456M1-02202025","patient_status":"IMPROVED","transport_disposition":"OTHER_AGENCY_TRANSPORT"}],"medical_oxygen_hazard":{"presence":{"contributed_to_flame_spread":true,"type":"PRESENT"}},"nonfd_aids":["UTILITIES_PUBLIC_WORKS","HOUSING_SERVICES"],"other_alarm":{"presence":{"alarm_types":["HEAT_DETECTOR","CARBON_MONOXIDE"],"type":"PRESENT"}},"smoke_alarm":{"presence":{"alarm_types":["INTERCONNECTED","HARDWIRED"],"operation":{"alerted_failed_other":{"occupant_action":"EVACUATED","type":"OPERATED_ALERTED_OCCUPANT"}},"type":"PRESENT","working":true}},"special_modifiers":["URBAN_CONFLAGRATION","VIOLENCE_AGAINST_RESPONDER"],"tactic_timestamps":{"command_established":"2025-05-03T18:53:59+00:00","completed_sizeup":"2025-05-03T18:55:08+00:00","extrication_complete":"2025-05-03T19:01:00+00:00"},"unit_responses":[{"med_responses":[{}],"reported_unit_id":"E84","response_mode":"EMERGENT","staffing":4,"transport_mode":"NON_EMERGENT","unable_to_dispatch":false,"unit_neris_id":"FD24027077S000U001"},{"med_responses":[{}],"reported_unit_id":"BC1","response_mode":"EMERGENT","staffing":1,"unable_to_dispatch":false,"unit_neris_id":"FD24027077S000U022"},{"med_responses":[{"at_patient":"2025-05-03T18:53:01+00:00"}],"reported_unit_id":"MED2","response_mode":"NON_EMERGENT","staffing":2,"unable_to_dispatch":false,"unit_neris_id":"FD24027077S000U011"}]}]},"PvPowergenHazardPayload":{"properties":{"type":{"type":"string","const":"PHOTOVOLTAICS","title":"Type","x-ui-label":"Whether power generation hardware was involved in the incident.","x-ui-hint":"Select one."},"source_or_target":{"anyOf":[{"$ref":"#/components/schemas/TypeEmerghazPvIgnValue"},{"type":"null"}],"description":"Whether photovoltaics were the source of ignition or target. Only applies to `PHOTOVOLTAICS` type.","x-ui-label":"Were photovoltaics the source of ignition or target?","x-ui-hint":"Select one."},"pv_type":{"anyOf":[{"$ref":"#/components/schemas/TypeEmerghazPvValue"},{"type":"null"}],"description":"Type of photovoltaics were involved in the incident. Only applies to `PHOTOVOLTAICS` type.","x-ui-label":"What type of photovoltaics were involved in the incident?","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"PvPowergenHazardPayload"},"PvPowergenHazardResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"},"source_or_target":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Or Target","description":"Whether photovoltaics were the source of ignition or target. Only applies to `PHOTOVOLTAICS` type."},"pv_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pv Type","description":"Type of photovoltaics were involved in the incident. Only applies to `PHOTOVOLTAICS` type."}},"type":"object","required":["last_modified","type"],"title":"PvPowergenHazardResponse"},"RegionPayload":{"properties":{"crs":{"anyOf":[{"type":"integer"},{"type":"string"}],"title":"Crs","description":"EPSG code for the coordinate reference system","default":4326},"geometry":{"$ref":"#/components/schemas/GeoJsonMultiPolygon"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Internal Id"}},"type":"object","required":["geometry"],"title":"RegionPayload"},"RegionResponse":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Internal Id"},"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"RegionResponse"},"RegionSetPayload":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"primary":{"type":"boolean","title":"Primary"},"coverage":{"type":"boolean","title":"Coverage"},"juris":{"type":"boolean","title":"Juris"},"type":{"$ref":"#/components/schemas/TypeRegionValue"},"regions":{"items":{"$ref":"#/components/schemas/RegionPayload"},"type":"array","title":"Regions"}},"additionalProperties":false,"type":"object","required":["primary","coverage","juris","type"],"title":"RegionSetPayload"},"RegionSetResponse":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"primary":{"type":"boolean","title":"Primary"},"coverage":{"type":"boolean","title":"Coverage"},"juris":{"type":"boolean","title":"Juris"},"type":{"type":"string","title":"Type"},"regions":{"items":{"$ref":"#/components/schemas/RegionResponse"},"type":"array","title":"Regions"}},"type":"object","required":["primary","coverage","juris","type"],"title":"RegionSetResponse"},"ReleasePayload":{"properties":{"estimated_amount":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Amount","description":"Estimated amount released by volume or weight.","examples":[1345],"x-ui-label":"Enter the quantity released."},"unit_of_measurement":{"anyOf":[{"$ref":"#/components/schemas/TypeHazardUnitValue"},{"type":"null"}],"description":"Unit of measurement of the estimated amount released.","x-ui-label":"Select the unit of the quantity released.","x-ui-hint":"Select one."},"physical_state":{"anyOf":[{"$ref":"#/components/schemas/TypeHazardPhysicalStateValue"},{"type":"null"}],"description":"Physical state of the chemical.","x-ui-label":"Select the physical state of the material.","x-ui-hint":"Select one."},"released_into":{"anyOf":[{"$ref":"#/components/schemas/TypeHazardReleasedIntoValue"},{"type":"null"}],"description":"Environment which was contaminated.","x-ui-label":"Select the environment contaminated by the release.","x-ui-hint":"Select all that apply."},"cause":{"anyOf":[{"$ref":"#/components/schemas/TypeHazardCauseValue"},{"type":"null"}],"description":"Cause of the hazmat release.","x-ui-label":"Select the cause of the release."}},"additionalProperties":false,"type":"object","title":"ReleasePayload"},"ReleaseResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"estimated_amount":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Amount","description":"Estimated amount released by volume or weight.","examples":[1345]},"unit_of_measurement":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unit Of Measurement","description":"Unit of measurement of the estimated amount released."},"physical_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical State","description":"Physical state of the chemical."},"released_into":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Released Into","description":"Environment which was contaminated."},"cause":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cause","description":"Cause of the hazmat release."}},"type":"object","required":["last_modified"],"title":"ReleaseResponse"},"ReleaseWithReleaseFactorsPayload":{"properties":{"estimated_amount":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Amount","description":"Estimated amount released by volume or weight.","examples":[1345]},"unit_of_measurement":{"anyOf":[{"$ref":"#/components/schemas/TypeHazardUnitValue"},{"type":"null"}],"description":"Unit of measurement of the estimated amount released."},"physical_state":{"anyOf":[{"$ref":"#/components/schemas/TypeHazardPhysicalStateValue"},{"type":"null"}],"description":"Physical state of the chemical."},"released_into":{"anyOf":[{"$ref":"#/components/schemas/TypeHazardReleasedIntoValue"},{"type":"null"}],"description":"Environment which was contaminated."},"cause":{"anyOf":[{"$ref":"#/components/schemas/TypeHazardCauseValue"},{"type":"null"}],"description":"Cause of the hazmat release."},"release_factors":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeHazsitReleaseFactorsValue"},"type":"array"},{"type":"null"}],"title":"Release Factors","description":"The factors that contributed to the release of the hazardous material."}},"additionalProperties":false,"type":"object","title":"ReleaseWithReleaseFactorsPayload"},"ReleaseWithReleaseFactorsResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"estimated_amount":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Amount","description":"Estimated amount released by volume or weight.","examples":[1345]},"unit_of_measurement":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unit Of Measurement","description":"Unit of measurement of the estimated amount released."},"physical_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Physical State","description":"Physical state of the chemical."},"released_into":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Released Into","description":"Environment which was contaminated."},"cause":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cause","description":"Cause of the hazmat release."},"release_factors":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Release Factors"}},"type":"object","required":["last_modified"],"title":"ReleaseWithReleaseFactorsResponse"},"RemovalPayload":{"properties":{"gas_isolation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Gas Isolation","description":"Whether the space was isolated from the flow of heat and/or toxic gases.","x-ui-label":"Describe whether the location at which the person was found was isolated from the flow of heat and/or toxic gases?","x-ui-hint":"Select one."},"type":{"type":"string","const":"REMOVAL_FROM_STRUCTURE","title":"Type","x-ui-label":"Describe the primary rescue action taken.","x-ui-hint":"Select one."},"room_type":{"anyOf":[{"$ref":"#/components/schemas/TypeRoomValue"},{"type":"null"}],"description":"Type of room/space from which the occupant was rescued.","x-ui-label":"Describe the type of room/space was occupant rescued from.","x-ui-hint":"Select one."},"elevation_type":{"anyOf":[{"$ref":"#/components/schemas/TypeRescueElevationValue"},{"type":"null"}],"description":"Elevation at which the occupant was found.","x-ui-label":"At what elevation was the person found?","x-ui-hint":"Select one."},"rescue_path_type":{"anyOf":[{"$ref":"#/components/schemas/TypeRescuePathValue"},{"type":"null"}],"description":"The type of route or pathway utilized to remove the person.","x-ui-label":"Describe the route along which removal from the structure occurred.","x-ui-hint":"Select one."},"fire_removal":{"anyOf":[{"$ref":"#/components/schemas/FireRemovalPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["type"],"title":"RemovalPayload"},"RemovalResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"incident_neris_id":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Incident Neris Id","description":"NERIS ID of the incident. This is comprised of the department NERIS ID concatenated with `dispatch.internal_id` and the epoch time of `dispatch.call_create` in the record as initially submitted. An incident's NERIS_ID is immutable, and so should not be relied upon for extraction of `internal_id` or `call_create`, as these attributes could have been updated.","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"]},"gas_isolation":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Gas Isolation","description":"Whether the space was isolated from the flow of heat and/or toxic gases."},"type":{"type":"string","title":"Type"},"room_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Room Type","description":"Type of room/space from which the occupant was rescued."},"elevation_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Elevation Type","description":"Elevation at which the occupant was found."},"rescue_path_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rescue Path Type","description":"The type of route or pathway utilized to remove the person."},"fire_removal":{"anyOf":[{"$ref":"#/components/schemas/FireRemovalResponse"},{"type":"null"}]}},"type":"object","required":["incident_neris_id","type"],"title":"RemovalResponse"},"RemoveAidAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveAidAction"},"RemoveCasualtyRescueAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveCasualtyRescueAction"},"RemoveChemicalAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveChemicalAction"},"RemoveCommentAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveCommentAction"},"RemoveCrossStreetAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveCrossStreetAction"},"RemoveDispatchUnitResponseAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveDispatchUnitResponseAction"},"RemoveElectricHazardAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveElectricHazardAction"},"RemoveExposureAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveExposureAction"},"RemoveIncidentTypeAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveIncidentTypeAction"},"RemoveIncidentUnitResponseAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveIncidentUnitResponseAction"},"RemoveLocationAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveLocationAction"},"RemoveMedResponseAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveMedResponseAction"},"RemoveMedicalAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemoveMedicalAction"},"RemovePowergenHazardAction":{"properties":{"action":{"type":"string","const":"remove","title":"Action","description":"Remove an element from the array"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier (nuid) of the element to be removed"}},"additionalProperties":false,"type":"object","required":["action","neris_uid"],"title":"RemovePowergenHazardAction"},"RescuePayload":{"properties":{"presence_known":{"anyOf":[{"$ref":"#/components/schemas/PresenceKnownPayload"},{"type":"null"}],"description":"Details of whether the presence of an occupant in need of rescue was known."},"mayday":{"anyOf":[{"$ref":"#/components/schemas/MaydayPayload"},{"type":"null"}],"description":"Details of a mayday called during the incident."},"ffrescue_or_nonffrescue":{"oneOf":[{"$ref":"#/components/schemas/FfRescuePayload"},{"$ref":"#/components/schemas/NonFfRescuePayload"}],"title":"Ffrescue Or Nonffrescue","description":"Whether the rescue was _performed_ by a firefighter or nonfirefighter.","discriminator":{"propertyName":"type","mapping":{"EVAC_ASSISTED_BY_FIREFIGHTER":"#/components/schemas/FfRescuePayload","NO_RESCUE_NEEDED":"#/components/schemas/NonFfRescuePayload","RESCUED_BY_FF_RIT":"#/components/schemas/FfRescuePayload","RESCUED_BY_FIREFIGHTER":"#/components/schemas/FfRescuePayload","RESCUED_BY_NONFIREFIGHTER":"#/components/schemas/NonFfRescuePayload","SELF_EVACUATION":"#/components/schemas/NonFfRescuePayload"}}}},"additionalProperties":false,"type":"object","required":["ffrescue_or_nonffrescue"],"title":"RescuePayload"},"RescueResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"ffrescue_or_nonffrescue":{"anyOf":[{"$ref":"#/components/schemas/FfRescueResponse"},{"$ref":"#/components/schemas/NonFfRescueResponse"}],"title":"Ffrescue Or Nonffrescue","description":"Whether the rescue was _performed_ by a firefighter or nonfirefighter."},"presence_known":{"anyOf":[{"$ref":"#/components/schemas/PresenceKnownResponse"},{"type":"null"}],"description":"Details of whether the presence of an occupant in need of rescue was known."},"mayday":{"anyOf":[{"$ref":"#/components/schemas/MaydayResponse"},{"type":"null"}],"description":"Details of a mayday called during the incident."}},"type":"object","required":["last_modified","ffrescue_or_nonffrescue"],"title":"RescueResponse"},"Response":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."}},"type":"object","title":"Response"},"RoleActionResourcePayload":{"properties":{"action":{"$ref":"#/components/schemas/TypeActionValue","description":"The type of action allowed on the resource."},"resource":{"$ref":"#/components/schemas/TypeResourceValue","description":"The type of resource the action is allowed on."}},"additionalProperties":false,"type":"object","required":["action","resource"],"title":"RoleActionResourcePayload"},"RoleActionResourceResponse":{"properties":{"action":{"type":"string","title":"Action","description":"The type of action allowed on the resource."},"resource":{"type":"string","title":"Resource","description":"The type of resource the action is allowed on."}},"type":"object","required":["action","resource"],"title":"RoleActionResourceResponse"},"RolePayload":{"properties":{"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"action_resources":{"items":{"$ref":"#/components/schemas/RoleActionResourcePayload"},"type":"array","title":"Action Resources"}},"additionalProperties":false,"type":"object","required":["name"],"title":"RolePayload"},"RoleResponse":{"properties":{"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"nuid":{"type":"string","format":"uuid","title":"Nuid","description":"The unique identifier for the role"},"action_resources":{"items":{"$ref":"#/components/schemas/RoleActionResourceResponse"},"type":"array","title":"Action Resources"}},"type":"object","required":["name","nuid"],"title":"RoleResponse"},"RoleWithEntitySetResponse":{"properties":{"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"nuid":{"type":"string","format":"uuid","title":"Nuid","description":"The unique identifier for the role"},"action_resources":{"items":{"$ref":"#/components/schemas/RoleActionResourceResponse"},"type":"array","title":"Action Resources"},"entity_set":{"$ref":"#/components/schemas/EntitySetResponse","description":"The entity set associated with the role attachment"}},"type":"object","required":["name","nuid","entity_set"],"title":"RoleWithEntitySetResponse"},"SetActionTacticAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/ActionTacticPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetActionTacticAction"},"SetBoolAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"boolean","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetBoolAction"},"SetCookingFireSuppressionAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/CookingFireSuppressionPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetCookingFireSuppressionAction"},"SetCsstHazardAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/CsstHazardPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetCsstHazardAction"},"SetDateTimeAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","format":"date-time","title":"Value","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string. Must be after Jan 1, 2025 EST and prior to the time of the request.","examples":["2025-06-13T21:14:02.162497-07:00","1749874442.162497",1749874442.162497,1749874442]}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetDateTimeAction"},"SetDeterminantCodeAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","maxLength":8,"title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetDeterminantCodeAction"},"SetDictAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"additionalProperties":true,"type":"object","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetDictAction"},"SetDispatchAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/DispatchPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetDispatchAction"},"SetFireAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/FirePayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetFireAction"},"SetFireAlarmAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/FireAlarmPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetFireAlarmAction"},"SetFireSuppressionAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/FireSuppressionPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetFireSuppressionAction"},"SetFloatAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"number","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetFloatAction"},"SetGeoMultipolygonAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/GeoMultipolygon"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetGeoMultipolygonAction"},"SetGeoPointAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/GeoPoint"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetGeoPointAction"},"SetHazsitAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/HazsitPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetHazsitAction"},"SetIncidentBaseAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/IncidentBasePayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetIncidentBaseAction"},"SetIncidentTacticTimestampsAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/IncidentTacticTimestampsPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetIncidentTacticTimestampsAction"},"SetIntAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"integer","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetIntAction"},"SetMedicalOxygenHazardAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/MedicalOxygenHazardPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetMedicalOxygenHazardAction"},"SetNarrativeStrAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","maxLength":100000,"minLength":1,"title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetNarrativeStrAction"},"SetNerisIdDeptStrAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","pattern":"^FD\\d{8}$","title":"Value","examples":["FD24027334","FD24160543"]}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetNerisIdDeptStrAction"},"SetNerisIdUnitStrAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}U[A-Z\\d]{3}$","title":"Value","examples":["FD24027334S001U003","FD24160543S010U001"]}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetNerisIdUnitStrAction"},"SetNotpresentAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","enum":["NOT_PRESENT","NOT_APPLICABLE"],"title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetNotpresentAction"},"SetOperation":{"type":"string","enum":["INTERSECTION","UNION"],"title":"SetOperation"},"SetOtherAlarmAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/OtherAlarmPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetOtherAlarmAction"},"SetPresentAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"PRESENT","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetPresentAction"},"SetRelListTypeActionTacticValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeActionTacticValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeActionTacticValueAction"},"SetRelListTypeAidNonfdValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeAidNonfdValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeAidNonfdValueAction"},"SetRelListTypeAlarmFireValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeAlarmFireValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeAlarmFireValueAction"},"SetRelListTypeAlarmOtherValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeAlarmOtherValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeAlarmOtherValueAction"},"SetRelListTypeAlarmSmokeValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeAlarmSmokeValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeAlarmSmokeValueAction"},"SetRelListTypeCasualtyPpeValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"items":{"$ref":"#/components/schemas/TypeCasualtyPpeValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetRelListTypeCasualtyPpeValueAction"},"SetRelListTypeDisplaceCauseValueRelExposureAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeDisplaceCauseValueRelExposure"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeDisplaceCauseValueRelExposureAction"},"SetRelListTypeDisplaceCauseValueRelIncidentAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeDisplaceCauseValueRelIncident"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeDisplaceCauseValueRelIncidentAction"},"SetRelListTypeEmerghazSuppressionValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeEmerghazSuppressionValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeEmerghazSuppressionValueAction"},"SetRelListTypeFireInvestValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeFireInvestValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeFireInvestValueAction"},"SetRelListTypeLocationModifierValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"enum":[],"title":"TypeLocationModifierValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeLocationModifierValueAction"},"SetRelListTypeRescueActionValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeRescueActionValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeRescueActionValueAction"},"SetRelListTypeRescueImpedimentValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeRescueImpedimentValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeRescueImpedimentValueAction"},"SetRelListTypeSpecialModifierValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeSpecialModifierValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeSpecialModifierValueAction"},"SetRelListTypeSuppressApplianceValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeSuppressApplianceValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeSuppressApplianceValueAction"},"SetRelListTypeSuppressCookingValue":{"properties":{"action":{"type":"string","const":"set","title":"Action","default":"set"},"value":{"items":{"$ref":"#/components/schemas/TypeSuppressCookingValue"},"type":"array","title":"Value"}},"additionalProperties":false,"type":"object","required":["value"],"title":"SetRelListTypeSuppressCookingValue"},"SetSimpleStrAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","maxLength":255,"minLength":1,"title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetSimpleStrAction"},"SetSmokeAlarmAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/SmokeAlarmPayload"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"SetSmokeAlarmAction"},"ShiftPayload":{"properties":{"count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Count"},"duration":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration"},"signup":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Signup"}},"additionalProperties":false,"type":"object","title":"ShiftPayload"},"ShiftResponse":{"properties":{"count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Count"},"duration":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration"},"signup":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Signup"}},"type":"object","title":"ShiftResponse"},"SmokeAlarmAlertedPayload":{"properties":{"type":{"type":"string","const":"OPERATED_ALERTED_OCCUPANT","title":"Type","x-ui-label":"Describe whether the smoke alarm operated as intended during the incident.","x-ui-hint":"Select one."},"occupant_action":{"anyOf":[{"$ref":"#/components/schemas/TypeOccupantResponseValue"},{"type":"null"}],"description":"If the alarm operated, describes the occupant reaction or inaction that resulted from the alarm signal.","x-ui-label":"Describe the occupant reaction or inaction that resulted from the alarm signal.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"SmokeAlarmAlertedPayload"},"SmokeAlarmAlertedResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"},"occupant_action":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Occupant Action","description":"If the alarm operated, describes the occupant reaction or inaction that resulted from the alarm signal."}},"type":"object","required":["last_modified","type"],"title":"SmokeAlarmAlertedResponse"},"SmokeAlarmFailedPayload":{"properties":{"type":{"type":"string","const":"FAILED_TO_OPERATE","title":"Type","x-ui-label":"Describe whether the smoke alarm operated as intended during the incident.","x-ui-hint":"Select one."},"failure_reason":{"anyOf":[{"$ref":"#/components/schemas/TypeAlarmFailureValue"},{"type":"null"}],"description":"Reason for alarm failure.","x-ui-label":"Describe the reason for the failure.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"SmokeAlarmFailedPayload"},"SmokeAlarmFailedResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"},"failure_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Failure Reason","description":"Reason for alarm failure."}},"type":"object","required":["last_modified","type"],"title":"SmokeAlarmFailedResponse"},"SmokeAlarmNotPresentPayload":{"properties":{"type":{"type":"string","enum":["NOT_PRESENT","NOT_APPLICABLE"],"title":"Type","x-ui-label":"Describe whether there was at least one smoke alarm present.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"SmokeAlarmNotPresentPayload"},"SmokeAlarmNotPresentResponse":{"properties":{"type":{"type":"string","title":"Type"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]}},"type":"object","required":["type","last_modified"],"title":"SmokeAlarmNotPresentResponse"},"SmokeAlarmOperationPayload":{"properties":{"alerted_failed_other":{"oneOf":[{"$ref":"#/components/schemas/SmokeAlarmAlertedPayload"},{"$ref":"#/components/schemas/SmokeAlarmFailedPayload"},{"$ref":"#/components/schemas/SmokeAlarmOtherPayload"}],"title":"Alerted Failed Other","discriminator":{"propertyName":"type","mapping":{"FAILED_TO_OPERATE":"#/components/schemas/SmokeAlarmFailedPayload","INSUFFICIENT_SOURCE":"#/components/schemas/SmokeAlarmOtherPayload","NO_OCCUPANT_TO_NOTIFY":"#/components/schemas/SmokeAlarmOtherPayload","OPERATED_ALERTED_OCCUPANT":"#/components/schemas/SmokeAlarmAlertedPayload","OPERATED_FAILED_TO_ALERT_OCCUPANT":"#/components/schemas/SmokeAlarmOtherPayload"}}}},"additionalProperties":false,"type":"object","required":["alerted_failed_other"],"title":"SmokeAlarmOperationPayload"},"SmokeAlarmOperationResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"alerted_failed_other":{"oneOf":[{"$ref":"#/components/schemas/SmokeAlarmAlertedResponse"},{"$ref":"#/components/schemas/SmokeAlarmFailedResponse"},{"$ref":"#/components/schemas/SmokeAlarmOtherResponse"}],"title":"Alerted Failed Other"}},"type":"object","required":["last_modified","alerted_failed_other"],"title":"SmokeAlarmOperationResponse"},"SmokeAlarmOtherPayload":{"properties":{"type":{"type":"string","enum":["OPERATED_FAILED_TO_ALERT_OCCUPANT","NO_OCCUPANT_TO_NOTIFY","INSUFFICIENT_SOURCE"],"title":"Type","x-ui-label":"Describe whether the smoke alarm operated as intended during the incident.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["type"],"title":"SmokeAlarmOtherPayload"},"SmokeAlarmOtherResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type"}},"type":"object","required":["last_modified","type"],"title":"SmokeAlarmOtherResponse"},"SmokeAlarmPayload":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/SmokeAlarmPresentPayload"},{"$ref":"#/components/schemas/SmokeAlarmNotPresentPayload"}],"title":"Presence","discriminator":{"propertyName":"type","mapping":{"NOT_APPLICABLE":"#/components/schemas/SmokeAlarmNotPresentPayload","NOT_PRESENT":"#/components/schemas/SmokeAlarmNotPresentPayload","PRESENT":"#/components/schemas/SmokeAlarmPresentPayload"}}}},"additionalProperties":false,"type":"object","required":["presence"],"title":"SmokeAlarmPayload"},"SmokeAlarmPresentPayload":{"properties":{"type":{"type":"string","const":"PRESENT","title":"Type","x-ui-label":"Describe whether there was at least one smoke alarm present.","x-ui-hint":"Select one."},"working":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Working","x-ui-label":"Describe whether smoke alarm was working or successfully tested.","x-ui-hint":"Select one."},"alarm_types":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeAlarmSmokeValue"},"type":"array"},{"type":"null"}],"title":"Alarm Types","x-ui-label":"Describe the type(s) of smoke alarm(s) present.","x-ui-hint":"Select all that apply."},"operation":{"anyOf":[{"$ref":"#/components/schemas/SmokeAlarmOperationPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["type"],"title":"SmokeAlarmPresentPayload"},"SmokeAlarmPresentResponse":{"properties":{"type":{"type":"string","title":"Type"},"working":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Working"},"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"alarm_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Alarm Types"},"operation":{"anyOf":[{"$ref":"#/components/schemas/SmokeAlarmOperationResponse"},{"type":"null"}]}},"type":"object","required":["type","last_modified"],"title":"SmokeAlarmPresentResponse"},"SmokeAlarmResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"presence":{"oneOf":[{"$ref":"#/components/schemas/SmokeAlarmPresentResponse"},{"$ref":"#/components/schemas/SmokeAlarmNotPresentResponse"}],"title":"Presence"}},"type":"object","required":["last_modified","presence"],"title":"SmokeAlarmResponse"},"SortDirection":{"type":"string","enum":["ASCENDING","DESCENDING"],"title":"SortDirection"},"SpecialModifierResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type","description":"Incident modifier(s) that can help describe the magnitude or class of the incident."}},"type":"object","required":["last_modified","type"],"title":"SpecialModifierResponse"},"StaffingPayload":{"properties":{"active_firefighters_career_ft":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Firefighters Career Ft"},"active_firefighters_career_pt":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Firefighters Career Pt"},"active_firefighters_volunteer":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Firefighters Volunteer"},"active_ems_only_career_ft":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Ems Only Career Ft"},"active_ems_only_career_pt":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Ems Only Career Pt"},"active_ems_only_volunteer":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Ems Only Volunteer"},"active_civilians_career_ft":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Civilians Career Ft"},"active_civilians_career_pt":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Civilians Career Pt"},"active_civilians_volunteer":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Civilians Volunteer"}},"additionalProperties":false,"type":"object","title":"StaffingPayload"},"StaffingResponse":{"properties":{"active_firefighters_career_ft":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Firefighters Career Ft"},"active_firefighters_career_pt":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Firefighters Career Pt"},"active_firefighters_volunteer":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Firefighters Volunteer"},"active_ems_only_career_ft":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Ems Only Career Ft"},"active_ems_only_career_pt":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Ems Only Career Pt"},"active_ems_only_volunteer":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Ems Only Volunteer"},"active_civilians_career_ft":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Civilians Career Ft"},"active_civilians_career_pt":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Civilians Career Pt"},"active_civilians_volunteer":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Active Civilians Volunteer"}},"type":"object","title":"StaffingResponse"},"StatesTerrs":{"type":"string","enum":["AL","AK","AS","AZ","AR","CA","CO","CT","DE","DC","FL","FM","GA","GU","HI","ID","IL","IN","IA","KS","KY","LA","ME","MD","MA","MI","MH","MN","MS","MO","MP","MT","NA","NE","NV","NH","NJ","NM","NY","NC","ND","OH","OK","OR","PA","PR","PW","RI","SC","SD","TN","TX","UM","UT","VT","VA","VI","WA","WV","WI","WY","AA","AE","AP","AB","BC","MB","NB","NL","NS","NT","NU","ON","PE","QC","SK","YT","AGU","BCN","BCS","CAM","CHP","CHH","CMX","COA","COL","DUR","GUA","GRO","HID","JAL","MIC","MOR","MEX","NAY","NLE","OAX","PUE","QUE","ROO","SLP","SIN","SON","TAB","TAM","TLA","VER","YUC","ZAC"],"title":"StatesTerrs"},"StationCreatedModifiedResponse":{"properties":{"neris_id":{"type":"string","title":"Neris Id"},"version":{"type":"integer","title":"Version"},"valid_start":{"type":"string","format":"date-time","title":"Valid Start"},"units":{"items":{"$ref":"#/components/schemas/UnitCreatedModifiedResponse"},"type":"array","title":"Units"}},"type":"object","required":["neris_id","version","valid_start"],"title":"StationCreatedModifiedResponse"},"StationResponse":{"properties":{"address_line_1":{"type":"string","title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"type":"string","title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","title":"Zip Code"},"station_id":{"type":"string","title":"Station Id"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Internal Id"},"staffing":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Staffing"},"neris_id":{"type":"string","title":"Neris Id"},"version":{"type":"integer","title":"Version"},"units":{"items":{"$ref":"#/components/schemas/UnitResponse"},"type":"array","title":"Units"},"location":{"anyOf":[{"type":"string","maxLength":2083,"minLength":1,"format":"uri"},{"type":"null"}],"title":"Location"}},"type":"object","required":["address_line_1","city","state","zip_code","station_id","neris_id","version"],"title":"StationResponse"},"StationSummaryInfoResponse":{"properties":{"neris_id":{"type":"string","title":"Neris Id"},"station_id":{"type":"string","title":"Station Id"},"address_line_1":{"type":"string","title":"Address Line 1"},"city":{"type":"string","title":"City"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"type":"string","title":"Zip Code"},"location":{"anyOf":[{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."},{"type":"null"}],"title":"Location"}},"type":"object","required":["neris_id","station_id","address_line_1","city","state","zip_code"],"title":"StationSummaryInfoResponse"},"StructureAnalysisPayload":{"properties":{"ignition_source":{"type":"boolean","title":"Ignition Source","description":"Whether the structure was the ignition source of the entire fire. Only one structure should be the ignition source.","default":false,"x-ui-label":"Describe whether this structure is the source of the ignition.","x-ui-hint":"In the event of a structure fire with exposure, this would indicate that this is the structure for which the Structure Fire Cause / Origin Module is being completed. In the event of an outdoor fire, you may choose this option OR put a point on the map, but not both."},"accessory_dwelling":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Accessory Dwelling","description":"Whether the structure is an accessory dwelling.","x-ui-label":"Describe whether the structure was an accessory dwelling unit."},"estimated_property_value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Property Value","description":"The estimated property value.","x-ui-label":"Enter the estimated value of the structure."},"estimated_contents_value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Contents Value","description":"The estimated property's contents value.","x-ui-label":"Enter the estimated value of the contents."},"estimated_property_loss_value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Property Loss Value","description":"The estimated loss of value of the property.","x-ui-label":"Enter the estimated value LOSS of the structure."},"estimated_contents_loss_value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Contents Loss Value","description":"The estimated lost value of the property's contents.","x-ui-label":"Enter the estimated value LOSS of the contents."},"non_permitted_modifications":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Non Permitted Modifications","description":"Whether the structure has non-permitted modification(s).","x-ui-label":"Were any non-permitted modifications observed?"},"structure_unstable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Structure Unstable","description":"Whether the structure was unstable before the incident.","x-ui-label":"Was the structure unstable prior to the incident?"},"occupant_count":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Occupant Count","description":"Number of occupants (must be positive).","x-ui-label":"If occupied / in use, enter the number of occupants."},"occupants_displaced":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Occupants Displaced","description":"Whether occupants were displaced.","x-ui-label":"Were the occupants displaced?","x-ui-hint":"Displaced indicates that the structure is no longer inhabitable."},"unit_count":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Unit Count","description":"The number of units in the structure.","x-ui-label":"Enter the number of units in the structure."},"year_built":{"anyOf":[{"type":"integer","exclusiveMaximum":3000.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Year Built","description":"The year the structure was built.","x-ui-label":"Enter the year the structure was built."},"damage_assessment":{"$ref":"#/components/schemas/TypeFireBldgDamageValue","description":"The damage assessment of the structure.","x-ui-label":"What is the damage assessment of the structure?"},"location":{"anyOf":[{"$ref":"#/components/schemas/LocationPayload"},{"type":"null"}]},"location_use":{"anyOf":[{"$ref":"#/components/schemas/LocationUsePayload"},{"type":"null"}]},"inspection":{"anyOf":[{"$ref":"#/components/schemas/InspectionPayload"},{"type":"null"}]},"displacement_causes":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeDisplaceCauseValue"},"type":"array"},{"type":"null"}],"title":"Displacement Causes","description":"The danger that caused people to be displaced.","x-ui-label":"Select the cause of the displacement."},"construction_type":{"anyOf":[{"$ref":"#/components/schemas/TypeConstructionValue"},{"type":"null"}],"description":"The construction type of the structure.","x-ui-label":"Select the construction type."},"roof_material":{"anyOf":[{"$ref":"#/components/schemas/TypeRoofMaterialValue"},{"type":"null"}],"description":"The roof material of the structure.","x-ui-label":"Select the roof material."},"eaves":{"anyOf":[{"$ref":"#/components/schemas/TypeEavesValue"},{"type":"null"}],"description":"The type of eaves on the structure.","x-ui-label":"Select the eaves type, if applicable."},"screen_vent_type":{"anyOf":[{"$ref":"#/components/schemas/TypeVentsValue"},{"type":"null"}],"description":"The type of screen vent on the structure.","x-ui-label":"Select the type(s) of screen(s)/vent(s) observed."},"exterior_finish":{"anyOf":[{"$ref":"#/components/schemas/TypeExteriorFinishValue"},{"type":"null"}],"description":"The exterior finish of the structure.","x-ui-label":"Select the type of exterior finish."},"foundation":{"anyOf":[{"$ref":"#/components/schemas/TypeFoundationValue"},{"type":"null"}],"description":"The type of foundation the structure had.","x-ui-label":"Select the foundation type of the structure."},"window_panes":{"anyOf":[{"$ref":"#/components/schemas/TypeWindowPanesValue"},{"type":"null"}],"description":"The type of window panes the structure had.","x-ui-label":"Select the type(s) of window pane(s)."},"window_damage_assessment":{"anyOf":[{"$ref":"#/components/schemas/TypeFireBldgDamageValue"},{"type":"null"}],"description":"The damage assessment of the window panes.","x-ui-label":"What is the damage assessment of the window(s)?"},"nearest_structure_distance":{"anyOf":[{"$ref":"#/components/schemas/DistancePayload"},{"type":"null"}],"description":"The distance to the next nearest structure."},"exterior_ignition_location":{"anyOf":[{"$ref":"#/components/schemas/TypeDinsOriginLocationValue"},{"type":"null"}],"description":"The first location of the structure that was affected in the incident if it was ignited from the exterior.","x-ui-label":"What was the location where the fire may have spread to the structure?"},"exterior_ignition_causes":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeDinsOriginCauseValue"},"type":"array"},{"type":"null"}],"title":"Exterior Ignition Causes","description":"The causes that made the structure ignite from the exterior.","x-ui-label":"Referencing the above question, how did the ignition source spread fire to the structure?"},"attachments":{"anyOf":[{"items":{"$ref":"#/components/schemas/StructureAttachmentPayload"},"type":"array"},{"type":"null"}],"title":"Attachments","description":"The attachments to the structure."},"point":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}],"x-ui-label":"Place a point on the map to represent the location of the STRUCTURE on the property.","x-ui-hint":"Note: some structures have different addresses on the same property / parcel. If there are any additional structures on the property that do not have their own address, place a pin on the map where each structure is located relative to the main one."}},"additionalProperties":false,"type":"object","required":["damage_assessment"],"title":"StructureAnalysisPayload","description":"\n\n
Validation Rules\n\n- A `structure` must have a `location` or `point`.\n- Occupant fields cannot be set when `in_use` is `false`.\n- `occupants_displaced` cannot be set when `occupant_count` is not set.\n- `displacement_causes` cannot be set if neither `occupants_displaced` nor `occupant_count` is set.\n- `exterior_ignition_location` and `exterior_ignition_causes` cannot be set when `ignition_source` is `true`.\n\n
"},"StructureAnalysisResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"ignition_source":{"type":"boolean","title":"Ignition Source","description":"Whether the structure was the ignition source of the entire fire. Only one structure should be the ignition source.","default":false},"accessory_dwelling":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Accessory Dwelling","description":"Whether the structure is an accessory dwelling."},"estimated_property_value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Property Value","description":"The estimated property value."},"estimated_contents_value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Contents Value","description":"The estimated property's contents value."},"estimated_property_loss_value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Property Loss Value","description":"The estimated loss of value of the property."},"estimated_contents_loss_value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Estimated Contents Loss Value","description":"The estimated lost value of the property's contents."},"non_permitted_modifications":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Non Permitted Modifications","description":"Whether the structure has non-permitted modification(s)."},"structure_unstable":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Structure Unstable","description":"Whether the structure was unstable before the incident."},"occupant_count":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Occupant Count","description":"Number of occupants (must be positive)."},"occupants_displaced":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Occupants Displaced","description":"Whether occupants were displaced."},"unit_count":{"anyOf":[{"type":"integer","minimum":0.0},{"type":"null"}],"title":"Unit Count","description":"The number of units in the structure."},"year_built":{"anyOf":[{"type":"integer","exclusiveMaximum":3000.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Year Built","description":"The year the structure was built."},"damage_assessment":{"type":"string","title":"Damage Assessment","description":"The damage assessment of the structure."},"location":{"anyOf":[{"$ref":"#/components/schemas/LocationResponse"},{"type":"null"}]},"location_use":{"anyOf":[{"$ref":"#/components/schemas/LocationUseResponse"},{"type":"null"}]},"inspection":{"anyOf":[{"$ref":"#/components/schemas/InspectionResponse"},{"type":"null"}]},"displacement_causes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Displacement Causes","description":"The danger that caused people to be displaced."},"construction_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Construction Type","description":"The construction type of the structure."},"roof_material":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Roof Material","description":"The roof material of the structure."},"eaves":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Eaves","description":"The type of eaves on the structure."},"screen_vent_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Vent Type","description":"The type of screen vent on the structure."},"exterior_finish":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Exterior Finish","description":"The exterior finish of the structure."},"foundation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Foundation","description":"The type of foundation the structure had."},"window_panes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Window Panes","description":"The type of window panes the structure had."},"nearest_structure_distance":{"anyOf":[{"$ref":"#/components/schemas/DistanceResponse"},{"type":"null"}],"description":"The distance to the next nearest structure."},"exterior_ignition_location":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Exterior Ignition Location","description":"The first location of the structure that was affected in the incident if it was ignited from the exterior."},"exterior_ignition_causes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Exterior Ignition Causes","description":"The causes that made the structure ignite from the exterior."},"attachments":{"anyOf":[{"items":{"$ref":"#/components/schemas/StructureAttachmentResponse"},"type":"array"},{"type":"null"}],"title":"Attachments","description":"The attachments to the structure."},"point":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Point"}},"type":"object","required":["last_modified","damage_assessment"],"title":"StructureAnalysisResponse"},"StructureAttachmentPayload":{"properties":{"type":{"$ref":"#/components/schemas/TypeAttachedValue","description":"The type of the addition to the structure.","x-ui-label":"What type of addition was attached?"},"grade":{"anyOf":[{"$ref":"#/components/schemas/TypeDeckPorchGradeValue"},{"type":"null"}],"description":"The grade of the addition to the structure.","x-ui-label":"What was the grade of the addition?"},"impacted":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Impacted","description":"Whether the addition to the structure was impacted by the fire.","x-ui-label":"Was the addition impacted by the fire?"},"materials":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeAttachedMaterialValue"},"type":"array"},{"type":"null"}],"title":"Materials","description":"The materials the addition was made of.","x-ui-label":"What material(s) was the addition made of?"}},"additionalProperties":false,"type":"object","required":["type"],"title":"StructureAttachmentPayload"},"StructureAttachmentResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"type":{"type":"string","title":"Type","description":"The type of the addition to the structure."},"grade":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Grade","description":"The grade of the addition to the structure."},"impacted":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Impacted","description":"Whether the addition to the structure was impacted by the fire."},"materials":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Materials","description":"The materials the addition was made of."}},"type":"object","required":["last_modified","type"],"title":"StructureAttachmentResponse"},"StructureFireLocationDetailPayload":{"properties":{"type":{"type":"string","const":"STRUCTURE","title":"Type","default":"STRUCTURE"},"progression_evident":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Progression Evident","description":"Whether the fire extended beyond the conditions found upon arrival.","x-ui-label":"Did the fire extend beyond the conditions found upon arrival?"},"floor_of_origin":{"type":"integer","title":"Floor Of Origin","description":"Story above or below ground of fire origin. Negative integer is below ground, positive is above ground. 1 is ground floor.","examples":[1,2,3],"x-ui-label":"The presumed story above or below ground of fire origin.","x-ui-hint":"Negative values are below ground, positive values are above ground. 1 is ground floor. If unknown, enter zero."},"arrival_condition":{"$ref":"#/components/schemas/TypeFireConditionArrivalValue","description":"Fire conditions upon arriving on the scene of the incident.","x-ui-label":"The fire conditions upon arriving on the scene of the incident","x-ui-hint":"Select one."},"damage_type":{"$ref":"#/components/schemas/TypeFireBldgDamageValue","description":"Rating of damage to the fire building of origin as selected from a list of values.","x-ui-label":"Rating of damage to the fire building of origin.","x-ui-hint":"Select one."},"room_of_origin_type":{"$ref":"#/components/schemas/TypeRoomValue","description":"Type of room of origin of the fire.","x-ui-label":"The presumed room of origin of the fire.","x-ui-hint":"Select one."},"cause":{"$ref":"#/components/schemas/TypeFireCauseInValue","description":"General categorization of the cause (or likely cause) of the structure fire.","x-ui-label":"General categorization of the cause (or likely cause) of the structure fire.","x-ui-hint":"Select one."}},"additionalProperties":false,"type":"object","required":["floor_of_origin","arrival_condition","damage_type","room_of_origin_type","cause"],"title":"StructureFireLocationDetailPayload"},"StructureFireLocationDetailResponse":{"properties":{"type":{"type":"string","const":"STRUCTURE","title":"Type","default":"STRUCTURE"},"progression_evident":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Progression Evident","description":"Whether the fire extended beyond the conditions found upon arrival."},"floor_of_origin":{"type":"integer","title":"Floor Of Origin","description":"Story above or below ground of fire origin. Negative integer is below ground, positive is above ground. 1 is ground floor.","examples":[1,2,3]},"arrival_condition":{"type":"string","title":"Arrival Condition","description":"Fire conditions upon arriving on the scene of the incident."},"damage_type":{"type":"string","title":"Damage Type","description":"Rating of damage to the fire building of origin as selected from a list of values."},"room_of_origin_type":{"type":"string","title":"Room Of Origin Type","description":"Type of room of origin of the fire."},"cause":{"type":"string","title":"Cause","description":"General categorization of the cause (or likely cause) of the structure fire."}},"type":"object","required":["floor_of_origin","arrival_condition","damage_type","room_of_origin_type","cause"],"title":"StructureFireLocationDetailResponse"},"StructureFireOriginPayload":{"properties":{"level_of_origin":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Level Of Origin","description":"Level of the fire origin (positive for above ground, negative for below ground).","x-ui-label":"The presumed story above or below ground of fire origin.","x-ui-hint":"Negative values are below ground, positive values are above ground. 1 is ground floor. If unknown, enter zero."},"minor_involved":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Minor Involved","description":"Whether the fire's ignition had a minor involved.","x-ui-label":"Was there a minor involved in the ignition of the fire?"},"general_cause":{"anyOf":[{"$ref":"#/components/schemas/TypeGeneralFireCauseValue"},{"type":"null"}],"description":"The general cause of the indoor fire.","x-ui-label":"Select the general cause of the indoor fire."},"heat_sources":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeFireCauseInValue"},"type":"array"},{"type":"null"}],"title":"Heat Sources","description":"The heat sources involved in the indoor fire.","x-ui-label":"Select the heat source of the fire."},"cause":{"$ref":"#/components/schemas/TypeIndoorCauseValue","description":"The primary cause of the indoor fire.","x-ui-label":"Select the specific cause of the fire."},"item_first_ignited":{"anyOf":[{"$ref":"#/components/schemas/TypeItemFirstIgnitedValue"},{"type":"null"}],"x-ui-label":"Select the item first ignited, if applicable."},"room_of_origin":{"anyOf":[{"$ref":"#/components/schemas/TypeRoomValue"},{"type":"null"}],"description":"The room the fire originated in.","x-ui-label":"Select the room of origin of the fire.","x-ui-hint":"If fire originated in an interstitial space, select \"Other\" and continue to next question."},"interstitial_space":{"anyOf":[{"$ref":"#/components/schemas/TypeInterstitialSpaceValue"},{"type":"null"}],"description":"The interstitial space the fire originated in.\n\n - `intersitial_space` has been deprecated. Use `interstitial_space` instead.","x-ui-label":"If the fire began in an interstitial space, please select the appropriate location."},"fire_spread":{"anyOf":[{"$ref":"#/components/schemas/TypeFireSpreadValue"},{"type":"null"}],"description":"The spread of the fire.","x-ui-label":"Describe the fire spread.","x-ui-hint":"i.e., to which of the following was the fire contained?"},"contributing_hazards":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeContributingHazardsValue"},"type":"array"},{"type":"null"}],"title":"Contributing Hazards","description":"The contributing hazards to the fire.","x-ui-label":"Select any hazards that may have impacted conditions."},"initial_detection":{"anyOf":[{"$ref":"#/components/schemas/TypeInitialDetectionValue"},{"type":"null"}],"description":"The initial detection of the fire.","x-ui-label":"How was the fire initially detected?"},"smoke_alarm":{"anyOf":[{"$ref":"#/components/schemas/SmokeAlarmPayload"},{"type":"null"}]},"fire_alarm":{"anyOf":[{"$ref":"#/components/schemas/FireAlarmPayload"},{"type":"null"}]},"other_alarm":{"anyOf":[{"$ref":"#/components/schemas/OtherAlarmPayload"},{"type":"null"}]},"fire_suppression":{"anyOf":[{"$ref":"#/components/schemas/FireSuppressionPayload"},{"type":"null"}]},"cooking_fire_suppression":{"anyOf":[{"$ref":"#/components/schemas/CookingFireSuppressionPayload"},{"type":"null"}]}},"additionalProperties":false,"type":"object","required":["cause"],"title":"StructureFireOriginPayload"},"StructureFireOriginResponse":{"additionalProperties":true,"type":"object"},"TimeZones":{"type":"string","enum":["Africa/Abidjan","Africa/Accra","Africa/Algiers","Africa/Bissau","Africa/Cairo","Africa/Casablanca","Africa/Ceuta","Africa/El_Aaiun","Africa/Johannesburg","Africa/Juba","Africa/Khartoum","Africa/Lagos","Africa/Maputo","Africa/Monrovia","Africa/Nairobi","Africa/Ndjamena","Africa/Sao_Tome","Africa/Tripoli","Africa/Tunis","Africa/Windhoek","America/Adak","America/Anchorage","America/Araguaina","America/Argentina/Buenos_Aires","America/Argentina/Catamarca","America/Argentina/Cordoba","America/Argentina/Jujuy","America/Argentina/La_Rioja","America/Argentina/Mendoza","America/Argentina/Rio_Gallegos","America/Argentina/Salta","America/Argentina/San_Juan","America/Argentina/San_Luis","America/Argentina/Tucuman","America/Argentina/Ushuaia","America/Asuncion","America/Atikokan","America/Atka","America/Bahia","America/Bahia_Banderas","America/Barbados","America/Belem","America/Belize","America/Blanc-Sablon","America/Boa_Vista","America/Bogota","America/Boise","America/Cambridge_Bay","America/Campo_Grande","America/Cancun","America/Caracas","America/Cayenne","America/Chicago","America/Chihuahua","America/Costa_Rica","America/Creston","America/Cuiaba","America/Curacao","America/Danmarkshavn","America/Dawson","America/Dawson_Creek","America/Denver","America/Detroit","America/Edmonton","America/Eirunepe","America/El_Salvador","America/Fort_Nelson","America/Fort_Wayne","America/Fortaleza","America/Glace_Bay","America/Goose_Bay","America/Grand_Turk","America/Guatemala","America/Guayaquil","America/Guyana","America/Halifax","America/Havana","America/Hermosillo","America/Indiana/Indianapolis","America/Indiana/Knox","America/Indiana/Marengo","America/Indiana/Petersburg","America/Indiana/Tell_City","America/Indiana/Vevay","America/Indiana/Vincennes","America/Indiana/Winamac","America/Indianapolis","America/Inuvik","America/Iqaluit","America/Jamaica","America/Juneau","America/Kentucky/Louisville","America/Kentucky/Monticello","America/La_Paz","America/Lima","America/Los_Angeles","America/Maceio","America/Managua","America/Manaus","America/Martinique","America/Matamoros","America/Mazatlan","America/Menominee","America/Merida","America/Metlakatla","America/Mexico_City","America/Miquelon","America/Moncton","America/Monterrey","America/Montevideo","America/Nassau","America/New_York","America/Nipigon","America/Nome","America/Noronha","America/North_Dakota/Beulah","America/North_Dakota/Center","America/North_Dakota/New_Salem","America/Nuuk","America/Ojinaga","America/Panama","America/Pangnirtung","America/Paramaribo","America/Phoenix","America/Port_of_Spain","America/Port-au-Prince","America/Porto_Velho","America/Puerto_Rico","America/Punta_Arenas","America/Rainy_River","America/Rankin_Inlet","America/Recife","America/Regina","America/Resolute","America/Rio_Branco","America/Santarem","America/Santiago","America/Santo_Domingo","America/Sao_Paulo","America/Scoresbysund","America/Sitka","America/St_Johns","America/Swift_Current","America/Tegucigalpa","America/Thule","America/Thunder_Bay","America/Tijuana","America/Toronto","America/Vancouver","America/Virgin","America/Whitehorse","America/Winnipeg","America/Yakutat","America/Yellowknife","Antarctica/Casey","Antarctica/Davis","Antarctica/DumontDUrville","Antarctica/Macquarie","Antarctica/Mawson","Antarctica/Palmer","Antarctica/Rothera","Antarctica/Syowa","Antarctica/Troll","Antarctica/Vostok","Asia/Almaty","Asia/Amman","Asia/Anadyr","Asia/Aqtau","Asia/Aqtobe","Asia/Ashgabat","Asia/Atyrau","Asia/Baghdad","Asia/Baku","Asia/Bangkok","Asia/Barnaul","Asia/Beirut","Asia/Bishkek","Asia/Brunei","Asia/Chita","Asia/Choibalsan","Asia/Colombo","Asia/Damascus","Asia/Dhaka","Asia/Dili","Asia/Dubai","Asia/Dushanbe","Asia/Famagusta","Asia/Gaza","Asia/Hebron","Asia/Ho_Chi_Minh","Asia/Hong_Kong","Asia/Hovd","Asia/Irkutsk","Asia/Jakarta","Asia/Jayapura","Asia/Jerusalem","Asia/Kabul","Asia/Kamchatka","Asia/Karachi","Asia/Kathmandu","Asia/Khandyga","Asia/Kolkata","Asia/Krasnoyarsk","Asia/Kuala_Lumpur","Asia/Kuching","Asia/Macau","Asia/Magadan","Asia/Makassar","Asia/Manila","Asia/Nicosia","Asia/Novokuznetsk","Asia/Novosibirsk","Asia/Omsk","Asia/Oral","Asia/Pontianak","Asia/Pyongyang","Asia/Qatar","Asia/Qostanay","Asia/Qyzylorda","Asia/Riyadh","Asia/Sakhalin","Asia/Samarkand","Asia/Seoul","Asia/Shanghai","Asia/Singapore","Asia/Srednekolymsk","Asia/Taipei","Asia/Tashkent","Asia/Tbilisi","Asia/Tehran","Asia/Thimphu","Asia/Tokyo","Asia/Tomsk","Asia/Ulaanbaatar","Asia/Urumqi","Asia/Ust-Nera","Asia/Vladivostok","Asia/Yakutsk","Asia/Yangon","Asia/Yekaterinburg","Asia/Yerevan","Atlantic/Azores","Atlantic/Bermuda","Atlantic/Canary","Atlantic/Cape_Verde","Atlantic/Faroe","Atlantic/Madeira","Atlantic/Reykjavik","Atlantic/South_Georgia","Atlantic/Stanley","Australia/Adelaide","Australia/Brisbane","Australia/Broken_Hill","Australia/Darwin","Australia/Eucla","Australia/Hobart","Australia/Lindeman","Australia/Lord_Howe","Australia/Melbourne","Australia/Perth","Australia/Sydney","Europe/Amsterdam","Europe/Andorra","Europe/Astrakhan","Europe/Athens","Europe/Belgrade","Europe/Berlin","Europe/Brussels","Europe/Bucharest","Europe/Budapest","Europe/Chisinau","Europe/Copenhagen","Europe/Dublin","Europe/Gibraltar","Europe/Helsinki","Europe/Istanbul","Europe/Kaliningrad","Europe/Kiev","Europe/Kirov","Europe/Lisbon","Europe/London","Europe/Luxembourg","Europe/Madrid","Europe/Malta","Europe/Minsk","Europe/Monaco","Europe/Moscow","Europe/Oslo","Europe/Paris","Europe/Prague","Europe/Riga","Europe/Rome","Europe/Samara","Europe/Saratov","Europe/Simferopol","Europe/Sofia","Europe/Stockholm","Europe/Tallinn","Europe/Tirane","Europe/Ulyanovsk","Europe/Uzhgorod","Europe/Vienna","Europe/Vilnius","Europe/Volgograd","Europe/Warsaw","Europe/Zaporozhye","Europe/Zurich","Indian/Chagos","Indian/Christmas","Indian/Cocos","Indian/Kerguelen","Indian/Mahe","Indian/Maldives","Indian/Mauritius","Indian/Reunion","Navajo","Pacific/Apia","Pacific/Auckland","Pacific/Bougainville","Pacific/Chatham","Pacific/Chuuk","Pacific/Easter","Pacific/Efate","Pacific/Enderbury","Pacific/Fakaofo","Pacific/Fiji","Pacific/Funafuti","Pacific/Galapagos","Pacific/Gambier","Pacific/Guadalcanal","Pacific/Guam","Pacific/Honolulu","Pacific/Kiritimati","Pacific/Kosrae","Pacific/Kwajalein","Pacific/Majuro","Pacific/Marquesas","Pacific/Nauru","Pacific/Niue","Pacific/Norfolk","Pacific/Noumea","Pacific/Pago_Pago","Pacific/Palau","Pacific/Pitcairn","Pacific/Pohnpei","Pacific/Port_Moresby","Pacific/Rarotonga","Pacific/Saipan","Pacific/Samoa","Pacific/Tahiti","Pacific/Tarawa","Pacific/Tongatapu","Pacific/Wake","Pacific/Wallis","RU","US/Alaska","US/Aleutian","US/Arizona","US/Central","US/East-Indiana","US/Eastern","US/Hawaii","US/Indiana-Starke","US/Michigan","US/Mountain","US/Pacific","US/Samoa","UTC"],"title":"TimeZones"},"TokenBody":{"properties":{"username":{"type":"string","title":"Username","description":"The username if grant_type is \"password\", \"email_otp\", \"software_token_mfa\", or \"new_password_required\""},"password":{"type":"string","title":"Password","description":"The password if grant_type is \"password\""},"refresh_token":{"type":"string","title":"Refresh Token","description":"The refresh token if the grant_type is \"refresh_token\""},"email_otp":{"type":"string","title":"Email Otp","description":"The OTP if the grant_type is \"email_otp\""},"software_token_mfa":{"type":"string","title":"Software Token Mfa","description":"The OTP if the grant_type is \"software_token_mfa\""},"new_password":{"type":"string","title":"New Password","description":"The new password if grant_type is \"new_password_required\""},"session":{"type":"string","title":"Session","description":"The session if the grant type is \"email_otp\", \"software_token_mfa\" or \"new_password_required\""},"generate_refresh_token":{"type":"boolean","title":"Generate Refresh Token","description":"Whether to return a refresh token in the response","default":false},"grant_type":{"$ref":"#/components/schemas/GrantType","description":"Requested grant type"}},"type":"object","required":["grant_type"],"title":"TokenBody"},"TypeAccountValue":{"type":"string","enum":["CAD","RMS","USER"],"title":"TypeAccountValue"},"TypeActionTacticValue":{"type":"string","enum":["COMMAND_AND_CONTROL||ACCOUNTABILITY_OFFICER_ASSIGNED","COMMAND_AND_CONTROL||ESTABLISH_INCIDENT_COMMAND","COMMAND_AND_CONTROL||INCIDENT_ASSESSMENT_COMPLETED","COMMAND_AND_CONTROL||NOTIFY_OTHER_AGENCIES","COMMAND_AND_CONTROL||PIO_ASSIGNED","COMMAND_AND_CONTROL||SAFETY_OFFICER_ASSIGNED","CONTAINMENT||OUTSIDE_FIRE_SUPPRESSION||DOZER_FUEL_BREAK","CONTAINMENT||OUTSIDE_FIRE_SUPPRESSION||HAND_CREW_FUEL_BREAK","EMERGENCY_MEDICAL_CARE||PATIENT_ASSESSMENT","EMERGENCY_MEDICAL_CARE||PATIENT_REFERRAL","EMERGENCY_MEDICAL_CARE||PROVIDE_ADVANCED_LIFE_SUPPORT","EMERGENCY_MEDICAL_CARE||PROVIDE_BASIC_LIFE_SUPPORT","EMERGENCY_MEDICAL_CARE||PROVIDE_TRANSPORT","FORCIBLE_ENTRY","HAZARDOUS_SITUATION_MITIGATION||ATMOSPHERIC_MONITORING_EXTERIOR_FENCELINE","HAZARDOUS_SITUATION_MITIGATION||ATMOSPHERIC_MONITORING_INTERIOR","HAZARDOUS_SITUATION_MITIGATION||DECONTAMINATION","HAZARDOUS_SITUATION_MITIGATION||LEAK_STOP","HAZARDOUS_SITUATION_MITIGATION||REMOVE_HAZARD","HAZARDOUS_SITUATION_MITIGATION||SPILL_CONTROL","HAZARDOUS_SITUATION_MITIGATION||TAKE_SAMPLES","INFORMATION_ENFORCEMENT||ENFORCE_CODE_OR_LAW","INFORMATION_ENFORCEMENT||PROVIDE_PUBLIC_INFORMATION","INFORMATION_ENFORCEMENT||REFER_TO_PROPER_AHJ","INVESTIGATION","NON_STRUCTURE_SEARCH||BODY_RECOVERY","NON_STRUCTURE_SEARCH||SEARCH_AREA_OF_COLLAPSE","NON_STRUCTURE_SEARCH||SEARCH_UNDERGROUND_INFRASTRUCTURE","NON_STRUCTURE_SEARCH||SEARCH_WATERWAY","NON_STRUCTURE_SEARCH||USAR_K9_SEARCH","NON_STRUCTURE_SEARCH||WIDE_AREA_OUTDOOR_SEARCH","PERSONNEL_CONTAMINATION_REDUCTION||CLEAN_CAB_TRANSPORT","PERSONNEL_CONTAMINATION_REDUCTION||ON_SCENE_CONTAMINATION_REDUCTION","PERSONNEL_CONTAMINATION_REDUCTION||PPE_WASHED_POST_INCIDENT","PROVIDE_EQUIPMENT||PROVIDE_DRONE_VIDEO_EQUIPMENT","PROVIDE_EQUIPMENT||PROVIDE_ELECTRICAL_POWER","PROVIDE_EQUIPMENT||PROVIDE_LIGHT","PROVIDE_EQUIPMENT||PROVIDE_SPECIAL_EQUIPMENT","PROVIDE_EVACUATION_SUPPORT||CONNECTED_INTERIOR_SPACES","PROVIDE_EVACUATION_SUPPORT||LARGE_AREA","PROVIDE_EVACUATION_SUPPORT||NEARBY_BUILDINGS","PROVIDE_EVACUATION_SUPPORT||REMOTE_INTERIOR_SPACES","PROVIDE_SERVICES||ASSIST_ANIMAL","PROVIDE_SERVICES||ASSIST_UNINJURED_PERSON","PROVIDE_SERVICES||CONTROL_CROWD","PROVIDE_SERVICES||CONTROL_TRAFFIC","PROVIDE_SERVICES||DAMAGE_ASSESSMENT","PROVIDE_SERVICES||PROVIDE_APPARATUS_WATER","PROVIDE_SERVICES||REMOVE_WATER","PROVIDE_SERVICES||RESTORE_RESET_ALARM_SYSTEM","PROVIDE_SERVICES||RESTORE_SPRINKLER_SYSTEM","PROVIDE_SERVICES||SECURE_PROPERTY","PROVIDE_SERVICES||SHUT_DOWN_ALARM","PROVIDE_SERVICES||SHUT_DOWN_SPRINKLER_SYSTEM","SALVAGE_AND_OVERHAUL","SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH","SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH||DURING_SUPPRESSION","SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH||POST_SUPPRESSION","SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH||PRIOR_TO_SUPPRESSION","SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH","SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH||DURING_SUPPRESSION","SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH||POST_SUPPRESSION","SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH||PRIOR_TO_SUPPRESSION","SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||BACKBURN","SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||CONFINEMENT","SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||ESTABLISH_FIRE_LINES","SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||FIRE_CONTROL_EXTINGUISHMENT","SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||FIRE_RETARDANT_DROP","SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||STRUCTURE_PROTECTION","SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||WATER_DROP","SUPPRESSION||STRUCTURAL_FIRE_SUPPRESSION||EXTERIOR","SUPPRESSION||STRUCTURAL_FIRE_SUPPRESSION||EXTERIOR_AND_INTERIOR","SUPPRESSION||STRUCTURAL_FIRE_SUPPRESSION||INTERIOR","VENTILATION||HORIZONTAL","VENTILATION||HORIZONTAL||DURING_SUPPRESSION","VENTILATION||HORIZONTAL||POST_SUPPRESSION","VENTILATION||HORIZONTAL||PRIOR_TO_SUPPRESSION","VENTILATION||HYDRAULIC","VENTILATION||HYDRAULIC||DURING_SUPPRESSION","VENTILATION||HYDRAULIC||POST_SUPPRESSION","VENTILATION||HYDRAULIC||PRIOR_TO_SUPPRESSION","VENTILATION||POSITIVE_PRESSURE","VENTILATION||POSITIVE_PRESSURE||DURING_SUPPRESSION","VENTILATION||POSITIVE_PRESSURE||POST_SUPPRESSION","VENTILATION||POSITIVE_PRESSURE||PRIOR_TO_SUPPRESSION","VENTILATION||VERTICAL","VENTILATION||VERTICAL||DURING_SUPPRESSION","VENTILATION||VERTICAL||POST_SUPPRESSION","VENTILATION||VERTICAL||PRIOR_TO_SUPPRESSION"],"title":"TypeActionTacticValue"},"TypeActionValue":{"type":"string","enum":["CREATE","DELETE","READ","UPDATE"],"title":"TypeActionValue"},"TypeAidAgreementValue":{"type":"string","enum":["AUTOMATIC","CONTRACTUAL","MUTUAL"],"title":"TypeAidAgreementValue"},"TypeAidDirectionValue":{"type":"string","enum":["GIVEN","RECEIVED"],"title":"TypeAidDirectionValue"},"TypeAidNonfdValue":{"type":"string","enum":["ANIMAL_SERVICES","EMS","HOUSING_SERVICES","LAW_ENFORCEMENT","REMEDIATION_SERVICES","SOCIAL_SERVICES","UTILITIES_PUBLIC_WORKS"],"title":"TypeAidNonfdValue"},"TypeAidValue":{"type":"string","enum":["ACTING_AS_AID","IN_LIEU_AID","SUPPORT_AID"],"title":"TypeAidValue"},"TypeAlarmFailureValue":{"type":"string","enum":["DEVICE_MALFUNCTION","EXPIRED","IMPROPER_INSTALLATION","NO_BATTERY","OTHER_NON_FUNCTIONAL_CAUSE","TAMPER","UNABLE_TO_DETERMINE"],"title":"TypeAlarmFailureValue"},"TypeAlarmFireValue":{"type":"string","enum":["AUTOMATIC","MANUAL","MANUAL_AND_AUTOMATIC"],"title":"TypeAlarmFireValue"},"TypeAlarmOperationValue":{"type":"string","enum":["FAILED_TO_OPERATE","INSUFFICIENT_SOURCE","NO_OCCUPANT_TO_NOTIFY","OPERATED_ALERTED_OCCUPANT","OPERATED_FAILED_TO_ALERT_OCCUPANT"],"title":"TypeAlarmOperationValue"},"TypeAlarmOtherValue":{"type":"string","enum":["CARBON_MONOXIDE","HEAT_DETECTOR","NATURAL_GAS","OTHER_CHEMICAL_DETECTOR"],"title":"TypeAlarmOtherValue"},"TypeAlarmSmokeValue":{"type":"string","enum":["BED_SHAKER","COMBINATION","HARDWIRED","HARD_OF_HEARING_WITH_STROBE","INTERCONNECTED","LONG_LIFE_BATTERY_POWERED","REPLACEABLE_BATTERY_POWERED","UNKNOWN"],"title":"TypeAlarmSmokeValue"},"TypeAspectValue":{"type":"string","enum":["EAST","NORTH","NORTH_EAST","NORTH_WEST","SOUTH","SOUTH_EAST","SOUTH_WEST","WEST"],"title":"TypeAspectValue"},"TypeAttachedMaterialValue":{"type":"string","enum":["COMBUSTIBLE","NON_COMBUSTIBLE","UNKNOWN"],"title":"TypeAttachedMaterialValue"},"TypeAttachedValue":{"type":"string","enum":["CARPORT_LEANTO","FENCE","GARAGE","OTHER","PATIO_COVER_AWNING","PORCH_DECK"],"title":"TypeAttachedValue"},"TypeAutoBodyStyleValue":{"type":"string","enum":["AMBULANCE","BUS","CONVERTIBLE","COUPE","FIRE_TRUCK","HARDTOP","HATCHBACK","HEARSE","LIMOUSINE","MINIVAN","MOTORIZED_HOME","OTHER","PICKUP","ROADSTER","SEDAN","STATION_WAGON","SUV","VAN"],"title":"TypeAutoBodyStyleValue"},"TypeAutoMakeValue":{"type":"string","enum":["ACURA","AGCO","ALFA_ROMEO","ALPINA","ALPINE","AMC","ARIEL","ASTON_MARTIN","AUDI","BENTLEY_MOTORS","BMW","BOLLINGER_MOTORS","BUGATTI","BUICK","BYD","CADILLAC","CANOO","CASE_IH","CATERHAM","CHEVROLET","CHRYSLER","CITROEN","CLAAS","CUPRA","CZINGER","DACIA","DAEWOO","DAF","DAIHATSU","DATSUN","DELOREAN_MOTOR_COMPANY","DEUTZ_FAHR","DODGE","DONKERVOORT","EAGLE","FARADAY_FUTURE","FENDT","FERRARI","FIAT","FISKER","FORD","FREIGHTLINER","GENESIS","GEO","GMC","HARLEY_DAVIDSON","HENNESSEY","HONDA","HUMMER","HYUNDAI","INEOS","INFINITI","INTERNATIONAL","ISUZU","IVECO","JAGUAR","JEEP","JOHN_DEERE","KARMA","KIA","KOENIGSEGG","KUBOTA","LADA","LAMBORGHINI","LANCIA","LAND_ROVER","LEXUS","LINCOLN","LORDSTOWN_MOTORS","LOTUS","LUCID_MOTORS","MAHINDRA_AND_MAHINDRA","MAN","MASERATI","MASSEY_FERGUSON","MAYBACH","MAZDA","MCLAREN","MERCEDES_AMG","MERCEDES_BENZ","MERCURY","MG","MINI","MITSUBISHI","MORGAN","MULLEN","NEW_HOLLAND","NISSAN","OLDSMOBILE","OPEL","OTHER","PAGANI","PEUGEOT","PLYMOUTH","POLARIS","POLESTAR","PONTIAC","PORSCHE","PROTON","RAM","RENAULT","RIMAC","RIVIAN","ROLLS_ROYCE","SAAB","SALEEN","SATURN","SCANIA","SCION","SEAT","SKODA","SMART","SPYKER","SRT","SSC_NORTH_AMERICA","SUBARU","SUZUKI","TESLA","TOYOTA","TVR","VAUXHALL","VINFAST","VOLKSWAGEN","VOLVO","YAMAHA","YUGO"],"title":"TypeAutoMakeValue"},"TypeBatteryCellValue":{"type":"string","enum":["BUTTON_COIN","CYLINDRICAL","OTHER","POUCH_POLYMER","PRISMATIC","UNKNOWN"],"title":"TypeBatteryCellValue"},"TypeBatteryChemistryValue":{"type":"string","enum":["ALKALINE","LEAD_ACID","LITHIUM_ION","LITHIUM_IRON_PHOSPHATE","LITHIUM_METAL","LITHIUM_SULPHUR","NICKEL_METAL_HYDRIDE","OTHER","SODIUM_ION","UNKNOWN"],"title":"TypeBatteryChemistryValue"},"TypeCasualtyActionValue":{"type":"string","enum":["ADVANCING_OPERATING_HOSELINE","CARRYING_SETTINGUP_EQUIPMENT","DURING_INCIDENT_RESPONSE","EMS_PATIENT_CARE","FORCIBLE_ENTRY","INCIDENT_COMMAND","OTHER","PUMP_OPERATIONS","SCENE_SAFETY_DIRECTING_TRAFFIC","SEARCH_RESCUE","STANDBY","VEHICLE_EXTRICATION","VENTILATION"],"title":"TypeCasualtyActionValue"},"TypeCasualtyCauseValue":{"type":"string","enum":["CAUGHT_TRAPPED_BY_FIRE_EXPLOSION","CAUGHT_TRAPPED_BY_OBJECT","COLLAPSE","EXPOSURE","FALL_JUMP","OTHER","STRESS_OVEREXERTION","STRUCK_CONTACT_WITH_OBJECT","VEHICLE_COLLISION"],"title":"TypeCasualtyCauseValue"},"TypeCasualtyPpeValue":{"type":"string","enum":["3_4_BOOTS","BRUSH_GEAR","BUNKER_PANTS","FACE_SHIELD_GOGGLES","GLOVES","HELMET","NONE","OTHER_SPECIAL_EQUIPMENT","PASS_DEVICE","PROTECTIVE_HOOD","REFLECTIVE_VEST","RUBBER_KNEE_BOOTS","SCBA","TURNOUT_COAT"],"title":"TypeCasualtyPpeValue"},"TypeCasualtyTimelineValue":{"type":"string","enum":["AFTER_CONCLUSION_OF_INCIDENT","CONTINUING_OPERATIONS","EXTENDED_OPERATIONS","INITIAL_RESPONSE","RESPONDING","UNKNOWN"],"title":"TypeCasualtyTimelineValue"},"TypeConstructionValue":{"type":"string","enum":["TYPE_IA","TYPE_IB","TYPE_IIA","TYPE_IIB","TYPE_IIIA","TYPE_IIIB","TYPE_IV","TYPE_VA","TYPE_VB","UNKNOWN"],"title":"TypeConstructionValue"},"TypeConsumerProductValue":{"type":"string","enum":["ALARM_ESCAPE_PROTECTION_DEVICES","CHILD_NURSERY_EQUIPMENT","FARM_SUPPLIES_EQUIPMENT","FURNISHINGS_FIXTURES","GENERAL_HOUSEHOLD_APPLIANCE","HEATING_COOLING_VENT_APPLIANCE","HOME_COMM_ENTERTAINMENT_HOBBY_EQUIPMENT","HOME_FAMILY_MAINTENANCE_PRODUCT","HOUSEHOLD_PACKAGING_CONTAINER","HOUSEWARE_NON_POWERED","KITCHEN_APPLIANCE","MISC_PRODUCT","PERSONAL_USE_ITEM","REGULATED_PRODUCTS","SPORTS_RECREATION","STRUCTURES_CONSTRUCTION","TOYS","TOYS_MOBILITY_OTHER","WORKSHOP_TOOL_APPARATUS","YARD_GARDEN_EQUIPMENT"],"title":"TypeConsumerProductValue"},"TypeContributingHazardsValue":{"type":"string","enum":["CIRCUITS_TRIPPED","CO_DETECTED","GAS_FUEL_ON","OTHER"],"title":"TypeContributingHazardsValue"},"TypeDeckPorchGradeValue":{"type":"string","enum":["BELOW_GRADE_SUNKEN","ELEVATED","ON_GRADE"],"title":"TypeDeckPorchGradeValue"},"TypeDeptValue":{"type":"string","enum":["CAREER","COMBINATION","VOLUNTEER"],"title":"TypeDeptValue"},"TypeDinsOriginCauseValue":{"type":"string","enum":["DIRECT_FLAME_IMPINGEMENT","EMBERS","OTHER","RADIANT_HEAT","UNKNOWN"],"title":"TypeDinsOriginCauseValue"},"TypeDinsOriginLocationValue":{"type":"string","enum":["ATTACHED_FENCE","ATTACHED_PATIO_COVER_CARPORT","DECK_ELEVATED","DECK_ON_GRADE","EAVES","OTHER","ROOF","SIDING","UNKNOWN","VENT","WINDOW"],"title":"TypeDinsOriginLocationValue"},"TypeDispProtoFireValue":{"type":"string","enum":["APCO","IAED","OTHER","PROQA"],"title":"TypeDispProtoFireValue"},"TypeDispProtoMedValue":{"type":"string","enum":["APCO","IAED","OTHER","PROQA"],"title":"TypeDispProtoMedValue"},"TypeDisplaceCauseValue":{"type":"string","enum":["COLLAPSE","FIRE","HAZARDOUS_SITUATION","OTHER","SMOKE","UTILITIES","WATER"],"title":"TypeDisplaceCauseValue"},"TypeDisplaceCauseValueRelExposure":{"type":"string","enum":["COLLAPSE","FIRE","HAZARDOUS_SITUATION","OTHER","SMOKE","UTILITIES","WATER"],"title":"TypeDisplaceCauseValueRelExposure"},"TypeDisplaceCauseValueRelIncident":{"type":"string","enum":["COLLAPSE","FIRE","HAZARDOUS_SITUATION","OTHER","SMOKE","UTILITIES","WATER"],"title":"TypeDisplaceCauseValueRelIncident"},"TypeDistanceUnitValue":{"type":"string","enum":["FEET","MILES"],"title":"TypeDistanceUnitValue"},"TypeDutyValue":{"type":"string","enum":["AFTER_INCIDENT","OTHER_ON_DUTY_INCIDENT","RESPONDING_TO_EMERGENCY_INCIDENT","RETURNING_FROM_EMERGENCY_INCIDENT","TRAINING","WORKING_AT_SCENE_OF_FIRE_INCIDENT","WORKING_AT_SCENE_OF_NONFIRE_INCIDENT"],"title":"TypeDutyValue"},"TypeEavesValue":{"type":"string","enum":["ENCLOSED","NO_EAVES","UNENCLOSED","UNKNOWN"],"title":"TypeEavesValue"},"TypeEmerghazElecValue":{"type":"string","enum":["CONSUMER_PRODUCTS||APPLIANCE_TOOL","CONSUMER_PRODUCTS||CELL_PHONE","CONSUMER_PRODUCTS||COMPUTER_TABLET","CONSUMER_PRODUCTS||ELECTRONIC_CIGARETTE","CONSUMER_PRODUCTS||OTHER","CONSUMER_PRODUCTS||POWER_BANK","CONSUMER_PRODUCTS||TOY","ELECTRIC_VEHICLE||BOAT_COMMERCIAL||FUEL_CELL","ELECTRIC_VEHICLE||BOAT_COMMERCIAL||FULL_ELECTRIC","ELECTRIC_VEHICLE||BOAT_COMMERCIAL||HYBRID","ELECTRIC_VEHICLE||BOAT_COMMERCIAL||PLUG_IN_HYBRID","ELECTRIC_VEHICLE||BOAT_RECREATIONAL||FUEL_CELL","ELECTRIC_VEHICLE||BOAT_RECREATIONAL||FULL_ELECTRIC","ELECTRIC_VEHICLE||BOAT_RECREATIONAL||HYBRID","ELECTRIC_VEHICLE||BOAT_RECREATIONAL||PLUG_IN_HYBRID","ELECTRIC_VEHICLE||BUS_RR||FUEL_CELL","ELECTRIC_VEHICLE||BUS_RR||FULL_ELECTRIC","ELECTRIC_VEHICLE||BUS_RR||HYBRID","ELECTRIC_VEHICLE||BUS_RR||PLUG_IN_HYBRID","ELECTRIC_VEHICLE||CAR_RR||FUEL_CELL","ELECTRIC_VEHICLE||CAR_RR||FULL_ELECTRIC","ELECTRIC_VEHICLE||CAR_RR||HYBRID","ELECTRIC_VEHICLE||CAR_RR||PLUG_IN_HYBRID","ELECTRIC_VEHICLE||LIGHT_EV_NRR||FUEL_CELL","ELECTRIC_VEHICLE||LIGHT_EV_NRR||FULL_ELECTRIC","ELECTRIC_VEHICLE||LIGHT_EV_NRR||HYBRID","ELECTRIC_VEHICLE||LIGHT_EV_NRR||PLUG_IN_HYBRID","ELECTRIC_VEHICLE||OTHER||FUEL_CELL","ELECTRIC_VEHICLE||OTHER||FULL_ELECTRIC","ELECTRIC_VEHICLE||OTHER||HYBRID","ELECTRIC_VEHICLE||OTHER||PLUG_IN_HYBRID","ELECTRIC_VEHICLE||TRUCK_COMMERCIAL_RR||FUEL_CELL","ELECTRIC_VEHICLE||TRUCK_COMMERCIAL_RR||FULL_ELECTRIC","ELECTRIC_VEHICLE||TRUCK_COMMERCIAL_RR||HYBRID","ELECTRIC_VEHICLE||TRUCK_COMMERCIAL_RR||PLUG_IN_HYBRID","ELECTRIC_VEHICLE||TRUCK_PASSENGER_RR||FUEL_CELL","ELECTRIC_VEHICLE||TRUCK_PASSENGER_RR||FULL_ELECTRIC","ELECTRIC_VEHICLE||TRUCK_PASSENGER_RR||HYBRID","ELECTRIC_VEHICLE||TRUCK_PASSENGER_RR||PLUG_IN_HYBRID","ENERGY_STORAGE_SYSTEM||BATTERY","ENERGY_STORAGE_SYSTEM||COMPRESSED_AIR","ENERGY_STORAGE_SYSTEM||FLYWHEEL","ENERGY_STORAGE_SYSTEM||HYDROELECTRIC","ENERGY_STORAGE_SYSTEM||OTHER","E_MOBILITY||ELECTRIC_SCOOTER_MOPED","E_MOBILITY||OTHER","E_MOBILITY||PERSONAL_MOBILITY_ASSIST","E_MOBILITY||POWER_ASSISTED_BICYCLE"],"title":"TypeEmerghazElecValue"},"TypeEmerghazPvIgnValue":{"type":"string","enum":["SOURCE","TARGET"],"title":"TypeEmerghazPvIgnValue"},"TypeEmerghazPvValue":{"type":"string","enum":["OTHER","PANEL_POWER_GENERATION","PANEL_WATER_HEATING","THIN_FILM_POWER_GENERATION","TILE_POWER_GENERATION"],"title":"TypeEmerghazPvValue"},"TypeEmerghazSuppressionValue":{"type":"string","enum":["BATTERY_PENETRATION","FIRE_BLANKET","RUN_COURSE","SUBMERGE_BURY","SUPPRESSION_WATER_ADDITIVE","SUPPRESSION_WATER_ONLY"],"title":"TypeEmerghazSuppressionValue"},"TypeEntitySetValue":{"type":"string","enum":["ALL","COUNTY","FEDERAL","INDIVIDUAL","NGO","NONE","STATE","SYSTEM"],"title":"TypeEntitySetValue"},"TypeEntityValue":{"type":"string","enum":["CONTRACT","FEDERAL","LOCAL","OTHER","PRIVATE","STATE","TRANSPORTATION","TRIBAL"],"title":"TypeEntityValue"},"TypeExposureDamageValue":{"type":"string","enum":["MAJOR_DAMAGE","MINOR_DAMAGE","MODERATE_DAMAGE","NO_DAMAGE"],"title":"TypeExposureDamageValue"},"TypeExposureItemValue":{"type":"string","enum":["OBJECT_OTHER","OUTDOOR_ENVIRONMENT","STRUCTURE","VEHICLE"],"title":"TypeExposureItemValue"},"TypeExteriorFinishValue":{"type":"string","enum":["ASHPHALT","BRICK_STONE","CONCRETE","EIFS","FIBER_CEMENT","METAL","STUCCO","VINYL","WOOD"],"title":"TypeExteriorFinishValue"},"TypeFaJurisdictionValue":{"type":"string","enum":["FEDERAL"],"title":"TypeFaJurisdictionValue"},"TypeFireBldgDamageValue":{"type":"string","enum":["MAJOR_DAMAGE","MINOR_DAMAGE","MODERATE_DAMAGE","NO_DAMAGE"],"title":"TypeFireBldgDamageValue"},"TypeFireCauseInValue":{"type":"string","enum":["ACT_OF_NATURE","BATTERY_POWER_STORAGE","CHEMICAL","COOKING","ELECTRICAL","EXPLOSIVES_FIREWORKS","HEAT_FROM_ANOTHER_OBJECT","INCENDIARY","OPEN_FLAME","OPERATING_EQUIPMENT","OTHER_HEAT_SOURCE","SMOKING_MATERIALS_ILLICIT_DRUGS","UNABLE_TO_BE_DETERMINED"],"title":"TypeFireCauseInValue"},"TypeFireCauseOutValue":{"type":"string","enum":["BATTERY_POWER_STORAGE","DEBRIS_OPEN_BURNING","EQUIPMENT_VEHICLE_USE","FIREARMS_EXPLOSIVES","FIREWORKS","INCENDIARY","NATURAL","POWER_GEN_TRANS_DIST","RAILROAD_OPS_MAINTENANCE","RECREATION_CEREMONY","SMOKING_MATERIALS_ILLICIT_DRUGS","SPREAD_FROM_CONTROLLED_BURN","STRUCTURE","UNABLE_TO_BE_DETERMINED"],"title":"TypeFireCauseOutValue"},"TypeFireConditionArrivalValue":{"type":"string","enum":["FIRE_OUT_UPON_ARRIVAL","FIRE_SPREAD_BEYOND_STRUCTURE","NO_SMOKE_FIRE_SHOWING","SMOKE_FIRE_SHOWING","SMOKE_SHOWING","STRUCTURE_INVOLVED"],"title":"TypeFireConditionArrivalValue"},"TypeFireInvestNeedValue":{"type":"string","enum":["NO","NOT_APPLICABLE","NOT_EVALUATED","NO_CAUSE_OBVIOUS","OTHER","YES"],"title":"TypeFireInvestNeedValue"},"TypeFireInvestValue":{"type":"string","enum":["INVESTIGATED_BY_ARSON_FIRE_INVESTIGATOR","INVESTIGATED_BY_INSURANCE","INVESTIGATED_BY_NONFIRE_LAW_ENFORCEMENT","INVESTIGATED_BY_OTHER","INVESTIGATED_BY_OUTSIDE_AGENCY","INVESTIGATED_BY_STATE_FIRE_MARSHAL","INVESTIGATED_ON_SCENE_RESOURCE","NONE"],"title":"TypeFireInvestValue"},"TypeFireSpreadValue":{"type":"string","enum":["BEYOND_BUILDING","BUILDING","FLOOR","OBJECT","ROOM"],"title":"TypeFireSpreadValue"},"TypeFmJurisdictionValue":{"type":"string","enum":["COUNTY","LOCAL","STATE"],"title":"TypeFmJurisdictionValue"},"TypeFoundationValue":{"type":"string","enum":["CONCRETE_PANELS","CRAWL_SPACE","FULL_BASEMENT","INSULATED_CONCRETE","PIER_AND_BEAM_PILE","POURED_CONCRETE_SLAB","SLAB_ON_GRADE","STONE","WOOD"],"title":"TypeFoundationValue"},"TypeFuelArrangementValue":{"type":"string","enum":["CROWN_FUELS","GROUND_FUELS","SURFACE_FUELS"],"title":"TypeFuelArrangementValue"},"TypeFuelDistributionValue":{"type":"string","enum":["CONTINUOUS_HORIZONTAL","LADDER_VERTICAL"],"title":"TypeFuelDistributionValue"},"TypeFuelSizeValue":{"type":"string","enum":["025_TO_1","1_TO_3","3_TO_8","GREATER_THAN_8","LESS_THAN_025"],"title":"TypeFuelSizeValue"},"TypeFullPartialValue":{"type":"string","enum":["EXTENT_UNKNOWN","FULL","PARTIAL"],"title":"TypeFullPartialValue"},"TypeGenderValue":{"type":"string","enum":["FEMALE","MALE","OTHER_GENDER_IDENTITY","TRANSGENDER_FEMALE_MALE_TO_FEMALE","TRANSGENDER_MALE_FEMALE_TO_MALE","UNKNOWN"],"title":"TypeGenderValue"},"TypeGeneralFireCauseValue":{"type":"string","enum":["ACCIDENTAL","INCENDIARY","NATURAL","UNDETERMINED"],"title":"TypeGeneralFireCauseValue"},"TypeHazardCauseValue":{"type":"string","enum":["ACT_OF_NATURE","CAUSE_UNDER_INVESTIGATION","CONTAINER_CONTAINMENT_FAILURE","INTENTIONAL","UNINTENTIONAL"],"title":"TypeHazardCauseValue"},"TypeHazardDispositionValue":{"type":"string","enum":["COMPLETED_FIRE_SERVICE_ONLY","COMPLETED_WITH_FIRE_SERVICE_PRESENT","RELEASED_TO_COUNTY_AGENCY","RELEASED_TO_FEDERAL_AGENCY","RELEASED_TO_LOCAL_AGENCY","RELEASED_TO_PRIVATE_AGENCY","RELEASED_TO_PROPERTY_OWNER","RELEASED_TO_STATE_AGENCY"],"title":"TypeHazardDispositionValue"},"TypeHazardDotValue":{"type":"string","enum":["CORROSIVES","EXPLOSIVES","FLAMMABLE_LIQUIDS","FLAMMABLE_SOLIDS","GASES","MISCELLANEOUS_DANGEROUS_SUBSTANCES","OXIDIZERS","POISONS_AND_ETIOLOGIC_MATERIALS","RADIOACTIVE_MATERIALS"],"title":"TypeHazardDotValue"},"TypeHazardPhysicalStateValue":{"type":"string","enum":["GAS","LIQUID","RADIOACTIVE","SOLID","UNKNOWN"],"title":"TypeHazardPhysicalStateValue"},"TypeHazardReleasedIntoValue":{"type":"string","enum":["AIR","GROUND","WATER"],"title":"TypeHazardReleasedIntoValue"},"TypeHazardUnitValue":{"type":"string","enum":["GAS_CUBIC_CENTIMETER","GAS_CUBIC_FOOT","GAS_CUBIC_INCH","GAS_CUBIC_METER","GAS_POUND","LIQUID_CUBIC_CENTIMETER","LIQUID_CUP","LIQUID_GALLON","LIQUID_GRAM","LIQUID_KILOGRAM","LIQUID_LITER","LIQUID_MILLIGRAM","LIQUID_MILLILITER","LIQUID_OUNCE","LIQUID_PINT","LIQUID_POUND","LIQUID_QUART","LIQUID_TABLESPOON","LIQUID_TEASPOON","RADIOACTIVE_CURIE","RADIOACTIVE_MEGABECQUEREL","RADIOACTIVE_MICROCURIE","RADIOACTIVE_MILLICURIE","RADIOACTIVE_TERABECQUEREL","SOLID_GRAM","SOLID_KILOGRAM","SOLID_LITER","SOLID_MILLIGRAM","SOLID_OUNCE","SOLID_POUND","SOLID_TABLESPOON","SOLID_TEASPOON","SOLID_TON"],"title":"TypeHazardUnitValue"},"TypeHazardValue":{"type":"string","enum":["ENVIRONMENTAL_DAMAGE","EXPLOSION","FIRE","LEAK","MATERIAL_ENTERED_WATERWAY","SOLID_DISPERSION","SPILL","VAPOR_GAS_DISPERSION"],"title":"TypeHazardValue"},"TypeHazsitReleaseFactorsValue":{"type":"string","enum":["DESIGN_CONSTRUCTION_INSTALLATION_DEFICIENCY||CONSTRUCTION_DEFICIENCY","DESIGN_CONSTRUCTION_INSTALLATION_DEFICIENCY||DESIGN_DEFICIENCY","DESIGN_CONSTRUCTION_INSTALLATION_DEFICIENCY||INSTALLATION_DEFICIENCY","FAILURE_TO_CONTROL_HAZARDOUS_MATERIAL||ABANDONED_OR_DISCARDED","FAILURE_TO_CONTROL_HAZARDOUS_MATERIAL||IMPROPER_ENVIRONMENT_STOAGE","FAILURE_TO_CONTROL_HAZARDOUS_MATERIAL||IMPROPER_TRANSFER","MECHANICAL_FAILURE_MALFUNCTION||AUTOMATIC_CONTROL_FAILURE","MECHANICAL_FAILURE_MALFUNCTION||ELECTRICAL_FAILURE","MECHANICAL_FAILURE_MALFUNCTION||LACK_OF_MAINTENANCE","MECHANICAL_FAILURE_MALFUNCTION||MANUAL_CONTROL_FAILURE","MISUSE_OF_HAZARDOUS_MATERIALS||IMPROPER_CONTAINER","MISUSE_OF_HAZARDOUS_MATERIALS||IMPROPER_MIXING","MISUSE_OF_HAZARDOUS_MATERIALS||IMPROPER_USE","NATURAL_CONDITION||EARTHQUAKE","NATURAL_CONDITION||FLOOD","NATURAL_CONDITION||HIGH_TEMPERATURE","NATURAL_CONDITION||HIGH_WIND","NATURAL_CONDITION||LIGHTNING","NATURAL_CONDITION||LOWER_TEMPERATURE","OPERATIONAL_DEFICIENCY||ACCIDENTAL_IMPROPER_POWER_ON","OPERATIONAL_DEFICIENCY||ACCIDENTAL_IMPROPER_SHUTDOWN","OPERATIONAL_DEFICIENCY||COLLISION_OVERTURN_KNOCKDOWN","OPERATIONAL_DEFICIENCY||EQUIPMENT_UNATTENDED"],"title":"TypeHazsitReleaseFactorsValue"},"TypeHumanFactorsValue":{"type":"string","enum":["ALCOHOL_USE","ASLEEP","CULTURAL_RELIGIOUS_BEHAVIOR","ELDERLY_AGING","HOARDING_DISORDER","HOMELESSNESS","ILLICIT_DRUG_USE","INTELLECTUAL_DISABILITY","JUVENILE_BEHAVIOR","MEDICAL_CONDITION","MEDICAL_OXYGEN","MENTAL_HEALTH","NONE","OTHER","PHYSICAL_DISABILITY"],"title":"TypeHumanFactorsValue"},"TypeIncidentAnalysisStatusPayloadValue":{"type":"string","enum":["APPROVED","REJECTED"],"title":"TypeIncidentAnalysisStatusPayloadValue"},"TypeIncidentComplexityValue":{"type":"string","enum":["ICS_TYPE_1","ICS_TYPE_2","ICS_TYPE_3","ICS_TYPE_4","ICS_TYPE_5"],"title":"TypeIncidentComplexityValue"},"TypeIncidentStatusPayloadValue":{"type":"string","enum":["APPROVED","REJECTED"],"title":"TypeIncidentStatusPayloadValue"},"TypeIncidentStatusValue":{"type":"string","enum":["APPROVED","DELETED","FAILED","PENDING_APPROVAL","PENDING_INCIDENT_DATA","REJECTED","SUBMITTED"],"title":"TypeIncidentStatusValue"},"TypeIncidentValue":{"type":"string","enum":["FIRE||OUTSIDE_FIRE||CONSTRUCTION_WASTE","FIRE||OUTSIDE_FIRE||DUMPSTER_OUTDOOR_CONTAINER_FIRE","FIRE||OUTSIDE_FIRE||OTHER_OUTSIDE_FIRE","FIRE||OUTSIDE_FIRE||OUTSIDE_TANK_FIRE","FIRE||OUTSIDE_FIRE||TRASH_RUBBISH_FIRE","FIRE||OUTSIDE_FIRE||UTILITY_INFRASTRUCTURE_FIRE","FIRE||OUTSIDE_FIRE||VEGETATION_GRASS_FIRE","FIRE||OUTSIDE_FIRE||WILDFIRE_URBAN_INTERFACE","FIRE||OUTSIDE_FIRE||WILDFIRE_WILDLAND","FIRE||SPECIAL_FIRE||ESS_FIRE","FIRE||SPECIAL_FIRE||EXPLOSION","FIRE||SPECIAL_FIRE||INFRASTRUCTURE_FIRE","FIRE||STRUCTURE_FIRE||CHIMNEY_FIRE","FIRE||STRUCTURE_FIRE||CONFINED_COOKING_APPLIANCE_FIRE","FIRE||STRUCTURE_FIRE||ROOM_AND_CONTENTS_FIRE","FIRE||STRUCTURE_FIRE||STRUCTURAL_INVOLVEMENT_FIRE","FIRE||TRANSPORTATION_FIRE||AIRCRAFT_FIRE","FIRE||TRANSPORTATION_FIRE||BOAT_PERSONAL_WATERCRAFT_BARGE_FIRE","FIRE||TRANSPORTATION_FIRE||POWERED_MOBILITY_DEVICE_FIRE","FIRE||TRANSPORTATION_FIRE||TRAIN_RAIL_FIRE","FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_COMMERCIAL","FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_FOOD_TRUCK","FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_PASSENGER","FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_RV","HAZSIT||HAZARDOUS_MATERIALS||BIOLOGICAL_RELEASE_INCIDENT","HAZSIT||HAZARDOUS_MATERIALS||CARBON_MONOXIDE_RELEASE","HAZSIT||HAZARDOUS_MATERIALS||FUEL_SPILL_ODOR","HAZSIT||HAZARDOUS_MATERIALS||GAS_LEAK_ODOR","HAZSIT||HAZARDOUS_MATERIALS||HAZMAT_RELEASE_FACILITY","HAZSIT||HAZARDOUS_MATERIALS||HAZMAT_RELEASE_TRANSPORT","HAZSIT||HAZARDOUS_MATERIALS||RADIOACTIVE_RELEASE_INCIDENT","HAZSIT||HAZARD_NONCHEM||BOMB_THREAT_RESPONSE_SUSPICIOUS_PACKAGE","HAZSIT||HAZARD_NONCHEM||ELEC_HAZARD_SHORT_CIRCUIT","HAZSIT||HAZARD_NONCHEM||ELEC_POWER_LINE_DOWN_ARCHING_MALFUNC","HAZSIT||HAZARD_NONCHEM||MOTOR_VEHICLE_COLLISION","HAZSIT||INVESTIGATION||ODOR","HAZSIT||INVESTIGATION||SMOKE_INVESTIGATION","HAZSIT||OVERPRESSURE||NO_RUPTURE","HAZSIT||OVERPRESSURE||RUPTURE_WITHOUT_FIRE","LAWENFORCE","MEDICAL||ILLNESS","MEDICAL||ILLNESS||ABDOMINAL_PAIN","MEDICAL||ILLNESS||ALLERGIC_REACTION_STINGS","MEDICAL||ILLNESS||ALTERED_MENTAL_STATUS","MEDICAL||ILLNESS||BACK_PAIN_NON_TRAUMA","MEDICAL||ILLNESS||BREATHING_PROBLEMS","MEDICAL||ILLNESS||CARDIAC_ARREST","MEDICAL||ILLNESS||CHEST_PAIN_NON_TRAUMA","MEDICAL||ILLNESS||CONVULSIONS_SEIZURES","MEDICAL||ILLNESS||DIABETIC_PROBLEMS","MEDICAL||ILLNESS||HEADACHE","MEDICAL||ILLNESS||HEART_PROBLEMS","MEDICAL||ILLNESS||NAUSEA_VOMITING","MEDICAL||ILLNESS||NO_APPROPRIATE_CHOICE","MEDICAL||ILLNESS||OVERDOSE","MEDICAL||ILLNESS||PANDEMIC_EPIDEMIC_OUTBREAK","MEDICAL||ILLNESS||PREGNANCY_CHILDBIRTH","MEDICAL||ILLNESS||PSYCHOLOGICAL_BEHAVIOR_ISSUES","MEDICAL||ILLNESS||SICK_CASE","MEDICAL||ILLNESS||STROKE_CVA","MEDICAL||ILLNESS||UNCONSCIOUS_VICTIM","MEDICAL||ILLNESS||UNKNOWN_PROBLEM","MEDICAL||ILLNESS||WELL_PERSON_CHECK","MEDICAL||INJURY","MEDICAL||INJURY||ANIMAL_BITES","MEDICAL||INJURY||ASSAULT","MEDICAL||INJURY||BURNS_EXPLOSION","MEDICAL||INJURY||CARBON_MONOXIDE_OTHER_INHALATION_INJURY","MEDICAL||INJURY||CHOKING","MEDICAL||INJURY||DROWNING_DIVING_SCUBA_ACCIDENT","MEDICAL||INJURY||ELECTROCUTION","MEDICAL||INJURY||EYE_TRAUMA","MEDICAL||INJURY||FALL","MEDICAL||INJURY||GUNSHOT_WOUND","MEDICAL||INJURY||HEAT_COLD_EXPOSURE","MEDICAL||INJURY||HEMORRHAGE_LACERATION","MEDICAL||INJURY||INDUSTRIAL_INACCESSIBLE_ENTRAPMENT","MEDICAL||INJURY||MOTOR_VEHICLE_COLLISION","MEDICAL||INJURY||OTHER_TRAUMATIC_INJURY","MEDICAL||INJURY||POISONING","MEDICAL||INJURY||STAB_PENETRATING_TRAUMA","MEDICAL||OTHER||AIRMEDICAL_TRANSPORT","MEDICAL||OTHER||COMMUNITY_PUBLIC_HEALTH","MEDICAL||OTHER||HEALTHCARE_PROFESSIONAL_ADMISSION","MEDICAL||OTHER||INTERCEPT_OTHER_UNIT","MEDICAL||OTHER||MEDICAL_ALARM","MEDICAL||OTHER||STANDBY_REQUEST","MEDICAL||OTHER||TRANSFER_INTERFACILITY","NOEMERG||CANCELLED","NOEMERG||FALSE_ALARM||ACCIDENTAL_ALARM","NOEMERG||FALSE_ALARM||BOMB_SCARE","NOEMERG||FALSE_ALARM||INTENTIONAL_FALSE_ALARM","NOEMERG||FALSE_ALARM||MALFUNCTIONING_ALARM","NOEMERG||FALSE_ALARM||OTHER_FALSE_CALL","NOEMERG||GOOD_INTENT||CONTROLLED_BURNING_AUTHORIZED","NOEMERG||GOOD_INTENT||INVESTIGATE_HAZARDOUS_RELEASE","NOEMERG||GOOD_INTENT||NO_INCIDENT_FOUND_LOCATION_ERROR","NOEMERG||GOOD_INTENT||SMOKE_FROM_NONHOSTILE_SOURCE","PUBSERV||ALARMS_NONMED||CO_ALARM","PUBSERV||ALARMS_NONMED||FIRE_ALARM","PUBSERV||ALARMS_NONMED||GAS_ALARM","PUBSERV||ALARMS_NONMED||OTHER_ALARM","PUBSERV||CITIZEN_ASSIST||CITIZEN_ASSIST_SERVICE_CALL","PUBSERV||CITIZEN_ASSIST||LIFT_ASSIST","PUBSERV||CITIZEN_ASSIST||LOST_PERSON","PUBSERV||CITIZEN_ASSIST||PERSON_IN_DISTRESS","PUBSERV||DISASTER_WEATHER||DAMAGE_ASSESSMENT","PUBSERV||DISASTER_WEATHER||WEATHER_RESPONSE","PUBSERV||OTHER||DAMAGED_HYDRANT","PUBSERV||OTHER||MOVE_UP","PUBSERV||OTHER||STANDBY","RESCUE||OUTSIDE||BACKCOUNTRY_RESCUE","RESCUE||OUTSIDE||CONFINED_SPACE_RESCUE","RESCUE||OUTSIDE||EXTRICATION_ENTRAPPED","RESCUE||OUTSIDE||HIGH_ANGLE_RESCUE","RESCUE||OUTSIDE||LIMITED_NO_ACCESS","RESCUE||OUTSIDE||LOW_ANGLE_RESCUE","RESCUE||OUTSIDE||STEEP_ANGLE_RESCUE","RESCUE||OUTSIDE||TRENCH","RESCUE||STRUCTURE||BUILDING_STRUCTURE_COLLAPSE","RESCUE||STRUCTURE||CONFINED_SPACE_RESCUE","RESCUE||STRUCTURE||ELEVATOR_ESCALATOR_RESCUE","RESCUE||STRUCTURE||EXTRICATION_ENTRAPPED","RESCUE||TRANSPORTATION||AVIATION_COLLISION_CRASH","RESCUE||TRANSPORTATION||AVIATION_STANDBY","RESCUE||TRANSPORTATION||MOTOR_VEHICLE_EXTRICATION_ENTRAPPED","RESCUE||TRANSPORTATION||TRAIN_RAIL_COLLISION_DERAILMENT","RESCUE||WATER||PERSON_IN_WATER_STANDING","RESCUE||WATER||PERSON_IN_WATER_SWIFTWATER","RESCUE||WATER||WATERCRAFT_IN_DISTRESS"],"title":"TypeIncidentValue"},"TypeIndoorCauseValue":{"type":"string","enum":["ACT_OF_NATURE||GLASS_REFRACTION_MAGNIFYING_GLASS","ACT_OF_NATURE||LIGHTNING","ACT_OF_NATURE||OTHER","ACT_OF_NATURE||SPONTANEOUS_COMBUSTION","ACT_OF_NATURE||UNKNOWN","ACT_OF_NATURE||VOLCANO","BATTERY_POWER_STORAGE||BATTERY_CONSUMER_PRODUCT","BATTERY_POWER_STORAGE||ENERGY_STORAGE_SYSTEM","CHEMICAL||CHEMICAL_LABORATORY","COOKING||DRY_COOKING","COOKING||KITCHEN_APPLIANCE_OVEN","COOKING||OIL_GREASE","ELECTRICAL||ELECTRICAL_EQUIPMENT","ELECTRICAL||ELECTRICAL_TRANSMISSION_DISTRIBUTION_SYSTEMS","ELECTRICAL||ELECTRIC_FENCE","ELECTRICAL||OTHER","ELECTRICAL||SOLAR_UTILITY_SYSTEM","ELECTRICAL||UNKNOWN","EXPLOSIVES_FIREWORKS||AMMONIUM_NITRATE_FUEL_OIL","EXPLOSIVES_FIREWORKS||BLACK_POWDER_MUZZLE_LOADING","EXPLOSIVES_FIREWORKS||BLASTING","EXPLOSIVES_FIREWORKS||CONSUMER_AERIAL||COMETS_MINES_SHELLS","EXPLOSIVES_FIREWORKS||CONSUMER_AERIAL||HELICOPTERS","EXPLOSIVES_FIREWORKS||CONSUMER_AERIAL||MISSILES","EXPLOSIVES_FIREWORKS||CONSUMER_AERIAL||OTHER_SPECIALTY_ITEMS","EXPLOSIVES_FIREWORKS||CONSUMER_AERIAL||RELOADABLE_TUBE_AERIAL","EXPLOSIVES_FIREWORKS||CONSUMER_AERIAL||ROCKETS","EXPLOSIVES_FIREWORKS||CONSUMER_AERIAL||ROMAN_CANDLES","EXPLOSIVES_FIREWORKS||CONSUMER_NON_AERIAL||CHASERS","EXPLOSIVES_FIREWORKS||CONSUMER_NON_AERIAL||COMBINATION_ITEMS","EXPLOSIVES_FIREWORKS||CONSUMER_NON_AERIAL||FIRECRACKERS","EXPLOSIVES_FIREWORKS||CONSUMER_NON_AERIAL||FOUNTAINS","EXPLOSIVES_FIREWORKS||CONSUMER_NON_AERIAL||GROUND_SPINNERS","EXPLOSIVES_FIREWORKS||CONSUMER_NON_AERIAL||HANDHELD_SPARKLERS","EXPLOSIVES_FIREWORKS||CONSUMER_NON_AERIAL||OTHER_SPECIALTY_ITEMS","EXPLOSIVES_FIREWORKS||CONSUMER_NON_AERIAL||WHEELS","EXPLOSIVES_FIREWORKS||DEREGULATED_NOVELTIES||PARTY_POPPERS","EXPLOSIVES_FIREWORKS||DEREGULATED_NOVELTIES||SMOKE_BALLS","EXPLOSIVES_FIREWORKS||DEREGULATED_NOVELTIES||SNAKES","EXPLOSIVES_FIREWORKS||DEREGULATED_NOVELTIES||SNAP_CAPS","EXPLOSIVES_FIREWORKS||DEREGULATED_NOVELTIES||WIRE_CORE_SPARKLERS","EXPLOSIVES_FIREWORKS||DETONATING_CORD","EXPLOSIVES_FIREWORKS||EXPLODING_TARGET_SHOOTING","EXPLOSIVES_FIREWORKS||EXPLOSIVES||1_4_STICK","EXPLOSIVES_FIREWORKS||EXPLOSIVES||ALTERED_CONSUMER_FIREWORKS","EXPLOSIVES_FIREWORKS||EXPLOSIVES||CHERRY_BOMB","EXPLOSIVES_FIREWORKS||EXPLOSIVES||HOMEMADE_EXPLOSIVE_DEVICE","EXPLOSIVES_FIREWORKS||EXPLOSIVES||M_100","EXPLOSIVES_FIREWORKS||EXPLOSIVES||M_1000","EXPLOSIVES_FIREWORKS||EXPLOSIVES||M_80","EXPLOSIVES_FIREWORKS||EXPLOSIVES||SILVER_SALUTE","EXPLOSIVES_FIREWORKS||EXPLOSIVES||SPARKLER_BOMB","EXPLOSIVES_FIREWORKS||FLARES_FUSES","EXPLOSIVES_FIREWORKS||IMPROVISED_EXPLOSIVE_DEVICE","EXPLOSIVES_FIREWORKS||INERT_TARGET_SHOOTING","EXPLOSIVES_FIREWORKS||MILITARY_ORDINANCE","EXPLOSIVES_FIREWORKS||MODEL_AMATEUR_ROCKETS","EXPLOSIVES_FIREWORKS||NON_MILITARY_TRACER","EXPLOSIVES_FIREWORKS||OTHER","EXPLOSIVES_FIREWORKS||OTHER_EXPLOSIVES","EXPLOSIVES_FIREWORKS||PIPE_BOMB","EXPLOSIVES_FIREWORKS||PROFESSIONAL_DISPLAY||INDOOR_PROXIMATE","EXPLOSIVES_FIREWORKS||PROFESSIONAL_DISPLAY||OUTDOOR_AERIAL_SHELL","EXPLOSIVES_FIREWORKS||PROFESSIONAL_DISPLAY||OUTDOOR_GROUND_LEVEL","EXPLOSIVES_FIREWORKS||PROFESSIONAL_DISPLAY||OUTDOOR_LOW_LEVEL","EXPLOSIVES_FIREWORKS||PROFESSIONAL_DISPLAY||OUTDOOR_PROXIMATE","EXPLOSIVES_FIREWORKS||UNKNOWN","HEAT_FROM_ANOTHER_OBJECT||SPREAD_FROM_OUTSIDE_STRUCTURE","INCENDIARY||DEVICE","INCENDIARY||FLINT_FRICTION","INCENDIARY||GLASS_REFRACTION_MAGNIFYING_GLASS","INCENDIARY||HOT_SET","INCENDIARY||LIGHTER_MATCHES","INCENDIARY||OTHER","INCENDIARY||UNKNOWN","OPEN_FLAME||BARBEQUE_SMOKER","OPEN_FLAME||EXPOSURE_FIRE_PIT_OPEN_FLAME","OPEN_FLAME||LUMINARY_CANDLES_INCENSE","OPEN_FLAME||OTHER","OPEN_FLAME||UNKNOWN","OPERATING_EQUIPMENT||HEATING_COOLING_EQUIPMENT","OPERATING_EQUIPMENT||HOT_WORK","OPERATING_EQUIPMENT||OTHER","OPERATING_EQUIPMENT||OTHER_SMALL_ENGINE_EQUIPMENT","OPERATING_EQUIPMENT||UNKNOWN","OTHER_HEAT_SOURCE||OTHER","SMOKING_MATERIALS_ILLICIT_DRUGS||CIGAR_CIGARETTE_PIPE","SMOKING_MATERIALS_ILLICIT_DRUGS||DRUG_PARAPHERNALIA","SMOKING_MATERIALS_ILLICIT_DRUGS||ELECTRONIC_CIGARETTE","SMOKING_MATERIALS_ILLICIT_DRUGS||ILLEGAL_SUBSTANCE_MANUFACTURE","SMOKING_MATERIALS_ILLICIT_DRUGS||OTHER","SMOKING_MATERIALS_ILLICIT_DRUGS||UNKNOWN","UNABLE_TO_BE_DETERMINED||NOT_INVESTIGATED","UNABLE_TO_BE_DETERMINED||ORIGIN_AND_OR_CAUSE_NOT_IDENTIFIED","UNABLE_TO_BE_DETERMINED||ORIGIN_DESTROYED","UNABLE_TO_BE_DETERMINED||UNDER_INVESTIGATION"],"title":"TypeIndoorCauseValue"},"TypeIndoorOutdoorValue":{"type":"string","enum":["INDOOR","OUTDOOR"],"title":"TypeIndoorOutdoorValue"},"TypeInitialDetectionValue":{"type":"string","enum":["ALERTED_BY_PERSON","AUDIBLE_NOISE","AUTOMATIC_SUPPRESSION","HEAT_ALARM","MANUAL_ACTIVATION","NO_INITIAL_DETECTION","ODOR","OTHER","PET","SMOKE_ALARM","SPECIALTY_DETECTOR","UNKNOWN","VISUAL_SIGHTING"],"title":"TypeInitialDetectionValue"},"TypeIntegrationValue":{"type":"string","enum":["CAD","RMS","RO","S123"],"title":"TypeIntegrationValue"},"TypeInterstitialSpaceValue":{"type":"string","enum":["ATTIC","CEILING","CRAWL_SPACE","DUCT","FLOOR_ASSEMBLY","OTHER"],"title":"TypeInterstitialSpaceValue"},"TypeItemFirstIgnitedValue":{"type":"string","enum":["ALARM_ESCAPE_PROTECTION_DEVICES","CHILD_NURSERY_EQUIPMENT","CLOTHING_FABRIC","COOKING_MATERIALS","FARM_SUPPLIES_EQUIPMENT","FLAMMABLE_CHEMICALS","FURNISHINGS_FIXTURES","GENERAL_HOUSEHOLD_APPLIANCE","HEATING_COOLING_VENT_APPLIANCE","HOME_COMM_ENTERTAINMENT_HOBBY_EQUIPMENT","HOME_FAMILY_MAINTENANCE_PRODUCT","HOUSEHOLD_PACKAGING_CONTAINER","HOUSEWARE_NON_POWERED","KITCHEN_APPLIANCE","MEDICAL_EQUIPMENT","MISC_PRODUCT","PERSONAL_USE_ITEM","POWER_GEN_TRANS_DIST","REGULATED_PRODUCTS","RUBBISH_TRASH","SPORTS_RECREATION","STRUCTURAL_COMPONENTS","STRUCTURES_CONSTRUCTION","TOYS","VEGETATION","WORKSHOP_TOOL_APPARATUS","YARD_GARDEN_EQUIPMENT"],"title":"TypeItemFirstIgnitedValue"},"TypeJobClassificationValue":{"type":"string","enum":["CAREER","INDUSTRIAL","PAID_ON_CALL","PART_TIME","VOLUNTEER","WILDLAND_CONTRACT","WILDLAND_FULL_TIME","WILDLAND_PART_TIME"],"title":"TypeJobClassificationValue"},"TypeLocCspCountryValue":{"type":"string","enum":["AD","AE","AF","AG","AI","AL","AM","AO","AQ","AR","AS","AT","AU","AW","AX","AZ","BA","BB","BD","BE","BF","BG","BH","BI","BJ","BL","BM","BN","BO","BQ","BR","BS","BT","BV","BW","BY","BZ","CA","CC","CD","CF","CG","CH","CI","CK","CL","CM","CN","CO","CR","CU","CV","CW","CX","CY","CZ","DE","DJ","DK","DM","DO","DZ","EC","EE","EG","EH","ER","ES","ET","FI","FJ","FK","FM","FO","FR","GA","GB","GD","GE","GF","GG","GH","GI","GL","GM","GN","GP","GQ","GR","GS","GT","GU","GW","GY","HK","HM","HN","HR","HT","HU","ID","IE","IL","IM","IN","IO","IQ","IR","IS","IT","JE","JM","JO","JP","KE","KG","KH","KI","KM","KN","KP","KR","KW","KY","KZ","LA","LB","LC","LI","LK","LR","LS","LT","LU","LV","LY","MA","MC","MD","ME","MF","MG","MH","MK","ML","MM","MN","MO","MP","MQ","MR","MS","MT","MU","MV","MW","MX","MY","MZ","NA","NC","NE","NF","NG","NI","NL","NO","NP","NR","NU","NZ","OM","PA","PE","PF","PG","PH","PK","PL","PM","PN","PR","PS","PT","PW","PY","QA","RE","RO","RS","RU","RW","SA","SB","SC","SD","SE","SG","SH","SI","SJ","SK","SL","SM","SN","SO","SR","SS","ST","SV","SX","SY","SZ","TC","TD","TF","TG","TH","TJ","TK","TL","TM","TN","TO","TR","TT","TV","TW","TZ","UA","UG","UM","US","UY","UZ","VA","VC","VE","VG","VI","VN","VU","WF","WS","YE","YT","ZA","ZM","ZW"],"title":"TypeLocCspCountryValue"},"TypeLocPlaceValue":{"type":"string","enum":["AIRCRAFT","AIRPORT","ARENA","AUTOMOBILE","BANK","BAR","BICYCLE","BUS","BUS_STATION","CAFE","CLASSROOM","CLUB","CONSTRUCTION","CONVENTION_CENTER","GOVERNMENT","HOSPITAL","HOTEL","INDUSTRIAL","LIBRARY","MOTORCYCLE","MUSEUM","OFFICE","OTHER","OUTDOORS","PARKING","PLACE_OF_WORSHIP","PRISON","PUBLIC","PUBLIC_TRANSPORT","RESIDENCE","RESTAURANT","SCHOOL","SHOPPING_AREA","STADIUM","STORE","STREET","THEATER","TRAIN","TRAIN_STATION","TRUCK","UNDERWAY","UNKNOWN","WAREHOUSE","WATER","WATERCRAFT"],"title":"TypeLocPlaceValue"},"TypeLocSnDirectionValue":{"type":"string","enum":["EASTBOUND","NORTHBOUND","SOUTHBOUND","WESTBOUND"],"title":"TypeLocSnDirectionValue"},"TypeLocSnPrePostValue":{"type":"string","enum":["ABBEY","ACCESS","ACCESS ROAD","ACRES","AIRPORT","ALCOVE","ALLE","ALLEY","ANNEX","APPROACH","ARC","ARCADE","ARCH","AVENIDA","AVENUE","AVENUE CIRCLE","AVENUE COURT","AVENUE LOOP","AVENUE PATH","AVENUE PLACE","AVENUE WAY","BANK","BAY","BAYOU","BAYWAY","BEACH","BEND","BLUFF","BLUFFS","BOARDWALK","BOTTOM","BOULEVARD","BRANCH","BRIDGE","BROOK","BROOKS","BUREAU OF INDIAN AFFAIRS ROUTE","BURG","BURGS","BYPASS","CALLE","CALLEJON","CAMINO","CAMP","CANYON","CAPE","CARTWAY","CAUSEWAY","CENTER","CENTERS","CENTRE","CHANNEL","CHASE","CHEMIN","CIRCLE","CIRCLES","CIRCUS","CLIFF","CLIFFS","CLOSE","CLUB","CLUSTER","COAST HIGHWAY","COMMON","COMMONS","CONCESSION ROAD","CONCOURSE","CONNECT","CONNECTOR","CORNER","CORNERS","CORRIDOR","CORSO","CORTE","COUNTY FOREST ROAD","COUNTY HIGHWAY","COUNTY ROAD","COUNTY ROUTE","COUNTY STATE AID HIGHWAY","COURS","COURSE","COURT","COURTS","COVE","COVES","CREEK","CRESCENT","CREST","CROSS","CROSSING","CROSSINGS","CROSSOVER","CROSSROAD","CROSSROADS","CROSSWAY","CURVE","CUSTER COUNTY ROAD","CUTOFF","CUTTING","DALE","DAM","DAWSON COUNTY ROAD","DELL","DIVIDE","DOCK","DOWN","DOWNS","DRAW","DRIFT","DRIVE","DRIVES","DRIVEWAY","DUGWAY","ECHO","EDGE","END","ENTRANCE","ENTRY","ESPLANADE","ESTATE","ESTATES","EXCHANGE","EXIT","EXPRESSWAY","EXTENSION","EXTENSIONS","FALL","FALLS","FARE","FARM","FARM TO MARKET","FEDERAL-AID SECONDARY HIGHWAY","FERRY","FIELD","FIELDS","FLAT","FLATS","FLOWAGE","FLYWAY","FORD","FORDS","FOREST","FOREST HIGHWAY","FOREST ROAD","FORGE","FORGES","FORK","FORKS","FORT","FREEWAY","FRONT","FRONTAGE ROAD","GABLES","GARDEN","GARDENS","GARTH","GATE","GATES","GATEWAY","GLADE","GLEN","GLENS","GORGE","GRADE","GREEN","GREENS","GREENWAY","GROVE","GROVES","HARBOR","HARBORS","HARBOUR","HAUL ROAD","HAVEN","HEATH","HEIGHTS","HIDEAWAY","HIGHWAY","HILL","HILLS","HOLLOW","HORN","HORSESHOE","INDIAN SERVICE ROAD","INLET","INTERSTATE","INTERVAL","ISLAND","ISLANDS","ISLE","ISLES","J-TURN","JUNCTION","JUNCTIONS","KEEP","KEY","KEYS","KNOLL","KNOLLS","LAIR","LAKE","LAKES","LAND","LANDING","LANE","LANE CIRCLE","LANE COURT","LANE ROAD","LATERAL","LEDGE","LIGHT","LIGHTS","LINE","LOAF","LOCK","LOCKS","LODGE","LOOKOUT","LOOP","LOOP ROAD","LUGAR","MALL","MANOR","MANORS","MARKET","MEADOW","MEADOWS","MEWS","MILL","MILLS","MISSION","MONTANA HIGHWAY","MOTORWAY","MOUNT","MOUNTAIN","MOUNTAINS","NARROWS","NATIONAL FOREST DEVELOPMENT ROAD","NECK","NOOK","NORTH CAROLINA HIGHWAY","OAKS","OLD COUNTY ROAD","ORCHARD","OVAL","OVERLOOK","OVERPASS","OVI","PARK","PARKE","PARKS","PARKWAY","PARKWAYS","PASEO","PASS","PASSAGE","PATH","PATHWAY","PIAZZA","PIKE","PINE","PINES","PLACE","PLACITA","PLAIN","PLAINS","PLATZ","PLAZA","POINT","POINTE","POINTS","PORT","PORTS","PRAIRIE","PRIVATE ROAD","PROMENADE","PUBLIC ACCESS","QUARTER","QUAY","RADIAL","RAMP","RANCH","RANCHO","RAPID","RAPIDS","REACH","RECREATIONAL ROAD","REST","RETREAT","RIDGE","RIDGES","RISE","RIVER","RIVER ROAD","ROAD","ROADS","ROUND","ROUTE","ROW","RUE","RUN","RUNNE","RUNWAY","SHOAL","SHOALS","SHORE","SHORES","SIDEROAD","SKIES","SKYWAY","SLIP","SPRING","SPRINGS","SPUR","SPURS","SQUARE","SQUARES","STATE HIGHWAY","STATE PARK ROAD","STATE PARKWAY","STATE ROAD","STATE ROUTE","STATE SECONDARY","STATE SPUR","STATION","STRAND","STRASSE","STRAVENUE","STREAM","STREET","STREET CIRCLE","STREET COURT","STREET LOOP","STREET PATH","STREET PLACE","STREET WAY","STREETS","STRIP","SUMMIT","TAXIWAY","TERMINAL","TERN","TERRACE","THROUGHWAY","THRUWAY","TIMBER ROAD","TOWN ROAD","TOWNLINE","TOWNSHIP ROAD","TRACE","TRACK","TRAFFICWAY","TRAIL","TRAILER","TRIANGLE","TRUCK TRAIL","TUNNEL","TURN","TURNPIKE","UNDERPASS","UNION","UNIONS","UNITED STATES FOREST SERVICE ROAD","UNITED STATES HIGHWAY","UUNYE","VALLEY","VALLEYS","VIA","VIADUCT","VIEW","VIEWS","VILLA","VILLAGE","VILLAGES","VILLE","VISTA","VOG","WADDY","WALK","WALKS","WALL","WAY","WAYS","WEEG","WELL","WELLS","WOODS","WYE","WYND"],"title":"TypeLocSnPrePostValue"},"TypeLocSnPreSepValue":{"type":"string","enum":["AT","DE","DEL","DES","DE_LA","DE_LAS","IN_THE","OF","OF_THE","ON_THE","TO","TO_THE"],"title":"TypeLocSnPreSepValue"},"TypeLocationCrossStreetValue":{"type":"string","enum":["CLOSEST","INCIDENT_IN_INTERSECTION","SECOND_CLOSEST"],"title":"TypeLocationCrossStreetValue"},"TypeLocationUseValue":{"type":"string","enum":["AGRICULTURE_STRUCT||ANIMAL_PROCESSING","AGRICULTURE_STRUCT||AUCTION_FEEDLOT","AGRICULTURE_STRUCT||FARM_BUILDING","AGRICULTURE_STRUCT||STORAGE_SILO","AGRICULTURE_STRUCT||VETERINARY_LIVESTOCK","ASSEMBLY||COMMUNITY_CENTER","ASSEMBLY||CONVENTION_CENTER","ASSEMBLY||INDOOR_ARENA","ASSEMBLY||MUSEUM_EXHIBIT_HALL_LIBRARY","ASSEMBLY||OUTDOOR_ARENA_AMPHITHEATER_PARK","ASSEMBLY||RELIGIOUS","ASSEMBLY||TEMP_OUTDOOR_STRUCT_EVENT","COMMERCIAL||BAR_NIGHTCLUB","COMMERCIAL||ENTERTAINMENT_RECREATION","COMMERCIAL||OFFICE_OTHER_TECHNICAL_SERVICES","COMMERCIAL||RESTAURANT_CAFE","COMMERCIAL||RETAIL_WHOLESALE_TRADE","COMMERCIAL||THEATERS_STUDIO","COMMERCIAL||VEHICLE_FUELING_CHARGING_STATION","COMMERCIAL||VEHICLE_REPAIR_SERVICES","COMMERCIAL||VETERINARY_PET","EDUCATION||COLLEGES_UNIVERSITIES","EDUCATION||DORMITORY_HOUSING","EDUCATION||K_12_SCHOOLS","EDUCATION||OTHER_EDUCATIONAL_BUILDINGS","EDUCATION||PREK_DAYCARE","GOVERNMENT||FIRE_MEDICAL_STATION","GOVERNMENT||GENERAL_SERVICES","GOVERNMENT||JAIL_PRISON_REFORMATORY","GOVERNMENT||NON_CIVILIAN_STRUCTURES","GOVERNMENT||POLICE_EMERGENCY_STATION","HEALTH_CARE||ALCOHOL_DRUG_REHABILITATION_CENTER","HEALTH_CARE||HOSPITAL_24_HOUR_MEDICAL_FACILITIES","HEALTH_CARE||MEDICAL_OFFICE_CLINIC","HEALTH_CARE||NURSING_HOME_ASSISTED_LIVING_RESIDENCE_ONSITE","INDUSTRIAL||CHEMICAL","INDUSTRIAL||COLD_STORAGE","INDUSTRIAL||FOOD_DRUGS","INDUSTRIAL||HEAVY","INDUSTRIAL||LIGHT","INDUSTRIAL||METALS_MINERALS_PROCESSING","OUTDOOR_INDUSTRIAL||CONSTRUCTION_SITE","OUTDOOR_INDUSTRIAL||DUMP_LANDFILL","OUTDOOR_INDUSTRIAL||INDUSTRIAL_YARD","OUTDOOR_INDUSTRIAL||MINE","OUTDOOR||CAMP_SITE","OUTDOOR||FOREST_GRASSLANDS_WOODLAND_WILDLAND_AREAS","OUTDOOR||GROUND_VACANT_LAND","OUTDOOR||HIKING_TRAIL","OUTDOOR||OPEN_WATER","OUTDOOR||ORCHARD_CROPS_FARMLAND","OUTDOOR||PLAYGROUND_PARK_RECREATIONAL_AREA","OUTDOOR||WATERFRONT","RESIDENTIAL||ATTACHED_SINGLE_FAMILY_DWELLING","RESIDENTIAL||CONGREGATE_HOUSING","RESIDENTIAL||DETATCHED_GARAGE","RESIDENTIAL||DETATCHED_SINGLE_FAMILY_DWELLING","RESIDENTIAL||MANUFACTURED_MOBILE_HOME","RESIDENTIAL||MULTI_FAMILY_HIGHRISE_DWELLING","RESIDENTIAL||MULTI_FAMILY_LOWRISE_DWELLING","RESIDENTIAL||MULTI_FAMILY_MIDRISE_DWELLING","RESIDENTIAL||TEMPORARY_LODGING_HOTEL_MOTEL","RESIDENTIAL||UNHOUSED_TEMPORARY_SHELTER","ROADWAY_ACCESS||BRIDGE","ROADWAY_ACCESS||HIGHWAY_INTERSTATE","ROADWAY_ACCESS||LIMITED_ACCESS_HIGHWAY_INTERSTATE","ROADWAY_ACCESS||PARKING_LOT_GARAGE","ROADWAY_ACCESS||RAILROAD_RAILYARD","ROADWAY_ACCESS||SIDEWALK","ROADWAY_ACCESS||STREET","ROADWAY_ACCESS||TUNNEL","STORAGE||STORAGE_MULTI_TENANT","STORAGE||STORAGE_PORTABLE_BUILDING","STORAGE||STORAGE_SINGLE_TENANT","UNCLASSIFIED||UNCLASSIFIED","UTILITY_MISC||ENERGY_FACILITY_INFRASTRUCTURE","UTILITY_MISC||TRANSPORTATION_STATION_HUB_AREA","UTILITY_MISC||TRASH_RECYCLING_FACILITY","UTILITY_MISC||WATER_SANITATION_FACILITY_INFRASTRUCTURE"],"title":"TypeLocationUseValue"},"TypeMedicalPatientCareValue":{"type":"string","enum":["PATIENT_DEAD_ON_ARRIVAL","PATIENT_EVALUATED_CARE_PROVIDED","PATIENT_EVALUATED_NO_CARE_REQUIRED","PATIENT_EVALUATED_REFUSED_CARE","PATIENT_REFUSED_EVALUATION_CARE","PATIENT_SUPPORT_SERVICES_PROVIDED"],"title":"TypeMedicalPatientCareValue"},"TypeMedicalPatientStatusValue":{"type":"string","enum":["IMPROVED","UNCHANGED","WORSE"],"title":"TypeMedicalPatientStatusValue"},"TypeMedicalTransportValue":{"type":"string","enum":["NONPATIENT_TRANSPORT","NO_TRANSPORT","OTHER_AGENCY_TRANSPORT","PATIENT_REFUSED_TRANSPORT","TRANSPORT_BY_EMS_UNIT"],"title":"TypeMedicalTransportValue"},"TypeNoactionValue":{"type":"string","enum":["CANCELLED","NO_INCIDENT_FOUND","STAGED_STANDBY"],"title":"TypeNoactionValue"},"TypeOccupantResponseValue":{"type":"string","enum":["ATTEMPTED_TO_EXTINGUISH","ATTEMPTED_TO_RESCUE_ANIMALS","ATTEMPTED_TO_RESCUE_OCCUPANTS","EVACUATED","IGNORED_ALARM","UNABLE_TO_RESPOND","UNKNOWN"],"title":"TypeOccupantResponseValue"},"TypeOutdoorActivitiesValue":{"type":"string","enum":["CONSTRUCTION","ELECTRICAL_UTILITIES","FARMING_RANCHING","GOVERNMENTAL_TRIBAL_ACTIVITIES","HUNTING_TRAPPING_FISHING","LAW_ENFORCEMENT","LOGGING_FORESTRY","MILITARY","MINING_EXTRACTION","MOTORIST","OTHER","OTHER_UTILITIES","PRIVATE_RESIDENTIAL","RAILROAD","RECREATION","SUPPRESSION_ACTIVITY","TRANSIENT","UNKNOWN","WEATHER"],"title":"TypeOutdoorActivitiesValue"},"TypeOutdoorCauseValue":{"type":"string","enum":["DEBRIS_OPEN_BURNING||BRANDING","DEBRIS_OPEN_BURNING||BURNING_PERSONAL_ITEMS","DEBRIS_OPEN_BURNING||BURN_BARREL","DEBRIS_OPEN_BURNING||DISTRESS_SIGNAL_FIRE","DEBRIS_OPEN_BURNING||DITCH_FENCE_LINE_BURNING","DEBRIS_OPEN_BURNING||ESCAPED_PRESCRIBED_BURN","DEBRIS_OPEN_BURNING||FIELD_AGRICULTURAL_BURNING","DEBRIS_OPEN_BURNING||HAND_PILE_SLASH","DEBRIS_OPEN_BURNING||MACHINE_PILE_SLASH","DEBRIS_OPEN_BURNING||OPEN_TRASH_BURNING","DEBRIS_OPEN_BURNING||OTHER","DEBRIS_OPEN_BURNING||OTHER_LAND_CLEARING","DEBRIS_OPEN_BURNING||PEST_CONTROL_DETERRENT_SMOKE_OUT","DEBRIS_OPEN_BURNING||RIGHT_OF_WAY_CLEARING","DEBRIS_OPEN_BURNING||UNKNOWN","DEBRIS_OPEN_BURNING||YARD_DEBRIS","EQUIPMENT_VEHICLE_USE||AIRCRAFT","EQUIPMENT_VEHICLE_USE||CHAINSAW_BRUSH_SAW_WEED_TRIMMER","EQUIPMENT_VEHICLE_USE||COMMERCIAL_TRANSPORT_VEHICLE","EQUIPMENT_VEHICLE_USE||ELECTRIC_MOTOR_POWER_TOOLS_BATTERY","EQUIPMENT_VEHICLE_USE||HEAVY_EQUIPMENT_IMPLEMENTS","EQUIPMENT_VEHICLE_USE||HOT_WORK","EQUIPMENT_VEHICLE_USE||OHV_ATV_MOTORCYCLE","EQUIPMENT_VEHICLE_USE||OTHER","EQUIPMENT_VEHICLE_USE||OTHER_SMALL_ENGINE_EQUIPMENT","EQUIPMENT_VEHICLE_USE||PASSENGER_VEHICLE_MOTORIZED_RV","EQUIPMENT_VEHICLE_USE||TRACTOR_MOWER_BRUSH_HOG","EQUIPMENT_VEHICLE_USE||TRAILER","EQUIPMENT_VEHICLE_USE||UAS_MODEL_ROCKETS_AIRPLANES","EQUIPMENT_VEHICLE_USE||UNKNOWN","EQUIPMENT_VEHICLE_USE||WATERCRAFT","FIREARMS_EXPLOSIVES||AMMONIUM_NITRATE_FUEL_OIL","FIREARMS_EXPLOSIVES||BLACK_POWDER_MUZZLE_LOADING","FIREARMS_EXPLOSIVES||BLASTING","FIREARMS_EXPLOSIVES||DETONATED_CORD","FIREARMS_EXPLOSIVES||EXPLODING_TARGET_SHOOTING","FIREARMS_EXPLOSIVES||FLARES_FUSES","FIREARMS_EXPLOSIVES||IMPROVISED_EXPLOSIVE_DEVICE","FIREARMS_EXPLOSIVES||INERT_TARGET_SHOOTING","FIREARMS_EXPLOSIVES||MILITARY_ORDINANCE","FIREARMS_EXPLOSIVES||NON_MILITARY_TRACER","FIREARMS_EXPLOSIVES||OTHER","FIREARMS_EXPLOSIVES||OTHER_EXPLOSIVES","FIREARMS_EXPLOSIVES||PIPE_BOMB","FIREARMS_EXPLOSIVES||UNKNOWN","FIREWORKS||CONSUMER_AERIAL||COMETS_MINES_SHELLS","FIREWORKS||CONSUMER_AERIAL||HELICOPTERS","FIREWORKS||CONSUMER_AERIAL||MISSILES","FIREWORKS||CONSUMER_AERIAL||OTHER_SPECIALTY_ITEMS","FIREWORKS||CONSUMER_AERIAL||RELOADABLE_TUBE_AERIAL","FIREWORKS||CONSUMER_AERIAL||ROCKETS","FIREWORKS||CONSUMER_AERIAL||ROMAN_CANDLES","FIREWORKS||CONSUMER_NON_AERIAL||CHASERS","FIREWORKS||CONSUMER_NON_AERIAL||COMBINATION_ITEMS","FIREWORKS||CONSUMER_NON_AERIAL||FIRECRACKERS","FIREWORKS||CONSUMER_NON_AERIAL||FOUNTAINS","FIREWORKS||CONSUMER_NON_AERIAL||GROUND_SPINNERS","FIREWORKS||CONSUMER_NON_AERIAL||HANDHELD_SPARKLERS","FIREWORKS||CONSUMER_NON_AERIAL||OTHER_SPECIALTY_ITEMS","FIREWORKS||CONSUMER_NON_AERIAL||WHEELS","FIREWORKS||DEREGULATED_NOVELTIES||PARTY_POPPERS","FIREWORKS||DEREGULATED_NOVELTIES||SMOKE_BALLS","FIREWORKS||DEREGULATED_NOVELTIES||SNAKES","FIREWORKS||DEREGULATED_NOVELTIES||SNAP_CAPS","FIREWORKS||DEREGULATED_NOVELTIES||WIRE_CORE_SPARKLERS","FIREWORKS||EXPLOSIVES||1_4_STICK","FIREWORKS||EXPLOSIVES||ALTERED_CONSUMER_FIREWORKS","FIREWORKS||EXPLOSIVES||CHERRY_BOMB","FIREWORKS||EXPLOSIVES||HOMEMADE_EXPLOSIVE_DEVICE","FIREWORKS||EXPLOSIVES||M_100","FIREWORKS||EXPLOSIVES||M_1000","FIREWORKS||EXPLOSIVES||M_80","FIREWORKS||EXPLOSIVES||SILVER_SALUTE","FIREWORKS||EXPLOSIVES||SPARKLER_BOMB","FIREWORKS||MODEL_AMATEUR_ROCKETS","FIREWORKS||OTHER","FIREWORKS||PROFESSIONAL_DISPLAY||INDOOR_PROXIMATE","FIREWORKS||PROFESSIONAL_DISPLAY||OUTDOOR_AERIAL_SHELL","FIREWORKS||PROFESSIONAL_DISPLAY||OUTDOOR_GROUND_LEVEL","FIREWORKS||PROFESSIONAL_DISPLAY||OUTDOOR_LOW_LEVEL","FIREWORKS||PROFESSIONAL_DISPLAY||OUTDOOR_PROXIMATE","FIREWORKS||UNKNOWN","INCENDIARY||DEVICE","INCENDIARY||FLINT_FRICTION","INCENDIARY||GLASS_REFRACTION_MAGNIFYING_GLASS","INCENDIARY||HOT_SET","INCENDIARY||LIGHTER_MATCHES","INCENDIARY||OTHER","INCENDIARY||UNKNOWN","NATURAL||ANIMAL","NATURAL||GLASS_REFRACTION_MAGNIFYING_GLASS","NATURAL||LIGHTNING","NATURAL||OTHER","NATURAL||SPONTANEOUS_COMBUSTION","NATURAL||UNKNOWN","NATURAL||VOLCANO","POWER_GEN_TRANS_DIST||ELECTRICAL_TRANSMISSION_DISTRIBUTION_SYSTEMS","POWER_GEN_TRANS_DIST||ENERGY_STORAGE_SYSTEM","POWER_GEN_TRANS_DIST||OIL_GAS_PRODUCTION_TRANSPORTATION","POWER_GEN_TRANS_DIST||OTHER","POWER_GEN_TRANS_DIST||SOLAR_UTILITY_SYSTEM","POWER_GEN_TRANS_DIST||UNKNOWN","POWER_GEN_TRANS_DIST||WIND_TURBINE_WINDMILLS_UTILITY_SYSTEM","RAILROAD_OPS_MAINTENANCE||BRAKES","RAILROAD_OPS_MAINTENANCE||DERAILMENT","RAILROAD_OPS_MAINTENANCE||DYNAMIC_GRID_FAILURE","RAILROAD_OPS_MAINTENANCE||EXHAUST_PARTICLES","RAILROAD_OPS_MAINTENANCE||HOT_WORK","RAILROAD_OPS_MAINTENANCE||OTHER","RAILROAD_OPS_MAINTENANCE||OTHER_MECHANICAL_FAILURE","RAILROAD_OPS_MAINTENANCE||RAIL_GRINDING","RAILROAD_OPS_MAINTENANCE||RIGHT_OF_WAY_VEGETATION_MAINTENANCE","RAILROAD_OPS_MAINTENANCE||TRACK_REPLACEMENT","RAILROAD_OPS_MAINTENANCE||UNKNOWN","RECREATION_CEREMONY||BARBECUE_SMOKER","RECREATION_CEREMONY||BONFIRE_PARTY_FIRE","RECREATION_CEREMONY||CAMPFIRE","RECREATION_CEREMONY||CEREMONIAL_FIRE","RECREATION_CEREMONY||GAS_COOKING_WARMING_LIGHTING_DEVICE","RECREATION_CEREMONY||LUMINARY","RECREATION_CEREMONY||OTHER","RECREATION_CEREMONY||OUTDOOR_OVEN_FIREPLACE_METAL_FIRE_RING","RECREATION_CEREMONY||UNKNOWN","SMOKING_MATERIALS_ILLICIT_DRUGS||CIGAR_CIGARETTE_PIPE","SMOKING_MATERIALS_ILLICIT_DRUGS||DRUG_PARAPHERNALIA","SMOKING_MATERIALS_ILLICIT_DRUGS||ELECTRONIC_CIGARETTE","SMOKING_MATERIALS_ILLICIT_DRUGS||ILLEGAL_SUBSTANCE_MANUFACTURE","SMOKING_MATERIALS_ILLICIT_DRUGS||OTHER","SMOKING_MATERIALS_ILLICIT_DRUGS||UNKNOWN","SPREAD_FROM_CONTROLLED_BURN","STRUCTURE||ELECTRIC_FENCE","STRUCTURE||SPREAD_FROM_STRUCTURE","UNABLE_TO_BE_DETERMINED||NOT_INVESTIGATED","UNABLE_TO_BE_DETERMINED||ORIGIN_AND_OR_CAUSE_NOT_IDENTIFIED","UNABLE_TO_BE_DETERMINED||ORIGIN_DESTROYED","UNABLE_TO_BE_DETERMINED||UNDER_INVESTIGATION"],"title":"TypeOutdoorCauseValue"},"TypePopSourceValue":{"type":"string","enum":["CENSUS_DERIVED","DEPARTMENT_ENTERED"],"title":"TypePopSourceValue"},"TypePowertrainValue":{"type":"string","enum":["BATTERY_ELECTRIC_VEHICLE","FUEL_CELL_ELECTRIC_VEHICLE","HYBRID_ELECTRIC_VEHICLE","INTERNAL_COMBUSTION_ENGINE","PLUG_IN_HYBRID_ELECTRIC_VEHICLE"],"title":"TypePowertrainValue"},"TypeProductContributionValue":{"type":"string","enum":["IGNITION","RELEASE","SPREAD"],"title":"TypeProductContributionValue"},"TypePsapCapaValue":{"type":"string","enum":["LEGACY","NG911"],"title":"TypePsapCapaValue"},"TypePsapDiscValue":{"type":"string","enum":["MULTIPLE","SINGLE"],"title":"TypePsapDiscValue"},"TypePsapJurisValue":{"type":"string","enum":["MULTIPLE","SINGLE"],"title":"TypePsapJurisValue"},"TypePsapValue":{"type":"string","enum":["PRIMARY","SECONDARY"],"title":"TypePsapValue"},"TypeRaceValue":{"type":"string","enum":["AMERICAN_INDIAN_ALASKA_NATIVE","ASIAN","BLACK_AFRICAN_AMERICAN","HISPANIC_LATINO","MIDDLE_EASTERN_NORTH_AFRICAN","NATIVE_HAWAIIAN_PACIFIC_ISLANDER","OTHER","UNKNOWN","WHITE"],"title":"TypeRaceValue"},"TypeRateOfSpreadValue":{"type":"string","enum":["CRITICAL","DANGEROUS","MODERATE","SLOW","UNKNOWN"],"title":"TypeRateOfSpreadValue"},"TypeRegionValue":{"type":"string","enum":["BATTALION","COUNCIL_DISTRICT","DISTRICT","DIVISION","FIRST_DUE","JURISDICTION","OTHER"],"title":"TypeRegionValue"},"TypeRelativePositionValue":{"type":"string","enum":["LOWER_SLOPE","MID_SLOPE","RIDGE_TOP","UNKNOWN","UPPER_SLOPE","VALLEY_BOTTOM"],"title":"TypeRelativePositionValue"},"TypeRescueActionValue":{"type":"string","enum":["BRACE_WALL_INFRASTRUCTURE","BREAK_BREACH_WALL","HYDRAULIC_TOOL_USE","NONE","ROPE_RIGGING","SUPPLY_AIR","TRENCH_SHORING","UNDERWATER_DIVE","VENTILATION"],"title":"TypeRescueActionValue"},"TypeRescueElevationValue":{"type":"string","enum":["ON_BED","ON_FLOOR","ON_FURNITURE","OTHER"],"title":"TypeRescueElevationValue"},"TypeRescueImpedimentValue":{"type":"string","enum":["ACCESS_LIMITATIONS","HOARDING_CONDITIONS","IMPAIRED_PERSON","NONE","OTHER","PHYSICAL_MEDICAL_CONDITIONS_PERSON"],"title":"TypeRescueImpedimentValue"},"TypeRescueModeValue":{"type":"string","enum":["DISENTANGLEMENT","EXTRICATION","OTHER","RECOVERY","REMOVAL_FROM_STRUCTURE"],"title":"TypeRescueModeValue"},"TypeRescuePathValue":{"type":"string","enum":["REMOVAL_ALONG_ALT_PATH","REMOVAL_ALONG_PRIMARY_PATH"],"title":"TypeRescuePathValue"},"TypeRescuePresenceKnownValue":{"type":"string","enum":["KNOWN_ARRIVAL","KNOWN_DISPATCH","KNOWN_DURING"],"title":"TypeRescuePresenceKnownValue"},"TypeResourceValue":{"type":"string","enum":["ENTITY","ENTITY_ACTIVATION","ENTITY_FEATURE_FLAGS","ENTITY_RELATIONSHIP","ENTITY_RELATIONSHIP_ACKNOWLEDGMENT","ENTITY_ROLE_ENTITY_SET_ATTACHMENT","ENTITY_SET","INCIDENT","INCIDENT_ANALYSIS","INCIDENT_HISTORY","INCIDENT_STATUS","INTEGRATION","INTEGRATION_ENROLLMENT","NO_ACTIVITY_REPORT","ROLE","STATION","USER","USER_ACTIVATION","USER_ENTITY_ACTIVATION","USER_ENTITY_MEMBERSHIP","USER_PERMISSIONS","USER_ROLE_ENTITY_SET_ATTACHMENT"],"title":"TypeResourceValue"},"TypeResponseModeValue":{"type":"string","enum":["EMERGENT","NON_EMERGENT"],"title":"TypeResponseModeValue"},"TypeRoofMaterialValue":{"type":"string","enum":["ASPHALT_SHINGLES","CLAY_TILES","COMPOSITE_SHINGLES","CONCRETE_TILES","MEMBRANE","METAL","OTHER","SLATE","SOLAR_TILES","WOOD_SHINGLES"],"title":"TypeRoofMaterialValue"},"TypeRoomValue":{"type":"string","enum":["ASSEMBLY","ATTIC","BALCONY_PORCH_DECK","BASEMENT","BATHROOM","BEDROOM","GARAGE","HALLWAY_FOYER","KITCHEN","LIVING_SPACE","OFFICE","OTHER","UNKNOWN","UTILITY_ROOM"],"title":"TypeRoomValue"},"TypeServEmsValue":{"type":"string","enum":["AERO_TRANSPORT","ALS_NO_TRANSPORT","ALS_TRANSPORT","BLS_NO_TRANSPORT","BLS_TRANSPORT","COMMUNITY_MED","NO_MEDICAL"],"title":"TypeServEmsValue"},"TypeServFdValue":{"type":"string","enum":["ANIMAL_TECHRESCUE","ARFF_FIREFIGHTING","CAUSE_ORIGIN","CAVE_SAR","COLLAPSE_RESCUE","CONFINED_SPACE","DIVE_SAR","FLOOD_SAR","HAZMAT_OPS","HAZMAT_TECHNICIAN","HELO_SAR","HIGHRISE_FIREFIGHTING","ICE_RESCUE","MACHINERY_RESCUE","MARINE_FIREFIGHTING","MINE_SAR","PETROCHEM_FIREFIGHTING","REHABILITATION","ROPE_RESCUE","RRD_EXISTING","RRD_NEWCONST","RRD_PLANS","RRD_PUBLICED","STRUCTURAL_FIREFIGHTING","SURF_RESCUE","SWIFTWATER_SAR","TOWER_SAR","TRAINING_DRIVER","TRAINING_ELF","TRAINING_OD","TRAINING_VETFF","TRENCH_RESCUE","VEHICLE_RESCUE","WATERCRAFT_RESCUE","WATER_SAR","WILDERNESS_SAR","WILDLAND_FIREFIGHTING"],"title":"TypeServFdValue"},"TypeServInvestValue":{"type":"string","enum":["COMPANY_LEVEL","DEDICATED","K9_DETECT","LAW_ENFORCEMENT","YOUTH_FIRESETTER"],"title":"TypeServInvestValue"},"TypeSourceTargetValue":{"type":"string","enum":["SOURCE","TARGET","UNKNOWN"],"title":"TypeSourceTargetValue"},"TypeSpecialModifierValue":{"type":"string","enum":["ACTIVE_ASSAILANT","COUNTY_LOCAL_DECLARED_DISASTER","FEDERAL_DECLARED_DISASTER","MCI","STATE_DECLARED_DISASTER","URBAN_CONFLAGRATION","VIOLENCE_AGAINST_RESPONDER","WORLD_CUP_2026"],"title":"TypeSpecialModifierValue"},"TypeSuppressApplianceValue":{"type":"string","enum":["AIRATTACK_HELITACK","BOOSTER_FIRE_HOSE","BUILDING_FDC","BUILDING_STANDPIPE","ELEVATED_MASTER_STREAM_STANDPIPE","FIRE_EXTINGUISHER","GROUND_MONITOR","MASTER_STREAM","MEDIUM_DIAMETER_FIRE_HOSE","NONE","OTHER","SMALL_DIAMETER_FIRE_HOSE"],"title":"TypeSuppressApplianceValue"},"TypeSuppressCookingValue":{"type":"string","enum":["COMMERCIAL_HOOD_SUPPRESSION","ELECTRIC_POWER_CUTOFF_DEVICE","OTHER","RESIDENTIAL_HOOD_MOUNTED","TEMPERATURE_LIMITING_STOVE"],"title":"TypeSuppressCookingValue"},"TypeSuppressFireValue":{"type":"string","enum":["CLEAN_AGENT_SYSTEM","DELUGE_SYSTEM","DRY_PIPE_SPRINKLER_SYSTEM","INDUSTRIAL_DRY_CHEM_SYSTEM","OTHER","PRE_ACTION_SYSTEM","UNKNOWN","WET_PIPE_SPRINKLER_SYSTEM"],"title":"TypeSuppressFireValue"},"TypeSuppressNoOperationValue":{"type":"string","enum":["INSUFFICIENT_SOURCE","INSUFFICIENT_WATER_SUPPLY","SYSTEM_DAMAGED_COMPROMISED","SYSTEM_INOPERABLE","SYSTEM_NOT_SUITABLE","SYSTEM_SHUTOFF_DURING_INCIDENT","SYSTEM_SHUTOFF_PRIOR_TO_INCIDENT","UNABLE_TO_DETERMINE"],"title":"TypeSuppressNoOperationValue"},"TypeSuppressOperationValue":{"type":"string","enum":["NO_OPERATION","OPERATED_EFFECTIVE","OPERATED_NOT_EFFECTIVE"],"title":"TypeSuppressOperationValue"},"TypeSuppressTimeValue":{"type":"string","enum":["DURING_SUPPRESSION","POST_SUPPRESSION","PRE_SUPPRESSION"],"title":"TypeSuppressTimeValue"},"TypeUnitValue":{"type":"string","enum":["AIR_EMS","AIR_LIGHT","AIR_RECON","AIR_TANKER","ALS_AMB","ARFF","ATV_EMS","ATV_FIRE","BLS_AMB","BOAT","BOAT_LARGE","CHIEF_STAFF_COMMAND","CREW","CREW_TRANS","DECON","DOZER","EMS_NOTRANS","EMS_SUPV","ENGINE_STRUCT","ENGINE_WUI","FOAM","HAZMAT","HELO_FIRE","HELO_GENERAL","HELO_RESCUE","INVEST","LADDER_QUINT","LADDER_SMALL","LADDER_TALL","LADDER_TILLER","MAB","MOBILE_COMMS","MOBILE_ICP","OTHER_GROUND","PLATFORM","PLATFORM_QUINT","POV","QUINT_TALL","REHAB","RESCUE_HEAVY","RESCUE_LIGHT","RESCUE_MEDIUM","RESCUE_USAR","RESCUE_WATER","SCBA","TENDER","UAS_FIRE","UAS_RECON","UTIL"],"title":"TypeUnitValue"},"TypeVacancyValue":{"type":"string","enum":["ABANDONED","DAMAGE_DECAY","FORECLOSURE","FOR_SALE_LEASE","NEW_CONSTRUCTION_REMODEL","SEASONAL_OCCASIONALLY_OCCUPIED","UNKNOWN"],"title":"TypeVacancyValue"},"TypeVehicleDamageValue":{"type":"string","enum":["DAMAGED_DRIVABLE","DAMAGED_NOT_DRIVABLE","NO_DAMAGE"],"title":"TypeVehicleDamageValue"},"TypeVentsValue":{"type":"string","enum":["MESH_SCREEN_GREATER_THAN_1_8_INCH","MESH_SCREEN_LESS_THAN_1_8_INCH","NO_VENTS","UNSCREENED"],"title":"TypeVentsValue"},"TypeWaterSupplyValue":{"type":"string","enum":["DRAFT_FROM_STATIC_SOURCE","FOAM_ADDITIVE","HYDRANT_GREATER_500","HYDRANT_LESS_500","NONE","NURSE_OTHER_APPARATUS","SUPPLY_FROM_FIRE_BOAT","TANK_WATER","WATER_TENDER_SHUTTLE"],"title":"TypeWaterSupplyValue"},"TypeWindowPanesValue":{"type":"string","enum":["DOUBLE_PANE","NO_WINDOWS","SINGLE_PANE","TRIPLE_PANE","UNKNOWN"],"title":"TypeWindowPanesValue"},"TypeYesNoUnknownValue":{"type":"string","enum":["NO","UNKNOWN","YES"],"title":"TypeYesNoUnknownValue"},"UnitCreatedModifiedResponse":{"properties":{"neris_id":{"type":"string","title":"Neris Id"},"version":{"type":"integer","title":"Version"},"valid_start":{"type":"string","format":"date-time","title":"Valid Start"}},"type":"object","required":["neris_id","version","valid_start"],"title":"UnitCreatedModifiedResponse"},"UnitResponse":{"properties":{"staffing":{"type":"integer","minimum":0.0,"title":"Staffing","description":"The minimum staffing required for the unit to be dispatched to an incident."},"dedicated_staffing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Dedicated Staffing","description":"Whether the unit has dedicated staffing."},"neris_id":{"type":"string","title":"Neris Id"},"version":{"type":"integer","title":"Version"},"type":{"type":"string","title":"Type"},"cad_designation_1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cad Designation 1"},"cad_designation_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cad Designation 2"}},"type":"object","required":["staffing","neris_id","version","type"],"title":"UnitResponse"},"UnitResponseResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"unit_neris_id":{"anyOf":[{"type":"string","pattern":"^FD\\d{8}S[A-Z\\d]{3}U[A-Z\\d]{3}$","examples":["FD24027334S001U003","FD24160543S010U001"]},{"type":"null"}],"title":"Unit Neris Id","description":"NERIS ID of the unit responding to the incident.","examples":["FD24027000S000U000"]},"reported_unit_id":{"anyOf":[{"type":"string","maxLength":255,"minLength":1},{"type":"null"}],"title":"Reported Unit Id","description":"Name or ID of the unit responding to the incident if not yet in Entity Spec (or cannot be furnished).","examples":["A702"]},"staffing":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Staffing","description":"On-scene staffing of unit.","examples":[4]},"unable_to_dispatch":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Unable To Dispatch","description":"Set to TRUE if unit is unable to dispatch."},"dispatch":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Dispatch","description":"Timestamp that the unit is dispatched."},"enroute_to_scene":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Enroute To Scene","description":"Timestamp that the unit goes enroute to scene of the incident."},"on_scene":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"On Scene","description":"Timestamp that the unit arrived on scene."},"canceled_enroute":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Canceled Enroute","description":"Timestamp that the unit was canceled prior to arrival."},"staging":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Staging","description":"Timestamp that the unit stages on scene."},"unit_clear":{"anyOf":[{"type":"string","format":"date-time","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},{"type":"null"}],"title":"Unit Clear","description":"Timestamp that the unit clears the incident."},"response_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Response Mode","description":"Response mode of unit as part of the unit's response to an incident."},"transport_mode":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Transport Mode","description":"Transport mode of unit as part of the unit's response to a hospital."},"med_responses":{"anyOf":[{"items":{"$ref":"#/components/schemas/MedResponseResponse"},"type":"array"},{"type":"null"}],"title":"Med Responses"},"point":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"$ref":"#/components/schemas/GeoMultipolygon"},{"type":"string","description":"The pygeoapi URL of the geometry if geo_format is url."}],"description":"The geometry data from pygeoapi."},{"type":"null"}],"title":"Point"}},"type":"object","required":["last_modified"],"title":"UnitResponseResponse"},"UnsetAction":{"properties":{"action":{"type":"string","const":"unset","title":"Action","description":"Remove (nullify) the value of an attribute"}},"additionalProperties":false,"type":"object","required":["action"],"title":"UnsetAction"},"UpdateEntityActivationPayload":{"properties":{"active":{"type":"boolean","title":"Active"}},"additionalProperties":false,"type":"object","required":["active"],"title":"UpdateEntityActivationPayload"},"UpdateIncidentStatusPayload":{"properties":{"status":{"$ref":"#/components/schemas/TypeIncidentStatusPayloadValue","description":"Status of the incident in the administrativie approval process."}},"additionalProperties":false,"type":"object","required":["status"],"title":"UpdateIncidentStatusPayload"},"UpdateUserPayload":{"properties":{"email":{"type":"string","format":"email","title":"Email","description":"The new user's email address"},"given_name":{"type":"string","title":"Given Name"},"family_name":{"type":"string","title":"Family Name"},"phone_number":{"anyOf":[{"type":"string","pattern":"^(\\+?[1|0])?( )*([ \\[\\(])?\\d{3}([\\)\\]\\. \\-])?( )*\\d{3}([\\. \\-])?( )*\\d{4}$","examples":["+15555555555","(555)555-55555","555.555.5555","(555) 555-5555","15555555555","[555]555-5555"]},{"type":"null"}],"title":"Phone Number"}},"additionalProperties":false,"type":"object","required":["email","given_name","family_name"],"title":"UpdateUserPayload"},"UserActivationPayload":{"properties":{"active":{"type":"boolean","title":"Active"}},"additionalProperties":false,"type":"object","required":["active"],"title":"UserActivationPayload"},"UserAllEntitiesPermissionResponse":{"properties":{"resources":{"additionalProperties":{"items":{"$ref":"#/components/schemas/TypeActionValue"},"type":"array","uniqueItems":true},"propertyNames":{"$ref":"#/components/schemas/TypeResourceValue"},"type":"object","title":"Resources"}},"additionalProperties":false,"type":"object","required":["resources"],"title":"UserAllEntitiesPermissionResponse"},"UserEntityPermissionsResponse":{"properties":{"resources":{"additionalProperties":{"items":{"$ref":"#/components/schemas/TypeActionValue"},"type":"array","uniqueItems":true},"propertyNames":{"$ref":"#/components/schemas/TypeResourceValue"},"type":"object","title":"Resources"},"active":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Active"},"member":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Member"}},"additionalProperties":false,"type":"object","required":["resources","active","member"],"title":"UserEntityPermissionsResponse"},"UserEntitySummaryInfoResponse":{"properties":{"active":{"type":"boolean","title":"Active"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"neris_id":{"type":"string","title":"Neris Id"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}]},"zip_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zip Code"}},"type":"object","required":["active","neris_id"],"title":"UserEntitySummaryInfoResponse"},"UserFeatureFlags":{"properties":{"ui_access":{"type":"boolean","title":"Ui Access"}},"additionalProperties":false,"type":"object","required":["ui_access"],"title":"UserFeatureFlags"},"UserInfoResponse":{"properties":{"sub":{"type":"string","format":"uuid","title":"Sub"},"email":{"type":"string","format":"email","title":"Email"},"active":{"type":"boolean","title":"Active"},"username":{"type":"string","format":"uuid","title":"Username"},"given_name":{"type":"string","title":"Given Name"},"family_name":{"type":"string","title":"Family Name"},"status":{"$ref":"#/components/schemas/UserStatus"},"email_verified":{"type":"boolean","title":"Email Verified"},"roles":{"anyOf":[{"items":{"$ref":"#/components/schemas/RoleWithEntitySetResponse"},"type":"array"},{"type":"null"}],"title":"Roles"}},"type":"object","required":["sub","email","active","username","given_name","family_name","status","email_verified"],"title":"UserInfoResponse"},"UserPermissionsResponse":{"properties":{"active":{"type":"boolean","title":"Active"},"entities":{"additionalProperties":{"anyOf":[{"$ref":"#/components/schemas/UserEntityPermissionsResponse"},{"$ref":"#/components/schemas/UserAllEntitiesPermissionResponse"}]},"type":"object","title":"Entities"}},"additionalProperties":false,"type":"object","required":["active","entities"],"title":"UserPermissionsResponse"},"UserRoleEntitySetAttachmentPathParameter":{"type":"string","enum":["user","role","entity_set"],"title":"UserRoleEntitySetAttachmentPathParameter"},"UserRoleEntitySetAttachmentResponse":{"properties":{"role":{"$ref":"#/components/schemas/RoleResponse"},"user":{"$ref":"#/components/schemas/neris__api__auth__models__UserResponse"},"entity_set":{"$ref":"#/components/schemas/EntitySetResponse"}},"additionalProperties":false,"type":"object","required":["role","user","entity_set"],"title":"UserRoleEntitySetAttachmentResponse"},"UserStatus":{"type":"string","enum":["UNCONFIRMED","CONFIRMED","ARCHIVED","COMPROMISED","UNKNOWN","RESET_REQUIRED","FORCE_CHANGE_PASSWORD","EXTERNAL_PROVIDER"],"title":"UserStatus"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VehicleAutomobilePayload":{"properties":{"identification_num":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Identification Num","description":"The vehicle identification number (VIN) of the vehicle."},"manufacture_year":{"anyOf":[{"type":"integer","exclusiveMaximum":3000.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Manufacture Year","description":"The year the vehicle was manufactured."},"state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"description":"The state the vehicle was registered in."},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"The model of the vehicle."},"damage":{"$ref":"#/components/schemas/TypeVehicleDamageValue","description":"The type of damage to the vehicle."},"powertrain":{"anyOf":[{"$ref":"#/components/schemas/TypePowertrainValue"},{"type":"null"}],"description":"The powertrain of the vehicle."},"type":{"type":"string","const":"AUTOMOBILE","title":"Type","description":"The type of vehicle (automobile)."},"auto_body_style":{"anyOf":[{"$ref":"#/components/schemas/TypeAutoBodyStyleValue"},{"type":"null"}],"description":"The style of body of the vehicle."},"make":{"anyOf":[{"$ref":"#/components/schemas/TypeAutoMakeValue"},{"type":"null"}],"description":"The make of the vehicle."}},"additionalProperties":false,"type":"object","required":["damage","type"],"title":"VehicleAutomobilePayload","description":"Automobile: type must be \"AUTOMOBILE\"; requires make and auto_body_style."},"VehicleAutomobileResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"identification_num":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Identification Num","description":"The vehicle identification number (VIN) of the vehicle."},"manufacture_year":{"anyOf":[{"type":"integer","exclusiveMaximum":3000.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Manufacture Year","description":"The year the vehicle was manufactured."},"state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"description":"The state the vehicle was registered in."},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"The model of the vehicle."},"damage":{"type":"string","title":"Damage","description":"The type of damage to the vehicle."},"powertrain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Powertrain","description":"The powertrain of the vehicle."},"type":{"type":"string","const":"AUTOMOBILE","title":"Type","description":"The type of vehicle."},"vehicle_info":{"anyOf":[{"$ref":"#/components/schemas/DecodeVinResultResponse"},{"type":"null"}]},"auto_body_style":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Auto Body Style","description":"The style of body of the vehicle."},"make":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Make","description":"The make of the vehicle."}},"type":"object","required":["last_modified","damage","type"],"title":"VehicleAutomobileResponse"},"VehicleOtherPayload":{"properties":{"identification_num":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Identification Num","description":"The vehicle identification number (VIN) of the vehicle."},"manufacture_year":{"anyOf":[{"type":"integer","exclusiveMaximum":3000.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Manufacture Year","description":"The year the vehicle was manufactured."},"state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"description":"The state the vehicle was registered in."},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"The model of the vehicle."},"damage":{"$ref":"#/components/schemas/TypeVehicleDamageValue","description":"The type of damage to the vehicle."},"powertrain":{"anyOf":[{"$ref":"#/components/schemas/TypePowertrainValue"},{"type":"null"}],"description":"The powertrain of the vehicle."},"type":{"type":"string","enum":["AERIAL_LIFT_PLATFORM","AERIAL_OTHER","AIRPLANE","ATV","BICYCLE","BOAT_SHIP","BUS","CARGO","CONSTRUCTION","FARM","FOOD_TRUCK","FREIGHT","HELICOPTER","JET_SKI","MILITARY_OTHER","MOTORCYCLE","MOTORIZED_HOME","OTHER","RAIL","SCOOTER_MOPED","SPORT_UTILITY","TOW_TRUCK","TRAILER","UTILITY_VEHICLE","VAN"],"title":"Type","description":"The type of vehicle (non-automobile, e.g. MOTORCYCLE, BUS)."}},"additionalProperties":false,"type":"object","required":["damage","type"],"title":"VehicleOtherPayload","description":"Non-automobile vehicle. type must not be AUTOMOBILE (e.g. MOTORCYCLE, BUS)."},"VehicleOtherResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"identification_num":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Identification Num","description":"The vehicle identification number (VIN) of the vehicle."},"manufacture_year":{"anyOf":[{"type":"integer","exclusiveMaximum":3000.0,"exclusiveMinimum":0.0},{"type":"null"}],"title":"Manufacture Year","description":"The year the vehicle was manufactured."},"state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"description":"The state the vehicle was registered in."},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model","description":"The model of the vehicle."},"damage":{"type":"string","title":"Damage","description":"The type of damage to the vehicle."},"powertrain":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Powertrain","description":"The powertrain of the vehicle."},"type":{"type":"string","enum":["AERIAL_LIFT_PLATFORM","AERIAL_OTHER","AIRPLANE","ATV","BICYCLE","BOAT_SHIP","BUS","CARGO","CONSTRUCTION","FARM","FOOD_TRUCK","FREIGHT","HELICOPTER","JET_SKI","MILITARY_OTHER","MOTORCYCLE","MOTORIZED_HOME","OTHER","RAIL","SCOOTER_MOPED","SPORT_UTILITY","TOW_TRUCK","TRAILER","UTILITY_VEHICLE","VAN"],"title":"Type","description":"The type of vehicle (non-automobile, e.g. MOTORCYCLE, BUS)."},"vehicle_info":{"anyOf":[{"$ref":"#/components/schemas/DecodeVinResultResponse"},{"type":"null"}]}},"type":"object","required":["last_modified","damage","type"],"title":"VehicleOtherResponse"},"WeatherResponse":{"properties":{"neris_uid":{"type":"integer","title":"Neris Uid","description":"The NERIS unique identifier for the item."},"last_modified":{"type":"string","format":"date-time","title":"Last Modified","description":"The date and time the top-level item was created or updated in the system.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"incident_neris_id":{"type":"string","pattern":"^FD\\d{8}\\|[\\w\\-\\:]+\\|\\d{10}$","title":"Incident Neris Id","description":"NERIS ID of the incident. This is comprised of the department NERIS ID concatenated with `dispatch.internal_id` and the epoch time of `dispatch.call_create` in the record as initially submitted. An incident's NERIS_ID is immutable, and so should not be relied upon for extraction of `internal_id` or `call_create`, as these attributes could have been updated.","examples":["FD12345678|abc123xyz|1729023498","FD24027334|incident42:f-tiw|1729023498","FD12345678|0457812893477|1729023498"]},"timestamp":{"type":"string","format":"date-time","title":"Timestamp","description":"An ISO 8601 formatted datetime string or Unix time (seconds since 1 January 1970) as a float, integer, or string.","examples":["2024-06-13T21:14:02.162497-07:00","1718338442.162497",1718338442.162497,1718338442]},"main":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Main","description":"Weather category"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"Weather conditions within the category"},"temp":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Temp","description":"Temperature (degrees Fahrenheit)"},"dew_point":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Dew Point","description":"Dew point temperature (degrees Fahrenheit)"},"feels_like":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Feels Like","description":"Perceived temperature (degress Fahrenheit)"},"pressure":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pressure","description":"Atmospheric pressure (millibars)"},"humidity":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Humidity","description":"Relative humidity (percent)"},"temp_min":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Temp Min","description":"Minimum temperature within a large city or megalopolis (degrees Fahrenheit)"},"temp_max":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Temp Max","description":"Maximum temperature within a large city or megalopolis (degrees Fahrenheit)"},"heat_index":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Heat Index","description":"Heat index in degrees Fahrenheit"},"windchill":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Windchill","description":"Windchill in degrees Fahrenheit"},"visibility":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Visibility","description":"Visibility distance (feet)"},"wind_speed":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Wind Speed","description":"Wind speed (miles per hour)"},"wind_deg":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Wind Deg","description":"Wind direction (degrees clockwise from North)"},"cloud_percent":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cloud Percent","description":"Amount of cloud cover (percent)"},"rain_accum_1h":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rain Accum 1H","description":"Rain accumulation over the previous hour (inches)"},"snow_vol_1h":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Snow Vol 1H","description":"Snow accumulation over the previous hour (inches)"},"day_part":{"anyOf":[{"type":"string","enum":["day","night"]},{"type":"null"}],"title":"Day Part","description":"Whether it was day or night at the incident time and location"},"air_quality_co":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Air Quality Co","description":"Carbon monoxide (micrograms per cubic meter)"},"air_quality_o3":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Air Quality O3","description":"Ozone (micrograms per cubic meter)"},"air_quality_no2":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Air Quality No2","description":"Nitrogen dioxide (micrograms per cubic meter)"},"air_quality_so2":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Air Quality So2","description":"Sulphur dioxide (micrograms per cubic meter)"},"air_quality_pm2_5":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Air Quality Pm2 5","description":"PM2.5 (micrograms per cubic meter)"},"air_quality_pm10":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Air Quality Pm10","description":"PM10 (micrograms per cubic meter)"},"air_quality_us_epa_index":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Air Quality Us Epa Index","description":"US EPA AQI category index"},"air_quality_gb_defra_index":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Air Quality Gb Defra Index","description":"UK DEFRA AQI category index"},"pollen_hazel":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pollen Hazel","description":"Hazel pollen grains per cubic meter of air"},"pollen_alder":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pollen Alder","description":"Alder pollen grains per cubic meter of air"},"pollen_birch":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pollen Birch","description":"Birch pollen grains per cubic meter of air"},"pollen_oak":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pollen Oak","description":"Oak pollen grains per cubic meter of air"},"pollen_grass":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pollen Grass","description":"Grass pollen grains per cubic meter of air"},"pollen_mugwort":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pollen Mugwort","description":"Mugwort pollen grains per cubic meter of air"},"pollen_ragweed":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pollen Ragweed","description":"Ragweed pollen grains per cubic meter of air"}},"type":"object","required":["last_modified","incident_neris_id","timestamp"],"title":"WeatherResponse"},"YesOrNo":{"type":"string","enum":["Y","N"],"title":"YesOrNo"},"_PatchActionActionProperties":{"properties":{"actions":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetRelListTypeActionTacticValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetRelListTypeActionTacticValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Actions"}},"additionalProperties":false,"type":"object","title":"_PatchActionActionProperties"},"_PatchActionTacticActionProperties":{"properties":{"action_noaction":{"oneOf":[{"$ref":"#/components/schemas/PatchActionAction"},{"$ref":"#/components/schemas/PatchNoactionAction"},{"type":"null"}],"title":"Action Noaction"}},"additionalProperties":false,"type":"object","title":"_PatchActionTacticActionProperties"},"_PatchAidActionProperties":{"properties":{"department_neris_id":{"anyOf":[{"$ref":"#/components/schemas/SetNerisIdDeptStrAction"},{"type":"null"}]},"aid_type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeAidValueAction"},{"type":"null"}]},"aid_direction":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeAidDirectionValueAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchAidActionProperties"},"_PatchCasualtyActionProperties":{"properties":{"injury_or_noninjury":{"anyOf":[{"$ref":"#/components/schemas/PatchInjuryAction"},{"$ref":"#/components/schemas/PatchNoinjuryAction"}],"title":"Injury Or Noninjury"}},"additionalProperties":false,"type":"object","required":["injury_or_noninjury"],"title":"_PatchCasualtyActionProperties"},"_PatchCasualtyRescueActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeFfNonffAction"},{"type":"null"}]},"casualty":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchCasualtyAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchCasualtyAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Casualty"},"rescue":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchRescueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchRescueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Rescue"},"birth_month_year":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Birth Month Year","description":"\n\n
Validation Rules\n\n- `birth_month_year` must be in `MM/YYYY` format.\n\n
"},"gender":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeGenderValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeGenderValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Gender"},"race":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeRaceValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeRaceValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Race"},"rank":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Rank"},"years_of_service":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetFloatAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetFloatAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Years Of Service"}},"additionalProperties":false,"type":"object","title":"_PatchCasualtyRescueActionProperties","description":"\n\n
Validation Rules\n\n- `rescue.presence_known` only possible if `type` is `NONFF`.\n- `mayday` only possible if `type` is `FF`.\n- `casualty.injury_or_noninjury.ff_injury_details` only possible if `type` is `FF`.\n- `rank` and `years_of_service` only possible if `type` is `FF`.\n\n
"},"_PatchChemicalActionProperties":{"properties":{"dot_class":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeHazardDotValueAction"},{"type":"null"}]},"name":{"anyOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"type":"null"}]},"release_occurred":{"anyOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"type":"null"}]},"release":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchReleaseAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchReleaseAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Release"}},"additionalProperties":false,"type":"object","title":"_PatchChemicalActionProperties","description":"\n\n
Validation Rules\n\n- `release` only possible if `release_occurred` is `true`.\n\n
"},"_PatchCommentActionProperties":{"properties":{"comment":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetNarrativeStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetNarrativeStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Comment"},"timestamp":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Timestamp"}},"additionalProperties":false,"type":"object","title":"_PatchCommentActionProperties"},"_PatchCookingFireSuppressionActionProperties":{"properties":{"presence":{"anyOf":[{"$ref":"#/components/schemas/PatchCookingFireSuppressionPresentAction"},{"$ref":"#/components/schemas/PatchCookingFireSuppressionNotPresentAction"},{"type":"null"}],"title":"Presence"}},"additionalProperties":false,"type":"object","title":"_PatchCookingFireSuppressionActionProperties"},"_PatchCookingFireSuppressionNotPresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetNotpresentAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchCookingFireSuppressionNotPresentActionProperties"},"_PatchCookingFireSuppressionPresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetPresentAction"},{"type":"null"}]},"suppression_types":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetRelListTypeSuppressCookingValue"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetRelListTypeSuppressCookingValue","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Suppression Types"},"operation_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeSuppressOperationValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeSuppressOperationValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Operation Type"}},"additionalProperties":false,"type":"object","title":"_PatchCookingFireSuppressionPresentActionProperties"},"_PatchCrossStreetActionProperties":{"properties":{"number_prefix":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Number Prefix"},"number":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Number"},"number_suffix":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Number Suffix"},"complete_number":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Complete Number"},"distance_marker":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Distance Marker"},"street_prefix_modifier":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street Prefix Modifier"},"street_prefix_direction":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street Prefix Direction"},"street_prefix":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeLocSnPrePostValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Street Prefix"},"street_preposition_type_separator":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeLocSnPreSepValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Street Preposition Type Separator"},"street":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street"},"street_postfix":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeLocSnPrePostValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Street Postfix"},"street_postfix_direction":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street Postfix Direction"},"street_postfix_modifier":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street Postfix Modifier"},"direction_of_travel":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeLocSnDirectionValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Direction Of Travel"},"cross_street_modifier":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeLocationCrossStreetValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeLocationCrossStreetValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Cross Street Modifier"}},"additionalProperties":false,"type":"object","title":"_PatchCrossStreetActionProperties"},"_PatchCsstHazardActionProperties":{"properties":{"ignition_source":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Ignition Source"},"lightning_suspected":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeYesNoUnknownValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeYesNoUnknownValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Lightning Suspected"},"grounded":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeYesNoUnknownValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeYesNoUnknownValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Grounded"}},"additionalProperties":false,"type":"object","title":"_PatchCsstHazardActionProperties"},"_PatchDispatchActionProperties":{"properties":{"location":{"anyOf":[{"$ref":"#/components/schemas/PatchLocationAction"},{"type":"null"}]},"point":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetGeoPointAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetGeoPointAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Point"},"comments":{"anyOf":[{"items":{"anyOf":[{"$ref":"#/components/schemas/AppendCommentAction"},{"$ref":"#/components/schemas/RemoveCommentAction"},{"$ref":"#/components/schemas/PatchCommentAction"}]},"type":"array"},{"type":"null"}],"title":"Comments","description":"List of actions to take"},"unit_responses":{"anyOf":[{"items":{"anyOf":[{"$ref":"#/components/schemas/AppendDispatchUnitResponseAction"},{"$ref":"#/components/schemas/RemoveDispatchUnitResponseAction"},{"$ref":"#/components/schemas/PatchDispatchUnitResponseAction"}]},"type":"array"},{"type":"null"}],"title":"Unit Responses","description":"List of actions to take"},"tactic_timestamps":{"anyOf":[{"$ref":"#/components/schemas/PatchDispatchTacticTimestampsAction"},{"type":"null"}]},"center_id":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Center Id","description":"\n\n
Validation Rules\n\n- `center_id` must be exactly four digits.\n\n
"},"incident_number":{"anyOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"type":"null"}]},"determinant_code":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDeterminantCodeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDeterminantCodeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Determinant Code"},"incident_code":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Incident Code"},"disposition":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Disposition"},"automatic_alarm":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Automatic Alarm"},"call_arrival":{"anyOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"type":"null"}]},"call_answered":{"anyOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"type":"null"}]},"call_create":{"anyOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"type":"null"}]},"incident_clear":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Incident Clear"}},"additionalProperties":false,"type":"object","title":"_PatchDispatchActionProperties","description":"\n\n
Validation Rules\n\n- `call_arrival` must not be after `call_answered`.\n- `call_arrival` must not be after `call_create`.\n- `call_answered` must not be after `call_create`.\n\n
"},"_PatchDispatchUnitResponseActionProperties":{"properties":{"med_responses":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendMedResponseAction"},{"$ref":"#/components/schemas/RemoveMedResponseAction"},{"$ref":"#/components/schemas/PatchMedResponseAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendMedResponseAction","patch":"#/components/schemas/PatchMedResponseAction","remove":"#/components/schemas/RemoveMedResponseAction"}}},"type":"array"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Med Responses"},"point":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetGeoPointAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetGeoPointAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Point"},"unit_neris_id":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetNerisIdUnitStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetNerisIdUnitStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unit Neris Id"},"reported_unit_id":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Reported Unit Id"},"staffing":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Staffing"},"dispatch":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Dispatch"},"enroute_to_scene":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Enroute To Scene"},"on_scene":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"On Scene"},"canceled_enroute":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Canceled Enroute"},"staging":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Staging"},"unit_clear":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unit Clear"},"unable_to_dispatch":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unable To Dispatch"},"response_mode__unit":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeResponseMode_UnitValueAction"},{"type":"null"}]},"response_mode__transport":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeResponseMode_TransportValueAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchDispatchUnitResponseActionProperties","description":"\n\n
Validation Rules\n\n- `neris_id_unit`, `reported_id_unit`, or both must be provided.\n\n
"},"_PatchElectricHazardActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeEmerghazElecValueAction"},{"type":"null"}]},"source_or_target":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeSourceTargetValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeSourceTargetValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Source Or Target"},"involved_in_crash":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Involved In Crash"},"fire_details":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchElectricHazardFireAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchElectricHazardFireAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Fire Details"}},"additionalProperties":false,"type":"object","title":"_PatchElectricHazardActionProperties","description":"\n\n
Validation Rules\n\n- `involved_in_crash` only applicable to `ELECTRIC_VEHICLE` types.\n\n
"},"_PatchElectricHazardFireActionProperties":{"properties":{"reignition":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Reignition"},"suppression_types":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeEmerghazSuppressionValueAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchElectricHazardFireActionProperties"},"_PatchExposureActionProperties":{"properties":{"location_detail":{"anyOf":[{"$ref":"#/components/schemas/PatchExternalExposureAction"},{"$ref":"#/components/schemas/PatchInternalExposureAction"},{"type":"null"}],"title":"Location Detail"},"location":{"anyOf":[{"$ref":"#/components/schemas/PatchLocationAction"},{"type":"null"}]},"location_use":{"anyOf":[{"$ref":"#/components/schemas/PatchLocationUseAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Location Use"},"point":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetGeoPointAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetGeoPointAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Point"},"polygon":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetGeoMultipolygonAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetGeoMultipolygonAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Polygon"},"displacement_count":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Displacement Count"},"displacement_causes":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeDisplaceCauseValueRelExposureAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Displacement Causes"},"people_present":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"People Present"},"damage_type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeExposureDamageValueAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchExposureActionProperties"},"_PatchExternalExposureActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeExposureLocAction"},{"type":"null"}]},"item_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeExposureItemValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeExposureItemValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Item Type"}},"additionalProperties":false,"type":"object","title":"_PatchExternalExposureActionProperties"},"_PatchFfInjuryDetailsActionProperties":{"properties":{"job_classification":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeJobClassificationValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Job Classification"},"unit_neris_id":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetNerisIdUnitStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetNerisIdUnitStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unit Neris Id"},"reported_unit_id":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Reported Unit Id"},"unit_continuity":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unit Continuity"},"duty_type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeDutyValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Duty Type"},"action_type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeCasualtyActionValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Action Type"},"incident_command":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Incident Command","default":[null]},"incident_stage":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeCasualtyTimelineValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Incident Stage"},"injury_ff_ppe":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeCasualtyPpeValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Injury Ff Ppe"}},"additionalProperties":false,"type":"object","title":"_PatchFfInjuryDetailsActionProperties","description":"\n\n
Validation Rules\n\n- If `NONE` is included in `ppe_items` it must be the only item in the array.\n\n
"},"_PatchFfRescueActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeRescueFfrescueAction"},{"type":"null"}]},"removal_or_nonremoval":{"anyOf":[{"$ref":"#/components/schemas/PatchRemovalAction"},{"type":"null"}]},"rescue_mode":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeRescueModeValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeRescueModeValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Rescue Mode"},"actions":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeRescueActionValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Actions"},"impediments":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeRescueImpedimentValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Impediments"}},"additionalProperties":false,"type":"object","title":"_PatchFfRescueActionProperties","description":"\n\n
Validation Rules\n\n- If `NONE` is included in `actions` it must be the only item in the array.\n- If `NONE` is included in `impediments` it must be the only item in the array.\n\n
"},"_PatchFireActionProperties":{"properties":{"location_detail":{"oneOf":[{"$ref":"#/components/schemas/PatchStructureFireLocationDetailAction"},{"$ref":"#/components/schemas/PatchOutsideFireLocationDetailAction"},{"type":"null"}],"title":"Location Detail"},"water_supply":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeWaterSupplyValueAction"},{"type":"null"}]},"investigation_needed":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeFireInvestNeedValueAction"},{"type":"null"}]},"investigation_types":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeFireInvestValueAction"},{"type":"null"}]},"suppression_appliances":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeSuppressApplianceValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Suppression Appliances"}},"additionalProperties":false,"type":"object","title":"_PatchFireActionProperties","description":"\n\n
Validation Rules\n\n- If `NONE` is included in `suppression_appliances` it must be the only item in the array.\n- If `NONE` is included in `investigation_types` it must be the only item in the array.\n\n
"},"_PatchFireAlarmActionProperties":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/PatchFireAlarmPresentAction"},{"$ref":"#/components/schemas/PatchFireAlarmNotpresentAction"},{"type":"null"}],"title":"Presence"}},"additionalProperties":false,"type":"object","title":"_PatchFireAlarmActionProperties"},"_PatchFireAlarmNotpresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetNotpresentAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchFireAlarmNotpresentActionProperties"},"_PatchFireAlarmPresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetPresentAction"},{"type":"null"}]},"alarm_types":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeAlarmFireValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Alarm Types"},"operation_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeAlarmOperationValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeAlarmOperationValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Operation Type"}},"additionalProperties":false,"type":"object","title":"_PatchFireAlarmPresentActionProperties"},"_PatchFireRemovalActionProperties":{"properties":{"relative_suppression_time":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeSuppressTimeValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeSuppressTimeValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Relative Suppression Time"}},"additionalProperties":false,"type":"object","title":"_PatchFireRemovalActionProperties"},"_PatchFireSuppressionActionProperties":{"properties":{"presence":{"anyOf":[{"$ref":"#/components/schemas/PatchFireSuppressionPresentAction"},{"$ref":"#/components/schemas/PatchFireSuppressionNotPresentAction"},{"type":"null"}],"title":"Presence"}},"additionalProperties":false,"type":"object","title":"_PatchFireSuppressionActionProperties"},"_PatchFireSuppressionEffectiveActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeSuppressOperationEffectiveAction"},{"type":"null"}]},"sprinklers_activated":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Sprinklers Activated"}},"additionalProperties":false,"type":"object","title":"_PatchFireSuppressionEffectiveActionProperties"},"_PatchFireSuppressionFailedActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeSuppressOperationFailedAction"},{"type":"null"}]},"failure_reason":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeSuppressNoOperationValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeSuppressNoOperationValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Failure Reason"}},"additionalProperties":false,"type":"object","title":"_PatchFireSuppressionFailedActionProperties"},"_PatchFireSuppressionIneffectiveActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeSuppressOperationIneffectiveAction"},{"type":"null"}]},"failure_reason":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeSuppressNoOperationValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeSuppressNoOperationValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Failure Reason"},"sprinklers_activated":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Sprinklers Activated"}},"additionalProperties":false,"type":"object","title":"_PatchFireSuppressionIneffectiveActionProperties"},"_PatchFireSuppressionNotPresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetNotpresentAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchFireSuppressionNotPresentActionProperties"},"_PatchFireSuppressionOperationActionProperties":{"properties":{"effectiveness":{"anyOf":[{"$ref":"#/components/schemas/PatchFireSuppressionEffectiveAction"},{"$ref":"#/components/schemas/PatchFireSuppressionIneffectiveAction"},{"$ref":"#/components/schemas/PatchFireSuppressionFailedAction"},{"type":"null"}],"title":"Effectiveness"}},"additionalProperties":false,"type":"object","title":"_PatchFireSuppressionOperationActionProperties"},"_PatchFireSuppressionPresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetPresentAction"},{"type":"null"}]},"suppression_types":{"anyOf":[{"$ref":"#/components/schemas/PatchFireSuppressionTypeAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Suppression Types"},"operation_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchFireSuppressionOperationAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchFireSuppressionOperationAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Operation Type"}},"additionalProperties":false,"type":"object","title":"_PatchFireSuppressionPresentActionProperties"},"_PatchFireSuppressionTypeActionProperties":{"properties":{"fire_suppression_type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeSuppressFireValueAction"},{"type":"null"}]},"full_partial":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeFullPartialValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeFullPartialValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Full Partial"}},"additionalProperties":false,"type":"object","title":"_PatchFireSuppressionTypeActionProperties"},"_PatchHazsitActionProperties":{"properties":{"disposition":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeHazardDispositionValueAction"},{"type":"null"}]},"evacuated":{"anyOf":[{"$ref":"#/components/schemas/SetIntAction"},{"type":"null"}]},"chemicals":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendChemicalAction"},{"$ref":"#/components/schemas/RemoveChemicalAction"},{"$ref":"#/components/schemas/PatchChemicalAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendChemicalAction","patch":"#/components/schemas/PatchChemicalAction","remove":"#/components/schemas/RemoveChemicalAction"}}},"type":"array"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Chemicals"}},"additionalProperties":false,"type":"object","title":"_PatchHazsitActionProperties"},"_PatchInUseActionProperties":{"properties":{"in_use":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"In Use"},"intended":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Intended"}},"additionalProperties":false,"type":"object","title":"_PatchInUseActionProperties","description":"\n\n
Validation Rules\n\n- `intended` can only be set if `in_use` is `true`.\n\n
"},"_PatchIncidentActionProperties":{"properties":{"base":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchIncidentBaseAction"},{"$ref":"#/components/schemas/SetIncidentBaseAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchIncidentBaseAction","set":"#/components/schemas/SetIncidentBaseAction"}}},{"type":"null"}],"title":"Base"},"incident_types":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendIncidentTypeAction"},{"$ref":"#/components/schemas/RemoveIncidentTypeAction"},{"$ref":"#/components/schemas/PatchIncidentTypeAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendIncidentTypeAction","patch":"#/components/schemas/PatchIncidentTypeAction","remove":"#/components/schemas/RemoveIncidentTypeAction"}}},"type":"array"},{"type":"null"}],"title":"Incident Types","description":"List of actions to take"},"special_modifiers":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetRelListTypeSpecialModifierValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetRelListTypeSpecialModifierValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Special Modifiers"},"aids":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendAidAction"},{"$ref":"#/components/schemas/RemoveAidAction"},{"$ref":"#/components/schemas/PatchAidAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendAidAction","patch":"#/components/schemas/PatchAidAction","remove":"#/components/schemas/RemoveAidAction"}}},"type":"array"},{"type":"null"}],"title":"Aids","description":"List of actions to take"},"nonfd_aids":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetRelListTypeAidNonfdValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetRelListTypeAidNonfdValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Nonfd Aids"},"actions_tactics":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchActionTacticAction"},{"$ref":"#/components/schemas/SetActionTacticAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchActionTacticAction","set":"#/components/schemas/SetActionTacticAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Actions Tactics"},"dispatch":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchDispatchAction"},{"$ref":"#/components/schemas/SetDispatchAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchDispatchAction","set":"#/components/schemas/SetDispatchAction"}}},{"type":"null"}],"title":"Dispatch"},"tactic_timestamps":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchIncidentTacticTimestampsAction"},{"$ref":"#/components/schemas/SetIncidentTacticTimestampsAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchIncidentTacticTimestampsAction","set":"#/components/schemas/SetIncidentTacticTimestampsAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Tactic Timestamps"},"unit_responses":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendIncidentUnitResponseAction"},{"$ref":"#/components/schemas/RemoveIncidentUnitResponseAction"},{"$ref":"#/components/schemas/PatchIncidentUnitResponseAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendIncidentUnitResponseAction","patch":"#/components/schemas/PatchIncidentUnitResponseAction","remove":"#/components/schemas/RemoveIncidentUnitResponseAction"}}},"type":"array"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Unit Responses","description":"List of actions to take"},"exposures":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendExposureAction"},{"$ref":"#/components/schemas/RemoveExposureAction"},{"$ref":"#/components/schemas/PatchExposureAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendExposureAction","patch":"#/components/schemas/PatchExposureAction","remove":"#/components/schemas/RemoveExposureAction"}}},"type":"array"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Exposures","description":"List of actions to take"},"casualty_rescues":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendCasualtyRescueAction"},{"$ref":"#/components/schemas/RemoveCasualtyRescueAction"},{"$ref":"#/components/schemas/PatchCasualtyRescueAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendCasualtyRescueAction","patch":"#/components/schemas/PatchCasualtyRescueAction","remove":"#/components/schemas/RemoveCasualtyRescueAction"}}},"type":"array"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Casualty Rescues","description":"List of actions to take"},"fire_detail":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchFireAction"},{"$ref":"#/components/schemas/SetFireAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchFireAction","set":"#/components/schemas/SetFireAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Fire Detail"},"hazsit_detail":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchHazsitAction"},{"$ref":"#/components/schemas/SetHazsitAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchHazsitAction","set":"#/components/schemas/SetHazsitAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Hazsit Detail"},"medical_details":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendMedicalAction"},{"$ref":"#/components/schemas/RemoveMedicalAction"},{"$ref":"#/components/schemas/PatchMedicalAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendMedicalAction","patch":"#/components/schemas/PatchMedicalAction","remove":"#/components/schemas/RemoveMedicalAction"}}},"type":"array"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Medical Details","description":"List of actions to perform"},"smoke_alarm":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchSmokeAlarmAction"},{"$ref":"#/components/schemas/SetSmokeAlarmAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchSmokeAlarmAction","set":"#/components/schemas/SetSmokeAlarmAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Smoke Alarm"},"fire_alarm":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchFireAlarmAction"},{"$ref":"#/components/schemas/SetFireAlarmAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchFireAlarmAction","set":"#/components/schemas/SetFireAlarmAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Fire Alarm"},"other_alarm":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchOtherAlarmAction"},{"$ref":"#/components/schemas/SetOtherAlarmAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchOtherAlarmAction","set":"#/components/schemas/SetOtherAlarmAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Other Alarm"},"fire_suppression":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchFireSuppressionAction"},{"$ref":"#/components/schemas/SetFireSuppressionAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchFireSuppressionAction","set":"#/components/schemas/SetFireSuppressionAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Fire Suppression"},"cooking_fire_suppression":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchCookingFireSuppressionAction"},{"$ref":"#/components/schemas/SetCookingFireSuppressionAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchCookingFireSuppressionAction","set":"#/components/schemas/SetCookingFireSuppressionAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Cooking Fire Suppression"},"electric_hazards":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendElectricHazardAction"},{"$ref":"#/components/schemas/RemoveElectricHazardAction"},{"$ref":"#/components/schemas/PatchElectricHazardAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendElectricHazardAction","patch":"#/components/schemas/PatchElectricHazardAction","remove":"#/components/schemas/RemoveElectricHazardAction"}}},"type":"array"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Electric Hazards","description":"List of actions to take, or remove electric hazards completely"},"powergen_hazards":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendPvPowergenHazardAction"},{"$ref":"#/components/schemas/AppendOtherPowergenHazardAction"},{"$ref":"#/components/schemas/RemovePowergenHazardAction"},{"$ref":"#/components/schemas/PatchPvPowergenHazardAction"},{"$ref":"#/components/schemas/PatchOtherPowergenHazardAction"}]},"type":"array"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Powergen Hazards"},"csst_hazard":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchCsstHazardAction"},{"$ref":"#/components/schemas/SetCsstHazardAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchCsstHazardAction","set":"#/components/schemas/SetCsstHazardAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Csst Hazard"},"medical_oxygen_hazard":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchMedicalOxygenHazardAction"},{"$ref":"#/components/schemas/SetMedicalOxygenHazardAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchMedicalOxygenHazardAction","set":"#/components/schemas/SetMedicalOxygenHazardAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Medical Oxygen Hazard"}},"additionalProperties":false,"type":"object","title":"_PatchIncidentActionProperties"},"_PatchIncidentBaseActionProperties":{"properties":{"people_present":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"People Present"},"animals_rescued":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Animals Rescued"},"impediment_narrative":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetNarrativeStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetNarrativeStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Impediment Narrative"},"outcome_narrative":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetNarrativeStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetNarrativeStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Outcome Narrative"},"displacement_count":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Displacement Count"},"displacement_causes":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeDisplaceCauseValueRelIncidentAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Displacement Causes"},"point":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetGeoPointAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetGeoPointAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Point"},"polygon":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetGeoMultipolygonAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetGeoMultipolygonAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Polygon"},"location":{"anyOf":[{"$ref":"#/components/schemas/PatchLocationAction"},{"type":"null"}]},"location_use":{"anyOf":[{"$ref":"#/components/schemas/PatchLocationUseAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchIncidentBaseActionProperties"},"_PatchIncidentTypeActionProperties":{"properties":{"incident_type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeIncidentValueAction"},{"type":"null"}]},"primary_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Primary Type"}},"additionalProperties":false,"type":"object","title":"_PatchIncidentTypeActionProperties"},"_PatchIncidentUnitResponseActionProperties":{"properties":{"med_responses":{"anyOf":[{"items":{"oneOf":[{"$ref":"#/components/schemas/AppendMedResponseAction"},{"$ref":"#/components/schemas/RemoveMedResponseAction"},{"$ref":"#/components/schemas/PatchMedResponseAction"}],"discriminator":{"propertyName":"action","mapping":{"append":"#/components/schemas/AppendMedResponseAction","patch":"#/components/schemas/PatchMedResponseAction","remove":"#/components/schemas/RemoveMedResponseAction"}}},"type":"array"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Med Responses"},"point":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetGeoPointAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetGeoPointAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Point"},"unit_neris_id":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetNerisIdUnitStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetNerisIdUnitStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unit Neris Id"},"reported_unit_id":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Reported Unit Id"},"staffing":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Staffing"},"dispatch":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Dispatch"},"enroute_to_scene":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Enroute To Scene"},"on_scene":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"On Scene"},"canceled_enroute":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Canceled Enroute"},"staging":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Staging"},"unit_clear":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unit Clear"},"unable_to_dispatch":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unable To Dispatch"},"response_mode__unit":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeResponseMode_UnitValueAction"},{"type":"null"}]},"response_mode__transport":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeResponseMode_TransportValueAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchIncidentUnitResponseActionProperties","description":"\n\n
Validation Rules\n\n- `neris_id_unit`, `reported_id_unit`, or both must be provided.\n\n
"},"_PatchInjuryActionProperties":{"properties":{"type":{"$ref":"#/components/schemas/_SetTypeCasualtyAction"},"cause":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeCasualtyCauseValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeCasualtyCauseValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Cause"},"ff_injury_details":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchFfInjuryDetailsAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchFfInjuryDetailsAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Ff Injury Details"}},"additionalProperties":false,"type":"object","required":["type"],"title":"_PatchInjuryActionProperties"},"_PatchInternalExposureActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeInternalExposureLocAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchInternalExposureActionProperties"},"_PatchLocationActionProperties":{"properties":{"additional_attributes":{"anyOf":[{"$ref":"#/components/schemas/SetDictAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Additional Attributes"},"place_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeLocPlaceValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeLocPlaceValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Place Type"},"postal_community":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Postal Community"},"neighborhood_community":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Neighborhood Community"},"unincorporated_community":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unincorporated Community"},"incorporated_municipality":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Incorporated Municipality"},"county":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"County"},"state":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"State"},"postal_code":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Postal Code"},"postal_code_extension":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Postal Code Extension"},"country":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeLocCspCountryValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Country"},"street_prefix_modifier":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street Prefix Modifier"},"street_prefix_direction":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street Prefix Direction"},"street_prefix":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeLocSnPrePostValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Street Prefix"},"street_preposition_type_separator":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeLocSnPreSepValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Street Preposition Type Separator"},"street":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street"},"street_postfix":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeLocSnPrePostValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Street Postfix"},"street_postfix_direction":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street Postfix Direction"},"street_postfix_modifier":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Street Postfix Modifier"},"direction_of_travel":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeLocSnDirectionValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Direction Of Travel"},"number_prefix":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Number Prefix"},"number":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Number"},"number_suffix":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Number Suffix"},"complete_number":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Complete Number"},"distance_marker":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Distance Marker"},"structure":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Structure"},"subsite":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Subsite"},"site":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Site"},"wing":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Wing"},"floor":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Floor"},"unit_prefix":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unit Prefix"},"unit_value":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unit Value"},"room":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Room"},"section":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Section"},"row":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Row"},"seat":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Seat"},"additional_info":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Additional Info"},"marker":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Marker"},"cross_streets":{"anyOf":[{"items":{"anyOf":[{"$ref":"#/components/schemas/PatchCrossStreetAction"},{"$ref":"#/components/schemas/AppendCrossStreetAction"},{"$ref":"#/components/schemas/RemoveCrossStreetAction"}]},"type":"array"},{"type":"null"}],"title":"Cross Streets","description":"List of actions to take"},"location_aliases":{"anyOf":[{"items":{"anyOf":[{"$ref":"#/components/schemas/AppendLocationAction"},{"$ref":"#/components/schemas/RemoveLocationAction"}]},"type":"array"},{"type":"null"}],"title":"Location Aliases","description":"List of actions to take"}},"additionalProperties":false,"type":"object","title":"_PatchLocationActionProperties"},"_PatchLocationUseActionProperties":{"properties":{"use_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeLocationUseValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeLocationUseValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Use Type"},"vacancy_cause":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeVacancyValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeVacancyValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Vacancy Cause"},"secondary_use":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeLocationUseValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeLocationUseValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Secondary Use"},"in_use":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchInUseAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchInUseAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"In Use"}},"additionalProperties":false,"type":"object","title":"_PatchLocationUseActionProperties"},"_PatchMedResponseActionProperties":{"properties":{"at_patient":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"At Patient"},"enroute_to_hospital":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Enroute To Hospital"},"arrived_at_hospital":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Arrived At Hospital"},"transferred_to_agency":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Transferred To Agency"},"transferred_to_facility":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Transferred To Facility"},"hospital_cleared":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Hospital Cleared"},"hospital_destination":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Hospital Destination"}},"additionalProperties":false,"type":"object","title":"_PatchMedResponseActionProperties"},"_PatchMedicalActionProperties":{"properties":{"patient_care_report_id":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetSimpleStrAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetSimpleStrAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Patient Care Report Id"},"patient_care_evaluation":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeMedicalPatientCareValueAction"},{"type":"null"}]},"patient_status":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeMedicalPatientStatusValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeMedicalPatientStatusValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Patient Status"},"transport_disposition":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeMedicalTransportValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeMedicalTransportValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Transport Disposition"}},"additionalProperties":false,"type":"object","title":"_PatchMedicalActionProperties"},"_PatchMedicalOxygenHazardPresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetPresentAction"},{"type":"null"}]},"contributed_to_flame_spread":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Contributed To Flame Spread"}},"additionalProperties":false,"type":"object","title":"_PatchMedicalOxygenHazardPresentActionProperties"},"_PatchNoactionActionProperties":{"properties":{"noaction_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeNoactionValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeNoactionValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Noaction Type"}},"additionalProperties":false,"type":"object","title":"_PatchNoactionActionProperties"},"_PatchNoinjuryActionProperties":{"properties":{"type":{"$ref":"#/components/schemas/_SetTypeCasualtyNoinjuryAction"}},"additionalProperties":false,"type":"object","required":["type"],"title":"_PatchNoinjuryActionProperties"},"_PatchNonFfRescueActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeRescueNonffrescueAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchNonFfRescueActionProperties"},"_PatchOtherAlarmActionProperties":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/PatchOtherAlarmPresentAction"},{"$ref":"#/components/schemas/PatchOtherAlarmNotpresentAction"},{"type":"null"}],"title":"Presence"}},"additionalProperties":false,"type":"object","title":"_PatchOtherAlarmActionProperties"},"_PatchOtherAlarmNotpresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetNotpresentAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchOtherAlarmNotpresentActionProperties"},"_PatchOtherAlarmPresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetPresentAction"},{"type":"null"}]},"alarm_types":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeAlarmOtherValueAction"},{"$ref":"#/components/schemas/UnsetAction"},{"type":"null"}],"title":"Alarm Types"}},"additionalProperties":false,"type":"object","title":"_PatchOtherAlarmPresentActionProperties"},"_PatchOtherPowergenHazardActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeEmerghazOtherPowergenAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchOtherPowergenHazardActionProperties"},"_PatchOutsideFireLocationDetailActionProperties":{"properties":{"cause":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeFireCauseOutValueAction"},{"type":"null"}]},"acres_burned":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetFloatAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetFloatAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Acres Burned"}},"additionalProperties":false,"type":"object","title":"_PatchOutsideFireLocationDetailActionProperties"},"_PatchPresenceKnownActionProperties":{"properties":{"presence_known_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeRescuePresenceKnownValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeRescuePresenceKnownValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Presence Known Type"}},"additionalProperties":false,"type":"object","title":"_PatchPresenceKnownActionProperties"},"_PatchPvPowergenHazardActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeEmerghazPvPowergenAction"},{"type":"null"}]},"source_or_target":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeEmerghazPvIgnValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeEmerghazPvIgnValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Source Or Target"},"pv_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeEmerghazPvValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeEmerghazPvValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Pv Type"}},"additionalProperties":false,"type":"object","title":"_PatchPvPowergenHazardActionProperties"},"_PatchReleaseActionProperties":{"properties":{"estimated_amount":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetIntAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetIntAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Estimated Amount"},"unit_of_measurement":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeHazardUnitValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeHazardUnitValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Unit Of Measurement"},"physical_state":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeHazardPhysicalStateValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeHazardPhysicalStateValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Physical State"},"released_into":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeHazardReleasedIntoValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeHazardReleasedIntoValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Released Into"},"cause":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeHazardCauseValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeHazardCauseValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Cause"}},"additionalProperties":false,"type":"object","title":"_PatchReleaseActionProperties"},"_PatchRemovalActionProperties":{"properties":{"type":{"type":"string","const":"REMOVAL","title":"Type","default":"REMOVAL"},"fire_removal":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchFireRemovalAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchFireRemovalAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Fire Removal"},"room_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeRoomValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeRoomValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Room Type"},"elevation_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeRescueElevationValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeRescueElevationValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Elevation Type"},"gas_isolation":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Gas Isolation"},"rescue_path_type":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeRescuePathValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeRescuePathValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Rescue Path Type"}},"additionalProperties":false,"type":"object","title":"_PatchRemovalActionProperties"},"_PatchRescueActionProperties":{"properties":{"ffrescue_or_nonffrescue":{"anyOf":[{"$ref":"#/components/schemas/PatchFfRescueAction"},{"$ref":"#/components/schemas/PatchNonFfRescueAction"},{"type":"null"}],"title":"Ffrescue Or Nonffrescue"},"presence_known":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchPresenceKnownAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchPresenceKnownAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Presence Known"},"mayday":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/PatchRescueMaydayAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"patch":"#/components/schemas/PatchRescueMaydayAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Mayday"}},"additionalProperties":false,"type":"object","title":"_PatchRescueActionProperties"},"_PatchRescueMaydayActionProperties":{"properties":{"mayday":{"type":"boolean","const":true,"title":"Mayday","default":true},"relative_suppression_time":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeSuppressTimeValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeSuppressTimeValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Relative Suppression Time"},"rit_activated":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Rit Activated"}},"additionalProperties":false,"type":"object","title":"_PatchRescueMaydayActionProperties"},"_PatchSmokeAlarmActionProperties":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/PatchSmokeAlarmPresentAction"},{"$ref":"#/components/schemas/PatchSmokeAlarmNotpresentAction"},{"type":"null"}],"title":"Presence"}},"additionalProperties":false,"type":"object","title":"_PatchSmokeAlarmActionProperties"},"_PatchSmokeAlarmNotpresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetNotpresentAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchSmokeAlarmNotpresentActionProperties"},"_PatchSmokeAlarmOperationActionProperties":{"properties":{"alerted_failed":{"anyOf":[{"$ref":"#/components/schemas/PatchSmokeAlarmOperationAlertedAction"},{"$ref":"#/components/schemas/PatchSmokeAlarmOperationFailedAction"},{"type":"null"}],"title":"Alerted Failed"}},"additionalProperties":false,"type":"object","title":"_PatchSmokeAlarmOperationActionProperties"},"_PatchSmokeAlarmOperationAlertedActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeAlarmOperationAlertedAction"},{"type":"null"}]},"occupant_action":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeOccupantResponseValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeOccupantResponseValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Occupant Action"}},"additionalProperties":false,"type":"object","title":"_PatchSmokeAlarmOperationAlertedActionProperties"},"_PatchSmokeAlarmOperationFailedActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeAlarmOperationFailedAction"},{"type":"null"}]},"failure_reason":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/_SetTypeAlarmFailureValueAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/_SetTypeAlarmFailureValueAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Failure Reason"}},"additionalProperties":false,"type":"object","title":"_PatchSmokeAlarmOperationFailedActionProperties"},"_PatchSmokeAlarmPresentActionProperties":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetPresentAction"},{"type":"null"}]},"working":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Working"},"alarm_types":{"anyOf":[{"$ref":"#/components/schemas/SetRelListTypeAlarmSmokeValueAction"},{"type":"null"}]},"operation":{"anyOf":[{"$ref":"#/components/schemas/PatchSmokeAlarmOperationAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchSmokeAlarmPresentActionProperties"},"_PatchStructureFireLocationDetailActionProperties":{"properties":{"arrival_condition":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeFireConditionArrivalValueAction"},{"type":"null"}]},"progression_evident":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetBoolAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetBoolAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Progression Evident"},"damage_type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeFireBldgDamageValueAction"},{"type":"null"}]},"floor_of_origin":{"anyOf":[{"$ref":"#/components/schemas/SetIntAction"},{"type":"null"}]},"room_of_origin_type":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeRoomValueAction"},{"type":"null"}]},"cause":{"anyOf":[{"$ref":"#/components/schemas/_SetTypeFireCauseInValueAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchStructureFireLocationDetailActionProperties"},"_PatchTacticTimestampsActionProperties":{"properties":{"command_established":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Command Established"},"completed_sizeup":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Completed Sizeup"},"suppression_complete":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Suppression Complete"},"primary_search_begin":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Primary Search Begin"},"primary_search_complete":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Primary Search Complete"},"water_on_fire":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Water On Fire"},"fire_under_control":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Fire Under Control"},"fire_knocked_down":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Fire Knocked Down"},"extrication_complete":{"anyOf":[{"oneOf":[{"$ref":"#/components/schemas/SetDateTimeAction"},{"$ref":"#/components/schemas/UnsetAction"}],"discriminator":{"propertyName":"action","mapping":{"set":"#/components/schemas/SetDateTimeAction","unset":"#/components/schemas/UnsetAction"}}},{"type":"null"}],"title":"Extrication Complete"}},"additionalProperties":false,"type":"object","title":"_PatchTacticTimestampsActionProperties"},"_SetTypeAidDirectionValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeAidDirectionValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeAidDirectionValueAction"},"_SetTypeAidValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeAidValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeAidValueAction"},"_SetTypeAlarmFailureValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeAlarmFailureValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeAlarmFailureValueAction"},"_SetTypeAlarmOperationAlertedAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"OPERATED_ALERTED_OCCUPANT","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeAlarmOperationAlertedAction"},"_SetTypeAlarmOperationFailedAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"FAILED_TO_OPERATE","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeAlarmOperationFailedAction"},"_SetTypeAlarmOperationValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeAlarmOperationValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeAlarmOperationValueAction"},"_SetTypeCasualtyAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","enum":["INJURED_NONFATAL","INJURED_FATAL"],"title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeCasualtyAction"},"_SetTypeCasualtyActionValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeCasualtyActionValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeCasualtyActionValueAction"},"_SetTypeCasualtyCauseValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeCasualtyCauseValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeCasualtyCauseValueAction"},"_SetTypeCasualtyNoinjuryAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"UNINJURED","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeCasualtyNoinjuryAction"},"_SetTypeCasualtyTimelineValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeCasualtyTimelineValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeCasualtyTimelineValueAction"},"_SetTypeDutyValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeDutyValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeDutyValueAction"},"_SetTypeEmerghazElecValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeEmerghazElecValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeEmerghazElecValueAction"},"_SetTypeEmerghazOtherPowergenAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","enum":["WIND_TURBINE","OTHER","NOT_APPLICABLE"],"title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeEmerghazOtherPowergenAction"},"_SetTypeEmerghazPvIgnValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeEmerghazPvIgnValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeEmerghazPvIgnValueAction"},"_SetTypeEmerghazPvPowergenAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"PHOTOVOLTAICS","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeEmerghazPvPowergenAction"},"_SetTypeEmerghazPvValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeEmerghazPvValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeEmerghazPvValueAction"},"_SetTypeExposureDamageValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeExposureDamageValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeExposureDamageValueAction"},"_SetTypeExposureItemValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeExposureItemValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeExposureItemValueAction"},"_SetTypeExposureLocAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"EXTERNAL_EXPOSURE","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeExposureLocAction"},"_SetTypeFfNonffAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","enum":["FF","NONFF"],"title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeFfNonffAction"},"_SetTypeFireBldgDamageValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeFireBldgDamageValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeFireBldgDamageValueAction"},"_SetTypeFireCauseInValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeFireCauseInValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeFireCauseInValueAction"},"_SetTypeFireCauseOutValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeFireCauseOutValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeFireCauseOutValueAction"},"_SetTypeFireConditionArrivalValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeFireConditionArrivalValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeFireConditionArrivalValueAction"},"_SetTypeFireInvestNeedValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeFireInvestNeedValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeFireInvestNeedValueAction"},"_SetTypeFullPartialValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeFullPartialValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeFullPartialValueAction"},"_SetTypeGenderValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeGenderValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeGenderValueAction"},"_SetTypeHazardCauseValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeHazardCauseValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeHazardCauseValueAction"},"_SetTypeHazardDispositionValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeHazardDispositionValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeHazardDispositionValueAction"},"_SetTypeHazardDotValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeHazardDotValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeHazardDotValueAction"},"_SetTypeHazardPhysicalStateValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeHazardPhysicalStateValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeHazardPhysicalStateValueAction"},"_SetTypeHazardReleasedIntoValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeHazardReleasedIntoValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeHazardReleasedIntoValueAction"},"_SetTypeHazardUnitValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeHazardUnitValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeHazardUnitValueAction"},"_SetTypeIncidentValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeIncidentValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeIncidentValueAction"},"_SetTypeInternalExposureLocAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"INTERNAL_EXPOSURE","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeInternalExposureLocAction"},"_SetTypeJobClassificationValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeJobClassificationValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeJobClassificationValueAction"},"_SetTypeLocCspCountryValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeLocCspCountryValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeLocCspCountryValueAction"},"_SetTypeLocPlaceValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeLocPlaceValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeLocPlaceValueAction"},"_SetTypeLocSnDirectionValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeLocSnDirectionValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeLocSnDirectionValueAction"},"_SetTypeLocSnPrePostValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeLocSnPrePostValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeLocSnPrePostValueAction"},"_SetTypeLocSnPreSepValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeLocSnPreSepValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeLocSnPreSepValueAction"},"_SetTypeLocationCrossStreetValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeLocationCrossStreetValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeLocationCrossStreetValueAction"},"_SetTypeLocationUseValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeLocationUseValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeLocationUseValueAction"},"_SetTypeMedicalPatientCareValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeMedicalPatientCareValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeMedicalPatientCareValueAction"},"_SetTypeMedicalPatientStatusValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeMedicalPatientStatusValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeMedicalPatientStatusValueAction"},"_SetTypeMedicalTransportValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeMedicalTransportValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeMedicalTransportValueAction"},"_SetTypeNoactionValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeNoactionValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeNoactionValueAction"},"_SetTypeOccupantResponseValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeOccupantResponseValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeOccupantResponseValueAction"},"_SetTypeRaceValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeRaceValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeRaceValueAction"},"_SetTypeRescueElevationValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeRescueElevationValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeRescueElevationValueAction"},"_SetTypeRescueFfrescueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","enum":["RESCUED_BY_FIREFIGHTER","RESCUED_BY_FF_RIT","EVAC_ASSISTED_BY_FIREFIGHTER"],"title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeRescueFfrescueAction"},"_SetTypeRescueModeValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeRescueModeValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeRescueModeValueAction"},"_SetTypeRescueNonffrescueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","enum":["RESCUED_BY_NONFIREFIGHTER","SELF_EVACUATION","NO_RESCUE_NEEDED"],"title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeRescueNonffrescueAction"},"_SetTypeRescuePathValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeRescuePathValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeRescuePathValueAction"},"_SetTypeRescuePresenceKnownValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeRescuePresenceKnownValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeRescuePresenceKnownValueAction"},"_SetTypeResponseMode_TransportValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeResponseModeValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeResponseMode_TransportValueAction"},"_SetTypeResponseMode_UnitValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeResponseModeValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeResponseMode_UnitValueAction"},"_SetTypeRoomValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeRoomValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeRoomValueAction"},"_SetTypeSourceTargetValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeSourceTargetValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeSourceTargetValueAction"},"_SetTypeSuppressFireValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeSuppressFireValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeSuppressFireValueAction"},"_SetTypeSuppressNoOperationValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeSuppressNoOperationValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeSuppressNoOperationValueAction"},"_SetTypeSuppressOperationEffectiveAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"OPERATED_EFFECTIVE","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeSuppressOperationEffectiveAction"},"_SetTypeSuppressOperationFailedAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"NO_OPERATION","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeSuppressOperationFailedAction"},"_SetTypeSuppressOperationIneffectiveAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"type":"string","const":"OPERATED_NOT_EFFECTIVE","title":"Value"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeSuppressOperationIneffectiveAction"},"_SetTypeSuppressOperationValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeSuppressOperationValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeSuppressOperationValueAction"},"_SetTypeSuppressTimeValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeSuppressTimeValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeSuppressTimeValueAction"},"_SetTypeVacancyValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeVacancyValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeVacancyValueAction"},"_SetTypeWaterSupplyValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeWaterSupplyValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeWaterSupplyValueAction"},"_SetTypeYesNoUnknownValueAction":{"properties":{"action":{"type":"string","const":"set","title":"Action","description":"Set the value of an attribute"},"value":{"$ref":"#/components/schemas/TypeYesNoUnknownValue"}},"additionalProperties":false,"type":"object","required":["action","value"],"title":"_SetTypeYesNoUnknownValueAction"},"neris__api__auth__models__UserResponse":{"properties":{"sub":{"type":"string","format":"uuid","title":"Sub"}},"additionalProperties":false,"type":"object","required":["sub"],"title":"UserResponse"},"neris__api__incident__models__DepartmentTimeZoneResponse":{"properties":{"time_zone":{"anyOf":[{"$ref":"#/components/schemas/TimeZones"},{"type":"null"}],"description":"Time zone of the department that reported the incident"}},"type":"object","title":"DepartmentTimeZoneResponse"},"neris__api__incident__patch_models___PatchMedicalOxygenHazardActionProperties__1":{"properties":{"presence":{"oneOf":[{"$ref":"#/components/schemas/PatchMedicalOxygenHazardPresentAction"},{"$ref":"#/components/schemas/PatchMedicalOxygenHazardNotpresentAction"},{"type":"null"}],"title":"Presence"}},"additionalProperties":false,"type":"object","title":"_PatchMedicalOxygenHazardActionProperties"},"neris__api__incident__patch_models___PatchMedicalOxygenHazardActionProperties__2":{"properties":{"type":{"anyOf":[{"$ref":"#/components/schemas/SetNotpresentAction"},{"type":"null"}]}},"additionalProperties":false,"type":"object","title":"_PatchMedicalOxygenHazardActionProperties"},"neris__api__incident_analysis__models__DepartmentTimeZoneResponse":{"properties":{"time_zone":{"anyOf":[{"$ref":"#/components/schemas/TimeZones"},{"type":"null"}],"description":"Time zone of the department that reported the incident analysis"}},"type":"object","title":"DepartmentTimeZoneResponse"},"neris__api__user__models__UserResponse":{"properties":{"sub":{"type":"string","format":"uuid","title":"Sub"},"username":{"type":"string","format":"uuid","title":"Username"},"email":{"type":"string","format":"email","title":"Email"},"phone_number":{"anyOf":[{"type":"string","pattern":"^(\\+?[1|0])?( )*([ \\[\\(])?\\d{3}([\\)\\]\\. \\-])?( )*\\d{3}([\\. \\-])?( )*\\d{4}$","examples":["+15555555555","(555)555-55555","555.555.5555","(555) 555-5555","15555555555","[555]555-5555"]},{"type":"null"}],"title":"Phone Number"},"status":{"$ref":"#/components/schemas/UserStatus"},"given_name":{"type":"string","title":"Given Name"},"family_name":{"type":"string","title":"Family Name"},"active":{"type":"boolean","title":"Active"},"email_verified":{"type":"boolean","title":"Email Verified"},"feature_flags":{"$ref":"#/components/schemas/UserFeatureFlags"}},"type":"object","required":["sub","username","email","status","given_name","family_name","active","email_verified","feature_flags"],"title":"UserResponse"},"StationPayload":{"additionalProperties":false,"description":"\n\n
Validation Rules\n\n- The concatenated `address_line_1` + `address_line_2` must be at least 6 characters long.\n\n
","properties":{"address_line_1":{"maxLength":255,"minLength":2,"title":"Address Line 1","type":"string"},"address_line_2":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Address Line 2"},"city":{"maxLength":255,"minLength":1,"title":"City","type":"string"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"maxLength":10,"minLength":5,"title":"Zip Code","type":"string"},"station_id":{"title":"Station Id","type":"string"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Internal Id"},"staffing":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"title":"Staffing"},"neris_id":{"title":"Neris Id","type":"string"},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}],"default":null},"units":{"items":{"oneOf":[{"$ref":"#/components/schemas/UnitPayload"},{"$ref":"#/components/schemas/CreateUnitPayload"}]},"title":"Units","type":"array"}},"required":["address_line_1","city","state","zip_code","station_id","neris_id"],"title":"StationPayload","type":"object"},"UnitPayload":{"additionalProperties":false,"description":"\n\n
Validation Rules\n\n- `dedicated_staffing` must not be `true` if `staffing` is `0`.\n\n
","properties":{"staffing":{"description":"The minimum staffing required for the unit to be dispatched to an incident.","minimum":0,"title":"Staffing","type":"integer"},"dedicated_staffing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"description":"Whether the unit has dedicated staffing.","title":"Dedicated Staffing"},"type":{"$ref":"#/components/schemas/TypeUnitValue","description":"The type of unit housed at the station, and its associated capability."},"cad_designation_1":{"description":"A unit's designation in the computer-aided dispatch (CAD) system.","examples":["CAR15","Quint-42","HAZMAT #08"],"pattern":"^[\\w\\(\\)\\#\\- ]{1,64}$","title":"Cad Designation 1","type":"string"},"cad_designation_2":{"anyOf":[{"examples":["CAR15","Quint-42","HAZMAT #08"],"pattern":"^[\\w\\(\\)\\#\\- ]{1,64}$","type":"string"},{"type":"null"}],"default":null,"title":"Cad Designation 2"},"neris_id":{"title":"Neris Id","type":"string"}},"required":["staffing","type","cad_designation_1","neris_id"],"title":"UnitPayload","type":"object"},"DepartmentPayload":{"additionalProperties":false,"description":"\n\n
Validation Rules\n\n- The concatenated `address_line_1` + `address_line_2` must be at least 6 characters long.\n\n
","properties":{"mail_address_line_1":{"anyOf":[{"maxLength":255,"minLength":2,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"default":null},"mail_zip_code":{"anyOf":[{"maxLength":10,"minLength":5,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Zip Code"},"address_line_1":{"maxLength":255,"minLength":2,"title":"Address Line 1","type":"string"},"address_line_2":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Address Line 2"},"city":{"maxLength":255,"minLength":1,"title":"City","type":"string"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"maxLength":10,"minLength":5,"title":"Zip Code","type":"string"},"name":{"title":"Name","type":"string"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Email"},"continue_edu":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Continue Edu"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Internal Id"},"rms_software":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Rms Software"},"time_zone":{"$ref":"#/components/schemas/TimeZones"},"department_type":{"anyOf":[{"$ref":"#/components/schemas/TypeDeptValue"},{"type":"null"}],"default":null},"entity_type":{"anyOf":[{"$ref":"#/components/schemas/TypeEntityValue"},{"type":"null"}],"default":null},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationPayload"},{"type":"null"}]},"dispatch":{"anyOf":[{"$ref":"#/components/schemas/DepartmentDispatchPayload"},{"type":"null"}]},"staffing":{"anyOf":[{"$ref":"#/components/schemas/StaffingPayload"},{"type":"null"}]},"website":{"anyOf":[{"format":"uri","maxLength":2083,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Website"},"assessment":{"anyOf":[{"$ref":"#/components/schemas/AssessmentPayload"},{"type":"null"}]},"shift":{"anyOf":[{"$ref":"#/components/schemas/ShiftPayload"},{"type":"null"}]},"fire_services":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeServFdValue"},"type":"array"},{"type":"null"}],"title":"Fire Services"},"ems_services":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeServEmsValue"},"type":"array"},{"type":"null"}],"title":"Ems Services"},"investigation_services":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeServInvestValue"},"type":"array"},{"type":"null"}],"title":"Investigation Services"},"stations":{"items":{"oneOf":[{"$ref":"#/components/schemas/StationPayload"},{"$ref":"#/components/schemas/CreateStationPayload"}]},"title":"Stations","type":"array"},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}],"default":null},"region_sets":{"anyOf":[{"items":{"$ref":"#/components/schemas/RegionSetPayload"},"type":"array"},{"type":"null"}],"title":"Region Sets"},"aid_agreements":{"anyOf":[{"items":{"$ref":"#/components/schemas/AidAgreementPayload"},"type":"array"},{"type":"null"}],"title":"Aid Agreements"},"onboarding_status":{"anyOf":[{"$ref":"#/components/schemas/DeptOnboardingStatus"},{"type":"null"}],"default":"UNCONFIRMED"},"fips_code":{"default":null,"description":"FIPS code for NERIS ID generation","title":"Fips Code","type":"string"}},"required":["address_line_1","city","state","zip_code","name","time_zone"],"title":"DepartmentPayload","type":"object"},"FireMarshalPayload":{"additionalProperties":false,"description":"\n\n
Validation Rules\n\n- The concatenated `address_line_1` + `address_line_2` must be at least 6 characters long.\n\n
","properties":{"mail_address_line_1":{"anyOf":[{"maxLength":255,"minLength":2,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"default":null},"mail_zip_code":{"anyOf":[{"maxLength":10,"minLength":5,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Zip Code"},"address_line_1":{"maxLength":255,"minLength":2,"title":"Address Line 1","type":"string"},"address_line_2":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Address Line 2"},"city":{"maxLength":255,"minLength":1,"title":"City","type":"string"},"state":{"$ref":"#/components/schemas/StatesTerrs"},"zip_code":{"maxLength":10,"minLength":5,"title":"Zip Code","type":"string"},"name":{"title":"Name","type":"string"},"email":{"anyOf":[{"format":"email","type":"string"},{"type":"null"}],"default":null,"title":"Email"},"website":{"anyOf":[{"format":"uri","maxLength":2083,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Website"},"jurisdiction_type":{"$ref":"#/components/schemas/TypeFmJurisdictionValue"},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}],"default":null},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationPayload"},{"type":"null"}]}},"required":["address_line_1","city","state","zip_code","name","jurisdiction_type"],"title":"FireMarshalPayload","type":"object"},"PatchDepartmentPayload":{"additionalProperties":false,"properties":{"mail_address_line_1":{"anyOf":[{"maxLength":255,"minLength":2,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"default":null},"mail_zip_code":{"anyOf":[{"maxLength":10,"minLength":5,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Zip Code"},"address_line_1":{"default":null,"maxLength":255,"minLength":2,"title":"Address Line 1","type":"string"},"address_line_2":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Address Line 2"},"city":{"default":null,"maxLength":255,"minLength":1,"title":"City","type":"string"},"state":{"$ref":"#/components/schemas/StatesTerrs","default":null},"zip_code":{"default":null,"maxLength":10,"minLength":5,"title":"Zip Code","type":"string"},"internal_id":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Internal Id"},"name":{"default":null,"title":"Name","type":"string"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Email"},"website":{"anyOf":[{"format":"uri","maxLength":2083,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Website"},"continue_edu":{"anyOf":[{"type":"boolean"},{"type":"null"}],"default":null,"title":"Continue Edu"},"department_type":{"anyOf":[{"$ref":"#/components/schemas/TypeDeptValue"},{"type":"null"}],"default":null},"entity_type":{"anyOf":[{"$ref":"#/components/schemas/TypeEntityValue"},{"type":"null"}],"default":null},"rms_software":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Rms Software"},"time_zone":{"$ref":"#/components/schemas/TimeZones","default":null},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationPayload"},{"type":"null"}],"default":null},"dispatch":{"anyOf":[{"$ref":"#/components/schemas/DepartmentDispatchPayload"},{"type":"null"}],"default":null},"staffing":{"anyOf":[{"$ref":"#/components/schemas/StaffingPayload"},{"type":"null"}],"default":null},"assessment":{"anyOf":[{"$ref":"#/components/schemas/AssessmentPayload"},{"type":"null"}],"default":null},"shift":{"anyOf":[{"$ref":"#/components/schemas/ShiftPayload"},{"type":"null"}],"default":null},"fire_services":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeServFdValue"},"type":"array"},{"type":"null"}],"default":null,"title":"Fire Services"},"ems_services":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeServEmsValue"},"type":"array"},{"type":"null"}],"default":null,"title":"Ems Services"},"investigation_services":{"anyOf":[{"items":{"$ref":"#/components/schemas/TypeServInvestValue"},"type":"array"},{"type":"null"}],"default":null,"title":"Investigation Services"},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}],"default":null},"region_sets":{"anyOf":[{"items":{"$ref":"#/components/schemas/RegionSetPayload"},"type":"array"},{"type":"null"}],"default":null,"title":"Region Sets"},"aid_agreements":{"anyOf":[{"items":{"$ref":"#/components/schemas/AidAgreementPayload"},"type":"array"},{"type":"null"}],"default":null,"title":"Aid Agreements"},"onboarding_status":{"$ref":"#/components/schemas/DeptOnboardingStatus","default":null}},"title":"PatchDepartmentPayload","type":"object"},"PatchFireMarshalPayload":{"additionalProperties":false,"properties":{"mail_address_line_1":{"anyOf":[{"maxLength":255,"minLength":2,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Address Line 1"},"mail_address_line_2":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Address Line 2"},"mail_city":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Mail City"},"mail_state":{"anyOf":[{"$ref":"#/components/schemas/StatesTerrs"},{"type":"null"}],"default":null},"mail_zip_code":{"anyOf":[{"maxLength":10,"minLength":5,"type":"string"},{"type":"null"}],"default":null,"title":"Mail Zip Code"},"address_line_1":{"default":null,"maxLength":255,"minLength":2,"title":"Address Line 1","type":"string"},"address_line_2":{"anyOf":[{"maxLength":255,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Address Line 2"},"city":{"default":null,"maxLength":255,"minLength":1,"title":"City","type":"string"},"state":{"$ref":"#/components/schemas/StatesTerrs","default":null},"zip_code":{"default":null,"maxLength":10,"minLength":5,"title":"Zip Code","type":"string"},"name":{"default":null,"title":"Name","type":"string"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"title":"Email"},"website":{"anyOf":[{"format":"uri","maxLength":2083,"minLength":1,"type":"string"},{"type":"null"}],"default":null,"title":"Website"},"jurisdiction_type":{"anyOf":[{"$ref":"#/components/schemas/TypeFmJurisdictionValue"},{"type":"null"}],"default":null},"location":{"anyOf":[{"$ref":"#/components/schemas/GeoPoint"},{"type":"null"}],"default":null},"population":{"anyOf":[{"$ref":"#/components/schemas/PopulationPayload"},{"type":"null"}],"default":null}},"title":"PatchFireMarshalPayload","type":"object"}}}} \ No newline at end of file diff --git a/Providers/Resgrid.Providers.Neris/Contract/neris-value-sets-v1.4.78-2026-09-03.json b/Providers/Resgrid.Providers.Neris/Contract/neris-value-sets-v1.4.78-2026-09-03.json new file mode 100644 index 00000000..7c68f06c --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/Contract/neris-value-sets-v1.4.78-2026-09-03.json @@ -0,0 +1,1309 @@ +{ + "contract_version": "1.4.78", + "pinned_on": "2026-09-03", + "source": "https://api.neris.fsri.org/v1/openapi.json", + "sets": { + "incident_type": { + "schema": "TypeIncidentValue", + "codes": [ + "FIRE||OUTSIDE_FIRE||CONSTRUCTION_WASTE", + "FIRE||OUTSIDE_FIRE||DUMPSTER_OUTDOOR_CONTAINER_FIRE", + "FIRE||OUTSIDE_FIRE||OTHER_OUTSIDE_FIRE", + "FIRE||OUTSIDE_FIRE||OUTSIDE_TANK_FIRE", + "FIRE||OUTSIDE_FIRE||TRASH_RUBBISH_FIRE", + "FIRE||OUTSIDE_FIRE||UTILITY_INFRASTRUCTURE_FIRE", + "FIRE||OUTSIDE_FIRE||VEGETATION_GRASS_FIRE", + "FIRE||OUTSIDE_FIRE||WILDFIRE_URBAN_INTERFACE", + "FIRE||OUTSIDE_FIRE||WILDFIRE_WILDLAND", + "FIRE||SPECIAL_FIRE||ESS_FIRE", + "FIRE||SPECIAL_FIRE||EXPLOSION", + "FIRE||SPECIAL_FIRE||INFRASTRUCTURE_FIRE", + "FIRE||STRUCTURE_FIRE||CHIMNEY_FIRE", + "FIRE||STRUCTURE_FIRE||CONFINED_COOKING_APPLIANCE_FIRE", + "FIRE||STRUCTURE_FIRE||ROOM_AND_CONTENTS_FIRE", + "FIRE||STRUCTURE_FIRE||STRUCTURAL_INVOLVEMENT_FIRE", + "FIRE||TRANSPORTATION_FIRE||AIRCRAFT_FIRE", + "FIRE||TRANSPORTATION_FIRE||BOAT_PERSONAL_WATERCRAFT_BARGE_FIRE", + "FIRE||TRANSPORTATION_FIRE||POWERED_MOBILITY_DEVICE_FIRE", + "FIRE||TRANSPORTATION_FIRE||TRAIN_RAIL_FIRE", + "FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_COMMERCIAL", + "FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_FOOD_TRUCK", + "FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_PASSENGER", + "FIRE||TRANSPORTATION_FIRE||VEHICLE_FIRE_RV", + "HAZSIT||HAZARDOUS_MATERIALS||BIOLOGICAL_RELEASE_INCIDENT", + "HAZSIT||HAZARDOUS_MATERIALS||CARBON_MONOXIDE_RELEASE", + "HAZSIT||HAZARDOUS_MATERIALS||FUEL_SPILL_ODOR", + "HAZSIT||HAZARDOUS_MATERIALS||GAS_LEAK_ODOR", + "HAZSIT||HAZARDOUS_MATERIALS||HAZMAT_RELEASE_FACILITY", + "HAZSIT||HAZARDOUS_MATERIALS||HAZMAT_RELEASE_TRANSPORT", + "HAZSIT||HAZARDOUS_MATERIALS||RADIOACTIVE_RELEASE_INCIDENT", + "HAZSIT||HAZARD_NONCHEM||BOMB_THREAT_RESPONSE_SUSPICIOUS_PACKAGE", + "HAZSIT||HAZARD_NONCHEM||ELEC_HAZARD_SHORT_CIRCUIT", + "HAZSIT||HAZARD_NONCHEM||ELEC_POWER_LINE_DOWN_ARCHING_MALFUNC", + "HAZSIT||HAZARD_NONCHEM||MOTOR_VEHICLE_COLLISION", + "HAZSIT||INVESTIGATION||ODOR", + "HAZSIT||INVESTIGATION||SMOKE_INVESTIGATION", + "HAZSIT||OVERPRESSURE||NO_RUPTURE", + "HAZSIT||OVERPRESSURE||RUPTURE_WITHOUT_FIRE", + "LAWENFORCE", + "MEDICAL||ILLNESS", + "MEDICAL||ILLNESS||ABDOMINAL_PAIN", + "MEDICAL||ILLNESS||ALLERGIC_REACTION_STINGS", + "MEDICAL||ILLNESS||ALTERED_MENTAL_STATUS", + "MEDICAL||ILLNESS||BACK_PAIN_NON_TRAUMA", + "MEDICAL||ILLNESS||BREATHING_PROBLEMS", + "MEDICAL||ILLNESS||CARDIAC_ARREST", + "MEDICAL||ILLNESS||CHEST_PAIN_NON_TRAUMA", + "MEDICAL||ILLNESS||CONVULSIONS_SEIZURES", + "MEDICAL||ILLNESS||DIABETIC_PROBLEMS", + "MEDICAL||ILLNESS||HEADACHE", + "MEDICAL||ILLNESS||HEART_PROBLEMS", + "MEDICAL||ILLNESS||NAUSEA_VOMITING", + "MEDICAL||ILLNESS||NO_APPROPRIATE_CHOICE", + "MEDICAL||ILLNESS||OVERDOSE", + "MEDICAL||ILLNESS||PANDEMIC_EPIDEMIC_OUTBREAK", + "MEDICAL||ILLNESS||PREGNANCY_CHILDBIRTH", + "MEDICAL||ILLNESS||PSYCHOLOGICAL_BEHAVIOR_ISSUES", + "MEDICAL||ILLNESS||SICK_CASE", + "MEDICAL||ILLNESS||STROKE_CVA", + "MEDICAL||ILLNESS||UNCONSCIOUS_VICTIM", + "MEDICAL||ILLNESS||UNKNOWN_PROBLEM", + "MEDICAL||ILLNESS||WELL_PERSON_CHECK", + "MEDICAL||INJURY", + "MEDICAL||INJURY||ANIMAL_BITES", + "MEDICAL||INJURY||ASSAULT", + "MEDICAL||INJURY||BURNS_EXPLOSION", + "MEDICAL||INJURY||CARBON_MONOXIDE_OTHER_INHALATION_INJURY", + "MEDICAL||INJURY||CHOKING", + "MEDICAL||INJURY||DROWNING_DIVING_SCUBA_ACCIDENT", + "MEDICAL||INJURY||ELECTROCUTION", + "MEDICAL||INJURY||EYE_TRAUMA", + "MEDICAL||INJURY||FALL", + "MEDICAL||INJURY||GUNSHOT_WOUND", + "MEDICAL||INJURY||HEAT_COLD_EXPOSURE", + "MEDICAL||INJURY||HEMORRHAGE_LACERATION", + "MEDICAL||INJURY||INDUSTRIAL_INACCESSIBLE_ENTRAPMENT", + "MEDICAL||INJURY||MOTOR_VEHICLE_COLLISION", + "MEDICAL||INJURY||OTHER_TRAUMATIC_INJURY", + "MEDICAL||INJURY||POISONING", + "MEDICAL||INJURY||STAB_PENETRATING_TRAUMA", + "MEDICAL||OTHER||AIRMEDICAL_TRANSPORT", + "MEDICAL||OTHER||COMMUNITY_PUBLIC_HEALTH", + "MEDICAL||OTHER||HEALTHCARE_PROFESSIONAL_ADMISSION", + "MEDICAL||OTHER||INTERCEPT_OTHER_UNIT", + "MEDICAL||OTHER||MEDICAL_ALARM", + "MEDICAL||OTHER||STANDBY_REQUEST", + "MEDICAL||OTHER||TRANSFER_INTERFACILITY", + "NOEMERG||CANCELLED", + "NOEMERG||FALSE_ALARM||ACCIDENTAL_ALARM", + "NOEMERG||FALSE_ALARM||BOMB_SCARE", + "NOEMERG||FALSE_ALARM||INTENTIONAL_FALSE_ALARM", + "NOEMERG||FALSE_ALARM||MALFUNCTIONING_ALARM", + "NOEMERG||FALSE_ALARM||OTHER_FALSE_CALL", + "NOEMERG||GOOD_INTENT||CONTROLLED_BURNING_AUTHORIZED", + "NOEMERG||GOOD_INTENT||INVESTIGATE_HAZARDOUS_RELEASE", + "NOEMERG||GOOD_INTENT||NO_INCIDENT_FOUND_LOCATION_ERROR", + "NOEMERG||GOOD_INTENT||SMOKE_FROM_NONHOSTILE_SOURCE", + "PUBSERV||ALARMS_NONMED||CO_ALARM", + "PUBSERV||ALARMS_NONMED||FIRE_ALARM", + "PUBSERV||ALARMS_NONMED||GAS_ALARM", + "PUBSERV||ALARMS_NONMED||OTHER_ALARM", + "PUBSERV||CITIZEN_ASSIST||CITIZEN_ASSIST_SERVICE_CALL", + "PUBSERV||CITIZEN_ASSIST||LIFT_ASSIST", + "PUBSERV||CITIZEN_ASSIST||LOST_PERSON", + "PUBSERV||CITIZEN_ASSIST||PERSON_IN_DISTRESS", + "PUBSERV||DISASTER_WEATHER||DAMAGE_ASSESSMENT", + "PUBSERV||DISASTER_WEATHER||WEATHER_RESPONSE", + "PUBSERV||OTHER||DAMAGED_HYDRANT", + "PUBSERV||OTHER||MOVE_UP", + "PUBSERV||OTHER||STANDBY", + "RESCUE||OUTSIDE||BACKCOUNTRY_RESCUE", + "RESCUE||OUTSIDE||CONFINED_SPACE_RESCUE", + "RESCUE||OUTSIDE||EXTRICATION_ENTRAPPED", + "RESCUE||OUTSIDE||HIGH_ANGLE_RESCUE", + "RESCUE||OUTSIDE||LIMITED_NO_ACCESS", + "RESCUE||OUTSIDE||LOW_ANGLE_RESCUE", + "RESCUE||OUTSIDE||STEEP_ANGLE_RESCUE", + "RESCUE||OUTSIDE||TRENCH", + "RESCUE||STRUCTURE||BUILDING_STRUCTURE_COLLAPSE", + "RESCUE||STRUCTURE||CONFINED_SPACE_RESCUE", + "RESCUE||STRUCTURE||ELEVATOR_ESCALATOR_RESCUE", + "RESCUE||STRUCTURE||EXTRICATION_ENTRAPPED", + "RESCUE||TRANSPORTATION||AVIATION_COLLISION_CRASH", + "RESCUE||TRANSPORTATION||AVIATION_STANDBY", + "RESCUE||TRANSPORTATION||MOTOR_VEHICLE_EXTRICATION_ENTRAPPED", + "RESCUE||TRANSPORTATION||TRAIN_RAIL_COLLISION_DERAILMENT", + "RESCUE||WATER||PERSON_IN_WATER_STANDING", + "RESCUE||WATER||PERSON_IN_WATER_SWIFTWATER", + "RESCUE||WATER||WATERCRAFT_IN_DISTRESS" + ] + }, + "action_tactic": { + "schema": "TypeActionTacticValue", + "codes": [ + "COMMAND_AND_CONTROL||ACCOUNTABILITY_OFFICER_ASSIGNED", + "COMMAND_AND_CONTROL||ESTABLISH_INCIDENT_COMMAND", + "COMMAND_AND_CONTROL||INCIDENT_ASSESSMENT_COMPLETED", + "COMMAND_AND_CONTROL||NOTIFY_OTHER_AGENCIES", + "COMMAND_AND_CONTROL||PIO_ASSIGNED", + "COMMAND_AND_CONTROL||SAFETY_OFFICER_ASSIGNED", + "CONTAINMENT||OUTSIDE_FIRE_SUPPRESSION||DOZER_FUEL_BREAK", + "CONTAINMENT||OUTSIDE_FIRE_SUPPRESSION||HAND_CREW_FUEL_BREAK", + "EMERGENCY_MEDICAL_CARE||PATIENT_ASSESSMENT", + "EMERGENCY_MEDICAL_CARE||PATIENT_REFERRAL", + "EMERGENCY_MEDICAL_CARE||PROVIDE_ADVANCED_LIFE_SUPPORT", + "EMERGENCY_MEDICAL_CARE||PROVIDE_BASIC_LIFE_SUPPORT", + "EMERGENCY_MEDICAL_CARE||PROVIDE_TRANSPORT", + "FORCIBLE_ENTRY", + "HAZARDOUS_SITUATION_MITIGATION||ATMOSPHERIC_MONITORING_EXTERIOR_FENCELINE", + "HAZARDOUS_SITUATION_MITIGATION||ATMOSPHERIC_MONITORING_INTERIOR", + "HAZARDOUS_SITUATION_MITIGATION||DECONTAMINATION", + "HAZARDOUS_SITUATION_MITIGATION||LEAK_STOP", + "HAZARDOUS_SITUATION_MITIGATION||REMOVE_HAZARD", + "HAZARDOUS_SITUATION_MITIGATION||SPILL_CONTROL", + "HAZARDOUS_SITUATION_MITIGATION||TAKE_SAMPLES", + "INFORMATION_ENFORCEMENT||ENFORCE_CODE_OR_LAW", + "INFORMATION_ENFORCEMENT||PROVIDE_PUBLIC_INFORMATION", + "INFORMATION_ENFORCEMENT||REFER_TO_PROPER_AHJ", + "INVESTIGATION", + "NON_STRUCTURE_SEARCH||BODY_RECOVERY", + "NON_STRUCTURE_SEARCH||SEARCH_AREA_OF_COLLAPSE", + "NON_STRUCTURE_SEARCH||SEARCH_UNDERGROUND_INFRASTRUCTURE", + "NON_STRUCTURE_SEARCH||SEARCH_WATERWAY", + "NON_STRUCTURE_SEARCH||USAR_K9_SEARCH", + "NON_STRUCTURE_SEARCH||WIDE_AREA_OUTDOOR_SEARCH", + "PERSONNEL_CONTAMINATION_REDUCTION||CLEAN_CAB_TRANSPORT", + "PERSONNEL_CONTAMINATION_REDUCTION||ON_SCENE_CONTAMINATION_REDUCTION", + "PERSONNEL_CONTAMINATION_REDUCTION||PPE_WASHED_POST_INCIDENT", + "PROVIDE_EQUIPMENT||PROVIDE_DRONE_VIDEO_EQUIPMENT", + "PROVIDE_EQUIPMENT||PROVIDE_ELECTRICAL_POWER", + "PROVIDE_EQUIPMENT||PROVIDE_LIGHT", + "PROVIDE_EQUIPMENT||PROVIDE_SPECIAL_EQUIPMENT", + "PROVIDE_EVACUATION_SUPPORT||CONNECTED_INTERIOR_SPACES", + "PROVIDE_EVACUATION_SUPPORT||LARGE_AREA", + "PROVIDE_EVACUATION_SUPPORT||NEARBY_BUILDINGS", + "PROVIDE_EVACUATION_SUPPORT||REMOTE_INTERIOR_SPACES", + "PROVIDE_SERVICES||ASSIST_ANIMAL", + "PROVIDE_SERVICES||ASSIST_UNINJURED_PERSON", + "PROVIDE_SERVICES||CONTROL_CROWD", + "PROVIDE_SERVICES||CONTROL_TRAFFIC", + "PROVIDE_SERVICES||DAMAGE_ASSESSMENT", + "PROVIDE_SERVICES||PROVIDE_APPARATUS_WATER", + "PROVIDE_SERVICES||REMOVE_WATER", + "PROVIDE_SERVICES||RESTORE_RESET_ALARM_SYSTEM", + "PROVIDE_SERVICES||RESTORE_SPRINKLER_SYSTEM", + "PROVIDE_SERVICES||SECURE_PROPERTY", + "PROVIDE_SERVICES||SHUT_DOWN_ALARM", + "PROVIDE_SERVICES||SHUT_DOWN_SPRINKLER_SYSTEM", + "SALVAGE_AND_OVERHAUL", + "SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH", + "SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH||DURING_SUPPRESSION", + "SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH||POST_SUPPRESSION", + "SEARCH_STRUCTURE||DOOR_INITIATED_SEARCH||PRIOR_TO_SUPPRESSION", + "SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH", + "SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH||DURING_SUPPRESSION", + "SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH||POST_SUPPRESSION", + "SEARCH_STRUCTURE||WINDOW_INITIATED_SEARCH||PRIOR_TO_SUPPRESSION", + "SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||BACKBURN", + "SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||CONFINEMENT", + "SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||ESTABLISH_FIRE_LINES", + "SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||FIRE_CONTROL_EXTINGUISHMENT", + "SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||FIRE_RETARDANT_DROP", + "SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||STRUCTURE_PROTECTION", + "SUPPRESSION||OUTSIDE_FIRE_SUPPRESSION||WATER_DROP", + "SUPPRESSION||STRUCTURAL_FIRE_SUPPRESSION||EXTERIOR", + "SUPPRESSION||STRUCTURAL_FIRE_SUPPRESSION||EXTERIOR_AND_INTERIOR", + "SUPPRESSION||STRUCTURAL_FIRE_SUPPRESSION||INTERIOR", + "VENTILATION||HORIZONTAL", + "VENTILATION||HORIZONTAL||DURING_SUPPRESSION", + "VENTILATION||HORIZONTAL||POST_SUPPRESSION", + "VENTILATION||HORIZONTAL||PRIOR_TO_SUPPRESSION", + "VENTILATION||HYDRAULIC", + "VENTILATION||HYDRAULIC||DURING_SUPPRESSION", + "VENTILATION||HYDRAULIC||POST_SUPPRESSION", + "VENTILATION||HYDRAULIC||PRIOR_TO_SUPPRESSION", + "VENTILATION||POSITIVE_PRESSURE", + "VENTILATION||POSITIVE_PRESSURE||DURING_SUPPRESSION", + "VENTILATION||POSITIVE_PRESSURE||POST_SUPPRESSION", + "VENTILATION||POSITIVE_PRESSURE||PRIOR_TO_SUPPRESSION", + "VENTILATION||VERTICAL", + "VENTILATION||VERTICAL||DURING_SUPPRESSION", + "VENTILATION||VERTICAL||POST_SUPPRESSION", + "VENTILATION||VERTICAL||PRIOR_TO_SUPPRESSION" + ] + }, + "aid_type": { + "schema": "TypeAidValue", + "codes": [ + "ACTING_AS_AID", + "IN_LIEU_AID", + "SUPPORT_AID" + ] + }, + "aid_direction": { + "schema": "TypeAidDirectionValue", + "codes": [ + "GIVEN", + "RECEIVED" + ] + }, + "aid_nonfd": { + "schema": "TypeAidNonfdValue", + "codes": [ + "ANIMAL_SERVICES", + "EMS", + "HOUSING_SERVICES", + "LAW_ENFORCEMENT", + "REMEDIATION_SERVICES", + "SOCIAL_SERVICES", + "UTILITIES_PUBLIC_WORKS" + ] + }, + "special_modifier": { + "schema": "TypeSpecialModifierValue", + "codes": [ + "ACTIVE_ASSAILANT", + "COUNTY_LOCAL_DECLARED_DISASTER", + "FEDERAL_DECLARED_DISASTER", + "MCI", + "STATE_DECLARED_DISASTER", + "URBAN_CONFLAGRATION", + "VIOLENCE_AGAINST_RESPONDER", + "WORLD_CUP_2026" + ] + }, + "response_mode": { + "schema": "TypeResponseModeValue", + "codes": [ + "EMERGENT", + "NON_EMERGENT" + ] + }, + "incident_status": { + "schema": "TypeIncidentStatusValue", + "codes": [ + "APPROVED", + "DELETED", + "FAILED", + "PENDING_APPROVAL", + "PENDING_INCIDENT_DATA", + "REJECTED", + "SUBMITTED" + ] + }, + "incident_status_payload": { + "schema": "TypeIncidentStatusPayloadValue", + "codes": [ + "APPROVED", + "REJECTED" + ] + }, + "location_place": { + "schema": "TypeLocPlaceValue", + "codes": [ + "AIRCRAFT", + "AIRPORT", + "ARENA", + "AUTOMOBILE", + "BANK", + "BAR", + "BICYCLE", + "BUS", + "BUS_STATION", + "CAFE", + "CLASSROOM", + "CLUB", + "CONSTRUCTION", + "CONVENTION_CENTER", + "GOVERNMENT", + "HOSPITAL", + "HOTEL", + "INDUSTRIAL", + "LIBRARY", + "MOTORCYCLE", + "MUSEUM", + "OFFICE", + "OTHER", + "OUTDOORS", + "PARKING", + "PLACE_OF_WORSHIP", + "PRISON", + "PUBLIC", + "PUBLIC_TRANSPORT", + "RESIDENCE", + "RESTAURANT", + "SCHOOL", + "SHOPPING_AREA", + "STADIUM", + "STORE", + "STREET", + "THEATER", + "TRAIN", + "TRAIN_STATION", + "TRUCK", + "UNDERWAY", + "UNKNOWN", + "WAREHOUSE", + "WATER", + "WATERCRAFT" + ] + }, + "location_use": { + "schema": "TypeLocationUseValue", + "codes": [ + "AGRICULTURE_STRUCT||ANIMAL_PROCESSING", + "AGRICULTURE_STRUCT||AUCTION_FEEDLOT", + "AGRICULTURE_STRUCT||FARM_BUILDING", + "AGRICULTURE_STRUCT||STORAGE_SILO", + "AGRICULTURE_STRUCT||VETERINARY_LIVESTOCK", + "ASSEMBLY||COMMUNITY_CENTER", + "ASSEMBLY||CONVENTION_CENTER", + "ASSEMBLY||INDOOR_ARENA", + "ASSEMBLY||MUSEUM_EXHIBIT_HALL_LIBRARY", + "ASSEMBLY||OUTDOOR_ARENA_AMPHITHEATER_PARK", + "ASSEMBLY||RELIGIOUS", + "ASSEMBLY||TEMP_OUTDOOR_STRUCT_EVENT", + "COMMERCIAL||BAR_NIGHTCLUB", + "COMMERCIAL||ENTERTAINMENT_RECREATION", + "COMMERCIAL||OFFICE_OTHER_TECHNICAL_SERVICES", + "COMMERCIAL||RESTAURANT_CAFE", + "COMMERCIAL||RETAIL_WHOLESALE_TRADE", + "COMMERCIAL||THEATERS_STUDIO", + "COMMERCIAL||VEHICLE_FUELING_CHARGING_STATION", + "COMMERCIAL||VEHICLE_REPAIR_SERVICES", + "COMMERCIAL||VETERINARY_PET", + "EDUCATION||COLLEGES_UNIVERSITIES", + "EDUCATION||DORMITORY_HOUSING", + "EDUCATION||K_12_SCHOOLS", + "EDUCATION||OTHER_EDUCATIONAL_BUILDINGS", + "EDUCATION||PREK_DAYCARE", + "GOVERNMENT||FIRE_MEDICAL_STATION", + "GOVERNMENT||GENERAL_SERVICES", + "GOVERNMENT||JAIL_PRISON_REFORMATORY", + "GOVERNMENT||NON_CIVILIAN_STRUCTURES", + "GOVERNMENT||POLICE_EMERGENCY_STATION", + "HEALTH_CARE||ALCOHOL_DRUG_REHABILITATION_CENTER", + "HEALTH_CARE||HOSPITAL_24_HOUR_MEDICAL_FACILITIES", + "HEALTH_CARE||MEDICAL_OFFICE_CLINIC", + "HEALTH_CARE||NURSING_HOME_ASSISTED_LIVING_RESIDENCE_ONSITE", + "INDUSTRIAL||CHEMICAL", + "INDUSTRIAL||COLD_STORAGE", + "INDUSTRIAL||FOOD_DRUGS", + "INDUSTRIAL||HEAVY", + "INDUSTRIAL||LIGHT", + "INDUSTRIAL||METALS_MINERALS_PROCESSING", + "OUTDOOR_INDUSTRIAL||CONSTRUCTION_SITE", + "OUTDOOR_INDUSTRIAL||DUMP_LANDFILL", + "OUTDOOR_INDUSTRIAL||INDUSTRIAL_YARD", + "OUTDOOR_INDUSTRIAL||MINE", + "OUTDOOR||CAMP_SITE", + "OUTDOOR||FOREST_GRASSLANDS_WOODLAND_WILDLAND_AREAS", + "OUTDOOR||GROUND_VACANT_LAND", + "OUTDOOR||HIKING_TRAIL", + "OUTDOOR||OPEN_WATER", + "OUTDOOR||ORCHARD_CROPS_FARMLAND", + "OUTDOOR||PLAYGROUND_PARK_RECREATIONAL_AREA", + "OUTDOOR||WATERFRONT", + "RESIDENTIAL||ATTACHED_SINGLE_FAMILY_DWELLING", + "RESIDENTIAL||CONGREGATE_HOUSING", + "RESIDENTIAL||DETATCHED_GARAGE", + "RESIDENTIAL||DETATCHED_SINGLE_FAMILY_DWELLING", + "RESIDENTIAL||MANUFACTURED_MOBILE_HOME", + "RESIDENTIAL||MULTI_FAMILY_HIGHRISE_DWELLING", + "RESIDENTIAL||MULTI_FAMILY_LOWRISE_DWELLING", + "RESIDENTIAL||MULTI_FAMILY_MIDRISE_DWELLING", + "RESIDENTIAL||TEMPORARY_LODGING_HOTEL_MOTEL", + "RESIDENTIAL||UNHOUSED_TEMPORARY_SHELTER", + "ROADWAY_ACCESS||BRIDGE", + "ROADWAY_ACCESS||HIGHWAY_INTERSTATE", + "ROADWAY_ACCESS||LIMITED_ACCESS_HIGHWAY_INTERSTATE", + "ROADWAY_ACCESS||PARKING_LOT_GARAGE", + "ROADWAY_ACCESS||RAILROAD_RAILYARD", + "ROADWAY_ACCESS||SIDEWALK", + "ROADWAY_ACCESS||STREET", + "ROADWAY_ACCESS||TUNNEL", + "STORAGE||STORAGE_MULTI_TENANT", + "STORAGE||STORAGE_PORTABLE_BUILDING", + "STORAGE||STORAGE_SINGLE_TENANT", + "UNCLASSIFIED||UNCLASSIFIED", + "UTILITY_MISC||ENERGY_FACILITY_INFRASTRUCTURE", + "UTILITY_MISC||TRANSPORTATION_STATION_HUB_AREA", + "UTILITY_MISC||TRASH_RECYCLING_FACILITY", + "UTILITY_MISC||WATER_SANITATION_FACILITY_INFRASTRUCTURE" + ] + }, + "vacancy": { + "schema": "TypeVacancyValue", + "codes": [ + "ABANDONED", + "DAMAGE_DECAY", + "FORECLOSURE", + "FOR_SALE_LEASE", + "NEW_CONSTRUCTION_REMODEL", + "SEASONAL_OCCASIONALLY_OCCUPIED", + "UNKNOWN" + ] + }, + "state": { + "schema": "StatesTerrs", + "codes": [ + "AL", + "AK", + "AS", + "AZ", + "AR", + "CA", + "CO", + "CT", + "DE", + "DC", + "FL", + "FM", + "GA", + "GU", + "HI", + "ID", + "IL", + "IN", + "IA", + "KS", + "KY", + "LA", + "ME", + "MD", + "MA", + "MI", + "MH", + "MN", + "MS", + "MO", + "MP", + "MT", + "NA", + "NE", + "NV", + "NH", + "NJ", + "NM", + "NY", + "NC", + "ND", + "OH", + "OK", + "OR", + "PA", + "PR", + "PW", + "RI", + "SC", + "SD", + "TN", + "TX", + "UM", + "UT", + "VT", + "VA", + "VI", + "WA", + "WV", + "WI", + "WY", + "AA", + "AE", + "AP", + "AB", + "BC", + "MB", + "NB", + "NL", + "NS", + "NT", + "NU", + "ON", + "PE", + "QC", + "SK", + "YT", + "AGU", + "BCN", + "BCS", + "CAM", + "CHP", + "CHH", + "CMX", + "COA", + "COL", + "DUR", + "GUA", + "GRO", + "HID", + "JAL", + "MIC", + "MOR", + "MEX", + "NAY", + "NLE", + "OAX", + "PUE", + "QUE", + "ROO", + "SLP", + "SIN", + "SON", + "TAB", + "TAM", + "TLA", + "VER", + "YUC", + "ZAC" + ] + }, + "country": { + "schema": "TypeLocCspCountryValue", + "codes": [ + "AD", + "AE", + "AF", + "AG", + "AI", + "AL", + "AM", + "AO", + "AQ", + "AR", + "AS", + "AT", + "AU", + "AW", + "AX", + "AZ", + "BA", + "BB", + "BD", + "BE", + "BF", + "BG", + "BH", + "BI", + "BJ", + "BL", + "BM", + "BN", + "BO", + "BQ", + "BR", + "BS", + "BT", + "BV", + "BW", + "BY", + "BZ", + "CA", + "CC", + "CD", + "CF", + "CG", + "CH", + "CI", + "CK", + "CL", + "CM", + "CN", + "CO", + "CR", + "CU", + "CV", + "CW", + "CX", + "CY", + "CZ", + "DE", + "DJ", + "DK", + "DM", + "DO", + "DZ", + "EC", + "EE", + "EG", + "EH", + "ER", + "ES", + "ET", + "FI", + "FJ", + "FK", + "FM", + "FO", + "FR", + "GA", + "GB", + "GD", + "GE", + "GF", + "GG", + "GH", + "GI", + "GL", + "GM", + "GN", + "GP", + "GQ", + "GR", + "GS", + "GT", + "GU", + "GW", + "GY", + "HK", + "HM", + "HN", + "HR", + "HT", + "HU", + "ID", + "IE", + "IL", + "IM", + "IN", + "IO", + "IQ", + "IR", + "IS", + "IT", + "JE", + "JM", + "JO", + "JP", + "KE", + "KG", + "KH", + "KI", + "KM", + "KN", + "KP", + "KR", + "KW", + "KY", + "KZ", + "LA", + "LB", + "LC", + "LI", + "LK", + "LR", + "LS", + "LT", + "LU", + "LV", + "LY", + "MA", + "MC", + "MD", + "ME", + "MF", + "MG", + "MH", + "MK", + "ML", + "MM", + "MN", + "MO", + "MP", + "MQ", + "MR", + "MS", + "MT", + "MU", + "MV", + "MW", + "MX", + "MY", + "MZ", + "NA", + "NC", + "NE", + "NF", + "NG", + "NI", + "NL", + "NO", + "NP", + "NR", + "NU", + "NZ", + "OM", + "PA", + "PE", + "PF", + "PG", + "PH", + "PK", + "PL", + "PM", + "PN", + "PR", + "PS", + "PT", + "PW", + "PY", + "QA", + "RE", + "RO", + "RS", + "RU", + "RW", + "SA", + "SB", + "SC", + "SD", + "SE", + "SG", + "SH", + "SI", + "SJ", + "SK", + "SL", + "SM", + "SN", + "SO", + "SR", + "SS", + "ST", + "SV", + "SX", + "SY", + "SZ", + "TC", + "TD", + "TF", + "TG", + "TH", + "TJ", + "TK", + "TL", + "TM", + "TN", + "TO", + "TR", + "TT", + "TV", + "TW", + "TZ", + "UA", + "UG", + "UM", + "US", + "UY", + "UZ", + "VA", + "VC", + "VE", + "VG", + "VI", + "VN", + "VU", + "WF", + "WS", + "YE", + "YT", + "ZA", + "ZM", + "ZW" + ] + }, + "street_prefix_postfix": { + "schema": "TypeLocSnPrePostValue", + "codes": [ + "ABBEY", + "ACCESS", + "ACCESS ROAD", + "ACRES", + "AIRPORT", + "ALCOVE", + "ALLE", + "ALLEY", + "ANNEX", + "APPROACH", + "ARC", + "ARCADE", + "ARCH", + "AVENIDA", + "AVENUE", + "AVENUE CIRCLE", + "AVENUE COURT", + "AVENUE LOOP", + "AVENUE PATH", + "AVENUE PLACE", + "AVENUE WAY", + "BANK", + "BAY", + "BAYOU", + "BAYWAY", + "BEACH", + "BEND", + "BLUFF", + "BLUFFS", + "BOARDWALK", + "BOTTOM", + "BOULEVARD", + "BRANCH", + "BRIDGE", + "BROOK", + "BROOKS", + "BUREAU OF INDIAN AFFAIRS ROUTE", + "BURG", + "BURGS", + "BYPASS", + "CALLE", + "CALLEJON", + "CAMINO", + "CAMP", + "CANYON", + "CAPE", + "CARTWAY", + "CAUSEWAY", + "CENTER", + "CENTERS", + "CENTRE", + "CHANNEL", + "CHASE", + "CHEMIN", + "CIRCLE", + "CIRCLES", + "CIRCUS", + "CLIFF", + "CLIFFS", + "CLOSE", + "CLUB", + "CLUSTER", + "COAST HIGHWAY", + "COMMON", + "COMMONS", + "CONCESSION ROAD", + "CONCOURSE", + "CONNECT", + "CONNECTOR", + "CORNER", + "CORNERS", + "CORRIDOR", + "CORSO", + "CORTE", + "COUNTY FOREST ROAD", + "COUNTY HIGHWAY", + "COUNTY ROAD", + "COUNTY ROUTE", + "COUNTY STATE AID HIGHWAY", + "COURS", + "COURSE", + "COURT", + "COURTS", + "COVE", + "COVES", + "CREEK", + "CRESCENT", + "CREST", + "CROSS", + "CROSSING", + "CROSSINGS", + "CROSSOVER", + "CROSSROAD", + "CROSSROADS", + "CROSSWAY", + "CURVE", + "CUSTER COUNTY ROAD", + "CUTOFF", + "CUTTING", + "DALE", + "DAM", + "DAWSON COUNTY ROAD", + "DELL", + "DIVIDE", + "DOCK", + "DOWN", + "DOWNS", + "DRAW", + "DRIFT", + "DRIVE", + "DRIVES", + "DRIVEWAY", + "DUGWAY", + "ECHO", + "EDGE", + "END", + "ENTRANCE", + "ENTRY", + "ESPLANADE", + "ESTATE", + "ESTATES", + "EXCHANGE", + "EXIT", + "EXPRESSWAY", + "EXTENSION", + "EXTENSIONS", + "FALL", + "FALLS", + "FARE", + "FARM", + "FARM TO MARKET", + "FEDERAL-AID SECONDARY HIGHWAY", + "FERRY", + "FIELD", + "FIELDS", + "FLAT", + "FLATS", + "FLOWAGE", + "FLYWAY", + "FORD", + "FORDS", + "FOREST", + "FOREST HIGHWAY", + "FOREST ROAD", + "FORGE", + "FORGES", + "FORK", + "FORKS", + "FORT", + "FREEWAY", + "FRONT", + "FRONTAGE ROAD", + "GABLES", + "GARDEN", + "GARDENS", + "GARTH", + "GATE", + "GATES", + "GATEWAY", + "GLADE", + "GLEN", + "GLENS", + "GORGE", + "GRADE", + "GREEN", + "GREENS", + "GREENWAY", + "GROVE", + "GROVES", + "HARBOR", + "HARBORS", + "HARBOUR", + "HAUL ROAD", + "HAVEN", + "HEATH", + "HEIGHTS", + "HIDEAWAY", + "HIGHWAY", + "HILL", + "HILLS", + "HOLLOW", + "HORN", + "HORSESHOE", + "INDIAN SERVICE ROAD", + "INLET", + "INTERSTATE", + "INTERVAL", + "ISLAND", + "ISLANDS", + "ISLE", + "ISLES", + "J-TURN", + "JUNCTION", + "JUNCTIONS", + "KEEP", + "KEY", + "KEYS", + "KNOLL", + "KNOLLS", + "LAIR", + "LAKE", + "LAKES", + "LAND", + "LANDING", + "LANE", + "LANE CIRCLE", + "LANE COURT", + "LANE ROAD", + "LATERAL", + "LEDGE", + "LIGHT", + "LIGHTS", + "LINE", + "LOAF", + "LOCK", + "LOCKS", + "LODGE", + "LOOKOUT", + "LOOP", + "LOOP ROAD", + "LUGAR", + "MALL", + "MANOR", + "MANORS", + "MARKET", + "MEADOW", + "MEADOWS", + "MEWS", + "MILL", + "MILLS", + "MISSION", + "MONTANA HIGHWAY", + "MOTORWAY", + "MOUNT", + "MOUNTAIN", + "MOUNTAINS", + "NARROWS", + "NATIONAL FOREST DEVELOPMENT ROAD", + "NECK", + "NOOK", + "NORTH CAROLINA HIGHWAY", + "OAKS", + "OLD COUNTY ROAD", + "ORCHARD", + "OVAL", + "OVERLOOK", + "OVERPASS", + "OVI", + "PARK", + "PARKE", + "PARKS", + "PARKWAY", + "PARKWAYS", + "PASEO", + "PASS", + "PASSAGE", + "PATH", + "PATHWAY", + "PIAZZA", + "PIKE", + "PINE", + "PINES", + "PLACE", + "PLACITA", + "PLAIN", + "PLAINS", + "PLATZ", + "PLAZA", + "POINT", + "POINTE", + "POINTS", + "PORT", + "PORTS", + "PRAIRIE", + "PRIVATE ROAD", + "PROMENADE", + "PUBLIC ACCESS", + "QUARTER", + "QUAY", + "RADIAL", + "RAMP", + "RANCH", + "RANCHO", + "RAPID", + "RAPIDS", + "REACH", + "RECREATIONAL ROAD", + "REST", + "RETREAT", + "RIDGE", + "RIDGES", + "RISE", + "RIVER", + "RIVER ROAD", + "ROAD", + "ROADS", + "ROUND", + "ROUTE", + "ROW", + "RUE", + "RUN", + "RUNNE", + "RUNWAY", + "SHOAL", + "SHOALS", + "SHORE", + "SHORES", + "SIDEROAD", + "SKIES", + "SKYWAY", + "SLIP", + "SPRING", + "SPRINGS", + "SPUR", + "SPURS", + "SQUARE", + "SQUARES", + "STATE HIGHWAY", + "STATE PARK ROAD", + "STATE PARKWAY", + "STATE ROAD", + "STATE ROUTE", + "STATE SECONDARY", + "STATE SPUR", + "STATION", + "STRAND", + "STRASSE", + "STRAVENUE", + "STREAM", + "STREET", + "STREET CIRCLE", + "STREET COURT", + "STREET LOOP", + "STREET PATH", + "STREET PLACE", + "STREET WAY", + "STREETS", + "STRIP", + "SUMMIT", + "TAXIWAY", + "TERMINAL", + "TERN", + "TERRACE", + "THROUGHWAY", + "THRUWAY", + "TIMBER ROAD", + "TOWN ROAD", + "TOWNLINE", + "TOWNSHIP ROAD", + "TRACE", + "TRACK", + "TRAFFICWAY", + "TRAIL", + "TRAILER", + "TRIANGLE", + "TRUCK TRAIL", + "TUNNEL", + "TURN", + "TURNPIKE", + "UNDERPASS", + "UNION", + "UNIONS", + "UNITED STATES FOREST SERVICE ROAD", + "UNITED STATES HIGHWAY", + "UUNYE", + "VALLEY", + "VALLEYS", + "VIA", + "VIADUCT", + "VIEW", + "VIEWS", + "VILLA", + "VILLAGE", + "VILLAGES", + "VILLE", + "VISTA", + "VOG", + "WADDY", + "WALK", + "WALKS", + "WALL", + "WAY", + "WAYS", + "WEEG", + "WELL", + "WELLS", + "WOODS", + "WYE", + "WYND" + ] + }, + "street_direction": { + "schema": "TypeLocSnDirectionValue", + "codes": [ + "EASTBOUND", + "NORTHBOUND", + "SOUTHBOUND", + "WESTBOUND" + ] + }, + "street_separator": { + "schema": "TypeLocSnPreSepValue", + "codes": [ + "AT", + "DE", + "DEL", + "DES", + "DE_LA", + "DE_LAS", + "IN_THE", + "OF", + "OF_THE", + "ON_THE", + "TO", + "TO_THE" + ] + }, + "cross_street_modifier": { + "schema": "TypeLocationCrossStreetValue", + "codes": [ + "CLOSEST", + "INCIDENT_IN_INTERSECTION", + "SECOND_CLOSEST" + ] + }, + "dispatch_protocol_fire": { + "schema": "TypeDispProtoFireValue", + "codes": [ + "APCO", + "IAED", + "OTHER", + "PROQA" + ] + }, + "dispatch_protocol_medical": { + "schema": "TypeDispProtoMedValue", + "codes": [ + "APCO", + "IAED", + "OTHER", + "PROQA" + ] + }, + "incident_complexity": { + "schema": "TypeIncidentComplexityValue", + "codes": [ + "ICS_TYPE_1", + "ICS_TYPE_2", + "ICS_TYPE_3", + "ICS_TYPE_4", + "ICS_TYPE_5" + ] + }, + "fire_condition_arrival": { + "schema": "TypeFireConditionArrivalValue", + "codes": [ + "FIRE_OUT_UPON_ARRIVAL", + "FIRE_SPREAD_BEYOND_STRUCTURE", + "NO_SMOKE_FIRE_SHOWING", + "SMOKE_FIRE_SHOWING", + "SMOKE_SHOWING", + "STRUCTURE_INVOLVED" + ] + }, + "dins_origin_location": { + "schema": "TypeDinsOriginLocationValue", + "codes": [ + "ATTACHED_FENCE", + "ATTACHED_PATIO_COVER_CARPORT", + "DECK_ELEVATED", + "DECK_ON_GRADE", + "EAVES", + "OTHER", + "ROOF", + "SIDING", + "UNKNOWN", + "VENT", + "WINDOW" + ] + }, + "dins_origin_cause": { + "schema": "TypeDinsOriginCauseValue", + "codes": [ + "DIRECT_FLAME_IMPINGEMENT", + "EMBERS", + "OTHER", + "RADIANT_HEAT", + "UNKNOWN" + ] + } + } +} \ No newline at end of file diff --git a/Providers/Resgrid.Providers.Neris/NerisApiClient.cs b/Providers/Resgrid.Providers.Neris/NerisApiClient.cs new file mode 100644 index 00000000..33cba8ef --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/NerisApiClient.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using Resgrid.Config; +using Resgrid.Framework; +using Resgrid.Model; +using Resgrid.Model.Providers; + +namespace Resgrid.Providers.Neris +{ + /// + /// The NERIS HTTP boundary generated against the pinned contract (Contract/neris-openapi-v1.4.78): POST /token + /// (form, grant_type), POST /incident/{entity}/validate (204 valid, 422 issues), POST /incident/{entity} + /// (201 IncidentCreatedResponse), PUT /incident/{entity}/{id}, GET /incident/{entity}/{id}. Bearer tokens are + /// cached per profile until shortly before expiry. Every reply is reduced to a NerisSubmissionOutcome; the + /// verbatim body rides in ResponseJson for the artifact and never anywhere else. + /// + public class NerisApiClient : INerisApiClient + { + private static readonly HttpClient SharedHttpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(Math.Max(5, NerisConfig.TimeoutSeconds)) }; + private static readonly ConcurrentDictionary Tokens = new ConcurrentDictionary(); + + private readonly HttpClient _http; + + public NerisApiClient() : this(SharedHttpClient) { } + + /// Test seam: a client over a fake handler. + public NerisApiClient(HttpClient http) + { + _http = http ?? throw new ArgumentNullException(nameof(http)); + } + + public static string BaseUrlFor(RmsNerisProfile profile) + { + if (!string.IsNullOrWhiteSpace(profile?.BaseUrlOverride)) + return profile.BaseUrlOverride.TrimEnd('/'); + if (profile?.Environment == NerisEnvironments.Sandbox && !string.IsNullOrWhiteSpace(NerisConfig.SandboxBaseUrl)) + return NerisConfig.SandboxBaseUrl.TrimEnd('/'); + return (NerisConfig.BaseUrl ?? string.Empty).TrimEnd('/'); + } + + public Task ValidateAsync(RmsNerisProfile profile, NerisCredential credential, string payloadJson, CancellationToken cancellationToken = default) + { + return SendAsync(profile, credential, HttpMethod.Post, () => $"/incident/{Entity(profile)}/validate", payloadJson, ValidateOutcome, cancellationToken); + } + + public Task CreateIncidentAsync(RmsNerisProfile profile, NerisCredential credential, string payloadJson, CancellationToken cancellationToken = default) + { + return SendAsync(profile, credential, HttpMethod.Post, () => $"/incident/{Entity(profile)}", payloadJson, CreateOutcome, cancellationToken); + } + + public Task UpdateIncidentAsync(RmsNerisProfile profile, NerisCredential credential, string nerisIncidentId, string payloadJson, CancellationToken cancellationToken = default) + { + return SendAsync(profile, credential, HttpMethod.Put, () => $"/incident/{Entity(profile)}/{Uri.EscapeDataString(nerisIncidentId ?? string.Empty)}", payloadJson, UpdateOutcome(nerisIncidentId), cancellationToken); + } + + public Task GetStatusAsync(RmsNerisProfile profile, NerisCredential credential, string nerisIncidentId, CancellationToken cancellationToken = default) + { + return SendAsync(profile, credential, HttpMethod.Get, () => $"/incident/{Entity(profile)}/{Uri.EscapeDataString(nerisIncidentId ?? string.Empty)}", null, StatusOutcome(nerisIncidentId), cancellationToken); + } + + private static string Entity(RmsNerisProfile profile) => Uri.EscapeDataString(profile.NerisEntityId); + + private async Task SendAsync(RmsNerisProfile profile, NerisCredential credential, HttpMethod method, Func pathFactory, string body, + Func interpret, CancellationToken cancellationToken) + { + if (profile == null || string.IsNullOrWhiteSpace(profile.NerisEntityId)) + return Fatal("The department has no NERIS entity ID configured."); + if (credential == null) + return Fatal("The department has no NERIS credential configured."); + + var path = pathFactory(); + + string token; + try + { + token = await GetTokenAsync(profile, credential, cancellationToken); + } + catch (NerisAuthException ex) + { + return Fatal(ex.Message); + } + catch (Exception ex) when (ex is HttpRequestException || ex is TaskCanceledException || ex is OperationCanceledException && !cancellationToken.IsCancellationRequested) + { + return Transient("NERIS token endpoint unreachable: " + ex.Message); + } + + try + { + using var request = new HttpRequestMessage(method, BaseUrlFor(profile) + path); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + if (body != null) + request.Content = new StringContent(body, Encoding.UTF8, "application/json"); + + using var response = await _http.SendAsync(request, cancellationToken); + var text = response.Content == null ? string.Empty : await response.Content.ReadAsStringAsync(cancellationToken); + + if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.Forbidden) + { + Tokens.TryRemove(TokenKey(profile), out _); + return WithStatus(Fatal($"NERIS refused the credential ({(int)response.StatusCode})."), response.StatusCode, text); + } + + if ((int)response.StatusCode == 429 || (int)response.StatusCode >= 500) + return WithStatus(Transient($"NERIS returned {(int)response.StatusCode}."), response.StatusCode, text); + + return interpret(response.StatusCode, text); + } + catch (Exception ex) when (ex is HttpRequestException || ex is TaskCanceledException || ex is OperationCanceledException && !cancellationToken.IsCancellationRequested) + { + return Transient("NERIS unreachable: " + ex.Message); + } + } + + private static NerisSubmissionOutcome ValidateOutcome(HttpStatusCode status, string text) + { + if (status == HttpStatusCode.NoContent || status == HttpStatusCode.OK) + return WithStatus(new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Accepted, Message = "Valid" }, status, text); + if ((int)status == 422) + return WithStatus(Rejected(text), status, text); + return WithStatus(Transient($"Unexpected NERIS validate reply {(int)status}."), status, text); + } + + private static NerisSubmissionOutcome CreateOutcome(HttpStatusCode status, string text) + { + if (status == HttpStatusCode.Created || status == HttpStatusCode.OK) + { + var json = TryParse(text); + var outcome = new NerisSubmissionOutcome + { + Kind = NerisOutcomeKind.Created, + ExternalId = (string)json?["neris_id"], + ExternalStatus = (string)json?["incident_status"]?["status"] + }; + return WithStatus(Promote(outcome), status, text); + } + if ((int)status == 422) + return WithStatus(Rejected(text), status, text); + return WithStatus(Transient($"Unexpected NERIS create reply {(int)status}."), status, text); + } + + private static Func UpdateOutcome(string nerisIncidentId) + { + return (status, text) => + { + if (status == HttpStatusCode.OK || status == HttpStatusCode.NoContent) + return WithStatus(new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Updated, ExternalId = nerisIncidentId }, status, text); + if ((int)status == 422) + return WithStatus(Rejected(text), status, text); + if (status == HttpStatusCode.NotFound) + return WithStatus(Fatal("NERIS no longer knows the incident; the next submission must create it again."), status, text); + return WithStatus(Transient($"Unexpected NERIS update reply {(int)status}."), status, text); + }; + } + + private static Func StatusOutcome(string nerisIncidentId) + { + return (status, text) => + { + if (status != HttpStatusCode.OK) + return WithStatus(Transient($"Unexpected NERIS status reply {(int)status}."), status, text); + + var json = TryParse(text); + var outcome = new NerisSubmissionOutcome { ExternalId = nerisIncidentId, ExternalStatus = (string)json?["incident_status"]?["status"], Kind = NerisOutcomeKind.Pending }; + return WithStatus(Promote(outcome), status, text); + }; + } + + /// NERIS incident status values: APPROVED, REJECTED, FAILED, SUBMITTED, PENDING_APPROVAL, PENDING_INCIDENT_DATA, DELETED. + public static NerisSubmissionOutcome Promote(NerisSubmissionOutcome outcome) + { + switch ((outcome.ExternalStatus ?? string.Empty).ToUpperInvariant()) + { + case "APPROVED": + outcome.Kind = NerisOutcomeKind.Accepted; + break; + case "REJECTED": + outcome.Kind = NerisOutcomeKind.Rejected; + if (outcome.Errors.Count == 0) + outcome.Errors.Add(new NerisSubmissionError { Code = "REJECTED", Message = "The destination rejected the incident." }); + break; + case "FAILED": + outcome.Kind = NerisOutcomeKind.Fatal; + outcome.Message ??= "The destination marked the incident FAILED."; + break; + case "DELETED": + outcome.Kind = NerisOutcomeKind.Fatal; + outcome.Message ??= "The destination deleted the incident."; + break; + default: + if (outcome.Kind == NerisOutcomeKind.Created && !string.IsNullOrEmpty(outcome.ExternalId)) + break; // created; status pending is expressed by the worker as AwaitingDestination + if (outcome.Kind != NerisOutcomeKind.Updated) + outcome.Kind = NerisOutcomeKind.Pending; + break; + } + return outcome; + } + + /// Reduces an HTTPValidationError body to codes and field paths; message text is kept short and never echoes payload values. + public static NerisSubmissionOutcome Rejected(string text) + { + var outcome = new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Rejected }; + var json = TryParse(text); + var details = json?["detail"] as JArray; + if (details != null) + { + foreach (var detail in details.OfType()) + { + var loc = detail["loc"] as JArray; + outcome.Errors.Add(new NerisSubmissionError + { + Code = ((string)detail["type"] ?? "validation").Trim(), + FieldPath = loc == null ? null : string.Join(".", loc.Select(l => (string)l).Where(l => !string.IsNullOrEmpty(l) && l != "body")), + Message = Truncate((string)detail["msg"], 300) + }); + } + } + else if (json?["detail"] != null) + { + outcome.Errors.Add(new NerisSubmissionError { Code = "validation", Message = Truncate(json["detail"].ToString(), 300) }); + } + + if (outcome.Errors.Count == 0) + outcome.Errors.Add(new NerisSubmissionError { Code = "validation", Message = "The destination rejected the payload." }); + + outcome.Message = $"{outcome.Errors.Count} validation issue(s)."; + return outcome; + } + + private async Task GetTokenAsync(RmsNerisProfile profile, NerisCredential credential, CancellationToken cancellationToken) + { + var key = TokenKey(profile); + if (Tokens.TryGetValue(key, out var cached) && cached.ExpiresOn > DateTime.UtcNow.AddSeconds(60)) + return cached.AccessToken; + + var form = new Dictionary { ["grant_type"] = profile.GrantType == NerisGrantTypes.Password ? "password" : "client_credentials" }; + if (profile.GrantType == NerisGrantTypes.Password) + { + form["username"] = credential.Username ?? string.Empty; + form["password"] = credential.Password ?? string.Empty; + } + else + { + form["username"] = credential.ClientId ?? string.Empty; + form["password"] = credential.ClientSecret ?? string.Empty; + } + + using var request = new HttpRequestMessage(HttpMethod.Post, BaseUrlFor(profile) + "/token") { Content = new FormUrlEncodedContent(form) }; + using var response = await _http.SendAsync(request, cancellationToken); + var text = response.Content == null ? string.Empty : await response.Content.ReadAsStringAsync(cancellationToken); + var json = TryParse(text); + + if (response.StatusCode == HttpStatusCode.OK && json?["access_token"] != null) + { + var expires = json["expires_in"]?.Type == JTokenType.Integer ? (int)json["expires_in"] : 3600; + var token = new CachedToken { AccessToken = (string)json["access_token"], ExpiresOn = DateTime.UtcNow.AddSeconds(expires) }; + Tokens[key] = token; + return token.AccessToken; + } + + if ((int)response.StatusCode == 429 || (int)response.StatusCode >= 500) + throw new HttpRequestException($"NERIS token endpoint returned {(int)response.StatusCode}."); + + var error = (string)json?["error"] ?? (string)json?["challenge_name"] ?? response.StatusCode.ToString(); + throw new NerisAuthException($"NERIS did not issue a token ({error})."); + } + + private static string TokenKey(RmsNerisProfile profile) => $"{profile.DepartmentId}:{profile.RmsNerisProfileId}:{profile.RowVersion}"; + + private static NerisSubmissionOutcome WithStatus(NerisSubmissionOutcome outcome, HttpStatusCode status, string text) + { + outcome.StatusCode = (int)status; + outcome.ResponseJson = text; + return outcome; + } + + private static NerisSubmissionOutcome Fatal(string message) => new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Fatal, Message = message }; + private static NerisSubmissionOutcome Transient(string message) => new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Transient, Message = message }; + + private static JObject TryParse(string text) + { + if (string.IsNullOrWhiteSpace(text)) + return null; + try { return JObject.Parse(text); } + catch { return null; } + } + + private static string Truncate(string value, int max) => value == null ? null : value.Length <= max ? value : value.Substring(0, max); + + private sealed class CachedToken + { + public string AccessToken { get; set; } + public DateTime ExpiresOn { get; set; } + } + + private sealed class NerisAuthException : Exception + { + public NerisAuthException(string message) : base(message) { } + } + } +} diff --git a/Providers/Resgrid.Providers.Neris/NerisMappingService.cs b/Providers/Resgrid.Providers.Neris/NerisMappingService.cs new file mode 100644 index 00000000..4d779d60 --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/NerisMappingService.cs @@ -0,0 +1,200 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Resgrid.Model; +using Resgrid.Model.Providers; + +namespace Resgrid.Providers.Neris +{ + /// + /// Pure mapping from the incident report snapshot to the pinned contract's IncidentPayload (RMS plan section + /// 5.5). Deterministic: the same snapshot yields byte-identical JSON, which is what makes the stored payload + /// artifact and its checksum meaningful. Nulls are omitted rather than sent; original CAD codes ride in + /// dispatch.incident_code beside the mapped incident types; nothing from the narrative's supplemental + /// (department-only) section or any restricted content enters the payload. + /// + public class NerisMappingService : INerisMappingService + { + public const string GeoPointType = "Point"; + public const int Wgs84 = 4326; + + public string BuildIncidentPayloadJson(NerisIncidentSnapshot snapshot, RmsNerisProfile profile) + { + if (snapshot?.Report == null) throw new ArgumentNullException(nameof(snapshot)); + if (profile == null) throw new ArgumentNullException(nameof(profile)); + + var report = snapshot.Report; + var location = MapLocation(snapshot.Location); + var point = MapPoint(snapshot.Location); + + var payload = new JObject + { + ["base"] = Compact(new JObject + { + ["department_neris_id"] = profile.NerisEntityId, + ["incident_number"] = report.IncidentNumber, + ["people_present"] = report.PeoplePresent, + ["animals_rescued"] = report.AnimalsRescued, + ["displacement_count"] = report.DisplacementCount, + ["impediment_narrative"] = Blank(snapshot.Narrative?.ImpedimentNarrative), + ["outcome_narrative"] = Blank(snapshot.Narrative?.OutcomeNarrative), + ["point"] = point, + ["location"] = location + }), + ["incident_types"] = new JArray(snapshot.Types.OrderBy(t => t.Ordinal).Select(t => Compact(new JObject + { + ["type"] = t.TypeCode, + ["primary"] = t.IsPrimary + }))), + ["dispatch"] = Compact(new JObject + { + ["incident_number"] = report.IncidentNumber, + ["center_id"] = Blank(report.DispatchCenterId), + ["determinant_code"] = Blank(report.DeterminantCode), + ["incident_code"] = Blank(report.DispatchIncidentCode), + ["disposition"] = Blank(report.Disposition), + ["call_create"] = Time(report.CallCreatedOn), + ["call_answered"] = Time(report.CallAnsweredOn), + ["call_arrival"] = Time(report.CallArrivalOn), + ["incident_clear"] = Time(report.IncidentClearedOn), + ["location"] = location, + ["point"] = point, + ["comments"] = snapshot.DispatchComments.Count == 0 ? null : new JArray(snapshot.DispatchComments.Select(c => Compact(new JObject + { + ["timestamp"] = Time(c.Timestamp), + ["comment"] = c.Comment + }))), + ["unit_responses"] = new JArray(snapshot.Units.OrderBy(u => u.Ordinal).Select(MapUnit)) + }) + }; + + if (snapshot.SpecialModifiers.Count > 0) + payload["special_modifiers"] = new JArray(snapshot.SpecialModifiers); + + if (snapshot.Aids.Any(a => !a.IsNonFireDepartment)) + { + payload["aids"] = new JArray(snapshot.Aids.Where(a => !a.IsNonFireDepartment).OrderBy(a => a.Ordinal).Select(a => Compact(new JObject + { + ["department_neris_id"] = a.CounterpartNerisId, + ["aid_type"] = a.AidType, + ["aid_direction"] = a.Direction + }))); + } + + if (snapshot.Aids.Any(a => a.IsNonFireDepartment && !string.IsNullOrWhiteSpace(a.NonFdType))) + payload["nonfd_aids"] = new JArray(snapshot.Aids.Where(a => a.IsNonFireDepartment && !string.IsNullOrWhiteSpace(a.NonFdType)).Select(a => a.NonFdType).Distinct()); + + if (snapshot.Tactics.Count > 0) + { + payload["actions_tactics"] = new JObject + { + ["action_noaction"] = new JObject + { + ["type"] = "ACTION", + ["actions"] = new JArray(snapshot.Tactics.OrderBy(t => t.Ordinal).Select(t => t.TacticCode).Distinct()) + } + }; + } + + if (snapshot.Units.Count > 0) + payload["unit_responses"] = new JArray(snapshot.Units.OrderBy(u => u.Ordinal).Select(MapUnit)); + + return payload.ToString(Formatting.None); + } + + private static JObject MapUnit(RmsUnitResponse unit) + { + return Compact(new JObject + { + ["unit_neris_id"] = Blank(unit.UnitNerisId), + ["reported_unit_id"] = Blank(unit.UnitNameSnapshot), + ["staffing"] = unit.Staffing, + ["unable_to_dispatch"] = unit.UnableToDispatch ? true : (bool?)null, + ["dispatch"] = Time(unit.DispatchedOn), + ["enroute_to_scene"] = Time(unit.EnrouteOn), + ["on_scene"] = Time(unit.OnSceneOn), + ["canceled_enroute"] = Time(unit.CanceledEnrouteOn), + ["staging"] = Time(unit.StagingOn), + ["unit_clear"] = Time(unit.ClearedOn), + ["response_mode"] = Blank(unit.ResponseMode), + ["transport_mode"] = Blank(unit.TransportMode) + }); + } + + private static JObject MapLocation(RmsLocation location) + { + if (location == null) + return new JObject(); + + var result = Compact(new JObject + { + ["number"] = ParseNumber(location.Number), + ["number_prefix"] = Blank(location.NumberPrefix), + ["number_suffix"] = Blank(location.NumberSuffix), + ["complete_number"] = Blank(location.Number), + ["street"] = Blank(location.Street), + ["unit_value"] = Blank(location.UnitValue), + ["incorporated_municipality"] = Blank(location.Municipality), + ["county"] = Blank(location.County), + ["state"] = Blank(location.State), + ["postal_code"] = Blank(location.PostalCode), + ["country"] = Blank(location.Country), + ["place_type"] = Blank(location.PlaceType), + ["additional_info"] = Blank(location.AddressText) + }); + + var cross = new[] { location.CrossStreet1, location.CrossStreet2 }.Where(s => !string.IsNullOrWhiteSpace(s)).ToList(); + if (cross.Count > 0) + result["cross_streets"] = new JArray(cross.Select(s => new JObject { ["street"] = s.Trim() })); + + return result; + } + + private static JObject MapPoint(RmsLocation location) + { + if (location?.Latitude == null || location.Longitude == null) + return null; + + return new JObject + { + ["crs"] = Wgs84, + ["geometry"] = new JObject + { + ["type"] = GeoPointType, + ["coordinates"] = new JArray((double)location.Longitude.Value, (double)location.Latitude.Value) + } + }; + } + + /// ISO-8601 UTC with a Z suffix, the unambiguous form of the contract's date-time. + public static string Time(DateTime? value) + { + if (!value.HasValue) + return null; + + var utc = value.Value.Kind == DateTimeKind.Utc ? value.Value : DateTime.SpecifyKind(value.Value, DateTimeKind.Utc); + return utc.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture); + } + + private static string Blank(string value) + { + return string.IsNullOrWhiteSpace(value) ? null : value.Trim(); + } + + private static JToken ParseNumber(string number) + { + return int.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) ? new JValue(n) : null; + } + + /// Drops null-valued properties so an unknown fact is absent, never "null" the destination might reject. + private static JObject Compact(JObject source) + { + foreach (var property in source.Properties().Where(p => p.Value == null || p.Value.Type == JTokenType.Null || p.Value is JValue v && v.Value == null).ToList()) + property.Remove(); + return source; + } + } +} diff --git a/Providers/Resgrid.Providers.Neris/NerisProfileService.cs b/Providers/Resgrid.Providers.Neris/NerisProfileService.cs new file mode 100644 index 00000000..dc35b10a --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/NerisProfileService.cs @@ -0,0 +1,252 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Resgrid.Config; +using Resgrid.Framework; +using Resgrid.Model; +using Resgrid.Model.Providers; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; + +namespace Resgrid.Providers.Neris +{ + /// + /// Department NERIS profile, credential (encrypted per department, decrypted only for a call), the pinned + /// value sets, and the department crosswalks. Seeds RmsNerisValueSets from the embedded snapshot on first use + /// so reports and admin screens can query them; the snapshot stays the source of truth for the pinned version. + /// + public class NerisProfileService : INerisProfileService + { + private static readonly SemaphoreSlim SeedLock = new SemaphoreSlim(1, 1); + private static volatile bool _seeded; + + private readonly IRmsNerisProfilesRepository _profiles; + private readonly IRmsNerisValueSetsRepository _valueSets; + private readonly IRmsNerisCrosswalksRepository _crosswalks; + private readonly IDepartmentsService _departments; + private readonly IEncryptionService _encryption; + + public NerisProfileService(IRmsNerisProfilesRepository profiles, IRmsNerisValueSetsRepository valueSets, IRmsNerisCrosswalksRepository crosswalks, + IDepartmentsService departments, IEncryptionService encryption) + { + _profiles = profiles; + _valueSets = valueSets; + _crosswalks = crosswalks; + _departments = departments; + _encryption = encryption; + } + + public string ContractVersion => NerisValueSetCatalog.Instance.ContractVersion; + + public IReadOnlyList ValueSetKeys => NerisValueSetCatalog.Instance.SetKeys; + + public Task GetProfileAsync(int departmentId) + { + return _profiles.GetByDepartmentIdAsync(departmentId); + } + + public async Task SaveProfileAsync(RmsNerisProfile profile, NerisCredential credential, string userId, CancellationToken cancellationToken = default) + { + if (profile == null) throw new ArgumentNullException(nameof(profile)); + if (profile.DepartmentId <= 0) throw new ArgumentException("A department is required.", nameof(profile)); + + var now = DateTime.UtcNow; + var existing = await _profiles.GetByDepartmentIdAsync(profile.DepartmentId); + var target = existing ?? new RmsNerisProfile + { + RmsNerisProfileId = Guid.NewGuid().ToString(), + DepartmentId = profile.DepartmentId, + ProtectionId = Guid.NewGuid().ToString(), + CreatedOn = now, + RowVersion = 0 + }; + + target.NerisEntityId = string.IsNullOrWhiteSpace(profile.NerisEntityId) ? null : profile.NerisEntityId.Trim().ToUpperInvariant(); + target.EntityName = profile.EntityName?.Trim(); + target.Environment = profile.Environment == NerisEnvironments.Sandbox ? NerisEnvironments.Sandbox : NerisEnvironments.Production; + target.BaseUrlOverride = string.IsNullOrWhiteSpace(profile.BaseUrlOverride) ? null : profile.BaseUrlOverride.Trim(); + target.GrantType = profile.GrantType == NerisGrantTypes.Password ? NerisGrantTypes.Password : NerisGrantTypes.ClientCredentials; + target.ContractVersion = ContractVersion; + target.AutoSubmitOnFinalize = profile.AutoSubmitOnFinalize; + target.IsEnabled = profile.IsEnabled; + target.UpdatedByUserId = userId; + target.ModifiedOn = now; + target.RowVersion += 1; + + if (credential != null) + { + var department = await _departments.GetDepartmentByIdAsync(profile.DepartmentId, false); + var json = JsonConvert.SerializeObject(credential); + target.EncryptedCredentialJson = _encryption.EncryptForDepartment(json, profile.DepartmentId, department?.Code); + } + + if (existing == null) + return await _profiles.InsertAsync(target, cancellationToken, true); + + return await _profiles.UpdateAsync(target, cancellationToken, true); + } + + public async Task GetCredentialAsync(RmsNerisProfile profile) + { + if (profile == null || !profile.HasCredential) + return null; + + var department = await _departments.GetDepartmentByIdAsync(profile.DepartmentId, false); + var json = _encryption.DecryptForDepartment(profile.EncryptedCredentialJson, profile.DepartmentId, department?.Code); + return string.IsNullOrWhiteSpace(json) ? null : JsonConvert.DeserializeObject(json); + } + + public async Task IsSubmissionEnabledAsync(int departmentId) + { + if (!NerisConfig.Enabled) + return false; + + var profile = await _profiles.GetByDepartmentIdAsync(departmentId); + return profile != null && profile.IsEnabled && !string.IsNullOrWhiteSpace(profile.NerisEntityId) && profile.HasCredential; + } + + public NerisValueSet GetValueSet(string setKey) + { + return NerisValueSetCatalog.Instance.Get(setKey); + } + + public async Task EnsureValueSetsSeededAsync(CancellationToken cancellationToken = default) + { + if (_seeded) + return; + + await SeedLock.WaitAsync(cancellationToken); + try + { + if (_seeded) + return; + + var catalog = NerisValueSetCatalog.Instance; + var expected = catalog.SetKeys.Sum(k => catalog.Get(k).Codes.Count); + var present = await _valueSets.CountForVersionAsync(catalog.ContractVersion); + if (present >= expected) + { + _seeded = true; + return; + } + + var now = DateTime.UtcNow; + foreach (var key in catalog.SetKeys) + { + var set = catalog.Get(key); + for (var i = 0; i < set.Codes.Count; i++) + { + var code = set.Codes[i]; + if (present > 0 && await _valueSets.ExistsAsync(catalog.ContractVersion, key, code)) + continue; + + await _valueSets.InsertAsync(new RmsNerisValueSetEntry + { + ContractVersion = catalog.ContractVersion, + SetKey = key, + Code = code, + Label = Label(code), + ParentCode = Parent(code), + SortOrder = i, + IsRetired = false, + CreatedOn = now + }, cancellationToken, true); + } + } + + _seeded = true; + } + catch (Exception ex) + { + Logging.LogException(ex, "NERIS value-set seeding failed; the embedded snapshot still serves mapping and validation."); + } + finally + { + SeedLock.Release(); + } + } + + public async Task ResolveCrosswalkAsync(int departmentId, string setKey, string localSource, string localCode) + { + if (string.IsNullOrWhiteSpace(localCode)) + return null; + + var row = await _crosswalks.GetAsync(departmentId, ContractVersion, setKey, localSource, localCode.Trim()); + return row?.NerisCode; + } + + public async Task> GetCrosswalksAsync(int departmentId) + { + return (await _crosswalks.GetForDepartmentAsync(departmentId, ContractVersion) ?? Enumerable.Empty()).ToList(); + } + + public async Task RemoveCrosswalkAsync(int departmentId, string setKey, string localSource, string localCode, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(localCode)) + return false; + + var existing = await _crosswalks.GetAsync(departmentId, ContractVersion, setKey, localSource, localCode.Trim()); + if (existing == null) + return false; + + return await _crosswalks.DeleteAsync(existing, cancellationToken); + } + + public async Task SaveCrosswalkAsync(int departmentId, string userId, string setKey, string localSource, string localCode, string nerisCode, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(localCode)) throw new ArgumentException("A local code is required.", nameof(localCode)); + if (!NerisValueSetCatalog.Instance.Contains(setKey, nerisCode)) + throw new ArgumentException($"'{nerisCode}' is not a value of the NERIS {setKey} set at contract {ContractVersion}.", nameof(nerisCode)); + + var now = DateTime.UtcNow; + var existing = await _crosswalks.GetAsync(departmentId, ContractVersion, setKey, localSource, localCode.Trim()); + if (existing != null) + { + existing.NerisCode = nerisCode; + existing.ModifiedOn = now; + existing.RowVersion += 1; + return await _crosswalks.UpdateAsync(existing, cancellationToken, true); + } + + return await _crosswalks.InsertAsync(new RmsNerisCrosswalk + { + RmsNerisCrosswalkId = Guid.NewGuid().ToString(), + DepartmentId = departmentId, + ProtectionId = Guid.NewGuid().ToString(), + ContractVersion = ContractVersion, + SetKey = setKey, + LocalSource = localSource, + LocalCode = localCode.Trim(), + NerisCode = nerisCode, + IsDefault = false, + CreatedByUserId = userId, + CreatedOn = now, + ModifiedOn = now, + RowVersion = 1 + }, cancellationToken, true); + } + + /// "FIRE||STRUCTURE_FIRE||ROOM_AND_CONTENTS" reads as "Room And Contents"; the code stays the identity. + public static string Label(string code) + { + if (string.IsNullOrEmpty(code)) + return code; + + var leaf = code.Split(new[] { "||" }, StringSplitOptions.None).Last(); + var words = leaf.Split('_', StringSplitOptions.RemoveEmptyEntries).Select(w => w.Length <= 3 && w.All(char.IsUpper) && w.Length > 1 ? w : char.ToUpperInvariant(w[0]) + w.Substring(1).ToLowerInvariant()); + return string.Join(" ", words); + } + + public static string Parent(string code) + { + if (string.IsNullOrEmpty(code)) + return null; + + var index = code.LastIndexOf("||", StringComparison.Ordinal); + return index > 0 ? code.Substring(0, index) : null; + } + } +} diff --git a/Providers/Resgrid.Providers.Neris/NerisProviderModule.cs b/Providers/Resgrid.Providers.Neris/NerisProviderModule.cs new file mode 100644 index 00000000..cfcf29b3 --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/NerisProviderModule.cs @@ -0,0 +1,22 @@ +using Autofac; +using Resgrid.Model.Providers; + +namespace Resgrid.Providers.Neris +{ + /// + /// Registers the NERIS boundary (RMS plan section 5.5): profile/value sets/crosswalks, pure mapping, local + + /// remote validation, the HTTP client, and submission orchestration. Loaded after ServicesModule in every + /// process, like the other Resgrid.Providers.* modules. + /// + public class NerisProviderModule : Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.Register(c => new NerisApiClient()).As().SingleInstance(); + builder.RegisterType().As().InstancePerLifetimeScope(); + } + } +} diff --git a/Providers/Resgrid.Providers.Neris/NerisSubmissionService.cs b/Providers/Resgrid.Providers.Neris/NerisSubmissionService.cs new file mode 100644 index 00000000..01ee0751 --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/NerisSubmissionService.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Resgrid.Model; +using Resgrid.Model.Providers; + +namespace Resgrid.Providers.Neris +{ + /// + /// One delivery attempt (RMS plan sections 5.3/5.5): create on first delivery, update in place once the + /// destination holds the incident, status poll while it reviews. Holds no database transaction; the worker + /// persists the outcome and decides retry/backoff from the outcome kind. + /// + public class NerisSubmissionService : INerisSubmissionService + { + private readonly INerisApiClient _client; + private readonly INerisProfileService _profiles; + + public NerisSubmissionService(INerisApiClient client, INerisProfileService profiles) + { + _client = client; + _profiles = profiles; + } + + public async Task DeliverAsync(RmsNerisProfile profile, RmsSubmission submission, string existingNerisIncidentId, CancellationToken cancellationToken = default) + { + if (submission == null) throw new ArgumentNullException(nameof(submission)); + if (string.IsNullOrWhiteSpace(submission.PayloadJson)) + return new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Fatal, Message = "The submission carries no payload." }; + + var credential = await _profiles.GetCredentialAsync(profile); + if (!string.IsNullOrWhiteSpace(existingNerisIncidentId)) + { + var update = await _client.UpdateIncidentAsync(profile, credential, existingNerisIncidentId, submission.PayloadJson, cancellationToken); + if (update.Kind == NerisOutcomeKind.Fatal && update.StatusCode == 404) + return await _client.CreateIncidentAsync(profile, credential, submission.PayloadJson, cancellationToken); + return update; + } + + return await _client.CreateIncidentAsync(profile, credential, submission.PayloadJson, cancellationToken); + } + + public async Task CheckStatusAsync(RmsNerisProfile profile, string nerisIncidentId, CancellationToken cancellationToken = default) + { + var credential = await _profiles.GetCredentialAsync(profile); + return await _client.GetStatusAsync(profile, credential, nerisIncidentId, cancellationToken); + } + } +} diff --git a/Providers/Resgrid.Providers.Neris/NerisValidationService.cs b/Providers/Resgrid.Providers.Neris/NerisValidationService.cs new file mode 100644 index 00000000..7c609686 --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/NerisValidationService.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Resgrid.Model; +using Resgrid.Model.Providers; + +namespace Resgrid.Providers.Neris +{ + /// + /// Local validation of an incident report against the pinned contract (RMS plan section 5.5): requiredness of + /// the payload's mandatory fields, value-set membership, identifier shapes, and time sequence. Remote validation + /// goes through the client's validate endpoint and is folded into the same issue shape. Local rules mirror the + /// contract's required lists so the common 95% of rejections surface before a submission is queued. + /// + public class NerisValidationService : INerisValidationService + { + public static readonly Regex DepartmentIdPattern = new Regex(@"^FD\d{8}$", RegexOptions.Compiled); + public static readonly Regex UnitIdPattern = new Regex(@"^FD\d{8}S[A-Z\d]{3}U[A-Z\d]{3}$", RegexOptions.Compiled); + public static readonly Regex AidDepartmentIdPattern = new Regex(@"^(FD|FM)\d{8}$", RegexOptions.Compiled); + + private readonly INerisApiClient _client; + private readonly INerisProfileService _profiles; + + public NerisValidationService(INerisApiClient client, INerisProfileService profiles) + { + _client = client; + _profiles = profiles; + } + + public List ValidateLocal(NerisIncidentSnapshot snapshot, RmsNerisProfile profile) + { + var issues = new List(); + if (snapshot?.Report == null) + return issues; + + var report = snapshot.Report; + var catalog = NerisValueSetCatalog.Instance; + void Add(string rule, RmsValidationSeverity severity, string path, string message) + { + issues.Add(new RmsValidationIssue + { + RmsValidationIssueId = Guid.NewGuid().ToString(), + DepartmentId = report.DepartmentId, + RecordId = report.RmsIncidentReportId, + ProfileVersion = catalog.ContractVersion, + RuleKey = rule, + Severity = (int)severity, + FieldPath = path, + Message = message, + Source = (int)RmsValidationSource.Local, + CreatedOn = DateTime.UtcNow + }); + } + + // base.* + if (profile == null || string.IsNullOrWhiteSpace(profile.NerisEntityId)) + Add("neris.profile.entity", RmsValidationSeverity.Error, "base.department_neris_id", "The department has no NERIS entity ID configured."); + else if (!DepartmentIdPattern.IsMatch(profile.NerisEntityId)) + Add("neris.profile.entity.shape", RmsValidationSeverity.Error, "base.department_neris_id", "The NERIS entity ID must look like FD12345678."); + + if (string.IsNullOrWhiteSpace(report.IncidentNumber)) + Add("neris.base.incident_number", RmsValidationSeverity.Error, "base.incident_number", "An incident number is required."); + + if (snapshot.Location == null || (string.IsNullOrWhiteSpace(snapshot.Location.Street) && string.IsNullOrWhiteSpace(snapshot.Location.AddressText) && snapshot.Location.Latitude == null)) + Add("neris.base.location", RmsValidationSeverity.Error, "base.location", "A location (street, address text, or coordinates) is required."); + + if (snapshot.Location != null) + { + if (!string.IsNullOrWhiteSpace(snapshot.Location.State) && !catalog.Contains("state", snapshot.Location.State)) + Add("neris.location.state", RmsValidationSeverity.Error, "base.location.state", $"'{snapshot.Location.State}' is not a NERIS state/territory code."); + if (!string.IsNullOrWhiteSpace(snapshot.Location.Country) && !catalog.Contains("country", snapshot.Location.Country)) + Add("neris.location.country", RmsValidationSeverity.Error, "base.location.country", $"'{snapshot.Location.Country}' is not a NERIS country code."); + if (!string.IsNullOrWhiteSpace(snapshot.Location.PlaceType) && !catalog.Contains("location_place", snapshot.Location.PlaceType)) + Add("neris.location.place_type", RmsValidationSeverity.Error, "base.location.place_type", $"'{snapshot.Location.PlaceType}' is not a NERIS place type."); + if (snapshot.Location.Latitude.HasValue != snapshot.Location.Longitude.HasValue) + Add("neris.location.point", RmsValidationSeverity.Error, "base.point", "Latitude and longitude must both be present."); + if (snapshot.Location.Latitude.HasValue && (Math.Abs(snapshot.Location.Latitude.Value) > 90 || Math.Abs(snapshot.Location.Longitude.Value) > 180)) + Add("neris.location.point.range", RmsValidationSeverity.Error, "base.point", "Coordinates are out of range."); + } + + // incident_types + if (snapshot.Types.Count == 0) + Add("neris.incident_types.required", RmsValidationSeverity.Error, "incident_types", "At least one incident type is required."); + if (snapshot.Types.Count > 0 && snapshot.Types.Count(t => t.IsPrimary) != 1) + Add("neris.incident_types.primary", RmsValidationSeverity.Error, "incident_types", "Exactly one incident type must be marked primary."); + foreach (var type in snapshot.Types.Where(t => !catalog.Contains("incident_type", t.TypeCode))) + Add("neris.incident_types.code", RmsValidationSeverity.Error, "incident_types", $"'{type.TypeCode}' is not a NERIS incident type at contract {catalog.ContractVersion}."); + + foreach (var modifier in snapshot.SpecialModifiers.Where(m => !catalog.Contains("special_modifier", m))) + Add("neris.special_modifiers.code", RmsValidationSeverity.Error, "special_modifiers", $"'{modifier}' is not a NERIS special modifier."); + + // dispatch.* + if (!report.CallCreatedOn.HasValue) + Add("neris.dispatch.call_create", RmsValidationSeverity.Error, "dispatch.call_create", "The call create time is required."); + if (!report.CallAnsweredOn.HasValue) + Add("neris.dispatch.call_answered", RmsValidationSeverity.Error, "dispatch.call_answered", "The call answered time is required."); + if (!report.CallArrivalOn.HasValue) + Add("neris.dispatch.call_arrival", RmsValidationSeverity.Error, "dispatch.call_arrival", "The first arrival time is required."); + if (report.CallCreatedOn.HasValue && report.CallArrivalOn.HasValue && report.CallArrivalOn < report.CallCreatedOn) + Add("neris.dispatch.sequence", RmsValidationSeverity.Error, "dispatch.call_arrival", "First arrival cannot be before the call was created."); + if (report.CallCreatedOn.HasValue && report.IncidentClearedOn.HasValue && report.IncidentClearedOn < report.CallCreatedOn) + Add("neris.dispatch.clear_sequence", RmsValidationSeverity.Error, "dispatch.incident_clear", "Incident clear cannot be before the call was created."); + + // unit_responses + if (snapshot.Units.Count == 0) + Add("neris.unit_responses.required", RmsValidationSeverity.Warning, "dispatch.unit_responses", "No unit responses are recorded."); + var index = 0; + foreach (var unit in snapshot.Units.OrderBy(u => u.Ordinal)) + { + var path = $"dispatch.unit_responses[{index++}]"; + if (!string.IsNullOrWhiteSpace(unit.UnitNerisId) && !UnitIdPattern.IsMatch(unit.UnitNerisId)) + Add("neris.unit.id.shape", RmsValidationSeverity.Error, path + ".unit_neris_id", $"Unit NERIS ID '{unit.UnitNerisId}' must look like FD12345678S001U003."); + if (string.IsNullOrWhiteSpace(unit.UnitNerisId) && string.IsNullOrWhiteSpace(unit.UnitNameSnapshot)) + Add("neris.unit.identity", RmsValidationSeverity.Error, path, "A unit response needs a NERIS unit ID or a reported unit ID."); + if (!string.IsNullOrWhiteSpace(unit.ResponseMode) && !catalog.Contains("response_mode", unit.ResponseMode)) + Add("neris.unit.response_mode", RmsValidationSeverity.Error, path + ".response_mode", $"'{unit.ResponseMode}' is not a NERIS response mode."); + if (!InOrder(unit.DispatchedOn, unit.EnrouteOn, unit.OnSceneOn, unit.ClearedOn)) + Add("neris.unit.sequence", RmsValidationSeverity.Error, path, $"Unit {unit.UnitNameSnapshot}: dispatch, en route, on scene and clear must be in time order."); + } + + // aids + foreach (var aid in snapshot.Aids) + { + if (aid.IsNonFireDepartment) + { + if (!catalog.Contains("aid_nonfd", aid.NonFdType)) + Add("neris.aid.nonfd", RmsValidationSeverity.Error, "nonfd_aids", $"'{aid.NonFdType}' is not a NERIS non-fire-department aid type."); + continue; + } + if (!catalog.Contains("aid_type", aid.AidType)) + Add("neris.aid.type", RmsValidationSeverity.Error, "aids", $"'{aid.AidType}' is not a NERIS aid type."); + if (!catalog.Contains("aid_direction", aid.Direction)) + Add("neris.aid.direction", RmsValidationSeverity.Error, "aids", $"'{aid.Direction}' is not a NERIS aid direction."); + if (string.IsNullOrWhiteSpace(aid.CounterpartNerisId) || !AidDepartmentIdPattern.IsMatch(aid.CounterpartNerisId)) + Add("neris.aid.counterpart", RmsValidationSeverity.Error, "aids", "Each aid entry needs the counterpart's NERIS ID (FD or FM followed by eight digits)."); + } + + foreach (var tactic in snapshot.Tactics.Where(t => !catalog.Contains("action_tactic", t.TacticCode))) + Add("neris.tactic.code", RmsValidationSeverity.Error, "actions_tactics", $"'{tactic.TacticCode}' is not a NERIS action/tactic."); + + return issues; + } + + public async Task> ValidateRemoteAsync(RmsNerisProfile profile, string payloadJson, CancellationToken cancellationToken = default) + { + var credential = await _profiles.GetCredentialAsync(profile); + var outcome = await _client.ValidateAsync(profile, credential, payloadJson, cancellationToken); + return ToIssues(outcome, profile?.DepartmentId ?? 0, null); + } + + /// Destination errors become issues of source Destination; a transient failure is one Warning so the author knows validation did not run. + public static List ToIssues(NerisSubmissionOutcome outcome, int departmentId, string recordId) + { + var now = DateTime.UtcNow; + var issues = new List(); + if (outcome == null) + return issues; + + if (outcome.Kind == NerisOutcomeKind.Rejected) + { + foreach (var error in outcome.Errors) + { + issues.Add(new RmsValidationIssue + { + RmsValidationIssueId = Guid.NewGuid().ToString(), DepartmentId = departmentId, RecordId = recordId, RuleKey = "neris.destination." + (error.Code ?? "error"), + Severity = (int)RmsValidationSeverity.Error, FieldPath = error.FieldPath, Message = error.Message, Source = (int)RmsValidationSource.Destination, CreatedOn = now + }); + } + } + else if (outcome.Kind == NerisOutcomeKind.Transient || outcome.Kind == NerisOutcomeKind.Fatal) + { + issues.Add(new RmsValidationIssue + { + RmsValidationIssueId = Guid.NewGuid().ToString(), DepartmentId = departmentId, RecordId = recordId, RuleKey = "neris.destination.unavailable", + Severity = (int)RmsValidationSeverity.Warning, FieldPath = null, Message = outcome.Message ?? "NERIS validation could not run.", Source = (int)RmsValidationSource.Destination, CreatedOn = now + }); + } + + return issues; + } + + private static bool InOrder(params DateTime?[] times) + { + DateTime? previous = null; + foreach (var time in times) + { + if (!time.HasValue) + continue; + if (previous.HasValue && time.Value < previous.Value) + return false; + previous = time; + } + return true; + } + } +} diff --git a/Providers/Resgrid.Providers.Neris/NerisValueSetCatalog.cs b/Providers/Resgrid.Providers.Neris/NerisValueSetCatalog.cs new file mode 100644 index 00000000..f46c5acb --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/NerisValueSetCatalog.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Newtonsoft.Json.Linq; +using Resgrid.Model; + +namespace Resgrid.Providers.Neris +{ + /// + /// The pinned value-set snapshot (Contract/neris-value-sets-*.json), embedded so mapping and validation work + /// without a database or the network. Codes are exactly the contract's enum values; the catalog never invents + /// or renames one. + /// + public sealed class NerisValueSetCatalog + { + private static readonly Lazy Embedded = new Lazy(LoadEmbedded); + + private readonly Dictionary _sets; + + private NerisValueSetCatalog(string contractVersion, string pinnedOn, Dictionary sets) + { + ContractVersion = contractVersion; + PinnedOn = pinnedOn; + _sets = sets; + } + + public static NerisValueSetCatalog Instance => Embedded.Value; + + public string ContractVersion { get; } + public string PinnedOn { get; } + public IReadOnlyList SetKeys => _sets.Keys.OrderBy(k => k, StringComparer.Ordinal).ToList(); + + public NerisValueSet Get(string setKey) + { + return setKey != null && _sets.TryGetValue(setKey, out var set) ? set : null; + } + + public bool Contains(string setKey, string code) + { + var set = Get(setKey); + return set != null && code != null && set.Codes.Contains(code, StringComparer.Ordinal); + } + + public static NerisValueSetCatalog Parse(string json) + { + var root = JObject.Parse(json); + var sets = new Dictionary(StringComparer.Ordinal); + foreach (var property in ((JObject)root["sets"]).Properties()) + { + var codes = ((JArray)property.Value["codes"]).Select(t => (string)t).ToList(); + sets[property.Name] = new NerisValueSet { SetKey = property.Name, SchemaName = (string)property.Value["schema"], Codes = codes }; + } + + return new NerisValueSetCatalog((string)root["contract_version"], (string)root["pinned_on"], sets); + } + + private static NerisValueSetCatalog LoadEmbedded() + { + var assembly = typeof(NerisValueSetCatalog).Assembly; + using var stream = assembly.GetManifestResourceStream("Resgrid.Providers.Neris.Contract.value-sets.json") + ?? throw new InvalidOperationException("The NERIS value-set snapshot is not embedded in the provider assembly."); + using var reader = new StreamReader(stream); + return Parse(reader.ReadToEnd()); + } + } +} diff --git a/Providers/Resgrid.Providers.Neris/Resgrid.Providers.Neris.csproj b/Providers/Resgrid.Providers.Neris/Resgrid.Providers.Neris.csproj new file mode 100644 index 00000000..d202d7a2 --- /dev/null +++ b/Providers/Resgrid.Providers.Neris/Resgrid.Providers.Neris.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + latest + disable + NERIS reporting destination: pinned OpenAPI contract, value sets, mapping, validation, and submission client (RMS plan section 5.5). + + + + + + + + + + + + + + + + + + + + diff --git a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs index d47935a4..60514564 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/DataModule.cs @@ -317,6 +317,21 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + // NERIS incident report aggregate (RMS-2, registry M0164-M0166) + 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(); + 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 418b5f66..311e9da8 100644 --- a/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs +++ b/Repositories/Resgrid.Repositories.DataRepository/Modules/TestingDataModule.cs @@ -270,6 +270,21 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); builder.RegisterType().As().InstancePerLifetimeScope(); + // NERIS incident report aggregate (RMS-2, registry M0164-M0166) + 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(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); + builder.RegisterType().As().InstancePerLifetimeScope(); } } } diff --git a/Repositories/Resgrid.Repositories.DataRepository/RmsIncidentRepositories.cs b/Repositories/Resgrid.Repositories.DataRepository/RmsIncidentRepositories.cs new file mode 100644 index 00000000..9c043978 --- /dev/null +++ b/Repositories/Resgrid.Repositories.DataRepository/RmsIncidentRepositories.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Dapper; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Connection; +using Resgrid.Model.Repositories.Queries; +using Resgrid.Repositories.DataRepository.Configs; + +namespace Resgrid.Repositories.DataRepository +{ + /// + /// NERIS incident report aggregate (registry M0164–M0166). Same dual-dialect style as RmsRepositories: every + /// query starts at DepartmentId, drafts are RevisionId IS NULL, revision copies are immutable. + /// + public class RmsIncidentReportsRepository : RmsRepositoryBase, IRmsIncidentReportsRepository + { + public RmsIncidentReportsRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) { } + + public Task GetByIdForDepartmentAsync(int departmentId, string reportId) + { + return QueryFirstOrDefaultAsync( + $"SELECT * FROM {Tbl("RmsIncidentReports")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RmsIncidentReportId")} = {P}ReportId", + new { DepartmentId = departmentId, ReportId = reportId }); + } + + public Task GetByCallAsync(int departmentId, int callId, string reportingEntityId) + { + return QueryFirstOrDefaultAsync( + $"SELECT * FROM {Tbl("RmsIncidentReports")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("CallId")} = {P}CallId AND {Col("ReportingEntityId")} = {P}ReportingEntityId AND {Col("DeletedOn")} IS NULL", + new { DepartmentId = departmentId, CallId = callId, ReportingEntityId = reportingEntityId ?? string.Empty }); + } + + public Task> GetByCallAnyEntityAsync(int departmentId, int callId) + { + return QueryAsync( + $"SELECT * FROM {Tbl("RmsIncidentReports")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("CallId")} = {P}CallId AND {Col("DeletedOn")} IS NULL ORDER BY {Col("CreatedOn")}", + new { DepartmentId = departmentId, CallId = callId }); + } + + public Task GetByIdempotencyKeyAsync(int departmentId, string idempotencyKey) + { + return QueryFirstOrDefaultAsync( + $"SELECT * FROM {Tbl("RmsIncidentReports")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("IdempotencyKey")} = {P}Key", + new { DepartmentId = departmentId, Key = idempotencyKey }); + } + + public Task GetByNerisIncidentIdAsync(int departmentId, string nerisIncidentId) + { + return QueryFirstOrDefaultAsync( + $"SELECT * FROM {Tbl("RmsIncidentReports")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("NerisIncidentId")} = {P}NerisId AND {Col("DeletedOn")} IS NULL", + new { DepartmentId = departmentId, NerisId = nerisIncidentId }); + } + + private (string where, DynamicParameters parameters) Filter(int departmentId, RmsIncidentReportQuery query) + { + var where = new StringBuilder($"{Col("DepartmentId")} = {P}DepartmentId AND {Col("DeletedOn")} IS NULL"); + var parameters = new DynamicParameters(); + parameters.Add("DepartmentId", departmentId); + + if (query.States != null && query.States.Count > 0) + { + where.Append($" AND {InList("State", "States")}"); + parameters.Add("States", InListValue(query.States)); + } + if (query.Year.HasValue) + { + where.Append($" AND {YearOf(Col("CreatedOn"))} = {P}Year"); + parameters.Add("Year", query.Year.Value); + } + if (query.CallId.HasValue) + { + where.Append($" AND {Col("CallId")} = {P}CallId"); + parameters.Add("CallId", query.CallId.Value); + } + if (!string.IsNullOrWhiteSpace(query.OwnerUserId)) + { + where.Append($" AND {Col("OwnerUserId")} = {P}OwnerUserId"); + parameters.Add("OwnerUserId", query.OwnerUserId); + } + if (query.StationGroupId.HasValue) + { + where.Append($" AND {Col("StationGroupId")} = {P}StationGroupId"); + parameters.Add("StationGroupId", query.StationGroupId.Value); + } + + return (where.ToString(), parameters); + } + + public Task> QueryAsync(int departmentId, RmsIncidentReportQuery query) + { + query ??= new RmsIncidentReportQuery(); + var (where, parameters) = Filter(departmentId, query); + parameters.Add("Skip", Math.Max(0, query.Skip)); + parameters.Add("Take", Math.Clamp(query.Take, 1, 10000)); + return QueryAsync( + $"SELECT * FROM {Tbl("RmsIncidentReports")} WHERE {where} ORDER BY {Col("CreatedOn")} DESC {Paging()}", parameters); + } + + public Task CountAsync(int departmentId, RmsIncidentReportQuery query) + { + var (where, parameters) = Filter(departmentId, query ?? new RmsIncidentReportQuery()); + return ScalarAsync($"SELECT COUNT(1) FROM {Tbl("RmsIncidentReports")} WHERE {where}", parameters); + } + + public Task> GetYearsAsync(int departmentId) + { + return QueryAsync( + $"SELECT DISTINCT {YearOf(Col("CreatedOn"))} AS Y FROM {Tbl("RmsIncidentReports")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("DeletedOn")} IS NULL ORDER BY Y DESC", + new { DepartmentId = departmentId }); + } + + public Task GetMaxRecordNumberSequenceAsync(int departmentId, string numberPrefix) + { + // Numbers are "{prefix}-{sequence}"; the sequence is the trailing numeric segment. + return ScalarAsync( + IsPostgres + ? $"SELECT COALESCE(MAX(CAST(SUBSTRING({Col("RecordNumber")} FROM '[0-9]+$') AS INTEGER)), 0) FROM {Tbl("RmsIncidentReports")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RecordNumber")} LIKE {P}Prefix" + : $"SELECT ISNULL(MAX(CAST(RIGHT({Col("RecordNumber")}, CHARINDEX('-', REVERSE({Col("RecordNumber")})) - 1) AS INT)), 0) FROM {Tbl("RmsIncidentReports")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RecordNumber")} LIKE {P}Prefix", + new { DepartmentId = departmentId, Prefix = numberPrefix + "-%" }); + } + + public async Task TryBumpRowVersionAsync(int departmentId, string reportId, long expectedRowVersion, CancellationToken cancellationToken = default) + { + var affected = await ExecuteAsync( + $"UPDATE {Tbl("RmsIncidentReports")} SET {Col("RowVersion")} = {Col("RowVersion")} + 1 WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RmsIncidentReportId")} = {P}ReportId AND {Col("RowVersion")} = {P}Expected", + new { DepartmentId = departmentId, ReportId = reportId, Expected = expectedRowVersion }, cancellationToken); + return affected == 1; + } + } + + /// Draft/revision child rows: one working draft (RevisionId null) and immutable revision copies. + public abstract class RmsIncidentChildRepository : RmsRepositoryBase, IRmsIncidentChildRepository where T : class, IEntity + { + private readonly string _table; + private readonly string _orderBy; + + protected RmsIncidentChildRepository(string table, string orderBy, IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) + { + _table = table; + _orderBy = orderBy; + } + + public Task> GetForRecordAsync(int departmentId, string recordId, string revisionId) + { + var revisionClause = revisionId == null ? $"{Col("RevisionId")} IS NULL" : $"{Col("RevisionId")} = {P}RevisionId"; + return QueryAsync( + $"SELECT * FROM {Tbl(_table)} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RecordId")} = {P}RecordId AND {revisionClause} ORDER BY {Col(_orderBy)}", + new { DepartmentId = departmentId, RecordId = recordId, RevisionId = revisionId }); + } + + public Task DeleteDraftForRecordAsync(int departmentId, string recordId, CancellationToken cancellationToken = default) + { + return ExecuteAsync( + $"DELETE FROM {Tbl(_table)} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RecordId")} = {P}RecordId AND {Col("RevisionId")} IS NULL", + new { DepartmentId = departmentId, RecordId = recordId }, cancellationToken); + } + } + + public class RmsSourceFactsRepository : RmsIncidentChildRepository, IRmsSourceFactsRepository + { + public RmsSourceFactsRepository(IConnectionProvider c, SqlConfiguration s, IUnitOfWork u, IQueryFactory q) : base("RmsSourceFacts", "FactKey", c, s, u, q) { } + } + + public class RmsUnitResponsesRepository : RmsIncidentChildRepository, IRmsUnitResponsesRepository + { + public RmsUnitResponsesRepository(IConnectionProvider c, SqlConfiguration s, IUnitOfWork u, IQueryFactory q) : base("RmsUnitResponses", "Ordinal", c, s, u, q) { } + } + + public class RmsIncidentTypesRepository : RmsIncidentChildRepository, IRmsIncidentTypesRepository + { + public RmsIncidentTypesRepository(IConnectionProvider c, SqlConfiguration s, IUnitOfWork u, IQueryFactory q) : base("RmsIncidentTypes", "Ordinal", c, s, u, q) { } + } + + public class RmsActionTacticsRepository : RmsIncidentChildRepository, IRmsActionTacticsRepository + { + public RmsActionTacticsRepository(IConnectionProvider c, SqlConfiguration s, IUnitOfWork u, IQueryFactory q) : base("RmsActionTactics", "Ordinal", c, s, u, q) { } + } + + public class RmsAidsRepository : RmsIncidentChildRepository, IRmsAidsRepository + { + public RmsAidsRepository(IConnectionProvider c, SqlConfiguration s, IUnitOfWork u, IQueryFactory q) : base("RmsAids", "Ordinal", c, s, u, q) { } + } + + public class RmsLocationsRepository : RmsIncidentChildRepository, IRmsLocationsRepository + { + public RmsLocationsRepository(IConnectionProvider c, SqlConfiguration s, IUnitOfWork u, IQueryFactory q) : base("RmsLocations", "CreatedOn", c, s, u, q) { } + } + + public class RmsNarrativesRepository : RmsIncidentChildRepository, IRmsNarrativesRepository + { + public RmsNarrativesRepository(IConnectionProvider c, SqlConfiguration s, IUnitOfWork u, IQueryFactory q) : base("RmsNarratives", "CreatedOn", c, s, u, q) { } + } + + public class RmsValidationIssuesRepository : RmsRepositoryBase, IRmsValidationIssuesRepository + { + public RmsValidationIssuesRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) { } + + public Task> GetForRecordAsync(int departmentId, string recordId) + { + return QueryAsync( + $"SELECT * FROM {Tbl("RmsValidationIssues")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RecordId")} = {P}RecordId AND {Col("ResolvedOn")} IS NULL ORDER BY {Col("Severity")}, {Col("FieldPath")}", + new { DepartmentId = departmentId, RecordId = recordId }); + } + + public async Task ReplaceForRecordAsync(int departmentId, string recordId, RmsValidationSource source, IEnumerable issues, CancellationToken cancellationToken = default) + { + await ExecuteAsync( + $"DELETE FROM {Tbl("RmsValidationIssues")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RecordId")} = {P}RecordId AND {Col("Source")} = {P}Source", + new { DepartmentId = departmentId, RecordId = recordId, Source = (int)source }, cancellationToken); + + foreach (var issue in issues ?? Enumerable.Empty()) + await InsertAsync(issue, cancellationToken, true); + } + } + + public class RmsSubmissionsRepository : RmsRepositoryBase, IRmsSubmissionsRepository + { + public RmsSubmissionsRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) { } + + public Task GetByIdForDepartmentAsync(int departmentId, string submissionId) + { + return QueryFirstOrDefaultAsync( + $"SELECT * FROM {Tbl("RmsSubmissions")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RmsSubmissionId")} = {P}Id", + new { DepartmentId = departmentId, Id = submissionId }); + } + + public Task> GetForRecordAsync(int departmentId, string recordId) + { + return QueryAsync( + $"SELECT * FROM {Tbl("RmsSubmissions")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RecordId")} = {P}RecordId ORDER BY {Col("QueuedOn")} DESC", + new { DepartmentId = departmentId, RecordId = recordId }); + } + + public Task GetByIdempotencyKeyAsync(string idempotencyKey) + { + return QueryFirstOrDefaultAsync( + $"SELECT * FROM {Tbl("RmsSubmissions")} WHERE {Col("IdempotencyKey")} = {P}Key", + new { Key = idempotencyKey }); + } + + public async Task> ClaimDueBatchAsync(string leaseOwner, TimeSpan leaseDuration, int batchSize, DateTime utcNow, CancellationToken cancellationToken = default) + { + var claimable = new[] { (int)RmsSubmissionState.Queued, (int)RmsSubmissionState.AwaitingDestination }; + var candidates = (await QueryAsync( + $"SELECT * FROM {Tbl("RmsSubmissions")} WHERE {InList("State", "States")} AND ({Col("NextAttemptOn")} IS NULL OR {Col("NextAttemptOn")} <= {P}Now) " + + $"AND ({Col("LeaseExpiresOn")} IS NULL OR {Col("LeaseExpiresOn")} < {P}Now) ORDER BY {Col("QueuedOn")} {Paging()}", + new { States = InListValue(claimable), Now = utcNow, Skip = 0, Take = Math.Clamp(batchSize, 1, 500) }, cancellationToken)).ToList(); + + var claimed = new List(); + var leaseUntil = utcNow.Add(leaseDuration); + foreach (var candidate in candidates) + { + // Single-flight: the row version guards two workers racing for the same submission. + var affected = await ExecuteAsync( + $"UPDATE {Tbl("RmsSubmissions")} SET {Col("LeaseOwner")} = {P}Owner, {Col("LeaseExpiresOn")} = {P}Until, {Col("RowVersion")} = {Col("RowVersion")} + 1 " + + $"WHERE {Col("RmsSubmissionId")} = {P}Id AND {Col("RowVersion")} = {P}Version", + new { Owner = leaseOwner, Until = leaseUntil, Id = candidate.RmsSubmissionId, Version = candidate.RowVersion }, cancellationToken); + if (affected != 1) + continue; + + candidate.LeaseOwner = leaseOwner; + candidate.LeaseExpiresOn = leaseUntil; + candidate.RowVersion += 1; + claimed.Add(candidate); + } + + return claimed; + } + + public Task CountByStateAsync(int state) + { + return ScalarAsync($"SELECT COUNT(1) FROM {Tbl("RmsSubmissions")} WHERE {Col("State")} = {P}State", new { State = state }); + } + + public Task SupersedeOpenForRecordAsync(int departmentId, string recordId, string exceptSubmissionId, DateTime utcNow, CancellationToken cancellationToken = default) + { + var open = new[] { (int)RmsSubmissionState.Queued, (int)RmsSubmissionState.InFlight, (int)RmsSubmissionState.AwaitingDestination, (int)RmsSubmissionState.Rejected, (int)RmsSubmissionState.Failed }; + return ExecuteAsync( + $"UPDATE {Tbl("RmsSubmissions")} SET {Col("State")} = {P}Superseded, {Col("CompletedOn")} = {P}Now, {Col("ModifiedOn")} = {P}Now, {Col("RowVersion")} = {Col("RowVersion")} + 1 " + + $"WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RecordId")} = {P}RecordId AND {InList("State", "States")} AND {Col("RmsSubmissionId")} <> {P}Except", + new { Superseded = (int)RmsSubmissionState.Superseded, Now = utcNow, DepartmentId = departmentId, RecordId = recordId, States = InListValue(open), Except = exceptSubmissionId ?? string.Empty }, cancellationToken); + } + } + + public class RmsSignaturesRepository : RmsRepositoryBase, IRmsSignaturesRepository + { + public RmsSignaturesRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) { } + + public Task> GetForRecordAsync(int departmentId, string recordId) + { + return QueryAsync( + $"SELECT * FROM {Tbl("RmsSignatures")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RecordId")} = {P}RecordId ORDER BY {Col("SignedOn")}", + new { DepartmentId = departmentId, RecordId = recordId }); + } + + public Task GetForRevisionAsync(int departmentId, string revisionId, RmsSignatureIntent intent) + { + return QueryFirstOrDefaultAsync( + $"SELECT * FROM {Tbl("RmsSignatures")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("RevisionId")} = {P}RevisionId AND {Col("Intent")} = {P}Intent", + new { DepartmentId = departmentId, RevisionId = revisionId, Intent = (int)intent }); + } + } + + public class RmsNerisProfilesRepository : RmsRepositoryBase, IRmsNerisProfilesRepository + { + public RmsNerisProfilesRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) { } + + public Task GetByDepartmentIdAsync(int departmentId) + { + return QueryFirstOrDefaultAsync( + $"SELECT * FROM {Tbl("RmsNerisProfiles")} WHERE {Col("DepartmentId")} = {P}DepartmentId", + new { DepartmentId = departmentId }); + } + } + + public class RmsNerisValueSetsRepository : RmsRepositoryBase, IRmsNerisValueSetsRepository + { + public RmsNerisValueSetsRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) { } + + public Task> GetSetAsync(string contractVersion, string setKey) + { + return QueryAsync( + $"SELECT * FROM {Tbl("RmsNerisValueSets")} WHERE {Col("ContractVersion")} = {P}Version AND {Col("SetKey")} = {P}SetKey ORDER BY {Col("SortOrder")}", + new { Version = contractVersion, SetKey = setKey }); + } + + public Task CountForVersionAsync(string contractVersion) + { + return ScalarAsync($"SELECT COUNT(1) FROM {Tbl("RmsNerisValueSets")} WHERE {Col("ContractVersion")} = {P}Version", new { Version = contractVersion }); + } + + public async Task ExistsAsync(string contractVersion, string setKey, string code) + { + return await ScalarAsync( + $"SELECT COUNT(1) FROM {Tbl("RmsNerisValueSets")} WHERE {Col("ContractVersion")} = {P}Version AND {Col("SetKey")} = {P}SetKey AND {Col("Code")} = {P}Code", + new { Version = contractVersion, SetKey = setKey, Code = code }) > 0; + } + } + + public class RmsNerisCrosswalksRepository : RmsRepositoryBase, IRmsNerisCrosswalksRepository + { + public RmsNerisCrosswalksRepository(IConnectionProvider connectionProvider, SqlConfiguration sqlConfiguration, IUnitOfWork unitOfWork, IQueryFactory queryFactory) + : base(connectionProvider, sqlConfiguration, unitOfWork, queryFactory) { } + + public Task> GetForDepartmentAsync(int departmentId, string contractVersion) + { + return QueryAsync( + $"SELECT * FROM {Tbl("RmsNerisCrosswalks")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("ContractVersion")} = {P}Version AND {Col("DeletedOn")} IS NULL ORDER BY {Col("SetKey")}, {Col("LocalSource")}, {Col("LocalCode")}", + new { DepartmentId = departmentId, Version = contractVersion }); + } + + public Task GetAsync(int departmentId, string contractVersion, string setKey, string localSource, string localCode) + { + return QueryFirstOrDefaultAsync( + $"SELECT * FROM {Tbl("RmsNerisCrosswalks")} WHERE {Col("DepartmentId")} = {P}DepartmentId AND {Col("ContractVersion")} = {P}Version AND {Col("SetKey")} = {P}SetKey AND {Col("LocalSource")} = {P}LocalSource AND {Col("LocalCode")} = {P}LocalCode AND {Col("DeletedOn")} IS NULL", + new { DepartmentId = departmentId, Version = contractVersion, SetKey = setKey, LocalSource = localSource, LocalCode = localCode }); + } + } +} diff --git a/Resgrid.sln b/Resgrid.sln index ef082b32..6202a8fb 100644 --- a/Resgrid.sln +++ b/Resgrid.sln @@ -124,6 +124,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resgrid.Search", "Core\Resg EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resgrid.Providers.Scanning", "Providers\Resgrid.Providers.Scanning\Resgrid.Providers.Scanning.csproj", "{7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Resgrid.Providers.Neris", "Providers\Resgrid.Providers.Neris\Resgrid.Providers.Neris.csproj", "{3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Azure|Any CPU = Azure|Any CPU @@ -1768,76 +1770,112 @@ Global {19AD2004-864D-4E68-8B3F-CE7116BD84AF}.Staging|x64.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Azure|Any CPU.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Azure|Any CPU.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Azure|Any CPU.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Azure|Any CPU.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Azure|Any CPU.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Azure|Any CPU.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Azure|x86.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Azure|x86.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Azure|x86.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Azure|x86.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Azure|x86.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Azure|x86.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Azure|x64.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Azure|x64.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Azure|x64.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Azure|x64.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Azure|x64.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Azure|x64.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Cloud|Any CPU.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Cloud|Any CPU.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Cloud|Any CPU.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Cloud|Any CPU.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Cloud|Any CPU.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Cloud|Any CPU.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Cloud|x86.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Cloud|x86.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Cloud|x86.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Cloud|x86.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Cloud|x86.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Cloud|x86.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Cloud|x64.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Cloud|x64.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Cloud|x64.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Cloud|x64.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Cloud|x64.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Cloud|x64.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Debug|Any CPU.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Debug|x86.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Debug|x86.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Debug|x86.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Debug|x86.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Debug|x86.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Debug|x86.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Debug|x64.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Debug|x64.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Debug|x64.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Debug|x64.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Debug|x64.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Debug|x64.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Docker|Any CPU.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Docker|Any CPU.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Docker|Any CPU.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Docker|Any CPU.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Docker|Any CPU.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Docker|Any CPU.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Docker|x86.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Docker|x86.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Docker|x86.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Docker|x86.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Docker|x86.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Docker|x86.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Docker|x64.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Docker|x64.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Docker|x64.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Docker|x64.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Docker|x64.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Docker|x64.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Release|Any CPU.ActiveCfg = Release|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Release|Any CPU.Build.0 = Release|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Release|Any CPU.Build.0 = Release|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Release|Any CPU.Build.0 = Release|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Release|x86.ActiveCfg = Release|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Release|x86.ActiveCfg = Release|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Release|x86.ActiveCfg = Release|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Release|x86.Build.0 = Release|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Release|x86.Build.0 = Release|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Release|x86.Build.0 = Release|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Release|x64.ActiveCfg = Release|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Release|x64.ActiveCfg = Release|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Release|x64.ActiveCfg = Release|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Release|x64.Build.0 = Release|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Release|x64.Build.0 = Release|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Release|x64.Build.0 = Release|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Staging|Any CPU.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Staging|Any CPU.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Staging|Any CPU.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Staging|Any CPU.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Staging|Any CPU.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Staging|Any CPU.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Staging|x86.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Staging|x86.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Staging|x86.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Staging|x86.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Staging|x86.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Staging|x86.Build.0 = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Staging|x64.ActiveCfg = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Staging|x64.ActiveCfg = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Staging|x64.ActiveCfg = Debug|Any CPU {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4}.Staging|x64.Build.0 = Debug|Any CPU {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41}.Staging|x64.Build.0 = Debug|Any CPU + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92}.Staging|x64.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1891,6 +1929,7 @@ Global {19AD2004-864D-4E68-8B3F-CE7116BD84AF} = {53B024F9-E293-42F1-BA67-7F68C3F3C243} {62BCBAE8-C7A3-4C5B-80C4-7C7310CB71B4} = {D43D1D6B-66A9-4A57-9EA3-8DECC92FA583} {7D1F3C2A-5B8E-4E61-9A0C-3F2B6C9D8E41} = {D43D1D6B-66A9-4A57-9EA3-8DECC92FA583} + {3C9E5B7A-2F14-4D8C-9E6B-7A1D5C3F8B92} = {D43D1D6B-66A9-4A57-9EA3-8DECC92FA583} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {156116FF-243E-45E8-8717-DB72E95F56AF} diff --git a/Tests/Resgrid.Tests/Allocations/trigger-baseline.json b/Tests/Resgrid.Tests/Allocations/trigger-baseline.json index 206001a8..f168e909 100644 --- a/Tests/Resgrid.Tests/Allocations/trigger-baseline.json +++ b/Tests/Resgrid.Tests/Allocations/trigger-baseline.json @@ -57,5 +57,9 @@ "RecordFinalized": 104, "RecordAmended": 105, "RecordVoided": 106, - "RecordCancelled": 107 + "RecordCancelled": 107, + "RecordSubmissionQueued": 108, + "RecordSubmissionAccepted": 109, + "RecordSubmissionRejected": 110, + "RecordSubmissionFailed": 111 } diff --git a/Tests/Resgrid.Tests/Bootstrapper.cs b/Tests/Resgrid.Tests/Bootstrapper.cs index 5fd9c23a..afb49908 100644 --- a/Tests/Resgrid.Tests/Bootstrapper.cs +++ b/Tests/Resgrid.Tests/Bootstrapper.cs @@ -34,6 +34,7 @@ public static void Initialize() builder.RegisterModule(new ServicesModule()); builder.RegisterModule(new Resgrid.Search.SearchModule()); builder.RegisterModule(new Resgrid.Providers.Scanning.ScanningProviderModule()); + builder.RegisterModule(new Resgrid.Providers.Neris.NerisProviderModule()); builder.RegisterModule(new ProviderModule()); builder.RegisterModule(new EmailProviderModule()); builder.RegisterModule(new WorkerFrameworkModule()); diff --git a/Tests/Resgrid.Tests/Providers/NerisApiClientTests.cs b/Tests/Resgrid.Tests/Providers/NerisApiClientTests.cs new file mode 100644 index 00000000..f49571f5 --- /dev/null +++ b/Tests/Resgrid.Tests/Providers/NerisApiClientTests.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Providers.Neris; + +namespace Resgrid.Tests.Providers +{ + /// + /// Contract fixtures for the NERIS client (RMS plan section 5.5: validate, create, update, status, rejection, + /// throttling, retry classification) against a scripted handler, so the wire protocol of the pinned contract — + /// form token, bearer header, 201/204/422/429/500 replies — is exercised without the network. + /// + [TestFixture] + public class NerisApiClientTests + { + private ScriptedHandler _handler; + private NerisApiClient _client; + private RmsNerisProfile _profile; + private NerisCredential _credential; + + [SetUp] + public void SetUp() + { + _handler = new ScriptedHandler(); + _client = new NerisApiClient(new HttpClient(_handler)); + // A fresh RowVersion per test keeps the static token cache from bleeding between tests. + _profile = NerisMappingTests.Profile(); + _profile.RmsNerisProfileId = Guid.NewGuid().ToString(); + _credential = new NerisCredential { ClientId = "client", ClientSecret = "secret" }; + _handler.Token(HttpStatusCode.OK, "{\"access_token\":\"tok-1\",\"expires_in\":3600,\"token_type\":\"bearer\"}"); + } + + [Test] + public async Task Create_sends_the_payload_verbatim_with_a_bearer_token_and_reads_the_incident_id() + { + _handler.Reply("POST", "/v1/incident/FD24027000", HttpStatusCode.Created, "{\"neris_id\":\"FD24027000I2026000123\",\"incident_status\":{\"status\":\"SUBMITTED\",\"last_modified\":\"2026-09-03T14:00:00Z\",\"created_by\":\"api\"}}"); + + var outcome = await _client.CreateIncidentAsync(_profile, _credential, "{\"base\":{}}"); + + outcome.Kind.Should().Be(NerisOutcomeKind.Created); + outcome.ExternalId.Should().Be("FD24027000I2026000123"); + outcome.ExternalStatus.Should().Be("SUBMITTED"); + outcome.StatusCode.Should().Be(201); + outcome.ResponseJson.Should().Contain("neris_id"); + + var token = _handler.Requests[0]; + token.Method.Should().Be("POST"); + token.Path.Should().Be("/v1/token"); + token.Body.Should().Contain("grant_type=client_credentials").And.Contain("username=client").And.Contain("password=secret"); + + var create = _handler.Requests[1]; + create.Authorization.Should().Be("Bearer tok-1"); + create.Body.Should().Be("{\"base\":{}}"); + create.ContentType.Should().StartWith("application/json"); + } + + [Test] + public async Task The_token_is_reused_until_it_expires() + { + _handler.Reply("POST", "/v1/incident/FD24027000/validate", HttpStatusCode.NoContent, ""); + + (await _client.ValidateAsync(_profile, _credential, "{}")).Kind.Should().Be(NerisOutcomeKind.Accepted); + (await _client.ValidateAsync(_profile, _credential, "{}")).Kind.Should().Be(NerisOutcomeKind.Accepted); + + _handler.Requests.Should().HaveCount(3, "one token call, two validate calls"); + } + + [Test] + public async Task A_422_becomes_a_rejection_with_field_paths_and_no_payload_echo() + { + _handler.Reply("POST", "/v1/incident/FD24027000", (HttpStatusCode)422, + "{\"detail\":[{\"loc\":[\"body\",\"dispatch\",\"call_answered\"],\"msg\":\"Field required\",\"type\":\"missing\"},{\"loc\":[\"body\",\"incident_types\",0,\"type\"],\"msg\":\"Input should be a valid enumeration member\",\"type\":\"enum\"}]}"); + + var outcome = await _client.CreateIncidentAsync(_profile, _credential, "{\"base\":{}}"); + + outcome.Kind.Should().Be(NerisOutcomeKind.Rejected); + outcome.Errors.Should().HaveCount(2); + outcome.Errors[0].Code.Should().Be("missing"); + outcome.Errors[0].FieldPath.Should().Be("dispatch.call_answered"); + outcome.Errors[1].FieldPath.Should().Be("incident_types.0.type"); + outcome.Message.Should().Be("2 validation issue(s)."); + } + + [TestCase(429)] + [TestCase(500)] + [TestCase(503)] + public async Task Throttling_and_server_errors_are_transient(int status) + { + _handler.Reply("POST", "/v1/incident/FD24027000", (HttpStatusCode)status, "{\"error\":\"busy\"}"); + + var outcome = await _client.CreateIncidentAsync(_profile, _credential, "{}"); + + outcome.Kind.Should().Be(NerisOutcomeKind.Transient); + outcome.StatusCode.Should().Be(status); + } + + [Test] + public async Task A_refused_credential_is_fatal_and_drops_the_cached_token() + { + _handler.Reply("POST", "/v1/incident/FD24027000", HttpStatusCode.Unauthorized, "{\"detail\":\"expired\"}"); + + var outcome = await _client.CreateIncidentAsync(_profile, _credential, "{}"); + outcome.Kind.Should().Be(NerisOutcomeKind.Fatal); + + _handler.Reply("POST", "/v1/incident/FD24027000", HttpStatusCode.Created, "{\"neris_id\":\"X\",\"incident_status\":{\"status\":\"SUBMITTED\"}}"); + await _client.CreateIncidentAsync(_profile, _credential, "{}"); + _handler.Requests.FindAll(r => r.Path == "/v1/token").Should().HaveCount(2, "the token was fetched again after the refusal"); + } + + [Test] + public async Task A_token_refusal_is_fatal_without_calling_the_incident_endpoint() + { + _handler.Token(HttpStatusCode.OK, "{\"error\":\"invalid_client\"}"); + + var outcome = await _client.CreateIncidentAsync(_profile, _credential, "{}"); + + outcome.Kind.Should().Be(NerisOutcomeKind.Fatal); + outcome.Message.Should().Contain("invalid_client"); + _handler.Requests.Should().OnlyContain(r => r.Path == "/v1/token"); + } + + [Test] + public async Task Update_and_status_map_destination_states() + { + _handler.Reply("PUT", "/v1/incident/FD24027000/FD24027000I1", HttpStatusCode.OK, "{\"last_modified\":\"2026-09-03T15:00:00Z\"}"); + _handler.Reply("GET", "/v1/incident/FD24027000/FD24027000I1", HttpStatusCode.OK, "{\"neris_id\":\"FD24027000I1\",\"incident_status\":{\"status\":\"APPROVED\",\"last_modified\":\"x\",\"created_by\":\"y\"}}"); + + (await _client.UpdateIncidentAsync(_profile, _credential, "FD24027000I1", "{}")).Kind.Should().Be(NerisOutcomeKind.Updated); + + var status = await _client.GetStatusAsync(_profile, _credential, "FD24027000I1"); + status.Kind.Should().Be(NerisOutcomeKind.Accepted); + status.ExternalStatus.Should().Be("APPROVED"); + + _handler.Reply("GET", "/v1/incident/FD24027000/FD24027000I1", HttpStatusCode.OK, "{\"incident_status\":{\"status\":\"REJECTED\"}}"); + var rejected = await _client.GetStatusAsync(_profile, _credential, "FD24027000I1"); + rejected.Kind.Should().Be(NerisOutcomeKind.Rejected); + rejected.Errors.Should().ContainSingle(e => e.Code == "REJECTED"); + + _handler.Reply("GET", "/v1/incident/FD24027000/FD24027000I1", HttpStatusCode.OK, "{\"incident_status\":{\"status\":\"PENDING_APPROVAL\"}}"); + (await _client.GetStatusAsync(_profile, _credential, "FD24027000I1")).Kind.Should().Be(NerisOutcomeKind.Pending); + } + + [Test] + public async Task A_missing_profile_or_credential_is_fatal_before_any_call() + { + (await _client.CreateIncidentAsync(new RmsNerisProfile { DepartmentId = 4 }, _credential, "{}")).Kind.Should().Be(NerisOutcomeKind.Fatal); + (await _client.CreateIncidentAsync(_profile, null, "{}")).Kind.Should().Be(NerisOutcomeKind.Fatal); + _handler.Requests.Should().BeEmpty(); + } + + public sealed class RecordedRequest + { + public string Method { get; set; } + public string Path { get; set; } + public string Body { get; set; } + public string Authorization { get; set; } + public string ContentType { get; set; } + } + + /// Replies keyed by "METHOD path"; the token endpoint has its own script. + private sealed class ScriptedHandler : HttpMessageHandler + { + private readonly Dictionary _replies = new Dictionary(StringComparer.OrdinalIgnoreCase); + public List Requests { get; } = new List(); + + public void Token(HttpStatusCode status, string body) => _replies["POST /v1/token"] = (status, body); + public void Reply(string method, string path, HttpStatusCode status, string body) => _replies[$"{method} {path}"] = (status, body); + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var body = request.Content == null ? null : await request.Content.ReadAsStringAsync(cancellationToken); + Requests.Add(new RecordedRequest + { + Method = request.Method.Method, + Path = request.RequestUri.AbsolutePath, + Body = body, + Authorization = request.Headers.Authorization?.ToString(), + ContentType = request.Content?.Headers.ContentType?.ToString() + }); + + if (!_replies.TryGetValue($"{request.Method.Method} {request.RequestUri.AbsolutePath}", out var reply)) + return new HttpResponseMessage(HttpStatusCode.NotFound) { Content = new StringContent("{\"detail\":\"not scripted\"}", Encoding.UTF8, "application/json") }; + + return new HttpResponseMessage(reply.status) { Content = new StringContent(reply.body ?? string.Empty, Encoding.UTF8, "application/json") }; + } + } + } +} diff --git a/Tests/Resgrid.Tests/Providers/NerisMappingTests.cs b/Tests/Resgrid.Tests/Providers/NerisMappingTests.cs new file mode 100644 index 00000000..e8c569de --- /dev/null +++ b/Tests/Resgrid.Tests/Providers/NerisMappingTests.cs @@ -0,0 +1,238 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Providers.Neris; + +namespace Resgrid.Tests.Providers +{ + /// + /// Golden NERIS payload and contract test (RMS plan section 7, "Golden NERIS payloads and contract tests + /// against the pinned production/test OpenAPI"): the mapper's output is checked key-by-key against the pinned + /// OpenAPI document — every emitted property must exist in the schema it lands in and every required property + /// of those schemas must be present — and every enum value must come from the pinned value sets. + /// + [TestFixture] + public class NerisMappingTests + { + public static RmsNerisProfile Profile() + { + return new RmsNerisProfile { RmsNerisProfileId = "p1", DepartmentId = 4, NerisEntityId = "FD24027000", Environment = NerisEnvironments.Sandbox, BaseUrlOverride = "https://neris.test/v1", GrantType = NerisGrantTypes.ClientCredentials, ContractVersion = "1.4.78", IsEnabled = true, EncryptedCredentialJson = "x" }; + } + + public static NerisIncidentSnapshot Snapshot() + { + var t0 = new DateTime(2026, 9, 3, 14, 0, 0, DateTimeKind.Utc); + return new NerisIncidentSnapshot + { + Report = new RmsIncidentReport + { + RmsIncidentReportId = "rep-1", DepartmentId = 4, CallId = 77, ReportingEntityId = "FD24027000", DefinitionKey = RmsDefinitionKeys.NerisIncidentReport, + IncidentNumber = "2026-000123", DispatchIncidentCode = "STRUCT FIRE", DispatchCenterId = "PSAP-1", Disposition = "Extinguished", + CallCreatedOn = t0, CallAnsweredOn = t0.AddSeconds(-20), CallArrivalOn = t0.AddMinutes(6), IncidentClearedOn = t0.AddMinutes(90), + PeoplePresent = true, DisplacementCount = 2, AnimalsRescued = 1, SpecialModifiersCsv = "MCI", State = (int)RmsRecordState.Finalized + }, + Location = new RmsLocation { Number = "100", Street = "Main St", Municipality = "Springfield", County = "Sangamon", State = "IL", PostalCode = "62701", Country = "US", PlaceType = "RESIDENCE", CrossStreet1 = "1st Ave", Latitude = 39.7817m, Longitude = -89.6501m, AddressText = "100 Main St, Springfield IL" }, + Types = new List + { + new RmsIncidentType { TypeCode = "FIRE||OUTSIDE_FIRE||TRASH_RUBBISH_FIRE", IsPrimary = true, LocalCode = "STRUCT FIRE", Ordinal = 0 }, + new RmsIncidentType { TypeCode = "FIRE||OUTSIDE_FIRE||VEGETATION_GRASS_FIRE", IsPrimary = false, Ordinal = 1 } + }, + Units = new List + { + new RmsUnitResponse { UnitId = 5, UnitNameSnapshot = "E1", UnitNerisId = "FD24027000S001U001", Staffing = 4, DispatchedOn = t0.AddMinutes(1), EnrouteOn = t0.AddMinutes(2), OnSceneOn = t0.AddMinutes(6), ClearedOn = t0.AddMinutes(80), ResponseMode = "EMERGENT", Ordinal = 0 }, + new RmsUnitResponse { UnitId = 6, UnitNameSnapshot = "L2", Staffing = 3, DispatchedOn = t0.AddMinutes(1), EnrouteOn = t0.AddMinutes(3), OnSceneOn = t0.AddMinutes(8), ClearedOn = t0.AddMinutes(85), ResponseMode = "EMERGENT", Ordinal = 1 } + }, + Aids = new List + { + new RmsAid { Direction = "RECEIVED", AidType = "SUPPORT_AID", CounterpartNerisId = "FD24027334", CounterpartName = "Neighbor FD", Ordinal = 0 }, + new RmsAid { IsNonFireDepartment = true, NonFdType = "LAW_ENFORCEMENT", Ordinal = 1 } + }, + Tactics = new List { new RmsActionTactic { TacticCode = "COMMAND_AND_CONTROL||ESTABLISH_INCIDENT_COMMAND", OccurredOn = t0.AddMinutes(7), Ordinal = 0 } }, + Narrative = new RmsNarrative { Narrative = "Dumpster fire extinguished with one line.", ImpedimentNarrative = "None", OutcomeNarrative = "Fire out, no extension.", SupplementalJson = "{\"dept_only\":\"never sent\"}" }, + DispatchComments = new List { new NerisDispatchComment { Timestamp = t0.AddSeconds(30), Comment = "Caller reports flames behind store" } }, + SpecialModifiers = new List { "MCI" } + }; + } + + /// Newtonsoft turns ISO strings into Date tokens by default; the contract check wants the wire text. + private static JObject ParsePayload(string json) + { + return Newtonsoft.Json.JsonConvert.DeserializeObject(json, new Newtonsoft.Json.JsonSerializerSettings { DateParseHandling = Newtonsoft.Json.DateParseHandling.None }); + } + + private static JObject Spec() + { + var directory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + while (directory != null && !System.IO.File.Exists(Path.Combine(directory.FullName, "Resgrid.sln"))) + directory = directory.Parent; + var path = Path.Combine(directory!.FullName, "Providers", "Resgrid.Providers.Neris", "Contract", "neris-openapi-v1.4.78-2026-09-03.json"); + System.IO.File.Exists(path).Should().BeTrue("the pinned NERIS OpenAPI document must be checked in"); + return JObject.Parse(System.IO.File.ReadAllText(path)); + } + + [Test] + public void The_payload_is_deterministic_and_carries_the_required_base_and_dispatch_facts() + { + var mapper = new NerisMappingService(); + var first = mapper.BuildIncidentPayloadJson(Snapshot(), Profile()); + var second = mapper.BuildIncidentPayloadJson(Snapshot(), Profile()); + + first.Should().Be(second, "the stored artifact and its checksum depend on byte-identical output"); + + var payload = ParsePayload(first); + ((string)payload["base"]["department_neris_id"]).Should().Be("FD24027000"); + ((string)payload["base"]["incident_number"]).Should().Be("2026-000123"); + ((bool)payload["base"]["people_present"]).Should().BeTrue(); + ((string)payload["base"]["location"]["street"]).Should().Be("Main St"); + ((int)payload["base"]["location"]["number"]).Should().Be(100); + ((double)payload["base"]["point"]["geometry"]["coordinates"][0]).Should().Be(-89.6501); + ((string)payload["dispatch"]["call_create"]).Should().Be("2026-09-03T14:00:00Z"); + ((string)payload["dispatch"]["incident_code"]).Should().Be("STRUCT FIRE", "the original CAD code rides beside the mapped types"); + payload["dispatch"]["unit_responses"].Should().HaveCount(2); + ((string)payload["dispatch"]["unit_responses"][0]["unit_neris_id"]).Should().Be("FD24027000S001U001"); + ((string)payload["dispatch"]["unit_responses"][1]["reported_unit_id"]).Should().Be("L2"); + payload["incident_types"].Should().HaveCount(2); + ((bool)payload["incident_types"][0]["primary"]).Should().BeTrue(); + ((string)payload["aids"][0]["department_neris_id"]).Should().Be("FD24027334"); + ((string)payload["nonfd_aids"][0]).Should().Be("LAW_ENFORCEMENT"); + ((string)payload["actions_tactics"]["action_noaction"]["type"]).Should().Be("ACTION"); + ((string)payload["special_modifiers"][0]).Should().Be("MCI"); + ((string)payload["dispatch"]["comments"][0]["comment"]).Should().Be("Caller reports flames behind store"); + } + + [Test] + public void The_payload_never_carries_nulls_supplemental_sections_or_the_narrative_body() + { + var json = new NerisMappingService().BuildIncidentPayloadJson(Snapshot(), Profile()); + + json.Should().NotContain("null"); + json.Should().NotContain("dept_only", "supplemental department questions never enter the submission payload"); + json.Should().NotContain("Dumpster fire extinguished", "the narrative body is not a NERIS field on this contract"); + json.Should().Contain("\"outcome_narrative\":\"Fire out, no extension.\""); + } + + [Test] + public void Every_enum_in_the_payload_comes_from_the_pinned_value_sets() + { + var catalog = NerisValueSetCatalog.Instance; + var payload = ParsePayload(new NerisMappingService().BuildIncidentPayloadJson(Snapshot(), Profile())); + + catalog.ContractVersion.Should().Be("1.4.78"); + foreach (var type in payload["incident_types"].Select(t => (string)t["type"])) + catalog.Contains("incident_type", type).Should().BeTrue(type); + foreach (var unit in payload["dispatch"]["unit_responses"]) + catalog.Contains("response_mode", (string)unit["response_mode"]).Should().BeTrue(); + catalog.Contains("aid_type", (string)payload["aids"][0]["aid_type"]).Should().BeTrue(); + catalog.Contains("aid_direction", (string)payload["aids"][0]["aid_direction"]).Should().BeTrue(); + catalog.Contains("aid_nonfd", (string)payload["nonfd_aids"][0]).Should().BeTrue(); + catalog.Contains("special_modifier", (string)payload["special_modifiers"][0]).Should().BeTrue(); + catalog.Contains("action_tactic", (string)payload["actions_tactics"]["action_noaction"]["actions"][0]).Should().BeTrue(); + catalog.Contains("location_place", (string)payload["base"]["location"]["place_type"]).Should().BeTrue(); + catalog.Contains("state", (string)payload["base"]["location"]["state"]).Should().BeTrue(); + catalog.Contains("country", (string)payload["base"]["location"]["country"]).Should().BeTrue(); + } + + [Test] + public void The_payload_conforms_to_the_pinned_openapi_incident_schema() + { + var spec = Spec(); + var payload = ParsePayload(new NerisMappingService().BuildIncidentPayloadJson(Snapshot(), Profile())); + + var problems = new List(); + Check(spec, "IncidentPayload", payload, "$", problems); + problems.Should().BeEmpty(string.Join("\n", problems)); + } + + /// Walks the payload against a named schema: unknown properties and missing required ones are problems; nested objects and arrays recurse through $ref/anyOf/oneOf. + private static void Check(JObject spec, string schemaName, JToken value, string path, List problems) + { + var schema = (JObject)spec["components"]["schemas"][schemaName]; + if (schema["enum"] != null) + { + if (!((JArray)schema["enum"]).Any(e => (string)e == (string)value)) + problems.Add($"{path}: '{value}' is not in {schemaName}"); + return; + } + + if (value.Type != JTokenType.Object) + { + if (schema["properties"] != null) + problems.Add($"{path}: expected an object for {schemaName}"); + return; + } + + var properties = (JObject)schema["properties"] ?? new JObject(); + var required = ((JArray)schema["required"] ?? new JArray()).Select(r => (string)r).ToList(); + var obj = (JObject)value; + + foreach (var name in required.Where(r => obj[r] == null)) + problems.Add($"{path}: required '{name}' of {schemaName} is missing"); + + foreach (var property in obj.Properties()) + { + if (properties[property.Name] == null) + { + problems.Add($"{path}.{property.Name}: not a property of {schemaName}"); + continue; + } + CheckAgainst(spec, (JObject)properties[property.Name], property.Value, $"{path}.{property.Name}", problems); + } + } + + private static void CheckAgainst(JObject spec, JObject propertySchema, JToken value, string path, List problems) + { + var refName = RefName(propertySchema); + if (refName != null) + { + Check(spec, refName, value, path, problems); + return; + } + + var alternatives = (JArray)propertySchema["anyOf"] ?? (JArray)propertySchema["oneOf"]; + if (alternatives != null) + { + // Pick the alternative that matches the token shape (object ref, array, or scalar); null alternatives never apply to emitted values. + var candidates = alternatives.OfType().Where(a => (string)a["type"] != "null").ToList(); + var matching = candidates.FirstOrDefault(a => Matches(a, value)) ?? candidates.FirstOrDefault(); + if (matching != null) + CheckAgainst(spec, matching, value, path, problems); + return; + } + + if ((string)propertySchema["type"] == "array") + { + if (value.Type != JTokenType.Array) + { + problems.Add($"{path}: expected an array"); + return; + } + var index = 0; + foreach (var item in (JArray)value) + CheckAgainst(spec, (JObject)propertySchema["items"], item, $"{path}[{index++}]", problems); + } + } + + private static bool Matches(JObject alternative, JToken value) + { + var type = (string)alternative["type"]; + if (alternative["$ref"] != null) return value.Type == JTokenType.Object || value.Type == JTokenType.String; + if (type == "array") return value.Type == JTokenType.Array; + if (type == "string") return value.Type == JTokenType.String || value.Type == JTokenType.Date; + if (type == "integer") return value.Type == JTokenType.Integer; + if (type == "number") return value.Type == JTokenType.Float || value.Type == JTokenType.Integer; + if (type == "boolean") return value.Type == JTokenType.Boolean; + return false; + } + + private static string RefName(JObject schema) + { + var reference = (string)schema["$ref"]; + return reference?.Split('/').Last(); + } + } +} diff --git a/Tests/Resgrid.Tests/Providers/NerisValidationTests.cs b/Tests/Resgrid.Tests/Providers/NerisValidationTests.cs new file mode 100644 index 00000000..1091608f --- /dev/null +++ b/Tests/Resgrid.Tests/Providers/NerisValidationTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Model.Providers; +using Resgrid.Providers.Neris; + +namespace Resgrid.Tests.Providers +{ + /// + /// Local NERIS validation (RMS plan section 7: requiredness, conditional value sets, time-sequence tests): + /// the mandatory base/dispatch facts, value-set membership, identifier shapes and per-unit time order surface + /// before a submission is queued; destination replies fold into the same issue shape. + /// + [TestFixture] + public class NerisValidationTests + { + private static NerisValidationService Service() + { + return new NerisValidationService(Mock.Of(), Mock.Of()); + } + + [Test] + public void A_complete_report_has_no_errors() + { + var issues = Service().ValidateLocal(NerisMappingTests.Snapshot(), NerisMappingTests.Profile()); + + issues.Where(i => i.Severity == (int)RmsValidationSeverity.Error).Should().BeEmpty(string.Join("; ", issues.Select(i => i.RuleKey))); + } + + [Test] + public void Missing_mandatory_facts_are_reported_with_stable_rule_keys() + { + var snapshot = NerisMappingTests.Snapshot(); + snapshot.Report.IncidentNumber = null; + snapshot.Report.CallCreatedOn = null; + snapshot.Report.CallArrivalOn = null; + snapshot.Types.Clear(); + snapshot.Location = null; + + var keys = Service().ValidateLocal(snapshot, NerisMappingTests.Profile()).Select(i => i.RuleKey).ToList(); + + keys.Should().Contain(new[] { "neris.base.incident_number", "neris.dispatch.call_create", "neris.dispatch.call_arrival", "neris.incident_types.required", "neris.base.location" }); + } + + [Test] + public void The_profile_entity_id_shape_and_a_single_primary_type_are_enforced() + { + var profile = NerisMappingTests.Profile(); + profile.NerisEntityId = "24027000"; + var snapshot = NerisMappingTests.Snapshot(); + snapshot.Types.ForEach(t => t.IsPrimary = true); + snapshot.Types.Add(new RmsIncidentType { TypeCode = "NOT||A||TYPE", Ordinal = 2 }); + + var issues = Service().ValidateLocal(snapshot, profile); + + issues.Select(i => i.RuleKey).Should().Contain(new[] { "neris.profile.entity.shape", "neris.incident_types.primary", "neris.incident_types.code" }); + issues.Single(i => i.RuleKey == "neris.incident_types.code").Message.Should().Contain("NOT||A||TYPE"); + } + + [Test] + public void Unit_identifier_shape_response_mode_and_time_order_are_checked_per_unit() + { + var snapshot = NerisMappingTests.Snapshot(); + var unit = snapshot.Units[0]; + unit.UnitNerisId = "E1-BAD"; + unit.ResponseMode = "FAST"; + unit.OnSceneOn = unit.DispatchedOn.Value.AddMinutes(-10); + + var issues = Service().ValidateLocal(snapshot, NerisMappingTests.Profile()); + + issues.Select(i => i.RuleKey).Should().Contain(new[] { "neris.unit.id.shape", "neris.unit.response_mode", "neris.unit.sequence" }); + issues.Where(i => i.FieldPath != null && i.FieldPath.StartsWith("dispatch.unit_responses[0]")).Should().HaveCount(3); + } + + [Test] + public void Aid_entries_need_a_counterpart_id_and_valid_codes() + { + var snapshot = NerisMappingTests.Snapshot(); + snapshot.Aids[0].CounterpartNerisId = "ABC"; + snapshot.Aids[0].AidType = "HELP"; + snapshot.Aids[1].NonFdType = "PIZZA"; + + var keys = Service().ValidateLocal(snapshot, NerisMappingTests.Profile()).Select(i => i.RuleKey).ToList(); + + keys.Should().Contain(new[] { "neris.aid.counterpart", "neris.aid.type", "neris.aid.nonfd" }); + } + + [Test] + public void Destination_replies_fold_into_issues_of_source_destination() + { + var rejected = new NerisSubmissionOutcome + { + Kind = NerisOutcomeKind.Rejected, + Errors = new List { new NerisSubmissionError { Code = "missing", FieldPath = "dispatch.call_answered", Message = "Field required" } } + }; + var transient = new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Transient, Message = "NERIS returned 503." }; + + var rejectedIssues = NerisValidationService.ToIssues(rejected, 4, "rep-1"); + rejectedIssues.Should().ContainSingle(i => i.RuleKey == "neris.destination.missing" && i.FieldPath == "dispatch.call_answered" && i.Source == (int)RmsValidationSource.Destination && i.Severity == (int)RmsValidationSeverity.Error); + + var transientIssues = NerisValidationService.ToIssues(transient, 4, "rep-1"); + transientIssues.Should().ContainSingle(i => i.RuleKey == "neris.destination.unavailable" && i.Severity == (int)RmsValidationSeverity.Warning); + + NerisValidationService.ToIssues(new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Accepted }, 4, "rep-1").Should().BeEmpty(); + } + + [Test] + public void Labels_and_parents_derive_from_the_code_without_changing_it() + { + NerisProfileService.Label("FIRE||OUTSIDE_FIRE||TRASH_RUBBISH_FIRE").Should().Be("Trash Rubbish Fire"); + NerisProfileService.Parent("FIRE||OUTSIDE_FIRE||TRASH_RUBBISH_FIRE").Should().Be("FIRE||OUTSIDE_FIRE"); + NerisProfileService.Parent("MCI").Should().BeNull(); + NerisProfileService.Label("EMS").Should().Be("EMS"); + } + } +} diff --git a/Tests/Resgrid.Tests/Resgrid.Tests.csproj b/Tests/Resgrid.Tests/Resgrid.Tests.csproj index 3a585750..a3d40b33 100644 --- a/Tests/Resgrid.Tests/Resgrid.Tests.csproj +++ b/Tests/Resgrid.Tests/Resgrid.Tests.csproj @@ -46,6 +46,7 @@ + diff --git a/Tests/Resgrid.Tests/Rms/FakeIncidentStore.cs b/Tests/Resgrid.Tests/Rms/FakeIncidentStore.cs new file mode 100644 index 00000000..44ff3d74 --- /dev/null +++ b/Tests/Resgrid.Tests/Rms/FakeIncidentStore.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Repositories.Queries; + +namespace Resgrid.Tests.Rms +{ + /// + /// In-memory stand-ins for the RMS-2 incident report repositories (M0164–M0166). Reuses FakeRmsStore for the + /// shared tables (revisions, group scope, shares, projections, audits, outbox, unit of work) so the incident + /// report service and the submission worker logic can be asserted without a database. + /// + public sealed class FakeIncidentStore + { + public FakeRmsStore Shared { get; } = new FakeRmsStore(); + + public List Reports { get; } = new List(); + public List Facts { get; } = new List(); + public List Units { get; } = new List(); + public List Types { get; } = new List(); + public List Tactics { get; } = new List(); + public List Aids { get; } = new List(); + public List Locations { get; } = new List(); + public List Narratives { get; } = new List(); + public List Issues { get; } = new List(); + public List Submissions { get; } = new List(); + public List Signatures { get; } = new List(); + + public Mock ReportsRepo { get; } = new Mock(); + public Mock FactsRepo { get; } = new Mock(); + public Mock UnitsRepo { get; } = new Mock(); + public Mock TypesRepo { get; } = new Mock(); + public Mock TacticsRepo { get; } = new Mock(); + public Mock AidsRepo { get; } = new Mock(); + public Mock LocationsRepo { get; } = new Mock(); + public Mock NarrativesRepo { get; } = new Mock(); + public Mock IssuesRepo { get; } = new Mock(); + public Mock SubmissionsRepo { get; } = new Mock(); + public Mock SignaturesRepo { get; } = new Mock(); + + public List Revisions => Shared.Revisions; + public List Scopes => Shared.Scopes; + public List Projections => Shared.Projections; + public List Audits => Shared.Audits; + public List Outbox => Shared.Outbox; + public Mock UnitOfWork => Shared.UnitOfWork; + + public FakeIncidentStore() + { + // Reports + ReportsRepo.Setup(r => r.InsertAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RmsIncidentReport e, CancellationToken c, bool f) => { Reports.Add(e); return e; }); + ReportsRepo.Setup(r => r.UpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RmsIncidentReport e, CancellationToken c, bool f) => { Reports.RemoveAll(x => x.RmsIncidentReportId == e.RmsIncidentReportId); Reports.Add(e); return e; }); + ReportsRepo.Setup(r => r.GetByIdForDepartmentAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string id) => Reports.FirstOrDefault(x => x.DepartmentId == d && x.RmsIncidentReportId == id)); + ReportsRepo.Setup(r => r.GetByCallAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, int call, string entity) => Reports.FirstOrDefault(x => x.DepartmentId == d && x.CallId == call && x.ReportingEntityId == entity && x.DeletedOn == null)); + ReportsRepo.Setup(r => r.GetByCallAnyEntityAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, int call) => Reports.Where(x => x.DepartmentId == d && x.CallId == call).ToList()); + ReportsRepo.Setup(r => r.GetByNerisIncidentIdAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string id) => Reports.FirstOrDefault(x => x.DepartmentId == d && x.NerisIncidentId == id)); + ReportsRepo.Setup(r => r.QueryAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, RmsIncidentReportQuery q) => Reports.Where(x => x.DepartmentId == d && x.DeletedOn == null).ToList()); + ReportsRepo.Setup(r => r.CountAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, RmsIncidentReportQuery q) => Reports.Count(x => x.DepartmentId == d && x.DeletedOn == null)); + ReportsRepo.Setup(r => r.GetYearsAsync(It.IsAny())) + .ReturnsAsync((int d) => Reports.Where(x => x.DepartmentId == d).Select(x => (x.CallCreatedOn ?? x.CreatedOn).Year).Distinct().ToList()); + ReportsRepo.Setup(r => r.GetMaxRecordNumberSequenceAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string prefix) => Reports + .Where(x => x.DepartmentId == d && x.RecordNumber != null && x.RecordNumber.StartsWith(prefix, StringComparison.Ordinal)) + .Select(x => int.TryParse(x.RecordNumber.Substring(prefix.Length), out var n) ? n : 0) + .DefaultIfEmpty(0).Max()); + ReportsRepo.Setup(r => r.TryBumpRowVersionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string id, long expected, CancellationToken c) => + { + var row = Reports.FirstOrDefault(x => x.DepartmentId == d && x.RmsIncidentReportId == id && x.RowVersion == expected); + if (row == null) return false; + row.RowVersion = expected + 1; + return true; + }); + + // Child rows (draft rows carry RevisionId null; revision copies carry the revision id) + Wire(FactsRepo, Facts, x => x.RmsSourceFactId, x => x.DepartmentId, x => x.RecordId, x => x.RevisionId); + Wire(UnitsRepo, Units, x => x.RmsUnitResponseId, x => x.DepartmentId, x => x.RecordId, x => x.RevisionId); + Wire(TypesRepo, Types, x => x.RmsIncidentTypeId, x => x.DepartmentId, x => x.RecordId, x => x.RevisionId); + Wire(TacticsRepo, Tactics, x => x.RmsActionTacticId, x => x.DepartmentId, x => x.RecordId, x => x.RevisionId); + Wire(AidsRepo, Aids, x => x.RmsAidId, x => x.DepartmentId, x => x.RecordId, x => x.RevisionId); + Wire(LocationsRepo, Locations, x => x.RmsLocationId, x => x.DepartmentId, x => x.RecordId, x => x.RevisionId); + Wire(NarrativesRepo, Narratives, x => x.RmsNarrativeId, x => x.DepartmentId, x => x.RecordId, x => x.RevisionId); + + // Validation issues: a run replaces every issue of its source + IssuesRepo.Setup(r => r.GetForRecordAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string id) => Issues.Where(x => x.DepartmentId == d && x.RecordId == id).ToList()); + IssuesRepo.Setup(r => r.ReplaceForRecordAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) + .Returns((int d, string id, RmsValidationSource source, IEnumerable issues, CancellationToken c) => + { + Issues.RemoveAll(x => x.DepartmentId == d && x.RecordId == id && x.Source == (int)source); + foreach (var issue in issues ?? Enumerable.Empty()) + { + issue.RmsValidationIssueId ??= Guid.NewGuid().ToString(); + issue.DepartmentId = d; + issue.RecordId = id; + issue.Source = (int)source; + Issues.Add(issue); + } + return Task.CompletedTask; + }); + + // Submissions + SubmissionsRepo.Setup(r => r.InsertAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RmsSubmission e, CancellationToken c, bool f) => { Submissions.Add(e); return e; }); + SubmissionsRepo.Setup(r => r.UpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RmsSubmission e, CancellationToken c, bool f) => { Submissions.RemoveAll(x => x.RmsSubmissionId == e.RmsSubmissionId); Submissions.Add(e); return e; }); + SubmissionsRepo.Setup(r => r.GetByIdForDepartmentAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string id) => Submissions.FirstOrDefault(x => x.DepartmentId == d && x.RmsSubmissionId == id)); + SubmissionsRepo.Setup(r => r.GetForRecordAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string id) => Submissions.Where(x => x.DepartmentId == d && x.RecordId == id).OrderByDescending(x => x.QueuedOn).ToList()); + SubmissionsRepo.Setup(r => r.GetByIdempotencyKeyAsync(It.IsAny())) + .ReturnsAsync((string key) => Submissions.FirstOrDefault(x => x.IdempotencyKey == key)); + SubmissionsRepo.Setup(r => r.CountByStateAsync(It.IsAny())) + .ReturnsAsync((int state) => Submissions.Count(x => x.State == state)); + SubmissionsRepo.Setup(r => r.SupersedeOpenForRecordAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string id, string except, DateTime now, CancellationToken c) => + { + var open = Submissions.Where(x => x.DepartmentId == d && x.RecordId == id && x.RmsSubmissionId != except && IsOpen(x.State)).ToList(); + foreach (var row in open) + { + row.State = (int)RmsSubmissionState.Superseded; + row.CompletedOn = now; + row.ModifiedOn = now; + } + return open.Count; + }); + SubmissionsRepo.Setup(r => r.ClaimDueBatchAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((string owner, TimeSpan lease, int batch, DateTime now, CancellationToken c) => + { + var due = Submissions + .Where(x => (x.State == (int)RmsSubmissionState.Queued || x.State == (int)RmsSubmissionState.AwaitingDestination) + && (x.NextAttemptOn == null || x.NextAttemptOn <= now) + && (x.LeaseExpiresOn == null || x.LeaseExpiresOn < now)) + .OrderBy(x => x.QueuedOn).Take(batch).ToList(); + foreach (var row in due) + { + row.LeaseOwner = owner; + row.LeaseExpiresOn = now.Add(lease); + } + return due; + }); + + // Signatures + SignaturesRepo.Setup(r => r.InsertAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RmsSignature e, CancellationToken c, bool f) => { Signatures.Add(e); return e; }); + SignaturesRepo.Setup(r => r.GetForRecordAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string id) => Signatures.Where(x => x.DepartmentId == d && x.RecordId == id).ToList()); + SignaturesRepo.Setup(r => r.GetForRevisionAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string rev, RmsSignatureIntent intent) => Signatures.FirstOrDefault(x => x.DepartmentId == d && x.RevisionId == rev && x.Intent == (int)intent)); + } + + public static bool IsOpen(int state) + { + return state == (int)RmsSubmissionState.Queued || state == (int)RmsSubmissionState.InFlight || state == (int)RmsSubmissionState.AwaitingDestination; + } + + private static void Wire(Mock repo, List rows, Func id, Func dept, Func record, Func revision) + where T : class, IEntity + where TRepo : class, IRmsIncidentChildRepository + { + repo.Setup(r => r.InsertAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((T e, CancellationToken c, bool f) => { rows.Add(e); return e; }); + repo.Setup(r => r.UpdateAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((T e, CancellationToken c, bool f) => { rows.RemoveAll(x => id(x) == id(e)); rows.Add(e); return e; }); + repo.Setup(r => r.GetForRecordAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string rec, string rev) => (IEnumerable)rows.Where(x => dept(x) == d && record(x) == rec && revision(x) == rev).ToList()); + repo.Setup(r => r.DeleteDraftForRecordAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string rec, CancellationToken c) => rows.RemoveAll(x => dept(x) == d && record(x) == rec && revision(x) == null)); + } + } +} diff --git a/Tests/Resgrid.Tests/Rms/IncidentReportsServiceTests.cs b/Tests/Resgrid.Tests/Rms/IncidentReportsServiceTests.cs new file mode 100644 index 00000000..d7bacee5 --- /dev/null +++ b/Tests/Resgrid.Tests/Rms/IncidentReportsServiceTests.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Model.Providers; +using Resgrid.Model.Services; +using Resgrid.Providers.Neris; +using Resgrid.Services; +using Resgrid.Services.Records; + +namespace Resgrid.Tests.Rms +{ + /// + /// RMS-2 incident report lifecycle (plan sections 4.2, 5.2.1, 5.3): source-aware prefill with provenance, + /// SingleAuthoritative start, corrections on provenance rows, validation gating the signature, the revision + + /// attestation + submission chain, correction after rejection, and void superseding open submissions. + /// + [TestFixture] + public class IncidentReportsServiceTests + { + private const int Dept = 42; + private const int CallId = 77; + private static readonly DateTime LoggedOn = new DateTime(2026, 9, 1, 8, 0, 0, DateTimeKind.Utc); + + private FakeIncidentStore _store; + private Mock _settings; + private Mock _groups; + private Mock _profiles; + private Mock _roles; + private Mock _units; + private Mock _calls; + private Mock _adp; + private Mock _neris; + private Mock _validation; + private Mock _aggregator; + private RmsNerisProfile _profile; + private bool _submissionEnabled; + private List _localIssues; + private Call _call; + private IncidentReportsService _service; + + [SetUp] + public void SetUp() + { + Resgrid.Config.SystemBehaviorConfig.CacheEnabled = false; + _store = new FakeIncidentStore(); + + _settings = new Mock(); + _settings.Setup(s => s.GetRecordsNumberingConfigAsync(Dept, It.IsAny())).ReturnsAsync(new RecordsNumberingConfig()); + _settings.Setup(s => s.GetRecordsReviewDueHoursAsync(Dept, It.IsAny())).ReturnsAsync(72); + + _groups = new Mock(); + _groups.Setup(g => g.GetGroupForUserAsync("author", Dept)).ReturnsAsync(new DepartmentGroup { DepartmentGroupId = 11, Name = "Station 1" }); + + _profiles = new Mock(); + _profiles.Setup(p => p.GetProfileByUserIdAsync(It.IsAny(), It.IsAny())).ReturnsAsync((string id, bool b) => new UserProfile { UserId = id, FirstName = "First", LastName = id }); + + _roles = new Mock(); + _roles.Setup(r => r.GetRolesForUserAsync("author", Dept)).ReturnsAsync(new List { new PersonnelRole { Name = "Captain" } }); + + _units = new Mock(); + _units.Setup(u => u.GetUnitByIdAsync(5)).ReturnsAsync(new Unit { UnitId = 5, DepartmentId = Dept, Name = "Engine 5", Type = "Engine", StationGroupId = 13 }); + _units.Setup(u => u.GetUnitStatesForCallAsync(Dept, CallId)).ReturnsAsync(new List + { + new UnitState { UnitId = 5, State = (int)UnitStateTypes.Responding, Timestamp = LoggedOn.AddMinutes(2) }, + new UnitState { UnitId = 5, State = (int)UnitStateTypes.OnScene, Timestamp = LoggedOn.AddMinutes(10) }, + new UnitState { UnitId = 5, State = (int)UnitStateTypes.Available, Timestamp = LoggedOn.AddMinutes(60) } + }); + + _call = new Call + { + CallId = CallId, DepartmentId = Dept, Number = "2026-000123", Name = "Structure fire", Type = "Fire", Priority = 3, + LoggedOn = LoggedOn, Address = "1 Main St", GeoLocationData = "39.5,-104.9", NatureOfCall = "Smoke showing", + CallNotes = new List { new CallNote { CallNoteId = 1, Note = "Caller reports smoke from the roof", Timestamp = LoggedOn.AddMinutes(1) } }, + UnitDispatches = new List { new CallDispatchUnit { CallDispatchUnitId = 1, UnitId = 5, DispatchedOn = LoggedOn.AddMinutes(1) } } + }; + _calls = new Mock(); + _calls.Setup(c => c.GetCallByIdAsync(CallId, It.IsAny())).ReturnsAsync(() => _call); + _calls.Setup(c => c.PopulateCallData(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(() => _call); + + _adp = new Mock(); + _adp.Setup(a => a.GetPinnedCatalogVersionAsync(Dept)).ReturnsAsync(0); + + _profile = new RmsNerisProfile { DepartmentId = Dept, NerisEntityId = "FD24027000", ContractVersion = "1.4.78", AutoSubmitOnFinalize = true, IsEnabled = true }; + _submissionEnabled = true; + _neris = new Mock(); + _neris.SetupGet(n => n.ContractVersion).Returns("1.4.78"); + _neris.Setup(n => n.GetProfileAsync(Dept)).ReturnsAsync(() => _profile); + _neris.Setup(n => n.IsSubmissionEnabledAsync(Dept)).ReturnsAsync(() => _submissionEnabled); + _neris.Setup(n => n.ResolveCrosswalkAsync(Dept, "incident_type", NerisCrosswalkSources.CallType, "Fire")).ReturnsAsync("FIRE||STRUCTURE_FIRE||RESIDENTIAL"); + + _localIssues = new List(); + _validation = new Mock(); + _validation.Setup(v => v.ValidateLocal(It.IsAny(), It.IsAny())).Returns(() => _localIssues.ToList()); + + _aggregator = new Mock(); + var outbox = new DomainEventOutboxService(_store.Shared.OutboxRepo.Object, _aggregator.Object); + + _service = new IncidentReportsService(_store.ReportsRepo.Object, _store.FactsRepo.Object, _store.UnitsRepo.Object, _store.TypesRepo.Object, + _store.TacticsRepo.Object, _store.AidsRepo.Object, _store.LocationsRepo.Object, _store.NarrativesRepo.Object, _store.IssuesRepo.Object, + _store.SubmissionsRepo.Object, _store.SignaturesRepo.Object, _store.Shared.RevisionsRepo.Object, _store.Shared.AuditsRepo.Object, + _store.Shared.ScopesRepo.Object, _store.Shared.SharesRepo.Object, _store.Shared.ProjectionsRepo.Object, outbox, _settings.Object, + _groups.Object, _profiles.Object, _roles.Object, _units.Object, _calls.Object, _adp.Object, _store.UnitOfWork.Object, + _neris.Object, new NerisMappingService(), _validation.Object); + } + + [Test] + public async Task Start_from_call_prefills_dispatch_facts_with_provenance() + { + var aggregate = await _service.StartFromCallAsync(Dept, "author", CallId); + + aggregate.State.Should().Be(RmsRecordState.Draft); + aggregate.Report.ReportingEntityId.Should().Be("FD24027000"); + aggregate.Report.DefinitionKey.Should().Be(RmsDefinitionKeys.NerisIncidentReport); + aggregate.Report.IncidentNumber.Should().Be("2026-000123"); + aggregate.Report.CallCreatedOn.Should().Be(LoggedOn); + aggregate.Report.CallArrivalOn.Should().Be(LoggedOn.AddMinutes(10), "first on-scene from the unit state log"); + aggregate.Report.DraftReference.Should().StartWith("I-"); + aggregate.Report.RecordNumber.Should().BeNull("numbers are allocated at finalize"); + + aggregate.Location.Should().NotBeNull(); + aggregate.Location.AddressText.Should().Be("1 Main St"); + aggregate.Location.Latitude.Should().Be(39.5m); + aggregate.Location.SourceKind.Should().Be((int)RmsSourceKind.Dispatch); + + aggregate.Units.Should().ContainSingle(); + var unit = aggregate.Units[0]; + unit.UnitId.Should().Be(5); + unit.DispatchedOn.Should().Be(LoggedOn.AddMinutes(1)); + unit.EnrouteOn.Should().Be(LoggedOn.AddMinutes(2)); + unit.OnSceneOn.Should().Be(LoggedOn.AddMinutes(10)); + unit.ClearedOn.Should().Be(LoggedOn.AddMinutes(60)); + unit.TimesSourceKind.Should().Be((int)RmsSourceKind.App); + + aggregate.Types.Should().ContainSingle(t => t.IsPrimary && t.TypeCode == "FIRE||STRUCTURE_FIRE||RESIDENTIAL" && t.LocalCode == "Fire"); + + var facts = aggregate.Facts; + facts.Single(f => f.FactKey == NerisFactKeys.IncidentNumber).SourceKind.Should().Be((int)RmsSourceKind.Dispatch); + facts.Single(f => f.FactKey == NerisFactKeys.CallAnswered).SourceKind.Should().Be((int)RmsSourceKind.Derived, "Resgrid holds no PSAP answered time"); + facts.Single(f => f.FactKey == NerisFactKeys.UnitTime(5, "on_scene")).SourceKind.Should().Be((int)RmsSourceKind.App); + facts.Single(f => f.FactKey == NerisFactKeys.IncidentType).SourceKind.Should().Be((int)RmsSourceKind.Derived); + facts.Should().ContainSingle(f => f.FactKey == IncidentReportsService.DispatchCommentFactPrefix + "0" && f.SourceValue == "Caller reports smoke from the roof"); + facts.Should().OnlyContain(f => f.CorrectedOn == null && f.RevisionId == null); + + _store.Outbox.Should().ContainSingle(o => o.TriggerEventType == (int)WorkflowTriggerEventType.RecordCreated && o.AggregateType == IncidentReportsService.IncidentAggregate); + _store.Projections.Should().ContainSingle(p => p.RmsRecordSearchProjectionId == aggregate.Report.RmsIncidentReportId && p.RecordKind == (int)RmsRecordKind.IncidentReport && p.CallId == CallId); + _store.Scopes.Should().Contain(s => s.DepartmentGroupId == 11 && s.AnchorType == (int)RmsGroupScopeAnchorType.Author); + _store.Scopes.Should().Contain(s => s.DepartmentGroupId == 13 && s.AnchorType == (int)RmsGroupScopeAnchorType.Unit); + _store.Audits.Should().Contain(a => a.RecordId == aggregate.Report.RmsIncidentReportId && a.Action == (int)RmsAccessAuditAction.Change); + } + + [Test] + public async Task Start_from_call_twice_returns_the_existing_report() + { + var first = await _service.StartFromCallAsync(Dept, "author", CallId); + var second = await _service.StartFromCallAsync(Dept, "p2", CallId); + + second.Report.RmsIncidentReportId.Should().Be(first.Report.RmsIncidentReportId, "one authoritative report per entity per Call"); + _store.Reports.Should().ContainSingle(); + _store.Outbox.Should().ContainSingle(); + } + + [Test] + public async Task Save_draft_records_corrections_on_the_provenance_row() + { + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + var version = started.Report.RowVersion; + var input = DraftFrom(started); + input.IncidentNumber = "2026-000124"; + input.Units[0].OnSceneOn = LoggedOn.AddMinutes(12); + input.Narrative = "Fire confined to the attic."; + input.Location.Street = "Main St"; + + var saved = await _service.SaveDraftAsync(Dept, "author", started.Report.RmsIncidentReportId, started.Report.RowVersion, input); + + saved.Report.IncidentNumber.Should().Be("2026-000124"); + var number = saved.Facts.Single(f => f.FactKey == NerisFactKeys.IncidentNumber); + number.SourceValue.Should().Be("2026-000123", "the dispatch value survives"); + number.CurrentValue.Should().Be("2026-000124"); + number.CorrectedOn.Should().NotBeNull(); + number.CorrectedByUserId.Should().Be("author"); + + var onScene = saved.Facts.Single(f => f.FactKey == NerisFactKeys.UnitTime(5, "on_scene")); + onScene.CurrentValue.Should().Be(IncidentReportsService.Iso(LoggedOn.AddMinutes(12))); + onScene.SourceValue.Should().Be(IncidentReportsService.Iso(LoggedOn.AddMinutes(10))); + saved.Facts.Single(f => f.FactKey == NerisFactKeys.UnitTime(5, "dispatch")).CorrectedOn.Should().BeNull("unchanged values keep their provenance untouched"); + + saved.Units.Should().ContainSingle(u => u.UnitId == 5 && u.OnSceneOn == LoggedOn.AddMinutes(12)); + saved.Narrative.Narrative.Should().Be("Fire confined to the attic."); + saved.Location.Street.Should().Be("Main St"); + saved.Report.RowVersion.Should().Be(version + 1); + _store.Outbox.Should().ContainSingle("draft saves never raise Workflow events"); + } + + [Test] + public async Task Finalize_is_blocked_by_local_validation_errors() + { + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + _localIssues.Add(new RmsValidationIssue { RuleKey = "neris.dispatch.call_answered.required", Severity = (int)RmsValidationSeverity.Error, FieldPath = "dispatch.call_answered", Message = "required" }); + + Func act = () => _service.FinalizeAsync(Dept, "author", started.Report.RmsIncidentReportId, started.Report.RowVersion, null, "10.0.0.1", null, null); + + await act.Should().ThrowAsync(); + _store.Reports.Single().State.Should().Be((int)RmsRecordState.Draft); + _store.Revisions.Should().BeEmpty(); + _store.Signatures.Should().BeEmpty(); + _store.Submissions.Should().BeEmpty(); + _store.Issues.Should().ContainSingle(i => i.RuleKey == "neris.dispatch.call_answered.required" && i.Source == (int)RmsValidationSource.Local); + _store.Shared.Discards.Should().BeGreaterThan(0, "the transaction rolls back"); + } + + [Test] + public async Task Finalize_writes_revision_signature_and_queues_the_submission() + { + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + var reportId = started.Report.RmsIncidentReportId; + + var final = await _service.FinalizeAsync(Dept, "author", reportId, started.Report.RowVersion, null, "10.0.0.1", null, null); + + final.State.Should().Be(RmsRecordState.Submitted, "auto-submit is on and the destination is enabled"); + final.Report.RecordNumber.Should().StartWith(IncidentReportsService.NumberPrefix + "-"); + final.Report.FinalizedOn.Should().NotBeNull(); + final.Report.RevisionCount.Should().Be(1); + + var revision = _store.Revisions.Single(); + revision.RecordKind.Should().Be((int)RmsRecordKind.IncidentReport); + revision.Transition.Should().Be((int)RmsRevisionTransition.Finalized); + revision.Checksum.Should().NotBeNullOrEmpty(); + revision.AttestationStatementVersion.Should().Be(IncidentReportsService.AttestationStatementVersion); + final.Report.CurrentRevisionId.Should().Be(revision.RmsRevisionId); + _store.Facts.Should().Contain(f => f.RevisionId == revision.RmsRevisionId, "the revision keeps its own copies of the provenance rows"); + _store.Units.Should().Contain(u => u.RevisionId == revision.RmsRevisionId); + + var signature = _store.Signatures.Single(); + signature.RevisionId.Should().Be(revision.RmsRevisionId); + signature.ArtifactChecksum.Should().Be(revision.Checksum, "the attestation binds to the revision checksum"); + signature.Intent.Should().Be((int)RmsSignatureIntent.Attestation); + signature.SignerRoleSnapshot.Should().Be("Captain"); + signature.IpAddress.Should().Be("10.0.0.1"); + + var submission = _store.Submissions.Single(); + submission.State.Should().Be((int)RmsSubmissionState.Queued); + submission.Destination.Should().Be(RmsSubmissionDestinations.Neris); + submission.DestinationVersion.Should().Be("1.4.78"); + submission.RevisionId.Should().Be(revision.RmsRevisionId); + submission.IdempotencyKey.Should().Be(IncidentReportsService.IdempotencyKey(Dept, reportId, revision.RmsRevisionId)); + submission.PayloadJson.Should().Contain("\"department_neris_id\":\"FD24027000\""); + submission.PayloadChecksum.Should().Be(RecordSnapshotSerializer.Checksum(submission.PayloadJson)); + final.Report.LastSubmissionId.Should().Be(submission.RmsSubmissionId); + final.Submissions.Should().ContainSingle(); + + _store.Outbox.Select(o => o.TriggerEventType).Should().ContainInOrder((int)WorkflowTriggerEventType.RecordCreated, (int)WorkflowTriggerEventType.RecordFinalized, (int)WorkflowTriggerEventType.RecordSubmissionQueued); + var queued = JObject.Parse(_store.Outbox.Single(o => o.TriggerEventType == (int)WorkflowTriggerEventType.RecordSubmissionQueued).PayloadJson); + queued["submission"]["state"].Value().Should().Be("Queued"); + queued["submission"]["destination"].Value().Should().Be("NERIS"); + queued["submission"].Children().Select(p => p.Name).Should().NotContain(new[] { "payload", "payload_json", "response" }, "the sanitized block never carries payloads"); + queued["record"]["kind"].Value().Should().Be("IncidentReport"); + _store.Audits.Should().Contain(a => a.Action == (int)RmsAccessAuditAction.Sign && a.IpAddress == "10.0.0.1"); + _store.Audits.Should().Contain(a => a.Action == (int)RmsAccessAuditAction.Submit); + } + + [Test] + public async Task Finalize_without_auto_submit_stays_finalized_until_queued_explicitly() + { + _profile.AutoSubmitOnFinalize = false; + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + var reportId = started.Report.RmsIncidentReportId; + + var final = await _service.FinalizeAsync(Dept, "author", reportId, started.Report.RowVersion, null, null, null, null); + final.State.Should().Be(RmsRecordState.Finalized); + _store.Submissions.Should().BeEmpty(); + + var queued = await _service.QueueSubmissionAsync(Dept, "author", reportId); + queued.State.Should().Be(RmsRecordState.Submitted); + _store.Submissions.Should().ContainSingle(s => s.State == (int)RmsSubmissionState.Queued && s.RevisionId == final.Report.CurrentRevisionId); + _store.Outbox.Should().ContainSingle(o => o.TriggerEventType == (int)WorkflowTriggerEventType.RecordSubmissionQueued); + + Func again = () => _service.QueueSubmissionAsync(Dept, "author", reportId); + await again.Should().ThrowAsync("the same revision is already queued"); + } + + [Test] + public async Task Queue_submission_refuses_when_the_destination_is_disabled() + { + _submissionEnabled = false; + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + + var final = await _service.FinalizeAsync(Dept, "author", started.Report.RmsIncidentReportId, started.Report.RowVersion, null, null, null, null); + final.State.Should().Be(RmsRecordState.Finalized, "nothing is queued while the destination is off"); + _store.Submissions.Should().BeEmpty(); + + Func act = () => _service.QueueSubmissionAsync(Dept, "author", started.Report.RmsIncidentReportId); + await act.Should().ThrowAsync(); + } + + [Test] + public async Task Correct_and_resubmit_after_rejection_issues_a_new_revision_and_key() + { + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + var reportId = started.Report.RmsIncidentReportId; + var final = await _service.FinalizeAsync(Dept, "author", reportId, started.Report.RowVersion, null, null, null, null); + var firstRevision = final.Report.CurrentRevisionId; + var firstSubmission = _store.Submissions.Single(); + + // The worker rejected the delivery (RecordsSubmissionService is covered separately). + var report = _store.Reports.Single(); + report.State = (int)RmsRecordState.Rejected; + report.RejectedOn = DateTime.UtcNow; + report.RejectionSummary = "dispatch.call_answered (required)"; + firstSubmission.State = (int)RmsSubmissionState.Rejected; + firstSubmission.CompletedOn = DateTime.UtcNow; + + var input = DraftFrom(await _service.GetAsync(Dept, reportId)); + input.CallAnsweredOn = LoggedOn.AddSeconds(30); + var edited = await _service.SaveDraftAsync(Dept, "author", reportId, report.RowVersion, input); + edited.State.Should().Be(RmsRecordState.Rejected, "a rejected report is editable in place"); + + var corrected = await _service.CorrectAndResubmitAsync(Dept, "author", reportId, edited.Report.RowVersion, null, null, "destination-rejection", "Answered time added"); + + corrected.State.Should().Be(RmsRecordState.Submitted); + corrected.Report.RevisionCount.Should().Be(2); + var second = _store.Revisions.Single(r => r.RevisionNumber == 2); + second.PriorRevisionId.Should().Be(firstRevision, "N+1 references N"); + second.Transition.Should().Be((int)RmsRevisionTransition.Amended); + second.ReasonCode.Should().Be("destination-rejection"); + _store.Signatures.Should().HaveCount(2); + + _store.Submissions.Should().HaveCount(2); + var resubmission = _store.Submissions.Single(s => s.RevisionId == second.RmsRevisionId); + resubmission.State.Should().Be((int)RmsSubmissionState.Queued); + resubmission.IdempotencyKey.Should().NotBe(firstSubmission.IdempotencyKey, "a new revision gets a new idempotency key"); + resubmission.PayloadJson.Should().Contain(IncidentReportsService.Iso(LoggedOn.AddSeconds(30))); + firstSubmission.State.Should().Be((int)RmsSubmissionState.Rejected, "closed submissions are history, never rewritten"); + + _store.Outbox.Count(o => o.TriggerEventType == (int)WorkflowTriggerEventType.RecordAmended).Should().Be(1); + _store.Outbox.Count(o => o.TriggerEventType == (int)WorkflowTriggerEventType.RecordSubmissionQueued).Should().Be(2); + } + + [Test] + public async Task Correct_and_resubmit_requires_a_rejected_report() + { + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + + Func act = () => _service.CorrectAndResubmitAsync(Dept, "author", started.Report.RmsIncidentReportId, started.Report.RowVersion, null, null, "x", null); + + await act.Should().ThrowAsync(); + } + + [Test] + public async Task Void_waits_for_the_destination_answer_then_records_a_void_revision() + { + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + var reportId = started.Report.RmsIncidentReportId; + await _service.FinalizeAsync(Dept, "author", reportId, started.Report.RowVersion, null, null, null, null); + _store.Submissions.Single().State.Should().Be((int)RmsSubmissionState.Queued); + + // In flight: the destination has (or may have) the revision, so the void must wait for its answer. + Func inFlight = () => _service.VoidAsync(Dept, "author", reportId, "duplicate", "Entered twice"); + await inFlight.Should().ThrowAsync(); + + // The worker rejected the delivery (RecordsSubmissionService is covered separately). + var report = _store.Reports.Single(); + report.State = (int)RmsRecordState.Rejected; + var submission = _store.Submissions.Single(); + submission.State = (int)RmsSubmissionState.Rejected; + submission.CompletedOn = DateTime.UtcNow; + + var voided = await _service.VoidAsync(Dept, "author", reportId, "duplicate", "Entered twice"); + + voided.State.Should().Be(RmsRecordState.Voided); + voided.Report.RevisionCount.Should().Be(2); + voided.Report.VoidReasonCode.Should().Be("duplicate"); + var revision = _store.Revisions.Single(r => r.RevisionNumber == 2); + revision.Transition.Should().Be((int)RmsRevisionTransition.Voided); + revision.AttestedOn.Should().BeNull("a void is not attested"); + _store.Signatures.Should().ContainSingle("only the finalize was signed"); + _store.Submissions.Single().State.Should().Be((int)RmsSubmissionState.Rejected, "closed submissions are history"); + _store.Outbox.Should().ContainSingle(o => o.TriggerEventType == (int)WorkflowTriggerEventType.RecordVoided); + _store.Projections.Single().State.Should().Be((int)RmsRecordState.Voided); + } + + [Test] + public async Task Cancel_from_draft_keeps_no_number_and_raises_107() + { + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + + var cancelled = await _service.CancelAsync(Dept, "author", started.Report.RmsIncidentReportId); + + cancelled.State.Should().Be(RmsRecordState.Cancelled); + cancelled.Report.RecordNumber.Should().BeNull(); + var payload = JObject.Parse(_store.Outbox.Single(o => o.TriggerEventType == (int)WorkflowTriggerEventType.RecordCancelled).PayloadJson); + payload["extra"]["number_disposition"].Value().Should().Be("none"); + } + + [Test] + public async Task Snapshot_carries_dispatch_comments_in_time_order() + { + var started = await _service.StartFromCallAsync(Dept, "author", CallId); + + var snapshot = await _service.BuildSnapshotAsync(Dept, started.Report.RmsIncidentReportId); + + snapshot.Report.RmsIncidentReportId.Should().Be(started.Report.RmsIncidentReportId); + snapshot.DispatchComments.Should().ContainSingle(c => c.Comment == "Caller reports smoke from the roof" && c.Timestamp == LoggedOn.AddMinutes(1)); + snapshot.Units.Should().ContainSingle(); + } + + /// A draft input carrying the aggregate's current values, so a test can change one thing and save. + private static IncidentReportDraftInput DraftFrom(IncidentReportAggregate a) + { + var r = a.Report; + return new IncidentReportDraftInput + { + IncidentNumber = r.IncidentNumber, + CallCreatedOn = r.CallCreatedOn, + CallAnsweredOn = r.CallAnsweredOn, + CallArrivalOn = r.CallArrivalOn, + IncidentClearedOn = r.IncidentClearedOn, + DispatchCenterId = r.DispatchCenterId, + DeterminantCode = r.DeterminantCode, + DispatchIncidentCode = r.DispatchIncidentCode, + Disposition = r.Disposition, + PeoplePresent = r.PeoplePresent, + DisplacementCount = r.DisplacementCount, + AnimalsRescued = r.AnimalsRescued, + StationGroupId = r.StationGroupId, + Location = a.Location == null ? null : new IncidentLocationInput + { + AddressText = a.Location.AddressText, Number = a.Location.Number, Street = a.Location.Street, Municipality = a.Location.Municipality, + County = a.Location.County, State = a.Location.State, PostalCode = a.Location.PostalCode, Country = a.Location.Country, + Latitude = a.Location.Latitude, Longitude = a.Location.Longitude + }, + Types = a.Types.Select(t => new IncidentTypeInput { TypeCode = t.TypeCode, IsPrimary = t.IsPrimary }).ToList(), + Units = a.Units.Select(u => new IncidentUnitResponseInput + { + UnitId = u.UnitId, UnitNerisId = u.UnitNerisId, Staffing = u.Staffing, DispatchedOn = u.DispatchedOn, EnrouteOn = u.EnrouteOn, + OnSceneOn = u.OnSceneOn, StagingOn = u.StagingOn, CanceledEnrouteOn = u.CanceledEnrouteOn, ClearedOn = u.ClearedOn, ResponseMode = u.ResponseMode, TransportMode = u.TransportMode + }).ToList(), + Aids = a.Aids.Select(x => new IncidentAidInput { Direction = x.Direction, AidType = x.AidType, CounterpartNerisId = x.CounterpartNerisId, CounterpartName = x.CounterpartName, IsNonFireDepartment = x.IsNonFireDepartment, NonFdType = x.NonFdType }).ToList(), + Tactics = a.Tactics.Select(t => new IncidentTacticInput { TacticCode = t.TacticCode, ActorUnitId = t.ActorUnitId, OccurredOn = t.OccurredOn }).ToList(), + Narrative = a.Narrative?.Narrative, + ImpedimentNarrative = a.Narrative?.ImpedimentNarrative, + OutcomeNarrative = a.Narrative?.OutcomeNarrative, + SupplementalJson = a.Narrative?.SupplementalJson + }; + } + } +} diff --git a/Tests/Resgrid.Tests/Rms/RecordAttachmentUploadServiceTests.cs b/Tests/Resgrid.Tests/Rms/RecordAttachmentUploadServiceTests.cs new file mode 100644 index 00000000..20542bdb --- /dev/null +++ b/Tests/Resgrid.Tests/Rms/RecordAttachmentUploadServiceTests.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Concurrent; +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.Services; +using Resgrid.Services.Records; + +namespace Resgrid.Tests.Rms +{ + /// In-memory state store for the v4 Records contract tests. + public sealed class MemoryRecordsApiStateStore : IRecordsApiStateStore + { + public ConcurrentDictionary Entries { get; } = new ConcurrentDictionary(StringComparer.Ordinal); + + public Task GetAsync(string key) + { + return Task.FromResult(Entries.TryGetValue(key, out var entry) && entry.ExpiresOn > DateTime.UtcNow ? entry.Value : null); + } + + public Task SetAsync(string key, string value, TimeSpan timeToLive) + { + Entries[key] = (value, DateTime.UtcNow.Add(timeToLive)); + return Task.CompletedTask; + } + + public Task RemoveAsync(string key) + { + Entries.TryRemove(key, out _); + return Task.CompletedTask; + } + } + + /// Resumable attachment sessions (RMS plan 5.3): ordered chunks, resume from ReceivedBytes, checksum gate, hygiene path on completion. + [TestFixture] + public class RecordAttachmentUploadServiceTests + { + private const int Dept = 42; + private const string RecordId = "rec-1"; + + private MemoryRecordsApiStateStore _store; + private Mock _records; + private RecordAttachmentUploadService _service; + private List<(string FileName, byte[] Data)> _stored; + + [SetUp] + public void SetUp() + { + _store = new MemoryRecordsApiStateStore(); + _stored = new List<(string, byte[])>(); + _records = new Mock(); + _records.Setup(r => r.GetAsync(Dept, RecordId, It.IsAny())) + .ReturnsAsync(new RecordAggregate { Record = new RmsOperationalRecord { RmsOperationalRecordId = RecordId, DepartmentId = Dept, State = (int)RmsRecordState.Draft } }); + _records.Setup(r => r.GetAsync(Dept, "final", It.IsAny())) + .ReturnsAsync(new RecordAggregate { Record = new RmsOperationalRecord { RmsOperationalRecordId = "final", DepartmentId = Dept, State = (int)RmsRecordState.Finalized } }); + _records.Setup(r => r.AddAttachmentAsync(Dept, "author", RecordId, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string u, string rec, string name, string type, byte[] data, string desc, CancellationToken c) => + { + _stored.Add((name, data)); + return new RmsRecordAttachment { RmsRecordAttachmentId = "att-1", RecordId = rec, FileName = name, ContentType = type, ByteSize = data.Length, Checksum = RecordSnapshotSerializer.Checksum(data), ScanState = (int)RmsAttachmentScanState.Skipped }; + }); + _service = new RecordAttachmentUploadService(_store, _records.Object); + } + + private static byte[] Bytes(int length, int seed = 7) + { + var data = new byte[length]; + var rng = new Random(seed); + rng.NextBytes(data); + return data; + } + + [Test] + public async Task Chunks_in_order_then_complete_stores_through_the_hygiene_path() + { + var file = Bytes(_service.ChunkSize * 2 + 100); + var session = await _service.BeginAsync(Dept, "author", RecordId, "scene.jpg", "image/jpeg", file.Length, RecordAttachmentUploadService.Sha256Hex(file)); + session.ChunkCount.Should().Be(3); + session.State.Should().Be(RecordUploadSessionState.Open); + + session = await _service.AppendAsync(Dept, "author", session.UploadId, 0, file.Take(_service.ChunkSize).ToArray()); + session.ReceivedBytes.Should().Be(_service.ChunkSize); + session = await _service.AppendAsync(Dept, "author", session.UploadId, _service.ChunkSize, file.Skip(_service.ChunkSize).Take(_service.ChunkSize).ToArray()); + session = await _service.AppendAsync(Dept, "author", session.UploadId, 2 * _service.ChunkSize, file.Skip(2 * _service.ChunkSize).ToArray()); + session.IsComplete.Should().BeTrue(); + + var attachment = await _service.CompleteAsync(Dept, "author", session.UploadId, "Scene photo"); + + attachment.RmsRecordAttachmentId.Should().Be("att-1"); + _stored.Should().ContainSingle(); + _stored[0].Data.Should().Equal(file, "the assembled bytes reach AddAttachmentAsync unchanged"); + _stored[0].FileName.Should().Be("scene.jpg"); + (await _service.GetAsync(Dept, "author", session.UploadId)).State.Should().Be(RecordUploadSessionState.Completed); + _store.Entries.Keys.Should().NotContain(k => k.StartsWith("RecordsApiUploadChunk_"), "chunks are released after completion"); + } + + [Test] + public async Task Out_of_order_chunk_is_refused_and_the_session_reports_where_to_resume() + { + var file = Bytes(_service.ChunkSize + 10); + var session = await _service.BeginAsync(Dept, "author", RecordId, "a.png", "image/png", file.Length, RecordAttachmentUploadService.Sha256Hex(file)); + await _service.AppendAsync(Dept, "author", session.UploadId, 0, file.Take(_service.ChunkSize).ToArray()); + + Func replay = () => _service.AppendAsync(Dept, "author", session.UploadId, 0, file.Take(_service.ChunkSize).ToArray()); + (await replay.Should().ThrowAsync()).Which.Code.Should().Be("bad_offset"); + + var resumed = await _service.GetAsync(Dept, "author", session.UploadId); + resumed.ReceivedBytes.Should().Be(_service.ChunkSize, "the client resumes from ReceivedBytes"); + await _service.AppendAsync(Dept, "author", session.UploadId, resumed.ReceivedBytes, file.Skip(_service.ChunkSize).ToArray()); + (await _service.CompleteAsync(Dept, "author", session.UploadId, null)).Should().NotBeNull(); + } + + [Test] + public async Task Checksum_mismatch_refuses_completion_and_never_stores() + { + var file = Bytes(1000); + var session = await _service.BeginAsync(Dept, "author", RecordId, "a.png", "image/png", file.Length, RecordAttachmentUploadService.Sha256Hex(Bytes(1000, 99))); + await _service.AppendAsync(Dept, "author", session.UploadId, 0, file); + + Func act = () => _service.CompleteAsync(Dept, "author", session.UploadId, null); + + (await act.Should().ThrowAsync()).Which.Code.Should().Be("checksum_mismatch"); + _stored.Should().BeEmpty(); + } + + [Test] + public async Task Incomplete_session_cannot_complete() + { + var file = Bytes(_service.ChunkSize + 1); + var session = await _service.BeginAsync(Dept, "author", RecordId, "a.png", "image/png", file.Length, RecordAttachmentUploadService.Sha256Hex(file)); + await _service.AppendAsync(Dept, "author", session.UploadId, 0, file.Take(_service.ChunkSize).ToArray()); + + Func act = () => _service.CompleteAsync(Dept, "author", session.UploadId, null); + + (await act.Should().ThrowAsync()).Which.Code.Should().Be("incomplete"); + } + + [Test] + public async Task Declared_size_over_the_cap_or_a_bad_checksum_is_refused_at_begin() + { + Func big = () => _service.BeginAsync(Dept, "author", RecordId, "a.png", "image/png", RecordAttachmentHygiene.MaxBytes + 1, new string('a', 64)); + (await big.Should().ThrowAsync()).Which.Code.Should().Be("too_large"); + + Func hash = () => _service.BeginAsync(Dept, "author", RecordId, "a.png", "image/png", 10, "not-a-hash"); + (await hash.Should().ThrowAsync()).Which.Code.Should().Be("checksum_mismatch"); + } + + [Test] + public async Task Finalized_record_refuses_new_upload_sessions() + { + Func act = () => _service.BeginAsync(Dept, "author", "final", "a.png", "image/png", 10, new string('a', 64)); + await act.Should().ThrowAsync(); + } + + [Test] + public async Task Another_user_cannot_see_or_append_to_the_session() + { + var file = Bytes(10); + var session = await _service.BeginAsync(Dept, "author", RecordId, "a.png", "image/png", file.Length, RecordAttachmentUploadService.Sha256Hex(file)); + + (await _service.GetAsync(Dept, "intruder", session.UploadId)).Should().BeNull(); + Func act = () => _service.AppendAsync(Dept, "intruder", session.UploadId, 0, file); + (await act.Should().ThrowAsync()).Which.Code.Should().Be("not_found"); + } + + [Test] + public async Task Hygiene_rejection_surfaces_as_rejected_and_the_session_stays_open_for_a_retry() + { + var file = Bytes(10); + _records.Setup(r => r.AddAttachmentAsync(Dept, "author", RecordId, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new RecordAttachmentRejectedException("Attachment 'a.svg' is not an allowed type.")); + var session = await _service.BeginAsync(Dept, "author", RecordId, "a.svg", "image/svg+xml", file.Length, RecordAttachmentUploadService.Sha256Hex(file)); + await _service.AppendAsync(Dept, "author", session.UploadId, 0, file); + + Func act = () => _service.CompleteAsync(Dept, "author", session.UploadId, null); + + (await act.Should().ThrowAsync()).Which.Code.Should().Be("rejected"); + (await _service.GetAsync(Dept, "author", session.UploadId)).State.Should().Be(RecordUploadSessionState.Open); + } + + [Test] + public async Task Idempotency_service_replays_by_department_user_and_key() + { + var idempotency = new RecordsApiIdempotencyService(_store); + + (await idempotency.TryGetRecordIdAsync(Dept, "u1", "k1")).Should().BeNull(); + await idempotency.RememberAsync(Dept, "u1", "k1", "rec-9"); + + (await idempotency.TryGetRecordIdAsync(Dept, "u1", "k1")).Should().Be("rec-9"); + (await idempotency.TryGetRecordIdAsync(Dept, "u2", "k1")).Should().BeNull("keys are scoped to the user"); + (await idempotency.TryGetRecordIdAsync(43, "u1", "k1")).Should().BeNull("keys are scoped to the department"); + (await idempotency.TryGetRecordIdAsync(Dept, "u1", null)).Should().BeNull(); + } + + [Test] + public void Conflict_resolver_names_only_the_paths_the_stale_copy_would_change() + { + var current = new RecordAggregate + { + Record = new RmsOperationalRecord { RmsOperationalRecordId = RecordId, State = (int)RmsRecordState.Draft, RowVersion = 5, StartedOn = new DateTime(2026, 9, 1, 8, 0, 0, DateTimeKind.Utc), StationGroupId = 3 }, + Details = new RmsOperationalRecordDetail { Narrative = "Server narrative", Course = "CPR" }, + Units = new List { new RmsRecordUnitResponse { UnitId = 5 } } + }; + var attempted = new RecordDraftInput + { + StartedOn = new DateTime(2026, 9, 1, 8, 0, 0, DateTimeKind.Utc), + StationGroupId = 3, + Details = new RmsOperationalRecordDetail { Narrative = "My narrative", Course = "CPR" }, + Units = new List { new RecordUnitResponseInput { UnitId = 5 }, new RecordUnitResponseInput { UnitId = 6 } } + }; + + var conflict = RecordDraftConflictResolver.Describe(attempted, current, 4); + + conflict.ExpectedRowVersion.Should().Be(4); + conflict.CurrentRowVersion.Should().Be(5); + conflict.CurrentState.Should().Be(RmsRecordState.Draft); + conflict.ChangedFieldPaths.Should().BeEquivalentTo(new[] { "Details.Narrative", "Units" }); + } + + [Test] + public void Definition_catalog_describes_every_locked_definition_with_its_fields() + { + var catalog = RecordDefinitionCatalog.Describe(); + + catalog.Select(d => d.Key).Should().Contain(RmsDefinitionKeys.LockedTypes.Keys).And.Contain(RmsDefinitionKeys.NerisIncidentReport); + catalog.Should().OnlyContain(d => d.MinimumClientCapability == RecordsApiContract.LockedDefinitionCapability && d.Locked); + var coroner = catalog.Single(d => d.Key == RmsDefinitionKeys.Coroner); + coroner.Restricted.Should().BeTrue(); + coroner.Fields.Where(f => f.Restricted).Select(f => f.Key).Should().BeEquivalentTo(RecordSnapshotSerializer.RestrictedDetailFields); + catalog.Single(d => d.Key == RmsDefinitionKeys.UnitActivity).Fields.Should().Contain(f => f.Key == "UnitId" && f.Required); + catalog.Should().OnlyContain(d => d.Fields.Any(f => f.Key == "Narrative")); + catalog.Single(d => d.Key == RmsDefinitionKeys.NerisIncidentReport).RequiresCall.Should().BeTrue(); + } + + [Test] + public void ETag_round_trips_through_the_contract_helpers() + { + RecordsApiContract.ToETag(7).Should().Be("W/\"7\""); + RecordsApiContract.ParseETag("W/\"7\"").Should().Be(7); + RecordsApiContract.ParseETag("\"12\"").Should().Be(12); + RecordsApiContract.ParseETag("3").Should().Be(3); + RecordsApiContract.ParseETag("W/\"abc\"").Should().BeNull(); + RecordsApiContract.ParseETag(null).Should().BeNull(); + } + } +} diff --git a/Tests/Resgrid.Tests/Rms/RecordsGroupScopePreviewTests.cs b/Tests/Resgrid.Tests/Rms/RecordsGroupScopePreviewTests.cs index 1a864cc0..884add39 100644 --- a/Tests/Resgrid.Tests/Rms/RecordsGroupScopePreviewTests.cs +++ b/Tests/Resgrid.Tests/Rms/RecordsGroupScopePreviewTests.cs @@ -55,7 +55,7 @@ public async Task The_preview_counts_records_per_group_unanchored_records_and_un departments.Setup(d => d.GetAllAdminsForDepartmentAsync(Dept)).ReturnsAsync(new List { new IdentityUser { UserId = "a" } }); var service = new RecordsAuthorizationService(Mock.Of(), departments.Object, groups.Object, Mock.Of(), - Mock.Of(), records.Object, scopes.Object, Mock.Of(), Mock.Of(), legacy.Object); + Mock.Of(), records.Object, scopes.Object, Mock.Of(), Mock.Of(), legacy.Object, Mock.Of()); var preview = await service.PreviewGroupScopingAsync(Dept); diff --git a/Tests/Resgrid.Tests/Rms/RecordsNotificationServiceTests.cs b/Tests/Resgrid.Tests/Rms/RecordsNotificationServiceTests.cs index 4d1625e9..2af54980 100644 --- a/Tests/Resgrid.Tests/Rms/RecordsNotificationServiceTests.cs +++ b/Tests/Resgrid.Tests/Rms/RecordsNotificationServiceTests.cs @@ -36,7 +36,7 @@ public void SetUp() var settings = new Mock(); settings.Setup(s => s.GetTextToCallNumberForDepartmentAsync(Dept)).ReturnsAsync("+15555550100"); - _service = new RecordsNotificationService(_records.Object, _communication.Object, departments.Object, settings.Object, _profiles.Object); + _service = new RecordsNotificationService(_records.Object, _communication.Object, departments.Object, settings.Object, _profiles.Object, new Mock().Object); } private static RmsOperationalRecord Returned() => new RmsOperationalRecord diff --git a/Tests/Resgrid.Tests/Rms/RecordsSubmissionServiceTests.cs b/Tests/Resgrid.Tests/Rms/RecordsSubmissionServiceTests.cs new file mode 100644 index 00000000..6fea5795 --- /dev/null +++ b/Tests/Resgrid.Tests/Rms/RecordsSubmissionServiceTests.cs @@ -0,0 +1,333 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using Resgrid.Config; +using Resgrid.Model; +using Resgrid.Model.Events; +using Resgrid.Model.Providers; +using Resgrid.Model.Queue; +using Resgrid.Services; +using Resgrid.Services.Records; + +namespace Resgrid.Tests.Rms +{ + /// + /// Worker 41 logic (RMS plan sections 5.3, 5.5, 5.6): delivery outcomes drive the submission and report states, + /// triggers 109–111 and notification 33, transient failures back off and fail after MaxAttempts, and stale or + /// disabled work is never delivered. + /// + [TestFixture] + public class RecordsSubmissionServiceTests + { + private const int Dept = 42; + private const string ReportId = "report-1"; + + private FakeIncidentStore _store; + private Mock _profiles; + private Mock _delivery; + private Mock _outboundQueue; + private List _notifications; + private RmsNerisProfile _profile; + private bool _enabled; + private bool _previousEnabled; + private RecordsSubmissionService _service; + + [SetUp] + public void SetUp() + { + _previousEnabled = NerisConfig.Enabled; + NerisConfig.Enabled = true; + _store = new FakeIncidentStore(); + + _profile = new RmsNerisProfile { DepartmentId = Dept, NerisEntityId = "FD24027000", ContractVersion = "1.4.78", IsEnabled = true }; + _enabled = true; + _profiles = new Mock(); + _profiles.Setup(p => p.GetProfileAsync(Dept)).ReturnsAsync(() => _profile); + _profiles.Setup(p => p.IsSubmissionEnabledAsync(Dept)).ReturnsAsync(() => _enabled); + + _delivery = new Mock(); + + _notifications = new List(); + _outboundQueue = new Mock(); + _outboundQueue.Setup(q => q.EnqueueNotification(It.IsAny())).Callback(n => _notifications.Add(n)).ReturnsAsync(true); + + var outbox = new DomainEventOutboxService(_store.Shared.OutboxRepo.Object, new Mock().Object); + _service = new RecordsSubmissionService(_store.SubmissionsRepo.Object, _store.ReportsRepo.Object, _store.Shared.ProjectionsRepo.Object, + _store.Shared.AuditsRepo.Object, _profiles.Object, _delivery.Object, outbox, _outboundQueue.Object, _store.UnitOfWork.Object); + } + + [TearDown] + public void TearDown() + { + NerisConfig.Enabled = _previousEnabled; + } + + [Test] + public async Task Created_outcome_moves_the_report_to_Submitted_and_awaits_the_destination() + { + var submission = Seed(RmsRecordState.Finalized, RmsSubmissionState.Queued); + _delivery.Setup(d => d.DeliverAsync(_profile, submission, null, It.IsAny())) + .ReturnsAsync(new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Created, StatusCode = 201, ExternalId = "FD24027000I2026000123", ExternalStatus = "SUBMITTED", ResponseJson = "{\"neris_id\":\"FD24027000I2026000123\"}" }); + + var result = await _service.ProcessAsync(submission); + + result.State.Should().Be((int)RmsSubmissionState.AwaitingDestination); + result.Attempts.Should().Be(1); + result.SentOn.Should().NotBeNull(); + result.ExternalId.Should().Be("FD24027000I2026000123"); + result.ExternalStatus.Should().Be("SUBMITTED"); + result.ResponseStatusCode.Should().Be(201); + result.ResponseChecksum.Should().Be(RecordSnapshotSerializer.Checksum(result.ResponseJson), "the response artifact is stored verbatim with its checksum"); + result.NextAttemptOn.Should().NotBeNull("the status poll is scheduled"); + result.LeaseOwner.Should().BeNull(); + result.CompletedOn.Should().BeNull(); + + var report = _store.Reports.Single(); + report.State.Should().Be((int)RmsRecordState.Submitted); + report.NerisIncidentId.Should().Be("FD24027000I2026000123"); + report.LastSubmissionId.Should().Be(submission.RmsSubmissionId); + report.LastSubmissionState.Should().Be((int)RmsSubmissionState.AwaitingDestination); + _store.Projections.Single().State.Should().Be((int)RmsRecordState.Submitted); + _store.Projections.Single().SearchText.Should().Contain("FD24027000I2026000123"); + _store.Outbox.Should().BeEmpty("108 was raised when the submission was queued; delivery alone raises nothing"); + _notifications.Should().BeEmpty(); + _store.Audits.Should().ContainSingle(a => a.Action == (int)RmsAccessAuditAction.Submit && a.Purpose == "Submission delivered" && a.Successful); + } + + [Test] + public async Task Accepted_status_raises_109() + { + var submission = Seed(RmsRecordState.Submitted, RmsSubmissionState.AwaitingDestination, externalId: "FD24027000I2026000123"); + _delivery.Setup(d => d.CheckStatusAsync(_profile, "FD24027000I2026000123", It.IsAny())) + .ReturnsAsync(new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Accepted, StatusCode = 200, ExternalId = "FD24027000I2026000123", ExternalStatus = "APPROVED", ResponseJson = "{\"incident_status\":{\"status\":\"APPROVED\"}}" }); + + var result = await _service.ProcessAsync(submission); + + result.State.Should().Be((int)RmsSubmissionState.Accepted); + result.Attempts.Should().Be(0, "a status poll is not a delivery attempt"); + result.CompletedOn.Should().NotBeNull(); + result.NextAttemptOn.Should().BeNull(); + var report = _store.Reports.Single(); + report.State.Should().Be((int)RmsRecordState.Accepted); + report.AcceptedOn.Should().NotBeNull(); + + var entry = _store.Outbox.Single(); + entry.TriggerEventType.Should().Be((int)WorkflowTriggerEventType.RecordSubmissionAccepted); + entry.AggregateType.Should().Be(IncidentReportsService.IncidentAggregate); + var payload = JObject.Parse(entry.PayloadJson); + payload["submission"]["state"].Value().Should().Be("Accepted"); + payload["submission"]["external_status"].Value().Should().Be("APPROVED"); + payload["record"]["neris_incident_id"].Value().Should().Be("FD24027000I2026000123"); + payload["record_change"]["previous_state"].Value().Should().Be("Submitted"); + _notifications.Should().BeEmpty(); + _delivery.Verify(d => d.DeliverAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task Rejected_outcome_raises_110_and_notification_33_with_codes_and_paths_only() + { + var submission = Seed(RmsRecordState.Finalized, RmsSubmissionState.Queued); + _delivery.Setup(d => d.DeliverAsync(_profile, submission, null, It.IsAny())) + .ReturnsAsync(new NerisSubmissionOutcome + { + Kind = NerisOutcomeKind.Rejected, StatusCode = 422, ExternalStatus = "REJECTED", ResponseJson = "{\"detail\":[{\"loc\":[\"body\",\"dispatch\",\"call_answered\"],\"msg\":\"field required\",\"type\":\"value_error.missing\"}]}", + Errors = new List { new NerisSubmissionError { Code = "value_error.missing", FieldPath = "dispatch.call_answered", Message = "field required" }, new NerisSubmissionError { Code = "invalid_entity" } } + }); + + var result = await _service.ProcessAsync(submission); + + result.State.Should().Be((int)RmsSubmissionState.Rejected); + result.ErrorSummary.Should().Be("dispatch.call_answered (value_error.missing); invalid_entity"); + result.CompletedOn.Should().NotBeNull(); + var report = _store.Reports.Single(); + report.State.Should().Be((int)RmsRecordState.Rejected); + report.RejectedOn.Should().NotBeNull(); + report.RejectionSummary.Should().Be(result.ErrorSummary); + + var entry = _store.Outbox.Single(); + entry.TriggerEventType.Should().Be((int)WorkflowTriggerEventType.RecordSubmissionRejected); + var payload = JObject.Parse(entry.PayloadJson); + payload["submission"]["error_summary"].Value().Should().Be(result.ErrorSummary); + entry.PayloadJson.Should().NotContain("field required", "destination response bodies never reach the Workflow payload"); + + var notification = _notifications.Single(); + notification.Type.Should().Be((int)EventTypes.RecordSubmissionRejected); + notification.DepartmentId.Should().Be(Dept); + notification.Value.Should().Be(ReportId); + notification.UserId.Should().Be("author"); + } + + [Test] + public async Task Transient_outcome_backs_off_then_fails_after_MaxAttempts_with_111() + { + var submission = Seed(RmsRecordState.Finalized, RmsSubmissionState.Queued, maxAttempts: 2); + _delivery.Setup(d => d.DeliverAsync(_profile, submission, null, It.IsAny())) + .ReturnsAsync(new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Transient, StatusCode = 503, Message = "NERIS returned 503." }); + + var first = await _service.ProcessAsync(submission); + first.State.Should().Be((int)RmsSubmissionState.Queued); + first.Attempts.Should().Be(1); + first.NextAttemptOn.Should().BeCloseTo(DateTime.UtcNow.AddMinutes(RecordsSubmissionService.Backoff(1)), TimeSpan.FromSeconds(10)); + first.ErrorSummary.Should().Be("NERIS returned 503."); + first.LeaseOwner.Should().BeNull(); + _store.Reports.Single().State.Should().Be((int)RmsRecordState.Finalized, "a deferred delivery leaves the report where it was"); + _store.Outbox.Should().BeEmpty(); + + var second = await _service.ProcessAsync(submission); + second.State.Should().Be((int)RmsSubmissionState.Failed); + second.Attempts.Should().Be(2); + second.CompletedOn.Should().NotBeNull(); + second.ErrorSummary.Should().StartWith("Delivery exhausted its retries"); + _store.Outbox.Should().ContainSingle(o => o.TriggerEventType == (int)WorkflowTriggerEventType.RecordSubmissionFailed); + _store.Reports.Single().LastSubmissionState.Should().Be((int)RmsSubmissionState.Failed); + _notifications.Should().BeEmpty("a failed delivery is an operator concern, not the author's"); + } + + [Test] + public async Task Fatal_outcome_fails_immediately() + { + var submission = Seed(RmsRecordState.Finalized, RmsSubmissionState.Queued, maxAttempts: 5); + _delivery.Setup(d => d.DeliverAsync(_profile, submission, null, It.IsAny())) + .ReturnsAsync(new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Fatal, StatusCode = 401, Message = "The NERIS credential was refused." }); + + var result = await _service.ProcessAsync(submission); + + result.State.Should().Be((int)RmsSubmissionState.Failed); + result.ErrorSummary.Should().Be("The NERIS credential was refused."); + _store.Outbox.Should().ContainSingle(o => o.TriggerEventType == (int)WorkflowTriggerEventType.RecordSubmissionFailed); + _store.Audits.Single().Successful.Should().BeFalse(); + } + + [Test] + public async Task Voided_report_supersedes_the_submission_without_delivery() + { + var submission = Seed(RmsRecordState.Voided, RmsSubmissionState.Queued); + + var result = await _service.ProcessAsync(submission); + + result.State.Should().Be((int)RmsSubmissionState.Superseded); + result.CompletedOn.Should().NotBeNull(); + result.LeaseOwner.Should().BeNull(); + _delivery.Verify(d => d.DeliverAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + _store.Outbox.Should().BeEmpty(); + } + + [Test] + public async Task Disabled_destination_defers_without_delivery() + { + _enabled = false; + var submission = Seed(RmsRecordState.Finalized, RmsSubmissionState.Queued); + + var result = await _service.ProcessAsync(submission); + + result.State.Should().Be((int)RmsSubmissionState.Queued); + result.Attempts.Should().Be(0); + result.NextAttemptOn.Should().NotBeNull(); + result.LeaseOwner.Should().BeNull(); + _delivery.Verify(d => d.DeliverAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task Missing_report_fails_the_submission() + { + var submission = new RmsSubmission { RmsSubmissionId = "orphan", DepartmentId = Dept, RecordId = "gone", State = (int)RmsSubmissionState.Queued, MaxAttempts = 5, QueuedOn = DateTime.UtcNow }; + _store.Submissions.Add(submission); + + var result = await _service.ProcessAsync(submission); + + result.State.Should().Be((int)RmsSubmissionState.Failed); + result.ErrorSummary.Should().Be("The report no longer exists."); + _store.Outbox.Should().BeEmpty("there is no aggregate to raise an event for"); + } + + [Test] + public async Task Sweep_claims_due_rows_and_counts_outcomes() + { + var a = Seed(RmsRecordState.Finalized, RmsSubmissionState.Queued, id: "a"); + var b = Seed(RmsRecordState.Finalized, RmsSubmissionState.Queued, id: "b", reportId: "report-2"); + var notDue = Seed(RmsRecordState.Finalized, RmsSubmissionState.Queued, id: "c", reportId: "report-3"); + notDue.NextAttemptOn = DateTime.UtcNow.AddHours(1); + _delivery.Setup(d => d.DeliverAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync((RmsNerisProfile p, RmsSubmission s, string existing, CancellationToken c) => new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Created, StatusCode = 201, ExternalId = "FD24027000I" + s.RmsSubmissionId, ExternalStatus = "SUBMITTED" }); + + var result = await _service.SweepAsync(); + + result.Claimed.Should().Be(2); + result.Delivered.Should().Be(2); + result.Errors.Should().Be(0); + a.State.Should().Be((int)RmsSubmissionState.AwaitingDestination); + b.State.Should().Be((int)RmsSubmissionState.AwaitingDestination); + notDue.State.Should().Be((int)RmsSubmissionState.Queued); + result.Message.Should().Contain("claimed 2"); + } + + [Test] + public async Task Sweep_is_a_no_op_while_NERIS_is_disabled() + { + NerisConfig.Enabled = false; + Seed(RmsRecordState.Finalized, RmsSubmissionState.Queued); + + var result = await _service.SweepAsync(); + + result.Claimed.Should().Be(0); + _store.Submissions.Single().LeaseOwner.Should().BeNull(); + } + + [Test] + public void Backoff_doubles_from_the_configured_base_and_caps_at_one_day() + { + var baseMinutes = Math.Max(1, NerisConfig.RetryBackoffMinutes); + + RecordsSubmissionService.Backoff(1).Should().Be(baseMinutes); + RecordsSubmissionService.Backoff(2).Should().Be(baseMinutes * 2); + RecordsSubmissionService.Backoff(3).Should().Be(baseMinutes * 4); + RecordsSubmissionService.Backoff(40).Should().Be(24 * 60); + } + + [Test] + public void Summarize_uses_codes_and_field_paths_never_messages() + { + var outcome = new NerisSubmissionOutcome + { + Kind = NerisOutcomeKind.Rejected, + Message = "Unprocessable Entity", + Errors = new List { new NerisSubmissionError { Code = "missing", FieldPath = "base.location", Message = "Location with PII" } } + }; + + RecordsSubmissionService.Summarize(outcome).Should().Be("base.location (missing)"); + RecordsSubmissionService.Summarize(new NerisSubmissionOutcome { Kind = NerisOutcomeKind.Rejected, Message = "Rejected." }).Should().Be("Rejected."); + } + + private RmsSubmission Seed(RmsRecordState reportState, RmsSubmissionState submissionState, int maxAttempts = 5, string id = "sub-1", string reportId = ReportId, string externalId = null) + { + var now = DateTime.UtcNow; + if (_store.Reports.All(r => r.RmsIncidentReportId != reportId)) + { + _store.Reports.Add(new RmsIncidentReport + { + RmsIncidentReportId = reportId, DepartmentId = Dept, CallId = 77, ReportingEntityId = "FD24027000", DefinitionKey = RmsDefinitionKeys.NerisIncidentReport, + DefinitionVersion = 1, LifecyclePreset = (int)RmsLifecyclePreset.QuickEntry, State = (int)reportState, RecordNumber = "INC-2026-0001", DraftReference = "I-ABCDE", + IncidentNumber = "2026-000123", CurrentRevisionId = "rev-1", RevisionCount = 1, AuthorUserId = "author", OwnerUserId = "author", NerisIncidentId = externalId, + CreatedOn = now, ModifiedOn = now, RowVersion = 3 + }); + _store.Projections.Add(new RmsRecordSearchProjection { RmsRecordSearchProjectionId = reportId, DepartmentId = Dept, RecordKind = (int)RmsRecordKind.IncidentReport, State = (int)reportState, SearchText = "INC-2026-0001", RowVersion = 1 }); + } + + var submission = new RmsSubmission + { + RmsSubmissionId = id, DepartmentId = Dept, RecordId = reportId, RecordKind = (int)RmsRecordKind.IncidentReport, RevisionId = "rev-1", + Destination = RmsSubmissionDestinations.Neris, DestinationVersion = "1.4.78", IdempotencyKey = "key-" + id, State = (int)submissionState, + MaxAttempts = maxAttempts, ExternalId = externalId, PayloadJson = "{}", PayloadChecksum = RecordSnapshotSerializer.Checksum("{}"), + QueuedOn = now.AddMinutes(-5), CreatedByUserId = "author", CreatedOn = now, ModifiedOn = now, RowVersion = 1 + }; + _store.Submissions.Add(submission); + return submission; + } + } +} diff --git a/Tests/Resgrid.Tests/Rms/RmsCascadeSafetyTests.cs b/Tests/Resgrid.Tests/Rms/RmsCascadeSafetyTests.cs index 23070778..bc4af4eb 100644 --- a/Tests/Resgrid.Tests/Rms/RmsCascadeSafetyTests.cs +++ b/Tests/Resgrid.Tests/Rms/RmsCascadeSafetyTests.cs @@ -25,7 +25,9 @@ public class RmsCascadeSafetyTests typeof(RmsOperationalRecord), typeof(RmsOperationalRecordDetail), typeof(RmsRevision), typeof(RmsRecordParticipant), typeof(RmsRecordUnitResponse), typeof(RmsRecordAttachment), typeof(RmsExternalReference), typeof(RmsRecordGroupScope), typeof(RmsRecordShare), typeof(RmsAccessAudit), typeof(RmsRecordSearchProjection), typeof(RmsDepartmentCutover), - typeof(RmsDepartmentCutoverEvent), typeof(DomainEventOutboxEntry), typeof(RmsRecordPrintLayout) + typeof(RmsDepartmentCutoverEvent), typeof(DomainEventOutboxEntry), typeof(RmsRecordPrintLayout), + typeof(RmsIncidentReport), typeof(RmsSourceFact), typeof(RmsUnitResponse), typeof(RmsIncidentType), typeof(RmsActionTactic), typeof(RmsAid), + typeof(RmsLocation), typeof(RmsNarrative), typeof(RmsValidationIssue), typeof(RmsSubmission), typeof(RmsSignature), typeof(RmsNerisProfile), typeof(RmsNerisCrosswalk) }; [Test] @@ -58,7 +60,7 @@ public void Every_rms_entity_declares_the_tenant_and_protection_columns() foreach (var type in Rms1Entities.Where(t => t != typeof(DomainEventOutboxEntry))) { type.GetProperty("DepartmentId").Should().NotBeNull($"{type.Name} needs DepartmentId"); - if (type != typeof(RmsRecordGroupScope) && type != typeof(RmsDepartmentCutoverEvent) && type != typeof(RmsRecordPrintLayout) && type != typeof(RmsAccessAudit)) + if (type != typeof(RmsRecordGroupScope) && type != typeof(RmsDepartmentCutoverEvent) && type != typeof(RmsRecordPrintLayout) && type != typeof(RmsAccessAudit) && type != typeof(RmsValidationIssue)) type.GetProperty("ProtectionId").Should().NotBeNull($"{type.Name} needs ProtectionId"); } } diff --git a/Tests/Resgrid.Tests/Rms/RmsContainerCompositionTests.cs b/Tests/Resgrid.Tests/Rms/RmsContainerCompositionTests.cs index 04094f14..4f90acb6 100644 --- a/Tests/Resgrid.Tests/Rms/RmsContainerCompositionTests.cs +++ b/Tests/Resgrid.Tests/Rms/RmsContainerCompositionTests.cs @@ -22,6 +22,17 @@ public void Records_services_resolve_from_the_container() Resolve().Should().NotBeNull(); Resolve().Should().NotBeNull(); Resolve().Should().NotBeNull(); + // RMS-2 NERIS boundary + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); Resolve().Should().NotBeNull(); Resolve().Should().NotBeNull(); Resolve().Should().NotBeNull(); @@ -54,6 +65,21 @@ public void Records_repositories_resolve_from_the_container() Resolve().Should().NotBeNull(); Resolve().Should().NotBeNull(); Resolve().Should().NotBeNull(); + // RMS-2 (M0164-M0166) + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); + Resolve().Should().NotBeNull(); } [Test] diff --git a/Tests/Resgrid.Tests/Rms/RmsIdentifierPinTests.cs b/Tests/Resgrid.Tests/Rms/RmsIdentifierPinTests.cs index c1140909..e5b56e88 100644 --- a/Tests/Resgrid.Tests/Rms/RmsIdentifierPinTests.cs +++ b/Tests/Resgrid.Tests/Rms/RmsIdentifierPinTests.cs @@ -74,6 +74,10 @@ public void Workflow_triggers_in_the_rms_1_subset_are_the_registry_values() ((int)WorkflowTriggerEventType.RecordSubmittedForReview).Should().Be(101); ((int)WorkflowTriggerEventType.RecordReturnedForCorrection).Should().Be(102); ((int)WorkflowTriggerEventType.RecordFinalized).Should().Be(104); + ((int)WorkflowTriggerEventType.RecordSubmissionQueued).Should().Be(108); + ((int)WorkflowTriggerEventType.RecordSubmissionAccepted).Should().Be(109); + ((int)WorkflowTriggerEventType.RecordSubmissionRejected).Should().Be(110); + ((int)WorkflowTriggerEventType.RecordSubmissionFailed).Should().Be(111); ((int)WorkflowTriggerEventType.RecordAmended).Should().Be(105); ((int)WorkflowTriggerEventType.RecordVoided).Should().Be(106); ((int)WorkflowTriggerEventType.RecordCancelled).Should().Be(107); diff --git a/Tests/Resgrid.Tests/Web/Services/RecordsApiControllerTests.cs b/Tests/Resgrid.Tests/Web/Services/RecordsApiControllerTests.cs new file mode 100644 index 00000000..37945a0c --- /dev/null +++ b/Tests/Resgrid.Tests/Web/Services/RecordsApiControllerTests.cs @@ -0,0 +1,392 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; +using Moq; +using NUnit.Framework; +using Resgrid.Model; +using Resgrid.Model.Providers; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; +using Resgrid.Providers.Claims; +using Resgrid.Web.Services.Controllers.v4; +using Resgrid.Web.Services.Models.v4.Records; +using Resgrid.Web.ServicesCore.Helpers; + +namespace Resgrid.Tests.Web.Services +{ + /// + /// v4 Records contract (RMS-1B, plan 5.3/5.4/5.9.1): flag gate, capability manifest with the protection block, + /// ETag on reads, 409 conflict with changed field paths, idempotent create replay, command idempotency, field-client + /// flag gate, delta cursor with tombstones, restricted-field withholding, visibility denial. + /// + [TestFixture] + public class RecordsApiControllerTests + { + private const int Dept = 42; + private const string Me = "author"; + private const string RecordId = "11111111-1111-1111-1111-111111111111"; + + private Mock _records; + private Mock _cutover; + private Mock _authorization; + private Mock _flags; + private Mock _settings; + private Mock _adp; + private Mock _search; + private Mock _uploads; + private Mock _idempotency; + private RecordsModuleState _moduleState; + private RecordsController _controller; + private DefaultHttpContext _http; + private Activity _activity; + + [SetUp] + public void SetUp() + { + _records = new Mock(); + _cutover = new Mock(); + _moduleState = new RecordsModuleState { DepartmentId = Dept, FlagEnabled = true, Activated = true, CutoverState = RmsDepartmentCutoverState.Active, LegacyWritesBlocked = true }; + _cutover.Setup(c => c.GetModuleStateAsync(Dept, It.IsAny())).ReturnsAsync(() => _moduleState); + + _authorization = new Mock(); + _authorization.Setup(a => a.CanUserViewRecordAsync(Me, It.IsAny(), Dept)).ReturnsAsync(true); + _authorization.Setup(a => a.GetVisibleGroupIdsAsync(Me, Dept)).ReturnsAsync((List)null); + + _flags = new Mock(); + _flags.Setup(f => f.IsEnabledAsync(It.IsAny(), Dept, It.IsAny(), It.IsAny>())).ReturnsAsync(false); + + _settings = new Mock(); + _settings.Setup(s => s.GetRecordsGroupVisibilityModeAsync(Dept, It.IsAny())).ReturnsAsync(RecordsGroupVisibilityMode.DepartmentWide); + _adp = new Mock(); + _search = new Mock(); + _search.SetupGet(s => s.IsAvailable).Returns(false); + _uploads = new Mock(); + _uploads.SetupGet(u => u.ChunkSize).Returns(512 * 1024); + _idempotency = new Mock(); + + _http = new DefaultHttpContext + { + User = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.PrimarySid, Me), + new Claim(ClaimTypes.PrimaryGroupSid, Dept.ToString()), + new Claim(ResgridClaimTypes.Resources.Record, ResgridClaimTypes.Actions.View), + new Claim(ResgridClaimTypes.Resources.Record, ResgridClaimTypes.Actions.Create), + new Claim(ResgridClaimTypes.Resources.Record, ResgridClaimTypes.Actions.Finalize) + }, "test")) + }; + _http.Connection.RemoteIpAddress = System.Net.IPAddress.Loopback; + ClaimsAuthorizationHelper._httpContextAccessor = new HttpContextAccessor { HttpContext = _http }; + _activity = new Activity("RecordsApiControllerTests").Start(); + + _controller = new RecordsController(_records.Object, _cutover.Object, _authorization.Object, _flags.Object, _settings.Object, _adp.Object, _search.Object, _uploads.Object, _idempotency.Object) + { + ControllerContext = new ControllerContext { HttpContext = _http } + }; + } + + [TearDown] + public void TearDown() + { + _activity?.Stop(); + } + + private static RecordAggregate Aggregate(long rowVersion = 3, string definitionKey = RmsDefinitionKeys.Training, RmsRecordState state = RmsRecordState.Draft) + { + return new RecordAggregate + { + Record = new RmsOperationalRecord + { + RmsOperationalRecordId = RecordId, DepartmentId = Dept, DefinitionKey = definitionKey, DefinitionVersion = 1, + RecordType = (int)RmsDefinitionKeys.LockedTypes[definitionKey], LifecyclePreset = (int)RmsLifecyclePreset.QuickEntry, State = (int)state, + DraftReference = "D-ABCDE", AuthorUserId = Me, OwnerUserId = Me, RowVersion = rowVersion, CreatedOn = DateTime.UtcNow, ModifiedOn = DateTime.UtcNow + }, + Details = new RmsOperationalRecordDetail { Narrative = "Drill", Course = "CPR", CaseNumber = "C-1", BodyLocation = "Room 2" } + }; + } + + [Test] + public async Task Flag_off_hides_every_endpoint() + { + _moduleState.FlagEnabled = false; + + (await _controller.Capabilities()).Result.Should().BeOfType(); + (await _controller.GetRecord(RecordId)).Result.Should().BeOfType(); + (await _controller.GetRecords(null, null, null, null, null, null)).Result.Should().BeOfType(); + (await _controller.Changes()).Result.Should().BeOfType(); + (await _controller.CreateDraft(new SaveRecordDraftInput { DefinitionKey = RmsDefinitionKeys.Training }, CancellationToken.None)).Result.Should().BeOfType(); + } + + [Test] + public async Task Capabilities_carry_the_contract_definitions_permissions_and_the_inert_protection_block() + { + _flags.Setup(f => f.IsEnabledAsync(FeatureFlagKeys.RecordsFieldResponder, Dept, It.IsAny(), It.IsAny>())).ReturnsAsync(true); + + var response = await _controller.Capabilities(); + + var data = ((RecordsCapabilitiesResult)((OkObjectResult)response.Result).Value).Data; + data.ContractVersion.Should().Be(RecordsApiContract.Version); + data.RecordsUsable.Should().BeTrue(); + data.Permissions.CanCreate.Should().BeTrue(); + data.Permissions.CanFinalize.Should().BeTrue(); + data.Permissions.CanVoid.Should().BeFalse(); + data.FieldClients.Responder.Should().BeTrue(); + data.FieldClients.Unit.Should().BeFalse(); + data.Definitions.Select(d => d.Key).Should().Contain(RmsDefinitionKeys.LockedTypes.Keys); + data.Definitions.Should().OnlyContain(d => d.MinimumClientCapability == RecordsApiContract.LockedDefinitionCapability); + data.Protection.State.Should().Be("NotInstalled"); + data.Protection.CatalogVersion.Should().Be(0); + data.Protection.GrantExpiresOn.Should().BeNull(); + data.Protection.StepUpWindowMinutes.Should().BeNull(); + data.Protection.MinimumClientVersion.Should().BeNull(); + data.Search.Available.Should().BeFalse(); + data.MaxAttachmentBytes.Should().Be(Resgrid.Services.Records.RecordAttachmentHygiene.MaxBytes); + data.GroupVisibilityMode.Should().Be("DepartmentWide"); + } + + [Test] + public async Task Capabilities_protection_block_reflects_an_enrolled_department() + { + _adp.Setup(a => a.GetPolicyByDepartmentIdAsync(Dept, It.IsAny())).ReturnsAsync(new DepartmentDataProtectionPolicy { State = (int)DepartmentDataProtectionState.Enabled, CatalogVersion = 3, StepUpWindowMinutes = 15 }); + + var data = ((RecordsCapabilitiesResult)((OkObjectResult)(await _controller.Capabilities()).Result).Value).Data; + + data.Protection.State.Should().Be("Enabled"); + data.Protection.CatalogVersion.Should().Be(3); + data.Protection.StepUpWindowMinutes.Should().Be(15); + } + + [Test] + public async Task Get_record_sets_the_ETag_audits_the_read_and_withholds_restricted_fields() + { + _records.Setup(r => r.GetAsync(Dept, RecordId, true)).ReturnsAsync(Aggregate(7, RmsDefinitionKeys.Coroner)); + + var response = await _controller.GetRecord(RecordId); + + var result = (RecordResult)((OkObjectResult)response.Result).Value; + result.Data.ETag.Should().Be("W/\"7\""); + _http.Response.Headers[HeaderNames.ETag].ToString().Should().Be("W/\"7\""); + result.Data.IsRestricted.Should().BeTrue(); + result.Data.Details.Narrative.Should().Be("Drill", "the narrative is not part of the restricted section"); + result.Data.Details.CaseNumber.Should().BeNull("the caller lacks RecordRestricted_View"); + result.Data.WithheldFields.Should().Contain(new[] { "CaseNumber", "BodyLocation" }); + result.Data.AvailableTransitions.Should().Contain("Finalized"); + _records.Verify(r => r.RecordAccessAsync(Dept, Me, RecordId, null, RmsAccessAuditAction.Read, null, It.IsAny(), RmsOriginClient.Api), Times.Once); + } + + [Test] + public async Task Visibility_refusal_is_a_404_and_an_audited_denial() + { + _authorization.Setup(a => a.CanUserViewRecordAsync(Me, RecordId, Dept)).ReturnsAsync(false); + + (await _controller.GetRecord(RecordId)).Result.Should().BeOfType(); + + _records.Verify(r => r.GetAsync(Dept, RecordId, It.IsAny()), Times.Never); + _records.Verify(r => r.RecordAccessAsync(Dept, Me, RecordId, null, RmsAccessAuditAction.Denied, null, It.IsAny(), RmsOriginClient.Api), Times.Once); + } + + [Test] + public async Task Create_draft_returns_201_and_a_replayed_idempotency_key_returns_200_with_the_same_record() + { + RecordDraftInput captured = null; + _records.Setup(r => r.CreateDraftAsync(Dept, Me, It.IsAny(), It.IsAny())) + .ReturnsAsync((int d, string u, RecordDraftInput input, CancellationToken c) => { captured = input; return Aggregate(input.IdempotencyKey == "replay" ? 4 : 1); }); + _http.Request.Headers[RecordsApiContract.IdempotencyKeyHeader] = "k-1"; + + var created = await _controller.CreateDraft(new SaveRecordDraftInput { DefinitionKey = RmsDefinitionKeys.Training, ClientRecordId = RecordId, Details = new RecordDetailsInput { Narrative = "Drill" }, OriginClient = (int)RmsOriginClient.Web }, CancellationToken.None); + + created.Result.Should().BeOfType().Which.StatusCode.Should().Be(StatusCodes.Status201Created); + captured.IdempotencyKey.Should().Be("k-1", "the Idempotency-Key header is honored"); + captured.ClientRecordId.Should().Be(RecordId); + captured.OriginClient.Should().Be(RmsOriginClient.Api, "Web is never an API origin"); + + var replayed = await _controller.CreateDraft(new SaveRecordDraftInput { DefinitionKey = RmsDefinitionKeys.Training, IdempotencyKey = "replay" }, CancellationToken.None); + replayed.Result.Should().BeOfType(); + ((RecordResult)((OkObjectResult)replayed.Result).Value).Data.RecordId.Should().Be(RecordId); + } + + [Test] + public async Task Field_client_below_its_flag_fails_closed_for_authoring() + { + var response = await _controller.CreateDraft(new SaveRecordDraftInput { DefinitionKey = RmsDefinitionKeys.Training, OriginClient = (int)RmsOriginClient.Responder }, CancellationToken.None); + + var problem = response.Result.Should().BeOfType().Which; + problem.StatusCode.Should().Be(StatusCodes.Status403Forbidden); + ((ProblemDetails)problem.Value).Type.Should().Be("field_records_disabled"); + _records.Verify(r => r.CreateDraftAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + + _flags.Setup(f => f.IsEnabledAsync(FeatureFlagKeys.RecordsFieldResponder, Dept, It.IsAny(), It.IsAny>())).ReturnsAsync(true); + _records.Setup(r => r.CreateDraftAsync(Dept, Me, It.IsAny(), It.IsAny())).ReturnsAsync(Aggregate(1)); + (await _controller.CreateDraft(new SaveRecordDraftInput { DefinitionKey = RmsDefinitionKeys.Training, OriginClient = (int)RmsOriginClient.Responder }, CancellationToken.None)) + .Result.Should().BeOfType().Which.StatusCode.Should().Be(StatusCodes.Status201Created); + } + + [Test] + public async Task Stale_save_returns_409_with_the_current_record_and_the_changed_field_paths() + { + _records.Setup(r => r.GetAsync(Dept, RecordId, It.IsAny())).ReturnsAsync(Aggregate(5)); + _records.Setup(r => r.SaveDraftAsync(Dept, Me, RecordId, 4, It.IsAny(), It.IsAny())) + .ThrowsAsync(new RecordConcurrencyException(RecordId, 4, 5)); + + var response = await _controller.SaveDraft(new SaveRecordDraftInput { RecordId = RecordId, RowVersion = 4, Details = new RecordDetailsInput { Narrative = "Mine", Course = "CPR", CaseNumber = "C-1", BodyLocation = "Room 2" } }, CancellationToken.None); + + var conflict = response.Result.Should().BeOfType().Which; + var data = ((RecordConflictResult)conflict.Value).Data; + data.ExpectedRowVersion.Should().Be(4); + data.CurrentRowVersion.Should().Be(5); + data.CurrentStateName.Should().Be("Draft"); + data.ChangedFieldPaths.Should().BeEquivalentTo(new[] { "Details.Narrative" }); + data.Current.Details.Narrative.Should().Be("Drill"); + _http.Response.Headers[HeaderNames.ETag].ToString().Should().Be("W/\"5\""); + } + + [Test] + public async Task Save_takes_the_row_version_from_If_Match_when_the_body_has_none() + { + _records.Setup(r => r.GetAsync(Dept, RecordId, It.IsAny())).ReturnsAsync(Aggregate(5)); + _records.Setup(r => r.SaveDraftAsync(Dept, Me, RecordId, 5, It.IsAny(), It.IsAny())).ReturnsAsync(Aggregate(6)); + _http.Request.Headers[HeaderNames.IfMatch] = "W/\"5\""; + + var response = await _controller.SaveDraft(new SaveRecordDraftInput { RecordId = RecordId, Details = new RecordDetailsInput { Narrative = "Drill" } }, CancellationToken.None); + + ((RecordResult)((OkObjectResult)response.Result).Value).Data.RowVersion.Should().Be(6); + } + + [Test] + public async Task Save_without_any_precondition_is_refused() + { + var response = await _controller.SaveDraft(new SaveRecordDraftInput { RecordId = RecordId }, CancellationToken.None); + + response.Result.Should().BeOfType().Which.StatusCode.Should().Be(StatusCodes.Status428PreconditionRequired); + _records.Verify(r => r.SaveDraftAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task Finalize_requires_attestation_checks_the_row_version_and_remembers_the_idempotency_key() + { + _records.Setup(r => r.GetAsync(Dept, RecordId, It.IsAny())).ReturnsAsync(Aggregate(5)); + _records.Setup(r => r.FinalizeAsync(Dept, Me, RecordId, 5, "1", null, null, It.IsAny())).ReturnsAsync(Aggregate(6, RmsDefinitionKeys.Training, RmsRecordState.Finalized)); + + var unattested = await _controller.Finalize(new RecordCommandInput { RecordId = RecordId, RowVersion = 5 }, CancellationToken.None); + ((ProblemDetails)((ObjectResult)unattested.Result).Value).Type.Should().Be("attestation_required"); + + var finalized = await _controller.Finalize(new RecordCommandInput { RecordId = RecordId, RowVersion = 5, Attested = true, IdempotencyKey = "fin-1" }, CancellationToken.None); + ((RecordResult)((OkObjectResult)finalized.Result).Value).Data.StateName.Should().Be("Finalized"); + _idempotency.Verify(i => i.RememberAsync(Dept, Me, "fin-1", RecordId), Times.Once); + + // Replay: the remembered key short-circuits the transition. + _idempotency.Setup(i => i.TryGetRecordIdAsync(Dept, Me, "fin-1")).ReturnsAsync(RecordId); + var replayed = await _controller.Finalize(new RecordCommandInput { RecordId = RecordId, RowVersion = 99, Attested = true, IdempotencyKey = "fin-1" }, CancellationToken.None); + replayed.Result.Should().BeOfType(); + _records.Verify(r => r.FinalizeAsync(Dept, Me, RecordId, It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [Test] + public async Task Illegal_transition_is_a_409_problem() + { + _records.Setup(r => r.GetAsync(Dept, RecordId, It.IsAny())).ReturnsAsync(Aggregate(5, RmsDefinitionKeys.Training, RmsRecordState.Finalized)); + _records.Setup(r => r.CancelAsync(Dept, Me, RecordId, It.IsAny())).ThrowsAsync(new RecordTransitionException(RecordId, RmsRecordState.Finalized, RmsRecordState.Cancelled)); + + var response = await _controller.Cancel(new RecordCommandInput { RecordId = RecordId }, CancellationToken.None); + + var problem = response.Result.Should().BeOfType().Which; + problem.StatusCode.Should().Be(StatusCodes.Status409Conflict); + ((ProblemDetails)problem.Value).Type.Should().Be("record_transition"); + } + + [Test] + public async Task Changes_returns_a_cursor_tombstones_and_drops_rows_outside_the_callers_scope() + { + var t0 = new DateTime(2026, 9, 3, 10, 0, 0, DateTimeKind.Utc); + _authorization.Setup(a => a.GetVisibleGroupIdsAsync(Me, Dept)).ReturnsAsync(new List { 1 }); + _authorization.Setup(a => a.CanUserViewRecordAsync(Me, "hidden", Dept)).ReturnsAsync(false); + _records.Setup(r => r.GetChangesSinceAsync(Dept, It.IsAny(), 3)).ReturnsAsync(new List + { + new RmsRecordSearchProjection { RmsRecordSearchProjectionId = "live", State = (int)RmsRecordState.Draft, ModifiedOn = t0, RecordCreatedOn = t0 }, + new RmsRecordSearchProjection { RmsRecordSearchProjectionId = "hidden", State = (int)RmsRecordState.Draft, ModifiedOn = t0.AddMinutes(1), RecordCreatedOn = t0 }, + new RmsRecordSearchProjection { RmsRecordSearchProjectionId = "gone", State = (int)RmsRecordState.Voided, ModifiedOn = t0.AddMinutes(2), RecordCreatedOn = t0 } + }); + + var response = await _controller.Changes(0, 2); + + var data = ((RecordsChangesResult)((OkObjectResult)response.Result).Value).Data; + data.HasMore.Should().BeTrue("three rows came back for a page of two"); + data.Records.Select(r => r.RecordId).Should().BeEquivalentTo(new[] { "live" }, "the hidden row is outside the caller's group scope"); + data.ServerTimestampMs.Should().Be(new DateTimeOffset(t0.AddMinutes(1)).ToUnixTimeMilliseconds(), "the cursor stops at the last row of the page so nothing is skipped"); + + _records.Setup(r => r.GetChangesSinceAsync(Dept, It.IsAny(), 3)).ReturnsAsync(new List + { + new RmsRecordSearchProjection { RmsRecordSearchProjectionId = "gone", State = (int)RmsRecordState.Voided, ModifiedOn = t0.AddMinutes(2), RecordCreatedOn = t0 } + }); + var next = ((RecordsChangesResult)((OkObjectResult)(await _controller.Changes(data.ServerTimestampMs, 2)).Result).Value).Data; + next.HasMore.Should().BeFalse(); + next.Records.Should().ContainSingle(r => r.RecordId == "gone" && r.IsTombstone, "tombstones ride through regardless of scope"); + } + + [Test] + public async Task List_passes_the_callers_group_scope_to_the_query() + { + RmsRecordQuery captured = null; + _authorization.Setup(a => a.GetVisibleGroupIdsAsync(Me, Dept)).ReturnsAsync(new List { 1, 2 }); + _records.Setup(r => r.QueryAsync(Dept, It.IsAny())).ReturnsAsync((int d, RmsRecordQuery q) => { captured = q; return new List { new RmsRecordSearchProjection { RmsRecordSearchProjectionId = "a", State = 1 } }; }); + _records.Setup(r => r.CountAsync(Dept, It.IsAny())).ReturnsAsync(1); + + var response = await _controller.GetRecords(2026, RmsDefinitionKeys.Run, (int)RmsRecordState.Draft, null, null, null, 0, 1000); + + var result = (RecordsResult)((OkObjectResult)response.Result).Value; + result.Data.Should().ContainSingle(); + result.Total.Should().Be(1); + captured.VisibleGroupIds.Should().Equal(1, 2); + captured.ViewerUserId.Should().Be(Me); + captured.Take.Should().Be(RecordsController.MaxPageSize, "page size is capped"); + } + + [Test] + public async Task Search_degrades_to_the_filtered_queue_when_the_host_is_off() + { + _records.Setup(r => r.QueryAsync(Dept, It.IsAny())).ReturnsAsync(new List()); + _records.Setup(r => r.CountAsync(Dept, It.IsAny())).ReturnsAsync(0); + + var result = (RecordsResult)((OkObjectResult)(await _controller.Search("smoke", null, null, null)).Result).Value; + + result.SearchDegraded.Should().BeTrue(); + _search.Verify(s => s.SearchAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task Upload_session_errors_map_to_http_codes() + { + _records.Setup(r => r.GetAsync(Dept, RecordId, It.IsAny())).ReturnsAsync(Aggregate(5)); + _uploads.Setup(u => u.BeginAsync(Dept, Me, RecordId, "a.png", "image/png", 10, "x")).ThrowsAsync(new RecordUploadSessionException("checksum_mismatch", "bad hash")); + _uploads.Setup(u => u.AppendAsync(Dept, Me, "s1", 0, It.IsAny())).ThrowsAsync(new RecordUploadSessionException("bad_offset", "order")); + _uploads.Setup(u => u.CompleteAsync(Dept, Me, "s2", null, It.IsAny())).ThrowsAsync(new RecordUploadSessionException("not_found", "gone")); + + ((ObjectResult)(await _controller.BeginUpload(new BeginRecordUploadInput { RecordId = RecordId, FileName = "a.png", ContentType = "image/png", ByteSize = 10, Sha256 = "x" })).Result).StatusCode.Should().Be(StatusCodes.Status422UnprocessableEntity); + ((ObjectResult)(await _controller.UploadChunk(new RecordUploadChunkInput { UploadId = "s1", Offset = 0, Data = Convert.ToBase64String(new byte[] { 1 }) })).Result).StatusCode.Should().Be(StatusCodes.Status409Conflict); + ((ObjectResult)(await _controller.CompleteUpload(new CompleteRecordUploadInput { UploadId = "s2" }, CancellationToken.None)).Result).StatusCode.Should().Be(StatusCodes.Status404NotFound); + } + + [Test] + public async Task Not_activated_department_cannot_write_but_still_reads_the_manifest() + { + _moduleState.Activated = false; + _moduleState.CutoverState = null; + + var manifest = ((RecordsCapabilitiesResult)((OkObjectResult)(await _controller.Capabilities()).Result).Value).Data; + manifest.RecordsUsable.Should().BeFalse(); + + var create = await _controller.CreateDraft(new SaveRecordDraftInput { DefinitionKey = RmsDefinitionKeys.Training }, CancellationToken.None); + var problem = create.Result.Should().BeOfType().Which; + problem.StatusCode.Should().Be(StatusCodes.Status409Conflict); + ((ProblemDetails)problem.Value).Type.Should().Be("records_not_activated"); + } + } +} diff --git a/Web/Resgrid.Web.Services/Controllers/v4/IncidentReportsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/IncidentReportsController.cs new file mode 100644 index 00000000..5667b498 --- /dev/null +++ b/Web/Resgrid.Web.Services/Controllers/v4/IncidentReportsController.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Resgrid.Model; +using Resgrid.Model.Providers; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; +using Resgrid.Providers.Claims; +using Resgrid.Services.Records; +using Resgrid.Web.Services.Helpers; +using Resgrid.Web.Services.Models.v4.Records; +using Resgrid.Web.ServicesCore.Helpers; + +namespace Resgrid.Web.Services.Controllers.v4 +{ + /// + /// NERIS incident reports over the v4 Records contract (RMS-2 aggregate, RMS-1B client contract): one authoritative + /// report per Call, source-aware prefill with provenance, ETag-guarded draft saves, local/destination validation, + /// attestation, and sanitized submission history. Same gates as Records: flag first, per-record visibility on + /// every read, Field Records flags for field clients. + /// + [Route("api/v{VersionId:apiVersion}/[controller]")] + [ApiVersion("4.0")] + [ApiExplorerSettings(GroupName = "v4")] + public class IncidentReportsController : V4AuthenticatedApiControllerbase + { + private readonly IIncidentReportsService _incidentReports; + private readonly IRecordsCutoverService _cutoverService; + private readonly IRecordsAuthorizationService _recordsAuthorizationService; + private readonly INerisProfileService _neris; + private readonly IFeatureToggleService _featureToggleService; + private readonly IRecordsApiIdempotencyService _idempotency; + + public IncidentReportsController(IIncidentReportsService incidentReports, IRecordsCutoverService cutoverService, IRecordsAuthorizationService recordsAuthorizationService, + INerisProfileService neris, IFeatureToggleService featureToggleService, IRecordsApiIdempotencyService idempotency) + { + _incidentReports = incidentReports; + _cutoverService = cutoverService; + _recordsAuthorizationService = recordsAuthorizationService; + _neris = neris; + _featureToggleService = featureToggleService; + _idempotency = idempotency; + } + + #region Reads + + [HttpGet("GetIncidentReports")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> GetIncidentReports(int? year, int? state, int? callId, string owner, int? group, int skip = 0, int take = 50) + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + + var result = new IncidentReportsResult(); + if (moduleState.RecordsUsable) + { + var query = new RmsIncidentReportQuery + { + Year = year, + States = state.HasValue ? new List { state.Value } : null, + CallId = callId, + OwnerUserId = string.IsNullOrWhiteSpace(owner) ? null : owner, + StationGroupId = group, + Skip = Math.Max(0, skip), + Take = Math.Max(1, Math.Min(RecordsController.MaxPageSize, take)) + }; + var visible = await _recordsAuthorizationService.GetVisibleGroupIdsAsync(UserId, DepartmentId); + foreach (var report in await _incidentReports.QueryAsync(DepartmentId, query)) + { + if (visible == null || await _recordsAuthorizationService.CanUserViewRecordAsync(UserId, report.RmsIncidentReportId, DepartmentId)) + result.Data.Add(IncidentReportsApiMapper.ToSummary(report)); + } + result.Total = visible == null ? await _incidentReports.CountAsync(DepartmentId, query) : query.Skip + result.Data.Count; + result.Page = query.Skip / query.Take; + } + + result.PageSize = result.Data.Count; + result.Status = result.Data.Count > 0 ? ResponseHelper.Success : ResponseHelper.NotFound; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + /// One report with all sections, provenance facts, validation issues and sanitized submission history. Sets a weak ETag. + [HttpGet("GetIncidentReport")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> GetIncidentReport(string id) + { + if (!await FlagOnAsync()) + return NotFound(); + var aggregate = await LoadAuthorizedAsync(id, true); + if (aggregate == null) + return NotFound(); + + await _incidentReports.RecordAccessAsync(DepartmentId, UserId, id, null, RmsAccessAuditAction.Read, null, IpAddressHelper.GetRequestIP(Request, true)); + return Ok(await WrapAsync(aggregate)); + } + + /// The authoritative report for a Call, if one exists. + [HttpGet("GetForCall")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> GetForCall(int callId) + { + if (!await FlagOnAsync()) + return NotFound(); + var existing = await _incidentReports.GetForCallAsync(DepartmentId, callId); + if (existing == null) + return NotFound(); + + var aggregate = await LoadAuthorizedAsync(existing.Report.RmsIncidentReportId, true); + if (aggregate == null) + return NotFound(); + return Ok(await WrapAsync(aggregate)); + } + + #endregion + + #region Authoring + + /// Starts the report for a Call with dispatch prefill (SingleAuthoritative): a second start returns the existing report with 200. + [HttpPost("Start")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> Start(StartIncidentReportInput input, CancellationToken cancellationToken) + { + if (input == null || input.CallId <= 0) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + + var origin = RecordsApiHelper.ResolveOrigin(input.OriginClient); + var gate = await FieldClientGateAsync(origin); + if (gate != null) + return gate; + + try + { + var existing = await _incidentReports.GetForCallAsync(DepartmentId, input.CallId); + var aggregate = await _incidentReports.StartFromCallAsync(DepartmentId, UserId, input.CallId, origin, cancellationToken); + var result = await WrapAsync(aggregate, existing != null ? ResponseHelper.Success : ResponseHelper.Created); + return existing != null ? Ok(result) : StatusCode(StatusCodes.Status201Created, result); + } + catch (ArgumentException ex) + { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, type: "record_validation"); + } + } + + /// ETag-guarded draft save; 409 with the current report on a stale row version. + [HttpPost("SaveDraft")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> SaveDraft(SaveIncidentReportDraftInput input, CancellationToken cancellationToken) + { + if (input == null || string.IsNullOrWhiteSpace(input.ReportId)) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + + var rowVersion = RecordsApiHelper.ResolveRowVersion(input.RowVersion, Request); + if (!rowVersion.HasValue) + return Problem(statusCode: StatusCodes.Status428PreconditionRequired, title: "RowVersion or If-Match is required for a draft save.", type: "precondition_required"); + + var origin = RecordsApiHelper.ResolveOrigin(input.OriginClient); + var gate = await FieldClientGateAsync(origin); + if (gate != null) + return gate; + + var current = await LoadAuthorizedAsync(input.ReportId); + if (current == null) + return NotFound(); + if (!CanEditReport(current.Report)) + return Forbid(); + + try + { + var saved = await _incidentReports.SaveDraftAsync(DepartmentId, UserId, input.ReportId, rowVersion.Value, IncidentReportsApiMapper.ToDraftInput(input, origin), cancellationToken); + return Ok(await WrapAsync(saved, ResponseHelper.Updated)); + } + catch (RecordConcurrencyException ex) + { + return await ConflictAsync(input.ReportId, ex.ExpectedRowVersion); + } + catch (ArgumentException ex) + { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, type: "record_validation"); + } + catch (RecordTransitionException ex) + { + return Problem(statusCode: StatusCodes.Status409Conflict, title: ex.Message, type: "record_transition"); + } + } + + /// Runs local validation (and the destination's validate endpoint when IncludeDestination and submission is enabled); issues stay on the report. + [HttpPost("Validate")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> Validate(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + if (input == null || string.IsNullOrWhiteSpace(input.ReportId)) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + if (await LoadAuthorizedAsync(input.ReportId) == null) + return NotFound(); + + try + { + var issues = await _incidentReports.ValidateAsync(DepartmentId, input.ReportId, input.IncludeDestination, cancellationToken); + var result = new IncidentValidationResult { Data = issues.Select(IncidentReportsApiMapper.ToIssue).ToList(), HasBlockingIssues = issues.Any(i => i.Severity == (int)RmsValidationSeverity.Error), Status = ResponseHelper.Success }; + result.PageSize = result.Data.Count; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) + { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, type: "record_validation"); + } + } + + #endregion + + #region Lifecycle + + [HttpPost("SubmitForReview")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Create)] + public Task> SubmitForReview(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, true, rowVersion => _incidentReports.SubmitForReviewAsync(DepartmentId, UserId, input.ReportId, rowVersion, cancellationToken)); + } + + [HttpPost("ReturnForCorrection")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Review)] + public Task> ReturnForCorrection(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _incidentReports.ReturnForCorrectionAsync(DepartmentId, UserId, input.ReportId, input.ReasonCode, input.ReasonText, cancellationToken)); + } + + /// Validate-then-sign: local errors return 422 with the issues; otherwise the revision, attestation and (when the profile allows) the submission are written. + [HttpPost("Finalize")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Finalize)] + public async Task> Finalize(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + if (input != null && !input.Attested) + return Problem(statusCode: StatusCodes.Status400BadRequest, title: "The attestation statement must be accepted to finalize.", type: "attestation_required"); + return await CommandAsync(input, true, rowVersion => _incidentReports.FinalizeAsync(DepartmentId, UserId, input.ReportId, rowVersion, IncidentReportsService.AttestationStatementVersion, IpAddressHelper.GetRequestIP(Request, true), input.ReasonCode, input.ReasonText, cancellationToken)); + } + + /// After a rejection: a new revision and a new idempotency key, back into the submission queue. + [HttpPost("CorrectAndResubmit")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Submit)] + public async Task> CorrectAndResubmit(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + if (input != null && !input.Attested) + return Problem(statusCode: StatusCodes.Status400BadRequest, title: "The attestation statement must be accepted to resubmit.", type: "attestation_required"); + return await CommandAsync(input, true, rowVersion => _incidentReports.CorrectAndResubmitAsync(DepartmentId, UserId, input.ReportId, rowVersion, IncidentReportsService.AttestationStatementVersion, IpAddressHelper.GetRequestIP(Request, true), input.ReasonCode, input.ReasonText, cancellationToken)); + } + + /// Queues (or re-queues) the current revision for the destination. + [HttpPost("Submit")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Submit)] + public Task> Submit(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _incidentReports.QueueSubmissionAsync(DepartmentId, UserId, input.ReportId, cancellationToken)); + } + + [HttpPost("OpenAmendment")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Amend)] + public Task> OpenAmendment(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _incidentReports.OpenAmendmentAsync(DepartmentId, UserId, input.ReportId, cancellationToken)); + } + + [HttpPost("AbandonAmendment")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Amend)] + public Task> AbandonAmendment(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _incidentReports.AbandonAmendmentAsync(DepartmentId, UserId, input.ReportId, cancellationToken)); + } + + [HttpPost("Void")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Void)] + public Task> Void(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _incidentReports.VoidAsync(DepartmentId, UserId, input.ReportId, input.ReasonCode, input.ReasonText, cancellationToken)); + } + + [HttpPost("Cancel")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Void)] + public Task> Cancel(IncidentReportCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _incidentReports.CancelAsync(DepartmentId, UserId, input.ReportId, cancellationToken)); + } + + private async Task> CommandAsync(IncidentReportCommandInput input, bool requiresRowVersion, Func> action) + { + if (input == null || string.IsNullOrWhiteSpace(input.ReportId)) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + + var origin = RecordsApiHelper.ResolveOrigin(input.OriginClient); + var gate = await FieldClientGateAsync(origin); + if (gate != null) + return gate; + + var current = await LoadAuthorizedAsync(input.ReportId); + if (current == null) + return NotFound(); + + var key = RecordsApiHelper.ResolveIdempotencyKey(input.IdempotencyKey, Request); + if (key != null) + { + var replayed = await _idempotency.TryGetRecordIdAsync(DepartmentId, UserId, key); + if (string.Equals(replayed, input.ReportId, StringComparison.Ordinal)) + return Ok(await WrapAsync(await _incidentReports.GetAsync(DepartmentId, input.ReportId, true), ResponseHelper.Success)); + } + + long rowVersion = current.Report.RowVersion; + if (requiresRowVersion) + { + var supplied = RecordsApiHelper.ResolveRowVersion(input.RowVersion, Request); + if (!supplied.HasValue) + return Problem(statusCode: StatusCodes.Status428PreconditionRequired, title: "RowVersion or If-Match is required for this command.", type: "precondition_required"); + rowVersion = supplied.Value; + } + + try + { + var aggregate = await action(rowVersion); + if (key != null) + await _idempotency.RememberAsync(DepartmentId, UserId, key, input.ReportId); + return Ok(await WrapAsync(aggregate, ResponseHelper.Updated)); + } + catch (RecordConcurrencyException ex) + { + return await ConflictAsync(input.ReportId, ex.ExpectedRowVersion); + } + catch (IncidentReportValidationException ex) + { + var result = new IncidentValidationResult { Data = ex.Issues.Select(IncidentReportsApiMapper.ToIssue).ToList(), HasBlockingIssues = true, Status = ResponseHelper.Failure }; + result.PageSize = result.Data.Count; + ResponseHelper.PopulateV4ResponseData(result); + return StatusCode(StatusCodes.Status422UnprocessableEntity, result); + } + catch (RecordTransitionException ex) + { + return Problem(statusCode: StatusCodes.Status409Conflict, title: ex.Message, type: "record_transition"); + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) + { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, type: "record_validation"); + } + } + + #endregion + + #region Helpers + + private async Task FlagOnAsync() + { + return (await _cutoverService.GetModuleStateAsync(DepartmentId)).FlagEnabled; + } + + private async Task UsableAsync() + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + if (!moduleState.RecordsUsable) + return Problem(statusCode: StatusCodes.Status409Conflict, title: "Records is not activated for this department.", type: "records_not_activated"); + return null; + } + + private async Task FieldClientGateAsync(RmsOriginClient origin) + { + var flag = RecordsApiHelper.FieldFlagFor(origin); + if (flag == null || await _featureToggleService.IsEnabledAsync(flag, DepartmentId)) + return null; + return Problem(statusCode: StatusCodes.Status403Forbidden, title: $"Field Records are not enabled for the {origin} app in this department.", type: "field_records_disabled"); + } + + private async Task LoadAuthorizedAsync(string id, bool includeHistory = false) + { + if (string.IsNullOrWhiteSpace(id)) + return null; + + if (!await _recordsAuthorizationService.CanUserViewRecordAsync(UserId, id, DepartmentId)) + { + await _incidentReports.RecordAccessAsync(DepartmentId, UserId, id, null, RmsAccessAuditAction.Denied, null, IpAddressHelper.GetRequestIP(Request, true)); + return null; + } + + return await _incidentReports.GetAsync(DepartmentId, id, includeHistory); + } + + private bool CanEditReport(RmsIncidentReport report) + { + if (!ClaimsAuthorizationHelper.CanCreateRecord()) + return false; + + return ClaimsAuthorizationHelper.IsUserDepartmentAdmin() + || string.Equals(report.OwnerUserId, UserId, StringComparison.OrdinalIgnoreCase) + || string.Equals(report.AuthorUserId, UserId, StringComparison.OrdinalIgnoreCase) + || (report.AmendsRevisionId != null && ClaimsAuthorizationHelper.CanAmendRecords()); + } + + private async Task> ConflictAsync(string reportId, long expectedRowVersion) + { + var current = await _incidentReports.GetAsync(DepartmentId, reportId, true); + if (current == null) + return NotFound(); + + var result = new IncidentReportConflictResult + { + Data = new IncidentReportConflictData + { + ReportId = reportId, + ExpectedRowVersion = expectedRowVersion, + CurrentRowVersion = current.Report.RowVersion, + CurrentState = current.Report.State, + CurrentStateName = ((RmsRecordState)current.Report.State).ToString(), + Current = IncidentReportsApiMapper.ToReport(current, await _neris.IsSubmissionEnabledAsync(DepartmentId)) + }, + PageSize = 1, + Status = RecordsController.ConflictStatus + }; + ResponseHelper.PopulateV4ResponseData(result); + RecordsApiHelper.SetETag(Response, current.Report.RowVersion); + return Conflict(result); + } + + private async Task WrapAsync(IncidentReportAggregate aggregate, string status = ResponseHelper.Success) + { + var result = new IncidentReportResult { Data = IncidentReportsApiMapper.ToReport(aggregate, await _neris.IsSubmissionEnabledAsync(DepartmentId)), PageSize = 1, Status = status }; + ResponseHelper.PopulateV4ResponseData(result); + RecordsApiHelper.SetETag(Response, aggregate.Report.RowVersion); + return result; + } + + #endregion + } +} diff --git a/Web/Resgrid.Web.Services/Controllers/v4/RecordsController.cs b/Web/Resgrid.Web.Services/Controllers/v4/RecordsController.cs new file mode 100644 index 00000000..9cb06521 --- /dev/null +++ b/Web/Resgrid.Web.Services/Controllers/v4/RecordsController.cs @@ -0,0 +1,977 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Resgrid.Framework; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; +using Resgrid.Providers.Claims; +using Resgrid.Services.Records; +using Resgrid.Web.Services.Helpers; +using Resgrid.Web.Services.Models.v4.Records; +using Resgrid.Web.ServicesCore.Helpers; + +namespace Resgrid.Web.Services.Controllers.v4 +{ + /// + /// Records (RMS) client contract, RMS-1B (plan sections 5.3, 5.4, 5.9.1): capability manifest with the locked + /// definition catalog and the Protected Data block, authorization-filtered list/search, a delta cursor with + /// tombstones, ETag-guarded draft saves that return the changed field paths on conflict, scoped idempotency on + /// creates and commands, revision history and diffs, and resumable checksummed attachment uploads. Every action + /// gates on the Records.System flag first; every read passes the per-record visibility rule. + /// + [Route("api/v{VersionId:apiVersion}/[controller]")] + [ApiVersion("4.0")] + [ApiExplorerSettings(GroupName = "v4")] + public class RecordsController : V4AuthenticatedApiControllerbase + { + public const string ConflictStatus = "conflict"; + public const int MaxPageSize = 200; + + private readonly IRecordsService _recordsService; + private readonly IRecordsCutoverService _cutoverService; + private readonly IRecordsAuthorizationService _recordsAuthorizationService; + private readonly IFeatureToggleService _featureToggleService; + private readonly IDepartmentSettingsService _departmentSettingsService; + private readonly IDepartmentDataProtectionService _dataProtectionService; + private readonly IRecordsSearchService _recordsSearch; + private readonly IRecordAttachmentUploadService _uploads; + private readonly IRecordsApiIdempotencyService _idempotency; + + public RecordsController(IRecordsService recordsService, IRecordsCutoverService cutoverService, IRecordsAuthorizationService recordsAuthorizationService, + IFeatureToggleService featureToggleService, IDepartmentSettingsService departmentSettingsService, IDepartmentDataProtectionService dataProtectionService, + IRecordsSearchService recordsSearch, IRecordAttachmentUploadService uploads, IRecordsApiIdempotencyService idempotency) + { + _recordsService = recordsService; + _cutoverService = cutoverService; + _recordsAuthorizationService = recordsAuthorizationService; + _featureToggleService = featureToggleService; + _departmentSettingsService = departmentSettingsService; + _dataProtectionService = dataProtectionService; + _recordsSearch = recordsSearch; + _uploads = uploads; + _idempotency = idempotency; + } + + #region Capabilities + + /// + /// The Records capability manifest: module/cutover state, the caller's effective Record permissions, per-app + /// Field Records flags, the published (locked) definition catalog with field applicability and minimum client + /// capability, search availability, upload limits, and the Protected Data block (plan 5.9.1). + /// + [HttpGet("Capabilities")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> Capabilities() + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + + var data = new RecordsCapabilitiesData + { + ContractVersion = RecordsApiContract.Version, + ModuleEnabled = moduleState.FlagEnabled, + RecordsUsable = moduleState.RecordsUsable, + Activated = moduleState.Activated, + ActivatedOn = moduleState.ActivatedOn, + CutoverState = moduleState.CutoverState?.ToString(), + Permissions = new RecordsPermissionsData + { + CanView = ClaimsAuthorizationHelper.CanViewRecords(), + CanCreate = ClaimsAuthorizationHelper.CanCreateRecord(), + CanReview = ClaimsAuthorizationHelper.CanReviewRecords(), + CanApprove = ClaimsAuthorizationHelper.CanApproveRecords(), + CanFinalize = ClaimsAuthorizationHelper.CanFinalizeRecords(), + CanSubmit = ClaimsAuthorizationHelper.CanSubmitRecords(), + CanAmend = ClaimsAuthorizationHelper.CanAmendRecords(), + CanVoid = ClaimsAuthorizationHelper.CanVoidRecords(), + CanExport = ClaimsAuthorizationHelper.CanExportRecords(), + CanShare = ClaimsAuthorizationHelper.CanShareRecords(), + CanReassign = ClaimsAuthorizationHelper.CanReassignRecordDrafts(), + CanViewRestricted = ClaimsAuthorizationHelper.CanViewRestrictedRecords(), + CanViewLegacy = ClaimsAuthorizationHelper.CanViewLegacyRecords(), + IsDepartmentAdmin = ClaimsAuthorizationHelper.IsUserDepartmentAdmin() + }, + FieldClients = new RecordsFieldClientsData + { + Responder = moduleState.RecordsUsable && await _featureToggleService.IsEnabledAsync(FeatureFlagKeys.RecordsFieldResponder, DepartmentId), + Unit = moduleState.RecordsUsable && await _featureToggleService.IsEnabledAsync(FeatureFlagKeys.RecordsFieldUnit, DepartmentId), + IncidentCommand = moduleState.RecordsUsable && await _featureToggleService.IsEnabledAsync(FeatureFlagKeys.RecordsFieldIncidentCommand, DepartmentId), + Dispatch = moduleState.RecordsUsable && await _featureToggleService.IsEnabledAsync(FeatureFlagKeys.RecordsFieldDispatch, DepartmentId) + }, + Definitions = RecordsApiMapper.ToDefinitions(), + Search = new RecordsSearchCapabilityData { Available = _recordsSearch.IsAvailable, NarrativeAvailable = await NarrativeSearchAvailableAsync() }, + Protection = await ProtectionAsync(), + UploadChunkSize = _uploads.ChunkSize, + MaxAttachmentBytes = RecordAttachmentHygiene.MaxBytes, + ServerTimestampMs = RecordsApiHelper.ToUnixMs(DateTime.UtcNow) + }; + + try + { + data.GroupVisibilityMode = (await _departmentSettingsService.GetRecordsGroupVisibilityModeAsync(DepartmentId)).ToString(); + } + catch (Exception ex) + { + Logging.LogException(ex); + } + + var result = new RecordsCapabilitiesResult { Data = data, PageSize = 1, Status = ResponseHelper.Success }; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + /// Plan 5.9.1: the same shape before and after Protected Data enrollment; NotInstalled while the subsystem is absent. + private async Task ProtectionAsync() + { + var protection = new RecordsProtectionData(); + try + { + var policy = await _dataProtectionService.GetPolicyByDepartmentIdAsync(DepartmentId); + if (policy == null) + return protection; + protection.State = ((DepartmentDataProtectionState)policy.State).ToString(); + protection.CatalogVersion = policy.CatalogVersion; + protection.StepUpWindowMinutes = policy.StepUpWindowMinutes; + } + catch (Exception ex) + { + Logging.LogException(ex, "Records capability protection block fell back to NotInstalled."); + } + return protection; + } + + private async Task NarrativeSearchAvailableAsync() + { + try + { + if (!_recordsSearch.IsAvailable || await _dataProtectionService.IsProtectionEnforcedAsync(DepartmentId)) + return false; + var config = await _departmentSettingsService.GetRecordsSearchConfigAsync(DepartmentId); + return config != null && config.IndexNarrative; + } + catch (Exception ex) + { + Logging.LogException(ex); + return false; + } + } + + #endregion + + #region Lists / delta + + /// The authorization-filtered record queue (safe projection columns only). + [HttpGet("GetRecords")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> GetRecords(int? year, string definitionKey, int? state, int? callId, string owner, int? group, int skip = 0, int take = 50) + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + + var result = new RecordsResult(); + if (!moduleState.RecordsUsable) + { + result.Status = ResponseHelper.NotFound; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + var query = new RmsRecordQuery + { + Year = year, + DefinitionKey = string.IsNullOrWhiteSpace(definitionKey) ? null : definitionKey, + States = state.HasValue ? new List { state.Value } : null, + CallId = callId, + OwnerUserId = string.IsNullOrWhiteSpace(owner) ? null : owner, + StationGroupId = group, + VisibleGroupIds = await _recordsAuthorizationService.GetVisibleGroupIdsAsync(UserId, DepartmentId), + ViewerUserId = UserId, + Skip = Math.Max(0, skip), + Take = Math.Max(1, Math.Min(MaxPageSize, take)) + }; + + result.Data = (await _recordsService.QueryAsync(DepartmentId, query)).Select(RecordsApiMapper.ToSummary).ToList(); + result.Total = await _recordsService.CountAsync(DepartmentId, query); + result.Page = query.Skip / query.Take; + result.PageSize = result.Data.Count; + result.Status = result.Data.Count > 0 ? ResponseHelper.Success : ResponseHelper.NotFound; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + /// + /// Free-text search over the records index (plan 5.10). Hits are re-checked against per-record visibility; + /// when the host is off or unavailable the filtered queue is returned with SearchDegraded set and the text is not applied. + /// + [HttpGet("Search")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> Search(string text, int? year, string definitionKey, int? state, int skip = 0, int take = 50) + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + if (string.IsNullOrWhiteSpace(text)) + return BadRequest(); + + var result = new RecordsResult(); + if (!moduleState.RecordsUsable) + { + result.Status = ResponseHelper.NotFound; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + var visible = await _recordsAuthorizationService.GetVisibleGroupIdsAsync(UserId, DepartmentId); + var states = state.HasValue ? new List { state.Value } : null; + take = Math.Max(1, Math.Min(MaxPageSize, take)); + skip = Math.Max(0, skip); + + RecordsSearchResult search = null; + if (_recordsSearch.IsAvailable) + { + try + { + search = await _recordsSearch.SearchAsync(DepartmentId, new RecordsSearchRequest + { + Text = text.Trim(), VisibleGroupIds = visible, ViewerUserId = UserId, States = states, DefinitionKey = string.IsNullOrWhiteSpace(definitionKey) ? null : definitionKey, Year = year, Skip = skip, Take = take + }); + } + catch (Exception ex) + { + Logging.LogException(ex, "Records API search failed; returning the filtered queue."); + } + } + + if (search == null || !search.Available) + { + // Never quietly reimplemented as LIKE (plan 5.10): the queue renders and the caller is told. + result.SearchDegraded = true; + var query = new RmsRecordQuery { Year = year, DefinitionKey = string.IsNullOrWhiteSpace(definitionKey) ? null : definitionKey, States = states, VisibleGroupIds = visible, ViewerUserId = UserId, Skip = skip, Take = take }; + result.Data = (await _recordsService.QueryAsync(DepartmentId, query)).Select(RecordsApiMapper.ToSummary).ToList(); + result.Total = await _recordsService.CountAsync(DepartmentId, query); + } + else + { + var recordSource = ((int)RmsSearchSourceType.Record).ToString(); + var ids = search.Hits.Where(h => h.SourceType == recordSource && !string.IsNullOrWhiteSpace(h.SourceId)).Select(h => h.SourceId).Distinct().ToList(); + var loaded = (await _recordsService.GetProjectionsByIdsAsync(DepartmentId, ids)).ToDictionary(p => p.RmsRecordSearchProjectionId, StringComparer.OrdinalIgnoreCase); + var dropped = 0; + foreach (var id in ids) + { + if (!loaded.TryGetValue(id, out var projection) || !await _recordsAuthorizationService.CanUserViewRecordAsync(UserId, id, DepartmentId)) + { + dropped++; + continue; + } + result.Data.Add(RecordsApiMapper.ToSummary(projection)); + } + // Totals never disclose a record the caller cannot open. + result.Total = dropped == 0 ? search.Total : skip + result.Data.Count; + result.Truncated = search.Truncated; + } + + result.Page = skip / take; + result.PageSize = result.Data.Count; + result.Status = result.Data.Count > 0 ? ResponseHelper.Success : ResponseHelper.NotFound; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + /// + /// Delta cursor (plan 5.3): records modified after (Unix epoch ms; 0 = full pull), + /// oldest first, with tombstones for cancelled, voided and deleted records so clients remove them locally. + /// Persist Data.ServerTimestampMs and pass it back as the next since; loop while HasMore. + /// + [HttpGet("Changes")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> Changes(long since = 0, int take = 200) + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + + take = Math.Max(1, Math.Min(500, take)); + var result = new RecordsChangesResult { Data = new RecordsChangesData { Since = since } }; + var now = DateTime.UtcNow; + + if (moduleState.RecordsUsable) + { + var visible = await _recordsAuthorizationService.GetVisibleGroupIdsAsync(UserId, DepartmentId); + var rows = await _recordsService.GetChangesSinceAsync(DepartmentId, RecordsApiHelper.FromUnixMs(since), take + 1); + var hasMore = rows.Count > take; + var page = rows.Take(take).ToList(); + foreach (var projection in page) + { + // Tombstones ride through regardless of scope (the client may hold the row); live rows pass the visibility rule. + var summary = RecordsApiMapper.ToSummary(projection); + if (!summary.IsTombstone && visible != null && !await _recordsAuthorizationService.CanUserViewRecordAsync(UserId, projection.RmsRecordSearchProjectionId, DepartmentId)) + continue; + result.Data.Records.Add(summary); + } + result.Data.HasMore = hasMore; + result.Data.ServerTimestampMs = hasMore && page.Count > 0 ? RecordsApiHelper.ToUnixMs(page[page.Count - 1].ModifiedOn) : RecordsApiHelper.ToUnixMs(now); + } + else + { + result.Data.ServerTimestampMs = RecordsApiHelper.ToUnixMs(now); + } + + result.PageSize = result.Data.Records.Count; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + #endregion + + #region Reads + + /// One record with its working details, participants, units, attachment metadata and revision summaries. Sets a weak ETag. + [HttpGet("GetRecord")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> GetRecord(string id) + { + if (!await FlagOnAsync()) + return NotFound(); + + var aggregate = await LoadAuthorizedAsync(id, true); + if (aggregate == null) + return NotFound(); + + await _recordsService.RecordAccessAsync(DepartmentId, UserId, id, null, RmsAccessAuditAction.Read, null, IpAddressHelper.GetRequestIP(Request, true), RmsOriginClient.Api); + return Ok(Wrap(aggregate)); + } + + [HttpGet("GetRevisions")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> GetRevisions(string id) + { + if (!await FlagOnAsync()) + return NotFound(); + if (await LoadAuthorizedAsync(id) == null) + return NotFound(); + + var result = new RecordRevisionsResult { Data = (await _recordsService.GetRevisionsAsync(DepartmentId, id)).OrderByDescending(r => r.RevisionNumber).Select(RecordsApiMapper.ToRevision).ToList() }; + result.PageSize = result.Data.Count; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + /// A revision rendered from its own snapshot; restricted fields withheld without RecordRestricted_View. + [HttpGet("GetRevision")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> GetRevision(string id, string revisionId) + { + if (!await FlagOnAsync()) + return NotFound(); + var aggregate = await LoadAuthorizedAsync(id, true); + if (aggregate == null) + return NotFound(); + + var revision = aggregate.Revisions.FirstOrDefault(r => string.Equals(r.RmsRevisionId, revisionId, StringComparison.Ordinal)); + if (revision == null) + return NotFound(); + + var snapshot = await _recordsService.GetRevisionSnapshotAsync(DepartmentId, revisionId); + if (snapshot == null) + return NotFound(); + + await _recordsService.RecordAccessAsync(DepartmentId, UserId, id, revisionId, RmsAccessAuditAction.Read, "Revision " + revision.RevisionNumber, IpAddressHelper.GetRequestIP(Request, true), RmsOriginClient.Api); + var result = new RecordRevisionSnapshotResult + { + Data = new RecordRevisionSnapshotData { Revision = RecordsApiMapper.ToRevision(revision), Snapshot = RecordsApiMapper.ToRecord(snapshot, ClaimsAuthorizationHelper.CanViewRestrictedRecords()) }, + PageSize = 1, + Status = ResponseHelper.Success + }; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + /// On-demand field-level diff between two revisions of the same record (plan 4.8). + [HttpGet("Diff")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> Diff(string id, string from, string to) + { + if (!await FlagOnAsync()) + return NotFound(); + var aggregate = await LoadAuthorizedAsync(id, true); + if (aggregate == null) + return NotFound(); + if (!aggregate.Revisions.Any(r => r.RmsRevisionId == from) || !aggregate.Revisions.Any(r => r.RmsRevisionId == to)) + return NotFound(); + + var diffs = await _recordsService.DiffRevisionsAsync(DepartmentId, from, to, ClaimsAuthorizationHelper.CanViewRestrictedRecords()); + var result = new RecordDiffResult + { + Data = new RecordDiffData { RecordId = id, FromRevisionId = from, ToRevisionId = to, Diffs = diffs.Select(d => new RecordFieldDiffData { Section = d.Section, FieldKey = d.FieldKey, OldValue = d.OldValue, NewValue = d.NewValue, Withheld = d.Withheld }).ToList() }, + Status = ResponseHelper.Success + }; + result.PageSize = result.Data.Diffs.Count; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + #endregion + + #region Drafts + + /// + /// Creates a draft. IdempotencyKey (body or Idempotency-Key header) makes the create replayable: the same key + /// returns the existing record with 200 instead of a duplicate. Field clients need their Records.Field.* flag. + /// + [HttpPost("CreateDraft")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> CreateDraft(SaveRecordDraftInput input, CancellationToken cancellationToken) + { + if (input == null || !ModelState.IsValid) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + + var origin = RecordsApiHelper.ResolveOrigin(input.OriginClient); + var gate = await FieldClientGateAsync(origin); + if (gate != null) + return gate; + + input.IdempotencyKey = RecordsApiHelper.ResolveIdempotencyKey(input.IdempotencyKey, Request); + try + { + var aggregate = await _recordsService.CreateDraftAsync(DepartmentId, UserId, RecordsApiMapper.ToDraftInput(input, origin), cancellationToken); + var replayed = !string.IsNullOrWhiteSpace(input.IdempotencyKey) && aggregate.Record.RowVersion > 1; + var result = Wrap(aggregate, replayed ? ResponseHelper.Success : ResponseHelper.Created); + return replayed ? Ok(result) : StatusCode(StatusCodes.Status201Created, result); + } + catch (Exception ex) when (ex is ArgumentException || ex is RecordsLegacyWriteBlockedException) + { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, type: "record_validation"); + } + catch (RecordTransitionException ex) + { + return Problem(statusCode: StatusCodes.Status409Conflict, title: ex.Message, type: "record_transition"); + } + } + + /// + /// ETag-guarded draft save. RowVersion (or If-Match) must equal the current row version; otherwise 409 with + /// the current record and the field paths the client's copy would have changed, for deliberate reconciliation. + /// + [HttpPost("SaveDraft")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status409Conflict)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> SaveDraft(SaveRecordDraftInput input, CancellationToken cancellationToken) + { + if (input == null || string.IsNullOrWhiteSpace(input.RecordId)) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + + var rowVersion = RecordsApiHelper.ResolveRowVersion(input.RowVersion, Request); + if (!rowVersion.HasValue) + return Problem(statusCode: StatusCodes.Status428PreconditionRequired, title: "RowVersion or If-Match is required for a draft save.", type: "precondition_required"); + + var origin = RecordsApiHelper.ResolveOrigin(input.OriginClient); + var gate = await FieldClientGateAsync(origin); + if (gate != null) + return gate; + + var current = await LoadAuthorizedAsync(input.RecordId); + if (current == null) + return NotFound(); + if (!CanEditRecord(current.Record)) + return Forbid(); + + var draft = RecordsApiMapper.ToDraftInput(input, origin); + draft.DefinitionKey = draft.DefinitionKey ?? current.Record.DefinitionKey; + try + { + var saved = await _recordsService.SaveDraftAsync(DepartmentId, UserId, input.RecordId, rowVersion.Value, draft, cancellationToken); + return Ok(Wrap(saved, ResponseHelper.Updated)); + } + catch (RecordConcurrencyException ex) + { + return await ConflictAsync(input.RecordId, ex.ExpectedRowVersion, draft); + } + catch (Exception ex) when (ex is ArgumentException || ex is RecordsLegacyWriteBlockedException) + { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, type: "record_validation"); + } + catch (RecordTransitionException ex) + { + return Problem(statusCode: StatusCodes.Status409Conflict, title: ex.Message, type: "record_transition"); + } + } + + #endregion + + #region Lifecycle commands + + [HttpPost("SubmitForReview")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Create)] + public Task> SubmitForReview(RecordCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, true, rowVersion => _recordsService.SubmitForReviewAsync(DepartmentId, UserId, input.RecordId, rowVersion, cancellationToken)); + } + + [HttpPost("ReturnForCorrection")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Review)] + public Task> ReturnForCorrection(RecordCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _recordsService.ReturnForCorrectionAsync(DepartmentId, UserId, input.RecordId, input.ReasonCode, input.ReasonText, cancellationToken)); + } + + [HttpPost("Approve")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Approve)] + public Task> Approve(RecordCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _recordsService.ApproveAsync(DepartmentId, UserId, input.RecordId, cancellationToken)); + } + + /// Finalize: validates, writes the immutable revision and attestation, emits the lifecycle events. Attested must be true. Online-only by design. + [HttpPost("Finalize")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Finalize)] + public async Task> Finalize(RecordCommandInput input, CancellationToken cancellationToken) + { + if (input != null && !input.Attested) + return Problem(statusCode: StatusCodes.Status400BadRequest, title: "The attestation statement must be accepted to finalize.", type: "attestation_required"); + return await CommandAsync(input, true, rowVersion => _recordsService.FinalizeAsync(DepartmentId, UserId, input.RecordId, rowVersion, string.IsNullOrWhiteSpace(input.AttestationStatementVersion) ? "1" : input.AttestationStatementVersion, input.ReasonCode, input.ReasonText, cancellationToken)); + } + + [HttpPost("OpenAmendment")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Amend)] + public Task> OpenAmendment(RecordCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _recordsService.OpenAmendmentAsync(DepartmentId, UserId, input.RecordId, cancellationToken)); + } + + [HttpPost("AbandonAmendment")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Amend)] + public Task> AbandonAmendment(RecordCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _recordsService.AbandonAmendmentAsync(DepartmentId, UserId, input.RecordId, cancellationToken)); + } + + [HttpPost("Void")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Void)] + public Task> Void(RecordCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _recordsService.VoidAsync(DepartmentId, UserId, input.RecordId, input.ReasonCode, input.ReasonText, cancellationToken)); + } + + [HttpPost("Cancel")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Void)] + public Task> Cancel(RecordCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _recordsService.CancelAsync(DepartmentId, UserId, input.RecordId, cancellationToken)); + } + + [HttpPost("Reassign")] + [Consumes(MediaTypeNames.Application.Json)] + [Authorize(Policy = ResgridResources.Record_Reassign)] + public Task> Reassign(RecordCommandInput input, CancellationToken cancellationToken) + { + return CommandAsync(input, false, _ => _recordsService.ReassignDraftAsync(DepartmentId, UserId, input.RecordId, input.NewOwnerUserId, input.ReasonText, cancellationToken)); + } + + /// + /// Shared command path: flag/usable gate, field-client gate, per-record visibility, scoped idempotency replay, + /// ETag check where the transition takes one, and the 409/400 mapping. Never last-writer-wins. + /// + private async Task> CommandAsync(RecordCommandInput input, bool requiresRowVersion, Func> action) + { + if (input == null || string.IsNullOrWhiteSpace(input.RecordId)) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + + var origin = RecordsApiHelper.ResolveOrigin(input.OriginClient); + var gate = await FieldClientGateAsync(origin); + if (gate != null) + return gate; + + var current = await LoadAuthorizedAsync(input.RecordId); + if (current == null) + return NotFound(); + + var key = RecordsApiHelper.ResolveIdempotencyKey(input.IdempotencyKey, Request); + if (key != null) + { + var replayed = await _idempotency.TryGetRecordIdAsync(DepartmentId, UserId, key); + if (string.Equals(replayed, input.RecordId, StringComparison.Ordinal)) + return Ok(Wrap(await _recordsService.GetAsync(DepartmentId, input.RecordId, true), ResponseHelper.Success)); + } + + long rowVersion = current.Record.RowVersion; + if (requiresRowVersion) + { + var supplied = RecordsApiHelper.ResolveRowVersion(input.RowVersion, Request); + if (!supplied.HasValue) + return Problem(statusCode: StatusCodes.Status428PreconditionRequired, title: "RowVersion or If-Match is required for this command.", type: "precondition_required"); + rowVersion = supplied.Value; + } + + try + { + var aggregate = await action(rowVersion); + if (key != null) + await _idempotency.RememberAsync(DepartmentId, UserId, key, input.RecordId); + return Ok(Wrap(aggregate, ResponseHelper.Updated)); + } + catch (RecordConcurrencyException ex) + { + return await ConflictAsync(input.RecordId, ex.ExpectedRowVersion, null); + } + catch (RecordTransitionException ex) + { + return Problem(statusCode: StatusCodes.Status409Conflict, title: ex.Message, type: "record_transition"); + } + catch (Exception ex) when (ex is ArgumentException || ex is RecordsLegacyWriteBlockedException || ex is InvalidOperationException) + { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, type: "record_validation"); + } + } + + #endregion + + #region Attachments + + [HttpGet("GetAttachments")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> GetAttachments(string id) + { + if (!await FlagOnAsync()) + return NotFound(); + var aggregate = await LoadAuthorizedAsync(id); + if (aggregate == null) + return NotFound(); + + var result = new RecordAttachmentsResult { Data = aggregate.Attachments.Where(a => a.DeletedOn == null).Select(RecordsApiMapper.ToAttachment).ToList() }; + result.PageSize = result.Data.Count; + result.Status = ResponseHelper.Success; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + /// Attachment content (base64), department- and record-bound, audited as a read. + [HttpGet("GetAttachment")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task> GetAttachment(string id, string attachmentId) + { + if (!await FlagOnAsync()) + return NotFound(); + if (await LoadAuthorizedAsync(id) == null) + return NotFound(); + + var attachment = await _recordsService.GetAttachmentAsync(DepartmentId, attachmentId); + if (attachment == null || !string.Equals(attachment.RecordId, id, StringComparison.Ordinal) || attachment.Data == null || attachment.DeletedOn.HasValue) + return NotFound(); + + await _recordsService.RecordAccessAsync(DepartmentId, UserId, id, null, RmsAccessAuditAction.Read, "Attachment " + attachmentId, IpAddressHelper.GetRequestIP(Request, true), RmsOriginClient.Api); + var meta = RecordsApiMapper.ToAttachment(attachment); + var result = new RecordAttachmentContentResult + { + Data = new RecordAttachmentContentData + { + AttachmentId = meta.AttachmentId, RecordId = meta.RecordId, FileName = meta.FileName, ContentType = meta.ContentType, ByteSize = meta.ByteSize, Checksum = meta.Checksum, Description = meta.Description, + UploadedByUserId = meta.UploadedByUserId, UploadedOn = meta.UploadedOn, ScanState = meta.ScanState, ScanStateName = meta.ScanStateName, Data = Convert.ToBase64String(attachment.Data) + }, + PageSize = 1, + Status = ResponseHelper.Success + }; + ResponseHelper.PopulateV4ResponseData(result); + return Ok(result); + } + + /// Opens a resumable upload session (plan 5.3): declare size and SHA-256; send chunks with UploadChunk; finish with CompleteUpload. + [HttpPost("BeginUpload")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status201Created)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> BeginUpload(BeginRecordUploadInput input) + { + if (input == null || string.IsNullOrWhiteSpace(input.RecordId)) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + + var current = await LoadAuthorizedAsync(input.RecordId); + if (current == null) + return NotFound(); + if (!CanEditRecord(current.Record)) + return Forbid(); + + try + { + var session = await _uploads.BeginAsync(DepartmentId, UserId, input.RecordId, input.FileName, input.ContentType, input.ByteSize, input.Sha256); + return StatusCode(StatusCodes.Status201Created, WrapUpload(session, ResponseHelper.Created)); + } + catch (RecordUploadSessionException ex) + { + return UploadProblem(ex); + } + catch (Exception ex) when (ex is ArgumentException || ex is RecordTransitionException) + { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, type: "record_validation"); + } + } + + /// One chunk at the session's next offset (base64, at most ChunkSize bytes). Returns ReceivedBytes to resume from. + [HttpPost("UploadChunk")] + [Consumes(MediaTypeNames.Application.Json)] + [RequestSizeLimit(4 * 1024 * 1024)] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> UploadChunk(RecordUploadChunkInput input) + { + if (input == null || string.IsNullOrWhiteSpace(input.UploadId) || string.IsNullOrWhiteSpace(input.Data)) + return BadRequest(); + if (!await FlagOnAsync()) + return NotFound(); + + byte[] bytes; + try { bytes = Convert.FromBase64String(input.Data); } + catch (FormatException) { return Problem(statusCode: StatusCodes.Status400BadRequest, title: "Chunk data must be base64.", type: "record_validation"); } + + try + { + var session = await _uploads.AppendAsync(DepartmentId, UserId, input.UploadId, input.Offset, bytes); + return Ok(WrapUpload(session, ResponseHelper.Updated)); + } + catch (RecordUploadSessionException ex) + { + return UploadProblem(ex); + } + } + + /// Session status, for resuming after a dropped connection. + [HttpGet("GetUpload")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> GetUpload(string uploadId) + { + if (!await FlagOnAsync()) + return NotFound(); + var session = await _uploads.GetAsync(DepartmentId, UserId, uploadId); + if (session == null) + return NotFound(); + return Ok(WrapUpload(session, ResponseHelper.Success)); + } + + /// Verifies size and SHA-256, then stores the attachment through hygiene and the scanner. 422 on checksum mismatch or rejection. + [HttpPost("CompleteUpload")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status201Created)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> CompleteUpload(CompleteRecordUploadInput input, CancellationToken cancellationToken) + { + if (input == null || string.IsNullOrWhiteSpace(input.UploadId)) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + + try + { + var attachment = await _uploads.CompleteAsync(DepartmentId, UserId, input.UploadId, input.Description, cancellationToken); + var result = new RecordAttachmentResult { Data = RecordsApiMapper.ToAttachment(attachment), PageSize = 1, Status = ResponseHelper.Created }; + ResponseHelper.PopulateV4ResponseData(result); + return StatusCode(StatusCodes.Status201Created, result); + } + catch (RecordUploadSessionException ex) + { + return UploadProblem(ex); + } + catch (Exception ex) when (ex is ArgumentException || ex is RecordTransitionException) + { + return Problem(statusCode: StatusCodes.Status400BadRequest, title: ex.Message, type: "record_validation"); + } + } + + [HttpPost("AbortUpload")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> AbortUpload(RecordUploadIdInput input) + { + if (input == null || string.IsNullOrWhiteSpace(input.UploadId)) + return BadRequest(); + if (!await FlagOnAsync()) + return NotFound(); + if (!await _uploads.AbortAsync(DepartmentId, UserId, input.UploadId)) + return NotFound(); + var session = await _uploads.GetAsync(DepartmentId, UserId, input.UploadId); + return Ok(WrapUpload(session, ResponseHelper.Deleted)); + } + + [HttpPost("RemoveAttachment")] + [Consumes(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task> RemoveAttachment(RecordAttachmentIdInput input, CancellationToken cancellationToken) + { + if (input == null || string.IsNullOrWhiteSpace(input.RecordId) || string.IsNullOrWhiteSpace(input.AttachmentId)) + return BadRequest(); + var usable = await UsableAsync(); + if (usable != null) + return usable; + + var current = await LoadAuthorizedAsync(input.RecordId); + if (current == null) + return NotFound(); + if (!CanEditRecord(current.Record)) + return Forbid(); + + try + { + if (!await _recordsService.RemoveAttachmentAsync(DepartmentId, UserId, input.RecordId, input.AttachmentId, cancellationToken)) + return NotFound(); + return Ok(Wrap(await _recordsService.GetAsync(DepartmentId, input.RecordId, true), ResponseHelper.Deleted)); + } + catch (RecordTransitionException ex) + { + return Problem(statusCode: StatusCodes.Status409Conflict, title: ex.Message, type: "record_transition"); + } + } + + private ActionResult UploadProblem(RecordUploadSessionException ex) + { + int status; + switch (ex.Code) + { + case "not_found": status = StatusCodes.Status404NotFound; break; + case "too_large": status = StatusCodes.Status413PayloadTooLarge; break; + case "checksum_mismatch": + case "rejected": status = StatusCodes.Status422UnprocessableEntity; break; + default: status = StatusCodes.Status409Conflict; break; + } + return Problem(statusCode: status, title: ex.Message, type: "upload_" + ex.Code); + } + + private RecordUploadResult WrapUpload(RecordAttachmentUploadSession session, string status) + { + var result = new RecordUploadResult { Data = session == null ? null : RecordsApiMapper.ToUpload(session), PageSize = session == null ? 0 : 1, Status = status }; + ResponseHelper.PopulateV4ResponseData(result); + return result; + } + + #endregion + + #region Helpers + + private async Task FlagOnAsync() + { + return (await _cutoverService.GetModuleStateAsync(DepartmentId)).FlagEnabled; + } + + /// Writes need the module usable: flag on and the department activated. + private async Task UsableAsync() + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + if (!moduleState.RecordsUsable) + return Problem(statusCode: StatusCodes.Status409Conflict, title: "Records is not activated for this department.", type: "records_not_activated"); + return null; + } + + /// A field client below its flag fails closed for authoring (plan 4.1); non-field origins pass. + private async Task FieldClientGateAsync(RmsOriginClient origin) + { + var flag = RecordsApiHelper.FieldFlagFor(origin); + if (flag == null) + return null; + if (await _featureToggleService.IsEnabledAsync(flag, DepartmentId)) + return null; + return Problem(statusCode: StatusCodes.Status403Forbidden, title: $"Field Records are not enabled for the {origin} app in this department.", type: "field_records_disabled"); + } + + /// Loads a record only when the caller passes the per-record visibility rule; a refusal is audited as Denied. + private async Task LoadAuthorizedAsync(string id, bool includeRevisions = false) + { + if (string.IsNullOrWhiteSpace(id)) + return null; + + if (!await _recordsAuthorizationService.CanUserViewRecordAsync(UserId, id, DepartmentId)) + { + await _recordsService.RecordAccessAsync(DepartmentId, UserId, id, null, RmsAccessAuditAction.Denied, null, IpAddressHelper.GetRequestIP(Request, true), RmsOriginClient.Api); + return null; + } + + return await _recordsService.GetAsync(DepartmentId, id, includeRevisions); + } + + private bool CanEditRecord(RmsOperationalRecord record) + { + if (!ClaimsAuthorizationHelper.CanCreateRecord()) + return false; + + return ClaimsAuthorizationHelper.IsUserDepartmentAdmin() + || string.Equals(record.OwnerUserId, UserId, StringComparison.OrdinalIgnoreCase) + || string.Equals(record.AuthorUserId, UserId, StringComparison.OrdinalIgnoreCase) + || (record.AmendsRevisionId != null && ClaimsAuthorizationHelper.CanAmendRecords()); + } + + private async Task> ConflictAsync(string recordId, long expectedRowVersion, RecordDraftInput attempted) + { + var current = await _recordsService.GetAsync(DepartmentId, recordId, true); + if (current == null) + return NotFound(); + + var conflict = RecordDraftConflictResolver.Describe(attempted, current, expectedRowVersion); + var result = new RecordConflictResult { Data = RecordsApiMapper.ToConflict(conflict, current, ClaimsAuthorizationHelper.CanViewRestrictedRecords()), PageSize = 1, Status = ConflictStatus }; + ResponseHelper.PopulateV4ResponseData(result); + RecordsApiHelper.SetETag(Response, current.Record.RowVersion); + return Conflict(result); + } + + private RecordResult Wrap(RecordAggregate aggregate, string status = ResponseHelper.Success) + { + var result = new RecordResult { Data = RecordsApiMapper.ToRecord(aggregate, ClaimsAuthorizationHelper.CanViewRestrictedRecords()), PageSize = 1, Status = status }; + ResponseHelper.PopulateV4ResponseData(result); + RecordsApiHelper.SetETag(Response, aggregate.Record.RowVersion); + return result; + } + + #endregion + } +} diff --git a/Web/Resgrid.Web.Services/Helpers/RecordsApiHelper.cs b/Web/Resgrid.Web.Services/Helpers/RecordsApiHelper.cs new file mode 100644 index 00000000..c9c2ec03 --- /dev/null +++ b/Web/Resgrid.Web.Services/Helpers/RecordsApiHelper.cs @@ -0,0 +1,437 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; +using Resgrid.Model; +using Resgrid.Services.Records; +using Resgrid.Web.Services.Models.v4.Records; + +namespace Resgrid.Web.Services.Helpers +{ + /// Shared plumbing for the v4 Records contract: origin/field-flag resolution, ETag headers, UTC normalization. + public static class RecordsApiHelper + { + public static RmsOriginClient ResolveOrigin(int? value) + { + if (!value.HasValue || !Enum.IsDefined(typeof(RmsOriginClient), value.Value)) + return RmsOriginClient.Api; + var origin = (RmsOriginClient)value.Value; + // Web and System are not API origins; a caller claiming either is recorded as Api. + return origin == RmsOriginClient.Web || origin == RmsOriginClient.System ? RmsOriginClient.Api : origin; + } + + /// The Records.Field.* flag a field client must have on to create or edit; null for non-field origins. + public static string FieldFlagFor(RmsOriginClient origin) + { + switch (origin) + { + case RmsOriginClient.Responder: return FeatureFlagKeys.RecordsFieldResponder; + case RmsOriginClient.Unit: return FeatureFlagKeys.RecordsFieldUnit; + case RmsOriginClient.IncidentCommand: return FeatureFlagKeys.RecordsFieldIncidentCommand; + case RmsOriginClient.Dispatch: return FeatureFlagKeys.RecordsFieldDispatch; + default: return null; + } + } + + /// Body RowVersion wins; otherwise If-Match; null when neither is usable. + public static long? ResolveRowVersion(long? bodyValue, HttpRequest request) + { + if (bodyValue.HasValue && bodyValue.Value >= 0) + return bodyValue.Value; + if (request == null) + return null; + return RecordsApiContract.ParseETag(request.Headers[HeaderNames.IfMatch].ToString()); + } + + public static string ResolveIdempotencyKey(string bodyValue, HttpRequest request) + { + if (!string.IsNullOrWhiteSpace(bodyValue)) + return bodyValue.Trim(); + var header = request?.Headers[RecordsApiContract.IdempotencyKeyHeader].ToString(); + return string.IsNullOrWhiteSpace(header) ? null : header.Trim(); + } + + public static void SetETag(HttpResponse response, long rowVersion) + { + if (response != null) + response.Headers[HeaderNames.ETag] = RecordsApiContract.ToETag(rowVersion); + } + + /// Clients send UTC; a local-kind value is converted, an unspecified one is taken as UTC. + public static DateTime? Utc(DateTime? value) + { + if (!value.HasValue || value.Value == DateTime.MinValue) + return null; + var v = value.Value; + return v.Kind == DateTimeKind.Local ? v.ToUniversalTime() : DateTime.SpecifyKind(v, DateTimeKind.Utc); + } + + public static long ToUnixMs(DateTime value) => new DateTimeOffset(DateTime.SpecifyKind(value, DateTimeKind.Utc)).ToUnixTimeMilliseconds(); + + public static DateTime? FromUnixMs(long since) => since <= 0 ? (DateTime?)null : DateTimeOffset.FromUnixTimeMilliseconds(since).UtcDateTime; + } + + /// Entity to DTO mapping for the v4 Records controller. Restricted detail fields are withheld here, never downstream. + public static class RecordsApiMapper + { + public static RecordSummaryData ToSummary(RmsRecordSearchProjection p) + { + var state = (RmsRecordState)p.State; + return new RecordSummaryData + { + RecordId = p.RmsRecordSearchProjectionId, + RecordKind = ((RmsRecordKind)p.RecordKind).ToString(), + RecordNumber = p.RecordNumber, + DraftReference = p.DraftReference, + DefinitionKey = p.DefinitionKey, + DefinitionVersion = p.DefinitionVersion, + RecordType = p.RecordType, + State = p.State, + StateName = state.ToString(), + OccurredOn = p.OccurredOn, + CreatedOn = p.RecordCreatedOn, + FinalizedOn = p.FinalizedOn, + ModifiedOn = p.ModifiedOn, + StationGroupId = p.StationGroupId, + CallId = p.CallId, + CallNumber = p.CallNumber, + AuthorUserId = p.AuthorUserId, + OwnerUserId = p.OwnerUserId, + ReviewerUserId = p.ReviewerUserId, + DisplaySummary = p.DisplaySummary, + IsLegacy = p.IsLegacy, + RowVersion = p.RowVersion, + IsTombstone = p.DeletedOn.HasValue || state == RmsRecordState.Cancelled || state == RmsRecordState.Voided, + DeletedOn = p.DeletedOn + }; + } + + public static RecordData ToRecord(RecordAggregate aggregate, bool canViewRestricted) + { + var r = aggregate.Record; + var state = (RmsRecordState)r.State; + var preset = (RmsLifecyclePreset)r.LifecyclePreset; + var restricted = RmsDefinitionKeys.RestrictedClass.Contains(r.DefinitionKey ?? string.Empty); + var data = new RecordData + { + RecordId = r.RmsOperationalRecordId, + RecordKind = RmsRecordKind.Operational.ToString(), + DefinitionKey = r.DefinitionKey, + DefinitionVersion = r.DefinitionVersion, + RecordType = r.RecordType, + RecordTypeName = r.RecordType.HasValue ? ((RmsOperationalRecordType)r.RecordType.Value).ToString() : null, + LifecyclePreset = r.LifecyclePreset, + LifecyclePresetName = preset.ToString(), + State = r.State, + StateName = state.ToString(), + RecordNumber = r.RecordNumber, + DraftReference = r.DraftReference, + DisplaySummary = r.DisplaySummary, + StationGroupId = r.StationGroupId, + CallId = r.CallId, + ExternalId = r.ExternalId, + AuthorUserId = r.AuthorUserId, + OwnerUserId = r.OwnerUserId, + ReviewerUserId = r.ReviewerUserId, + ApproverUserId = r.ApproverUserId, + StartedOn = r.StartedOn, + EndedOn = r.EndedOn, + ReviewDueOn = r.ReviewDueOn, + SubmittedForReviewOn = r.SubmittedForReviewOn, + ReturnedOn = r.ReturnedOn, + ReturnReasonCode = r.ReturnReasonCode, + ReturnReasonText = r.ReturnReasonText, + ReturnCount = r.ReturnCount, + ApprovedOn = r.ApprovedOn, + FinalizedOn = r.FinalizedOn, + FinalizedByUserId = r.FinalizedByUserId, + CurrentRevisionId = r.CurrentRevisionId, + RevisionCount = r.RevisionCount, + AmendsRevisionId = r.AmendsRevisionId, + VoidedOn = r.VoidedOn, + VoidReasonCode = r.VoidReasonCode, + VoidReasonText = r.VoidReasonText, + CancelledOn = r.CancelledOn, + OriginClient = r.OriginClient, + CreatedOn = r.CreatedOn, + ModifiedOn = r.ModifiedOn, + RowVersion = r.RowVersion, + ETag = RecordsApiContract.ToETag(r.RowVersion), + IsEditable = RmsLifecycle.IsEditable(state) || r.AmendsRevisionId != null, + IsRestricted = restricted, + AvailableTransitions = RmsLifecycle.NextStates(preset, state).Select(s => s.ToString()).ToList(), + Details = ToDetails(aggregate.Details, restricted && !canViewRestricted, out var withheld), + WithheldFields = withheld, + Participants = aggregate.Participants.Select(p => new RecordParticipantData { UserId = p.UserId, DisplayName = p.DisplayNameSnapshot, GroupId = p.GroupIdSnapshot, GroupName = p.GroupNameSnapshot, UnitId = p.UnitId, Role = p.Role }).ToList(), + Units = aggregate.Units.Select(ToUnit).ToList(), + Attachments = aggregate.Attachments.Where(a => a.DeletedOn == null).Select(ToAttachment).ToList(), + Revisions = aggregate.Revisions.OrderByDescending(x => x.RevisionNumber).Select(ToRevision).ToList(), + GroupScopeIds = aggregate.GroupScope.Select(g => g.DepartmentGroupId).Distinct().ToList() + }; + return data; + } + + public static RecordData ToRecord(RecordSnapshot snapshot, bool canViewRestricted) + { + var restricted = RmsDefinitionKeys.RestrictedClass.Contains(snapshot.DefinitionKey ?? string.Empty); + return new RecordData + { + RecordId = snapshot.RecordId, + RecordKind = RmsRecordKind.Operational.ToString(), + DefinitionKey = snapshot.DefinitionKey, + DefinitionVersion = snapshot.DefinitionVersion, + RecordType = snapshot.RecordType, + RecordTypeName = snapshot.RecordType.HasValue ? ((RmsOperationalRecordType)snapshot.RecordType.Value).ToString() : null, + RecordNumber = snapshot.RecordNumber, + DraftReference = snapshot.DraftReference, + StationGroupId = snapshot.StationGroupId, + CallId = snapshot.CallId, + ExternalId = snapshot.ExternalId, + AuthorUserId = snapshot.AuthorUserId, + StartedOn = snapshot.StartedOn, + EndedOn = snapshot.EndedOn, + IsRestricted = restricted, + Details = ToDetails(snapshot.Details, restricted && !canViewRestricted, out var withheld), + WithheldFields = withheld, + Participants = snapshot.Participants.Select(p => new RecordParticipantData { UserId = p.UserId, DisplayName = p.DisplayNameSnapshot, GroupId = p.GroupIdSnapshot, GroupName = p.GroupNameSnapshot, UnitId = p.UnitId, Role = p.Role }).ToList(), + Units = snapshot.Units.Select(ToUnit).ToList(), + Attachments = snapshot.Attachments.Select(ToAttachment).ToList() + }; + } + + public static RecordDetailsData ToDetails(RmsOperationalRecordDetail d, bool withholdRestricted, out List withheld) + { + withheld = new List(); + d = d ?? new RmsOperationalRecordDetail(); + var data = new RecordDetailsData + { + Narrative = d.Narrative, InitialReport = d.InitialReport, Type = d.Type, Course = d.Course, CourseCode = d.CourseCode, Instructors = d.Instructors, Cause = d.Cause, + InvestigatedByUserId = d.InvestigatedByUserId, ContactName = d.ContactName, ContactNumber = d.ContactNumber, OtherPersonnel = d.OtherPersonnel, Location = d.Location, + OtherAgencies = d.OtherAgencies, OtherUnits = d.OtherUnits, BodyLocation = d.BodyLocation, PronouncedDeceasedBy = d.PronouncedDeceasedBy, CaseNumber = d.CaseNumber, + Destination = d.Destination, Facilitator = d.Facilitator, UnitId = d.UnitId, ActivityOn = d.ActivityOn, CallNumber = d.CallNumber, CallName = d.CallName, CallType = d.CallType, + CallPriority = d.CallPriority, CallLoggedOn = d.CallLoggedOn, CallAddress = d.CallAddress, CallNature = d.CallNature + }; + if (!withholdRestricted) + return data; + + foreach (var field in RecordSnapshotSerializer.RestrictedDetailFields) + { + var property = typeof(RecordDetailsData).GetProperty(field); + if (property == null || property.GetValue(data) == null) + continue; + property.SetValue(data, null); + withheld.Add(field); + } + return data; + } + + public static RecordUnitResponseData ToUnit(RmsRecordUnitResponse u) + { + return new RecordUnitResponseData { UnitId = u.UnitId, UnitName = u.UnitNameSnapshot, UnitType = u.UnitTypeSnapshot, StationGroupId = u.StationGroupIdSnapshot, Dispatched = u.Dispatched, Enroute = u.Enroute, OnScene = u.OnScene, Released = u.Released, InQuarters = u.InQuarters }; + } + + public static RecordAttachmentData ToAttachment(RmsRecordAttachment a) + { + return new RecordAttachmentData + { + AttachmentId = a.RmsRecordAttachmentId, RecordId = a.RecordId, FileName = a.FileName, ContentType = a.ContentType, ByteSize = a.ByteSize, Checksum = a.Checksum, + Description = a.Description, UploadedByUserId = a.UploadedByUserId, UploadedOn = a.UploadedOn, ScanState = a.ScanState, ScanStateName = ((RmsAttachmentScanState)a.ScanState).ToString() + }; + } + + public static RecordRevisionData ToRevision(RmsRevision r) + { + return new RecordRevisionData + { + RevisionId = r.RmsRevisionId, RevisionNumber = r.RevisionNumber, Transition = r.Transition, TransitionName = ((RmsRevisionTransition)r.Transition).ToString(), PriorRevisionId = r.PriorRevisionId, + Checksum = r.Checksum, ActorUserId = r.ActorUserId, ReasonCode = r.ReasonCode, ReasonText = r.ReasonText, AttestationStatementVersion = r.AttestationStatementVersion, AttestedOn = r.AttestedOn, CreatedOn = r.CreatedOn + }; + } + + public static RecordUploadData ToUpload(RecordAttachmentUploadSession s) + { + return new RecordUploadData + { + UploadId = s.UploadId, RecordId = s.RecordId, FileName = s.FileName, ContentType = s.ContentType, DeclaredSize = s.DeclaredSize, ReceivedBytes = s.ReceivedBytes, ChunkSize = s.ChunkSize, + ChunkCount = s.ChunkCount, State = (int)s.State, StateName = s.State.ToString(), ExpiresOn = s.ExpiresOn, AttachmentId = s.AttachmentId + }; + } + + public static RecordDraftInput ToDraftInput(SaveRecordDraftInput input, RmsOriginClient origin) + { + var d = input.Details ?? new RecordDetailsInput(); + return new RecordDraftInput + { + DefinitionKey = input.DefinitionKey, + CallId = input.CallId, + StationGroupId = input.StationGroupId, + ExternalId = input.ExternalId, + StartedOn = RecordsApiHelper.Utc(input.StartedOn), + EndedOn = RecordsApiHelper.Utc(input.EndedOn), + Details = new RmsOperationalRecordDetail + { + Narrative = d.Narrative, InitialReport = d.InitialReport, Type = d.Type, Course = d.Course, CourseCode = d.CourseCode, Instructors = d.Instructors, Cause = d.Cause, + InvestigatedByUserId = d.InvestigatedByUserId, ContactName = d.ContactName, ContactNumber = d.ContactNumber, OtherPersonnel = d.OtherPersonnel, Location = d.Location, + OtherAgencies = d.OtherAgencies, OtherUnits = d.OtherUnits, BodyLocation = d.BodyLocation, PronouncedDeceasedBy = d.PronouncedDeceasedBy, CaseNumber = d.CaseNumber, + Destination = d.Destination, Facilitator = d.Facilitator, UnitId = d.UnitId, ActivityOn = RecordsApiHelper.Utc(d.ActivityOn) + }, + Participants = (input.Participants ?? new List()).Where(p => !string.IsNullOrWhiteSpace(p.UserId)).Select(p => new RecordParticipantInput { UserId = p.UserId, UnitId = p.UnitId, Role = p.Role }).ToList(), + Units = (input.Units ?? new List()).Where(u => u.UnitId > 0).Select(u => new RecordUnitResponseInput + { + UnitId = u.UnitId, Dispatched = RecordsApiHelper.Utc(u.Dispatched), Enroute = RecordsApiHelper.Utc(u.Enroute), OnScene = RecordsApiHelper.Utc(u.OnScene), Released = RecordsApiHelper.Utc(u.Released), InQuarters = RecordsApiHelper.Utc(u.InQuarters) + }).ToList(), + ClientRecordId = input.ClientRecordId, + IdempotencyKey = input.IdempotencyKey, + OriginClient = origin, + DuplicateContinueReason = input.DuplicateContinueReason + }; + } + + public static RecordConflictData ToConflict(RecordDraftConflict conflict, RecordAggregate current, bool canViewRestricted) + { + return new RecordConflictData + { + RecordId = conflict.RecordId, + ExpectedRowVersion = conflict.ExpectedRowVersion, + CurrentRowVersion = conflict.CurrentRowVersion, + CurrentState = (int)conflict.CurrentState, + CurrentStateName = conflict.CurrentState.ToString(), + CurrentRevisionId = conflict.CurrentRevisionId, + ChangedFieldPaths = conflict.ChangedFieldPaths, + Current = current == null ? null : ToRecord(current, canViewRestricted) + }; + } + + public static List ToDefinitions() + { + return RecordDefinitionCatalog.Describe().Select(d => new RecordDefinitionData + { + Key = d.Key, Version = d.Version, Name = d.Name, RecordType = d.RecordType, RecordKind = d.RecordKind, LifecyclePreset = d.LifecyclePreset, LifecyclePresetName = d.LifecyclePresetName, + Cardinality = d.Cardinality, Restricted = d.Restricted, NumberPrefix = d.NumberPrefix, RequiresCall = d.RequiresCall, SupportsParticipants = d.SupportsParticipants, SupportsUnits = d.SupportsUnits, + SupportsAttachments = d.SupportsAttachments, MinimumClientCapability = d.MinimumClientCapability, Locked = d.Locked, + Fields = d.Fields.Select(f => new RecordFieldData { Key = f.Key, Section = f.Section, Type = f.Type, Required = f.Required, RequiredToFinalize = f.RequiredToFinalize, Restricted = f.Restricted }).ToList() + }).ToList(); + } + } + + /// Entity to DTO mapping for the v4 IncidentReports controller. Submissions are sanitized: no payload, no response body. + public static class IncidentReportsApiMapper + { + public static IncidentReportSummaryData ToSummary(RmsIncidentReport r) + { + return new IncidentReportSummaryData + { + ReportId = r.RmsIncidentReportId, RecordNumber = r.RecordNumber, DraftReference = r.DraftReference, CallId = r.CallId, IncidentNumber = r.IncidentNumber, NerisIncidentId = r.NerisIncidentId, + State = r.State, StateName = ((RmsRecordState)r.State).ToString(), LastSubmissionState = r.LastSubmissionState, + LastSubmissionStateName = r.LastSubmissionState.HasValue ? ((RmsSubmissionState)r.LastSubmissionState.Value).ToString() : null, + DisplaySummary = r.DisplaySummary, StationGroupId = r.StationGroupId, AuthorUserId = r.AuthorUserId, OwnerUserId = r.OwnerUserId, CallCreatedOn = r.CallCreatedOn, + CreatedOn = r.CreatedOn, ModifiedOn = r.ModifiedOn, FinalizedOn = r.FinalizedOn, RowVersion = r.RowVersion + }; + } + + public static IncidentReportData ToReport(IncidentReportAggregate a, bool submissionEnabled) + { + var r = a.Report; + var state = (RmsRecordState)r.State; + var preset = (RmsLifecyclePreset)r.LifecyclePreset; + var canQueue = submissionEnabled && r.AmendsRevisionId == null && !string.IsNullOrWhiteSpace(r.CurrentRevisionId) + && (state == RmsRecordState.Finalized || state == RmsRecordState.Amended || state == RmsRecordState.Corrected || (state == RmsRecordState.Rejected && a.Submissions.Any(s => s.State == (int)RmsSubmissionState.Failed))); + return new IncidentReportData + { + ReportId = r.RmsIncidentReportId, CallId = r.CallId, ReportingEntityId = r.ReportingEntityId, DefinitionKey = r.DefinitionKey, DefinitionVersion = r.DefinitionVersion, ProfileVersion = r.ProfileVersion, + LifecyclePreset = r.LifecyclePreset, State = r.State, StateName = state.ToString(), RecordNumber = r.RecordNumber, DraftReference = r.DraftReference, DisplaySummary = r.DisplaySummary, + IncidentNumber = r.IncidentNumber, NerisIncidentId = r.NerisIncidentId, LastSubmissionId = r.LastSubmissionId, LastSubmissionState = r.LastSubmissionState, + LastSubmissionStateName = r.LastSubmissionState.HasValue ? ((RmsSubmissionState)r.LastSubmissionState.Value).ToString() : null, LastSubmittedOn = r.LastSubmittedOn, + AcceptedOn = r.AcceptedOn, RejectedOn = r.RejectedOn, RejectionSummary = r.RejectionSummary, StationGroupId = r.StationGroupId, AuthorUserId = r.AuthorUserId, OwnerUserId = r.OwnerUserId, + ReviewerUserId = r.ReviewerUserId, CallCreatedOn = r.CallCreatedOn, CallAnsweredOn = r.CallAnsweredOn, CallArrivalOn = r.CallArrivalOn, IncidentClearedOn = r.IncidentClearedOn, + DispatchCenterId = r.DispatchCenterId, DeterminantCode = r.DeterminantCode, DispatchIncidentCode = r.DispatchIncidentCode, Disposition = r.Disposition, PeoplePresent = r.PeoplePresent, + DisplacementCount = r.DisplacementCount, AnimalsRescued = r.AnimalsRescued, + SpecialModifiers = string.IsNullOrWhiteSpace(r.SpecialModifiersCsv) ? new List() : r.SpecialModifiersCsv.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0).ToList(), + ReviewDueOn = r.ReviewDueOn, SubmittedForReviewOn = r.SubmittedForReviewOn, ReturnedOn = r.ReturnedOn, ReturnReasonCode = r.ReturnReasonCode, ReturnReasonText = r.ReturnReasonText, + FinalizedOn = r.FinalizedOn, CurrentRevisionId = r.CurrentRevisionId, RevisionCount = r.RevisionCount, AmendsRevisionId = r.AmendsRevisionId, VoidedOn = r.VoidedOn, VoidReasonCode = r.VoidReasonCode, + CancelledOn = r.CancelledOn, OriginClient = r.OriginClient, CreatedOn = r.CreatedOn, ModifiedOn = r.ModifiedOn, RowVersion = r.RowVersion, ETag = RecordsApiContract.ToETag(r.RowVersion), + IsEditable = RmsLifecycle.IsEditable(state) || state == RmsRecordState.Rejected || r.AmendsRevisionId != null, + SubmissionEnabled = submissionEnabled, CanQueueSubmission = canQueue, HasBlockingIssues = a.HasBlockingIssues, + AvailableTransitions = RmsLifecycle.NextStates(preset, state).Select(s => s.ToString()).ToList(), + Location = a.Location == null ? null : new IncidentLocationData + { + AddressText = a.Location.AddressText, Number = a.Location.Number, NumberPrefix = a.Location.NumberPrefix, NumberSuffix = a.Location.NumberSuffix, Street = a.Location.Street, UnitValue = a.Location.UnitValue, + Municipality = a.Location.Municipality, County = a.Location.County, State = a.Location.State, PostalCode = a.Location.PostalCode, Country = a.Location.Country, PlaceType = a.Location.PlaceType, + LocationUse = a.Location.LocationUse, CrossStreet1 = a.Location.CrossStreet1, CrossStreet2 = a.Location.CrossStreet2, Latitude = a.Location.Latitude, Longitude = a.Location.Longitude, + Jurisdiction = a.Location.Jurisdiction, SourceKind = a.Location.SourceKind, SourceKindName = ((RmsSourceKind)a.Location.SourceKind).ToString() + }, + Types = a.Types.OrderBy(t => t.Ordinal).Select(t => new IncidentTypeData { TypeCode = t.TypeCode, IsPrimary = t.IsPrimary, LocalCode = t.LocalCode, Ordinal = t.Ordinal }).ToList(), + Units = a.Units.OrderBy(u => u.Ordinal).Select(u => new IncidentUnitResponseData + { + UnitId = u.UnitId, UnitName = u.UnitNameSnapshot, UnitType = u.UnitTypeSnapshot, StationGroupId = u.StationGroupIdSnapshot, UnitNerisId = u.UnitNerisId, Staffing = u.Staffing, UnableToDispatch = u.UnableToDispatch, + DispatchedOn = u.DispatchedOn, EnrouteOn = u.EnrouteOn, OnSceneOn = u.OnSceneOn, StagingOn = u.StagingOn, CanceledEnrouteOn = u.CanceledEnrouteOn, ClearedOn = u.ClearedOn, ResponseMode = u.ResponseMode, + TransportMode = u.TransportMode, TimesSourceKind = u.TimesSourceKind, TimesSourceKindName = ((RmsSourceKind)u.TimesSourceKind).ToString(), Ordinal = u.Ordinal + }).ToList(), + Aids = a.Aids.OrderBy(x => x.Ordinal).Select(x => new IncidentAidData { Direction = x.Direction, AidType = x.AidType, CounterpartNerisId = x.CounterpartNerisId, CounterpartName = x.CounterpartName, IsNonFireDepartment = x.IsNonFireDepartment, NonFdType = x.NonFdType, Ordinal = x.Ordinal }).ToList(), + Tactics = a.Tactics.OrderBy(t => t.Ordinal).Select(t => new IncidentTacticData { TacticCode = t.TacticCode, ActorUnitId = t.ActorUnitId, OccurredOn = t.OccurredOn, Ordinal = t.Ordinal }).ToList(), + Narrative = a.Narrative?.Narrative, ImpedimentNarrative = a.Narrative?.ImpedimentNarrative, OutcomeNarrative = a.Narrative?.OutcomeNarrative, SupplementalJson = a.Narrative?.SupplementalJson, + Facts = a.Facts.OrderBy(f => f.FactKey).Select(f => new IncidentFactData + { + FactKey = f.FactKey, SourceKind = f.SourceKind, SourceKindName = ((RmsSourceKind)f.SourceKind).ToString(), SourceSystem = f.SourceSystem, SourceEntityType = f.SourceEntityType, SourceEntityId = f.SourceEntityId, + SourceValue = f.SourceValue, CurrentValue = f.CurrentValue, SourceTime = f.SourceTime, CorrectedOn = f.CorrectedOn, CorrectedByUserId = f.CorrectedByUserId + }).ToList(), + Issues = a.Issues.Select(ToIssue).ToList(), + Submissions = a.Submissions.OrderByDescending(s => s.QueuedOn).Select(s => new IncidentSubmissionData + { + SubmissionId = s.RmsSubmissionId, RevisionId = s.RevisionId, Destination = s.Destination, DestinationVersion = s.DestinationVersion, State = s.State, StateName = ((RmsSubmissionState)s.State).ToString(), + ExternalId = s.ExternalId, ExternalStatus = s.ExternalStatus, Attempts = s.Attempts, MaxAttempts = s.MaxAttempts, ErrorSummary = s.ErrorSummary, PayloadChecksum = s.PayloadChecksum, + QueuedOn = s.QueuedOn, SentOn = s.SentOn, CompletedOn = s.CompletedOn, NextAttemptOn = s.NextAttemptOn + }).ToList(), + Signatures = a.Signatures.OrderByDescending(s => s.SignedOn).Select(s => new IncidentSignatureData + { + SignatureId = s.RmsSignatureId, RevisionId = s.RevisionId, SignerUserId = s.SignerUserId, SignerName = s.SignerNameSnapshot, SignerRole = s.SignerRoleSnapshot, Intent = s.Intent, + StatementVersion = s.StatementVersion, StatementText = s.StatementText, SignedOn = s.SignedOn, ArtifactChecksum = s.ArtifactChecksum + }).ToList(), + Revisions = a.Revisions.OrderByDescending(x => x.RevisionNumber).Select(RecordsApiMapper.ToRevision).ToList(), + GroupScopeIds = a.GroupScope.Select(g => g.DepartmentGroupId).Distinct().ToList() + }; + } + + public static IncidentIssueData ToIssue(RmsValidationIssue i) + { + return new IncidentIssueData { RuleKey = i.RuleKey, Severity = i.Severity, SeverityName = ((RmsValidationSeverity)i.Severity).ToString(), FieldPath = i.FieldPath, Message = i.Message, Source = i.Source, SourceName = ((RmsValidationSource)i.Source).ToString() }; + } + + public static IncidentReportDraftInput ToDraftInput(SaveIncidentReportDraftInput input, RmsOriginClient origin) + { + return new IncidentReportDraftInput + { + IncidentNumber = input.IncidentNumber, + CallCreatedOn = RecordsApiHelper.Utc(input.CallCreatedOn), + CallAnsweredOn = RecordsApiHelper.Utc(input.CallAnsweredOn), + CallArrivalOn = RecordsApiHelper.Utc(input.CallArrivalOn), + IncidentClearedOn = RecordsApiHelper.Utc(input.IncidentClearedOn), + DispatchCenterId = input.DispatchCenterId, + DeterminantCode = input.DeterminantCode, + DispatchIncidentCode = input.DispatchIncidentCode, + Disposition = input.Disposition, + PeoplePresent = input.PeoplePresent, + DisplacementCount = input.DisplacementCount, + AnimalsRescued = input.AnimalsRescued, + SpecialModifiers = input.SpecialModifiers ?? new List(), + StationGroupId = input.StationGroupId, + Location = input.Location, + Types = input.Types ?? new List(), + Units = (input.Units ?? new List()).Select(u => new IncidentUnitResponseInput + { + UnitId = u.UnitId, UnitNerisId = u.UnitNerisId, ReportedUnitId = u.ReportedUnitId, Staffing = u.Staffing, UnableToDispatch = u.UnableToDispatch, + DispatchedOn = RecordsApiHelper.Utc(u.DispatchedOn), EnrouteOn = RecordsApiHelper.Utc(u.EnrouteOn), OnSceneOn = RecordsApiHelper.Utc(u.OnSceneOn), CanceledEnrouteOn = RecordsApiHelper.Utc(u.CanceledEnrouteOn), + StagingOn = RecordsApiHelper.Utc(u.StagingOn), ClearedOn = RecordsApiHelper.Utc(u.ClearedOn), ResponseMode = u.ResponseMode, TransportMode = u.TransportMode + }).ToList(), + Aids = input.Aids ?? new List(), + Tactics = (input.Tactics ?? new List()).Select(t => new IncidentTacticInput { TacticCode = t.TacticCode, ActorUnitId = t.ActorUnitId, OccurredOn = RecordsApiHelper.Utc(t.OccurredOn) }).ToList(), + Narrative = input.Narrative, + ImpedimentNarrative = input.ImpedimentNarrative, + OutcomeNarrative = input.OutcomeNarrative, + SupplementalJson = input.SupplementalJson, + OriginClient = origin + }; + } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Records/IncidentReportsApiModels.cs b/Web/Resgrid.Web.Services/Models/v4/Records/IncidentReportsApiModels.cs new file mode 100644 index 00000000..5ba81e14 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Records/IncidentReportsApiModels.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Generic; +using Resgrid.Model; + +namespace Resgrid.Web.Services.Models.v4.Records +{ + public class IncidentReportsResult : StandardApiResponseV4Base + { + public List Data { get; set; } = new List(); + public int Total { get; set; } + } + + public class IncidentReportSummaryData + { + public string ReportId { get; set; } + public string RecordNumber { get; set; } + public string DraftReference { get; set; } + public int CallId { get; set; } + public string IncidentNumber { get; set; } + public string NerisIncidentId { get; set; } + public int State { get; set; } + public string StateName { get; set; } + public int? LastSubmissionState { get; set; } + public string LastSubmissionStateName { get; set; } + public string DisplaySummary { get; set; } + public int? StationGroupId { get; set; } + public string AuthorUserId { get; set; } + public string OwnerUserId { get; set; } + public DateTime? CallCreatedOn { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public DateTime? FinalizedOn { get; set; } + public long RowVersion { get; set; } + } + + public class IncidentReportResult : StandardApiResponseV4Base + { + public IncidentReportData Data { get; set; } + } + + /// A hydrated NERIS incident report: header, dispatch facts, sections, provenance, validation issues, sanitized submission history. + public class IncidentReportData + { + public string ReportId { get; set; } + public int CallId { get; set; } + public string ReportingEntityId { get; set; } + public string DefinitionKey { get; set; } + public int DefinitionVersion { get; set; } + public string ProfileVersion { get; set; } + public int LifecyclePreset { get; set; } + public int State { get; set; } + public string StateName { get; set; } + public string RecordNumber { get; set; } + public string DraftReference { get; set; } + public string DisplaySummary { get; set; } + public string IncidentNumber { get; set; } + public string NerisIncidentId { get; set; } + public string LastSubmissionId { get; set; } + public int? LastSubmissionState { get; set; } + public string LastSubmissionStateName { get; set; } + public DateTime? LastSubmittedOn { get; set; } + public DateTime? AcceptedOn { get; set; } + public DateTime? RejectedOn { get; set; } + public string RejectionSummary { get; set; } + public int? StationGroupId { get; set; } + public string AuthorUserId { get; set; } + public string OwnerUserId { get; set; } + public string ReviewerUserId { get; set; } + public DateTime? CallCreatedOn { get; set; } + public DateTime? CallAnsweredOn { get; set; } + public DateTime? CallArrivalOn { get; set; } + public DateTime? IncidentClearedOn { get; set; } + public string DispatchCenterId { get; set; } + public string DeterminantCode { get; set; } + public string DispatchIncidentCode { get; set; } + public string Disposition { get; set; } + public bool? PeoplePresent { get; set; } + public int? DisplacementCount { get; set; } + public int? AnimalsRescued { get; set; } + public List SpecialModifiers { get; set; } = new List(); + public DateTime? ReviewDueOn { get; set; } + public DateTime? SubmittedForReviewOn { get; set; } + public DateTime? ReturnedOn { get; set; } + public string ReturnReasonCode { get; set; } + public string ReturnReasonText { get; set; } + public DateTime? FinalizedOn { get; set; } + public string CurrentRevisionId { get; set; } + public int RevisionCount { get; set; } + public string AmendsRevisionId { get; set; } + public DateTime? VoidedOn { get; set; } + public string VoidReasonCode { get; set; } + public DateTime? CancelledOn { get; set; } + public int OriginClient { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + public string ETag { get; set; } + public bool IsEditable { get; set; } + public bool SubmissionEnabled { get; set; } + public bool CanQueueSubmission { get; set; } + public bool HasBlockingIssues { get; set; } + public List AvailableTransitions { get; set; } = new List(); + public IncidentLocationData Location { get; set; } + public List Types { get; set; } = new List(); + public List Units { get; set; } = new List(); + public List Aids { get; set; } = new List(); + public List Tactics { get; set; } = new List(); + public string Narrative { get; set; } + public string ImpedimentNarrative { get; set; } + public string OutcomeNarrative { get; set; } + public string SupplementalJson { get; set; } + public List Facts { get; set; } = new List(); + public List Issues { get; set; } = new List(); + public List Submissions { get; set; } = new List(); + public List Signatures { get; set; } = new List(); + public List Revisions { get; set; } = new List(); + public List GroupScopeIds { get; set; } = new List(); + } + + public class IncidentLocationData + { + public string AddressText { get; set; } + public string Number { get; set; } + public string NumberPrefix { get; set; } + public string NumberSuffix { get; set; } + public string Street { get; set; } + public string UnitValue { get; set; } + public string Municipality { get; set; } + public string County { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string PlaceType { get; set; } + public string LocationUse { get; set; } + public string CrossStreet1 { get; set; } + public string CrossStreet2 { get; set; } + public decimal? Latitude { get; set; } + public decimal? Longitude { get; set; } + public string Jurisdiction { get; set; } + public int SourceKind { get; set; } + public string SourceKindName { get; set; } + } + + public class IncidentTypeData + { + public string TypeCode { get; set; } + public bool IsPrimary { get; set; } + public string LocalCode { get; set; } + public int Ordinal { get; set; } + } + + public class IncidentUnitResponseData + { + public int? UnitId { get; set; } + public string UnitName { get; set; } + public string UnitType { get; set; } + public int? StationGroupId { get; set; } + public string UnitNerisId { get; set; } + public int? Staffing { get; set; } + public bool UnableToDispatch { get; set; } + public DateTime? DispatchedOn { get; set; } + public DateTime? EnrouteOn { get; set; } + public DateTime? OnSceneOn { get; set; } + public DateTime? StagingOn { get; set; } + public DateTime? CanceledEnrouteOn { get; set; } + public DateTime? ClearedOn { get; set; } + public string ResponseMode { get; set; } + public string TransportMode { get; set; } + public int TimesSourceKind { get; set; } + public string TimesSourceKindName { get; set; } + public int Ordinal { get; set; } + } + + public class IncidentAidData + { + public string Direction { get; set; } + public string AidType { get; set; } + public string CounterpartNerisId { get; set; } + public string CounterpartName { get; set; } + public bool IsNonFireDepartment { get; set; } + public string NonFdType { get; set; } + public int Ordinal { get; set; } + } + + public class IncidentTacticData + { + public string TacticCode { get; set; } + public int? ActorUnitId { get; set; } + public DateTime? OccurredOn { get; set; } + public int Ordinal { get; set; } + } + + /// Provenance for one prefilled value (plan 4.2): where it came from, what it was, what it is now. + public class IncidentFactData + { + public string FactKey { get; set; } + public int SourceKind { get; set; } + public string SourceKindName { get; set; } + public string SourceSystem { get; set; } + public string SourceEntityType { get; set; } + public string SourceEntityId { get; set; } + public string SourceValue { get; set; } + public string CurrentValue { get; set; } + public DateTime? SourceTime { get; set; } + public DateTime? CorrectedOn { get; set; } + public string CorrectedByUserId { get; set; } + } + + public class IncidentIssueData + { + public string RuleKey { get; set; } + public int Severity { get; set; } + public string SeverityName { get; set; } + public string FieldPath { get; set; } + public string Message { get; set; } + public int Source { get; set; } + public string SourceName { get; set; } + } + + /// Sanitized delivery outcome (plan 5.6): state, external id/status, attempts, codes and field paths. Never payload or response bodies. + public class IncidentSubmissionData + { + public string SubmissionId { get; set; } + public string RevisionId { get; set; } + public string Destination { get; set; } + public string DestinationVersion { get; set; } + public int State { get; set; } + public string StateName { get; set; } + public string ExternalId { get; set; } + public string ExternalStatus { get; set; } + public int Attempts { get; set; } + public int MaxAttempts { get; set; } + public string ErrorSummary { get; set; } + public string PayloadChecksum { get; set; } + public DateTime QueuedOn { get; set; } + public DateTime? SentOn { get; set; } + public DateTime? CompletedOn { get; set; } + public DateTime? NextAttemptOn { get; set; } + } + + public class IncidentSignatureData + { + public string SignatureId { get; set; } + public string RevisionId { get; set; } + public string SignerUserId { get; set; } + public string SignerName { get; set; } + public string SignerRole { get; set; } + public int Intent { get; set; } + public string StatementVersion { get; set; } + public string StatementText { get; set; } + public DateTime SignedOn { get; set; } + public string ArtifactChecksum { get; set; } + } + + public class IncidentValidationResult : StandardApiResponseV4Base + { + public List Data { get; set; } = new List(); + public bool HasBlockingIssues { get; set; } + } + + /// Start (or return) the one authoritative incident report for a Call. + public class StartIncidentReportInput + { + public int CallId { get; set; } + public int? OriginClient { get; set; } + } + + /// Draft save; every list replaces the draft rows. Dates are UTC. + public class SaveIncidentReportDraftInput + { + public string ReportId { get; set; } + public long? RowVersion { get; set; } + public int? OriginClient { get; set; } + public string IncidentNumber { get; set; } + public DateTime? CallCreatedOn { get; set; } + public DateTime? CallAnsweredOn { get; set; } + public DateTime? CallArrivalOn { get; set; } + public DateTime? IncidentClearedOn { get; set; } + public string DispatchCenterId { get; set; } + public string DeterminantCode { get; set; } + public string DispatchIncidentCode { get; set; } + public string Disposition { get; set; } + public bool? PeoplePresent { get; set; } + public int? DisplacementCount { get; set; } + public int? AnimalsRescued { get; set; } + public List SpecialModifiers { get; set; } = new List(); + public int? StationGroupId { get; set; } + public IncidentLocationInput Location { get; set; } + public List Types { get; set; } = new List(); + public List Units { get; set; } = new List(); + public List Aids { get; set; } = new List(); + public List Tactics { get; set; } = new List(); + public string Narrative { get; set; } + public string ImpedimentNarrative { get; set; } + public string OutcomeNarrative { get; set; } + public string SupplementalJson { get; set; } + } + + public class IncidentReportCommandInput + { + public string ReportId { get; set; } + public long? RowVersion { get; set; } + public string IdempotencyKey { get; set; } + public string ReasonCode { get; set; } + public string ReasonText { get; set; } + public bool Attested { get; set; } + /// Validate: also call the destination's validate endpoint when submission is enabled. + public bool IncludeDestination { get; set; } = true; + public int? OriginClient { get; set; } + } + + /// 409 on a stale incident report save. + public class IncidentReportConflictResult : StandardApiResponseV4Base + { + public IncidentReportConflictData Data { get; set; } + } + + public class IncidentReportConflictData + { + public string ReportId { get; set; } + public long ExpectedRowVersion { get; set; } + public long CurrentRowVersion { get; set; } + public int CurrentState { get; set; } + public string CurrentStateName { get; set; } + public IncidentReportData Current { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Models/v4/Records/RecordsApiModels.cs b/Web/Resgrid.Web.Services/Models/v4/Records/RecordsApiModels.cs new file mode 100644 index 00000000..05987231 --- /dev/null +++ b/Web/Resgrid.Web.Services/Models/v4/Records/RecordsApiModels.cs @@ -0,0 +1,530 @@ +using System; +using System.Collections.Generic; + +namespace Resgrid.Web.Services.Models.v4.Records +{ + /// Capability manifest for the Records module (RMS plan sections 5.4, 5.9.1). + public class RecordsCapabilitiesResult : StandardApiResponseV4Base + { + public RecordsCapabilitiesData Data { get; set; } + } + + public class RecordsCapabilitiesData + { + /// The contract version this server speaks; a client below a definition's MinimumClientCapability fails closed for authoring. + public string ContractVersion { get; set; } + public bool ModuleEnabled { get; set; } + public bool RecordsUsable { get; set; } + public bool Activated { get; set; } + public DateTime? ActivatedOn { get; set; } + public string CutoverState { get; set; } + public string GroupVisibilityMode { get; set; } + public RecordsPermissionsData Permissions { get; set; } = new RecordsPermissionsData(); + public RecordsFieldClientsData FieldClients { get; set; } = new RecordsFieldClientsData(); + public List Definitions { get; set; } = new List(); + public RecordsSearchCapabilityData Search { get; set; } = new RecordsSearchCapabilityData(); + /// Protected Data block (plan 5.9.1): same shape before and after enrollment. + public RecordsProtectionData Protection { get; set; } = new RecordsProtectionData(); + public int UploadChunkSize { get; set; } + public long MaxAttachmentBytes { get; set; } + public long ServerTimestampMs { get; set; } + } + + public class RecordsPermissionsData + { + public bool CanView { get; set; } + public bool CanCreate { get; set; } + public bool CanReview { get; set; } + public bool CanApprove { get; set; } + public bool CanFinalize { get; set; } + public bool CanSubmit { get; set; } + public bool CanAmend { get; set; } + public bool CanVoid { get; set; } + public bool CanExport { get; set; } + public bool CanShare { get; set; } + public bool CanReassign { get; set; } + public bool CanViewRestricted { get; set; } + public bool CanViewLegacy { get; set; } + public bool IsDepartmentAdmin { get; set; } + } + + /// Per-app Field Records flags; a field client whose flag is off is refused on create/edit. + public class RecordsFieldClientsData + { + public bool Responder { get; set; } + public bool Unit { get; set; } + public bool IncidentCommand { get; set; } + public bool Dispatch { get; set; } + } + + public class RecordDefinitionData + { + public string Key { get; set; } + public int Version { get; set; } + public string Name { get; set; } + public int? RecordType { get; set; } + public string RecordKind { get; set; } + public int LifecyclePreset { get; set; } + public string LifecyclePresetName { get; set; } + public string Cardinality { get; set; } + public bool Restricted { get; set; } + public string NumberPrefix { get; set; } + public bool RequiresCall { get; set; } + public bool SupportsParticipants { get; set; } + public bool SupportsUnits { get; set; } + public bool SupportsAttachments { get; set; } + public string MinimumClientCapability { get; set; } + public bool Locked { get; set; } + public List Fields { get; set; } = new List(); + } + + public class RecordFieldData + { + public string Key { get; set; } + public string Section { get; set; } + public string Type { get; set; } + public bool Required { get; set; } + public bool RequiredToFinalize { get; set; } + public bool Restricted { get; set; } + } + + public class RecordsSearchCapabilityData + { + public bool Available { get; set; } + public bool NarrativeAvailable { get; set; } + } + + public class RecordsProtectionData + { + /// NotInstalled until the department is enrolled; then the DepartmentDataProtectionState name. + public string State { get; set; } = "NotInstalled"; + public int CatalogVersion { get; set; } + public string GrantExpiresOn { get; set; } + public int? StepUpWindowMinutes { get; set; } + public string MinimumClientVersion { get; set; } + } + + /// Paged list of records the caller may see. + public class RecordsResult : StandardApiResponseV4Base + { + public List Data { get; set; } = new List(); + public int Total { get; set; } + /// Search only: the text was not applied because the search host is off or unavailable. + public bool SearchDegraded { get; set; } + public bool Truncated { get; set; } + } + + /// Safe projection columns only (plan 5.10): never narrative, restricted detail or attachment bytes. + public class RecordSummaryData + { + public string RecordId { get; set; } + public string RecordKind { get; set; } + public string RecordNumber { get; set; } + public string DraftReference { get; set; } + public string DefinitionKey { get; set; } + public int DefinitionVersion { get; set; } + public int? RecordType { get; set; } + public int State { get; set; } + public string StateName { get; set; } + public DateTime? OccurredOn { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime? FinalizedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public int? StationGroupId { get; set; } + public int? CallId { get; set; } + public string CallNumber { get; set; } + public string AuthorUserId { get; set; } + public string OwnerUserId { get; set; } + public string ReviewerUserId { get; set; } + public string DisplaySummary { get; set; } + public bool IsLegacy { get; set; } + public long RowVersion { get; set; } + /// Delta feed: the record is gone for the client (deleted, cancelled or voided); remove it locally. + public bool IsTombstone { get; set; } + public DateTime? DeletedOn { get; set; } + } + + /// Delta cursor (plan 5.3): persist ServerTimestampMs and pass it back as since. + public class RecordsChangesResult : StandardApiResponseV4Base + { + public RecordsChangesData Data { get; set; } = new RecordsChangesData(); + } + + public class RecordsChangesData + { + public long Since { get; set; } + public long ServerTimestampMs { get; set; } + public bool HasMore { get; set; } + public List Records { get; set; } = new List(); + } + + public class RecordResult : StandardApiResponseV4Base + { + public RecordData Data { get; set; } + } + + /// A hydrated Record: header, working details, participants, units, attachment metadata and revision summaries. Restricted fields are withheld without RecordRestricted_View. + public class RecordData + { + public string RecordId { get; set; } + public string RecordKind { get; set; } + public string DefinitionKey { get; set; } + public int DefinitionVersion { get; set; } + public int? RecordType { get; set; } + public string RecordTypeName { get; set; } + public int LifecyclePreset { get; set; } + public string LifecyclePresetName { get; set; } + public int State { get; set; } + public string StateName { get; set; } + public string RecordNumber { get; set; } + public string DraftReference { get; set; } + public string DisplaySummary { get; set; } + public int? StationGroupId { get; set; } + public int? CallId { get; set; } + public string ExternalId { get; set; } + public string AuthorUserId { get; set; } + public string OwnerUserId { get; set; } + public string ReviewerUserId { get; set; } + public string ApproverUserId { get; set; } + public DateTime? StartedOn { get; set; } + public DateTime? EndedOn { get; set; } + public DateTime? ReviewDueOn { get; set; } + public DateTime? SubmittedForReviewOn { get; set; } + public DateTime? ReturnedOn { get; set; } + public string ReturnReasonCode { get; set; } + public string ReturnReasonText { get; set; } + public int ReturnCount { get; set; } + public DateTime? ApprovedOn { get; set; } + public DateTime? FinalizedOn { get; set; } + public string FinalizedByUserId { get; set; } + public string CurrentRevisionId { get; set; } + public int RevisionCount { get; set; } + public string AmendsRevisionId { get; set; } + public DateTime? VoidedOn { get; set; } + public string VoidReasonCode { get; set; } + public string VoidReasonText { get; set; } + public DateTime? CancelledOn { get; set; } + public int OriginClient { get; set; } + public DateTime CreatedOn { get; set; } + public DateTime ModifiedOn { get; set; } + public long RowVersion { get; set; } + /// Weak ETag of the header row (W/"{RowVersion}"); send it back as If-Match or RowVersion on saves and commands. + public string ETag { get; set; } + public bool IsEditable { get; set; } + public bool IsRestricted { get; set; } + public List WithheldFields { get; set; } = new List(); + public List AvailableTransitions { get; set; } = new List(); + public RecordDetailsData Details { get; set; } = new RecordDetailsData(); + public List Participants { get; set; } = new List(); + public List Units { get; set; } = new List(); + public List Attachments { get; set; } = new List(); + public List Revisions { get; set; } = new List(); + public List GroupScopeIds { get; set; } = new List(); + } + + public class RecordDetailsData + { + public string Narrative { get; set; } + public string InitialReport { get; set; } + public string Type { get; set; } + public string Course { get; set; } + public string CourseCode { get; set; } + public string Instructors { get; set; } + public string Cause { get; set; } + public string InvestigatedByUserId { get; set; } + public string ContactName { get; set; } + public string ContactNumber { get; set; } + public string OtherPersonnel { get; set; } + public string Location { get; set; } + public string OtherAgencies { get; set; } + public string OtherUnits { get; set; } + public string BodyLocation { get; set; } + public string PronouncedDeceasedBy { get; set; } + public string CaseNumber { get; set; } + public string Destination { get; set; } + public string Facilitator { get; set; } + public int? UnitId { get; set; } + public DateTime? ActivityOn { get; set; } + public string CallNumber { get; set; } + public string CallName { get; set; } + public string CallType { get; set; } + public int? CallPriority { get; set; } + public DateTime? CallLoggedOn { get; set; } + public string CallAddress { get; set; } + public string CallNature { get; set; } + } + + public class RecordParticipantData + { + public string UserId { get; set; } + public string DisplayName { get; set; } + public int? GroupId { get; set; } + public string GroupName { get; set; } + public int? UnitId { get; set; } + public string Role { get; set; } + } + + public class RecordUnitResponseData + { + public int UnitId { get; set; } + public string UnitName { get; set; } + public string UnitType { get; set; } + public int? StationGroupId { get; set; } + public DateTime? Dispatched { get; set; } + public DateTime? Enroute { get; set; } + public DateTime? OnScene { get; set; } + public DateTime? Released { get; set; } + public DateTime? InQuarters { get; set; } + } + + public class RecordAttachmentData + { + public string AttachmentId { get; set; } + public string RecordId { get; set; } + public string FileName { get; set; } + public string ContentType { get; set; } + public long ByteSize { get; set; } + public string Checksum { get; set; } + public string Description { get; set; } + public string UploadedByUserId { get; set; } + public DateTime UploadedOn { get; set; } + public int ScanState { get; set; } + public string ScanStateName { get; set; } + } + + public class RecordRevisionData + { + public string RevisionId { get; set; } + public int RevisionNumber { get; set; } + public int Transition { get; set; } + public string TransitionName { get; set; } + public string PriorRevisionId { get; set; } + public string Checksum { get; set; } + public string ActorUserId { get; set; } + public string ReasonCode { get; set; } + public string ReasonText { get; set; } + public string AttestationStatementVersion { get; set; } + public DateTime? AttestedOn { get; set; } + public DateTime CreatedOn { get; set; } + } + + /// Create or save a draft. Dates are UTC. Every list replaces the draft rows wholesale. + public class SaveRecordDraftInput + { + /// Null on create. + public string RecordId { get; set; } + /// Required on save (or send If-Match); the row version the client last saw. + public long? RowVersion { get; set; } + /// Scoped idempotency key for offline-created drafts: the same key returns the same record. + public string IdempotencyKey { get; set; } + /// Client-generated GUID for an offline-created draft; server-assigned when null. + public string ClientRecordId { get; set; } + public string DefinitionKey { get; set; } + public int? CallId { get; set; } + public int? StationGroupId { get; set; } + public string ExternalId { get; set; } + public DateTime? StartedOn { get; set; } + public DateTime? EndedOn { get; set; } + public RecordDetailsInput Details { get; set; } = new RecordDetailsInput(); + public List Participants { get; set; } = new List(); + public List Units { get; set; } = new List(); + public string DuplicateContinueReason { get; set; } + /// RmsOriginClient: 2 Responder, 3 Unit, 4 IncidentCommand, 5 Dispatch, 6 Api (default). Field clients are gated by their Records.Field.* flag. + public int? OriginClient { get; set; } + } + + public class RecordDetailsInput + { + public string Narrative { get; set; } + public string InitialReport { get; set; } + public string Type { get; set; } + public string Course { get; set; } + public string CourseCode { get; set; } + public string Instructors { get; set; } + public string Cause { get; set; } + public string InvestigatedByUserId { get; set; } + public string ContactName { get; set; } + public string ContactNumber { get; set; } + public string OtherPersonnel { get; set; } + public string Location { get; set; } + public string OtherAgencies { get; set; } + public string OtherUnits { get; set; } + public string BodyLocation { get; set; } + public string PronouncedDeceasedBy { get; set; } + public string CaseNumber { get; set; } + public string Destination { get; set; } + public string Facilitator { get; set; } + public int? UnitId { get; set; } + public DateTime? ActivityOn { get; set; } + } + + public class RecordParticipantInputData + { + public string UserId { get; set; } + public int? UnitId { get; set; } + public string Role { get; set; } + } + + public class RecordUnitResponseInputData + { + public int UnitId { get; set; } + public DateTime? Dispatched { get; set; } + public DateTime? Enroute { get; set; } + public DateTime? OnScene { get; set; } + public DateTime? Released { get; set; } + public DateTime? InQuarters { get; set; } + } + + /// A lifecycle command. RowVersion (or If-Match) guards ETag-checked transitions; IdempotencyKey replays the first outcome. + public class RecordCommandInput + { + public string RecordId { get; set; } + public long? RowVersion { get; set; } + public string IdempotencyKey { get; set; } + public string ReasonCode { get; set; } + public string ReasonText { get; set; } + /// Finalize: the author confirmed the attestation statement. + public bool Attested { get; set; } + public string AttestationStatementVersion { get; set; } + /// Reassign: the new draft owner. + public string NewOwnerUserId { get; set; } + public int? OriginClient { get; set; } + } + + /// 409: the client's row version is stale. Reconcile against Current and the changed field paths; never last-writer-wins. + public class RecordConflictResult : StandardApiResponseV4Base + { + public RecordConflictData Data { get; set; } + } + + public class RecordConflictData + { + public string RecordId { get; set; } + public long ExpectedRowVersion { get; set; } + public long CurrentRowVersion { get; set; } + public int CurrentState { get; set; } + public string CurrentStateName { get; set; } + public string CurrentRevisionId { get; set; } + public List ChangedFieldPaths { get; set; } = new List(); + public RecordData Current { get; set; } + } + + public class RecordRevisionsResult : StandardApiResponseV4Base + { + public List Data { get; set; } = new List(); + } + + /// One revision rendered from its snapshot; restricted fields withheld without RecordRestricted_View. + public class RecordRevisionSnapshotResult : StandardApiResponseV4Base + { + public RecordRevisionSnapshotData Data { get; set; } + } + + public class RecordRevisionSnapshotData + { + public RecordRevisionData Revision { get; set; } + public RecordData Snapshot { get; set; } + } + + public class RecordDiffResult : StandardApiResponseV4Base + { + public RecordDiffData Data { get; set; } + } + + public class RecordDiffData + { + public string RecordId { get; set; } + public string FromRevisionId { get; set; } + public string ToRevisionId { get; set; } + public List Diffs { get; set; } = new List(); + } + + public class RecordFieldDiffData + { + public string Section { get; set; } + public string FieldKey { get; set; } + public string OldValue { get; set; } + public string NewValue { get; set; } + public bool Withheld { get; set; } + } + + public class RecordAttachmentsResult : StandardApiResponseV4Base + { + public List Data { get; set; } = new List(); + } + + public class RecordAttachmentContentResult : StandardApiResponseV4Base + { + public RecordAttachmentContentData Data { get; set; } + } + + public class RecordAttachmentContentData : RecordAttachmentData + { + /// Base64 content. + public string Data { get; set; } + } + + /// Open a resumable upload: declare size and SHA-256 up front (plan 5.3). + public class BeginRecordUploadInput + { + public string RecordId { get; set; } + public string FileName { get; set; } + public string ContentType { get; set; } + public long ByteSize { get; set; } + /// Lower-case hex SHA-256 of the whole file. + public string Sha256 { get; set; } + } + + public class RecordUploadResult : StandardApiResponseV4Base + { + public RecordUploadData Data { get; set; } + } + + public class RecordUploadData + { + public string UploadId { get; set; } + public string RecordId { get; set; } + public string FileName { get; set; } + public string ContentType { get; set; } + public long DeclaredSize { get; set; } + /// Resume from here: the next chunk's Offset. + public long ReceivedBytes { get; set; } + public int ChunkSize { get; set; } + public int ChunkCount { get; set; } + public int State { get; set; } + public string StateName { get; set; } + public DateTime ExpiresOn { get; set; } + public string AttachmentId { get; set; } + } + + public class RecordUploadChunkInput + { + public string UploadId { get; set; } + /// Byte offset of this chunk; must equal the session's ReceivedBytes. + public long Offset { get; set; } + /// Base64 chunk, at most ChunkSize bytes. + public string Data { get; set; } + } + + public class CompleteRecordUploadInput + { + public string UploadId { get; set; } + public string Description { get; set; } + } + + public class RecordUploadIdInput + { + public string UploadId { get; set; } + } + + public class RecordAttachmentIdInput + { + public string RecordId { get; set; } + public string AttachmentId { get; set; } + } + + public class RecordAttachmentResult : StandardApiResponseV4Base + { + public RecordAttachmentData Data { get; set; } + } +} diff --git a/Web/Resgrid.Web.Services/Resgrid.Web.Services.csproj b/Web/Resgrid.Web.Services/Resgrid.Web.Services.csproj index 76693284..7b5ef111 100644 --- a/Web/Resgrid.Web.Services/Resgrid.Web.Services.csproj +++ b/Web/Resgrid.Web.Services/Resgrid.Web.Services.csproj @@ -117,6 +117,7 @@ + diff --git a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml index 15fe1eb5..63aebbb9 100644 --- a/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml +++ b/Web/Resgrid.Web.Services/Resgrid.Web.Services.xml @@ -1903,6 +1903,38 @@ Exports the full after-action report (summary, times, lanes, utilization, timeline) as CSV. + + + NERIS incident reports over the v4 Records contract (RMS-2 aggregate, RMS-1B client contract): one authoritative + report per Call, source-aware prefill with provenance, ETag-guarded draft saves, local/destination validation, + attestation, and sanitized submission history. Same gates as Records: flag first, per-record visibility on + every read, Field Records flags for field clients. + + + + One report with all sections, provenance facts, validation issues and sanitized submission history. Sets a weak ETag. + + + The authoritative report for a Call, if one exists. + + + Starts the report for a Call with dispatch prefill (SingleAuthoritative): a second start returns the existing report with 200. + + + ETag-guarded draft save; 409 with the current report on a stale row version. + + + Runs local validation (and the destination's validate endpoint when IncludeDestination and submission is enabled); issues stay on the report. + + + Validate-then-sign: local errors return 422 with the issues; otherwise the revision, attestation and (when the profile allows) the submission are written. + + + After a rejection: a new revision and a new idempotency key, back into the submission queue. + + + Queues (or re-queues) the current revision for the destination. + Incident-scoped ad-hoc resources: create units/personnel on the fly for non-Resgrid resources, build @@ -2414,6 +2446,95 @@ while the Incident Commander/PIO has public sharing enabled. Disabling sharing revokes the token immediately. + + + Records (RMS) client contract, RMS-1B (plan sections 5.3, 5.4, 5.9.1): capability manifest with the locked + definition catalog and the Protected Data block, authorization-filtered list/search, a delta cursor with + tombstones, ETag-guarded draft saves that return the changed field paths on conflict, scoped idempotency on + creates and commands, revision history and diffs, and resumable checksummed attachment uploads. Every action + gates on the Records.System flag first; every read passes the per-record visibility rule. + + + + + The Records capability manifest: module/cutover state, the caller's effective Record permissions, per-app + Field Records flags, the published (locked) definition catalog with field applicability and minimum client + capability, search availability, upload limits, and the Protected Data block (plan 5.9.1). + + + + Plan 5.9.1: the same shape before and after Protected Data enrollment; NotInstalled while the subsystem is absent. + + + The authorization-filtered record queue (safe projection columns only). + + + + Free-text search over the records index (plan 5.10). Hits are re-checked against per-record visibility; + when the host is off or unavailable the filtered queue is returned with SearchDegraded set and the text is not applied. + + + + + Delta cursor (plan 5.3): records modified after (Unix epoch ms; 0 = full pull), + oldest first, with tombstones for cancelled, voided and deleted records so clients remove them locally. + Persist Data.ServerTimestampMs and pass it back as the next since; loop while HasMore. + + + + One record with its working details, participants, units, attachment metadata and revision summaries. Sets a weak ETag. + + + A revision rendered from its own snapshot; restricted fields withheld without RecordRestricted_View. + + + On-demand field-level diff between two revisions of the same record (plan 4.8). + + + + Creates a draft. IdempotencyKey (body or Idempotency-Key header) makes the create replayable: the same key + returns the existing record with 200 instead of a duplicate. Field clients need their Records.Field.* flag. + + + + + ETag-guarded draft save. RowVersion (or If-Match) must equal the current row version; otherwise 409 with + the current record and the field paths the client's copy would have changed, for deliberate reconciliation. + + + + Finalize: validates, writes the immutable revision and attestation, emits the lifecycle events. Attested must be true. Online-only by design. + + + + Shared command path: flag/usable gate, field-client gate, per-record visibility, scoped idempotency replay, + ETag check where the transition takes one, and the 409/400 mapping. Never last-writer-wins. + + + + Attachment content (base64), department- and record-bound, audited as a read. + + + Opens a resumable upload session (plan 5.3): declare size and SHA-256; send chunks with UploadChunk; finish with CompleteUpload. + + + One chunk at the session's next offset (base64, at most ChunkSize bytes). Returns ReceivedBytes to resume from. + + + Session status, for resuming after a dropped connection. + + + Verifies size and SHA-256, then stores the attachment through hygiene and the scanner. 422 on checksum mismatch or rejection. + + + Writes need the module usable: flag on and the department activated. + + + A field client below its flag fails closed for authoring (plan 4.1); non-field origins pass. + + + Loads a record only when the caller passes the per-record visibility rule; a refusal is audited as Denied. + Reporting and analytics for the caller's department: a composite dashboard, realtime personnel/ @@ -5645,6 +5766,24 @@ Reads are inherited from and accept both formats. + + Shared plumbing for the v4 Records contract: origin/field-flag resolution, ETag headers, UTC normalization. + + + The Records.Field.* flag a field client must have on to create or edit; null for non-field origins. + + + Body RowVersion wins; otherwise If-Match; null when neither is usable. + + + Clients send UTC; a local-kind value is converted, an unspecified one is taken as UTC. + + + Entity to DTO mapping for the v4 Records controller. Restricted detail fields are withheld here, never downstream. + + + Entity to DTO mapping for the v4 IncidentReports controller. Submissions are sanitized: no payload, no response body. + Serialises a that is known to hold a UTC instant with an explicit "Z". @@ -11781,6 +11920,114 @@ Response Data + + A hydrated NERIS incident report: header, dispatch facts, sections, provenance, validation issues, sanitized submission history. + + + Provenance for one prefilled value (plan 4.2): where it came from, what it was, what it is now. + + + Sanitized delivery outcome (plan 5.6): state, external id/status, attempts, codes and field paths. Never payload or response bodies. + + + Start (or return) the one authoritative incident report for a Call. + + + Draft save; every list replaces the draft rows. Dates are UTC. + + + Validate: also call the destination's validate endpoint when submission is enabled. + + + 409 on a stale incident report save. + + + Capability manifest for the Records module (RMS plan sections 5.4, 5.9.1). + + + The contract version this server speaks; a client below a definition's MinimumClientCapability fails closed for authoring. + + + Protected Data block (plan 5.9.1): same shape before and after enrollment. + + + Per-app Field Records flags; a field client whose flag is off is refused on create/edit. + + + NotInstalled until the department is enrolled; then the DepartmentDataProtectionState name. + + + Paged list of records the caller may see. + + + Search only: the text was not applied because the search host is off or unavailable. + + + Safe projection columns only (plan 5.10): never narrative, restricted detail or attachment bytes. + + + Delta feed: the record is gone for the client (deleted, cancelled or voided); remove it locally. + + + Delta cursor (plan 5.3): persist ServerTimestampMs and pass it back as since. + + + A hydrated Record: header, working details, participants, units, attachment metadata and revision summaries. Restricted fields are withheld without RecordRestricted_View. + + + Weak ETag of the header row (W/"{RowVersion}"); send it back as If-Match or RowVersion on saves and commands. + + + Create or save a draft. Dates are UTC. Every list replaces the draft rows wholesale. + + + Null on create. + + + Required on save (or send If-Match); the row version the client last saw. + + + Scoped idempotency key for offline-created drafts: the same key returns the same record. + + + Client-generated GUID for an offline-created draft; server-assigned when null. + + + RmsOriginClient: 2 Responder, 3 Unit, 4 IncidentCommand, 5 Dispatch, 6 Api (default). Field clients are gated by their Records.Field.* flag. + + + A lifecycle command. RowVersion (or If-Match) guards ETag-checked transitions; IdempotencyKey replays the first outcome. + + + Finalize: the author confirmed the attestation statement. + + + Reassign: the new draft owner. + + + 409: the client's row version is stale. Reconcile against Current and the changed field paths; never last-writer-wins. + + + One revision rendered from its snapshot; restricted fields withheld without RecordRestricted_View. + + + Base64 content. + + + Open a resumable upload: declare size and SHA-256 up front (plan 5.3). + + + Lower-case hex SHA-256 of the whole file. + + + Resume from here: the next chunk's Offset. + + + Byte offset of this chunk; must equal the session's ReceivedBytes. + + + Base64 chunk, at most ChunkSize bytes. + Composite dashboard report (scalar totals, dense series, breakdowns). diff --git a/Web/Resgrid.Web.Services/Startup.cs b/Web/Resgrid.Web.Services/Startup.cs index e42aac0e..a88c723d 100644 --- a/Web/Resgrid.Web.Services/Startup.cs +++ b/Web/Resgrid.Web.Services/Startup.cs @@ -682,6 +682,7 @@ public void ConfigureContainer(ContainerBuilder builder) builder.RegisterModule(new ServicesModule()); builder.RegisterModule(new Resgrid.Search.SearchModule()); builder.RegisterModule(new Resgrid.Providers.Scanning.ScanningProviderModule()); + builder.RegisterModule(new Resgrid.Providers.Neris.NerisProviderModule()); builder.RegisterModule(new ProviderModule()); builder.RegisterModule(new EmailProviderModule()); builder.RegisterModule(new BusModule()); diff --git a/Web/Resgrid.Web/Areas/User/Controllers/IncidentReportsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/IncidentReportsController.cs new file mode 100644 index 00000000..ee5d3b63 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Controllers/IncidentReportsController.cs @@ -0,0 +1,731 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.Localization; +using Resgrid.Framework; +using Resgrid.Model; +using Resgrid.Model.Helpers; +using Resgrid.Model.Providers; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; +using Resgrid.Providers.Claims; +using Resgrid.Services.Records; +using Resgrid.Web.Areas.User.Models.Records; +using Resgrid.Web.Helpers; + +namespace Resgrid.Web.Areas.User.Controllers +{ + /// + /// NERIS incident reports (RMS-2, plan sections 4.2, 5.3, 5.5): queue, start-from-call, sectioned draft editing + /// with provenance, local/destination validation, finalize with attestation, submission history and the + /// department NERIS profile/crosswalk settings. Every read passes the per-record visibility rule. + /// + [Area("User")] + public class IncidentReportsController : SecureBaseController + { + private const string IncidentTypeSet = "incident_type"; + + private readonly IIncidentReportsService _incidentReports; + private readonly IRecordsCutoverService _cutoverService; + private readonly IRecordsAuthorizationService _recordsAuthorizationService; + private readonly IDepartmentsService _departmentsService; + private readonly IDepartmentGroupsService _departmentGroupsService; + private readonly IUnitsService _unitsService; + private readonly ICallsService _callsService; + private readonly INerisProfileService _neris; + private readonly IRmsSubmissionsRepository _submissions; + private readonly IStringLocalizer _localizer; + + public IncidentReportsController(IIncidentReportsService incidentReports, IRecordsCutoverService cutoverService, IRecordsAuthorizationService recordsAuthorizationService, + IDepartmentsService departmentsService, IDepartmentGroupsService departmentGroupsService, IUnitsService unitsService, ICallsService callsService, + INerisProfileService neris, IRmsSubmissionsRepository submissions, IStringLocalizer localizer) + { + _incidentReports = incidentReports; + _cutoverService = cutoverService; + _recordsAuthorizationService = recordsAuthorizationService; + _departmentsService = departmentsService; + _departmentGroupsService = departmentGroupsService; + _unitsService = unitsService; + _callsService = callsService; + _neris = neris; + _submissions = submissions; + _localizer = localizer; + } + + #region Queue / start + + [HttpGet] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task Index(int? year, string state, int page = 1) + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + + var profile = await _neris.GetProfileAsync(DepartmentId); + var model = new IncidentReportsIndexView + { + ModuleState = moduleState, + Department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId, false), + IsDepartmentAdmin = ClaimsAuthorizationHelper.IsUserDepartmentAdmin(), + SystemEnabled = Config.NerisConfig.Enabled, + ProfileConfigured = profile != null && !string.IsNullOrWhiteSpace(profile.NerisEntityId), + SubmissionEnabled = await _neris.IsSubmissionEnabledAsync(DepartmentId), + Year = year, + StateFilter = state, + Page = Math.Max(1, page), + States = Enum.GetValues(typeof(RmsRecordState)).Cast().Select(s => new SelectListItem { Value = ((int)s).ToString(), Text = s.ToString() }).ToList() + }; + if (TempData["RecordsMessage"] is string message) + model.Message = message; + if (TempData["RecordsError"] is string error) + model.ErrorMessage = error; + + if (!moduleState.RecordsUsable) + return View(model); + + model.Years = (await _incidentReports.GetYearsAsync(DepartmentId)).Select(y => new SelectListItem { Value = y.ToString(), Text = y.ToString() }).ToList(); + var query = new RmsIncidentReportQuery + { + Year = year, + States = int.TryParse(state, out var stateValue) ? new List { stateValue } : null, + Skip = (model.Page - 1) * model.PageSize, + Take = model.PageSize + }; + var visibleGroups = await _recordsAuthorizationService.GetVisibleGroupIdsAsync(UserId, DepartmentId); + var reports = await _incidentReports.QueryAsync(DepartmentId, query); + // Per-record visibility (plan 5.7.1): the queue never lists a report the viewer cannot open. + model.Reports = new List(); + foreach (var report in reports) + { + if (visibleGroups == null || await _recordsAuthorizationService.CanUserViewRecordAsync(UserId, report.RmsIncidentReportId, DepartmentId)) + model.Reports.Add(report); + } + model.Total = visibleGroups == null ? await _incidentReports.CountAsync(DepartmentId, query) : model.Reports.Count + query.Skip; + model.PersonnelNames = await PersonnelNamesAsync(); + + if (ClaimsAuthorizationHelper.CanCreateRecord()) + { + var calls = await _callsService.GetActiveCallsByDepartmentAsync(DepartmentId) ?? new List(); + model.ActiveCalls = calls.OrderByDescending(c => c.LoggedOn) + .Select(c => new SelectListItem { Value = c.CallId.ToString(), Text = $"{c.Number} - {c.Name}" }).ToList(); + } + + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task Start(int callId, CancellationToken cancellationToken) + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.RecordsUsable) + return NotFound(); + + try + { + var existing = await _incidentReports.GetForCallAsync(DepartmentId, callId); + var aggregate = await _incidentReports.StartFromCallAsync(DepartmentId, UserId, callId, RmsOriginClient.Web, cancellationToken); + TempData["RecordsMessage"] = existing != null ? _localizer["ExistingReportForCall"].Value : _localizer["IncidentReportStarted"].Value; + return RedirectToAction(existing != null ? "Details" : "Edit", new { id = aggregate.Report.RmsIncidentReportId }); + } + catch (ArgumentException ex) + { + TempData["RecordsError"] = ex.Message; + return RedirectToAction("Index"); + } + } + + /// Entry from a Call page: opens the existing report or starts one. + [HttpGet] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task ForCall(int callId) + { + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.RecordsUsable) + return NotFound(); + + var existing = await _incidentReports.GetForCallAsync(DepartmentId, callId); + if (existing != null) + return RedirectToAction("Details", new { id = existing.Report.RmsIncidentReportId }); + + return RedirectToAction("Index"); + } + + #endregion + + #region Reads + + [HttpGet] + [Authorize(Policy = ResgridResources.Record_View)] + public async Task Details(string id) + { + var model = await BuildDetailAsync(id); + if (model == null) + return NotFound(); + + if (TempData["RecordsMessage"] is string message) + model.Message = message; + await _incidentReports.RecordAccessAsync(DepartmentId, UserId, id, null, RmsAccessAuditAction.Read, null, IpAddressHelper.GetRequestIP(Request, true)); + return View(model); + } + + /// The immutable submission artifact (the exact payload sent), for administrators auditing a delivery. + [HttpGet] + [Authorize(Policy = ResgridResources.Record_Submit)] + public async Task Payload(string id, string submissionId) + { + if (!ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + return Unauthorized(); + + var aggregate = await LoadAuthorizedAsync(id, true); + if (aggregate == null) + return NotFound(); + + var submission = aggregate.Submissions.FirstOrDefault(s => s.RmsSubmissionId == submissionId); + if (submission == null || string.IsNullOrWhiteSpace(submission.PayloadJson)) + return NotFound(); + + await _incidentReports.RecordAccessAsync(DepartmentId, UserId, id, submission.RevisionId, RmsAccessAuditAction.Export, $"Submission payload {submission.RmsSubmissionId}", IpAddressHelper.GetRequestIP(Request, true)); + return File(Encoding.UTF8.GetBytes(submission.PayloadJson), "application/json", $"neris-{aggregate.Report.RecordNumber ?? aggregate.Report.DraftReference}-{submission.RmsSubmissionId.Substring(0, 8)}.json"); + } + + #endregion + + #region Authoring + + [HttpGet] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task Edit(string id) + { + var aggregate = await LoadAuthorizedAsync(id); + if (aggregate == null) + return NotFound(); + if (!CanEditReport(aggregate.Report)) + return Unauthorized(); + + var model = await BuildEditAsync(aggregate); + if (TempData["RecordsMessage"] is string message) + model.Message = message; + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task Edit(IncidentReportEditView model, CancellationToken cancellationToken) + { + var aggregate = await LoadAuthorizedAsync(model.ReportId); + if (aggregate == null) + return NotFound(); + if (!CanEditReport(aggregate.Report)) + return Unauthorized(); + + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId, false); + try + { + await _incidentReports.SaveDraftAsync(DepartmentId, UserId, model.ReportId, model.RowVersion, BuildInput(model, department), cancellationToken); + if (model.ValidateAfterSave) + { + var issues = await _incidentReports.ValidateAsync(DepartmentId, model.ReportId, true, cancellationToken); + TempData["RecordsMessage"] = issues.Count == 0 ? _localizer["NoValidationIssues"].Value : string.Format(_localizer["ValidationRun"].Value, issues.Count); + return RedirectToAction(issues.Any(i => i.Severity == (int)RmsValidationSeverity.Error) ? "Edit" : "Details", new { id = model.ReportId }); + } + + TempData["RecordsMessage"] = _localizer["RecordSaved"].Value; + return RedirectToAction("Details", new { id = model.ReportId }); + } + catch (RecordConcurrencyException) + { + return await EditWithErrorAsync(aggregate, _localizer["ConcurrencyError"]); + } + catch (Exception ex) when (ex is ArgumentException || ex is RecordTransitionException) + { + return await EditWithErrorAsync(aggregate, ex.Message); + } + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task Validate(string id, CancellationToken cancellationToken) + { + var aggregate = await LoadAuthorizedAsync(id); + if (aggregate == null) + return NotFound(); + + try + { + var issues = await _incidentReports.ValidateAsync(DepartmentId, id, true, cancellationToken); + TempData["RecordsMessage"] = issues.Count == 0 ? _localizer["NoValidationIssues"].Value : string.Format(_localizer["ValidationRun"].Value, issues.Count); + return RedirectToAction("Details", new { id }); + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) + { + return await DetailsWithErrorAsync(id, ex.Message); + } + } + + #endregion + + #region Lifecycle + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Create)] + public async Task SubmitForReview(string id, long rowVersion, CancellationToken cancellationToken) + { + return await TransitionAsync(id, () => _incidentReports.SubmitForReviewAsync(DepartmentId, UserId, id, rowVersion, cancellationToken)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Review)] + public async Task ReturnForCorrection(string id, string reasonCode, string reasonText, CancellationToken cancellationToken) + { + return await TransitionAsync(id, () => _incidentReports.ReturnForCorrectionAsync(DepartmentId, UserId, id, reasonCode, reasonText, cancellationToken)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Finalize)] + public async Task Finalize(string id, long rowVersion, bool attested, string reasonCode, string reasonText, CancellationToken cancellationToken) + { + if (!attested) + return await DetailsWithErrorAsync(id, _localizer["Attestation"]); + + return await TransitionAsync(id, () => _incidentReports.FinalizeAsync(DepartmentId, UserId, id, rowVersion, IncidentReportsService.AttestationStatementVersion, IpAddressHelper.GetRequestIP(Request, true), reasonCode, reasonText, cancellationToken)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Submit)] + public async Task CorrectAndResubmit(string id, long rowVersion, bool attested, string reasonCode, string reasonText, CancellationToken cancellationToken) + { + if (!attested) + return await DetailsWithErrorAsync(id, _localizer["Attestation"]); + + return await TransitionAsync(id, () => _incidentReports.CorrectAndResubmitAsync(DepartmentId, UserId, id, rowVersion, IncidentReportsService.AttestationStatementVersion, IpAddressHelper.GetRequestIP(Request, true), reasonCode, reasonText, cancellationToken)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Submit)] + public async Task Submit(string id, CancellationToken cancellationToken) + { + var result = await TransitionAsync(id, () => _incidentReports.QueueSubmissionAsync(DepartmentId, UserId, id, cancellationToken)); + if (result is RedirectToActionResult) + TempData["RecordsMessage"] = _localizer["SubmissionQueued"].Value; + return result; + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Amend)] + public async Task Amend(string id, CancellationToken cancellationToken) + { + return await TransitionAsync(id, () => _incidentReports.OpenAmendmentAsync(DepartmentId, UserId, id, cancellationToken), "Edit"); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Amend)] + public async Task AbandonAmendment(string id, CancellationToken cancellationToken) + { + return await TransitionAsync(id, () => _incidentReports.AbandonAmendmentAsync(DepartmentId, UserId, id, cancellationToken)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Void)] + public async Task Void(string id, string reasonCode, string reasonText, CancellationToken cancellationToken) + { + return await TransitionAsync(id, () => _incidentReports.VoidAsync(DepartmentId, UserId, id, reasonCode, reasonText, cancellationToken)); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Void)] + public async Task CancelDraft(string id, CancellationToken cancellationToken) + { + var result = await TransitionAsync(id, () => _incidentReports.CancelAsync(DepartmentId, UserId, id, cancellationToken)); + return result is RedirectToActionResult ? RedirectToAction("Index") : result; + } + + private async Task TransitionAsync(string id, Func> action, string successAction = "Details") + { + if (await LoadAuthorizedAsync(id) == null) + return NotFound(); + + try + { + await action(); + return RedirectToAction(successAction, new { id }); + } + catch (RecordConcurrencyException) + { + return await DetailsWithErrorAsync(id, _localizer["ConcurrencyError"]); + } + catch (IncidentReportValidationException ex) + { + return await DetailsWithErrorAsync(id, _localizer["ValidationBlocked"] + " " + ex.Message); + } + catch (Exception ex) when (ex is ArgumentException || ex is RecordTransitionException || ex is InvalidOperationException) + { + return await DetailsWithErrorAsync(id, ex.Message); + } + } + + #endregion + + #region Settings (NERIS profile + crosswalk) + + [HttpGet] + [Authorize(Policy = ResgridResources.Record_Submit)] + public async Task Settings() + { + if (!ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + return Unauthorized(); + + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + + var model = await BuildSettingsAsync(moduleState); + if (TempData["RecordsMessage"] is string message) + model.Message = message; + return View(model); + } + + [HttpPost] + [ValidateAntiForgeryToken] + [Authorize(Policy = ResgridResources.Record_Submit)] + public async Task Settings(NerisSettingsView model, CancellationToken cancellationToken) + { + if (!ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) + return Unauthorized(); + + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return NotFound(); + + try + { + var profile = await _neris.GetProfileAsync(DepartmentId) ?? new RmsNerisProfile { DepartmentId = DepartmentId }; + profile.NerisEntityId = string.IsNullOrWhiteSpace(model.NerisEntityId) ? null : model.NerisEntityId.Trim().ToUpperInvariant(); + profile.EntityName = string.IsNullOrWhiteSpace(model.EntityName) ? null : model.EntityName.Trim(); + profile.Environment = string.IsNullOrWhiteSpace(model.Environment) ? NerisEnvironments.Production : model.Environment; + profile.BaseUrlOverride = string.IsNullOrWhiteSpace(model.BaseUrlOverride) ? null : model.BaseUrlOverride.Trim(); + profile.GrantType = string.IsNullOrWhiteSpace(model.GrantType) ? NerisGrantTypes.Password : model.GrantType; + profile.AutoSubmitOnFinalize = model.AutoSubmitOnFinalize; + profile.IsEnabled = model.IsEnabled; + + // The credential is write-only: any filled field replaces the stored one; all blank keeps it. + NerisCredential credential = null; + if (!string.IsNullOrWhiteSpace(model.Password) || !string.IsNullOrWhiteSpace(model.ClientSecret) || !string.IsNullOrWhiteSpace(model.Username) || !string.IsNullOrWhiteSpace(model.ClientId)) + credential = new NerisCredential { Username = model.Username?.Trim(), Password = model.Password, ClientId = model.ClientId?.Trim(), ClientSecret = model.ClientSecret }; + + await _neris.SaveProfileAsync(profile, credential, UserId, cancellationToken); + + var existing = (await _neris.GetCrosswalksAsync(DepartmentId)).Where(c => c.SetKey == IncidentTypeSet && c.LocalSource == NerisCrosswalkSources.CallType).ToList(); + foreach (var row in model.Crosswalk ?? new List()) + { + if (string.IsNullOrWhiteSpace(row.CallType)) + continue; + var current = existing.FirstOrDefault(c => string.Equals(c.LocalCode, row.CallType, StringComparison.OrdinalIgnoreCase)); + if (string.IsNullOrWhiteSpace(row.NerisCode)) + { + if (current != null) + await _neris.RemoveCrosswalkAsync(DepartmentId, IncidentTypeSet, NerisCrosswalkSources.CallType, row.CallType, cancellationToken); + } + else if (current == null || !string.Equals(current.NerisCode, row.NerisCode, StringComparison.Ordinal)) + { + await _neris.SaveCrosswalkAsync(DepartmentId, UserId, IncidentTypeSet, NerisCrosswalkSources.CallType, row.CallType, row.NerisCode, cancellationToken); + } + } + + TempData["RecordsMessage"] = _localizer["NerisSettingsSaved"].Value; + return RedirectToAction("Settings"); + } + catch (ArgumentException ex) + { + var rebuilt = await BuildSettingsAsync(moduleState); + rebuilt.ErrorMessage = ex.Message; + return View(rebuilt); + } + } + + private async Task BuildSettingsAsync(RecordsModuleState moduleState) + { + var profile = await _neris.GetProfileAsync(DepartmentId); + var model = new NerisSettingsView + { + ModuleState = moduleState, + Department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId, false), + SystemEnabled = Config.NerisConfig.Enabled, + ContractVersion = _neris.ContractVersion, + NerisEntityId = profile?.NerisEntityId, + EntityName = profile?.EntityName, + Environment = profile?.Environment ?? NerisEnvironments.Production, + BaseUrlOverride = profile?.BaseUrlOverride, + GrantType = profile?.GrantType ?? NerisGrantTypes.Password, + AutoSubmitOnFinalize = profile?.AutoSubmitOnFinalize ?? false, + IsEnabled = profile?.IsEnabled ?? false, + HasCredential = !string.IsNullOrWhiteSpace(profile?.EncryptedCredentialJson), + LastTokenIssuedOn = profile?.LastTokenIssuedOn, + LastSuccessfulCallOn = profile?.LastSuccessfulCallOn, + LastError = profile?.LastError, + SubmissionEnabled = await _neris.IsSubmissionEnabledAsync(DepartmentId), + Environments = new List + { + new SelectListItem { Value = NerisEnvironments.Production, Text = NerisEnvironments.Production }, + new SelectListItem { Value = NerisEnvironments.Sandbox, Text = NerisEnvironments.Sandbox } + }, + GrantTypes = new List + { + new SelectListItem { Value = NerisGrantTypes.Password, Text = NerisGrantTypes.Password }, + new SelectListItem { Value = NerisGrantTypes.ClientCredentials, Text = NerisGrantTypes.ClientCredentials } + }, + IncidentTypeCodes = Codes(IncidentTypeSet) + }; + + var crosswalks = (await _neris.GetCrosswalksAsync(DepartmentId)).Where(c => c.SetKey == IncidentTypeSet && c.LocalSource == NerisCrosswalkSources.CallType).ToList(); + var callTypes = (await _callsService.GetCallTypesForDepartmentAsync(DepartmentId) ?? new List()).Select(t => t.Type).Where(t => !string.IsNullOrWhiteSpace(t)).Distinct(StringComparer.OrdinalIgnoreCase).OrderBy(t => t).ToList(); + foreach (var mapped in crosswalks.Select(c => c.LocalCode).Where(c => !callTypes.Contains(c, StringComparer.OrdinalIgnoreCase))) + callTypes.Add(mapped); + model.Crosswalk = callTypes.Select(t => new NerisCrosswalkRow { CallType = t, NerisCode = crosswalks.FirstOrDefault(c => string.Equals(c.LocalCode, t, StringComparison.OrdinalIgnoreCase))?.NerisCode }).ToList(); + + foreach (RmsSubmissionState state in Enum.GetValues(typeof(RmsSubmissionState))) + model.QueueCounts[state] = await _submissions.CountByStateAsync((int)state); + + return model; + } + + #endregion + + #region Helpers + + private async Task BuildDetailAsync(string id) + { + var aggregate = await LoadAuthorizedAsync(id, true); + if (aggregate == null) + return null; + + var groups = await _departmentGroupsService.GetAllGroupsForDepartmentAsync(DepartmentId) ?? new List(); + return new IncidentReportDetailView + { + Aggregate = aggregate, + Department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId, false), + Profile = await _neris.GetProfileAsync(DepartmentId), + SubmissionEnabled = await _neris.IsSubmissionEnabledAsync(DepartmentId), + PersonnelNames = await PersonnelNamesAsync(), + GroupNames = groups.ToDictionary(g => g.DepartmentGroupId, g => g.Name), + CanEdit = CanEditReport(aggregate.Report), + CanReview = ClaimsAuthorizationHelper.CanReviewRecords(), + CanFinalize = ClaimsAuthorizationHelper.CanFinalizeRecords(), + CanSubmit = ClaimsAuthorizationHelper.CanSubmitRecords(), + CanAmend = ClaimsAuthorizationHelper.CanAmendRecords(), + CanVoid = ClaimsAuthorizationHelper.CanVoidRecords(), + CanExport = ClaimsAuthorizationHelper.CanExportRecords(), + IsDepartmentAdmin = ClaimsAuthorizationHelper.IsUserDepartmentAdmin() + }; + } + + private async Task DetailsWithErrorAsync(string id, string error) + { + var model = await BuildDetailAsync(id); + if (model == null) + return NotFound(); + model.ErrorMessage = error; + return View("Details", model); + } + + private async Task EditWithErrorAsync(IncidentReportAggregate aggregate, string error) + { + var model = await BuildEditAsync(aggregate); + model.ErrorMessage = error; + return View("Edit", model); + } + + /// Loads a report only when the flag is on and the viewer passes the per-record visibility check. + private async Task LoadAuthorizedAsync(string id, bool includeHistory = false) + { + if (string.IsNullOrWhiteSpace(id)) + return null; + + var moduleState = await _cutoverService.GetModuleStateAsync(DepartmentId); + if (!moduleState.FlagEnabled) + return null; + + if (!await _recordsAuthorizationService.CanUserViewRecordAsync(UserId, id, DepartmentId)) + { + await _incidentReports.RecordAccessAsync(DepartmentId, UserId, id, null, RmsAccessAuditAction.Denied, null, IpAddressHelper.GetRequestIP(Request, true)); + return null; + } + + return await _incidentReports.GetAsync(DepartmentId, id, includeHistory); + } + + private bool CanEditReport(RmsIncidentReport report) + { + if (!ClaimsAuthorizationHelper.CanCreateRecord()) + return false; + + return ClaimsAuthorizationHelper.IsUserDepartmentAdmin() + || string.Equals(report.OwnerUserId, UserId, StringComparison.OrdinalIgnoreCase) + || string.Equals(report.AuthorUserId, UserId, StringComparison.OrdinalIgnoreCase) + || (report.AmendsRevisionId != null && ClaimsAuthorizationHelper.CanAmendRecords()); + } + + private async Task BuildEditAsync(IncidentReportAggregate aggregate) + { + var r = aggregate.Report; + var department = await _departmentsService.GetDepartmentByIdAsync(DepartmentId, false); + var call = await _callsService.GetCallByIdAsync(r.CallId); + var model = new IncidentReportEditView + { + ReportId = r.RmsIncidentReportId, + RowVersion = r.RowVersion, + DraftReference = r.DraftReference, + RecordNumber = r.RecordNumber, + CallId = r.CallId, + CallLabel = call == null ? r.CallId.ToString() : $"{call.Number} - {call.Name}", + IsAmendment = r.AmendsRevisionId != null, + IsRejected = r.State == (int)RmsRecordState.Rejected, + RejectionSummary = r.RejectionSummary, + State = (RmsRecordState)r.State, + IncidentNumber = r.IncidentNumber, + CallCreatedOn = r.CallCreatedOn?.TimeConverter(department), + CallAnsweredOn = r.CallAnsweredOn?.TimeConverter(department), + CallArrivalOn = r.CallArrivalOn?.TimeConverter(department), + IncidentClearedOn = r.IncidentClearedOn?.TimeConverter(department), + DispatchCenterId = r.DispatchCenterId, + DeterminantCode = r.DeterminantCode, + DispatchIncidentCode = r.DispatchIncidentCode, + Disposition = r.Disposition, + PeoplePresent = r.PeoplePresent, + DisplacementCount = r.DisplacementCount, + AnimalsRescued = r.AnimalsRescued, + SpecialModifiers = string.IsNullOrWhiteSpace(r.SpecialModifiersCsv) ? new List() : r.SpecialModifiersCsv.Split(',').Select(s => s.Trim()).Where(s => s.Length > 0).ToList(), + StationGroupId = r.StationGroupId, + Narrative = aggregate.Narrative?.Narrative, + ImpedimentNarrative = aggregate.Narrative?.ImpedimentNarrative, + OutcomeNarrative = aggregate.Narrative?.OutcomeNarrative, + Department = department, + Facts = aggregate.Facts.Where(f => !string.IsNullOrWhiteSpace(f.FactKey)).GroupBy(f => f.FactKey, StringComparer.Ordinal).ToDictionary(g => g.Key, g => g.First(), StringComparer.Ordinal), + Issues = aggregate.Issues + }; + + if (aggregate.Location != null) + { + var l = aggregate.Location; + model.AddressText = l.AddressText; model.Number = l.Number; model.NumberPrefix = l.NumberPrefix; model.NumberSuffix = l.NumberSuffix; model.Street = l.Street; model.UnitValue = l.UnitValue; + model.Municipality = l.Municipality; model.County = l.County; model.LocationState = l.State; model.PostalCode = l.PostalCode; model.Country = l.Country; + model.PlaceType = l.PlaceType; model.LocationUse = l.LocationUse; model.CrossStreet1 = l.CrossStreet1; model.CrossStreet2 = l.CrossStreet2; model.Latitude = l.Latitude; model.Longitude = l.Longitude; + } + + model.Types = aggregate.Types.OrderBy(t => t.Ordinal).Select(t => new IncidentTypeRow { TypeCode = t.TypeCode, IsPrimary = t.IsPrimary }).ToList(); + model.Units = aggregate.Units.Where(u => u.UnitId.HasValue).Select(u => new IncidentUnitRow + { + UnitId = u.UnitId.Value, Selected = true, UnitNerisId = u.UnitNerisId, Staffing = u.Staffing, UnableToDispatch = u.UnableToDispatch, + DispatchedOn = u.DispatchedOn?.TimeConverter(department), EnrouteOn = u.EnrouteOn?.TimeConverter(department), OnSceneOn = u.OnSceneOn?.TimeConverter(department), + StagingOn = u.StagingOn?.TimeConverter(department), CanceledEnrouteOn = u.CanceledEnrouteOn?.TimeConverter(department), ClearedOn = u.ClearedOn?.TimeConverter(department), + ResponseMode = u.ResponseMode + }).ToList(); + model.Aids = aggregate.Aids.OrderBy(a => a.Ordinal).Select(a => new IncidentAidRow { Direction = a.Direction, AidType = a.AidType, CounterpartNerisId = a.CounterpartNerisId, CounterpartName = a.CounterpartName, IsNonFireDepartment = a.IsNonFireDepartment, NonFdType = a.NonFdType }).ToList(); + model.Tactics = aggregate.Tactics.OrderBy(t => t.Ordinal).Select(t => new IncidentTacticRow { TacticCode = t.TacticCode, ActorUnitId = t.ActorUnitId, OccurredOn = t.OccurredOn?.TimeConverter(department) }).ToList(); + + model.IncidentTypeCodes = Codes(IncidentTypeSet); + model.TacticCodes = Codes("action_tactic"); + model.AidTypes = Codes("aid_type"); + model.AidDirections = Codes("aid_direction"); + model.NonFdTypes = Codes("aid_nonfd"); + model.SpecialModifierCodes = Codes("special_modifier"); + model.PlaceTypes = Codes("location_place"); + model.LocationUses = Codes("location_use"); + model.ResponseModes = Codes("response_mode"); + model.Stations = (await _departmentGroupsService.GetAllGroupsForDepartmentAsync(DepartmentId) ?? new List()).OrderBy(g => g.Name).Select(g => new SelectListItem { Value = g.DepartmentGroupId.ToString(), Text = g.Name }).ToList(); + model.AvailableUnits = (await _unitsService.GetUnitsForDepartmentAsync(DepartmentId) ?? new List()).OrderBy(u => u.Name).Select(u => new SelectListItem { Value = u.UnitId.ToString(), Text = u.Name }).ToList(); + return model; + } + + private IncidentReportDraftInput BuildInput(IncidentReportEditView model, Department department) + { + var hasLocation = new[] { model.AddressText, model.Number, model.Street, model.Municipality, model.PostalCode, model.PlaceType, model.LocationUse, model.CrossStreet1 }.Any(s => !string.IsNullOrWhiteSpace(s)) || model.Latitude.HasValue; + return new IncidentReportDraftInput + { + IncidentNumber = model.IncidentNumber, + CallCreatedOn = ToUtc(model.CallCreatedOn, department), + CallAnsweredOn = ToUtc(model.CallAnsweredOn, department), + CallArrivalOn = ToUtc(model.CallArrivalOn, department), + IncidentClearedOn = ToUtc(model.IncidentClearedOn, department), + DispatchCenterId = model.DispatchCenterId, + DeterminantCode = model.DeterminantCode, + DispatchIncidentCode = model.DispatchIncidentCode, + Disposition = model.Disposition, + PeoplePresent = model.PeoplePresent, + DisplacementCount = model.DisplacementCount, + AnimalsRescued = model.AnimalsRescued, + SpecialModifiers = model.SpecialModifiers ?? new List(), + StationGroupId = model.StationGroupId, + Location = !hasLocation ? null : new IncidentLocationInput + { + AddressText = model.AddressText, Number = model.Number, NumberPrefix = model.NumberPrefix, NumberSuffix = model.NumberSuffix, Street = model.Street, UnitValue = model.UnitValue, + Municipality = model.Municipality, County = model.County, State = model.LocationState, PostalCode = model.PostalCode, Country = model.Country, + PlaceType = model.PlaceType, LocationUse = model.LocationUse, CrossStreet1 = model.CrossStreet1, CrossStreet2 = model.CrossStreet2, Latitude = model.Latitude, Longitude = model.Longitude + }, + Types = (model.Types ?? new List()).Where(t => !string.IsNullOrWhiteSpace(t.TypeCode)).Select(t => new IncidentTypeInput { TypeCode = t.TypeCode, IsPrimary = t.IsPrimary }).ToList(), + Units = (model.Units ?? new List()).Where(u => u.Selected && u.UnitId > 0).Select(u => new IncidentUnitResponseInput + { + UnitId = u.UnitId, UnitNerisId = u.UnitNerisId, Staffing = u.Staffing, UnableToDispatch = u.UnableToDispatch, + DispatchedOn = ToUtc(u.DispatchedOn, department), EnrouteOn = ToUtc(u.EnrouteOn, department), OnSceneOn = ToUtc(u.OnSceneOn, department), + StagingOn = ToUtc(u.StagingOn, department), CanceledEnrouteOn = ToUtc(u.CanceledEnrouteOn, department), ClearedOn = ToUtc(u.ClearedOn, department), + ResponseMode = u.ResponseMode + }).ToList(), + Aids = (model.Aids ?? new List()).Select(a => new IncidentAidInput { Direction = a.Direction, AidType = a.AidType, CounterpartNerisId = a.CounterpartNerisId, CounterpartName = a.CounterpartName, IsNonFireDepartment = a.IsNonFireDepartment, NonFdType = a.NonFdType }).ToList(), + Tactics = (model.Tactics ?? new List()).Where(t => !string.IsNullOrWhiteSpace(t.TacticCode)).Select(t => new IncidentTacticInput { TacticCode = t.TacticCode, ActorUnitId = t.ActorUnitId, OccurredOn = ToUtc(t.OccurredOn, department) }).ToList(), + Narrative = model.Narrative, + ImpedimentNarrative = model.ImpedimentNarrative, + OutcomeNarrative = model.OutcomeNarrative, + OriginClient = RmsOriginClient.Web + }; + } + + private List Codes(string setKey) + { + var set = _neris.GetValueSet(setKey); + return (set?.Codes ?? new List()).Select(c => new SelectListItem { Value = c, Text = c.Replace("||", " / ").Replace('_', ' ') }).ToList(); + } + + private static DateTime? ToUtc(DateTime? local, Department department) + { + if (!local.HasValue || local.Value == DateTime.MinValue) + return null; + if (department == null || string.IsNullOrWhiteSpace(department.TimeZone)) + return DateTime.SpecifyKind(local.Value, DateTimeKind.Utc); + + return DateTimeHelpers.ConvertToUtc(local.Value, department.TimeZone, true); + } + + private async Task> PersonnelNamesAsync() + { + var names = await _departmentsService.GetAllPersonnelNamesForDepartmentAsync(DepartmentId); + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var name in names ?? new List()) + { + if (!string.IsNullOrWhiteSpace(name.UserId)) + map[name.UserId] = name.Name; + } + return map; + } + + #endregion + } +} diff --git a/Web/Resgrid.Web/Areas/User/Controllers/RecordsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/RecordsController.cs index f2a50bd1..3eaf4c90 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/RecordsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/RecordsController.cs @@ -199,7 +199,7 @@ public async Task Activate(RecordsActivateView model, Cancellatio return View(model); } - var result = await _cutoverService.ActivateAsync(DepartmentId, UserId, model.Reason, model.ViewGroupRecordsLockToGroup, cancellationToken); + var result = await _cutoverService.ActivateAsync(DepartmentId, UserId, model.Reason, model.ViewGroupRecordsLockToGroup, IpAddressHelper.GetRequestIP(Request, true), cancellationToken); if (!result.Success) { model.ErrorMessage = result.Error; diff --git a/Web/Resgrid.Web/Areas/User/Models/Records/IncidentReportsViewModels.cs b/Web/Resgrid.Web/Areas/User/Models/Records/IncidentReportsViewModels.cs new file mode 100644 index 00000000..7eb239d9 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Models/Records/IncidentReportsViewModels.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Mvc.Rendering; +using Resgrid.Model; + +namespace Resgrid.Web.Areas.User.Models.Records +{ + /// Incident report queue (RMS-2): one authoritative NERIS report per Call. + public class IncidentReportsIndexView : RecordsBaseView + { + public RecordsModuleState ModuleState { get; set; } + public Department Department { get; set; } + public bool IsDepartmentAdmin { get; set; } + public bool SubmissionEnabled { get; set; } + public bool ProfileConfigured { get; set; } + public bool SystemEnabled { get; set; } + public List Reports { get; set; } = new List(); + public int Total { get; set; } + public int Page { get; set; } = 1; + public int PageSize { get; set; } = 50; + public int? Year { get; set; } + public List Years { get; set; } = new List(); + public string StateFilter { get; set; } + public List States { get; set; } = new List(); + public List ActiveCalls { get; set; } = new List(); + public Dictionary PersonnelNames { get; set; } = new Dictionary(); + public int TotalPages => PageSize <= 0 ? 1 : Math.Max(1, (int)Math.Ceiling(Total / (double)PageSize)); + } + + /// Incident report detail: header, NERIS status, validation issues, sections, provenance and history. + public class IncidentReportDetailView : RecordsBaseView + { + public IncidentReportAggregate Aggregate { get; set; } + public Department Department { get; set; } + public RmsNerisProfile Profile { get; set; } + public bool SubmissionEnabled { get; set; } + public Dictionary PersonnelNames { get; set; } = new Dictionary(); + public Dictionary GroupNames { get; set; } = new Dictionary(); + public bool CanEdit { get; set; } + public bool CanReview { get; set; } + public bool CanFinalize { get; set; } + public bool CanSubmit { get; set; } + public bool CanAmend { get; set; } + public bool CanVoid { get; set; } + public bool CanExport { get; set; } + public bool IsDepartmentAdmin { get; set; } + public RmsRecordState State => (RmsRecordState)Aggregate.Report.State; + public bool RequiresReview => (RmsLifecyclePreset)Aggregate.Report.LifecyclePreset != RmsLifecyclePreset.QuickEntry; + public bool IsEditable => RmsLifecycle.IsEditable(State) || State == RmsRecordState.Rejected || Aggregate.Report.AmendsRevisionId != null; + public bool CanQueueSubmission => SubmissionEnabled && Aggregate.Report.AmendsRevisionId == null && !string.IsNullOrWhiteSpace(Aggregate.Report.CurrentRevisionId) + && (State == RmsRecordState.Finalized || State == RmsRecordState.Amended || State == RmsRecordState.Corrected || (State == RmsRecordState.Rejected && Aggregate.Submissions.Any(s => s.State == (int)RmsSubmissionState.Failed))); + } + + public class IncidentTypeRow + { + public string TypeCode { get; set; } + public bool IsPrimary { get; set; } + } + + public class IncidentUnitRow + { + public int UnitId { get; set; } + public bool Selected { get; set; } + public string UnitNerisId { get; set; } + public int? Staffing { get; set; } + public bool UnableToDispatch { get; set; } + public DateTime? DispatchedOn { get; set; } + public DateTime? EnrouteOn { get; set; } + public DateTime? OnSceneOn { get; set; } + public DateTime? StagingOn { get; set; } + public DateTime? CanceledEnrouteOn { get; set; } + public DateTime? ClearedOn { get; set; } + public string ResponseMode { get; set; } + } + + public class IncidentAidRow + { + public string Direction { get; set; } + public string AidType { get; set; } + public string CounterpartNerisId { get; set; } + public string CounterpartName { get; set; } + public bool IsNonFireDepartment { get; set; } + public string NonFdType { get; set; } + } + + public class IncidentTacticRow + { + public string TacticCode { get; set; } + public int? ActorUnitId { get; set; } + public DateTime? OccurredOn { get; set; } + } + + /// Draft editor for a NERIS incident report. Dates are department-local on the form and UTC in the service. + public class IncidentReportEditView : RecordsBaseView + { + public const int TypeRows = 3; + public const int AidRows = 3; + public const int TacticRows = 5; + + public string ReportId { get; set; } + public long RowVersion { get; set; } + public string DraftReference { get; set; } + public string RecordNumber { get; set; } + public int CallId { get; set; } + public string CallLabel { get; set; } + public bool IsAmendment { get; set; } + public bool IsRejected { get; set; } + public string RejectionSummary { get; set; } + public RmsRecordState State { get; set; } + + public string IncidentNumber { get; set; } + public DateTime? CallCreatedOn { get; set; } + public DateTime? CallAnsweredOn { get; set; } + public DateTime? CallArrivalOn { get; set; } + public DateTime? IncidentClearedOn { get; set; } + public string DispatchCenterId { get; set; } + public string DeterminantCode { get; set; } + public string DispatchIncidentCode { get; set; } + public string Disposition { get; set; } + public bool? PeoplePresent { get; set; } + public int? DisplacementCount { get; set; } + public int? AnimalsRescued { get; set; } + public List SpecialModifiers { get; set; } = new List(); + public int? StationGroupId { get; set; } + + public string AddressText { get; set; } + public string Number { get; set; } + public string NumberPrefix { get; set; } + public string NumberSuffix { get; set; } + public string Street { get; set; } + public string UnitValue { get; set; } + public string Municipality { get; set; } + public string County { get; set; } + public string LocationState { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + public string PlaceType { get; set; } + public string LocationUse { get; set; } + public string CrossStreet1 { get; set; } + public string CrossStreet2 { get; set; } + public decimal? Latitude { get; set; } + public decimal? Longitude { get; set; } + + public List Types { get; set; } = new List(); + public List Units { get; set; } = new List(); + public List Aids { get; set; } = new List(); + public List Tactics { get; set; } = new List(); + + public string Narrative { get; set; } + public string ImpedimentNarrative { get; set; } + public string OutcomeNarrative { get; set; } + + public bool ValidateAfterSave { get; set; } + + public Department Department { get; set; } + public List IncidentTypeCodes { get; set; } = new List(); + public List TacticCodes { get; set; } = new List(); + public List AidTypes { get; set; } = new List(); + public List AidDirections { get; set; } = new List(); + public List NonFdTypes { get; set; } = new List(); + public List SpecialModifierCodes { get; set; } = new List(); + public List PlaceTypes { get; set; } = new List(); + public List LocationUses { get; set; } = new List(); + public List ResponseModes { get; set; } = new List(); + public List Stations { get; set; } = new List(); + public List AvailableUnits { get; set; } = new List(); + /// Provenance rows keyed by fact key, so the form can show where a prefilled value came from. + public Dictionary Facts { get; set; } = new Dictionary(StringComparer.Ordinal); + public List Issues { get; set; } = new List(); + + public string ProvenanceFor(string factKey) + { + if (string.IsNullOrWhiteSpace(factKey) || !Facts.TryGetValue(factKey, out var fact)) + return null; + if (fact.CorrectedOn.HasValue) + return "Corrected"; + return ((RmsSourceKind)fact.SourceKind).ToString(); + } + } + + public class NerisCrosswalkRow + { + public string CallType { get; set; } + public string NerisCode { get; set; } + } + + /// Department NERIS profile, credential (write-only) and the call-type crosswalk (RMS plan section 5.5). + public class NerisSettingsView : RecordsBaseView + { + public RecordsModuleState ModuleState { get; set; } + public Department Department { get; set; } + public bool SystemEnabled { get; set; } + public string ContractVersion { get; set; } + public string NerisEntityId { get; set; } + public string EntityName { get; set; } + public string Environment { get; set; } + public string BaseUrlOverride { get; set; } + public string GrantType { get; set; } + public bool AutoSubmitOnFinalize { get; set; } + public bool IsEnabled { get; set; } + public bool HasCredential { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string ClientId { get; set; } + public string ClientSecret { get; set; } + public DateTime? LastTokenIssuedOn { get; set; } + public DateTime? LastSuccessfulCallOn { get; set; } + public string LastError { get; set; } + public bool SubmissionEnabled { get; set; } + public List Environments { get; set; } = new List(); + public List GrantTypes { get; set; } = new List(); + public List Crosswalk { get; set; } = new List(); + public List IncidentTypeCodes { get; set; } = new List(); + public Dictionary QueueCounts { get; set; } = new Dictionary(); + } +} diff --git a/Web/Resgrid.Web/Areas/User/Views/Dispatch/ViewCall.cshtml b/Web/Resgrid.Web/Areas/User/Views/Dispatch/ViewCall.cshtml index 74be49b3..8c6fb2bc 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Dispatch/ViewCall.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Dispatch/ViewCall.cshtml @@ -51,6 +51,10 @@ @localizer["UpdateCallHeader"] @localizer["CloseCallHeader"] } + @if (ClaimsAuthorizationHelper.CanCreateRecord()) + { +
@Html.AntiForgeryToken()
+ } diff --git a/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Details.cshtml b/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Details.cshtml new file mode 100644 index 00000000..761a8e59 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Details.cshtml @@ -0,0 +1,464 @@ +@using Resgrid.Model +@using Resgrid.Model.Helpers +@using Resgrid.Web.Helpers +@model Resgrid.Web.Areas.User.Models.Records.IncidentReportDetailView +@inject IStringLocalizer localizer +@{ + ViewBag.Title = "Resgrid | " + localizer["ViewIncidentReport"]; + var a = Model.Aggregate; + var r = a.Report; + var state = Model.State; + var reference = r.RecordNumber ?? r.DraftReference; + var needsReason = r.AmendsRevisionId != null || state == RmsRecordState.Rejected; + string Name(string userId) => string.IsNullOrEmpty(userId) ? "-" : (Model.PersonnelNames.TryGetValue(userId, out var n) ? n : userId); + string When(DateTime? value) => value.HasValue ? value.Value.TimeConverterToString(Model.Department) : "-"; + string Source(int kind) => ((RmsSourceKind)kind).ToString(); + var errors = a.Issues.Count(i => i.Severity == (int)RmsValidationSeverity.Error); +} + +
+
+

@localizer["ViewIncidentReport"]

+ +
+
+
+ @if (Model.CanEdit && Model.IsEditable) + { + @commonLocalizer["Edit"] +
@Html.AntiForgeryToken()
+ } + @if (Model.CanSubmit && Model.CanQueueSubmission) + { +
@Html.AntiForgeryToken()
+ } + @if (Model.CanAmend && RmsLifecycle.CanTransition((RmsLifecyclePreset)r.LifecyclePreset, state, RmsRecordState.Amended) && r.AmendsRevisionId == null) + { +
@Html.AntiForgeryToken()
+ } + @if (r.AmendsRevisionId != null && Model.CanAmend) + { +
@Html.AntiForgeryToken()
+ } + @if (Model.CanExport) + { + @localizer["Print"] + } +
+
+
+ +
+
+
+ @if (!string.IsNullOrEmpty(Model.Message)) + { +
@Model.Message
+ } + @if (!string.IsNullOrEmpty(Model.ErrorMessage)) + { +
@Model.ErrorMessage
+ } + @if (r.AmendsRevisionId != null) + { +
@localizer["AmendmentOpen"]
+ } + @if (state == RmsRecordState.Rejected) + { +
@localizer["RejectedNotice"] @r.RejectionSummary
+ } + else if (state == RmsRecordState.Submitted) + { +
@localizer["SubmittedNotice"]
+ } + else if (state == RmsRecordState.Accepted) + { +
@localizer["AcceptedNotice"]
+ } + @if (!Model.SubmissionEnabled && RmsLifecycle.IsEditable(state)) + { +
@localizer["SubmissionDisabledNotice"]
+ } + +
+
@reference @state
+
+
+
+
+
@localizer["RecordNumber"]
@(r.RecordNumber ?? "-")
+
@localizer["DraftReference"]
@r.DraftReference
+
@localizer["IncidentNumber"]
@(r.IncidentNumber ?? "-")
+
@localizer["NerisIncidentId"]
@(r.NerisIncidentId ?? "-")
+
@localizer["Call"]
@r.CallId
+
@localizer["Author"]
@Name(r.AuthorUserId)
+
@localizer["Owner"]
@Name(r.OwnerUserId)
+
@localizer["Station"]
@(r.StationGroupId.HasValue && Model.GroupNames.TryGetValue(r.StationGroupId.Value, out var g) ? g : "-")
+
+
+
+
+
@localizer["CallCreated"]
@When(r.CallCreatedOn)
+
@localizer["CallAnswered"]
@When(r.CallAnsweredOn)
+
@localizer["CallArrival"]
@When(r.CallArrivalOn)
+
@localizer["IncidentCleared"]
@When(r.IncidentClearedOn)
+
@localizer["DispatchIncidentCode"]
@(r.DispatchIncidentCode ?? "-")
+
@localizer["Disposition"]
@(r.Disposition ?? "-")
+
@localizer["Finalized"]
@When(r.FinalizedOn)
+ @if (r.ReviewDueOn.HasValue && state == RmsRecordState.ReadyForReview) + { +
@localizer["ReviewDue"]
@When(r.ReviewDueOn)
+ } +
+
+
+ @if (!string.IsNullOrWhiteSpace(r.SpecialModifiersCsv)) + { +

@localizer["SpecialModifiers"]: @r.SpecialModifiersCsv.Replace(",", ", ")

+ } +
+
+ + @if (a.Issues.Count > 0) + { +
+
@localizer["ValidationIssues"] @a.Issues.Count
+
+ @if (errors > 0 && Model.IsEditable) + { +

@localizer["ValidationBlocked"]

+ } + + + + @foreach (var i in a.Issues.OrderBy(i => i.Severity).ThenBy(i => i.FieldPath)) + { + + + + + + + + } + +
@localizer["Severity"]@localizer["Field"]@localizer["Rule"]@localizer["IssueMessage"]@localizer["Source"]
@(((RmsValidationSeverity)i.Severity).ToString())@i.FieldPath@i.RuleKey@i.Message@(((RmsValidationSource)i.Source).ToString())
+
+
+ } + +
+
@localizer["Location"]
+
+ @if (a.Location == null) + { +

-

+ } + else + { +
+
@localizer["AddressText"]
@(a.Location.AddressText ?? "-")
+
@localizer["Street"]
@string.Join(" ", new[] { a.Location.NumberPrefix, a.Location.Number, a.Location.NumberSuffix, a.Location.Street, a.Location.UnitValue }.Where(s => !string.IsNullOrWhiteSpace(s)))
+
@localizer["Municipality"]
@string.Join(", ", new[] { a.Location.Municipality, a.Location.County, a.Location.State, a.Location.PostalCode, a.Location.Country }.Where(s => !string.IsNullOrWhiteSpace(s)))
+
@localizer["PlaceType"]
@(a.Location.PlaceType ?? "-")
+
@localizer["LocationUse"]
@(a.Location.LocationUse ?? "-")
+
@localizer["CrossStreet"]
@string.Join(" / ", new[] { a.Location.CrossStreet1, a.Location.CrossStreet2 }.Where(s => !string.IsNullOrWhiteSpace(s)))
+
@localizer["Coordinates"]
@(a.Location.Latitude.HasValue ? a.Location.Latitude + ", " + a.Location.Longitude : "-")
+
@localizer["Source"]
@Source(a.Location.SourceKind)
+
+ } +
+
+ +
+
@localizer["IncidentTypes"]
+
+ @if (a.Types.Count == 0) + { +

-

+ } + else + { +
    + @foreach (var t in a.Types.OrderBy(t => t.Ordinal)) + { +
  • @t.TypeCode @(t.IsPrimary ? "(" + localizer["Primary"] + ")" : string.Empty) @(string.IsNullOrWhiteSpace(t.LocalCode) ? string.Empty : " - " + t.LocalCode)
  • + } +
+ } +
+
+ +
+
@localizer["Units"]
+
+ @if (a.Units.Count == 0) + { +

-

+ } + else + { +
+ + + + @foreach (var u in a.Units.OrderBy(u => u.Ordinal)) + { + + } + +
@localizer["Unit"]@localizer["UnitNerisId"]@localizer["Dispatched"]@localizer["Enroute"]@localizer["OnScene"]@localizer["Staged"]@localizer["Cleared"]@localizer["ResponseMode"]@localizer["Source"]
@u.UnitNameSnapshot@(u.UnitNerisId ?? "-")@When(u.DispatchedOn)@When(u.EnrouteOn)@When(u.OnSceneOn)@When(u.StagingOn)@When(u.ClearedOn)@(u.ResponseMode ?? "-")@Source(u.TimesSourceKind)
+
+ } +
+
+ + @if (a.Aids.Count > 0 || a.Tactics.Count > 0) + { +
+
@localizer["Aid"] / @localizer["Tactics"]
+
+ @if (a.Aids.Count > 0) + { + + + + @foreach (var x in a.Aids.OrderBy(x => x.Ordinal)) + { + + } + +
@localizer["AidDirection"]@localizer["AidType"]@localizer["Counterpart"]@localizer["CounterpartNerisId"]
@x.Direction@(x.IsNonFireDepartment ? x.NonFdType : x.AidType)@(x.CounterpartName ?? "-")@(x.CounterpartNerisId ?? "-")
+ } + @if (a.Tactics.Count > 0) + { + + + + @foreach (var t in a.Tactics.OrderBy(t => t.Ordinal)) + { + + } + +
@localizer["Tactic"]@localizer["ActorUnit"]@localizer["OccurredOn"]
@t.TacticCode@(t.ActorUnitId.HasValue ? (a.Units.FirstOrDefault(u => u.UnitId == t.ActorUnitId)?.UnitNameSnapshot ?? t.ActorUnitId.ToString()) : "-")@When(t.OccurredOn)
+ } +
+
+ } + +
+
@localizer["Narrative"]
+
+

@(a.Narrative?.Narrative ?? "-")

+ @if (!string.IsNullOrWhiteSpace(a.Narrative?.ImpedimentNarrative)) + { +

@localizer["ImpedimentNarrative"]

@a.Narrative.ImpedimentNarrative

+ } + @if (!string.IsNullOrWhiteSpace(a.Narrative?.OutcomeNarrative)) + { +

@localizer["OutcomeNarrative"]

@a.Narrative.OutcomeNarrative

+ } +
+
+ + @if (a.Facts.Count > 0) + { + + } +
+
+ +
+
+ @if (Model.CanFinalize && r.AmendsRevisionId == null && RmsLifecycle.CanTransition((RmsLifecyclePreset)r.LifecyclePreset, state, RmsRecordState.Finalized)) + { +
+
@localizer["Finalize"]
+
+
+ @Html.AntiForgeryToken() + +
+ +
+
+
+ } + @if (Model.CanFinalize && r.AmendsRevisionId != null) + { +
+
@localizer["Finalize"]
+
+
+ @Html.AntiForgeryToken() + +
+
+
+ +
+
+
+ } + @if (Model.CanSubmit && state == RmsRecordState.Rejected && r.AmendsRevisionId == null) + { +
+
@localizer["Resubmit"]
+
+
+ @Html.AntiForgeryToken() + +
+
+
+ +
+
+
+ } + @if (Model.RequiresReview && Model.CanEdit && RmsLifecycle.CanTransition((RmsLifecyclePreset)r.LifecyclePreset, state, RmsRecordState.ReadyForReview)) + { +
+
+
+ @Html.AntiForgeryToken() + + +
+
+
+ } + @if (Model.CanReview && state == RmsRecordState.ReadyForReview) + { +
+
@localizer["ReturnForCorrection"]
+
+
+ @Html.AntiForgeryToken() +
+
+ +
+
+
+ } + @if (Model.CanVoid && RmsLifecycle.CanTransition((RmsLifecyclePreset)r.LifecyclePreset, state, RmsRecordState.Voided) && r.AmendsRevisionId == null) + { +
+
@localizer["Void"]
+
+
+ @Html.AntiForgeryToken() +
+
+ +
+
+
+ } + @if (Model.CanVoid && RmsLifecycle.CanTransition((RmsLifecyclePreset)r.LifecyclePreset, state, RmsRecordState.Cancelled)) + { +
+
+
+ @Html.AntiForgeryToken() + +
+
+
+ } + +
+
@localizer["Submissions"]
+
+ @if (a.Submissions.Count == 0) + { +

@localizer["NoSubmissions"]

+ } + else + { + @foreach (var s in a.Submissions.OrderByDescending(s => s.QueuedOn)) + { + var sState = (RmsSubmissionState)s.State; + var css = sState == RmsSubmissionState.Accepted ? "label-primary" : sState == RmsSubmissionState.Rejected || sState == RmsSubmissionState.Failed ? "label-danger" : sState == RmsSubmissionState.Superseded ? "label-default" : "label-info"; +
+
@localizer["SubmissionState"]
@sState @s.Destination @s.DestinationVersion
+
@localizer["Revision"]
@(a.Revisions.FirstOrDefault(x => x.RmsRevisionId == s.RevisionId)?.RevisionNumber.ToString() ?? "-")
+
@localizer["Attempts"]
@s.Attempts / @s.MaxAttempts
+
@localizer["ExternalStatus"]
@(s.ExternalStatus ?? "-")
+
@localizer["QueuedOn"]
@When(s.QueuedOn)
+
@localizer["SentOn"]
@When(s.SentOn)
+
@localizer["CompletedOn"]
@When(s.CompletedOn)
+ @if (!string.IsNullOrWhiteSpace(s.ErrorSummary)) + { +
@localizer["ErrorSummary"]
@s.ErrorSummary
+ } + @if (Model.IsDepartmentAdmin && Model.CanSubmit) + { +
@localizer["Payload"]
JSON @s.PayloadChecksum?.Substring(0, Math.Min(12, s.PayloadChecksum.Length))
+ } +
+
+ } + } +
+
+ +
+
@localizer["History"]
+
+ @if (a.Revisions.Count == 0) + { +

-

+ } + else + { + + + + @foreach (var rev in a.Revisions.OrderByDescending(x => x.RevisionNumber)) + { + + } + +
@localizer["Revision"]@localizer["Transition"]@localizer["Actor"]@localizer["Created"]
@rev.RevisionNumber@(((RmsRevisionTransition)rev.Transition).ToString()) @(string.IsNullOrWhiteSpace(rev.ReasonCode) ? string.Empty : "(" + rev.ReasonCode + ")")@Name(rev.ActorUserId)@When(rev.CreatedOn)
+ } + @if (a.Signatures.Count > 0) + { +

@localizer["Signatures"]

+ + + + @foreach (var s in a.Signatures.OrderByDescending(x => x.SignedOn)) + { + + } + +
@localizer["Revision"]@localizer["Signer"]@localizer["SignedOn"]@localizer["Checksum"]
@(a.Revisions.FirstOrDefault(x => x.RmsRevisionId == s.RevisionId)?.RevisionNumber.ToString() ?? "-")@(s.SignerNameSnapshot ?? Name(s.SignerUserId)) @s.SignerRoleSnapshot@When(s.SignedOn)@s.ArtifactChecksum?.Substring(0, Math.Min(12, s.ArtifactChecksum.Length))
+ } +
+
+
+
+
diff --git a/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Edit.cshtml b/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Edit.cshtml new file mode 100644 index 00000000..03ed7064 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Edit.cshtml @@ -0,0 +1,341 @@ +@using Resgrid.Model +@using Resgrid.Web.Helpers +@model Resgrid.Web.Areas.User.Models.Records.IncidentReportEditView +@inject IStringLocalizer localizer +@{ + ViewBag.Title = "Resgrid | " + localizer["EditIncidentReport"]; + var reference = string.IsNullOrWhiteSpace(Model.RecordNumber) ? Model.DraftReference : Model.RecordNumber; + string Dt(DateTime? v) => v.HasValue ? v.Value.ToString("yyyy-MM-ddTHH:mm") : string.Empty; + Func badge = key => + { + var p = Model.ProvenanceFor(key); + if (p == null) return Html.Raw(string.Empty); + var css = p == "Corrected" ? "label-warning" : p == "Derived" ? "label-default" : "label-info"; + return Html.Raw($"{localizer["Source" + p]}"); + }; +} + +
+
+

@localizer["EditIncidentReport"]

+ +
+
+ +
+ @Html.AntiForgeryToken() + @Html.HiddenFor(m => m.ReportId) + @Html.HiddenFor(m => m.RowVersion) + @Html.HiddenFor(m => m.CallId) + +
+
+
+ @if (!string.IsNullOrEmpty(Model.ErrorMessage)) + { +
@Model.ErrorMessage
+ } + @if (Model.IsAmendment) + { +
@localizer["AmendmentOpen"]
+ } + @if (Model.IsRejected) + { +
@localizer["RejectedNotice"] @Model.RejectionSummary
+ } + @if (Model.Issues.Count > 0) + { +
+ @localizer["ValidationIssues"] +
    + @foreach (var i in Model.Issues.OrderBy(i => i.Severity)) + { +
  • @(((RmsValidationSeverity)i.Severity).ToString()) @i.FieldPath @i.Message
  • + } +
+
+ } + +
+
@reference @Model.CallLabel
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
@localizer["DispatchTimes"]
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+ +
+
+
+ +
+ @foreach (var m in Model.SpecialModifierCodes) + { + + } +
+
+
+
+ +
+
@localizer["IncidentTypes"] @badge(NerisFactKeys.IncidentType)
+
+ @for (var i = 0; i < Resgrid.Web.Areas.User.Models.Records.IncidentReportEditView.TypeRows; i++) + { + var row = i < Model.Types.Count ? Model.Types[i] : new Resgrid.Web.Areas.User.Models.Records.IncidentTypeRow(); +
+ +
+ +
+
+
+ } +
+
+ +
+
@localizer["Location"] @badge(NerisFactKeys.Location)
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+ +
+ +
+
+
+ +
+ +
+ +
,
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+ +
+
@localizer["Units"]
+
+
+ + + + @for (var i = 0; i < Model.AvailableUnits.Count; i++) + { + var unitId = int.Parse(Model.AvailableUnits[i].Value); + var existing = Model.Units.FirstOrDefault(u => u.UnitId == unitId); + + + + + + + + + + + + + + } + +
@localizer["Unit"]@localizer["UnitNerisId"]@localizer["Staffing"]@localizer["Dispatched"]@localizer["Enroute"]@localizer["OnScene"]@localizer["Staged"]@localizer["CanceledEnroute"]@localizer["Cleared"]@localizer["ResponseMode"]
@Model.AvailableUnits[i].Text @badge(NerisFactKeys.UnitTime(unitId, "dispatch")) @badge(NerisFactKeys.UnitTime(unitId, "on_scene")) + +
+
+
+
+ +
+
@localizer["Aid"]
+
+ @for (var i = 0; i < Resgrid.Web.Areas.User.Models.Records.IncidentReportEditView.AidRows; i++) + { + var row = i < Model.Aids.Count ? Model.Aids[i] : new Resgrid.Web.Areas.User.Models.Records.IncidentAidRow(); +
+
+ +
+
+ +
+
+
+
+ + +
+
+ } +
+
+ +
+
@localizer["Tactics"]
+
+ @for (var i = 0; i < Resgrid.Web.Areas.User.Models.Records.IncidentReportEditView.TacticRows; i++) + { + var row = i < Model.Tactics.Count ? Model.Tactics[i] : new Resgrid.Web.Areas.User.Models.Records.IncidentTacticRow(); +
+
+ +
+
+ +
+
+
+ } +
+
+ +
+
@localizer["Narrative"]
+
+
+ +
+
+
+ +
+
+
+ +
+
+
+
+ +
+
+ @commonLocalizer["Cancel"] + + +
+
+
+
+
+
+
+
@localizer["Provenance"]
+
+

@localizer["ProvenanceIntro"]

+

@localizer["SourceDispatch"] @localizer["SourceApp"] @localizer["SourceDerived"] @localizer["SourceCorrected"]

+
+
+
+
+
+
diff --git a/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Index.cshtml new file mode 100644 index 00000000..c29e9ed5 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Index.cshtml @@ -0,0 +1,165 @@ +@using Resgrid.Model +@using Resgrid.Model.Helpers +@using Resgrid.Web.Helpers +@model Resgrid.Web.Areas.User.Models.Records.IncidentReportsIndexView +@inject IStringLocalizer localizer +@{ + ViewBag.Title = "Resgrid | " + localizer["IncidentReportsHeader"]; + string Name(string userId) => string.IsNullOrEmpty(userId) ? "-" : (Model.PersonnelNames.TryGetValue(userId, out var n) ? n : userId); + string When(DateTime? value) => value.HasValue ? value.Value.TimeConverterToString(Model.Department) : "-"; +} + +
+
+

@localizer["IncidentReportsHeader"]

+ +
+
+
+ @localizer["BackToRecords"] + @if (Model.IsDepartmentAdmin) + { + @localizer["NerisSettingsHeader"] + } +
+
+
+ +
+
+
+ @if (!string.IsNullOrEmpty(Model.Message)) + { +
@Model.Message
+ } + @if (!string.IsNullOrEmpty(Model.ErrorMessage)) + { +
@Model.ErrorMessage
+ } + @if (!Model.ModuleState.RecordsUsable) + { +
@localizer["NotActivatedNotice"]
+ } + else if (!Model.SystemEnabled) + { +
@localizer["SystemDisabledNotice"]
+ } + else if (!Model.SubmissionEnabled) + { +
@localizer["SubmissionDisabledNotice"]
+ } + +

@localizer["IncidentReportsIntro"]

+ + @if (Model.ModuleState.RecordsUsable && ClaimsAuthorizationHelper.CanCreateRecord()) + { +
+
@localizer["StartFromCall"]
+
+ @if (Model.ActiveCalls.Count == 0) + { +

@localizer["NoActiveCalls"]

+ } + else + { +
+ @Html.AntiForgeryToken() +
+ + +
+ +
+ } +
+
+ } + +
+
@localizer["IncidentReports"]
+
+
+
+ +
+
+ +
+ +
+ + @if (Model.Reports.Count == 0) + { +

@localizer["NoRecords"]

+ } + else + { +
+ + + + + + + + + + + + + + + + @foreach (var r in Model.Reports) + { + + + + + + + + + + + + } + +
@localizer["RecordNumber"]@localizer["IncidentNumber"]@localizer["Summary"]@localizer["State"]@localizer["SubmissionState"]@localizer["NerisIncidentId"]@localizer["Owner"]@localizer["Created"]@localizer["Actions"]
@(r.RecordNumber ?? r.DraftReference)@r.IncidentNumber@r.DisplaySummary@(((RmsRecordState)r.State).ToString())@(r.LastSubmissionState.HasValue ? ((RmsSubmissionState)r.LastSubmissionState.Value).ToString() : "-")@(r.NerisIncidentId ?? "-")@Name(r.OwnerUserId)@When(r.CreatedOn)@localizer["ViewRevision"]
+
+ @if (Model.TotalPages > 1) + { +
    + @for (var p = 1; p <= Model.TotalPages; p++) + { +
  • @p
  • + } +
+ } + } +
+
+
+
+
diff --git a/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Settings.cshtml b/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Settings.cshtml new file mode 100644 index 00000000..45f33a03 --- /dev/null +++ b/Web/Resgrid.Web/Areas/User/Views/IncidentReports/Settings.cshtml @@ -0,0 +1,160 @@ +@using Resgrid.Model +@using Resgrid.Model.Helpers +@model Resgrid.Web.Areas.User.Models.Records.NerisSettingsView +@inject IStringLocalizer localizer +@{ + ViewBag.Title = "Resgrid | " + localizer["NerisSettingsHeader"]; + string When(DateTime? value) => value.HasValue ? value.Value.TimeConverterToString(Model.Department) : "-"; +} + +
+
+

@localizer["NerisSettingsHeader"]

+ +
+
+ +
+ @Html.AntiForgeryToken() +
+
+
+ @if (!string.IsNullOrEmpty(Model.Message)) + { +
@Model.Message
+ } + @if (!string.IsNullOrEmpty(Model.ErrorMessage)) + { +
@Model.ErrorMessage
+ } + @if (!Model.SystemEnabled) + { +
@localizer["SystemDisabledNotice"]
+ } +

@localizer["NerisSettingsIntro"]

+ +
+
NERIS @localizer["ContractVersion"] @Model.ContractVersion
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
@localizer["Password"] / @localizer["ClientSecret"]
+
+

@(Model.HasCredential ? localizer["CredentialStored"] : localizer["CredentialMissing"])

+
+ +
+ +
+
+
+ +
+ +
+
+
+
+ +
+
@localizer["Crosswalk"]
+
+

@localizer["CrosswalkIntro"]

+ @if (Model.Crosswalk.Count == 0) + { +

-

+ } + else + { + + + + @for (var i = 0; i < Model.Crosswalk.Count; i++) + { + + + + + } + +
@localizer["CallType"]@localizer["TypeCode"]
@Model.Crosswalk[i].CallType + +
+ } +
+
+ +
+
+ @commonLocalizer["Cancel"] + +
+
+
+
+
+
+
+
@commonLocalizer["Status"]
+
+
+
@localizer["SubmissionState"]
@(Model.SubmissionEnabled ? commonLocalizer["Enabled"] : commonLocalizer["Disabled"])
+
@localizer["LastTokenIssued"]
@When(Model.LastTokenIssuedOn)
+
@localizer["LastSuccessfulCall"]
@When(Model.LastSuccessfulCallOn)
+ @if (!string.IsNullOrWhiteSpace(Model.LastError)) + { +
@localizer["LastError"]
@Model.LastError
+ } +
+
+
+
+
@localizer["QueueCounts"]
+
+
+ @foreach (var kv in Model.QueueCounts) + { +
@kv.Key
@kv.Value
+ } +
+
+
+
+
+
+
diff --git a/Web/Resgrid.Web/Areas/User/Views/Records/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Records/Index.cshtml index ad2f8d92..1c95cc2e 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Records/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Records/Index.cshtml @@ -36,6 +36,10 @@ { @localizer["NewRecord"] } + @if (Model.ModuleState.RecordsUsable) + { + @localizer["IncidentReports"] + } @if (Model.ModuleState.RecordsUsable && ClaimsAuthorizationHelper.CanExportRecords()) { @localizer["ExportCsv"] @@ -152,7 +156,7 @@ @((r.OccurredOn ?? r.RecordCreatedOn).TimeConverterToString(Model.Department)) @(Model.PersonnelNames.TryGetValue(r.AuthorUserId ?? string.Empty, out var name) ? name : r.AuthorUserId) - @localizer["ViewRevision"] + @localizer["ViewRevision"] } diff --git a/Web/Resgrid.Web/Resgrid.Web.csproj b/Web/Resgrid.Web/Resgrid.Web.csproj index 9ae0b201..582581b9 100644 --- a/Web/Resgrid.Web/Resgrid.Web.csproj +++ b/Web/Resgrid.Web/Resgrid.Web.csproj @@ -156,6 +156,7 @@ + diff --git a/Web/Resgrid.Web/Startup.cs b/Web/Resgrid.Web/Startup.cs index 84b9a5ad..c3e621ad 100644 --- a/Web/Resgrid.Web/Startup.cs +++ b/Web/Resgrid.Web/Startup.cs @@ -544,6 +544,7 @@ public void ConfigureContainer(ContainerBuilder builder) builder.RegisterModule(new ServicesModule()); builder.RegisterModule(new Resgrid.Search.SearchModule()); builder.RegisterModule(new Resgrid.Providers.Scanning.ScanningProviderModule()); + builder.RegisterModule(new Resgrid.Providers.Neris.NerisProviderModule()); builder.RegisterModule(new ProviderModule()); builder.RegisterModule(new EmailProviderModule()); builder.RegisterModule(new BusModule()); diff --git a/Web/Resgrid.Web/WebBootstrapper.cs b/Web/Resgrid.Web/WebBootstrapper.cs index 3f1f568a..1263919a 100644 --- a/Web/Resgrid.Web/WebBootstrapper.cs +++ b/Web/Resgrid.Web/WebBootstrapper.cs @@ -37,6 +37,7 @@ public static void Initialize(IServiceCollection services) builder.RegisterModule(new ServicesModule()); builder.RegisterModule(new Resgrid.Search.SearchModule()); builder.RegisterModule(new Resgrid.Providers.Scanning.ScanningProviderModule()); + builder.RegisterModule(new Resgrid.Providers.Neris.NerisProviderModule()); builder.RegisterModule(new ProviderModule()); builder.RegisterModule(new EmailProviderModule()); builder.RegisterModule(new BusModule()); diff --git a/Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js b/Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js index 2771c420..57aef69e 100644 --- a/Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js +++ b/Web/Resgrid.Web/wwwroot/js/app/internal/security/resgrid.security.permissions.js @@ -32,6 +32,10 @@ var resgrid; url: url, type: 'POST', headers: { 'RequestVerificationToken': antiForgeryToken() } + }).fail(function () { + // A rejected write leaves select2 showing roles the server never stored, so the + // failure has to be surfaced rather than swallowed as an unhandled rejection. + resgrid.common.notifications.showError('Permission Not Saved', 'The permission change could not be saved. Reload the page and try again.'); }); }); $.ajax({ diff --git a/Workers/Resgrid.Workers.Console/Commands/RmsSubmissionCommand.cs b/Workers/Resgrid.Workers.Console/Commands/RmsSubmissionCommand.cs new file mode 100644 index 00000000..5947b72b --- /dev/null +++ b/Workers/Resgrid.Workers.Console/Commands/RmsSubmissionCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using Quidjibo.Commands; + +namespace Resgrid.Workers.Console.Commands +{ + /// Worker ID 41 (registry section 3.3, RMS-2): NERIS/reporting-destination submission sweep (RMS plan sections 5.3/5.5). + public sealed class RmsSubmissionCommand : IQuidjiboCommand + { + public RmsSubmissionCommand(int id) + { + Id = id; + } + + public int Id { get; } + public Guid? CorrelationId { get; set; } + public Dictionary Metadata { get; set; } + } +} diff --git a/Workers/Resgrid.Workers.Console/Program.cs b/Workers/Resgrid.Workers.Console/Program.cs index c6e39537..9e4284dc 100644 --- a/Workers/Resgrid.Workers.Console/Program.cs +++ b/Workers/Resgrid.Workers.Console/Program.cs @@ -499,6 +499,14 @@ await Client.ScheduleAsync("Records Search Index", Cron.MinuteIntervals(1), stoppingToken); + // Worker ID 41 (Identifier Allocation Registry section 3.3, RMS-2): NERIS submission sweep. Claims due + // submissions with a lease, talks to the destination outside any transaction, no-op while NerisConfig.Enabled is off. + _logger.Log(LogLevel.Information, "Scheduling Records Submission"); + await Client.ScheduleAsync("Records Submission", + new Commands.RmsSubmissionCommand(41), + Cron.MinuteIntervals(1), + stoppingToken); + if (SystemBehaviorConfig.Utf8CleanupEnabled) { var utf8CleanupHour = SystemBehaviorConfig.Utf8CleanupHourUtc >= 0 && SystemBehaviorConfig.Utf8CleanupHourUtc <= 23 diff --git a/Workers/Resgrid.Workers.Console/Tasks/RmsSubmissionTask.cs b/Workers/Resgrid.Workers.Console/Tasks/RmsSubmissionTask.cs new file mode 100644 index 00000000..de5c5d28 --- /dev/null +++ b/Workers/Resgrid.Workers.Console/Tasks/RmsSubmissionTask.cs @@ -0,0 +1,45 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Quidjibo.Handlers; +using Quidjibo.Misc; +using Resgrid.Workers.Console.Commands; +using Resgrid.Workers.Framework.Logic; + +namespace Resgrid.Workers.Console.Tasks +{ + public sealed class RmsSubmissionTask : IQuidjiboHandler + { + private readonly ILogger _logger; + + public RmsSubmissionTask(ILogger logger) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public string Name => "Records Submission"; + public int Priority => 1; + + public async Task ProcessAsync(RmsSubmissionCommand command, IQuidjiboProgress progress, CancellationToken cancellationToken) + { + progress?.Report(1, $"Starting the {Name} Task"); + + try + { + var logic = new RmsSubmissionLogic(); + var result = await logic.Process(cancellationToken); + + if (!result.Item1) + throw new InvalidOperationException(result.Item2); + + _logger.LogInformation("RmsSubmission::{Summary}", result.Item2); + progress?.Report(100, $"Finishing the {Name} Task"); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + } + } +} diff --git a/Workers/Resgrid.Workers.Framework/Bootstrapper.cs b/Workers/Resgrid.Workers.Framework/Bootstrapper.cs index d0140375..96615633 100644 --- a/Workers/Resgrid.Workers.Framework/Bootstrapper.cs +++ b/Workers/Resgrid.Workers.Framework/Bootstrapper.cs @@ -37,6 +37,7 @@ public static void Initialize() builder.RegisterModule(new ServicesModule()); builder.RegisterModule(new Resgrid.Search.SearchModule()); builder.RegisterModule(new Resgrid.Providers.Scanning.ScanningProviderModule()); + builder.RegisterModule(new Resgrid.Providers.Neris.NerisProviderModule()); builder.RegisterModule(new ProviderModule()); builder.RegisterModule(new EmailProviderModule()); builder.RegisterModule(new WorkerFrameworkModule()); diff --git a/Workers/Resgrid.Workers.Framework/Logic/NotificationBroadcastLogic.cs b/Workers/Resgrid.Workers.Framework/Logic/NotificationBroadcastLogic.cs index 04bc80fc..0bc27e04 100644 --- a/Workers/Resgrid.Workers.Framework/Logic/NotificationBroadcastLogic.cs +++ b/Workers/Resgrid.Workers.Framework/Logic/NotificationBroadcastLogic.cs @@ -33,6 +33,14 @@ public static async Task ProcessNotificationItem(NotificationItem ni, stri return true; } + if (ni?.Type == (int)EventTypes.RecordSubmissionRejected) + { + // Records notification 33 (RMS-2): the destination rejected the author's incident report; author-targeted like 31. + var recordsNotificationService = Bootstrapper.GetKernel().Resolve(); + await recordsNotificationService.NotifySubmissionRejectedAsync(ni.DepartmentId, ni.Value, cancellationToken); + return true; + } + if (ni != null) { var _notificationService = Bootstrapper.GetKernel().Resolve(); diff --git a/Workers/Resgrid.Workers.Framework/Logic/RmsSubmissionLogic.cs b/Workers/Resgrid.Workers.Framework/Logic/RmsSubmissionLogic.cs new file mode 100644 index 00000000..2750c358 --- /dev/null +++ b/Workers/Resgrid.Workers.Framework/Logic/RmsSubmissionLogic.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Autofac; +using Resgrid.Config; +using Resgrid.Framework; +using Resgrid.Model.Services; + +namespace Resgrid.Workers.Framework.Logic +{ + /// + /// Worker command 41: the reporting-destination submission sweep (RMS plan sections 5.3/5.5). Drives + /// IRecordsSubmissionService, which talks to NERIS outside any database transaction; a no-op while + /// NerisConfig.Enabled is off. + /// + public sealed class RmsSubmissionLogic + { + public async Task> Process(CancellationToken cancellationToken) + { + try + { + if (!NerisConfig.Enabled) + return new Tuple(true, "NERIS submission disabled; nothing to do."); + + var service = Bootstrapper.GetKernel().Resolve(); + var result = await service.SweepAsync(cancellationToken); + + if (result.Errors > 0) + Logging.LogError($"Records submission sweep finished with {result.Errors} error(s): {result.Message}"); + + return new Tuple(true, result.Message); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } + catch (Exception ex) + { + Logging.LogException(ex); + return new Tuple(false, ex.ToString()); + } + } + } +} diff --git a/Workers/Resgrid.Workers.Framework/Resgrid.Workers.Framework.csproj b/Workers/Resgrid.Workers.Framework/Resgrid.Workers.Framework.csproj index 1d64fc34..86e51b8b 100644 --- a/Workers/Resgrid.Workers.Framework/Resgrid.Workers.Framework.csproj +++ b/Workers/Resgrid.Workers.Framework/Resgrid.Workers.Framework.csproj @@ -16,6 +16,7 @@ +