From 418e6eb3e803c6501f235f0fbc03ad6b231859f5 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 11:51:10 -0400 Subject: [PATCH 1/7] feat(backend): regra de concluir o atendimento do alerta (#76) --- .../application/alerts/red_alert_service.dart | 87 ++++++++++++ .../database/orm_alert_store.dart | 9 ++ .../unit/red_alert_discard_paths_test.dart | 15 ++ .../test/unit/red_alert_lifecycle_test.dart | 10 ++ .../test/unit/red_alert_service_test.dart | 131 ++++++++++++++++++ 5 files changed, 252 insertions(+) diff --git a/backend/sinalacs_server/lib/src/application/alerts/red_alert_service.dart b/backend/sinalacs_server/lib/src/application/alerts/red_alert_service.dart index 80fc87b4..4c1ad6b7 100644 --- a/backend/sinalacs_server/lib/src/application/alerts/red_alert_service.dart +++ b/backend/sinalacs_server/lib/src/application/alerts/red_alert_service.dart @@ -30,6 +30,17 @@ abstract interface class AlertStore { required AuditEvent audit, }); + /// Conclui o atendimento (#76): leva o alerta de `acknowledged` a + /// `resolved` no território do ACS. O desfecho é decidido sob trava da + /// linha, e `auditFor(desfecho)` é gravado na MESMA transação (fail-closed) + /// — exceto [AlertResolveOutcome.notFound], que não grava nada, como o ACK. + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }); + /// Recupera o alerta já criado para esta chave de idempotência, se houver. /// /// A deduplicação vive no armazenamento, e não em memória, para sobreviver a @@ -83,6 +94,32 @@ class RedAlertRecord { final String idempotencyKey; } +/// Desfecho de [AlertStore.resolve] (#76). +enum AlertResolveOutcome { + /// Inexistente ou de outra microárea: a mesma resposta, nada gravado. + notFound, + + /// Ainda `pending`: o recebimento precisa ser confirmado antes (INV-03 — + /// concluir sem ACK apagaria do painel um vermelho que ninguém confirmou). + notAcknowledged, + + /// `acknowledged` → `resolved` agora. + resolved, + + /// Já estava `resolved`: nada muda e o `resolvedAt` original volta. + alreadyResolved, +} + +class AlertResolveAttempt { + const AlertResolveAttempt(this.outcome, {this.resolvedAt}); + + final AlertResolveOutcome outcome; + + /// Quando o atendimento foi concluído; nulo em [AlertResolveOutcome.notFound] + /// e [AlertResolveOutcome.notAcknowledged]. + final DateTime? resolvedAt; +} + /// Status do alerta mais recente de um paciente, para RF05. /// /// Espelha os campos que `Alert` guarda além de [AlertDelivery] — `status` e @@ -94,6 +131,7 @@ class AlertStatusSnapshot { required this.status, required this.triggeredAt, this.acknowledgedAt, + this.resolvedAt, }); final String alertId; @@ -101,6 +139,9 @@ class AlertStatusSnapshot { final AlertStatus status; final DateTime triggeredAt; final DateTime? acknowledgedAt; + + /// Conclusão do atendimento pelo ACS (#76); nulo até lá. + final DateTime? resolvedAt; } /// Formato produzido por `locationCellFrom()` (apps/patient/lib/core/privacy/location_cell.dart): @@ -222,6 +263,52 @@ class RedAlertService { ); } + /// Texto da recusa de concluir um alerta que ninguém confirmou (#76). + static const notAcknowledgedMessage = + 'Confirme o recebimento antes de concluir o atendimento.'; + + /// Conclui o atendimento (#76, RF05): é a transição que faz o paciente ver + /// "Atendido". Só o ACS do território do alerta, e só depois do ACK. + /// Cada ato que encontra o alerta é auditado, inclusive a repetição e a + /// recusa por falta de ACK; a recusa vira [ArgumentError] DEPOIS de a + /// trilha gravar. + Future resolve({ + required AuthenticatedUser user, + required String alertId, + }) async { + Authorization.require( + user, + roles: {UserRole.acs}, + onDenied: () => + StateError('Somente ACS territorializados podem concluir atendimentos.'), + ); + if (alertId.trim().isEmpty) { + throw ArgumentError('O identificador do alerta é obrigatório.'); + } + final attempt = await _store.resolve( + alertId: alertId, + microAreaId: user.microAreaId!, + now: _clock().toUtc(), + auditFor: (outcome) => AuditEvent( + userId: user.id, + actionType: 'write', + resourceType: 'alert', + resourceId: alertId, + result: switch (outcome) { + AlertResolveOutcome.resolved => 'resolved', + AlertResolveOutcome.alreadyResolved => 'already_resolved', + AlertResolveOutcome.notAcknowledged => 'denied_not_acknowledged', + // O store não grava este; o texto existe só para o switch ser total. + AlertResolveOutcome.notFound => 'denied_not_found', + }, + ), + ); + if (attempt.outcome == AlertResolveOutcome.notAcknowledged) { + throw ArgumentError(notAcknowledgedMessage); + } + return attempt; + } + /// Status do alerta mais recente do próprio paciente autenticado (RF05, /// decisão §5). O paciente nunca informa `patientId` — vem sempre do token /// (INV-05), mesma disciplina de `TriageSessionService`/`VisitSyncService`. diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart index 4c326ae8..6a4f551a 100644 --- a/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart @@ -171,6 +171,15 @@ class OrmAlertStore implements AlertStore { return existing != null ? run(existing) : session.db.transaction(run); } + @override + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }) => + throw UnimplementedError('Task 2 (#76)'); + @override Future latestForPatient(String patientId) async { final row = await Alert.db.findFirstRow( diff --git a/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart b/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart index 5b403877..e716ca88 100644 --- a/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart +++ b/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart @@ -118,6 +118,21 @@ class _MemoryStore implements AlertStore { @override Future latestForPatient(String patientId) async => null; + + @override + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }) async { + if (status[alertId] != 'acknowledged') { + return const AlertResolveAttempt(AlertResolveOutcome.notAcknowledged); + } + audits.add(auditFor(AlertResolveOutcome.resolved)); + status[alertId] = 'resolved'; + return AlertResolveAttempt(AlertResolveOutcome.resolved, resolvedAt: now); + } } class _MemoryOutbox implements AlertOutbox { diff --git a/backend/sinalacs_server/test/unit/red_alert_lifecycle_test.dart b/backend/sinalacs_server/test/unit/red_alert_lifecycle_test.dart index 27ad9795..06f339ed 100644 --- a/backend/sinalacs_server/test/unit/red_alert_lifecycle_test.dart +++ b/backend/sinalacs_server/test/unit/red_alert_lifecycle_test.dart @@ -43,6 +43,16 @@ class RecordingStore implements AlertStore { Future latestForPatient(String patientId) async { throw UnimplementedError('não usado nos testes deste arquivo'); } + + @override + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }) async { + throw UnimplementedError('não usado nos testes deste arquivo'); + } } diff --git a/backend/sinalacs_server/test/unit/red_alert_service_test.dart b/backend/sinalacs_server/test/unit/red_alert_service_test.dart index 9af59cb2..b4f43ec8 100644 --- a/backend/sinalacs_server/test/unit/red_alert_service_test.dart +++ b/backend/sinalacs_server/test/unit/red_alert_service_test.dart @@ -43,6 +43,29 @@ class FakeAlertStore implements AlertStore { @override Future latestForPatient(String patientId) async => statusByPatient[patientId]; + + /// Desfecho que o "banco" decide para o próximo `resolve` (#76). + AlertResolveOutcome resolveOutcome = AlertResolveOutcome.resolved; + final List<({String alertId, String microAreaId, DateTime now})> resolveCalls = []; + + @override + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }) async { + resolveCalls.add((alertId: alertId, microAreaId: microAreaId, now: now)); + // Mesma regra do store real: inexistente/fora do território não grava. + if (resolveOutcome != AlertResolveOutcome.notFound) { + audits.add(auditFor(resolveOutcome)); + } + final carimbo = switch (resolveOutcome) { + AlertResolveOutcome.resolved || AlertResolveOutcome.alreadyResolved => now, + _ => null, + }; + return AlertResolveAttempt(resolveOutcome, resolvedAt: carimbo); + } } @@ -204,6 +227,114 @@ void main() { ); }); + group('resolve (#76)', () { + const acs = AuthenticatedUser( + id: 'acs-001', + role: UserRole.acs, + microAreaId: 'area-12', + deviceId: 'device-001', + ); + final agora = DateTime.utc(2026, 10, 16, 14); + + RedAlertService servico(FakeAlertStore store) => RedAlertService( + store: store, + outbox: FakeAlertOutbox(), + clock: () => agora, + ); + + test('conclui no território do token e audita resolved', () async { + final store = FakeAlertStore(); + + final r = await servico(store).resolve(user: acs, alertId: 'alerta-1'); + + expect(r.outcome, AlertResolveOutcome.resolved); + expect(r.resolvedAt, agora); + expect(store.resolveCalls.single.microAreaId, 'area-12'); + expect(store.resolveCalls.single.now, agora); + final audit = store.audits.single; + expect(audit.userId, 'acs-001'); + expect(audit.actionType, 'write'); + expect(audit.resourceType, 'alert'); + expect(audit.resourceId, 'alerta-1'); + expect(audit.result, 'resolved'); + }); + + test('repetir a conclusão é idempotente e audita already_resolved', () async { + final store = FakeAlertStore()..resolveOutcome = AlertResolveOutcome.alreadyResolved; + + final r = await servico(store).resolve(user: acs, alertId: 'alerta-1'); + + expect(r.outcome, AlertResolveOutcome.alreadyResolved); + expect(store.audits.single.result, 'already_resolved'); + }); + + test('alerta ainda pending é recusado com a mensagem e a recusa é auditada', () async { + final store = FakeAlertStore()..resolveOutcome = AlertResolveOutcome.notAcknowledged; + + await expectLater( + servico(store).resolve(user: acs, alertId: 'alerta-1'), + throwsA(isA().having( + (e) => e.message, + 'message', + RedAlertService.notAcknowledgedMessage, + )), + ); + expect(store.audits.single.result, 'denied_not_acknowledged'); + }); + + test('alerta inexistente ou de outra microárea devolve notFound sem auditar', () async { + final store = FakeAlertStore()..resolveOutcome = AlertResolveOutcome.notFound; + + final r = await servico(store).resolve(user: acs, alertId: 'alerta-1'); + + expect(r.outcome, AlertResolveOutcome.notFound); + expect(r.resolvedAt, isNull); + expect(store.audits, isEmpty); + }); + + test('paciente não conclui atendimento', () async { + final store = FakeAlertStore(); + const paciente = AuthenticatedUser( + id: 'paciente-1', + role: UserRole.patient, + microAreaId: 'area-12', + deviceId: 'device-1', + ); + + await expectLater( + servico(store).resolve(user: paciente, alertId: 'alerta-1'), + throwsA(isA()), + ); + expect(store.resolveCalls, isEmpty); + }); + + test('ACS sem microárea não conclui atendimento', () async { + final store = FakeAlertStore(); + const semArea = AuthenticatedUser( + id: 'acs-002', + role: UserRole.acs, + microAreaId: null, + deviceId: 'device-2', + ); + + await expectLater( + servico(store).resolve(user: semArea, alertId: 'alerta-1'), + throwsA(isA()), + ); + expect(store.resolveCalls, isEmpty); + }); + + test('identificador vazio é recusado antes do store', () async { + final store = FakeAlertStore(); + + await expectLater( + servico(store).resolve(user: acs, alertId: ' '), + throwsA(isA()), + ); + expect(store.resolveCalls, isEmpty); + }); + }); + group('statusFor (RF05)', () { test('devolve o status guardado para o paciente do token, não por parâmetro', () async { final store = FakeAlertStore() From 62e14607f2bdbcb8e7a6cafa629eaaa357e377fa Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 11:54:58 -0400 Subject: [PATCH 2/7] feat(backend): alerts.resolve leva o alerta confirmado a resolved (#76) --- .../lib/src/protocol/alert.dart | 15 + .../protocol/api/alert_resolve_result.dart | 116 + .../src/protocol/api/alert_status_result.dart | 13 + .../lib/src/protocol/client.dart | 167 +- .../lib/src/protocol/protocol.dart | 1145 +++-- .../lib/src/endpoints/alerts_endpoint.dart | 31 + .../lib/src/generated/alert.dart | 31 + .../generated/api/alert_resolve_result.dart | 128 + .../generated/api/alert_status_result.dart | 14 + .../lib/src/generated/endpoints.dart | 24 + .../lib/src/generated/protocol.dart | 1225 ++--- .../lib/src/generated/protocol.yaml | 1 + .../database/orm_alert_store.dart | 92 +- .../lib/src/models/alert.spy.yaml | 3 + .../models/api/alert_resolve_result.spy.yaml | 11 + .../models/api/alert_status_result.spy.yaml | 1 + .../20261016155336452/definition.json | 4529 +++++++++++++++++ .../20261016155336452/definition.sql | 939 ++++ .../20261016155336452/definition_project.json | 3193 ++++++++++++ .../20261016155336452/migration.json | 32 + .../20261016155336452/migration.sql | 25 + .../migrations/migration_registry.txt | 1 + .../integration/red_alert_cycle_test.dart | 181 + .../test_tools/serverpod_test_tools.dart | 181 +- .../unit/red_alert_discard_paths_test.dart | 3 +- 25 files changed, 10779 insertions(+), 1322 deletions(-) create mode 100644 backend/sinalacs_client/lib/src/protocol/api/alert_resolve_result.dart create mode 100644 backend/sinalacs_server/lib/src/generated/api/alert_resolve_result.dart create mode 100644 backend/sinalacs_server/lib/src/models/api/alert_resolve_result.spy.yaml create mode 100644 backend/sinalacs_server/migrations/20261016155336452/definition.json create mode 100644 backend/sinalacs_server/migrations/20261016155336452/definition.sql create mode 100644 backend/sinalacs_server/migrations/20261016155336452/definition_project.json create mode 100644 backend/sinalacs_server/migrations/20261016155336452/migration.json create mode 100644 backend/sinalacs_server/migrations/20261016155336452/migration.sql diff --git a/backend/sinalacs_client/lib/src/protocol/alert.dart b/backend/sinalacs_client/lib/src/protocol/alert.dart index 88fa218c..754ecce8 100644 --- a/backend/sinalacs_client/lib/src/protocol/alert.dart +++ b/backend/sinalacs_client/lib/src/protocol/alert.dart @@ -31,6 +31,7 @@ abstract class Alert implements _i1.SerializableModel { this.receivedAt, this.respondedAt, this.acknowledgedAt, + this.resolvedAt, required this.riskLevel, required this.locationHash, this.locationCell, @@ -53,6 +54,7 @@ abstract class Alert implements _i1.SerializableModel { DateTime? receivedAt, DateTime? respondedAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, required _i2.RiskLevel riskLevel, required String locationHash, String? locationCell, @@ -98,6 +100,9 @@ abstract class Alert implements _i1.SerializableModel { : _i1.DateTimeJsonExtension.fromJson( jsonSerialization['acknowledgedAt'], ), + resolvedAt: jsonSerialization['resolvedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson(jsonSerialization['resolvedAt']), riskLevel: _i2.RiskLevel.fromJson( (jsonSerialization['riskLevel'] as String), ), @@ -145,6 +150,10 @@ abstract class Alert implements _i1.SerializableModel { DateTime? acknowledgedAt; + /// Atendimento concluído pelo ACS (#76): carimbado uma única vez, na + /// transição acknowledged → resolved. Nulo enquanto não foi atendido. + DateTime? resolvedAt; + _i2.RiskLevel riskLevel; String locationHash; @@ -182,6 +191,7 @@ abstract class Alert implements _i1.SerializableModel { DateTime? receivedAt, DateTime? respondedAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, _i2.RiskLevel? riskLevel, String? locationHash, String? locationCell, @@ -206,6 +216,7 @@ abstract class Alert implements _i1.SerializableModel { if (receivedAt != null) 'receivedAt': receivedAt?.toJson(), if (respondedAt != null) 'respondedAt': respondedAt?.toJson(), if (acknowledgedAt != null) 'acknowledgedAt': acknowledgedAt?.toJson(), + if (resolvedAt != null) 'resolvedAt': resolvedAt?.toJson(), 'riskLevel': riskLevel.toJson(), 'locationHash': locationHash, if (locationCell != null) 'locationCell': locationCell, @@ -239,6 +250,7 @@ class _AlertImpl extends Alert { DateTime? receivedAt, DateTime? respondedAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, required _i2.RiskLevel riskLevel, required String locationHash, String? locationCell, @@ -259,6 +271,7 @@ class _AlertImpl extends Alert { receivedAt: receivedAt, respondedAt: respondedAt, acknowledgedAt: acknowledgedAt, + resolvedAt: resolvedAt, riskLevel: riskLevel, locationHash: locationHash, locationCell: locationCell, @@ -285,6 +298,7 @@ class _AlertImpl extends Alert { Object? receivedAt = _Undefined, Object? respondedAt = _Undefined, Object? acknowledgedAt = _Undefined, + Object? resolvedAt = _Undefined, _i2.RiskLevel? riskLevel, String? locationHash, Object? locationCell = _Undefined, @@ -310,6 +324,7 @@ class _AlertImpl extends Alert { acknowledgedAt: acknowledgedAt is DateTime? ? acknowledgedAt : this.acknowledgedAt, + resolvedAt: resolvedAt is DateTime? ? resolvedAt : this.resolvedAt, riskLevel: riskLevel ?? this.riskLevel, locationHash: locationHash ?? this.locationHash, locationCell: locationCell is String? ? locationCell : this.locationCell, diff --git a/backend/sinalacs_client/lib/src/protocol/api/alert_resolve_result.dart b/backend/sinalacs_client/lib/src/protocol/api/alert_resolve_result.dart new file mode 100644 index 00000000..0de1e97c --- /dev/null +++ b/backend/sinalacs_client/lib/src/protocol/api/alert_resolve_result.dart @@ -0,0 +1,116 @@ +/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */ +/* To generate run: "serverpod generate" */ + +// ignore_for_file: implementation_imports +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: public_member_api_docs +// ignore_for_file: type_literal_in_constant_pattern +// ignore_for_file: use_super_parameters +// ignore_for_file: invalid_use_of_internal_member + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'package:serverpod_client/serverpod_client.dart' as _i1; +import '../enums/alert_status.dart' as _i2; + +/// Resultado de alerts.resolve (#76). +/// +/// Mesmo desenho de AlertAckResult: resolved: false significa que não havia +/// alerta com esse id na microárea do ACS. Concluir duas vezes devolve +/// resolved: true com o resolvedAt da primeira conclusão. +abstract class AlertResolveResult implements _i1.SerializableModel { + AlertResolveResult._({ + required this.alertId, + required this.resolved, + this.status, + this.resolvedAt, + }); + + factory AlertResolveResult({ + required String alertId, + required bool resolved, + _i2.AlertStatus? status, + DateTime? resolvedAt, + }) = _AlertResolveResultImpl; + + factory AlertResolveResult.fromJson(Map jsonSerialization) { + return AlertResolveResult( + alertId: jsonSerialization['alertId'] as String, + resolved: _i1.BoolJsonExtension.fromJson(jsonSerialization['resolved']), + status: jsonSerialization['status'] == null + ? null + : _i2.AlertStatus.fromJson((jsonSerialization['status'] as String)), + resolvedAt: jsonSerialization['resolvedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson(jsonSerialization['resolvedAt']), + ); + } + + String alertId; + + bool resolved; + + _i2.AlertStatus? status; + + DateTime? resolvedAt; + + /// Returns a shallow copy of this [AlertResolveResult] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + AlertResolveResult copyWith({ + String? alertId, + bool? resolved, + _i2.AlertStatus? status, + DateTime? resolvedAt, + }); + @override + Map toJson() { + return { + '__className__': 'AlertResolveResult', + 'alertId': alertId, + 'resolved': resolved, + if (status != null) 'status': status?.toJson(), + if (resolvedAt != null) 'resolvedAt': resolvedAt?.toJson(), + }; + } + + @override + String toString() { + return _i1.SerializationManager.encode(this); + } +} + +class _Undefined {} + +class _AlertResolveResultImpl extends AlertResolveResult { + _AlertResolveResultImpl({ + required String alertId, + required bool resolved, + _i2.AlertStatus? status, + DateTime? resolvedAt, + }) : super._( + alertId: alertId, + resolved: resolved, + status: status, + resolvedAt: resolvedAt, + ); + + /// Returns a shallow copy of this [AlertResolveResult] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + @override + AlertResolveResult copyWith({ + String? alertId, + bool? resolved, + Object? status = _Undefined, + Object? resolvedAt = _Undefined, + }) { + return AlertResolveResult( + alertId: alertId ?? this.alertId, + resolved: resolved ?? this.resolved, + status: status is _i2.AlertStatus? ? status : this.status, + resolvedAt: resolvedAt is DateTime? ? resolvedAt : this.resolvedAt, + ); + } +} diff --git a/backend/sinalacs_client/lib/src/protocol/api/alert_status_result.dart b/backend/sinalacs_client/lib/src/protocol/api/alert_status_result.dart index 4755004f..5d32d8d7 100644 --- a/backend/sinalacs_client/lib/src/protocol/api/alert_status_result.dart +++ b/backend/sinalacs_client/lib/src/protocol/api/alert_status_result.dart @@ -28,6 +28,7 @@ abstract class AlertStatusResult implements _i1.SerializableModel { this.status, this.triggeredAt, this.acknowledgedAt, + this.resolvedAt, }); factory AlertStatusResult({ @@ -37,6 +38,7 @@ abstract class AlertStatusResult implements _i1.SerializableModel { _i3.AlertStatus? status, DateTime? triggeredAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, }) = _AlertStatusResultImpl; factory AlertStatusResult.fromJson(Map jsonSerialization) { @@ -59,6 +61,9 @@ abstract class AlertStatusResult implements _i1.SerializableModel { : _i1.DateTimeJsonExtension.fromJson( jsonSerialization['acknowledgedAt'], ), + resolvedAt: jsonSerialization['resolvedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson(jsonSerialization['resolvedAt']), ); } @@ -74,6 +79,8 @@ abstract class AlertStatusResult implements _i1.SerializableModel { DateTime? acknowledgedAt; + DateTime? resolvedAt; + /// Returns a shallow copy of this [AlertStatusResult] /// with some or all fields replaced by the given arguments. @_i1.useResult @@ -84,6 +91,7 @@ abstract class AlertStatusResult implements _i1.SerializableModel { _i3.AlertStatus? status, DateTime? triggeredAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, }); @override Map toJson() { @@ -95,6 +103,7 @@ abstract class AlertStatusResult implements _i1.SerializableModel { if (status != null) 'status': status?.toJson(), if (triggeredAt != null) 'triggeredAt': triggeredAt?.toJson(), if (acknowledgedAt != null) 'acknowledgedAt': acknowledgedAt?.toJson(), + if (resolvedAt != null) 'resolvedAt': resolvedAt?.toJson(), }; } @@ -114,6 +123,7 @@ class _AlertStatusResultImpl extends AlertStatusResult { _i3.AlertStatus? status, DateTime? triggeredAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, }) : super._( found: found, alertId: alertId, @@ -121,6 +131,7 @@ class _AlertStatusResultImpl extends AlertStatusResult { status: status, triggeredAt: triggeredAt, acknowledgedAt: acknowledgedAt, + resolvedAt: resolvedAt, ); /// Returns a shallow copy of this [AlertStatusResult] @@ -134,6 +145,7 @@ class _AlertStatusResultImpl extends AlertStatusResult { Object? status = _Undefined, Object? triggeredAt = _Undefined, Object? acknowledgedAt = _Undefined, + Object? resolvedAt = _Undefined, }) { return AlertStatusResult( found: found ?? this.found, @@ -144,6 +156,7 @@ class _AlertStatusResultImpl extends AlertStatusResult { acknowledgedAt: acknowledgedAt is DateTime? ? acknowledgedAt : this.acknowledgedAt, + resolvedAt: resolvedAt is DateTime? ? resolvedAt : this.resolvedAt, ); } } diff --git a/backend/sinalacs_client/lib/src/protocol/client.dart b/backend/sinalacs_client/lib/src/protocol/client.dart index 07625ae1..9b041e74 100644 --- a/backend/sinalacs_client/lib/src/protocol/client.dart +++ b/backend/sinalacs_client/lib/src/protocol/client.dart @@ -50,37 +50,39 @@ import 'package:sinalacs_client/src/protocol/api/terms_version_entry.dart' as _i23; import 'package:sinalacs_client/src/protocol/api/red_alert_result.dart' as _i24; import 'package:sinalacs_client/src/protocol/api/alert_ack_result.dart' as _i25; -import 'package:sinalacs_client/src/protocol/api/alert_status_result.dart' +import 'package:sinalacs_client/src/protocol/api/alert_resolve_result.dart' as _i26; -import 'package:sinalacs_client/src/protocol/api/development_login_result.dart' +import 'package:sinalacs_client/src/protocol/api/alert_status_result.dart' as _i27; -import 'package:sinalacs_client/src/protocol/api/totp_enrollment_start.dart' +import 'package:sinalacs_client/src/protocol/api/development_login_result.dart' as _i28; -import 'package:sinalacs_client/src/protocol/api/service_health.dart' as _i29; +import 'package:sinalacs_client/src/protocol/api/totp_enrollment_start.dart' + as _i29; +import 'package:sinalacs_client/src/protocol/api/service_health.dart' as _i30; import 'package:sinalacs_client/src/protocol/api/notice_send_result.dart' - as _i30; -import 'package:sinalacs_client/src/protocol/api/enrollment_token_result.dart' as _i31; -import 'package:sinalacs_client/src/protocol/api/enrollment_result.dart' +import 'package:sinalacs_client/src/protocol/api/enrollment_token_result.dart' as _i32; -import 'package:sinalacs_client/src/protocol/api/micro_area_patient.dart' +import 'package:sinalacs_client/src/protocol/api/enrollment_result.dart' as _i33; -import 'package:sinalacs_client/src/protocol/api/patient_data_overview.dart' +import 'package:sinalacs_client/src/protocol/api/micro_area_patient.dart' as _i34; -import 'package:sinalacs_client/src/protocol/api/patient_consent_record.dart' +import 'package:sinalacs_client/src/protocol/api/patient_data_overview.dart' as _i35; -import 'package:sinalacs_client/src/protocol/enums/consent_purpose.dart' +import 'package:sinalacs_client/src/protocol/api/patient_consent_record.dart' as _i36; -import 'package:sinalacs_client/src/protocol/api/terms_change_notice.dart' +import 'package:sinalacs_client/src/protocol/enums/consent_purpose.dart' as _i37; -import 'package:sinalacs_client/src/protocol/api/patient_data_subject_request_record.dart' +import 'package:sinalacs_client/src/protocol/api/terms_change_notice.dart' as _i38; -import 'package:sinalacs_client/src/protocol/api/triage_result.dart' as _i39; -import 'package:sinalacs_client/src/protocol/api/ubs_contact.dart' as _i40; +import 'package:sinalacs_client/src/protocol/api/patient_data_subject_request_record.dart' + as _i39; +import 'package:sinalacs_client/src/protocol/api/triage_result.dart' as _i40; +import 'package:sinalacs_client/src/protocol/api/ubs_contact.dart' as _i41; import 'package:sinalacs_client/src/protocol/api/visit_sync_result.dart' - as _i41; -import 'package:sinalacs_client/src/protocol/api/visit_sync_entry.dart' as _i42; -import 'protocol.dart' as _i43; + as _i42; +import 'package:sinalacs_client/src/protocol/api/visit_sync_entry.dart' as _i43; +import 'protocol.dart' as _i44; /// Backoffice, só para `coordinator` e `admin`: leitura (issue #40), gestão de /// contas (issue #43), atendimento de pedidos do titular (issue #42) e @@ -593,10 +595,27 @@ class EndpointAlerts extends EndpointAuthenticated { }, ); + /// Conclui o atendimento de um alerta já confirmado (#76, RF05): é o que + /// faz o paciente ver "Atendido". Alerta inexistente ou de outra microárea + /// devolve `resolved: false`, como o ACK; alerta ainda não confirmado é + /// [AlertValidationException] com o texto de + /// `RedAlertService.notAcknowledgedMessage`. + _i2.Future<_i26.AlertResolveResult> resolve({ + required String accessToken, + required String alertId, + }) => caller.callServerEndpoint<_i26.AlertResolveResult>( + 'alerts', + 'resolve', + { + 'accessToken': accessToken, + 'alertId': alertId, + }, + ); + /// Status do alerta mais recente do PRÓPRIO paciente (RF05, decisão §5). /// `patientId` nunca é parâmetro — vem do token (INV-05). - _i2.Future<_i26.AlertStatusResult> statusFor({required String accessToken}) => - caller.callServerEndpoint<_i26.AlertStatusResult>( + _i2.Future<_i27.AlertStatusResult> statusFor({required String accessToken}) => + caller.callServerEndpoint<_i27.AlertStatusResult>( 'alerts', 'statusFor', {'accessToken': accessToken}, @@ -618,9 +637,9 @@ class EndpointAuth extends _i1.EndpointRef { @override String get name => 'auth'; - _i2.Future<_i27.DevelopmentLoginResult> developmentLogin({ + _i2.Future<_i28.DevelopmentLoginResult> developmentLogin({ required String role, - }) => caller.callServerEndpoint<_i27.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( 'auth', 'developmentLogin', {'role': role}, @@ -638,12 +657,12 @@ class EndpointAuth extends _i1.EndpointRef { /// [totpCode] recebe `MfaRequiredException` (o app pede o código e repete a /// chamada); com `REQUIRE_ACS_MFA` ligado e a MFA não ativada, recebe /// `MfaEnrollmentRequiredException` (o app chama `beginTotpEnrollment`). - _i2.Future<_i27.DevelopmentLoginResult> loginInstitutional({ + _i2.Future<_i28.DevelopmentLoginResult> loginInstitutional({ required String matricula, required String password, String? deviceId, String? totpCode, - }) => caller.callServerEndpoint<_i27.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( 'auth', 'loginInstitutional', { @@ -664,11 +683,11 @@ class EndpointAuth extends _i1.EndpointRef { /// As recusas são as de [loginInstitutional]: mensagem única para matrícula /// inexistente e senha errada, `MfaRequiredException` sem o código, e o mesmo /// bloqueio por tentativas. - _i2.Future<_i27.DevelopmentLoginResult> loginStaff({ + _i2.Future<_i28.DevelopmentLoginResult> loginStaff({ required String matricula, required String password, String? totpCode, - }) => caller.callServerEndpoint<_i27.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( 'auth', 'loginStaff', { @@ -683,11 +702,11 @@ class EndpointAuth extends _i1.EndpointRef { /// por `bin/issue_staff_activation_code.dart`; a MFA é obrigatória, então não /// há login possível antes dela. Código errado, expirado ou já usado recebe a /// mesma recusa e conta como tentativa falha da conta. - _i2.Future<_i28.TotpEnrollmentStart> beginStaffTotpEnrollment({ + _i2.Future<_i29.TotpEnrollmentStart> beginStaffTotpEnrollment({ required String matricula, required String password, required String activationCode, - }) => caller.callServerEndpoint<_i28.TotpEnrollmentStart>( + }) => caller.callServerEndpoint<_i29.TotpEnrollmentStart>( 'auth', 'beginStaffTotpEnrollment', { @@ -717,10 +736,10 @@ class EndpointAuth extends _i1.EndpointRef { /// Renova a sessão do ACS sem pedir senha nem TOTP (LGPD-RT06). Público por /// desenho: quem chama já perdeu o JWT de 15 min — o refresh token, opaco e de /// uso único, é a credencial. Toda recusa é a mesma `SessionExpiredException`. - _i2.Future<_i27.DevelopmentLoginResult> refreshSession({ + _i2.Future<_i28.DevelopmentLoginResult> refreshSession({ required String refreshToken, required String deviceId, - }) => caller.callServerEndpoint<_i27.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( 'auth', 'refreshSession', { @@ -738,10 +757,10 @@ class EndpointAuth extends _i1.EndpointRef { ); /// Começa a ativação da MFA do ACS (RF07). Sem token: o ACS prova matrícula e senha. - _i2.Future<_i28.TotpEnrollmentStart> beginTotpEnrollment({ + _i2.Future<_i29.TotpEnrollmentStart> beginTotpEnrollment({ required String matricula, required String password, - }) => caller.callServerEndpoint<_i28.TotpEnrollmentStart>( + }) => caller.callServerEndpoint<_i29.TotpEnrollmentStart>( 'auth', 'beginTotpEnrollment', { @@ -806,11 +825,11 @@ class EndpointAuth extends _i1.EndpointRef { /// /// Toda recusa chega ao app como `OtpRequestException`, com a mensagem que o /// serviço escolheu: uma só, para não dizer se aquele CPF existe. - _i2.Future<_i27.DevelopmentLoginResult> verifyOtp({ + _i2.Future<_i28.DevelopmentLoginResult> verifyOtp({ required String cpf, required String code, String? deviceId, - }) => caller.callServerEndpoint<_i27.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( 'auth', 'verifyOtp', { @@ -877,8 +896,8 @@ class EndpointHealth extends _i1.EndpointRef { @override String get name => 'health'; - _i2.Future<_i29.ServiceHealth> check() => - caller.callServerEndpoint<_i29.ServiceHealth>( + _i2.Future<_i30.ServiceHealth> check() => + caller.callServerEndpoint<_i30.ServiceHealth>( 'health', 'check', {}, @@ -897,12 +916,12 @@ class EndpointNotices extends EndpointAuthenticated { /// consentiram com `segmentedPush`. A microárea vem do token, nunca de /// parâmetro. Auditoria: uma linha `community_notice` com quem enviou e a /// microárea; o texto do aviso nunca entra na trilha. - _i2.Future<_i30.NoticeSendResult> sendSegmented({ + _i2.Future<_i31.NoticeSendResult> sendSegmented({ required String accessToken, required String title, required String message, required String audience, - }) => caller.callServerEndpoint<_i30.NoticeSendResult>( + }) => caller.callServerEndpoint<_i31.NoticeSendResult>( 'notices', 'sendSegmented', { @@ -931,10 +950,10 @@ class EndpointOnboarding extends _i1.EndpointRef { String get name => 'onboarding'; /// Chamado pelo app do ACS. Exige sessão de ACS. - _i2.Future<_i31.EnrollmentTokenResult> generateEnrollmentToken({ + _i2.Future<_i32.EnrollmentTokenResult> generateEnrollmentToken({ required String accessToken, required String patientId, - }) => caller.callServerEndpoint<_i31.EnrollmentTokenResult>( + }) => caller.callServerEndpoint<_i32.EnrollmentTokenResult>( 'onboarding', 'generateEnrollmentToken', { @@ -945,13 +964,13 @@ class EndpointOnboarding extends _i1.EndpointRef { /// Chamado pelo app do paciente. Não exige sessão prévia — é a própria /// conclusão do onboarding que emite a primeira sessão. - _i2.Future<_i32.EnrollmentResult> completeEnrollment({ + _i2.Future<_i33.EnrollmentResult> completeEnrollment({ required String token, required bool healthDataConsent, required bool remindersConsent, required bool pushConsent, required bool termsAccepted, - }) => caller.callServerEndpoint<_i32.EnrollmentResult>( + }) => caller.callServerEndpoint<_i33.EnrollmentResult>( 'onboarding', 'completeEnrollment', { @@ -980,9 +999,9 @@ class EndpointPatients extends EndpointAuthenticated { @override String get name => 'patients'; - _i2.Future> listMicroArea({ + _i2.Future> listMicroArea({ required String accessToken, - }) => caller.callServerEndpoint>( + }) => caller.callServerEndpoint>( 'patients', 'listMicroArea', {'accessToken': accessToken}, @@ -1015,8 +1034,8 @@ class EndpointPatients extends EndpointAuthenticated { /// Painel "Meus Dados" do próprio paciente autenticado (LGPD, /// spec/lgpd_design.md linhas 417/581-595): confirmação de existência de /// tratamento e acesso aos dados pessoais. - _i2.Future<_i34.PatientDataOverview> myData({required String accessToken}) => - caller.callServerEndpoint<_i34.PatientDataOverview>( + _i2.Future<_i35.PatientDataOverview> myData({required String accessToken}) => + caller.callServerEndpoint<_i35.PatientDataOverview>( 'patients', 'myData', {'accessToken': accessToken}, @@ -1026,11 +1045,11 @@ class EndpointPatients extends EndpointAuthenticated { /// consentimento (LGPD-RF05): uma linha nova em `consent_logs`, nunca a /// edição da anterior. `healthDataProcessing` volta como /// [DataRightsException] — retirá-lo passa pelo pedido de exclusão. - _i2.Future<_i35.PatientConsentRecord> updateConsent({ + _i2.Future<_i36.PatientConsentRecord> updateConsent({ required String accessToken, - required _i36.ConsentPurpose purpose, + required _i37.ConsentPurpose purpose, required bool granted, - }) => caller.callServerEndpoint<_i35.PatientConsentRecord>( + }) => caller.callServerEndpoint<_i36.PatientConsentRecord>( 'patients', 'updateConsent', { @@ -1044,9 +1063,9 @@ class EndpointPatients extends EndpointAuthenticated { /// por quem entrou por OTP sem passar pelo onboarding, ou aceitou uma versão /// anterior. Só paciente; grava uma linha assinada em `consent_logs` só se a /// versão vigente ainda não foi aceita — repetir devolve a existente. - _i2.Future<_i35.PatientConsentRecord> acceptTermsOfUse({ + _i2.Future<_i36.PatientConsentRecord> acceptTermsOfUse({ required String accessToken, - }) => caller.callServerEndpoint<_i35.PatientConsentRecord>( + }) => caller.callServerEndpoint<_i36.PatientConsentRecord>( 'patients', 'acceptTermsOfUse', {'accessToken': accessToken}, @@ -1070,7 +1089,7 @@ class EndpointPatients extends EndpointAuthenticated { /// push (RF14). _i2.Future hasGrantedConsent({ required String accessToken, - required _i36.ConsentPurpose purpose, + required _i37.ConsentPurpose purpose, }) => caller.callServerEndpoint( 'patients', 'hasGrantedConsent', @@ -1083,9 +1102,9 @@ class EndpointPatients extends EndpointAuthenticated { /// Aviso de mudança dos termos ativo agora (LGPD-RF18, 15 dias de antecedência), /// ou `null`. Só paciente. Lê a agenda publicada em `terms_versions` e não grava /// linha de auditoria: nada do titular é lido nem gravado. - _i2.Future<_i37.TermsChangeNotice?> termsChangeNotice({ + _i2.Future<_i38.TermsChangeNotice?> termsChangeNotice({ required String accessToken, - }) => caller.callServerEndpoint<_i37.TermsChangeNotice?>( + }) => caller.callServerEndpoint<_i38.TermsChangeNotice?>( 'patients', 'termsChangeNotice', {'accessToken': accessToken}, @@ -1093,9 +1112,9 @@ class EndpointPatients extends EndpointAuthenticated { /// Pedido de exclusão/anonimização dos próprios dados (LGPD-RF08). /// Idempotente enquanto houver um pedido de exclusão em aberto. - _i2.Future<_i38.PatientDataSubjectRequestRecord> requestDataDeletion({ + _i2.Future<_i39.PatientDataSubjectRequestRecord> requestDataDeletion({ required String accessToken, - }) => caller.callServerEndpoint<_i38.PatientDataSubjectRequestRecord>( + }) => caller.callServerEndpoint<_i39.PatientDataSubjectRequestRecord>( 'patients', 'requestDataDeletion', {'accessToken': accessToken}, @@ -1104,10 +1123,10 @@ class EndpointPatients extends EndpointAuthenticated { /// Pedido de correção de um dado (LGPD-RF08). [details] é texto livre do /// titular, gravado cifrado; vazio ou acima de 500 caracteres volta como /// [DataRightsException]. - _i2.Future<_i38.PatientDataSubjectRequestRecord> requestDataCorrection({ + _i2.Future<_i39.PatientDataSubjectRequestRecord> requestDataCorrection({ required String accessToken, required String details, - }) => caller.callServerEndpoint<_i38.PatientDataSubjectRequestRecord>( + }) => caller.callServerEndpoint<_i39.PatientDataSubjectRequestRecord>( 'patients', 'requestDataCorrection', { @@ -1134,7 +1153,7 @@ class EndpointTriage extends EndpointAuthenticated { @override String get name => 'triage'; - _i2.Future<_i39.TriageResult> evaluate({ + _i2.Future<_i40.TriageResult> evaluate({ required String accessToken, required bool chestPain, required bool difficultyBreathing, @@ -1142,7 +1161,7 @@ class EndpointTriage extends EndpointAuthenticated { required bool persistentVomiting, required bool bleeding, required bool severeWeakness, - }) => caller.callServerEndpoint<_i39.TriageResult>( + }) => caller.callServerEndpoint<_i40.TriageResult>( 'triage', 'evaluate', { @@ -1165,8 +1184,8 @@ class EndpointUbs extends EndpointAuthenticated { @override String get name => 'ubs'; - _i2.Future<_i40.UbsContact> myContact({required String accessToken}) => - caller.callServerEndpoint<_i40.UbsContact>( + _i2.Future<_i41.UbsContact> myContact({required String accessToken}) => + caller.callServerEndpoint<_i41.UbsContact>( 'ubs', 'myContact', {'accessToken': accessToken}, @@ -1189,10 +1208,10 @@ class EndpointVisits extends EndpointAuthenticated { @override String get name => 'visits'; - _i2.Future> sync({ + _i2.Future> sync({ required String accessToken, - required List<_i42.VisitSyncEntry> visits, - }) => caller.callServerEndpoint>( + required List<_i43.VisitSyncEntry> visits, + }) => caller.callServerEndpoint>( 'visits', 'sync', { @@ -1206,11 +1225,11 @@ class EndpointVisits extends EndpointAuthenticated { /// /// A sessão do ACS é só o transporte — ver `VisitSyncService.syncLegacy`. /// Mesma transação por lote e mesma tradução de erros de [sync]. - _i2.Future> syncLegacy({ + _i2.Future> syncLegacy({ required String accessToken, required String deviceId, - required List<_i42.VisitSyncEntry> visits, - }) => caller.callServerEndpoint>( + required List<_i43.VisitSyncEntry> visits, + }) => caller.callServerEndpoint>( 'visits', 'syncLegacy', { @@ -1234,11 +1253,11 @@ class EndpointVisits extends EndpointAuthenticated { /// No máximo `VisitSyncService.maxLegacyBatch` visitas por chamada, checado /// antes de qualquer acesso ao banco. Um evento `visit_deferred_sync` por /// lote gravado vai para `audit_logs`, com o dono e nada clínico. - _i2.Future> syncDeferred({ + _i2.Future> syncDeferred({ required String uploadToken, required String deviceId, - required List<_i42.VisitSyncEntry> visits, - }) => caller.callServerEndpoint>( + required List<_i43.VisitSyncEntry> visits, + }) => caller.callServerEndpoint>( 'visits', 'syncDeferred', { @@ -1261,10 +1280,10 @@ class EndpointVisits extends EndpointAuthenticated { /// Sincronização central→dispositivo: visitas da microárea do ACS /// autenticado alteradas após `since`, para reconciliar um device que /// ficou offline ou foi reinstalado. - _i2.Future> pull({ + _i2.Future> pull({ required String accessToken, required DateTime since, - }) => caller.callServerEndpoint>( + }) => caller.callServerEndpoint>( 'visits', 'pull', { @@ -1294,7 +1313,7 @@ class Client extends _i1.ServerpodClientShared { bool? disconnectStreamsOnLostInternetConnection, }) : super( host, - _i43.Protocol(), + _i44.Protocol(), securityContext: securityContext, streamingConnectionTimeout: streamingConnectionTimeout, connectionTimeout: connectionTimeout, diff --git a/backend/sinalacs_client/lib/src/protocol/protocol.dart b/backend/sinalacs_client/lib/src/protocol/protocol.dart index f3511641..efe04f9e 100644 --- a/backend/sinalacs_client/lib/src/protocol/protocol.dart +++ b/backend/sinalacs_client/lib/src/protocol/protocol.dart @@ -39,84 +39,85 @@ import 'api/admin_password_reset_result.dart' as _i25; import 'api/admin_staff.dart' as _i26; import 'api/admin_staff_mfa_reset_result.dart' as _i27; import 'api/alert_ack_result.dart' as _i28; -import 'api/alert_status_result.dart' as _i29; -import 'api/development_login_result.dart' as _i30; -import 'api/enrollment_result.dart' as _i31; -import 'api/enrollment_token_result.dart' as _i32; -import 'api/micro_area_patient.dart' as _i33; -import 'api/notice_history_entry.dart' as _i34; -import 'api/notice_history_page.dart' as _i35; -import 'api/notice_send_result.dart' as _i36; -import 'api/patient_consent_record.dart' as _i37; -import 'api/patient_data_overview.dart' as _i38; -import 'api/patient_data_subject_request_record.dart' as _i39; -import 'api/patient_risk_event.dart' as _i40; -import 'api/red_alert_result.dart' as _i41; -import 'api/service_health.dart' as _i42; -import 'api/terms_change_notice.dart' as _i43; -import 'api/terms_version_entry.dart' as _i44; -import 'api/totp_enrollment_start.dart' as _i45; -import 'api/triage_result.dart' as _i46; -import 'api/ubs_contact.dart' as _i47; -import 'api/visit_sync_entry.dart' as _i48; -import 'api/visit_sync_result.dart' as _i49; -import 'audit_chain_checkpoint.dart' as _i50; -import 'audit_export.dart' as _i51; -import 'audit_log.dart' as _i52; -import 'consent_log.dart' as _i53; -import 'data_subject_request.dart' as _i54; -import 'enrollment_token.dart' as _i55; -import 'enums/alert_status.dart' as _i56; -import 'enums/arrival_method.dart' as _i57; -import 'enums/audit_chain_reason.dart' as _i58; -import 'enums/audit_integrity_state.dart' as _i59; -import 'enums/audit_row_seal.dart' as _i60; -import 'enums/consent_purpose.dart' as _i61; -import 'enums/data_subject_request_status.dart' as _i62; -import 'enums/data_subject_request_type.dart' as _i63; -import 'enums/escalation_reason.dart' as _i64; -import 'enums/risk_level.dart' as _i65; -import 'enums/scheduled_notice_status.dart' as _i66; -import 'enums/sync_status.dart' as _i67; -import 'enums/user_role.dart' as _i68; -import 'enums/visit_authorship.dart' as _i69; -import 'exceptions/admin_invalid_request_exception.dart' as _i70; -import 'exceptions/alert_dispatch_unavailable_exception.dart' as _i71; -import 'exceptions/alert_permission_exception.dart' as _i72; -import 'exceptions/alert_validation_exception.dart' as _i73; -import 'exceptions/authentication_failed_exception.dart' as _i74; -import 'exceptions/data_rights_exception.dart' as _i75; -import 'exceptions/endpoint_disabled_exception.dart' as _i76; -import 'exceptions/enrollment_exception.dart' as _i77; -import 'exceptions/mfa_enrollment_required_exception.dart' as _i78; -import 'exceptions/mfa_required_exception.dart' as _i79; -import 'exceptions/notice_delivery_exception.dart' as _i80; -import 'exceptions/otp_request_exception.dart' as _i81; -import 'exceptions/session_expired_exception.dart' as _i82; -import 'micro_area.dart' as _i83; -import 'otp_challenge.dart' as _i84; -import 'patient.dart' as _i85; -import 'push_token.dart' as _i86; -import 'scheduled_notice.dart' as _i87; -import 'staff_account.dart' as _i88; -import 'terms_version.dart' as _i89; -import 'triage_answer.dart' as _i90; -import 'triage_session.dart' as _i91; -import 'ubs.dart' as _i92; -import 'user.dart' as _i93; -import 'user_credential.dart' as _i94; -import 'visit.dart' as _i95; -import 'package:sinalacs_client/src/protocol/api/admin_micro_area.dart' as _i96; -import 'package:sinalacs_client/src/protocol/api/admin_acs.dart' as _i97; -import 'package:sinalacs_client/src/protocol/api/admin_staff.dart' as _i98; +import 'api/alert_resolve_result.dart' as _i29; +import 'api/alert_status_result.dart' as _i30; +import 'api/development_login_result.dart' as _i31; +import 'api/enrollment_result.dart' as _i32; +import 'api/enrollment_token_result.dart' as _i33; +import 'api/micro_area_patient.dart' as _i34; +import 'api/notice_history_entry.dart' as _i35; +import 'api/notice_history_page.dart' as _i36; +import 'api/notice_send_result.dart' as _i37; +import 'api/patient_consent_record.dart' as _i38; +import 'api/patient_data_overview.dart' as _i39; +import 'api/patient_data_subject_request_record.dart' as _i40; +import 'api/patient_risk_event.dart' as _i41; +import 'api/red_alert_result.dart' as _i42; +import 'api/service_health.dart' as _i43; +import 'api/terms_change_notice.dart' as _i44; +import 'api/terms_version_entry.dart' as _i45; +import 'api/totp_enrollment_start.dart' as _i46; +import 'api/triage_result.dart' as _i47; +import 'api/ubs_contact.dart' as _i48; +import 'api/visit_sync_entry.dart' as _i49; +import 'api/visit_sync_result.dart' as _i50; +import 'audit_chain_checkpoint.dart' as _i51; +import 'audit_export.dart' as _i52; +import 'audit_log.dart' as _i53; +import 'consent_log.dart' as _i54; +import 'data_subject_request.dart' as _i55; +import 'enrollment_token.dart' as _i56; +import 'enums/alert_status.dart' as _i57; +import 'enums/arrival_method.dart' as _i58; +import 'enums/audit_chain_reason.dart' as _i59; +import 'enums/audit_integrity_state.dart' as _i60; +import 'enums/audit_row_seal.dart' as _i61; +import 'enums/consent_purpose.dart' as _i62; +import 'enums/data_subject_request_status.dart' as _i63; +import 'enums/data_subject_request_type.dart' as _i64; +import 'enums/escalation_reason.dart' as _i65; +import 'enums/risk_level.dart' as _i66; +import 'enums/scheduled_notice_status.dart' as _i67; +import 'enums/sync_status.dart' as _i68; +import 'enums/user_role.dart' as _i69; +import 'enums/visit_authorship.dart' as _i70; +import 'exceptions/admin_invalid_request_exception.dart' as _i71; +import 'exceptions/alert_dispatch_unavailable_exception.dart' as _i72; +import 'exceptions/alert_permission_exception.dart' as _i73; +import 'exceptions/alert_validation_exception.dart' as _i74; +import 'exceptions/authentication_failed_exception.dart' as _i75; +import 'exceptions/data_rights_exception.dart' as _i76; +import 'exceptions/endpoint_disabled_exception.dart' as _i77; +import 'exceptions/enrollment_exception.dart' as _i78; +import 'exceptions/mfa_enrollment_required_exception.dart' as _i79; +import 'exceptions/mfa_required_exception.dart' as _i80; +import 'exceptions/notice_delivery_exception.dart' as _i81; +import 'exceptions/otp_request_exception.dart' as _i82; +import 'exceptions/session_expired_exception.dart' as _i83; +import 'micro_area.dart' as _i84; +import 'otp_challenge.dart' as _i85; +import 'patient.dart' as _i86; +import 'push_token.dart' as _i87; +import 'scheduled_notice.dart' as _i88; +import 'staff_account.dart' as _i89; +import 'terms_version.dart' as _i90; +import 'triage_answer.dart' as _i91; +import 'triage_session.dart' as _i92; +import 'ubs.dart' as _i93; +import 'user.dart' as _i94; +import 'user_credential.dart' as _i95; +import 'visit.dart' as _i96; +import 'package:sinalacs_client/src/protocol/api/admin_micro_area.dart' as _i97; +import 'package:sinalacs_client/src/protocol/api/admin_acs.dart' as _i98; +import 'package:sinalacs_client/src/protocol/api/admin_staff.dart' as _i99; import 'package:sinalacs_client/src/protocol/api/terms_version_entry.dart' - as _i99; -import 'package:sinalacs_client/src/protocol/api/micro_area_patient.dart' as _i100; -import 'package:sinalacs_client/src/protocol/api/visit_sync_result.dart' +import 'package:sinalacs_client/src/protocol/api/micro_area_patient.dart' as _i101; -import 'package:sinalacs_client/src/protocol/api/visit_sync_entry.dart' +import 'package:sinalacs_client/src/protocol/api/visit_sync_result.dart' as _i102; +import 'package:sinalacs_client/src/protocol/api/visit_sync_entry.dart' + as _i103; export 'acs.dart'; export 'acs_refresh_token.dart'; export 'acs_upload_token.dart'; @@ -144,6 +145,7 @@ export 'api/admin_password_reset_result.dart'; export 'api/admin_staff.dart'; export 'api/admin_staff_mfa_reset_result.dart'; export 'api/alert_ack_result.dart'; +export 'api/alert_resolve_result.dart'; export 'api/alert_status_result.dart'; export 'api/development_login_result.dart'; export 'api/enrollment_result.dart'; @@ -328,206 +330,209 @@ class Protocol extends _i1.SerializationManager { if (t == _i28.AlertAckResult) { return _i28.AlertAckResult.fromJson(data) as T; } - if (t == _i29.AlertStatusResult) { - return _i29.AlertStatusResult.fromJson(data) as T; + if (t == _i29.AlertResolveResult) { + return _i29.AlertResolveResult.fromJson(data) as T; + } + if (t == _i30.AlertStatusResult) { + return _i30.AlertStatusResult.fromJson(data) as T; } - if (t == _i30.DevelopmentLoginResult) { - return _i30.DevelopmentLoginResult.fromJson(data) as T; + if (t == _i31.DevelopmentLoginResult) { + return _i31.DevelopmentLoginResult.fromJson(data) as T; } - if (t == _i31.EnrollmentResult) { - return _i31.EnrollmentResult.fromJson(data) as T; + if (t == _i32.EnrollmentResult) { + return _i32.EnrollmentResult.fromJson(data) as T; } - if (t == _i32.EnrollmentTokenResult) { - return _i32.EnrollmentTokenResult.fromJson(data) as T; + if (t == _i33.EnrollmentTokenResult) { + return _i33.EnrollmentTokenResult.fromJson(data) as T; } - if (t == _i33.MicroAreaPatient) { - return _i33.MicroAreaPatient.fromJson(data) as T; + if (t == _i34.MicroAreaPatient) { + return _i34.MicroAreaPatient.fromJson(data) as T; } - if (t == _i34.NoticeHistoryEntry) { - return _i34.NoticeHistoryEntry.fromJson(data) as T; + if (t == _i35.NoticeHistoryEntry) { + return _i35.NoticeHistoryEntry.fromJson(data) as T; } - if (t == _i35.NoticeHistoryPage) { - return _i35.NoticeHistoryPage.fromJson(data) as T; + if (t == _i36.NoticeHistoryPage) { + return _i36.NoticeHistoryPage.fromJson(data) as T; } - if (t == _i36.NoticeSendResult) { - return _i36.NoticeSendResult.fromJson(data) as T; + if (t == _i37.NoticeSendResult) { + return _i37.NoticeSendResult.fromJson(data) as T; } - if (t == _i37.PatientConsentRecord) { - return _i37.PatientConsentRecord.fromJson(data) as T; + if (t == _i38.PatientConsentRecord) { + return _i38.PatientConsentRecord.fromJson(data) as T; } - if (t == _i38.PatientDataOverview) { - return _i38.PatientDataOverview.fromJson(data) as T; + if (t == _i39.PatientDataOverview) { + return _i39.PatientDataOverview.fromJson(data) as T; } - if (t == _i39.PatientDataSubjectRequestRecord) { - return _i39.PatientDataSubjectRequestRecord.fromJson(data) as T; + if (t == _i40.PatientDataSubjectRequestRecord) { + return _i40.PatientDataSubjectRequestRecord.fromJson(data) as T; } - if (t == _i40.PatientRiskEvent) { - return _i40.PatientRiskEvent.fromJson(data) as T; + if (t == _i41.PatientRiskEvent) { + return _i41.PatientRiskEvent.fromJson(data) as T; } - if (t == _i41.RedAlertResult) { - return _i41.RedAlertResult.fromJson(data) as T; + if (t == _i42.RedAlertResult) { + return _i42.RedAlertResult.fromJson(data) as T; } - if (t == _i42.ServiceHealth) { - return _i42.ServiceHealth.fromJson(data) as T; + if (t == _i43.ServiceHealth) { + return _i43.ServiceHealth.fromJson(data) as T; } - if (t == _i43.TermsChangeNotice) { - return _i43.TermsChangeNotice.fromJson(data) as T; + if (t == _i44.TermsChangeNotice) { + return _i44.TermsChangeNotice.fromJson(data) as T; } - if (t == _i44.TermsVersionEntry) { - return _i44.TermsVersionEntry.fromJson(data) as T; + if (t == _i45.TermsVersionEntry) { + return _i45.TermsVersionEntry.fromJson(data) as T; } - if (t == _i45.TotpEnrollmentStart) { - return _i45.TotpEnrollmentStart.fromJson(data) as T; + if (t == _i46.TotpEnrollmentStart) { + return _i46.TotpEnrollmentStart.fromJson(data) as T; } - if (t == _i46.TriageResult) { - return _i46.TriageResult.fromJson(data) as T; + if (t == _i47.TriageResult) { + return _i47.TriageResult.fromJson(data) as T; } - if (t == _i47.UbsContact) { - return _i47.UbsContact.fromJson(data) as T; + if (t == _i48.UbsContact) { + return _i48.UbsContact.fromJson(data) as T; } - if (t == _i48.VisitSyncEntry) { - return _i48.VisitSyncEntry.fromJson(data) as T; + if (t == _i49.VisitSyncEntry) { + return _i49.VisitSyncEntry.fromJson(data) as T; } - if (t == _i49.VisitSyncResult) { - return _i49.VisitSyncResult.fromJson(data) as T; + if (t == _i50.VisitSyncResult) { + return _i50.VisitSyncResult.fromJson(data) as T; } - if (t == _i50.AuditChainCheckpoint) { - return _i50.AuditChainCheckpoint.fromJson(data) as T; + if (t == _i51.AuditChainCheckpoint) { + return _i51.AuditChainCheckpoint.fromJson(data) as T; } - if (t == _i51.AuditExport) { - return _i51.AuditExport.fromJson(data) as T; + if (t == _i52.AuditExport) { + return _i52.AuditExport.fromJson(data) as T; } - if (t == _i52.AuditLog) { - return _i52.AuditLog.fromJson(data) as T; + if (t == _i53.AuditLog) { + return _i53.AuditLog.fromJson(data) as T; } - if (t == _i53.ConsentLog) { - return _i53.ConsentLog.fromJson(data) as T; + if (t == _i54.ConsentLog) { + return _i54.ConsentLog.fromJson(data) as T; } - if (t == _i54.DataSubjectRequest) { - return _i54.DataSubjectRequest.fromJson(data) as T; + if (t == _i55.DataSubjectRequest) { + return _i55.DataSubjectRequest.fromJson(data) as T; } - if (t == _i55.EnrollmentToken) { - return _i55.EnrollmentToken.fromJson(data) as T; + if (t == _i56.EnrollmentToken) { + return _i56.EnrollmentToken.fromJson(data) as T; } - if (t == _i56.AlertStatus) { - return _i56.AlertStatus.fromJson(data) as T; + if (t == _i57.AlertStatus) { + return _i57.AlertStatus.fromJson(data) as T; } - if (t == _i57.ArrivalMethod) { - return _i57.ArrivalMethod.fromJson(data) as T; + if (t == _i58.ArrivalMethod) { + return _i58.ArrivalMethod.fromJson(data) as T; } - if (t == _i58.AuditChainReason) { - return _i58.AuditChainReason.fromJson(data) as T; + if (t == _i59.AuditChainReason) { + return _i59.AuditChainReason.fromJson(data) as T; } - if (t == _i59.AuditIntegrityState) { - return _i59.AuditIntegrityState.fromJson(data) as T; + if (t == _i60.AuditIntegrityState) { + return _i60.AuditIntegrityState.fromJson(data) as T; } - if (t == _i60.AuditRowSeal) { - return _i60.AuditRowSeal.fromJson(data) as T; + if (t == _i61.AuditRowSeal) { + return _i61.AuditRowSeal.fromJson(data) as T; } - if (t == _i61.ConsentPurpose) { - return _i61.ConsentPurpose.fromJson(data) as T; + if (t == _i62.ConsentPurpose) { + return _i62.ConsentPurpose.fromJson(data) as T; } - if (t == _i62.DataSubjectRequestStatus) { - return _i62.DataSubjectRequestStatus.fromJson(data) as T; + if (t == _i63.DataSubjectRequestStatus) { + return _i63.DataSubjectRequestStatus.fromJson(data) as T; } - if (t == _i63.DataSubjectRequestType) { - return _i63.DataSubjectRequestType.fromJson(data) as T; + if (t == _i64.DataSubjectRequestType) { + return _i64.DataSubjectRequestType.fromJson(data) as T; } - if (t == _i64.EscalationReason) { - return _i64.EscalationReason.fromJson(data) as T; + if (t == _i65.EscalationReason) { + return _i65.EscalationReason.fromJson(data) as T; } - if (t == _i65.RiskLevel) { - return _i65.RiskLevel.fromJson(data) as T; + if (t == _i66.RiskLevel) { + return _i66.RiskLevel.fromJson(data) as T; } - if (t == _i66.ScheduledNoticeStatus) { - return _i66.ScheduledNoticeStatus.fromJson(data) as T; + if (t == _i67.ScheduledNoticeStatus) { + return _i67.ScheduledNoticeStatus.fromJson(data) as T; } - if (t == _i67.SyncStatus) { - return _i67.SyncStatus.fromJson(data) as T; + if (t == _i68.SyncStatus) { + return _i68.SyncStatus.fromJson(data) as T; } - if (t == _i68.UserRole) { - return _i68.UserRole.fromJson(data) as T; + if (t == _i69.UserRole) { + return _i69.UserRole.fromJson(data) as T; } - if (t == _i69.VisitAuthorship) { - return _i69.VisitAuthorship.fromJson(data) as T; + if (t == _i70.VisitAuthorship) { + return _i70.VisitAuthorship.fromJson(data) as T; } - if (t == _i70.AdminInvalidRequestException) { - return _i70.AdminInvalidRequestException.fromJson(data) as T; + if (t == _i71.AdminInvalidRequestException) { + return _i71.AdminInvalidRequestException.fromJson(data) as T; } - if (t == _i71.AlertDispatchUnavailableException) { - return _i71.AlertDispatchUnavailableException.fromJson(data) as T; + if (t == _i72.AlertDispatchUnavailableException) { + return _i72.AlertDispatchUnavailableException.fromJson(data) as T; } - if (t == _i72.AlertPermissionException) { - return _i72.AlertPermissionException.fromJson(data) as T; + if (t == _i73.AlertPermissionException) { + return _i73.AlertPermissionException.fromJson(data) as T; } - if (t == _i73.AlertValidationException) { - return _i73.AlertValidationException.fromJson(data) as T; + if (t == _i74.AlertValidationException) { + return _i74.AlertValidationException.fromJson(data) as T; } - if (t == _i74.AuthenticationFailedException) { - return _i74.AuthenticationFailedException.fromJson(data) as T; + if (t == _i75.AuthenticationFailedException) { + return _i75.AuthenticationFailedException.fromJson(data) as T; } - if (t == _i75.DataRightsException) { - return _i75.DataRightsException.fromJson(data) as T; + if (t == _i76.DataRightsException) { + return _i76.DataRightsException.fromJson(data) as T; } - if (t == _i76.EndpointDisabledException) { - return _i76.EndpointDisabledException.fromJson(data) as T; + if (t == _i77.EndpointDisabledException) { + return _i77.EndpointDisabledException.fromJson(data) as T; } - if (t == _i77.EnrollmentException) { - return _i77.EnrollmentException.fromJson(data) as T; + if (t == _i78.EnrollmentException) { + return _i78.EnrollmentException.fromJson(data) as T; } - if (t == _i78.MfaEnrollmentRequiredException) { - return _i78.MfaEnrollmentRequiredException.fromJson(data) as T; + if (t == _i79.MfaEnrollmentRequiredException) { + return _i79.MfaEnrollmentRequiredException.fromJson(data) as T; } - if (t == _i79.MfaRequiredException) { - return _i79.MfaRequiredException.fromJson(data) as T; + if (t == _i80.MfaRequiredException) { + return _i80.MfaRequiredException.fromJson(data) as T; } - if (t == _i80.NoticeDeliveryException) { - return _i80.NoticeDeliveryException.fromJson(data) as T; + if (t == _i81.NoticeDeliveryException) { + return _i81.NoticeDeliveryException.fromJson(data) as T; } - if (t == _i81.OtpRequestException) { - return _i81.OtpRequestException.fromJson(data) as T; + if (t == _i82.OtpRequestException) { + return _i82.OtpRequestException.fromJson(data) as T; } - if (t == _i82.SessionExpiredException) { - return _i82.SessionExpiredException.fromJson(data) as T; + if (t == _i83.SessionExpiredException) { + return _i83.SessionExpiredException.fromJson(data) as T; } - if (t == _i83.MicroArea) { - return _i83.MicroArea.fromJson(data) as T; + if (t == _i84.MicroArea) { + return _i84.MicroArea.fromJson(data) as T; } - if (t == _i84.OtpChallenge) { - return _i84.OtpChallenge.fromJson(data) as T; + if (t == _i85.OtpChallenge) { + return _i85.OtpChallenge.fromJson(data) as T; } - if (t == _i85.Patient) { - return _i85.Patient.fromJson(data) as T; + if (t == _i86.Patient) { + return _i86.Patient.fromJson(data) as T; } - if (t == _i86.PushToken) { - return _i86.PushToken.fromJson(data) as T; + if (t == _i87.PushToken) { + return _i87.PushToken.fromJson(data) as T; } - if (t == _i87.ScheduledNotice) { - return _i87.ScheduledNotice.fromJson(data) as T; + if (t == _i88.ScheduledNotice) { + return _i88.ScheduledNotice.fromJson(data) as T; } - if (t == _i88.StaffAccount) { - return _i88.StaffAccount.fromJson(data) as T; + if (t == _i89.StaffAccount) { + return _i89.StaffAccount.fromJson(data) as T; } - if (t == _i89.TermsVersion) { - return _i89.TermsVersion.fromJson(data) as T; + if (t == _i90.TermsVersion) { + return _i90.TermsVersion.fromJson(data) as T; } - if (t == _i90.TriageAnswer) { - return _i90.TriageAnswer.fromJson(data) as T; + if (t == _i91.TriageAnswer) { + return _i91.TriageAnswer.fromJson(data) as T; } - if (t == _i91.TriageSession) { - return _i91.TriageSession.fromJson(data) as T; + if (t == _i92.TriageSession) { + return _i92.TriageSession.fromJson(data) as T; } - if (t == _i92.Ubs) { - return _i92.Ubs.fromJson(data) as T; + if (t == _i93.Ubs) { + return _i93.Ubs.fromJson(data) as T; } - if (t == _i93.User) { - return _i93.User.fromJson(data) as T; + if (t == _i94.User) { + return _i94.User.fromJson(data) as T; } - if (t == _i94.UserCredential) { - return _i94.UserCredential.fromJson(data) as T; + if (t == _i95.UserCredential) { + return _i95.UserCredential.fromJson(data) as T; } - if (t == _i95.Visit) { - return _i95.Visit.fromJson(data) as T; + if (t == _i96.Visit) { + return _i96.Visit.fromJson(data) as T; } if (t == _i1.getType<_i2.Acs?>()) { return (data != null ? _i2.Acs.fromJson(data) : null) as T; @@ -628,250 +633,254 @@ class Protocol extends _i1.SerializationManager { if (t == _i1.getType<_i28.AlertAckResult?>()) { return (data != null ? _i28.AlertAckResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i29.AlertStatusResult?>()) { - return (data != null ? _i29.AlertStatusResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i29.AlertResolveResult?>()) { + return (data != null ? _i29.AlertResolveResult.fromJson(data) : null) + as T; } - if (t == _i1.getType<_i30.DevelopmentLoginResult?>()) { - return (data != null ? _i30.DevelopmentLoginResult.fromJson(data) : null) + if (t == _i1.getType<_i30.AlertStatusResult?>()) { + return (data != null ? _i30.AlertStatusResult.fromJson(data) : null) as T; + } + if (t == _i1.getType<_i31.DevelopmentLoginResult?>()) { + return (data != null ? _i31.DevelopmentLoginResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i31.EnrollmentResult?>()) { - return (data != null ? _i31.EnrollmentResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i32.EnrollmentResult?>()) { + return (data != null ? _i32.EnrollmentResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i32.EnrollmentTokenResult?>()) { - return (data != null ? _i32.EnrollmentTokenResult.fromJson(data) : null) + if (t == _i1.getType<_i33.EnrollmentTokenResult?>()) { + return (data != null ? _i33.EnrollmentTokenResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i33.MicroAreaPatient?>()) { - return (data != null ? _i33.MicroAreaPatient.fromJson(data) : null) as T; + if (t == _i1.getType<_i34.MicroAreaPatient?>()) { + return (data != null ? _i34.MicroAreaPatient.fromJson(data) : null) as T; } - if (t == _i1.getType<_i34.NoticeHistoryEntry?>()) { - return (data != null ? _i34.NoticeHistoryEntry.fromJson(data) : null) + if (t == _i1.getType<_i35.NoticeHistoryEntry?>()) { + return (data != null ? _i35.NoticeHistoryEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i35.NoticeHistoryPage?>()) { - return (data != null ? _i35.NoticeHistoryPage.fromJson(data) : null) as T; + if (t == _i1.getType<_i36.NoticeHistoryPage?>()) { + return (data != null ? _i36.NoticeHistoryPage.fromJson(data) : null) as T; } - if (t == _i1.getType<_i36.NoticeSendResult?>()) { - return (data != null ? _i36.NoticeSendResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i37.NoticeSendResult?>()) { + return (data != null ? _i37.NoticeSendResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i37.PatientConsentRecord?>()) { - return (data != null ? _i37.PatientConsentRecord.fromJson(data) : null) + if (t == _i1.getType<_i38.PatientConsentRecord?>()) { + return (data != null ? _i38.PatientConsentRecord.fromJson(data) : null) as T; } - if (t == _i1.getType<_i38.PatientDataOverview?>()) { - return (data != null ? _i38.PatientDataOverview.fromJson(data) : null) + if (t == _i1.getType<_i39.PatientDataOverview?>()) { + return (data != null ? _i39.PatientDataOverview.fromJson(data) : null) as T; } - if (t == _i1.getType<_i39.PatientDataSubjectRequestRecord?>()) { + if (t == _i1.getType<_i40.PatientDataSubjectRequestRecord?>()) { return (data != null - ? _i39.PatientDataSubjectRequestRecord.fromJson(data) + ? _i40.PatientDataSubjectRequestRecord.fromJson(data) : null) as T; } - if (t == _i1.getType<_i40.PatientRiskEvent?>()) { - return (data != null ? _i40.PatientRiskEvent.fromJson(data) : null) as T; + if (t == _i1.getType<_i41.PatientRiskEvent?>()) { + return (data != null ? _i41.PatientRiskEvent.fromJson(data) : null) as T; } - if (t == _i1.getType<_i41.RedAlertResult?>()) { - return (data != null ? _i41.RedAlertResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i42.RedAlertResult?>()) { + return (data != null ? _i42.RedAlertResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i42.ServiceHealth?>()) { - return (data != null ? _i42.ServiceHealth.fromJson(data) : null) as T; + if (t == _i1.getType<_i43.ServiceHealth?>()) { + return (data != null ? _i43.ServiceHealth.fromJson(data) : null) as T; } - if (t == _i1.getType<_i43.TermsChangeNotice?>()) { - return (data != null ? _i43.TermsChangeNotice.fromJson(data) : null) as T; + if (t == _i1.getType<_i44.TermsChangeNotice?>()) { + return (data != null ? _i44.TermsChangeNotice.fromJson(data) : null) as T; } - if (t == _i1.getType<_i44.TermsVersionEntry?>()) { - return (data != null ? _i44.TermsVersionEntry.fromJson(data) : null) as T; + if (t == _i1.getType<_i45.TermsVersionEntry?>()) { + return (data != null ? _i45.TermsVersionEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i45.TotpEnrollmentStart?>()) { - return (data != null ? _i45.TotpEnrollmentStart.fromJson(data) : null) + if (t == _i1.getType<_i46.TotpEnrollmentStart?>()) { + return (data != null ? _i46.TotpEnrollmentStart.fromJson(data) : null) as T; } - if (t == _i1.getType<_i46.TriageResult?>()) { - return (data != null ? _i46.TriageResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i47.TriageResult?>()) { + return (data != null ? _i47.TriageResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i47.UbsContact?>()) { - return (data != null ? _i47.UbsContact.fromJson(data) : null) as T; + if (t == _i1.getType<_i48.UbsContact?>()) { + return (data != null ? _i48.UbsContact.fromJson(data) : null) as T; } - if (t == _i1.getType<_i48.VisitSyncEntry?>()) { - return (data != null ? _i48.VisitSyncEntry.fromJson(data) : null) as T; + if (t == _i1.getType<_i49.VisitSyncEntry?>()) { + return (data != null ? _i49.VisitSyncEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i49.VisitSyncResult?>()) { - return (data != null ? _i49.VisitSyncResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i50.VisitSyncResult?>()) { + return (data != null ? _i50.VisitSyncResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i50.AuditChainCheckpoint?>()) { - return (data != null ? _i50.AuditChainCheckpoint.fromJson(data) : null) + if (t == _i1.getType<_i51.AuditChainCheckpoint?>()) { + return (data != null ? _i51.AuditChainCheckpoint.fromJson(data) : null) as T; } - if (t == _i1.getType<_i51.AuditExport?>()) { - return (data != null ? _i51.AuditExport.fromJson(data) : null) as T; + if (t == _i1.getType<_i52.AuditExport?>()) { + return (data != null ? _i52.AuditExport.fromJson(data) : null) as T; } - if (t == _i1.getType<_i52.AuditLog?>()) { - return (data != null ? _i52.AuditLog.fromJson(data) : null) as T; + if (t == _i1.getType<_i53.AuditLog?>()) { + return (data != null ? _i53.AuditLog.fromJson(data) : null) as T; } - if (t == _i1.getType<_i53.ConsentLog?>()) { - return (data != null ? _i53.ConsentLog.fromJson(data) : null) as T; + if (t == _i1.getType<_i54.ConsentLog?>()) { + return (data != null ? _i54.ConsentLog.fromJson(data) : null) as T; } - if (t == _i1.getType<_i54.DataSubjectRequest?>()) { - return (data != null ? _i54.DataSubjectRequest.fromJson(data) : null) + if (t == _i1.getType<_i55.DataSubjectRequest?>()) { + return (data != null ? _i55.DataSubjectRequest.fromJson(data) : null) as T; } - if (t == _i1.getType<_i55.EnrollmentToken?>()) { - return (data != null ? _i55.EnrollmentToken.fromJson(data) : null) as T; + if (t == _i1.getType<_i56.EnrollmentToken?>()) { + return (data != null ? _i56.EnrollmentToken.fromJson(data) : null) as T; } - if (t == _i1.getType<_i56.AlertStatus?>()) { - return (data != null ? _i56.AlertStatus.fromJson(data) : null) as T; + if (t == _i1.getType<_i57.AlertStatus?>()) { + return (data != null ? _i57.AlertStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i57.ArrivalMethod?>()) { - return (data != null ? _i57.ArrivalMethod.fromJson(data) : null) as T; + if (t == _i1.getType<_i58.ArrivalMethod?>()) { + return (data != null ? _i58.ArrivalMethod.fromJson(data) : null) as T; } - if (t == _i1.getType<_i58.AuditChainReason?>()) { - return (data != null ? _i58.AuditChainReason.fromJson(data) : null) as T; + if (t == _i1.getType<_i59.AuditChainReason?>()) { + return (data != null ? _i59.AuditChainReason.fromJson(data) : null) as T; } - if (t == _i1.getType<_i59.AuditIntegrityState?>()) { - return (data != null ? _i59.AuditIntegrityState.fromJson(data) : null) + if (t == _i1.getType<_i60.AuditIntegrityState?>()) { + return (data != null ? _i60.AuditIntegrityState.fromJson(data) : null) as T; } - if (t == _i1.getType<_i60.AuditRowSeal?>()) { - return (data != null ? _i60.AuditRowSeal.fromJson(data) : null) as T; + if (t == _i1.getType<_i61.AuditRowSeal?>()) { + return (data != null ? _i61.AuditRowSeal.fromJson(data) : null) as T; } - if (t == _i1.getType<_i61.ConsentPurpose?>()) { - return (data != null ? _i61.ConsentPurpose.fromJson(data) : null) as T; + if (t == _i1.getType<_i62.ConsentPurpose?>()) { + return (data != null ? _i62.ConsentPurpose.fromJson(data) : null) as T; } - if (t == _i1.getType<_i62.DataSubjectRequestStatus?>()) { + if (t == _i1.getType<_i63.DataSubjectRequestStatus?>()) { return (data != null - ? _i62.DataSubjectRequestStatus.fromJson(data) + ? _i63.DataSubjectRequestStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i63.DataSubjectRequestType?>()) { - return (data != null ? _i63.DataSubjectRequestType.fromJson(data) : null) + if (t == _i1.getType<_i64.DataSubjectRequestType?>()) { + return (data != null ? _i64.DataSubjectRequestType.fromJson(data) : null) as T; } - if (t == _i1.getType<_i64.EscalationReason?>()) { - return (data != null ? _i64.EscalationReason.fromJson(data) : null) as T; + if (t == _i1.getType<_i65.EscalationReason?>()) { + return (data != null ? _i65.EscalationReason.fromJson(data) : null) as T; } - if (t == _i1.getType<_i65.RiskLevel?>()) { - return (data != null ? _i65.RiskLevel.fromJson(data) : null) as T; + if (t == _i1.getType<_i66.RiskLevel?>()) { + return (data != null ? _i66.RiskLevel.fromJson(data) : null) as T; } - if (t == _i1.getType<_i66.ScheduledNoticeStatus?>()) { - return (data != null ? _i66.ScheduledNoticeStatus.fromJson(data) : null) + if (t == _i1.getType<_i67.ScheduledNoticeStatus?>()) { + return (data != null ? _i67.ScheduledNoticeStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i67.SyncStatus?>()) { - return (data != null ? _i67.SyncStatus.fromJson(data) : null) as T; + if (t == _i1.getType<_i68.SyncStatus?>()) { + return (data != null ? _i68.SyncStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i68.UserRole?>()) { - return (data != null ? _i68.UserRole.fromJson(data) : null) as T; + if (t == _i1.getType<_i69.UserRole?>()) { + return (data != null ? _i69.UserRole.fromJson(data) : null) as T; } - if (t == _i1.getType<_i69.VisitAuthorship?>()) { - return (data != null ? _i69.VisitAuthorship.fromJson(data) : null) as T; + if (t == _i1.getType<_i70.VisitAuthorship?>()) { + return (data != null ? _i70.VisitAuthorship.fromJson(data) : null) as T; } - if (t == _i1.getType<_i70.AdminInvalidRequestException?>()) { + if (t == _i1.getType<_i71.AdminInvalidRequestException?>()) { return (data != null - ? _i70.AdminInvalidRequestException.fromJson(data) + ? _i71.AdminInvalidRequestException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i71.AlertDispatchUnavailableException?>()) { + if (t == _i1.getType<_i72.AlertDispatchUnavailableException?>()) { return (data != null - ? _i71.AlertDispatchUnavailableException.fromJson(data) + ? _i72.AlertDispatchUnavailableException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i72.AlertPermissionException?>()) { + if (t == _i1.getType<_i73.AlertPermissionException?>()) { return (data != null - ? _i72.AlertPermissionException.fromJson(data) + ? _i73.AlertPermissionException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i73.AlertValidationException?>()) { + if (t == _i1.getType<_i74.AlertValidationException?>()) { return (data != null - ? _i73.AlertValidationException.fromJson(data) + ? _i74.AlertValidationException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i74.AuthenticationFailedException?>()) { + if (t == _i1.getType<_i75.AuthenticationFailedException?>()) { return (data != null - ? _i74.AuthenticationFailedException.fromJson(data) + ? _i75.AuthenticationFailedException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i75.DataRightsException?>()) { - return (data != null ? _i75.DataRightsException.fromJson(data) : null) + if (t == _i1.getType<_i76.DataRightsException?>()) { + return (data != null ? _i76.DataRightsException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i76.EndpointDisabledException?>()) { + if (t == _i1.getType<_i77.EndpointDisabledException?>()) { return (data != null - ? _i76.EndpointDisabledException.fromJson(data) + ? _i77.EndpointDisabledException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i77.EnrollmentException?>()) { - return (data != null ? _i77.EnrollmentException.fromJson(data) : null) + if (t == _i1.getType<_i78.EnrollmentException?>()) { + return (data != null ? _i78.EnrollmentException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i78.MfaEnrollmentRequiredException?>()) { + if (t == _i1.getType<_i79.MfaEnrollmentRequiredException?>()) { return (data != null - ? _i78.MfaEnrollmentRequiredException.fromJson(data) + ? _i79.MfaEnrollmentRequiredException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i79.MfaRequiredException?>()) { - return (data != null ? _i79.MfaRequiredException.fromJson(data) : null) + if (t == _i1.getType<_i80.MfaRequiredException?>()) { + return (data != null ? _i80.MfaRequiredException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i80.NoticeDeliveryException?>()) { - return (data != null ? _i80.NoticeDeliveryException.fromJson(data) : null) + if (t == _i1.getType<_i81.NoticeDeliveryException?>()) { + return (data != null ? _i81.NoticeDeliveryException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i81.OtpRequestException?>()) { - return (data != null ? _i81.OtpRequestException.fromJson(data) : null) + if (t == _i1.getType<_i82.OtpRequestException?>()) { + return (data != null ? _i82.OtpRequestException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i82.SessionExpiredException?>()) { - return (data != null ? _i82.SessionExpiredException.fromJson(data) : null) + if (t == _i1.getType<_i83.SessionExpiredException?>()) { + return (data != null ? _i83.SessionExpiredException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i83.MicroArea?>()) { - return (data != null ? _i83.MicroArea.fromJson(data) : null) as T; + if (t == _i1.getType<_i84.MicroArea?>()) { + return (data != null ? _i84.MicroArea.fromJson(data) : null) as T; } - if (t == _i1.getType<_i84.OtpChallenge?>()) { - return (data != null ? _i84.OtpChallenge.fromJson(data) : null) as T; + if (t == _i1.getType<_i85.OtpChallenge?>()) { + return (data != null ? _i85.OtpChallenge.fromJson(data) : null) as T; } - if (t == _i1.getType<_i85.Patient?>()) { - return (data != null ? _i85.Patient.fromJson(data) : null) as T; + if (t == _i1.getType<_i86.Patient?>()) { + return (data != null ? _i86.Patient.fromJson(data) : null) as T; } - if (t == _i1.getType<_i86.PushToken?>()) { - return (data != null ? _i86.PushToken.fromJson(data) : null) as T; + if (t == _i1.getType<_i87.PushToken?>()) { + return (data != null ? _i87.PushToken.fromJson(data) : null) as T; } - if (t == _i1.getType<_i87.ScheduledNotice?>()) { - return (data != null ? _i87.ScheduledNotice.fromJson(data) : null) as T; + if (t == _i1.getType<_i88.ScheduledNotice?>()) { + return (data != null ? _i88.ScheduledNotice.fromJson(data) : null) as T; } - if (t == _i1.getType<_i88.StaffAccount?>()) { - return (data != null ? _i88.StaffAccount.fromJson(data) : null) as T; + if (t == _i1.getType<_i89.StaffAccount?>()) { + return (data != null ? _i89.StaffAccount.fromJson(data) : null) as T; } - if (t == _i1.getType<_i89.TermsVersion?>()) { - return (data != null ? _i89.TermsVersion.fromJson(data) : null) as T; + if (t == _i1.getType<_i90.TermsVersion?>()) { + return (data != null ? _i90.TermsVersion.fromJson(data) : null) as T; } - if (t == _i1.getType<_i90.TriageAnswer?>()) { - return (data != null ? _i90.TriageAnswer.fromJson(data) : null) as T; + if (t == _i1.getType<_i91.TriageAnswer?>()) { + return (data != null ? _i91.TriageAnswer.fromJson(data) : null) as T; } - if (t == _i1.getType<_i91.TriageSession?>()) { - return (data != null ? _i91.TriageSession.fromJson(data) : null) as T; + if (t == _i1.getType<_i92.TriageSession?>()) { + return (data != null ? _i92.TriageSession.fromJson(data) : null) as T; } - if (t == _i1.getType<_i92.Ubs?>()) { - return (data != null ? _i92.Ubs.fromJson(data) : null) as T; + if (t == _i1.getType<_i93.Ubs?>()) { + return (data != null ? _i93.Ubs.fromJson(data) : null) as T; } - if (t == _i1.getType<_i93.User?>()) { - return (data != null ? _i93.User.fromJson(data) : null) as T; + if (t == _i1.getType<_i94.User?>()) { + return (data != null ? _i94.User.fromJson(data) : null) as T; } - if (t == _i1.getType<_i94.UserCredential?>()) { - return (data != null ? _i94.UserCredential.fromJson(data) : null) as T; + if (t == _i1.getType<_i95.UserCredential?>()) { + return (data != null ? _i95.UserCredential.fromJson(data) : null) as T; } - if (t == _i1.getType<_i95.Visit?>()) { - return (data != null ? _i95.Visit.fromJson(data) : null) as T; + if (t == _i1.getType<_i96.Visit?>()) { + return (data != null ? _i96.Visit.fromJson(data) : null) as T; } if (t == List<_i11.AdminAlert>) { return (data as List).map((e) => deserialize<_i11.AdminAlert>(e)).toList() @@ -892,27 +901,27 @@ class Protocol extends _i1.SerializationManager { if (t == List) { return (data as List).map((e) => deserialize(e)).toList() as T; } - if (t == List<_i34.NoticeHistoryEntry>) { + if (t == List<_i35.NoticeHistoryEntry>) { return (data as List) - .map((e) => deserialize<_i34.NoticeHistoryEntry>(e)) + .map((e) => deserialize<_i35.NoticeHistoryEntry>(e)) .toList() as T; } - if (t == List<_i37.PatientConsentRecord>) { + if (t == List<_i38.PatientConsentRecord>) { return (data as List) - .map((e) => deserialize<_i37.PatientConsentRecord>(e)) + .map((e) => deserialize<_i38.PatientConsentRecord>(e)) .toList() as T; } - if (t == List<_i40.PatientRiskEvent>) { + if (t == List<_i41.PatientRiskEvent>) { return (data as List) - .map((e) => deserialize<_i40.PatientRiskEvent>(e)) + .map((e) => deserialize<_i41.PatientRiskEvent>(e)) .toList() as T; } - if (t == List<_i39.PatientDataSubjectRequestRecord>) { + if (t == List<_i40.PatientDataSubjectRequestRecord>) { return (data as List) - .map((e) => deserialize<_i39.PatientDataSubjectRequestRecord>(e)) + .map((e) => deserialize<_i40.PatientDataSubjectRequestRecord>(e)) .toList() as T; } @@ -922,44 +931,44 @@ class Protocol extends _i1.SerializationManager { ) as T; } - if (t == List<_i96.AdminMicroArea>) { + if (t == List<_i97.AdminMicroArea>) { return (data as List) - .map((e) => deserialize<_i96.AdminMicroArea>(e)) + .map((e) => deserialize<_i97.AdminMicroArea>(e)) .toList() as T; } - if (t == List<_i97.AdminAcs>) { - return (data as List).map((e) => deserialize<_i97.AdminAcs>(e)).toList() + if (t == List<_i98.AdminAcs>) { + return (data as List).map((e) => deserialize<_i98.AdminAcs>(e)).toList() as T; } - if (t == List<_i98.AdminStaff>) { - return (data as List).map((e) => deserialize<_i98.AdminStaff>(e)).toList() + if (t == List<_i99.AdminStaff>) { + return (data as List).map((e) => deserialize<_i99.AdminStaff>(e)).toList() as T; } - if (t == List<_i99.TermsVersionEntry>) { + if (t == List<_i100.TermsVersionEntry>) { return (data as List) - .map((e) => deserialize<_i99.TermsVersionEntry>(e)) + .map((e) => deserialize<_i100.TermsVersionEntry>(e)) .toList() as T; } - if (t == List<_i100.MicroAreaPatient>) { + if (t == List<_i101.MicroAreaPatient>) { return (data as List) - .map((e) => deserialize<_i100.MicroAreaPatient>(e)) + .map((e) => deserialize<_i101.MicroAreaPatient>(e)) .toList() as T; } if (t == List) { return (data as List).map((e) => deserialize(e)).toList() as T; } - if (t == List<_i101.VisitSyncResult>) { + if (t == List<_i102.VisitSyncResult>) { return (data as List) - .map((e) => deserialize<_i101.VisitSyncResult>(e)) + .map((e) => deserialize<_i102.VisitSyncResult>(e)) .toList() as T; } - if (t == List<_i102.VisitSyncEntry>) { + if (t == List<_i103.VisitSyncEntry>) { return (data as List) - .map((e) => deserialize<_i102.VisitSyncEntry>(e)) + .map((e) => deserialize<_i103.VisitSyncEntry>(e)) .toList() as T; } @@ -995,74 +1004,75 @@ class Protocol extends _i1.SerializationManager { _i26.AdminStaff => 'AdminStaff', _i27.AdminStaffMfaResetResult => 'AdminStaffMfaResetResult', _i28.AlertAckResult => 'AlertAckResult', - _i29.AlertStatusResult => 'AlertStatusResult', - _i30.DevelopmentLoginResult => 'DevelopmentLoginResult', - _i31.EnrollmentResult => 'EnrollmentResult', - _i32.EnrollmentTokenResult => 'EnrollmentTokenResult', - _i33.MicroAreaPatient => 'MicroAreaPatient', - _i34.NoticeHistoryEntry => 'NoticeHistoryEntry', - _i35.NoticeHistoryPage => 'NoticeHistoryPage', - _i36.NoticeSendResult => 'NoticeSendResult', - _i37.PatientConsentRecord => 'PatientConsentRecord', - _i38.PatientDataOverview => 'PatientDataOverview', - _i39.PatientDataSubjectRequestRecord => 'PatientDataSubjectRequestRecord', - _i40.PatientRiskEvent => 'PatientRiskEvent', - _i41.RedAlertResult => 'RedAlertResult', - _i42.ServiceHealth => 'ServiceHealth', - _i43.TermsChangeNotice => 'TermsChangeNotice', - _i44.TermsVersionEntry => 'TermsVersionEntry', - _i45.TotpEnrollmentStart => 'TotpEnrollmentStart', - _i46.TriageResult => 'TriageResult', - _i47.UbsContact => 'UbsContact', - _i48.VisitSyncEntry => 'VisitSyncEntry', - _i49.VisitSyncResult => 'VisitSyncResult', - _i50.AuditChainCheckpoint => 'AuditChainCheckpoint', - _i51.AuditExport => 'AuditExport', - _i52.AuditLog => 'AuditLog', - _i53.ConsentLog => 'ConsentLog', - _i54.DataSubjectRequest => 'DataSubjectRequest', - _i55.EnrollmentToken => 'EnrollmentToken', - _i56.AlertStatus => 'AlertStatus', - _i57.ArrivalMethod => 'ArrivalMethod', - _i58.AuditChainReason => 'AuditChainReason', - _i59.AuditIntegrityState => 'AuditIntegrityState', - _i60.AuditRowSeal => 'AuditRowSeal', - _i61.ConsentPurpose => 'ConsentPurpose', - _i62.DataSubjectRequestStatus => 'DataSubjectRequestStatus', - _i63.DataSubjectRequestType => 'DataSubjectRequestType', - _i64.EscalationReason => 'EscalationReason', - _i65.RiskLevel => 'RiskLevel', - _i66.ScheduledNoticeStatus => 'ScheduledNoticeStatus', - _i67.SyncStatus => 'SyncStatus', - _i68.UserRole => 'UserRole', - _i69.VisitAuthorship => 'VisitAuthorship', - _i70.AdminInvalidRequestException => 'AdminInvalidRequestException', - _i71.AlertDispatchUnavailableException => + _i29.AlertResolveResult => 'AlertResolveResult', + _i30.AlertStatusResult => 'AlertStatusResult', + _i31.DevelopmentLoginResult => 'DevelopmentLoginResult', + _i32.EnrollmentResult => 'EnrollmentResult', + _i33.EnrollmentTokenResult => 'EnrollmentTokenResult', + _i34.MicroAreaPatient => 'MicroAreaPatient', + _i35.NoticeHistoryEntry => 'NoticeHistoryEntry', + _i36.NoticeHistoryPage => 'NoticeHistoryPage', + _i37.NoticeSendResult => 'NoticeSendResult', + _i38.PatientConsentRecord => 'PatientConsentRecord', + _i39.PatientDataOverview => 'PatientDataOverview', + _i40.PatientDataSubjectRequestRecord => 'PatientDataSubjectRequestRecord', + _i41.PatientRiskEvent => 'PatientRiskEvent', + _i42.RedAlertResult => 'RedAlertResult', + _i43.ServiceHealth => 'ServiceHealth', + _i44.TermsChangeNotice => 'TermsChangeNotice', + _i45.TermsVersionEntry => 'TermsVersionEntry', + _i46.TotpEnrollmentStart => 'TotpEnrollmentStart', + _i47.TriageResult => 'TriageResult', + _i48.UbsContact => 'UbsContact', + _i49.VisitSyncEntry => 'VisitSyncEntry', + _i50.VisitSyncResult => 'VisitSyncResult', + _i51.AuditChainCheckpoint => 'AuditChainCheckpoint', + _i52.AuditExport => 'AuditExport', + _i53.AuditLog => 'AuditLog', + _i54.ConsentLog => 'ConsentLog', + _i55.DataSubjectRequest => 'DataSubjectRequest', + _i56.EnrollmentToken => 'EnrollmentToken', + _i57.AlertStatus => 'AlertStatus', + _i58.ArrivalMethod => 'ArrivalMethod', + _i59.AuditChainReason => 'AuditChainReason', + _i60.AuditIntegrityState => 'AuditIntegrityState', + _i61.AuditRowSeal => 'AuditRowSeal', + _i62.ConsentPurpose => 'ConsentPurpose', + _i63.DataSubjectRequestStatus => 'DataSubjectRequestStatus', + _i64.DataSubjectRequestType => 'DataSubjectRequestType', + _i65.EscalationReason => 'EscalationReason', + _i66.RiskLevel => 'RiskLevel', + _i67.ScheduledNoticeStatus => 'ScheduledNoticeStatus', + _i68.SyncStatus => 'SyncStatus', + _i69.UserRole => 'UserRole', + _i70.VisitAuthorship => 'VisitAuthorship', + _i71.AdminInvalidRequestException => 'AdminInvalidRequestException', + _i72.AlertDispatchUnavailableException => 'AlertDispatchUnavailableException', - _i72.AlertPermissionException => 'AlertPermissionException', - _i73.AlertValidationException => 'AlertValidationException', - _i74.AuthenticationFailedException => 'AuthenticationFailedException', - _i75.DataRightsException => 'DataRightsException', - _i76.EndpointDisabledException => 'EndpointDisabledException', - _i77.EnrollmentException => 'EnrollmentException', - _i78.MfaEnrollmentRequiredException => 'MfaEnrollmentRequiredException', - _i79.MfaRequiredException => 'MfaRequiredException', - _i80.NoticeDeliveryException => 'NoticeDeliveryException', - _i81.OtpRequestException => 'OtpRequestException', - _i82.SessionExpiredException => 'SessionExpiredException', - _i83.MicroArea => 'MicroArea', - _i84.OtpChallenge => 'OtpChallenge', - _i85.Patient => 'Patient', - _i86.PushToken => 'PushToken', - _i87.ScheduledNotice => 'ScheduledNotice', - _i88.StaffAccount => 'StaffAccount', - _i89.TermsVersion => 'TermsVersion', - _i90.TriageAnswer => 'TriageAnswer', - _i91.TriageSession => 'TriageSession', - _i92.Ubs => 'Ubs', - _i93.User => 'User', - _i94.UserCredential => 'UserCredential', - _i95.Visit => 'Visit', + _i73.AlertPermissionException => 'AlertPermissionException', + _i74.AlertValidationException => 'AlertValidationException', + _i75.AuthenticationFailedException => 'AuthenticationFailedException', + _i76.DataRightsException => 'DataRightsException', + _i77.EndpointDisabledException => 'EndpointDisabledException', + _i78.EnrollmentException => 'EnrollmentException', + _i79.MfaEnrollmentRequiredException => 'MfaEnrollmentRequiredException', + _i80.MfaRequiredException => 'MfaRequiredException', + _i81.NoticeDeliveryException => 'NoticeDeliveryException', + _i82.OtpRequestException => 'OtpRequestException', + _i83.SessionExpiredException => 'SessionExpiredException', + _i84.MicroArea => 'MicroArea', + _i85.OtpChallenge => 'OtpChallenge', + _i86.Patient => 'Patient', + _i87.PushToken => 'PushToken', + _i88.ScheduledNotice => 'ScheduledNotice', + _i89.StaffAccount => 'StaffAccount', + _i90.TermsVersion => 'TermsVersion', + _i91.TriageAnswer => 'TriageAnswer', + _i92.TriageSession => 'TriageSession', + _i93.Ubs => 'Ubs', + _i94.User => 'User', + _i95.UserCredential => 'UserCredential', + _i96.Visit => 'Visit', _ => null, }; } @@ -1131,139 +1141,141 @@ class Protocol extends _i1.SerializationManager { return 'AdminStaffMfaResetResult'; case _i28.AlertAckResult(): return 'AlertAckResult'; - case _i29.AlertStatusResult(): + case _i29.AlertResolveResult(): + return 'AlertResolveResult'; + case _i30.AlertStatusResult(): return 'AlertStatusResult'; - case _i30.DevelopmentLoginResult(): + case _i31.DevelopmentLoginResult(): return 'DevelopmentLoginResult'; - case _i31.EnrollmentResult(): + case _i32.EnrollmentResult(): return 'EnrollmentResult'; - case _i32.EnrollmentTokenResult(): + case _i33.EnrollmentTokenResult(): return 'EnrollmentTokenResult'; - case _i33.MicroAreaPatient(): + case _i34.MicroAreaPatient(): return 'MicroAreaPatient'; - case _i34.NoticeHistoryEntry(): + case _i35.NoticeHistoryEntry(): return 'NoticeHistoryEntry'; - case _i35.NoticeHistoryPage(): + case _i36.NoticeHistoryPage(): return 'NoticeHistoryPage'; - case _i36.NoticeSendResult(): + case _i37.NoticeSendResult(): return 'NoticeSendResult'; - case _i37.PatientConsentRecord(): + case _i38.PatientConsentRecord(): return 'PatientConsentRecord'; - case _i38.PatientDataOverview(): + case _i39.PatientDataOverview(): return 'PatientDataOverview'; - case _i39.PatientDataSubjectRequestRecord(): + case _i40.PatientDataSubjectRequestRecord(): return 'PatientDataSubjectRequestRecord'; - case _i40.PatientRiskEvent(): + case _i41.PatientRiskEvent(): return 'PatientRiskEvent'; - case _i41.RedAlertResult(): + case _i42.RedAlertResult(): return 'RedAlertResult'; - case _i42.ServiceHealth(): + case _i43.ServiceHealth(): return 'ServiceHealth'; - case _i43.TermsChangeNotice(): + case _i44.TermsChangeNotice(): return 'TermsChangeNotice'; - case _i44.TermsVersionEntry(): + case _i45.TermsVersionEntry(): return 'TermsVersionEntry'; - case _i45.TotpEnrollmentStart(): + case _i46.TotpEnrollmentStart(): return 'TotpEnrollmentStart'; - case _i46.TriageResult(): + case _i47.TriageResult(): return 'TriageResult'; - case _i47.UbsContact(): + case _i48.UbsContact(): return 'UbsContact'; - case _i48.VisitSyncEntry(): + case _i49.VisitSyncEntry(): return 'VisitSyncEntry'; - case _i49.VisitSyncResult(): + case _i50.VisitSyncResult(): return 'VisitSyncResult'; - case _i50.AuditChainCheckpoint(): + case _i51.AuditChainCheckpoint(): return 'AuditChainCheckpoint'; - case _i51.AuditExport(): + case _i52.AuditExport(): return 'AuditExport'; - case _i52.AuditLog(): + case _i53.AuditLog(): return 'AuditLog'; - case _i53.ConsentLog(): + case _i54.ConsentLog(): return 'ConsentLog'; - case _i54.DataSubjectRequest(): + case _i55.DataSubjectRequest(): return 'DataSubjectRequest'; - case _i55.EnrollmentToken(): + case _i56.EnrollmentToken(): return 'EnrollmentToken'; - case _i56.AlertStatus(): + case _i57.AlertStatus(): return 'AlertStatus'; - case _i57.ArrivalMethod(): + case _i58.ArrivalMethod(): return 'ArrivalMethod'; - case _i58.AuditChainReason(): + case _i59.AuditChainReason(): return 'AuditChainReason'; - case _i59.AuditIntegrityState(): + case _i60.AuditIntegrityState(): return 'AuditIntegrityState'; - case _i60.AuditRowSeal(): + case _i61.AuditRowSeal(): return 'AuditRowSeal'; - case _i61.ConsentPurpose(): + case _i62.ConsentPurpose(): return 'ConsentPurpose'; - case _i62.DataSubjectRequestStatus(): + case _i63.DataSubjectRequestStatus(): return 'DataSubjectRequestStatus'; - case _i63.DataSubjectRequestType(): + case _i64.DataSubjectRequestType(): return 'DataSubjectRequestType'; - case _i64.EscalationReason(): + case _i65.EscalationReason(): return 'EscalationReason'; - case _i65.RiskLevel(): + case _i66.RiskLevel(): return 'RiskLevel'; - case _i66.ScheduledNoticeStatus(): + case _i67.ScheduledNoticeStatus(): return 'ScheduledNoticeStatus'; - case _i67.SyncStatus(): + case _i68.SyncStatus(): return 'SyncStatus'; - case _i68.UserRole(): + case _i69.UserRole(): return 'UserRole'; - case _i69.VisitAuthorship(): + case _i70.VisitAuthorship(): return 'VisitAuthorship'; - case _i70.AdminInvalidRequestException(): + case _i71.AdminInvalidRequestException(): return 'AdminInvalidRequestException'; - case _i71.AlertDispatchUnavailableException(): + case _i72.AlertDispatchUnavailableException(): return 'AlertDispatchUnavailableException'; - case _i72.AlertPermissionException(): + case _i73.AlertPermissionException(): return 'AlertPermissionException'; - case _i73.AlertValidationException(): + case _i74.AlertValidationException(): return 'AlertValidationException'; - case _i74.AuthenticationFailedException(): + case _i75.AuthenticationFailedException(): return 'AuthenticationFailedException'; - case _i75.DataRightsException(): + case _i76.DataRightsException(): return 'DataRightsException'; - case _i76.EndpointDisabledException(): + case _i77.EndpointDisabledException(): return 'EndpointDisabledException'; - case _i77.EnrollmentException(): + case _i78.EnrollmentException(): return 'EnrollmentException'; - case _i78.MfaEnrollmentRequiredException(): + case _i79.MfaEnrollmentRequiredException(): return 'MfaEnrollmentRequiredException'; - case _i79.MfaRequiredException(): + case _i80.MfaRequiredException(): return 'MfaRequiredException'; - case _i80.NoticeDeliveryException(): + case _i81.NoticeDeliveryException(): return 'NoticeDeliveryException'; - case _i81.OtpRequestException(): + case _i82.OtpRequestException(): return 'OtpRequestException'; - case _i82.SessionExpiredException(): + case _i83.SessionExpiredException(): return 'SessionExpiredException'; - case _i83.MicroArea(): + case _i84.MicroArea(): return 'MicroArea'; - case _i84.OtpChallenge(): + case _i85.OtpChallenge(): return 'OtpChallenge'; - case _i85.Patient(): + case _i86.Patient(): return 'Patient'; - case _i86.PushToken(): + case _i87.PushToken(): return 'PushToken'; - case _i87.ScheduledNotice(): + case _i88.ScheduledNotice(): return 'ScheduledNotice'; - case _i88.StaffAccount(): + case _i89.StaffAccount(): return 'StaffAccount'; - case _i89.TermsVersion(): + case _i90.TermsVersion(): return 'TermsVersion'; - case _i90.TriageAnswer(): + case _i91.TriageAnswer(): return 'TriageAnswer'; - case _i91.TriageSession(): + case _i92.TriageSession(): return 'TriageSession'; - case _i92.Ubs(): + case _i93.Ubs(): return 'Ubs'; - case _i93.User(): + case _i94.User(): return 'User'; - case _i94.UserCredential(): + case _i95.UserCredential(): return 'UserCredential'; - case _i95.Visit(): + case _i96.Visit(): return 'Visit'; } return null; @@ -1356,206 +1368,209 @@ class Protocol extends _i1.SerializationManager { if (dataClassName == 'AlertAckResult') { return deserialize<_i28.AlertAckResult>(data['data']); } + if (dataClassName == 'AlertResolveResult') { + return deserialize<_i29.AlertResolveResult>(data['data']); + } if (dataClassName == 'AlertStatusResult') { - return deserialize<_i29.AlertStatusResult>(data['data']); + return deserialize<_i30.AlertStatusResult>(data['data']); } if (dataClassName == 'DevelopmentLoginResult') { - return deserialize<_i30.DevelopmentLoginResult>(data['data']); + return deserialize<_i31.DevelopmentLoginResult>(data['data']); } if (dataClassName == 'EnrollmentResult') { - return deserialize<_i31.EnrollmentResult>(data['data']); + return deserialize<_i32.EnrollmentResult>(data['data']); } if (dataClassName == 'EnrollmentTokenResult') { - return deserialize<_i32.EnrollmentTokenResult>(data['data']); + return deserialize<_i33.EnrollmentTokenResult>(data['data']); } if (dataClassName == 'MicroAreaPatient') { - return deserialize<_i33.MicroAreaPatient>(data['data']); + return deserialize<_i34.MicroAreaPatient>(data['data']); } if (dataClassName == 'NoticeHistoryEntry') { - return deserialize<_i34.NoticeHistoryEntry>(data['data']); + return deserialize<_i35.NoticeHistoryEntry>(data['data']); } if (dataClassName == 'NoticeHistoryPage') { - return deserialize<_i35.NoticeHistoryPage>(data['data']); + return deserialize<_i36.NoticeHistoryPage>(data['data']); } if (dataClassName == 'NoticeSendResult') { - return deserialize<_i36.NoticeSendResult>(data['data']); + return deserialize<_i37.NoticeSendResult>(data['data']); } if (dataClassName == 'PatientConsentRecord') { - return deserialize<_i37.PatientConsentRecord>(data['data']); + return deserialize<_i38.PatientConsentRecord>(data['data']); } if (dataClassName == 'PatientDataOverview') { - return deserialize<_i38.PatientDataOverview>(data['data']); + return deserialize<_i39.PatientDataOverview>(data['data']); } if (dataClassName == 'PatientDataSubjectRequestRecord') { - return deserialize<_i39.PatientDataSubjectRequestRecord>(data['data']); + return deserialize<_i40.PatientDataSubjectRequestRecord>(data['data']); } if (dataClassName == 'PatientRiskEvent') { - return deserialize<_i40.PatientRiskEvent>(data['data']); + return deserialize<_i41.PatientRiskEvent>(data['data']); } if (dataClassName == 'RedAlertResult') { - return deserialize<_i41.RedAlertResult>(data['data']); + return deserialize<_i42.RedAlertResult>(data['data']); } if (dataClassName == 'ServiceHealth') { - return deserialize<_i42.ServiceHealth>(data['data']); + return deserialize<_i43.ServiceHealth>(data['data']); } if (dataClassName == 'TermsChangeNotice') { - return deserialize<_i43.TermsChangeNotice>(data['data']); + return deserialize<_i44.TermsChangeNotice>(data['data']); } if (dataClassName == 'TermsVersionEntry') { - return deserialize<_i44.TermsVersionEntry>(data['data']); + return deserialize<_i45.TermsVersionEntry>(data['data']); } if (dataClassName == 'TotpEnrollmentStart') { - return deserialize<_i45.TotpEnrollmentStart>(data['data']); + return deserialize<_i46.TotpEnrollmentStart>(data['data']); } if (dataClassName == 'TriageResult') { - return deserialize<_i46.TriageResult>(data['data']); + return deserialize<_i47.TriageResult>(data['data']); } if (dataClassName == 'UbsContact') { - return deserialize<_i47.UbsContact>(data['data']); + return deserialize<_i48.UbsContact>(data['data']); } if (dataClassName == 'VisitSyncEntry') { - return deserialize<_i48.VisitSyncEntry>(data['data']); + return deserialize<_i49.VisitSyncEntry>(data['data']); } if (dataClassName == 'VisitSyncResult') { - return deserialize<_i49.VisitSyncResult>(data['data']); + return deserialize<_i50.VisitSyncResult>(data['data']); } if (dataClassName == 'AuditChainCheckpoint') { - return deserialize<_i50.AuditChainCheckpoint>(data['data']); + return deserialize<_i51.AuditChainCheckpoint>(data['data']); } if (dataClassName == 'AuditExport') { - return deserialize<_i51.AuditExport>(data['data']); + return deserialize<_i52.AuditExport>(data['data']); } if (dataClassName == 'AuditLog') { - return deserialize<_i52.AuditLog>(data['data']); + return deserialize<_i53.AuditLog>(data['data']); } if (dataClassName == 'ConsentLog') { - return deserialize<_i53.ConsentLog>(data['data']); + return deserialize<_i54.ConsentLog>(data['data']); } if (dataClassName == 'DataSubjectRequest') { - return deserialize<_i54.DataSubjectRequest>(data['data']); + return deserialize<_i55.DataSubjectRequest>(data['data']); } if (dataClassName == 'EnrollmentToken') { - return deserialize<_i55.EnrollmentToken>(data['data']); + return deserialize<_i56.EnrollmentToken>(data['data']); } if (dataClassName == 'AlertStatus') { - return deserialize<_i56.AlertStatus>(data['data']); + return deserialize<_i57.AlertStatus>(data['data']); } if (dataClassName == 'ArrivalMethod') { - return deserialize<_i57.ArrivalMethod>(data['data']); + return deserialize<_i58.ArrivalMethod>(data['data']); } if (dataClassName == 'AuditChainReason') { - return deserialize<_i58.AuditChainReason>(data['data']); + return deserialize<_i59.AuditChainReason>(data['data']); } if (dataClassName == 'AuditIntegrityState') { - return deserialize<_i59.AuditIntegrityState>(data['data']); + return deserialize<_i60.AuditIntegrityState>(data['data']); } if (dataClassName == 'AuditRowSeal') { - return deserialize<_i60.AuditRowSeal>(data['data']); + return deserialize<_i61.AuditRowSeal>(data['data']); } if (dataClassName == 'ConsentPurpose') { - return deserialize<_i61.ConsentPurpose>(data['data']); + return deserialize<_i62.ConsentPurpose>(data['data']); } if (dataClassName == 'DataSubjectRequestStatus') { - return deserialize<_i62.DataSubjectRequestStatus>(data['data']); + return deserialize<_i63.DataSubjectRequestStatus>(data['data']); } if (dataClassName == 'DataSubjectRequestType') { - return deserialize<_i63.DataSubjectRequestType>(data['data']); + return deserialize<_i64.DataSubjectRequestType>(data['data']); } if (dataClassName == 'EscalationReason') { - return deserialize<_i64.EscalationReason>(data['data']); + return deserialize<_i65.EscalationReason>(data['data']); } if (dataClassName == 'RiskLevel') { - return deserialize<_i65.RiskLevel>(data['data']); + return deserialize<_i66.RiskLevel>(data['data']); } if (dataClassName == 'ScheduledNoticeStatus') { - return deserialize<_i66.ScheduledNoticeStatus>(data['data']); + return deserialize<_i67.ScheduledNoticeStatus>(data['data']); } if (dataClassName == 'SyncStatus') { - return deserialize<_i67.SyncStatus>(data['data']); + return deserialize<_i68.SyncStatus>(data['data']); } if (dataClassName == 'UserRole') { - return deserialize<_i68.UserRole>(data['data']); + return deserialize<_i69.UserRole>(data['data']); } if (dataClassName == 'VisitAuthorship') { - return deserialize<_i69.VisitAuthorship>(data['data']); + return deserialize<_i70.VisitAuthorship>(data['data']); } if (dataClassName == 'AdminInvalidRequestException') { - return deserialize<_i70.AdminInvalidRequestException>(data['data']); + return deserialize<_i71.AdminInvalidRequestException>(data['data']); } if (dataClassName == 'AlertDispatchUnavailableException') { - return deserialize<_i71.AlertDispatchUnavailableException>(data['data']); + return deserialize<_i72.AlertDispatchUnavailableException>(data['data']); } if (dataClassName == 'AlertPermissionException') { - return deserialize<_i72.AlertPermissionException>(data['data']); + return deserialize<_i73.AlertPermissionException>(data['data']); } if (dataClassName == 'AlertValidationException') { - return deserialize<_i73.AlertValidationException>(data['data']); + return deserialize<_i74.AlertValidationException>(data['data']); } if (dataClassName == 'AuthenticationFailedException') { - return deserialize<_i74.AuthenticationFailedException>(data['data']); + return deserialize<_i75.AuthenticationFailedException>(data['data']); } if (dataClassName == 'DataRightsException') { - return deserialize<_i75.DataRightsException>(data['data']); + return deserialize<_i76.DataRightsException>(data['data']); } if (dataClassName == 'EndpointDisabledException') { - return deserialize<_i76.EndpointDisabledException>(data['data']); + return deserialize<_i77.EndpointDisabledException>(data['data']); } if (dataClassName == 'EnrollmentException') { - return deserialize<_i77.EnrollmentException>(data['data']); + return deserialize<_i78.EnrollmentException>(data['data']); } if (dataClassName == 'MfaEnrollmentRequiredException') { - return deserialize<_i78.MfaEnrollmentRequiredException>(data['data']); + return deserialize<_i79.MfaEnrollmentRequiredException>(data['data']); } if (dataClassName == 'MfaRequiredException') { - return deserialize<_i79.MfaRequiredException>(data['data']); + return deserialize<_i80.MfaRequiredException>(data['data']); } if (dataClassName == 'NoticeDeliveryException') { - return deserialize<_i80.NoticeDeliveryException>(data['data']); + return deserialize<_i81.NoticeDeliveryException>(data['data']); } if (dataClassName == 'OtpRequestException') { - return deserialize<_i81.OtpRequestException>(data['data']); + return deserialize<_i82.OtpRequestException>(data['data']); } if (dataClassName == 'SessionExpiredException') { - return deserialize<_i82.SessionExpiredException>(data['data']); + return deserialize<_i83.SessionExpiredException>(data['data']); } if (dataClassName == 'MicroArea') { - return deserialize<_i83.MicroArea>(data['data']); + return deserialize<_i84.MicroArea>(data['data']); } if (dataClassName == 'OtpChallenge') { - return deserialize<_i84.OtpChallenge>(data['data']); + return deserialize<_i85.OtpChallenge>(data['data']); } if (dataClassName == 'Patient') { - return deserialize<_i85.Patient>(data['data']); + return deserialize<_i86.Patient>(data['data']); } if (dataClassName == 'PushToken') { - return deserialize<_i86.PushToken>(data['data']); + return deserialize<_i87.PushToken>(data['data']); } if (dataClassName == 'ScheduledNotice') { - return deserialize<_i87.ScheduledNotice>(data['data']); + return deserialize<_i88.ScheduledNotice>(data['data']); } if (dataClassName == 'StaffAccount') { - return deserialize<_i88.StaffAccount>(data['data']); + return deserialize<_i89.StaffAccount>(data['data']); } if (dataClassName == 'TermsVersion') { - return deserialize<_i89.TermsVersion>(data['data']); + return deserialize<_i90.TermsVersion>(data['data']); } if (dataClassName == 'TriageAnswer') { - return deserialize<_i90.TriageAnswer>(data['data']); + return deserialize<_i91.TriageAnswer>(data['data']); } if (dataClassName == 'TriageSession') { - return deserialize<_i91.TriageSession>(data['data']); + return deserialize<_i92.TriageSession>(data['data']); } if (dataClassName == 'Ubs') { - return deserialize<_i92.Ubs>(data['data']); + return deserialize<_i93.Ubs>(data['data']); } if (dataClassName == 'User') { - return deserialize<_i93.User>(data['data']); + return deserialize<_i94.User>(data['data']); } if (dataClassName == 'UserCredential') { - return deserialize<_i94.UserCredential>(data['data']); + return deserialize<_i95.UserCredential>(data['data']); } if (dataClassName == 'Visit') { - return deserialize<_i95.Visit>(data['data']); + return deserialize<_i96.Visit>(data['data']); } return super.deserializeByClassName(data); } diff --git a/backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart b/backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart index 0d51f926..23372c11 100644 --- a/backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart +++ b/backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart @@ -1,4 +1,5 @@ import 'package:serverpod/serverpod.dart'; +import 'package:sinalacs_server/src/application/alerts/red_alert_service.dart'; import 'package:sinalacs_server/src/endpoints/authenticated_endpoint.dart'; import 'package:sinalacs_server/src/generated/protocol.dart'; import 'package:sinalacs_server/src/runtime/alert_runtime.dart'; @@ -88,6 +89,35 @@ class AlertsEndpoint extends AuthenticatedEndpoint { } } + /// Conclui o atendimento de um alerta já confirmado (#76, RF05): é o que + /// faz o paciente ver "Atendido". Alerta inexistente ou de outra microárea + /// devolve `resolved: false`, como o ACK; alerta ainda não confirmado é + /// [AlertValidationException] com o texto de + /// `RedAlertService.notAcknowledgedMessage`. + Future resolve( + Session session, { + required String accessToken, + required String alertId, + }) async { + final user = authenticate(accessToken); + final service = AlertRuntime.instance.serviceFor(session); + + try { + final attempt = await service.resolve(user: user, alertId: alertId); + final found = attempt.outcome != AlertResolveOutcome.notFound; + return AlertResolveResult( + alertId: alertId, + resolved: found, + status: found ? AlertStatus.resolved : null, + resolvedAt: attempt.resolvedAt, + ); + } on ArgumentError catch (error) { + throw AlertValidationException(message: '${error.message}'); + } on StateError catch (error) { + throw AlertPermissionException(message: error.message); + } + } + /// Status do alerta mais recente do PRÓPRIO paciente (RF05, decisão §5). /// `patientId` nunca é parâmetro — vem do token (INV-05). Future statusFor( @@ -108,6 +138,7 @@ class AlertsEndpoint extends AuthenticatedEndpoint { status: snapshot.status, triggeredAt: snapshot.triggeredAt, acknowledgedAt: snapshot.acknowledgedAt, + resolvedAt: snapshot.resolvedAt, ); } on StateError catch (error) { throw AlertPermissionException(message: error.message); diff --git a/backend/sinalacs_server/lib/src/generated/alert.dart b/backend/sinalacs_server/lib/src/generated/alert.dart index 0ec43dd8..f0308f91 100644 --- a/backend/sinalacs_server/lib/src/generated/alert.dart +++ b/backend/sinalacs_server/lib/src/generated/alert.dart @@ -32,6 +32,7 @@ abstract class Alert this.receivedAt, this.respondedAt, this.acknowledgedAt, + this.resolvedAt, required this.riskLevel, required this.locationHash, this.locationCell, @@ -54,6 +55,7 @@ abstract class Alert DateTime? receivedAt, DateTime? respondedAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, required _i2.RiskLevel riskLevel, required String locationHash, String? locationCell, @@ -99,6 +101,9 @@ abstract class Alert : _i1.DateTimeJsonExtension.fromJson( jsonSerialization['acknowledgedAt'], ), + resolvedAt: jsonSerialization['resolvedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson(jsonSerialization['resolvedAt']), riskLevel: _i2.RiskLevel.fromJson( (jsonSerialization['riskLevel'] as String), ), @@ -148,6 +153,10 @@ abstract class Alert DateTime? acknowledgedAt; + /// Atendimento concluído pelo ACS (#76): carimbado uma única vez, na + /// transição acknowledged → resolved. Nulo enquanto não foi atendido. + DateTime? resolvedAt; + _i2.RiskLevel riskLevel; String locationHash; @@ -188,6 +197,7 @@ abstract class Alert DateTime? receivedAt, DateTime? respondedAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, _i2.RiskLevel? riskLevel, String? locationHash, String? locationCell, @@ -212,6 +222,7 @@ abstract class Alert if (receivedAt != null) 'receivedAt': receivedAt?.toJson(), if (respondedAt != null) 'respondedAt': respondedAt?.toJson(), if (acknowledgedAt != null) 'acknowledgedAt': acknowledgedAt?.toJson(), + if (resolvedAt != null) 'resolvedAt': resolvedAt?.toJson(), 'riskLevel': riskLevel.toJson(), 'locationHash': locationHash, if (locationCell != null) 'locationCell': locationCell, @@ -239,6 +250,7 @@ abstract class Alert if (receivedAt != null) 'receivedAt': receivedAt?.toJson(), if (respondedAt != null) 'respondedAt': respondedAt?.toJson(), if (acknowledgedAt != null) 'acknowledgedAt': acknowledgedAt?.toJson(), + if (resolvedAt != null) 'resolvedAt': resolvedAt?.toJson(), 'riskLevel': riskLevel.toJson(), 'locationHash': locationHash, if (locationCell != null) 'locationCell': locationCell, @@ -296,6 +308,7 @@ class _AlertImpl extends Alert { DateTime? receivedAt, DateTime? respondedAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, required _i2.RiskLevel riskLevel, required String locationHash, String? locationCell, @@ -316,6 +329,7 @@ class _AlertImpl extends Alert { receivedAt: receivedAt, respondedAt: respondedAt, acknowledgedAt: acknowledgedAt, + resolvedAt: resolvedAt, riskLevel: riskLevel, locationHash: locationHash, locationCell: locationCell, @@ -342,6 +356,7 @@ class _AlertImpl extends Alert { Object? receivedAt = _Undefined, Object? respondedAt = _Undefined, Object? acknowledgedAt = _Undefined, + Object? resolvedAt = _Undefined, _i2.RiskLevel? riskLevel, String? locationHash, Object? locationCell = _Undefined, @@ -367,6 +382,7 @@ class _AlertImpl extends Alert { acknowledgedAt: acknowledgedAt is DateTime? ? acknowledgedAt : this.acknowledgedAt, + resolvedAt: resolvedAt is DateTime? ? resolvedAt : this.resolvedAt, riskLevel: riskLevel ?? this.riskLevel, locationHash: locationHash ?? this.locationHash, locationCell: locationCell is String? ? locationCell : this.locationCell, @@ -433,6 +449,12 @@ class AlertUpdateTable extends _i1.UpdateTable { value, ); + _i1.ColumnValue resolvedAt(DateTime? value) => + _i1.ColumnValue( + table.resolvedAt, + value, + ); + _i1.ColumnValue<_i2.RiskLevel, _i2.RiskLevel> riskLevel( _i2.RiskLevel value, ) => _i1.ColumnValue( @@ -530,6 +552,10 @@ class AlertTable extends _i1.Table<_i1.UuidValue?> { 'acknowledgedAt', this, ); + resolvedAt = _i1.ColumnDateTime( + 'resolvedAt', + this, + ); riskLevel = _i1.ColumnEnum( 'riskLevel', this, @@ -595,6 +621,10 @@ class AlertTable extends _i1.Table<_i1.UuidValue?> { late final _i1.ColumnDateTime acknowledgedAt; + /// Atendimento concluído pelo ACS (#76): carimbado uma única vez, na + /// transição acknowledged → resolved. Nulo enquanto não foi atendido. + late final _i1.ColumnDateTime resolvedAt; + late final _i1.ColumnEnum<_i2.RiskLevel> riskLevel; late final _i1.ColumnString locationHash; @@ -630,6 +660,7 @@ class AlertTable extends _i1.Table<_i1.UuidValue?> { receivedAt, respondedAt, acknowledgedAt, + resolvedAt, riskLevel, locationHash, locationCell, diff --git a/backend/sinalacs_server/lib/src/generated/api/alert_resolve_result.dart b/backend/sinalacs_server/lib/src/generated/api/alert_resolve_result.dart new file mode 100644 index 00000000..7b48f768 --- /dev/null +++ b/backend/sinalacs_server/lib/src/generated/api/alert_resolve_result.dart @@ -0,0 +1,128 @@ +/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */ +/* To generate run: "serverpod generate" */ + +// ignore_for_file: implementation_imports +// ignore_for_file: library_private_types_in_public_api +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: public_member_api_docs +// ignore_for_file: type_literal_in_constant_pattern +// ignore_for_file: use_super_parameters +// ignore_for_file: invalid_use_of_internal_member + +// ignore_for_file: no_leading_underscores_for_library_prefixes + +import 'package:serverpod/serverpod.dart' as _i1; +import '../enums/alert_status.dart' as _i2; + +/// Resultado de alerts.resolve (#76). +/// +/// Mesmo desenho de AlertAckResult: resolved: false significa que não havia +/// alerta com esse id na microárea do ACS. Concluir duas vezes devolve +/// resolved: true com o resolvedAt da primeira conclusão. +abstract class AlertResolveResult + implements _i1.SerializableModel, _i1.ProtocolSerialization { + AlertResolveResult._({ + required this.alertId, + required this.resolved, + this.status, + this.resolvedAt, + }); + + factory AlertResolveResult({ + required String alertId, + required bool resolved, + _i2.AlertStatus? status, + DateTime? resolvedAt, + }) = _AlertResolveResultImpl; + + factory AlertResolveResult.fromJson(Map jsonSerialization) { + return AlertResolveResult( + alertId: jsonSerialization['alertId'] as String, + resolved: _i1.BoolJsonExtension.fromJson(jsonSerialization['resolved']), + status: jsonSerialization['status'] == null + ? null + : _i2.AlertStatus.fromJson((jsonSerialization['status'] as String)), + resolvedAt: jsonSerialization['resolvedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson(jsonSerialization['resolvedAt']), + ); + } + + String alertId; + + bool resolved; + + _i2.AlertStatus? status; + + DateTime? resolvedAt; + + /// Returns a shallow copy of this [AlertResolveResult] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + AlertResolveResult copyWith({ + String? alertId, + bool? resolved, + _i2.AlertStatus? status, + DateTime? resolvedAt, + }); + @override + Map toJson() { + return { + '__className__': 'AlertResolveResult', + 'alertId': alertId, + 'resolved': resolved, + if (status != null) 'status': status?.toJson(), + if (resolvedAt != null) 'resolvedAt': resolvedAt?.toJson(), + }; + } + + @override + Map toJsonForProtocol() { + return { + '__className__': 'AlertResolveResult', + 'alertId': alertId, + 'resolved': resolved, + if (status != null) 'status': status?.toJson(), + if (resolvedAt != null) 'resolvedAt': resolvedAt?.toJson(), + }; + } + + @override + String toString() { + return _i1.SerializationManager.encode(this); + } +} + +class _Undefined {} + +class _AlertResolveResultImpl extends AlertResolveResult { + _AlertResolveResultImpl({ + required String alertId, + required bool resolved, + _i2.AlertStatus? status, + DateTime? resolvedAt, + }) : super._( + alertId: alertId, + resolved: resolved, + status: status, + resolvedAt: resolvedAt, + ); + + /// Returns a shallow copy of this [AlertResolveResult] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + @override + AlertResolveResult copyWith({ + String? alertId, + bool? resolved, + Object? status = _Undefined, + Object? resolvedAt = _Undefined, + }) { + return AlertResolveResult( + alertId: alertId ?? this.alertId, + resolved: resolved ?? this.resolved, + status: status is _i2.AlertStatus? ? status : this.status, + resolvedAt: resolvedAt is DateTime? ? resolvedAt : this.resolvedAt, + ); + } +} diff --git a/backend/sinalacs_server/lib/src/generated/api/alert_status_result.dart b/backend/sinalacs_server/lib/src/generated/api/alert_status_result.dart index 6e306e38..8da03dc3 100644 --- a/backend/sinalacs_server/lib/src/generated/api/alert_status_result.dart +++ b/backend/sinalacs_server/lib/src/generated/api/alert_status_result.dart @@ -29,6 +29,7 @@ abstract class AlertStatusResult this.status, this.triggeredAt, this.acknowledgedAt, + this.resolvedAt, }); factory AlertStatusResult({ @@ -38,6 +39,7 @@ abstract class AlertStatusResult _i3.AlertStatus? status, DateTime? triggeredAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, }) = _AlertStatusResultImpl; factory AlertStatusResult.fromJson(Map jsonSerialization) { @@ -60,6 +62,9 @@ abstract class AlertStatusResult : _i1.DateTimeJsonExtension.fromJson( jsonSerialization['acknowledgedAt'], ), + resolvedAt: jsonSerialization['resolvedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson(jsonSerialization['resolvedAt']), ); } @@ -75,6 +80,8 @@ abstract class AlertStatusResult DateTime? acknowledgedAt; + DateTime? resolvedAt; + /// Returns a shallow copy of this [AlertStatusResult] /// with some or all fields replaced by the given arguments. @_i1.useResult @@ -85,6 +92,7 @@ abstract class AlertStatusResult _i3.AlertStatus? status, DateTime? triggeredAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, }); @override Map toJson() { @@ -96,6 +104,7 @@ abstract class AlertStatusResult if (status != null) 'status': status?.toJson(), if (triggeredAt != null) 'triggeredAt': triggeredAt?.toJson(), if (acknowledgedAt != null) 'acknowledgedAt': acknowledgedAt?.toJson(), + if (resolvedAt != null) 'resolvedAt': resolvedAt?.toJson(), }; } @@ -109,6 +118,7 @@ abstract class AlertStatusResult if (status != null) 'status': status?.toJson(), if (triggeredAt != null) 'triggeredAt': triggeredAt?.toJson(), if (acknowledgedAt != null) 'acknowledgedAt': acknowledgedAt?.toJson(), + if (resolvedAt != null) 'resolvedAt': resolvedAt?.toJson(), }; } @@ -128,6 +138,7 @@ class _AlertStatusResultImpl extends AlertStatusResult { _i3.AlertStatus? status, DateTime? triggeredAt, DateTime? acknowledgedAt, + DateTime? resolvedAt, }) : super._( found: found, alertId: alertId, @@ -135,6 +146,7 @@ class _AlertStatusResultImpl extends AlertStatusResult { status: status, triggeredAt: triggeredAt, acknowledgedAt: acknowledgedAt, + resolvedAt: resolvedAt, ); /// Returns a shallow copy of this [AlertStatusResult] @@ -148,6 +160,7 @@ class _AlertStatusResultImpl extends AlertStatusResult { Object? status = _Undefined, Object? triggeredAt = _Undefined, Object? acknowledgedAt = _Undefined, + Object? resolvedAt = _Undefined, }) { return AlertStatusResult( found: found ?? this.found, @@ -158,6 +171,7 @@ class _AlertStatusResultImpl extends AlertStatusResult { acknowledgedAt: acknowledgedAt is DateTime? ? acknowledgedAt : this.acknowledgedAt, + resolvedAt: resolvedAt is DateTime? ? resolvedAt : this.resolvedAt, ); } } diff --git a/backend/sinalacs_server/lib/src/generated/endpoints.dart b/backend/sinalacs_server/lib/src/generated/endpoints.dart index 6fb8cb55..7ecb2a93 100644 --- a/backend/sinalacs_server/lib/src/generated/endpoints.dart +++ b/backend/sinalacs_server/lib/src/generated/endpoints.dart @@ -977,6 +977,30 @@ class Endpoints extends _i1.EndpointDispatch { alertId: params['alertId'], ), ), + 'resolve': _i1.MethodConnector( + name: 'resolve', + params: { + 'accessToken': _i1.ParameterDescription( + name: 'accessToken', + type: _i1.getType(), + nullable: false, + ), + 'alertId': _i1.ParameterDescription( + name: 'alertId', + type: _i1.getType(), + nullable: false, + ), + }, + call: + ( + _i1.Session session, + Map params, + ) async => (endpoints['alerts'] as _i3.AlertsEndpoint).resolve( + session, + accessToken: params['accessToken'], + alertId: params['alertId'], + ), + ), 'statusFor': _i1.MethodConnector( name: 'statusFor', params: { diff --git a/backend/sinalacs_server/lib/src/generated/protocol.dart b/backend/sinalacs_server/lib/src/generated/protocol.dart index ea3fd689..b2e9c248 100644 --- a/backend/sinalacs_server/lib/src/generated/protocol.dart +++ b/backend/sinalacs_server/lib/src/generated/protocol.dart @@ -40,85 +40,86 @@ import 'api/admin_password_reset_result.dart' as _i26; import 'api/admin_staff.dart' as _i27; import 'api/admin_staff_mfa_reset_result.dart' as _i28; import 'api/alert_ack_result.dart' as _i29; -import 'api/alert_status_result.dart' as _i30; -import 'api/development_login_result.dart' as _i31; -import 'api/enrollment_result.dart' as _i32; -import 'api/enrollment_token_result.dart' as _i33; -import 'api/micro_area_patient.dart' as _i34; -import 'api/notice_history_entry.dart' as _i35; -import 'api/notice_history_page.dart' as _i36; -import 'api/notice_send_result.dart' as _i37; -import 'api/patient_consent_record.dart' as _i38; -import 'api/patient_data_overview.dart' as _i39; -import 'api/patient_data_subject_request_record.dart' as _i40; -import 'api/patient_risk_event.dart' as _i41; -import 'api/red_alert_result.dart' as _i42; -import 'api/service_health.dart' as _i43; -import 'api/terms_change_notice.dart' as _i44; -import 'api/terms_version_entry.dart' as _i45; -import 'api/totp_enrollment_start.dart' as _i46; -import 'api/triage_result.dart' as _i47; -import 'api/ubs_contact.dart' as _i48; -import 'api/visit_sync_entry.dart' as _i49; -import 'api/visit_sync_result.dart' as _i50; -import 'audit_chain_checkpoint.dart' as _i51; -import 'audit_export.dart' as _i52; -import 'audit_log.dart' as _i53; -import 'consent_log.dart' as _i54; -import 'data_subject_request.dart' as _i55; -import 'enrollment_token.dart' as _i56; -import 'enums/alert_status.dart' as _i57; -import 'enums/arrival_method.dart' as _i58; -import 'enums/audit_chain_reason.dart' as _i59; -import 'enums/audit_integrity_state.dart' as _i60; -import 'enums/audit_row_seal.dart' as _i61; -import 'enums/consent_purpose.dart' as _i62; -import 'enums/data_subject_request_status.dart' as _i63; -import 'enums/data_subject_request_type.dart' as _i64; -import 'enums/escalation_reason.dart' as _i65; -import 'enums/risk_level.dart' as _i66; -import 'enums/scheduled_notice_status.dart' as _i67; -import 'enums/sync_status.dart' as _i68; -import 'enums/user_role.dart' as _i69; -import 'enums/visit_authorship.dart' as _i70; -import 'exceptions/admin_invalid_request_exception.dart' as _i71; -import 'exceptions/alert_dispatch_unavailable_exception.dart' as _i72; -import 'exceptions/alert_permission_exception.dart' as _i73; -import 'exceptions/alert_validation_exception.dart' as _i74; -import 'exceptions/authentication_failed_exception.dart' as _i75; -import 'exceptions/data_rights_exception.dart' as _i76; -import 'exceptions/endpoint_disabled_exception.dart' as _i77; -import 'exceptions/enrollment_exception.dart' as _i78; -import 'exceptions/mfa_enrollment_required_exception.dart' as _i79; -import 'exceptions/mfa_required_exception.dart' as _i80; -import 'exceptions/notice_delivery_exception.dart' as _i81; -import 'exceptions/otp_request_exception.dart' as _i82; -import 'exceptions/session_expired_exception.dart' as _i83; -import 'micro_area.dart' as _i84; -import 'otp_challenge.dart' as _i85; -import 'patient.dart' as _i86; -import 'push_token.dart' as _i87; -import 'scheduled_notice.dart' as _i88; -import 'staff_account.dart' as _i89; -import 'terms_version.dart' as _i90; -import 'triage_answer.dart' as _i91; -import 'triage_session.dart' as _i92; -import 'ubs.dart' as _i93; -import 'user.dart' as _i94; -import 'user_credential.dart' as _i95; -import 'visit.dart' as _i96; +import 'api/alert_resolve_result.dart' as _i30; +import 'api/alert_status_result.dart' as _i31; +import 'api/development_login_result.dart' as _i32; +import 'api/enrollment_result.dart' as _i33; +import 'api/enrollment_token_result.dart' as _i34; +import 'api/micro_area_patient.dart' as _i35; +import 'api/notice_history_entry.dart' as _i36; +import 'api/notice_history_page.dart' as _i37; +import 'api/notice_send_result.dart' as _i38; +import 'api/patient_consent_record.dart' as _i39; +import 'api/patient_data_overview.dart' as _i40; +import 'api/patient_data_subject_request_record.dart' as _i41; +import 'api/patient_risk_event.dart' as _i42; +import 'api/red_alert_result.dart' as _i43; +import 'api/service_health.dart' as _i44; +import 'api/terms_change_notice.dart' as _i45; +import 'api/terms_version_entry.dart' as _i46; +import 'api/totp_enrollment_start.dart' as _i47; +import 'api/triage_result.dart' as _i48; +import 'api/ubs_contact.dart' as _i49; +import 'api/visit_sync_entry.dart' as _i50; +import 'api/visit_sync_result.dart' as _i51; +import 'audit_chain_checkpoint.dart' as _i52; +import 'audit_export.dart' as _i53; +import 'audit_log.dart' as _i54; +import 'consent_log.dart' as _i55; +import 'data_subject_request.dart' as _i56; +import 'enrollment_token.dart' as _i57; +import 'enums/alert_status.dart' as _i58; +import 'enums/arrival_method.dart' as _i59; +import 'enums/audit_chain_reason.dart' as _i60; +import 'enums/audit_integrity_state.dart' as _i61; +import 'enums/audit_row_seal.dart' as _i62; +import 'enums/consent_purpose.dart' as _i63; +import 'enums/data_subject_request_status.dart' as _i64; +import 'enums/data_subject_request_type.dart' as _i65; +import 'enums/escalation_reason.dart' as _i66; +import 'enums/risk_level.dart' as _i67; +import 'enums/scheduled_notice_status.dart' as _i68; +import 'enums/sync_status.dart' as _i69; +import 'enums/user_role.dart' as _i70; +import 'enums/visit_authorship.dart' as _i71; +import 'exceptions/admin_invalid_request_exception.dart' as _i72; +import 'exceptions/alert_dispatch_unavailable_exception.dart' as _i73; +import 'exceptions/alert_permission_exception.dart' as _i74; +import 'exceptions/alert_validation_exception.dart' as _i75; +import 'exceptions/authentication_failed_exception.dart' as _i76; +import 'exceptions/data_rights_exception.dart' as _i77; +import 'exceptions/endpoint_disabled_exception.dart' as _i78; +import 'exceptions/enrollment_exception.dart' as _i79; +import 'exceptions/mfa_enrollment_required_exception.dart' as _i80; +import 'exceptions/mfa_required_exception.dart' as _i81; +import 'exceptions/notice_delivery_exception.dart' as _i82; +import 'exceptions/otp_request_exception.dart' as _i83; +import 'exceptions/session_expired_exception.dart' as _i84; +import 'micro_area.dart' as _i85; +import 'otp_challenge.dart' as _i86; +import 'patient.dart' as _i87; +import 'push_token.dart' as _i88; +import 'scheduled_notice.dart' as _i89; +import 'staff_account.dart' as _i90; +import 'terms_version.dart' as _i91; +import 'triage_answer.dart' as _i92; +import 'triage_session.dart' as _i93; +import 'ubs.dart' as _i94; +import 'user.dart' as _i95; +import 'user_credential.dart' as _i96; +import 'visit.dart' as _i97; import 'package:sinalacs_server/src/generated/api/admin_micro_area.dart' - as _i97; -import 'package:sinalacs_server/src/generated/api/admin_acs.dart' as _i98; -import 'package:sinalacs_server/src/generated/api/admin_staff.dart' as _i99; + as _i98; +import 'package:sinalacs_server/src/generated/api/admin_acs.dart' as _i99; +import 'package:sinalacs_server/src/generated/api/admin_staff.dart' as _i100; import 'package:sinalacs_server/src/generated/api/terms_version_entry.dart' - as _i100; -import 'package:sinalacs_server/src/generated/api/micro_area_patient.dart' as _i101; -import 'package:sinalacs_server/src/generated/api/visit_sync_result.dart' +import 'package:sinalacs_server/src/generated/api/micro_area_patient.dart' as _i102; -import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' +import 'package:sinalacs_server/src/generated/api/visit_sync_result.dart' as _i103; +import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' + as _i104; export 'acs.dart'; export 'acs_refresh_token.dart'; export 'acs_upload_token.dart'; @@ -146,6 +147,7 @@ export 'api/admin_password_reset_result.dart'; export 'api/admin_staff.dart'; export 'api/admin_staff_mfa_reset_result.dart'; export 'api/alert_ack_result.dart'; +export 'api/alert_resolve_result.dart'; export 'api/alert_status_result.dart'; export 'api/development_login_result.dart'; export 'api/enrollment_result.dart'; @@ -880,6 +882,12 @@ class Protocol extends _i1.SerializationManagerServer { isNullable: true, dartType: 'DateTime?', ), + _i2.ColumnDefinition( + name: 'resolvedAt', + columnType: _i2.ColumnType.timestampWithoutTimeZone, + isNullable: true, + dartType: 'DateTime?', + ), _i2.ColumnDefinition( name: 'riskLevel', columnType: _i2.ColumnType.text, @@ -3082,206 +3090,209 @@ class Protocol extends _i1.SerializationManagerServer { if (t == _i29.AlertAckResult) { return _i29.AlertAckResult.fromJson(data) as T; } - if (t == _i30.AlertStatusResult) { - return _i30.AlertStatusResult.fromJson(data) as T; + if (t == _i30.AlertResolveResult) { + return _i30.AlertResolveResult.fromJson(data) as T; } - if (t == _i31.DevelopmentLoginResult) { - return _i31.DevelopmentLoginResult.fromJson(data) as T; + if (t == _i31.AlertStatusResult) { + return _i31.AlertStatusResult.fromJson(data) as T; } - if (t == _i32.EnrollmentResult) { - return _i32.EnrollmentResult.fromJson(data) as T; + if (t == _i32.DevelopmentLoginResult) { + return _i32.DevelopmentLoginResult.fromJson(data) as T; } - if (t == _i33.EnrollmentTokenResult) { - return _i33.EnrollmentTokenResult.fromJson(data) as T; + if (t == _i33.EnrollmentResult) { + return _i33.EnrollmentResult.fromJson(data) as T; } - if (t == _i34.MicroAreaPatient) { - return _i34.MicroAreaPatient.fromJson(data) as T; + if (t == _i34.EnrollmentTokenResult) { + return _i34.EnrollmentTokenResult.fromJson(data) as T; } - if (t == _i35.NoticeHistoryEntry) { - return _i35.NoticeHistoryEntry.fromJson(data) as T; + if (t == _i35.MicroAreaPatient) { + return _i35.MicroAreaPatient.fromJson(data) as T; } - if (t == _i36.NoticeHistoryPage) { - return _i36.NoticeHistoryPage.fromJson(data) as T; + if (t == _i36.NoticeHistoryEntry) { + return _i36.NoticeHistoryEntry.fromJson(data) as T; } - if (t == _i37.NoticeSendResult) { - return _i37.NoticeSendResult.fromJson(data) as T; + if (t == _i37.NoticeHistoryPage) { + return _i37.NoticeHistoryPage.fromJson(data) as T; } - if (t == _i38.PatientConsentRecord) { - return _i38.PatientConsentRecord.fromJson(data) as T; + if (t == _i38.NoticeSendResult) { + return _i38.NoticeSendResult.fromJson(data) as T; } - if (t == _i39.PatientDataOverview) { - return _i39.PatientDataOverview.fromJson(data) as T; + if (t == _i39.PatientConsentRecord) { + return _i39.PatientConsentRecord.fromJson(data) as T; } - if (t == _i40.PatientDataSubjectRequestRecord) { - return _i40.PatientDataSubjectRequestRecord.fromJson(data) as T; + if (t == _i40.PatientDataOverview) { + return _i40.PatientDataOverview.fromJson(data) as T; } - if (t == _i41.PatientRiskEvent) { - return _i41.PatientRiskEvent.fromJson(data) as T; + if (t == _i41.PatientDataSubjectRequestRecord) { + return _i41.PatientDataSubjectRequestRecord.fromJson(data) as T; } - if (t == _i42.RedAlertResult) { - return _i42.RedAlertResult.fromJson(data) as T; + if (t == _i42.PatientRiskEvent) { + return _i42.PatientRiskEvent.fromJson(data) as T; } - if (t == _i43.ServiceHealth) { - return _i43.ServiceHealth.fromJson(data) as T; + if (t == _i43.RedAlertResult) { + return _i43.RedAlertResult.fromJson(data) as T; } - if (t == _i44.TermsChangeNotice) { - return _i44.TermsChangeNotice.fromJson(data) as T; + if (t == _i44.ServiceHealth) { + return _i44.ServiceHealth.fromJson(data) as T; } - if (t == _i45.TermsVersionEntry) { - return _i45.TermsVersionEntry.fromJson(data) as T; + if (t == _i45.TermsChangeNotice) { + return _i45.TermsChangeNotice.fromJson(data) as T; } - if (t == _i46.TotpEnrollmentStart) { - return _i46.TotpEnrollmentStart.fromJson(data) as T; + if (t == _i46.TermsVersionEntry) { + return _i46.TermsVersionEntry.fromJson(data) as T; } - if (t == _i47.TriageResult) { - return _i47.TriageResult.fromJson(data) as T; + if (t == _i47.TotpEnrollmentStart) { + return _i47.TotpEnrollmentStart.fromJson(data) as T; } - if (t == _i48.UbsContact) { - return _i48.UbsContact.fromJson(data) as T; + if (t == _i48.TriageResult) { + return _i48.TriageResult.fromJson(data) as T; } - if (t == _i49.VisitSyncEntry) { - return _i49.VisitSyncEntry.fromJson(data) as T; + if (t == _i49.UbsContact) { + return _i49.UbsContact.fromJson(data) as T; } - if (t == _i50.VisitSyncResult) { - return _i50.VisitSyncResult.fromJson(data) as T; + if (t == _i50.VisitSyncEntry) { + return _i50.VisitSyncEntry.fromJson(data) as T; } - if (t == _i51.AuditChainCheckpoint) { - return _i51.AuditChainCheckpoint.fromJson(data) as T; + if (t == _i51.VisitSyncResult) { + return _i51.VisitSyncResult.fromJson(data) as T; } - if (t == _i52.AuditExport) { - return _i52.AuditExport.fromJson(data) as T; + if (t == _i52.AuditChainCheckpoint) { + return _i52.AuditChainCheckpoint.fromJson(data) as T; } - if (t == _i53.AuditLog) { - return _i53.AuditLog.fromJson(data) as T; + if (t == _i53.AuditExport) { + return _i53.AuditExport.fromJson(data) as T; } - if (t == _i54.ConsentLog) { - return _i54.ConsentLog.fromJson(data) as T; + if (t == _i54.AuditLog) { + return _i54.AuditLog.fromJson(data) as T; } - if (t == _i55.DataSubjectRequest) { - return _i55.DataSubjectRequest.fromJson(data) as T; + if (t == _i55.ConsentLog) { + return _i55.ConsentLog.fromJson(data) as T; } - if (t == _i56.EnrollmentToken) { - return _i56.EnrollmentToken.fromJson(data) as T; + if (t == _i56.DataSubjectRequest) { + return _i56.DataSubjectRequest.fromJson(data) as T; } - if (t == _i57.AlertStatus) { - return _i57.AlertStatus.fromJson(data) as T; + if (t == _i57.EnrollmentToken) { + return _i57.EnrollmentToken.fromJson(data) as T; } - if (t == _i58.ArrivalMethod) { - return _i58.ArrivalMethod.fromJson(data) as T; + if (t == _i58.AlertStatus) { + return _i58.AlertStatus.fromJson(data) as T; } - if (t == _i59.AuditChainReason) { - return _i59.AuditChainReason.fromJson(data) as T; + if (t == _i59.ArrivalMethod) { + return _i59.ArrivalMethod.fromJson(data) as T; } - if (t == _i60.AuditIntegrityState) { - return _i60.AuditIntegrityState.fromJson(data) as T; + if (t == _i60.AuditChainReason) { + return _i60.AuditChainReason.fromJson(data) as T; } - if (t == _i61.AuditRowSeal) { - return _i61.AuditRowSeal.fromJson(data) as T; + if (t == _i61.AuditIntegrityState) { + return _i61.AuditIntegrityState.fromJson(data) as T; } - if (t == _i62.ConsentPurpose) { - return _i62.ConsentPurpose.fromJson(data) as T; + if (t == _i62.AuditRowSeal) { + return _i62.AuditRowSeal.fromJson(data) as T; } - if (t == _i63.DataSubjectRequestStatus) { - return _i63.DataSubjectRequestStatus.fromJson(data) as T; + if (t == _i63.ConsentPurpose) { + return _i63.ConsentPurpose.fromJson(data) as T; } - if (t == _i64.DataSubjectRequestType) { - return _i64.DataSubjectRequestType.fromJson(data) as T; + if (t == _i64.DataSubjectRequestStatus) { + return _i64.DataSubjectRequestStatus.fromJson(data) as T; } - if (t == _i65.EscalationReason) { - return _i65.EscalationReason.fromJson(data) as T; + if (t == _i65.DataSubjectRequestType) { + return _i65.DataSubjectRequestType.fromJson(data) as T; } - if (t == _i66.RiskLevel) { - return _i66.RiskLevel.fromJson(data) as T; + if (t == _i66.EscalationReason) { + return _i66.EscalationReason.fromJson(data) as T; } - if (t == _i67.ScheduledNoticeStatus) { - return _i67.ScheduledNoticeStatus.fromJson(data) as T; + if (t == _i67.RiskLevel) { + return _i67.RiskLevel.fromJson(data) as T; } - if (t == _i68.SyncStatus) { - return _i68.SyncStatus.fromJson(data) as T; + if (t == _i68.ScheduledNoticeStatus) { + return _i68.ScheduledNoticeStatus.fromJson(data) as T; } - if (t == _i69.UserRole) { - return _i69.UserRole.fromJson(data) as T; + if (t == _i69.SyncStatus) { + return _i69.SyncStatus.fromJson(data) as T; } - if (t == _i70.VisitAuthorship) { - return _i70.VisitAuthorship.fromJson(data) as T; + if (t == _i70.UserRole) { + return _i70.UserRole.fromJson(data) as T; } - if (t == _i71.AdminInvalidRequestException) { - return _i71.AdminInvalidRequestException.fromJson(data) as T; + if (t == _i71.VisitAuthorship) { + return _i71.VisitAuthorship.fromJson(data) as T; } - if (t == _i72.AlertDispatchUnavailableException) { - return _i72.AlertDispatchUnavailableException.fromJson(data) as T; + if (t == _i72.AdminInvalidRequestException) { + return _i72.AdminInvalidRequestException.fromJson(data) as T; } - if (t == _i73.AlertPermissionException) { - return _i73.AlertPermissionException.fromJson(data) as T; + if (t == _i73.AlertDispatchUnavailableException) { + return _i73.AlertDispatchUnavailableException.fromJson(data) as T; } - if (t == _i74.AlertValidationException) { - return _i74.AlertValidationException.fromJson(data) as T; + if (t == _i74.AlertPermissionException) { + return _i74.AlertPermissionException.fromJson(data) as T; } - if (t == _i75.AuthenticationFailedException) { - return _i75.AuthenticationFailedException.fromJson(data) as T; + if (t == _i75.AlertValidationException) { + return _i75.AlertValidationException.fromJson(data) as T; } - if (t == _i76.DataRightsException) { - return _i76.DataRightsException.fromJson(data) as T; + if (t == _i76.AuthenticationFailedException) { + return _i76.AuthenticationFailedException.fromJson(data) as T; } - if (t == _i77.EndpointDisabledException) { - return _i77.EndpointDisabledException.fromJson(data) as T; + if (t == _i77.DataRightsException) { + return _i77.DataRightsException.fromJson(data) as T; } - if (t == _i78.EnrollmentException) { - return _i78.EnrollmentException.fromJson(data) as T; + if (t == _i78.EndpointDisabledException) { + return _i78.EndpointDisabledException.fromJson(data) as T; } - if (t == _i79.MfaEnrollmentRequiredException) { - return _i79.MfaEnrollmentRequiredException.fromJson(data) as T; + if (t == _i79.EnrollmentException) { + return _i79.EnrollmentException.fromJson(data) as T; } - if (t == _i80.MfaRequiredException) { - return _i80.MfaRequiredException.fromJson(data) as T; + if (t == _i80.MfaEnrollmentRequiredException) { + return _i80.MfaEnrollmentRequiredException.fromJson(data) as T; } - if (t == _i81.NoticeDeliveryException) { - return _i81.NoticeDeliveryException.fromJson(data) as T; + if (t == _i81.MfaRequiredException) { + return _i81.MfaRequiredException.fromJson(data) as T; } - if (t == _i82.OtpRequestException) { - return _i82.OtpRequestException.fromJson(data) as T; + if (t == _i82.NoticeDeliveryException) { + return _i82.NoticeDeliveryException.fromJson(data) as T; } - if (t == _i83.SessionExpiredException) { - return _i83.SessionExpiredException.fromJson(data) as T; + if (t == _i83.OtpRequestException) { + return _i83.OtpRequestException.fromJson(data) as T; } - if (t == _i84.MicroArea) { - return _i84.MicroArea.fromJson(data) as T; + if (t == _i84.SessionExpiredException) { + return _i84.SessionExpiredException.fromJson(data) as T; } - if (t == _i85.OtpChallenge) { - return _i85.OtpChallenge.fromJson(data) as T; + if (t == _i85.MicroArea) { + return _i85.MicroArea.fromJson(data) as T; } - if (t == _i86.Patient) { - return _i86.Patient.fromJson(data) as T; + if (t == _i86.OtpChallenge) { + return _i86.OtpChallenge.fromJson(data) as T; } - if (t == _i87.PushToken) { - return _i87.PushToken.fromJson(data) as T; + if (t == _i87.Patient) { + return _i87.Patient.fromJson(data) as T; } - if (t == _i88.ScheduledNotice) { - return _i88.ScheduledNotice.fromJson(data) as T; + if (t == _i88.PushToken) { + return _i88.PushToken.fromJson(data) as T; } - if (t == _i89.StaffAccount) { - return _i89.StaffAccount.fromJson(data) as T; + if (t == _i89.ScheduledNotice) { + return _i89.ScheduledNotice.fromJson(data) as T; } - if (t == _i90.TermsVersion) { - return _i90.TermsVersion.fromJson(data) as T; + if (t == _i90.StaffAccount) { + return _i90.StaffAccount.fromJson(data) as T; } - if (t == _i91.TriageAnswer) { - return _i91.TriageAnswer.fromJson(data) as T; + if (t == _i91.TermsVersion) { + return _i91.TermsVersion.fromJson(data) as T; } - if (t == _i92.TriageSession) { - return _i92.TriageSession.fromJson(data) as T; + if (t == _i92.TriageAnswer) { + return _i92.TriageAnswer.fromJson(data) as T; } - if (t == _i93.Ubs) { - return _i93.Ubs.fromJson(data) as T; + if (t == _i93.TriageSession) { + return _i93.TriageSession.fromJson(data) as T; } - if (t == _i94.User) { - return _i94.User.fromJson(data) as T; + if (t == _i94.Ubs) { + return _i94.Ubs.fromJson(data) as T; } - if (t == _i95.UserCredential) { - return _i95.UserCredential.fromJson(data) as T; + if (t == _i95.User) { + return _i95.User.fromJson(data) as T; } - if (t == _i96.Visit) { - return _i96.Visit.fromJson(data) as T; + if (t == _i96.UserCredential) { + return _i96.UserCredential.fromJson(data) as T; + } + if (t == _i97.Visit) { + return _i97.Visit.fromJson(data) as T; } if (t == _i1.getType<_i3.Acs?>()) { return (data != null ? _i3.Acs.fromJson(data) : null) as T; @@ -3382,250 +3393,254 @@ class Protocol extends _i1.SerializationManagerServer { if (t == _i1.getType<_i29.AlertAckResult?>()) { return (data != null ? _i29.AlertAckResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i30.AlertStatusResult?>()) { - return (data != null ? _i30.AlertStatusResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i30.AlertResolveResult?>()) { + return (data != null ? _i30.AlertResolveResult.fromJson(data) : null) + as T; + } + if (t == _i1.getType<_i31.AlertStatusResult?>()) { + return (data != null ? _i31.AlertStatusResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i31.DevelopmentLoginResult?>()) { - return (data != null ? _i31.DevelopmentLoginResult.fromJson(data) : null) + if (t == _i1.getType<_i32.DevelopmentLoginResult?>()) { + return (data != null ? _i32.DevelopmentLoginResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i32.EnrollmentResult?>()) { - return (data != null ? _i32.EnrollmentResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i33.EnrollmentResult?>()) { + return (data != null ? _i33.EnrollmentResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i33.EnrollmentTokenResult?>()) { - return (data != null ? _i33.EnrollmentTokenResult.fromJson(data) : null) + if (t == _i1.getType<_i34.EnrollmentTokenResult?>()) { + return (data != null ? _i34.EnrollmentTokenResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i34.MicroAreaPatient?>()) { - return (data != null ? _i34.MicroAreaPatient.fromJson(data) : null) as T; + if (t == _i1.getType<_i35.MicroAreaPatient?>()) { + return (data != null ? _i35.MicroAreaPatient.fromJson(data) : null) as T; } - if (t == _i1.getType<_i35.NoticeHistoryEntry?>()) { - return (data != null ? _i35.NoticeHistoryEntry.fromJson(data) : null) + if (t == _i1.getType<_i36.NoticeHistoryEntry?>()) { + return (data != null ? _i36.NoticeHistoryEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i36.NoticeHistoryPage?>()) { - return (data != null ? _i36.NoticeHistoryPage.fromJson(data) : null) as T; + if (t == _i1.getType<_i37.NoticeHistoryPage?>()) { + return (data != null ? _i37.NoticeHistoryPage.fromJson(data) : null) as T; } - if (t == _i1.getType<_i37.NoticeSendResult?>()) { - return (data != null ? _i37.NoticeSendResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i38.NoticeSendResult?>()) { + return (data != null ? _i38.NoticeSendResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i38.PatientConsentRecord?>()) { - return (data != null ? _i38.PatientConsentRecord.fromJson(data) : null) + if (t == _i1.getType<_i39.PatientConsentRecord?>()) { + return (data != null ? _i39.PatientConsentRecord.fromJson(data) : null) as T; } - if (t == _i1.getType<_i39.PatientDataOverview?>()) { - return (data != null ? _i39.PatientDataOverview.fromJson(data) : null) + if (t == _i1.getType<_i40.PatientDataOverview?>()) { + return (data != null ? _i40.PatientDataOverview.fromJson(data) : null) as T; } - if (t == _i1.getType<_i40.PatientDataSubjectRequestRecord?>()) { + if (t == _i1.getType<_i41.PatientDataSubjectRequestRecord?>()) { return (data != null - ? _i40.PatientDataSubjectRequestRecord.fromJson(data) + ? _i41.PatientDataSubjectRequestRecord.fromJson(data) : null) as T; } - if (t == _i1.getType<_i41.PatientRiskEvent?>()) { - return (data != null ? _i41.PatientRiskEvent.fromJson(data) : null) as T; + if (t == _i1.getType<_i42.PatientRiskEvent?>()) { + return (data != null ? _i42.PatientRiskEvent.fromJson(data) : null) as T; } - if (t == _i1.getType<_i42.RedAlertResult?>()) { - return (data != null ? _i42.RedAlertResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i43.RedAlertResult?>()) { + return (data != null ? _i43.RedAlertResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i43.ServiceHealth?>()) { - return (data != null ? _i43.ServiceHealth.fromJson(data) : null) as T; + if (t == _i1.getType<_i44.ServiceHealth?>()) { + return (data != null ? _i44.ServiceHealth.fromJson(data) : null) as T; } - if (t == _i1.getType<_i44.TermsChangeNotice?>()) { - return (data != null ? _i44.TermsChangeNotice.fromJson(data) : null) as T; + if (t == _i1.getType<_i45.TermsChangeNotice?>()) { + return (data != null ? _i45.TermsChangeNotice.fromJson(data) : null) as T; } - if (t == _i1.getType<_i45.TermsVersionEntry?>()) { - return (data != null ? _i45.TermsVersionEntry.fromJson(data) : null) as T; + if (t == _i1.getType<_i46.TermsVersionEntry?>()) { + return (data != null ? _i46.TermsVersionEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i46.TotpEnrollmentStart?>()) { - return (data != null ? _i46.TotpEnrollmentStart.fromJson(data) : null) + if (t == _i1.getType<_i47.TotpEnrollmentStart?>()) { + return (data != null ? _i47.TotpEnrollmentStart.fromJson(data) : null) as T; } - if (t == _i1.getType<_i47.TriageResult?>()) { - return (data != null ? _i47.TriageResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i48.TriageResult?>()) { + return (data != null ? _i48.TriageResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i48.UbsContact?>()) { - return (data != null ? _i48.UbsContact.fromJson(data) : null) as T; + if (t == _i1.getType<_i49.UbsContact?>()) { + return (data != null ? _i49.UbsContact.fromJson(data) : null) as T; } - if (t == _i1.getType<_i49.VisitSyncEntry?>()) { - return (data != null ? _i49.VisitSyncEntry.fromJson(data) : null) as T; + if (t == _i1.getType<_i50.VisitSyncEntry?>()) { + return (data != null ? _i50.VisitSyncEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i50.VisitSyncResult?>()) { - return (data != null ? _i50.VisitSyncResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i51.VisitSyncResult?>()) { + return (data != null ? _i51.VisitSyncResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i51.AuditChainCheckpoint?>()) { - return (data != null ? _i51.AuditChainCheckpoint.fromJson(data) : null) + if (t == _i1.getType<_i52.AuditChainCheckpoint?>()) { + return (data != null ? _i52.AuditChainCheckpoint.fromJson(data) : null) as T; } - if (t == _i1.getType<_i52.AuditExport?>()) { - return (data != null ? _i52.AuditExport.fromJson(data) : null) as T; + if (t == _i1.getType<_i53.AuditExport?>()) { + return (data != null ? _i53.AuditExport.fromJson(data) : null) as T; } - if (t == _i1.getType<_i53.AuditLog?>()) { - return (data != null ? _i53.AuditLog.fromJson(data) : null) as T; + if (t == _i1.getType<_i54.AuditLog?>()) { + return (data != null ? _i54.AuditLog.fromJson(data) : null) as T; } - if (t == _i1.getType<_i54.ConsentLog?>()) { - return (data != null ? _i54.ConsentLog.fromJson(data) : null) as T; + if (t == _i1.getType<_i55.ConsentLog?>()) { + return (data != null ? _i55.ConsentLog.fromJson(data) : null) as T; } - if (t == _i1.getType<_i55.DataSubjectRequest?>()) { - return (data != null ? _i55.DataSubjectRequest.fromJson(data) : null) + if (t == _i1.getType<_i56.DataSubjectRequest?>()) { + return (data != null ? _i56.DataSubjectRequest.fromJson(data) : null) as T; } - if (t == _i1.getType<_i56.EnrollmentToken?>()) { - return (data != null ? _i56.EnrollmentToken.fromJson(data) : null) as T; + if (t == _i1.getType<_i57.EnrollmentToken?>()) { + return (data != null ? _i57.EnrollmentToken.fromJson(data) : null) as T; } - if (t == _i1.getType<_i57.AlertStatus?>()) { - return (data != null ? _i57.AlertStatus.fromJson(data) : null) as T; + if (t == _i1.getType<_i58.AlertStatus?>()) { + return (data != null ? _i58.AlertStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i58.ArrivalMethod?>()) { - return (data != null ? _i58.ArrivalMethod.fromJson(data) : null) as T; + if (t == _i1.getType<_i59.ArrivalMethod?>()) { + return (data != null ? _i59.ArrivalMethod.fromJson(data) : null) as T; } - if (t == _i1.getType<_i59.AuditChainReason?>()) { - return (data != null ? _i59.AuditChainReason.fromJson(data) : null) as T; + if (t == _i1.getType<_i60.AuditChainReason?>()) { + return (data != null ? _i60.AuditChainReason.fromJson(data) : null) as T; } - if (t == _i1.getType<_i60.AuditIntegrityState?>()) { - return (data != null ? _i60.AuditIntegrityState.fromJson(data) : null) + if (t == _i1.getType<_i61.AuditIntegrityState?>()) { + return (data != null ? _i61.AuditIntegrityState.fromJson(data) : null) as T; } - if (t == _i1.getType<_i61.AuditRowSeal?>()) { - return (data != null ? _i61.AuditRowSeal.fromJson(data) : null) as T; + if (t == _i1.getType<_i62.AuditRowSeal?>()) { + return (data != null ? _i62.AuditRowSeal.fromJson(data) : null) as T; } - if (t == _i1.getType<_i62.ConsentPurpose?>()) { - return (data != null ? _i62.ConsentPurpose.fromJson(data) : null) as T; + if (t == _i1.getType<_i63.ConsentPurpose?>()) { + return (data != null ? _i63.ConsentPurpose.fromJson(data) : null) as T; } - if (t == _i1.getType<_i63.DataSubjectRequestStatus?>()) { + if (t == _i1.getType<_i64.DataSubjectRequestStatus?>()) { return (data != null - ? _i63.DataSubjectRequestStatus.fromJson(data) + ? _i64.DataSubjectRequestStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i64.DataSubjectRequestType?>()) { - return (data != null ? _i64.DataSubjectRequestType.fromJson(data) : null) + if (t == _i1.getType<_i65.DataSubjectRequestType?>()) { + return (data != null ? _i65.DataSubjectRequestType.fromJson(data) : null) as T; } - if (t == _i1.getType<_i65.EscalationReason?>()) { - return (data != null ? _i65.EscalationReason.fromJson(data) : null) as T; + if (t == _i1.getType<_i66.EscalationReason?>()) { + return (data != null ? _i66.EscalationReason.fromJson(data) : null) as T; } - if (t == _i1.getType<_i66.RiskLevel?>()) { - return (data != null ? _i66.RiskLevel.fromJson(data) : null) as T; + if (t == _i1.getType<_i67.RiskLevel?>()) { + return (data != null ? _i67.RiskLevel.fromJson(data) : null) as T; } - if (t == _i1.getType<_i67.ScheduledNoticeStatus?>()) { - return (data != null ? _i67.ScheduledNoticeStatus.fromJson(data) : null) + if (t == _i1.getType<_i68.ScheduledNoticeStatus?>()) { + return (data != null ? _i68.ScheduledNoticeStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i68.SyncStatus?>()) { - return (data != null ? _i68.SyncStatus.fromJson(data) : null) as T; + if (t == _i1.getType<_i69.SyncStatus?>()) { + return (data != null ? _i69.SyncStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i69.UserRole?>()) { - return (data != null ? _i69.UserRole.fromJson(data) : null) as T; + if (t == _i1.getType<_i70.UserRole?>()) { + return (data != null ? _i70.UserRole.fromJson(data) : null) as T; } - if (t == _i1.getType<_i70.VisitAuthorship?>()) { - return (data != null ? _i70.VisitAuthorship.fromJson(data) : null) as T; + if (t == _i1.getType<_i71.VisitAuthorship?>()) { + return (data != null ? _i71.VisitAuthorship.fromJson(data) : null) as T; } - if (t == _i1.getType<_i71.AdminInvalidRequestException?>()) { + if (t == _i1.getType<_i72.AdminInvalidRequestException?>()) { return (data != null - ? _i71.AdminInvalidRequestException.fromJson(data) + ? _i72.AdminInvalidRequestException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i72.AlertDispatchUnavailableException?>()) { + if (t == _i1.getType<_i73.AlertDispatchUnavailableException?>()) { return (data != null - ? _i72.AlertDispatchUnavailableException.fromJson(data) + ? _i73.AlertDispatchUnavailableException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i73.AlertPermissionException?>()) { + if (t == _i1.getType<_i74.AlertPermissionException?>()) { return (data != null - ? _i73.AlertPermissionException.fromJson(data) + ? _i74.AlertPermissionException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i74.AlertValidationException?>()) { + if (t == _i1.getType<_i75.AlertValidationException?>()) { return (data != null - ? _i74.AlertValidationException.fromJson(data) + ? _i75.AlertValidationException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i75.AuthenticationFailedException?>()) { + if (t == _i1.getType<_i76.AuthenticationFailedException?>()) { return (data != null - ? _i75.AuthenticationFailedException.fromJson(data) + ? _i76.AuthenticationFailedException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i76.DataRightsException?>()) { - return (data != null ? _i76.DataRightsException.fromJson(data) : null) + if (t == _i1.getType<_i77.DataRightsException?>()) { + return (data != null ? _i77.DataRightsException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i77.EndpointDisabledException?>()) { + if (t == _i1.getType<_i78.EndpointDisabledException?>()) { return (data != null - ? _i77.EndpointDisabledException.fromJson(data) + ? _i78.EndpointDisabledException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i78.EnrollmentException?>()) { - return (data != null ? _i78.EnrollmentException.fromJson(data) : null) + if (t == _i1.getType<_i79.EnrollmentException?>()) { + return (data != null ? _i79.EnrollmentException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i79.MfaEnrollmentRequiredException?>()) { + if (t == _i1.getType<_i80.MfaEnrollmentRequiredException?>()) { return (data != null - ? _i79.MfaEnrollmentRequiredException.fromJson(data) + ? _i80.MfaEnrollmentRequiredException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i80.MfaRequiredException?>()) { - return (data != null ? _i80.MfaRequiredException.fromJson(data) : null) + if (t == _i1.getType<_i81.MfaRequiredException?>()) { + return (data != null ? _i81.MfaRequiredException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i81.NoticeDeliveryException?>()) { - return (data != null ? _i81.NoticeDeliveryException.fromJson(data) : null) + if (t == _i1.getType<_i82.NoticeDeliveryException?>()) { + return (data != null ? _i82.NoticeDeliveryException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i82.OtpRequestException?>()) { - return (data != null ? _i82.OtpRequestException.fromJson(data) : null) + if (t == _i1.getType<_i83.OtpRequestException?>()) { + return (data != null ? _i83.OtpRequestException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i83.SessionExpiredException?>()) { - return (data != null ? _i83.SessionExpiredException.fromJson(data) : null) + if (t == _i1.getType<_i84.SessionExpiredException?>()) { + return (data != null ? _i84.SessionExpiredException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i84.MicroArea?>()) { - return (data != null ? _i84.MicroArea.fromJson(data) : null) as T; + if (t == _i1.getType<_i85.MicroArea?>()) { + return (data != null ? _i85.MicroArea.fromJson(data) : null) as T; } - if (t == _i1.getType<_i85.OtpChallenge?>()) { - return (data != null ? _i85.OtpChallenge.fromJson(data) : null) as T; + if (t == _i1.getType<_i86.OtpChallenge?>()) { + return (data != null ? _i86.OtpChallenge.fromJson(data) : null) as T; } - if (t == _i1.getType<_i86.Patient?>()) { - return (data != null ? _i86.Patient.fromJson(data) : null) as T; + if (t == _i1.getType<_i87.Patient?>()) { + return (data != null ? _i87.Patient.fromJson(data) : null) as T; } - if (t == _i1.getType<_i87.PushToken?>()) { - return (data != null ? _i87.PushToken.fromJson(data) : null) as T; + if (t == _i1.getType<_i88.PushToken?>()) { + return (data != null ? _i88.PushToken.fromJson(data) : null) as T; } - if (t == _i1.getType<_i88.ScheduledNotice?>()) { - return (data != null ? _i88.ScheduledNotice.fromJson(data) : null) as T; + if (t == _i1.getType<_i89.ScheduledNotice?>()) { + return (data != null ? _i89.ScheduledNotice.fromJson(data) : null) as T; } - if (t == _i1.getType<_i89.StaffAccount?>()) { - return (data != null ? _i89.StaffAccount.fromJson(data) : null) as T; + if (t == _i1.getType<_i90.StaffAccount?>()) { + return (data != null ? _i90.StaffAccount.fromJson(data) : null) as T; } - if (t == _i1.getType<_i90.TermsVersion?>()) { - return (data != null ? _i90.TermsVersion.fromJson(data) : null) as T; + if (t == _i1.getType<_i91.TermsVersion?>()) { + return (data != null ? _i91.TermsVersion.fromJson(data) : null) as T; } - if (t == _i1.getType<_i91.TriageAnswer?>()) { - return (data != null ? _i91.TriageAnswer.fromJson(data) : null) as T; + if (t == _i1.getType<_i92.TriageAnswer?>()) { + return (data != null ? _i92.TriageAnswer.fromJson(data) : null) as T; } - if (t == _i1.getType<_i92.TriageSession?>()) { - return (data != null ? _i92.TriageSession.fromJson(data) : null) as T; + if (t == _i1.getType<_i93.TriageSession?>()) { + return (data != null ? _i93.TriageSession.fromJson(data) : null) as T; } - if (t == _i1.getType<_i93.Ubs?>()) { - return (data != null ? _i93.Ubs.fromJson(data) : null) as T; + if (t == _i1.getType<_i94.Ubs?>()) { + return (data != null ? _i94.Ubs.fromJson(data) : null) as T; } - if (t == _i1.getType<_i94.User?>()) { - return (data != null ? _i94.User.fromJson(data) : null) as T; + if (t == _i1.getType<_i95.User?>()) { + return (data != null ? _i95.User.fromJson(data) : null) as T; } - if (t == _i1.getType<_i95.UserCredential?>()) { - return (data != null ? _i95.UserCredential.fromJson(data) : null) as T; + if (t == _i1.getType<_i96.UserCredential?>()) { + return (data != null ? _i96.UserCredential.fromJson(data) : null) as T; } - if (t == _i1.getType<_i96.Visit?>()) { - return (data != null ? _i96.Visit.fromJson(data) : null) as T; + if (t == _i1.getType<_i97.Visit?>()) { + return (data != null ? _i97.Visit.fromJson(data) : null) as T; } if (t == List<_i12.AdminAlert>) { return (data as List).map((e) => deserialize<_i12.AdminAlert>(e)).toList() @@ -3646,27 +3661,27 @@ class Protocol extends _i1.SerializationManagerServer { if (t == List) { return (data as List).map((e) => deserialize(e)).toList() as T; } - if (t == List<_i35.NoticeHistoryEntry>) { + if (t == List<_i36.NoticeHistoryEntry>) { return (data as List) - .map((e) => deserialize<_i35.NoticeHistoryEntry>(e)) + .map((e) => deserialize<_i36.NoticeHistoryEntry>(e)) .toList() as T; } - if (t == List<_i38.PatientConsentRecord>) { + if (t == List<_i39.PatientConsentRecord>) { return (data as List) - .map((e) => deserialize<_i38.PatientConsentRecord>(e)) + .map((e) => deserialize<_i39.PatientConsentRecord>(e)) .toList() as T; } - if (t == List<_i41.PatientRiskEvent>) { + if (t == List<_i42.PatientRiskEvent>) { return (data as List) - .map((e) => deserialize<_i41.PatientRiskEvent>(e)) + .map((e) => deserialize<_i42.PatientRiskEvent>(e)) .toList() as T; } - if (t == List<_i40.PatientDataSubjectRequestRecord>) { + if (t == List<_i41.PatientDataSubjectRequestRecord>) { return (data as List) - .map((e) => deserialize<_i40.PatientDataSubjectRequestRecord>(e)) + .map((e) => deserialize<_i41.PatientDataSubjectRequestRecord>(e)) .toList() as T; } @@ -3676,44 +3691,46 @@ class Protocol extends _i1.SerializationManagerServer { ) as T; } - if (t == List<_i97.AdminMicroArea>) { + if (t == List<_i98.AdminMicroArea>) { return (data as List) - .map((e) => deserialize<_i97.AdminMicroArea>(e)) + .map((e) => deserialize<_i98.AdminMicroArea>(e)) .toList() as T; } - if (t == List<_i98.AdminAcs>) { - return (data as List).map((e) => deserialize<_i98.AdminAcs>(e)).toList() + if (t == List<_i99.AdminAcs>) { + return (data as List).map((e) => deserialize<_i99.AdminAcs>(e)).toList() as T; } - if (t == List<_i99.AdminStaff>) { - return (data as List).map((e) => deserialize<_i99.AdminStaff>(e)).toList() + if (t == List<_i100.AdminStaff>) { + return (data as List) + .map((e) => deserialize<_i100.AdminStaff>(e)) + .toList() as T; } - if (t == List<_i100.TermsVersionEntry>) { + if (t == List<_i101.TermsVersionEntry>) { return (data as List) - .map((e) => deserialize<_i100.TermsVersionEntry>(e)) + .map((e) => deserialize<_i101.TermsVersionEntry>(e)) .toList() as T; } - if (t == List<_i101.MicroAreaPatient>) { + if (t == List<_i102.MicroAreaPatient>) { return (data as List) - .map((e) => deserialize<_i101.MicroAreaPatient>(e)) + .map((e) => deserialize<_i102.MicroAreaPatient>(e)) .toList() as T; } if (t == List) { return (data as List).map((e) => deserialize(e)).toList() as T; } - if (t == List<_i102.VisitSyncResult>) { + if (t == List<_i103.VisitSyncResult>) { return (data as List) - .map((e) => deserialize<_i102.VisitSyncResult>(e)) + .map((e) => deserialize<_i103.VisitSyncResult>(e)) .toList() as T; } - if (t == List<_i103.VisitSyncEntry>) { + if (t == List<_i104.VisitSyncEntry>) { return (data as List) - .map((e) => deserialize<_i103.VisitSyncEntry>(e)) + .map((e) => deserialize<_i104.VisitSyncEntry>(e)) .toList() as T; } @@ -3752,74 +3769,75 @@ class Protocol extends _i1.SerializationManagerServer { _i27.AdminStaff => 'AdminStaff', _i28.AdminStaffMfaResetResult => 'AdminStaffMfaResetResult', _i29.AlertAckResult => 'AlertAckResult', - _i30.AlertStatusResult => 'AlertStatusResult', - _i31.DevelopmentLoginResult => 'DevelopmentLoginResult', - _i32.EnrollmentResult => 'EnrollmentResult', - _i33.EnrollmentTokenResult => 'EnrollmentTokenResult', - _i34.MicroAreaPatient => 'MicroAreaPatient', - _i35.NoticeHistoryEntry => 'NoticeHistoryEntry', - _i36.NoticeHistoryPage => 'NoticeHistoryPage', - _i37.NoticeSendResult => 'NoticeSendResult', - _i38.PatientConsentRecord => 'PatientConsentRecord', - _i39.PatientDataOverview => 'PatientDataOverview', - _i40.PatientDataSubjectRequestRecord => 'PatientDataSubjectRequestRecord', - _i41.PatientRiskEvent => 'PatientRiskEvent', - _i42.RedAlertResult => 'RedAlertResult', - _i43.ServiceHealth => 'ServiceHealth', - _i44.TermsChangeNotice => 'TermsChangeNotice', - _i45.TermsVersionEntry => 'TermsVersionEntry', - _i46.TotpEnrollmentStart => 'TotpEnrollmentStart', - _i47.TriageResult => 'TriageResult', - _i48.UbsContact => 'UbsContact', - _i49.VisitSyncEntry => 'VisitSyncEntry', - _i50.VisitSyncResult => 'VisitSyncResult', - _i51.AuditChainCheckpoint => 'AuditChainCheckpoint', - _i52.AuditExport => 'AuditExport', - _i53.AuditLog => 'AuditLog', - _i54.ConsentLog => 'ConsentLog', - _i55.DataSubjectRequest => 'DataSubjectRequest', - _i56.EnrollmentToken => 'EnrollmentToken', - _i57.AlertStatus => 'AlertStatus', - _i58.ArrivalMethod => 'ArrivalMethod', - _i59.AuditChainReason => 'AuditChainReason', - _i60.AuditIntegrityState => 'AuditIntegrityState', - _i61.AuditRowSeal => 'AuditRowSeal', - _i62.ConsentPurpose => 'ConsentPurpose', - _i63.DataSubjectRequestStatus => 'DataSubjectRequestStatus', - _i64.DataSubjectRequestType => 'DataSubjectRequestType', - _i65.EscalationReason => 'EscalationReason', - _i66.RiskLevel => 'RiskLevel', - _i67.ScheduledNoticeStatus => 'ScheduledNoticeStatus', - _i68.SyncStatus => 'SyncStatus', - _i69.UserRole => 'UserRole', - _i70.VisitAuthorship => 'VisitAuthorship', - _i71.AdminInvalidRequestException => 'AdminInvalidRequestException', - _i72.AlertDispatchUnavailableException => + _i30.AlertResolveResult => 'AlertResolveResult', + _i31.AlertStatusResult => 'AlertStatusResult', + _i32.DevelopmentLoginResult => 'DevelopmentLoginResult', + _i33.EnrollmentResult => 'EnrollmentResult', + _i34.EnrollmentTokenResult => 'EnrollmentTokenResult', + _i35.MicroAreaPatient => 'MicroAreaPatient', + _i36.NoticeHistoryEntry => 'NoticeHistoryEntry', + _i37.NoticeHistoryPage => 'NoticeHistoryPage', + _i38.NoticeSendResult => 'NoticeSendResult', + _i39.PatientConsentRecord => 'PatientConsentRecord', + _i40.PatientDataOverview => 'PatientDataOverview', + _i41.PatientDataSubjectRequestRecord => 'PatientDataSubjectRequestRecord', + _i42.PatientRiskEvent => 'PatientRiskEvent', + _i43.RedAlertResult => 'RedAlertResult', + _i44.ServiceHealth => 'ServiceHealth', + _i45.TermsChangeNotice => 'TermsChangeNotice', + _i46.TermsVersionEntry => 'TermsVersionEntry', + _i47.TotpEnrollmentStart => 'TotpEnrollmentStart', + _i48.TriageResult => 'TriageResult', + _i49.UbsContact => 'UbsContact', + _i50.VisitSyncEntry => 'VisitSyncEntry', + _i51.VisitSyncResult => 'VisitSyncResult', + _i52.AuditChainCheckpoint => 'AuditChainCheckpoint', + _i53.AuditExport => 'AuditExport', + _i54.AuditLog => 'AuditLog', + _i55.ConsentLog => 'ConsentLog', + _i56.DataSubjectRequest => 'DataSubjectRequest', + _i57.EnrollmentToken => 'EnrollmentToken', + _i58.AlertStatus => 'AlertStatus', + _i59.ArrivalMethod => 'ArrivalMethod', + _i60.AuditChainReason => 'AuditChainReason', + _i61.AuditIntegrityState => 'AuditIntegrityState', + _i62.AuditRowSeal => 'AuditRowSeal', + _i63.ConsentPurpose => 'ConsentPurpose', + _i64.DataSubjectRequestStatus => 'DataSubjectRequestStatus', + _i65.DataSubjectRequestType => 'DataSubjectRequestType', + _i66.EscalationReason => 'EscalationReason', + _i67.RiskLevel => 'RiskLevel', + _i68.ScheduledNoticeStatus => 'ScheduledNoticeStatus', + _i69.SyncStatus => 'SyncStatus', + _i70.UserRole => 'UserRole', + _i71.VisitAuthorship => 'VisitAuthorship', + _i72.AdminInvalidRequestException => 'AdminInvalidRequestException', + _i73.AlertDispatchUnavailableException => 'AlertDispatchUnavailableException', - _i73.AlertPermissionException => 'AlertPermissionException', - _i74.AlertValidationException => 'AlertValidationException', - _i75.AuthenticationFailedException => 'AuthenticationFailedException', - _i76.DataRightsException => 'DataRightsException', - _i77.EndpointDisabledException => 'EndpointDisabledException', - _i78.EnrollmentException => 'EnrollmentException', - _i79.MfaEnrollmentRequiredException => 'MfaEnrollmentRequiredException', - _i80.MfaRequiredException => 'MfaRequiredException', - _i81.NoticeDeliveryException => 'NoticeDeliveryException', - _i82.OtpRequestException => 'OtpRequestException', - _i83.SessionExpiredException => 'SessionExpiredException', - _i84.MicroArea => 'MicroArea', - _i85.OtpChallenge => 'OtpChallenge', - _i86.Patient => 'Patient', - _i87.PushToken => 'PushToken', - _i88.ScheduledNotice => 'ScheduledNotice', - _i89.StaffAccount => 'StaffAccount', - _i90.TermsVersion => 'TermsVersion', - _i91.TriageAnswer => 'TriageAnswer', - _i92.TriageSession => 'TriageSession', - _i93.Ubs => 'Ubs', - _i94.User => 'User', - _i95.UserCredential => 'UserCredential', - _i96.Visit => 'Visit', + _i74.AlertPermissionException => 'AlertPermissionException', + _i75.AlertValidationException => 'AlertValidationException', + _i76.AuthenticationFailedException => 'AuthenticationFailedException', + _i77.DataRightsException => 'DataRightsException', + _i78.EndpointDisabledException => 'EndpointDisabledException', + _i79.EnrollmentException => 'EnrollmentException', + _i80.MfaEnrollmentRequiredException => 'MfaEnrollmentRequiredException', + _i81.MfaRequiredException => 'MfaRequiredException', + _i82.NoticeDeliveryException => 'NoticeDeliveryException', + _i83.OtpRequestException => 'OtpRequestException', + _i84.SessionExpiredException => 'SessionExpiredException', + _i85.MicroArea => 'MicroArea', + _i86.OtpChallenge => 'OtpChallenge', + _i87.Patient => 'Patient', + _i88.PushToken => 'PushToken', + _i89.ScheduledNotice => 'ScheduledNotice', + _i90.StaffAccount => 'StaffAccount', + _i91.TermsVersion => 'TermsVersion', + _i92.TriageAnswer => 'TriageAnswer', + _i93.TriageSession => 'TriageSession', + _i94.Ubs => 'Ubs', + _i95.User => 'User', + _i96.UserCredential => 'UserCredential', + _i97.Visit => 'Visit', _ => null, }; } @@ -3888,139 +3906,141 @@ class Protocol extends _i1.SerializationManagerServer { return 'AdminStaffMfaResetResult'; case _i29.AlertAckResult(): return 'AlertAckResult'; - case _i30.AlertStatusResult(): + case _i30.AlertResolveResult(): + return 'AlertResolveResult'; + case _i31.AlertStatusResult(): return 'AlertStatusResult'; - case _i31.DevelopmentLoginResult(): + case _i32.DevelopmentLoginResult(): return 'DevelopmentLoginResult'; - case _i32.EnrollmentResult(): + case _i33.EnrollmentResult(): return 'EnrollmentResult'; - case _i33.EnrollmentTokenResult(): + case _i34.EnrollmentTokenResult(): return 'EnrollmentTokenResult'; - case _i34.MicroAreaPatient(): + case _i35.MicroAreaPatient(): return 'MicroAreaPatient'; - case _i35.NoticeHistoryEntry(): + case _i36.NoticeHistoryEntry(): return 'NoticeHistoryEntry'; - case _i36.NoticeHistoryPage(): + case _i37.NoticeHistoryPage(): return 'NoticeHistoryPage'; - case _i37.NoticeSendResult(): + case _i38.NoticeSendResult(): return 'NoticeSendResult'; - case _i38.PatientConsentRecord(): + case _i39.PatientConsentRecord(): return 'PatientConsentRecord'; - case _i39.PatientDataOverview(): + case _i40.PatientDataOverview(): return 'PatientDataOverview'; - case _i40.PatientDataSubjectRequestRecord(): + case _i41.PatientDataSubjectRequestRecord(): return 'PatientDataSubjectRequestRecord'; - case _i41.PatientRiskEvent(): + case _i42.PatientRiskEvent(): return 'PatientRiskEvent'; - case _i42.RedAlertResult(): + case _i43.RedAlertResult(): return 'RedAlertResult'; - case _i43.ServiceHealth(): + case _i44.ServiceHealth(): return 'ServiceHealth'; - case _i44.TermsChangeNotice(): + case _i45.TermsChangeNotice(): return 'TermsChangeNotice'; - case _i45.TermsVersionEntry(): + case _i46.TermsVersionEntry(): return 'TermsVersionEntry'; - case _i46.TotpEnrollmentStart(): + case _i47.TotpEnrollmentStart(): return 'TotpEnrollmentStart'; - case _i47.TriageResult(): + case _i48.TriageResult(): return 'TriageResult'; - case _i48.UbsContact(): + case _i49.UbsContact(): return 'UbsContact'; - case _i49.VisitSyncEntry(): + case _i50.VisitSyncEntry(): return 'VisitSyncEntry'; - case _i50.VisitSyncResult(): + case _i51.VisitSyncResult(): return 'VisitSyncResult'; - case _i51.AuditChainCheckpoint(): + case _i52.AuditChainCheckpoint(): return 'AuditChainCheckpoint'; - case _i52.AuditExport(): + case _i53.AuditExport(): return 'AuditExport'; - case _i53.AuditLog(): + case _i54.AuditLog(): return 'AuditLog'; - case _i54.ConsentLog(): + case _i55.ConsentLog(): return 'ConsentLog'; - case _i55.DataSubjectRequest(): + case _i56.DataSubjectRequest(): return 'DataSubjectRequest'; - case _i56.EnrollmentToken(): + case _i57.EnrollmentToken(): return 'EnrollmentToken'; - case _i57.AlertStatus(): + case _i58.AlertStatus(): return 'AlertStatus'; - case _i58.ArrivalMethod(): + case _i59.ArrivalMethod(): return 'ArrivalMethod'; - case _i59.AuditChainReason(): + case _i60.AuditChainReason(): return 'AuditChainReason'; - case _i60.AuditIntegrityState(): + case _i61.AuditIntegrityState(): return 'AuditIntegrityState'; - case _i61.AuditRowSeal(): + case _i62.AuditRowSeal(): return 'AuditRowSeal'; - case _i62.ConsentPurpose(): + case _i63.ConsentPurpose(): return 'ConsentPurpose'; - case _i63.DataSubjectRequestStatus(): + case _i64.DataSubjectRequestStatus(): return 'DataSubjectRequestStatus'; - case _i64.DataSubjectRequestType(): + case _i65.DataSubjectRequestType(): return 'DataSubjectRequestType'; - case _i65.EscalationReason(): + case _i66.EscalationReason(): return 'EscalationReason'; - case _i66.RiskLevel(): + case _i67.RiskLevel(): return 'RiskLevel'; - case _i67.ScheduledNoticeStatus(): + case _i68.ScheduledNoticeStatus(): return 'ScheduledNoticeStatus'; - case _i68.SyncStatus(): + case _i69.SyncStatus(): return 'SyncStatus'; - case _i69.UserRole(): + case _i70.UserRole(): return 'UserRole'; - case _i70.VisitAuthorship(): + case _i71.VisitAuthorship(): return 'VisitAuthorship'; - case _i71.AdminInvalidRequestException(): + case _i72.AdminInvalidRequestException(): return 'AdminInvalidRequestException'; - case _i72.AlertDispatchUnavailableException(): + case _i73.AlertDispatchUnavailableException(): return 'AlertDispatchUnavailableException'; - case _i73.AlertPermissionException(): + case _i74.AlertPermissionException(): return 'AlertPermissionException'; - case _i74.AlertValidationException(): + case _i75.AlertValidationException(): return 'AlertValidationException'; - case _i75.AuthenticationFailedException(): + case _i76.AuthenticationFailedException(): return 'AuthenticationFailedException'; - case _i76.DataRightsException(): + case _i77.DataRightsException(): return 'DataRightsException'; - case _i77.EndpointDisabledException(): + case _i78.EndpointDisabledException(): return 'EndpointDisabledException'; - case _i78.EnrollmentException(): + case _i79.EnrollmentException(): return 'EnrollmentException'; - case _i79.MfaEnrollmentRequiredException(): + case _i80.MfaEnrollmentRequiredException(): return 'MfaEnrollmentRequiredException'; - case _i80.MfaRequiredException(): + case _i81.MfaRequiredException(): return 'MfaRequiredException'; - case _i81.NoticeDeliveryException(): + case _i82.NoticeDeliveryException(): return 'NoticeDeliveryException'; - case _i82.OtpRequestException(): + case _i83.OtpRequestException(): return 'OtpRequestException'; - case _i83.SessionExpiredException(): + case _i84.SessionExpiredException(): return 'SessionExpiredException'; - case _i84.MicroArea(): + case _i85.MicroArea(): return 'MicroArea'; - case _i85.OtpChallenge(): + case _i86.OtpChallenge(): return 'OtpChallenge'; - case _i86.Patient(): + case _i87.Patient(): return 'Patient'; - case _i87.PushToken(): + case _i88.PushToken(): return 'PushToken'; - case _i88.ScheduledNotice(): + case _i89.ScheduledNotice(): return 'ScheduledNotice'; - case _i89.StaffAccount(): + case _i90.StaffAccount(): return 'StaffAccount'; - case _i90.TermsVersion(): + case _i91.TermsVersion(): return 'TermsVersion'; - case _i91.TriageAnswer(): + case _i92.TriageAnswer(): return 'TriageAnswer'; - case _i92.TriageSession(): + case _i93.TriageSession(): return 'TriageSession'; - case _i93.Ubs(): + case _i94.Ubs(): return 'Ubs'; - case _i94.User(): + case _i95.User(): return 'User'; - case _i95.UserCredential(): + case _i96.UserCredential(): return 'UserCredential'; - case _i96.Visit(): + case _i97.Visit(): return 'Visit'; } className = _i2.Protocol().getClassNameForObject(data); @@ -4117,206 +4137,209 @@ class Protocol extends _i1.SerializationManagerServer { if (dataClassName == 'AlertAckResult') { return deserialize<_i29.AlertAckResult>(data['data']); } + if (dataClassName == 'AlertResolveResult') { + return deserialize<_i30.AlertResolveResult>(data['data']); + } if (dataClassName == 'AlertStatusResult') { - return deserialize<_i30.AlertStatusResult>(data['data']); + return deserialize<_i31.AlertStatusResult>(data['data']); } if (dataClassName == 'DevelopmentLoginResult') { - return deserialize<_i31.DevelopmentLoginResult>(data['data']); + return deserialize<_i32.DevelopmentLoginResult>(data['data']); } if (dataClassName == 'EnrollmentResult') { - return deserialize<_i32.EnrollmentResult>(data['data']); + return deserialize<_i33.EnrollmentResult>(data['data']); } if (dataClassName == 'EnrollmentTokenResult') { - return deserialize<_i33.EnrollmentTokenResult>(data['data']); + return deserialize<_i34.EnrollmentTokenResult>(data['data']); } if (dataClassName == 'MicroAreaPatient') { - return deserialize<_i34.MicroAreaPatient>(data['data']); + return deserialize<_i35.MicroAreaPatient>(data['data']); } if (dataClassName == 'NoticeHistoryEntry') { - return deserialize<_i35.NoticeHistoryEntry>(data['data']); + return deserialize<_i36.NoticeHistoryEntry>(data['data']); } if (dataClassName == 'NoticeHistoryPage') { - return deserialize<_i36.NoticeHistoryPage>(data['data']); + return deserialize<_i37.NoticeHistoryPage>(data['data']); } if (dataClassName == 'NoticeSendResult') { - return deserialize<_i37.NoticeSendResult>(data['data']); + return deserialize<_i38.NoticeSendResult>(data['data']); } if (dataClassName == 'PatientConsentRecord') { - return deserialize<_i38.PatientConsentRecord>(data['data']); + return deserialize<_i39.PatientConsentRecord>(data['data']); } if (dataClassName == 'PatientDataOverview') { - return deserialize<_i39.PatientDataOverview>(data['data']); + return deserialize<_i40.PatientDataOverview>(data['data']); } if (dataClassName == 'PatientDataSubjectRequestRecord') { - return deserialize<_i40.PatientDataSubjectRequestRecord>(data['data']); + return deserialize<_i41.PatientDataSubjectRequestRecord>(data['data']); } if (dataClassName == 'PatientRiskEvent') { - return deserialize<_i41.PatientRiskEvent>(data['data']); + return deserialize<_i42.PatientRiskEvent>(data['data']); } if (dataClassName == 'RedAlertResult') { - return deserialize<_i42.RedAlertResult>(data['data']); + return deserialize<_i43.RedAlertResult>(data['data']); } if (dataClassName == 'ServiceHealth') { - return deserialize<_i43.ServiceHealth>(data['data']); + return deserialize<_i44.ServiceHealth>(data['data']); } if (dataClassName == 'TermsChangeNotice') { - return deserialize<_i44.TermsChangeNotice>(data['data']); + return deserialize<_i45.TermsChangeNotice>(data['data']); } if (dataClassName == 'TermsVersionEntry') { - return deserialize<_i45.TermsVersionEntry>(data['data']); + return deserialize<_i46.TermsVersionEntry>(data['data']); } if (dataClassName == 'TotpEnrollmentStart') { - return deserialize<_i46.TotpEnrollmentStart>(data['data']); + return deserialize<_i47.TotpEnrollmentStart>(data['data']); } if (dataClassName == 'TriageResult') { - return deserialize<_i47.TriageResult>(data['data']); + return deserialize<_i48.TriageResult>(data['data']); } if (dataClassName == 'UbsContact') { - return deserialize<_i48.UbsContact>(data['data']); + return deserialize<_i49.UbsContact>(data['data']); } if (dataClassName == 'VisitSyncEntry') { - return deserialize<_i49.VisitSyncEntry>(data['data']); + return deserialize<_i50.VisitSyncEntry>(data['data']); } if (dataClassName == 'VisitSyncResult') { - return deserialize<_i50.VisitSyncResult>(data['data']); + return deserialize<_i51.VisitSyncResult>(data['data']); } if (dataClassName == 'AuditChainCheckpoint') { - return deserialize<_i51.AuditChainCheckpoint>(data['data']); + return deserialize<_i52.AuditChainCheckpoint>(data['data']); } if (dataClassName == 'AuditExport') { - return deserialize<_i52.AuditExport>(data['data']); + return deserialize<_i53.AuditExport>(data['data']); } if (dataClassName == 'AuditLog') { - return deserialize<_i53.AuditLog>(data['data']); + return deserialize<_i54.AuditLog>(data['data']); } if (dataClassName == 'ConsentLog') { - return deserialize<_i54.ConsentLog>(data['data']); + return deserialize<_i55.ConsentLog>(data['data']); } if (dataClassName == 'DataSubjectRequest') { - return deserialize<_i55.DataSubjectRequest>(data['data']); + return deserialize<_i56.DataSubjectRequest>(data['data']); } if (dataClassName == 'EnrollmentToken') { - return deserialize<_i56.EnrollmentToken>(data['data']); + return deserialize<_i57.EnrollmentToken>(data['data']); } if (dataClassName == 'AlertStatus') { - return deserialize<_i57.AlertStatus>(data['data']); + return deserialize<_i58.AlertStatus>(data['data']); } if (dataClassName == 'ArrivalMethod') { - return deserialize<_i58.ArrivalMethod>(data['data']); + return deserialize<_i59.ArrivalMethod>(data['data']); } if (dataClassName == 'AuditChainReason') { - return deserialize<_i59.AuditChainReason>(data['data']); + return deserialize<_i60.AuditChainReason>(data['data']); } if (dataClassName == 'AuditIntegrityState') { - return deserialize<_i60.AuditIntegrityState>(data['data']); + return deserialize<_i61.AuditIntegrityState>(data['data']); } if (dataClassName == 'AuditRowSeal') { - return deserialize<_i61.AuditRowSeal>(data['data']); + return deserialize<_i62.AuditRowSeal>(data['data']); } if (dataClassName == 'ConsentPurpose') { - return deserialize<_i62.ConsentPurpose>(data['data']); + return deserialize<_i63.ConsentPurpose>(data['data']); } if (dataClassName == 'DataSubjectRequestStatus') { - return deserialize<_i63.DataSubjectRequestStatus>(data['data']); + return deserialize<_i64.DataSubjectRequestStatus>(data['data']); } if (dataClassName == 'DataSubjectRequestType') { - return deserialize<_i64.DataSubjectRequestType>(data['data']); + return deserialize<_i65.DataSubjectRequestType>(data['data']); } if (dataClassName == 'EscalationReason') { - return deserialize<_i65.EscalationReason>(data['data']); + return deserialize<_i66.EscalationReason>(data['data']); } if (dataClassName == 'RiskLevel') { - return deserialize<_i66.RiskLevel>(data['data']); + return deserialize<_i67.RiskLevel>(data['data']); } if (dataClassName == 'ScheduledNoticeStatus') { - return deserialize<_i67.ScheduledNoticeStatus>(data['data']); + return deserialize<_i68.ScheduledNoticeStatus>(data['data']); } if (dataClassName == 'SyncStatus') { - return deserialize<_i68.SyncStatus>(data['data']); + return deserialize<_i69.SyncStatus>(data['data']); } if (dataClassName == 'UserRole') { - return deserialize<_i69.UserRole>(data['data']); + return deserialize<_i70.UserRole>(data['data']); } if (dataClassName == 'VisitAuthorship') { - return deserialize<_i70.VisitAuthorship>(data['data']); + return deserialize<_i71.VisitAuthorship>(data['data']); } if (dataClassName == 'AdminInvalidRequestException') { - return deserialize<_i71.AdminInvalidRequestException>(data['data']); + return deserialize<_i72.AdminInvalidRequestException>(data['data']); } if (dataClassName == 'AlertDispatchUnavailableException') { - return deserialize<_i72.AlertDispatchUnavailableException>(data['data']); + return deserialize<_i73.AlertDispatchUnavailableException>(data['data']); } if (dataClassName == 'AlertPermissionException') { - return deserialize<_i73.AlertPermissionException>(data['data']); + return deserialize<_i74.AlertPermissionException>(data['data']); } if (dataClassName == 'AlertValidationException') { - return deserialize<_i74.AlertValidationException>(data['data']); + return deserialize<_i75.AlertValidationException>(data['data']); } if (dataClassName == 'AuthenticationFailedException') { - return deserialize<_i75.AuthenticationFailedException>(data['data']); + return deserialize<_i76.AuthenticationFailedException>(data['data']); } if (dataClassName == 'DataRightsException') { - return deserialize<_i76.DataRightsException>(data['data']); + return deserialize<_i77.DataRightsException>(data['data']); } if (dataClassName == 'EndpointDisabledException') { - return deserialize<_i77.EndpointDisabledException>(data['data']); + return deserialize<_i78.EndpointDisabledException>(data['data']); } if (dataClassName == 'EnrollmentException') { - return deserialize<_i78.EnrollmentException>(data['data']); + return deserialize<_i79.EnrollmentException>(data['data']); } if (dataClassName == 'MfaEnrollmentRequiredException') { - return deserialize<_i79.MfaEnrollmentRequiredException>(data['data']); + return deserialize<_i80.MfaEnrollmentRequiredException>(data['data']); } if (dataClassName == 'MfaRequiredException') { - return deserialize<_i80.MfaRequiredException>(data['data']); + return deserialize<_i81.MfaRequiredException>(data['data']); } if (dataClassName == 'NoticeDeliveryException') { - return deserialize<_i81.NoticeDeliveryException>(data['data']); + return deserialize<_i82.NoticeDeliveryException>(data['data']); } if (dataClassName == 'OtpRequestException') { - return deserialize<_i82.OtpRequestException>(data['data']); + return deserialize<_i83.OtpRequestException>(data['data']); } if (dataClassName == 'SessionExpiredException') { - return deserialize<_i83.SessionExpiredException>(data['data']); + return deserialize<_i84.SessionExpiredException>(data['data']); } if (dataClassName == 'MicroArea') { - return deserialize<_i84.MicroArea>(data['data']); + return deserialize<_i85.MicroArea>(data['data']); } if (dataClassName == 'OtpChallenge') { - return deserialize<_i85.OtpChallenge>(data['data']); + return deserialize<_i86.OtpChallenge>(data['data']); } if (dataClassName == 'Patient') { - return deserialize<_i86.Patient>(data['data']); + return deserialize<_i87.Patient>(data['data']); } if (dataClassName == 'PushToken') { - return deserialize<_i87.PushToken>(data['data']); + return deserialize<_i88.PushToken>(data['data']); } if (dataClassName == 'ScheduledNotice') { - return deserialize<_i88.ScheduledNotice>(data['data']); + return deserialize<_i89.ScheduledNotice>(data['data']); } if (dataClassName == 'StaffAccount') { - return deserialize<_i89.StaffAccount>(data['data']); + return deserialize<_i90.StaffAccount>(data['data']); } if (dataClassName == 'TermsVersion') { - return deserialize<_i90.TermsVersion>(data['data']); + return deserialize<_i91.TermsVersion>(data['data']); } if (dataClassName == 'TriageAnswer') { - return deserialize<_i91.TriageAnswer>(data['data']); + return deserialize<_i92.TriageAnswer>(data['data']); } if (dataClassName == 'TriageSession') { - return deserialize<_i92.TriageSession>(data['data']); + return deserialize<_i93.TriageSession>(data['data']); } if (dataClassName == 'Ubs') { - return deserialize<_i93.Ubs>(data['data']); + return deserialize<_i94.Ubs>(data['data']); } if (dataClassName == 'User') { - return deserialize<_i94.User>(data['data']); + return deserialize<_i95.User>(data['data']); } if (dataClassName == 'UserCredential') { - return deserialize<_i95.UserCredential>(data['data']); + return deserialize<_i96.UserCredential>(data['data']); } if (dataClassName == 'Visit') { - return deserialize<_i96.Visit>(data['data']); + return deserialize<_i97.Visit>(data['data']); } if (dataClassName.startsWith('serverpod.')) { data['className'] = dataClassName.substring(10); @@ -4348,42 +4371,42 @@ class Protocol extends _i1.SerializationManagerServer { return _i8.AlertIdempotencyKey.t; case _i9.AlertOutboxEntry: return _i9.AlertOutboxEntry.t; - case _i51.AuditChainCheckpoint: - return _i51.AuditChainCheckpoint.t; - case _i52.AuditExport: - return _i52.AuditExport.t; - case _i53.AuditLog: - return _i53.AuditLog.t; - case _i54.ConsentLog: - return _i54.ConsentLog.t; - case _i55.DataSubjectRequest: - return _i55.DataSubjectRequest.t; - case _i56.EnrollmentToken: - return _i56.EnrollmentToken.t; - case _i84.MicroArea: - return _i84.MicroArea.t; - case _i85.OtpChallenge: - return _i85.OtpChallenge.t; - case _i86.Patient: - return _i86.Patient.t; - case _i87.PushToken: - return _i87.PushToken.t; - case _i88.ScheduledNotice: - return _i88.ScheduledNotice.t; - case _i89.StaffAccount: - return _i89.StaffAccount.t; - case _i90.TermsVersion: - return _i90.TermsVersion.t; - case _i92.TriageSession: - return _i92.TriageSession.t; - case _i93.Ubs: - return _i93.Ubs.t; - case _i94.User: - return _i94.User.t; - case _i95.UserCredential: - return _i95.UserCredential.t; - case _i96.Visit: - return _i96.Visit.t; + case _i52.AuditChainCheckpoint: + return _i52.AuditChainCheckpoint.t; + case _i53.AuditExport: + return _i53.AuditExport.t; + case _i54.AuditLog: + return _i54.AuditLog.t; + case _i55.ConsentLog: + return _i55.ConsentLog.t; + case _i56.DataSubjectRequest: + return _i56.DataSubjectRequest.t; + case _i57.EnrollmentToken: + return _i57.EnrollmentToken.t; + case _i85.MicroArea: + return _i85.MicroArea.t; + case _i86.OtpChallenge: + return _i86.OtpChallenge.t; + case _i87.Patient: + return _i87.Patient.t; + case _i88.PushToken: + return _i88.PushToken.t; + case _i89.ScheduledNotice: + return _i89.ScheduledNotice.t; + case _i90.StaffAccount: + return _i90.StaffAccount.t; + case _i91.TermsVersion: + return _i91.TermsVersion.t; + case _i93.TriageSession: + return _i93.TriageSession.t; + case _i94.Ubs: + return _i94.Ubs.t; + case _i95.User: + return _i95.User.t; + case _i96.UserCredential: + return _i96.UserCredential.t; + case _i97.Visit: + return _i97.Visit.t; } return null; } diff --git a/backend/sinalacs_server/lib/src/generated/protocol.yaml b/backend/sinalacs_server/lib/src/generated/protocol.yaml index 626e518f..2faae5f8 100644 --- a/backend/sinalacs_server/lib/src/generated/protocol.yaml +++ b/backend/sinalacs_server/lib/src/generated/protocol.yaml @@ -31,6 +31,7 @@ admin: alerts: - createRedAlert: - acknowledge: + - resolve: - statusFor: auth: - developmentLogin: diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart index 6a4f551a..9062cfee 100644 --- a/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart @@ -112,18 +112,25 @@ class OrmAlertStore implements AlertStore { final session = _session(); final alertUuid = UuidValue.fromString(alertId); final acsUuid = UuidValue.fromString(acsId); - final microAreaUuid = UuidValue.fromString(microAreaId); Future run(Transaction transaction) async { // O predicado de microárea é a barreira de territorialização: um ACS // nunca confirma alerta fora do seu território (INV-01). - final alert = await Alert.db.findFirstRow( - session, - where: (t) => - t.id.equals(alertUuid) & t.microAreaId.equals(microAreaUuid), + // Trava antes de ler (#76): um `resolve` concorrente que já gravou + // `resolved` é visto aqui, em vez de ser sobrescrito por um ACK que leu + // a linha antes dele. + final travado = await session.db.unsafeQuery( + ''' + SELECT id FROM alerts + WHERE id = @id::uuid AND "microAreaId" = @ma::uuid + FOR UPDATE + ''', + parameters: QueryParameters.named({'id': alertId, 'ma': microAreaId}), transaction: transaction, ); - if (alert == null) return false; + if (travado.isEmpty) return false; + final alert = + (await Alert.db.findById(session, alertUuid, transaction: transaction))!; final now = DateTime.now().toUtc(); await Alert.db.updateRow( @@ -132,7 +139,11 @@ class OrmAlertStore implements AlertStore { acsId: acsUuid, receivedAt: alert.receivedAt ?? now, acknowledgedAt: alert.acknowledgedAt ?? now, - status: AlertStatus.acknowledged, + // ACK depois da conclusão (reentrega, outro aparelho) não rebaixa + // o atendimento já concluído (#76). + status: alert.status == AlertStatus.resolved + ? AlertStatus.resolved + : AlertStatus.acknowledged, ), // Só as colunas do ACK: a linha foi lida sem lock, e um escalonamento // (#44) gravado entre a leitura e esta escrita seria apagado por um @@ -177,8 +188,70 @@ class OrmAlertStore implements AlertStore { required String microAreaId, required DateTime now, required AuditEvent Function(AlertResolveOutcome outcome) auditFor, - }) => - throw UnimplementedError('Task 2 (#76)'); + }) async { + // Id que não é UUID é "não existe", não erro de servidor: mesma resposta + // de um alerta de outra microárea. + if (!Uuid.isValidUUID(fromString: alertId) || + !Uuid.isValidUUID(fromString: microAreaId)) { + return const AlertResolveAttempt(AlertResolveOutcome.notFound); + } + final session = _session(); + + Future run(Transaction transaction) async { + // Escopo e trava numa instrução. O predicado de microárea é a barreira + // de territorialização (INV-01); o FOR UPDATE serializa com o ACK e com + // o escalonamento, que releem o status sob a mesma trava. + final linhas = await session.db.unsafeQuery( + ''' + SELECT status, "resolvedAt" + FROM alerts + WHERE id = @id::uuid AND "microAreaId" = @ma::uuid + FOR UPDATE + ''', + parameters: QueryParameters.named({'id': alertId, 'ma': microAreaId}), + transaction: transaction, + ); + if (linhas.isEmpty) { + return const AlertResolveAttempt(AlertResolveOutcome.notFound); + } + + final status = linhas.single[0] as String; + final AlertResolveAttempt attempt; + if (status == AlertStatus.resolved.name) { + attempt = AlertResolveAttempt( + AlertResolveOutcome.alreadyResolved, + resolvedAt: (linhas.single[1] as DateTime?)?.toUtc(), + ); + } else if (status != AlertStatus.acknowledged.name) { + attempt = const AlertResolveAttempt(AlertResolveOutcome.notAcknowledged); + } else { + final agora = now.toUtc(); + // Só status e resolvedAt: as colunas do ACK e do escalonamento ficam. + await session.db.unsafeExecute( + ''' + UPDATE alerts + SET status = @resolvido, "resolvedAt" = @agora::timestamp + WHERE id = @id::uuid + ''', + parameters: QueryParameters.named({ + 'id': alertId, + 'resolvido': AlertStatus.resolved.name, + 'agora': agora, + }), + transaction: transaction, + ); + attempt = AlertResolveAttempt(AlertResolveOutcome.resolved, resolvedAt: agora); + } + + // Fail-closed e por último, como no ACK: a trava da cadeia nunca + // precede outra. + await _appendAudit(session, transaction, auditFor(attempt.outcome)); + return attempt; + } + + final existing = _transaction; + return existing != null ? run(existing) : session.db.transaction(run); + } @override Future latestForPatient(String patientId) async { @@ -197,6 +270,7 @@ class OrmAlertStore implements AlertStore { status: row.status, triggeredAt: row.triggeredAt, acknowledgedAt: row.acknowledgedAt, + resolvedAt: row.resolvedAt, ); } } diff --git a/backend/sinalacs_server/lib/src/models/alert.spy.yaml b/backend/sinalacs_server/lib/src/models/alert.spy.yaml index 71ed47fb..74195846 100644 --- a/backend/sinalacs_server/lib/src/models/alert.spy.yaml +++ b/backend/sinalacs_server/lib/src/models/alert.spy.yaml @@ -14,6 +14,9 @@ fields: receivedAt: DateTime? respondedAt: DateTime? acknowledgedAt: DateTime? + ### Atendimento concluído pelo ACS (#76): carimbado uma única vez, na + ### transição acknowledged → resolved. Nulo enquanto não foi atendido. + resolvedAt: DateTime? riskLevel: RiskLevel locationHash: String ### Célula geográfica de baixa resolução para o mapa do ACS (~1,1 km). diff --git a/backend/sinalacs_server/lib/src/models/api/alert_resolve_result.spy.yaml b/backend/sinalacs_server/lib/src/models/api/alert_resolve_result.spy.yaml new file mode 100644 index 00000000..c08768be --- /dev/null +++ b/backend/sinalacs_server/lib/src/models/api/alert_resolve_result.spy.yaml @@ -0,0 +1,11 @@ +### Resultado de alerts.resolve (#76). +### +### Mesmo desenho de AlertAckResult: resolved: false significa que não havia +### alerta com esse id na microárea do ACS. Concluir duas vezes devolve +### resolved: true com o resolvedAt da primeira conclusão. +class: AlertResolveResult +fields: + alertId: String + resolved: bool + status: AlertStatus? + resolvedAt: DateTime? diff --git a/backend/sinalacs_server/lib/src/models/api/alert_status_result.spy.yaml b/backend/sinalacs_server/lib/src/models/api/alert_status_result.spy.yaml index 9e3a0df5..985651f2 100644 --- a/backend/sinalacs_server/lib/src/models/api/alert_status_result.spy.yaml +++ b/backend/sinalacs_server/lib/src/models/api/alert_status_result.spy.yaml @@ -11,3 +11,4 @@ fields: status: AlertStatus? triggeredAt: DateTime? acknowledgedAt: DateTime? + resolvedAt: DateTime? diff --git a/backend/sinalacs_server/migrations/20261016155336452/definition.json b/backend/sinalacs_server/migrations/20261016155336452/definition.json new file mode 100644 index 00000000..c7c809c4 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016155336452/definition.json @@ -0,0 +1,4529 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "moduleName": "sinalacs", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "acs", + "dartName": "Acs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSyncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_refresh_tokens", + "dartName": "AcsRefreshToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "familyId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idleExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "absoluteExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_refresh_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_family_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "familyId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_upload_tokens", + "dartName": "AcsUploadToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_upload_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_user_device_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deviceId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_deliveries", + "dartName": "AlertDeliveryRecord", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_alert_acs_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "alertId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "acsId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_idempotency_keys", + "dartName": "AlertIdempotencyKey", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_idempotency_keys_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_key_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_outbox", + "dartName": "AlertOutboxEntry", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "topic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_outbox_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pending_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publishedAt" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alerts", + "dartName": "Alert", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggeredAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "respondedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolvedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevel", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationCell", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AlertStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mqttTopic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "retryCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalationReason", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:EscalationReason?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_micro_area_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggeredAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_chain_checkpoints", + "dartName": "AuditChainCheckpoint", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_exports", + "dartName": "AuditExport", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "filtersJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "format", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentSha256", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chainState", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_exports_fk_0", + "columns": [ + "requestedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_requested_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_logs", + "dartName": "AuditLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actionType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "result", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_user_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "consent_logs", + "dartName": "ConsentLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purpose", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userAgent", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "consent_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "consent_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "data_subject_requests", + "dartName": "DataSubjectRequest", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestType", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestType" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsEncrypted", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsKeyVersion", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dueAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_1", + "columns": [ + "decidedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_user_id_type_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestType" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "enrollment_tokens", + "dartName": "EnrollmentToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdByAcsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_1", + "columns": [ + "createdByAcsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_token_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "micro_areas", + "dartName": "MicroArea", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "geoJsonBoundary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "otp_challenges", + "dartName": "OtpChallenge", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "codeHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "otp_challenges_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_user_id_created_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "patients", + "dartName": "Patient", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isChronic", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastLocationHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastTriageAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "patients_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "push_tokens", + "dartName": "PushToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "token", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "platform", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "push_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_token_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "token" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_user_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "scheduled_notices", + "dartName": "ScheduledNotice", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "audience", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledFor", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScheduledNoticeStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recipients", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accepted", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failureReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sentAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_1", + "columns": [ + "microAreaId" + ], + "referenceTable": "micro_areas", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "scheduledFor" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "staff_accounts", + "dartName": "StaffAccount", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeExpiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedBy", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "terms_versions", + "dartName": "TermsVersion", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effectiveFrom", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_1", + "columns": [ + "legallyReviewedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_2", + "columns": [ + "publishedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_version_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "version" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "triage_sessions", + "dartName": "TriageSession", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "triage_sessions_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "triage_sessions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ubs", + "dartName": "Ubs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "address", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "city", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contactPhone", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ubs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_credentials", + "dartName": "UserCredential", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordSalt", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "memoryKb", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "iterations", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parallelism", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failedAttempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockedUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockStreak", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpSecretEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpEnabledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpLastStep", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_credentials_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_user_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "users", + "dartName": "User", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cpfHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "birthDate", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UserRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_cpf_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "cpfHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_micro_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "visits", + "dartName": "Visit", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorship", + "columnType": 0, + "isNullable": false, + "columnDefault": "'acs'::text", + "dartType": "protocol:VisitAuthorship" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "originDeviceId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncStatus", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SyncStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "arrivalMethod", + "columnType": 0, + "isNullable": false, + "columnDefault": "'manual'::text", + "dartType": "protocol:ArrivalMethod" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "localId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_local_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "localId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage", + "dartName": "CloudStorageEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_cloud_storage_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "addedTime", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "byteData", + "columnType": 5, + "isNullable": false, + "dartType": "dart:typed_data:ByteData" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "verified", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_path_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_expiration", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "expiration" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_cloud_storage_direct_upload", + "dartName": "CloudStorageDirectUploadEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_cloud_storage_direct_upload_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "storageId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "path", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiration", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authKey", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_upload_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_cloud_storage_direct_upload_storage_path", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "storageId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "path" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_future_call", + "dartName": "FutureCallEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_future_call_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serializedObject", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "identifier", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_serverId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_future_call_identifier_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "identifier" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_connection_info", + "dartName": "ServerHealthConnectionInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_health_connection_info_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "closing", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idle", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_connection_info_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_connection_info_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_health_metric", + "dartName": "ServerHealthMetric", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_health_metric_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isHealthy", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "value", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "granularity", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_metric_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_health_metric_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "name" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "granularity" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_log", + "dartName": "LogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "reference", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logLevel", + "columnType": 6, + "isNullable": false, + "dartType": "protocol:LogLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_message_log", + "dartName": "MessageLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_message_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageName", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_message_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_message_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_method", + "dartName": "MethodInfo", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_method_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_method_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_method_endpoint_method_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "endpoint" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "method" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_migrations", + "dartName": "DatabaseMigrationVersion", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_migrations_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_migrations_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_migrations_ids", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "module" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_query_log", + "dartName": "QueryLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_query_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sessionLogId", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "messageId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "query", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": false, + "dartType": "double" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numRows", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "order", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "serverpod_query_log_fk_0", + "columns": [ + "sessionLogId" + ], + "referenceTable": "serverpod_session_log", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 4 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_query_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_query_log_sessionLogId_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sessionLogId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_readwrite_test", + "dartName": "ReadWriteTestEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_readwrite_test_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "number", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_readwrite_test_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_runtime_settings", + "dartName": "RuntimeSettings", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_runtime_settings_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettings", + "columnType": 8, + "isNullable": false, + "dartType": "protocol:LogSettings" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logSettingsOverrides", + "columnType": 8, + "isNullable": false, + "dartType": "List" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logServiceCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "logMalformedCalls", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_runtime_settings_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "serverpod_session_log", + "dartName": "SessionLogEntry", + "module": "serverpod", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 6, + "isNullable": false, + "columnDefault": "nextval('serverpod_session_log_id_seq'::regclass)", + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "serverId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "time", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "module", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "endpoint", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "method", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "duration", + "columnType": 3, + "isNullable": true, + "dartType": "double?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "numQueries", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "slow", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "error", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "stackTrace", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authenticatedUserId", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isOpen", + "columnType": 1, + "isNullable": true, + "dartType": "bool?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "touched", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_serverid_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "serverId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_time_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "time" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_touched_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "touched" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "serverpod_session_log_isopen_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "isOpen" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "sinalacs", + "version": "20261016155336452" + }, + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260129180959368" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20261016155336452/definition.sql b/backend/sinalacs_server/migrations/20261016155336452/definition.sql new file mode 100644 index 00000000..65e24b75 --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016155336452/definition.sql @@ -0,0 +1,939 @@ +BEGIN; + +-- +-- Class Acs as table acs +-- +CREATE TABLE "acs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "enrollmentId" text NOT NULL, + "ubsId" uuid NOT NULL, + "active" boolean NOT NULL, + "lastSyncAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "acs_ubs_idx" ON "acs" USING btree ("ubsId"); +CREATE UNIQUE INDEX "acs_enrollment_id_key" ON "acs" USING btree ("enrollmentId"); + +-- +-- Class AcsRefreshToken as table acs_refresh_tokens +-- +CREATE TABLE "acs_refresh_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "familyId" uuid NOT NULL, + "tokenHash" text NOT NULL, + "deviceId" text NOT NULL, + "issuedAt" timestamp without time zone NOT NULL, + "idleExpiresAt" timestamp without time zone NOT NULL, + "absoluteExpiresAt" timestamp without time zone NOT NULL, + "rotatedAt" timestamp without time zone, + "revokedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "acs_refresh_tokens_hash_key" ON "acs_refresh_tokens" USING btree ("tokenHash"); +CREATE INDEX "acs_refresh_tokens_family_idx" ON "acs_refresh_tokens" USING btree ("familyId"); +CREATE INDEX "acs_refresh_tokens_user_idx" ON "acs_refresh_tokens" USING btree ("userId"); + +-- +-- Class AcsUploadToken as table acs_upload_tokens +-- +CREATE TABLE "acs_upload_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "tokenHash" text NOT NULL, + "deviceId" text NOT NULL, + "issuedAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "revokedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "acs_upload_tokens_hash_key" ON "acs_upload_tokens" USING btree ("tokenHash"); +CREATE INDEX "acs_upload_tokens_user_device_idx" ON "acs_upload_tokens" USING btree ("userId", "deviceId"); + +-- +-- Class AlertDeliveryRecord as table alert_deliveries +-- +CREATE TABLE "alert_deliveries" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "alertId" uuid NOT NULL, + "acsId" uuid NOT NULL, + "acknowledgedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "alert_deliveries_alert_acs_key" ON "alert_deliveries" USING btree ("alertId", "acsId"); + +-- +-- Class AlertIdempotencyKey as table alert_idempotency_keys +-- +CREATE TABLE "alert_idempotency_keys" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "key" text NOT NULL, + "alertId" uuid NOT NULL, + "locationHash" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "alert_idempotency_keys_key_key" ON "alert_idempotency_keys" USING btree ("key"); + +-- +-- Class AlertOutboxEntry as table alert_outbox +-- +CREATE TABLE "alert_outbox" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "alertId" uuid NOT NULL, + "topic" text NOT NULL, + "payload" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "publishedAt" timestamp without time zone, + "attempts" bigint NOT NULL, + "nextAttemptAt" timestamp without time zone NOT NULL, + "lastError" text +); + +-- Indexes +CREATE INDEX "alert_outbox_pending_idx" ON "alert_outbox" USING btree ("publishedAt", "nextAttemptAt"); + +-- +-- Class Alert as table alerts +-- +CREATE TABLE "alerts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "acsId" uuid, + "microAreaId" uuid, + "triggeredAt" timestamp without time zone NOT NULL, + "receivedAt" timestamp without time zone, + "respondedAt" timestamp without time zone, + "acknowledgedAt" timestamp without time zone, + "resolvedAt" timestamp without time zone, + "riskLevel" text NOT NULL, + "locationHash" text NOT NULL, + "locationCell" text, + "status" text NOT NULL, + "mqttTopic" text NOT NULL, + "deviceId" text NOT NULL, + "retryCount" bigint NOT NULL, + "version" bigint NOT NULL, + "escalatedAt" timestamp without time zone, + "escalatedBy" uuid, + "escalationReason" text +); + +-- Indexes +CREATE INDEX "alerts_micro_area_status_idx" ON "alerts" USING btree ("microAreaId", "status", "triggeredAt"); + +-- +-- Class AuditChainCheckpoint as table audit_chain_checkpoints +-- +CREATE TABLE "audit_chain_checkpoints" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "sequence" bigint NOT NULL, + "entryHash" text NOT NULL, + "signature" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "audit_chain_checkpoints_sequence_idx" ON "audit_chain_checkpoints" USING btree ("sequence"); + +-- +-- Class AuditExport as table audit_exports +-- +CREATE TABLE "audit_exports" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "requestedBy" uuid NOT NULL, + "requestedAt" timestamp without time zone NOT NULL, + "filtersJson" text NOT NULL, + "format" text NOT NULL, + "rowCount" bigint NOT NULL, + "firstSequence" bigint, + "lastSequence" bigint, + "contentSha256" text NOT NULL, + "chainState" text NOT NULL +); + +-- Indexes +CREATE INDEX "audit_exports_requested_at_idx" ON "audit_exports" USING btree ("requestedAt"); + +-- +-- Class AuditLog as table audit_logs +-- +CREATE TABLE "audit_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "actionType" text NOT NULL, + "resourceType" text NOT NULL, + "resourceId" uuid, + "timestamp" timestamp without time zone NOT NULL, + "ipHash" text NOT NULL, + "result" text NOT NULL, + "sequence" bigint NOT NULL, + "previousHash" text NOT NULL, + "entryHash" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "audit_logs_sequence_idx" ON "audit_logs" USING btree ("sequence"); +CREATE INDEX "audit_logs_timestamp_idx" ON "audit_logs" USING btree ("timestamp"); +CREATE INDEX "audit_logs_user_sequence_idx" ON "audit_logs" USING btree ("userId", "sequence"); + +-- +-- Class ConsentLog as table consent_logs +-- +CREATE TABLE "consent_logs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "purpose" text NOT NULL, + "action" text NOT NULL, + "version" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "ipHash" text NOT NULL, + "userAgent" text NOT NULL, + "signature" text NOT NULL +); + +-- +-- Class DataSubjectRequest as table data_subject_requests +-- +CREATE TABLE "data_subject_requests" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "requestType" text NOT NULL, + "detailsEncrypted" text NOT NULL, + "detailsKeyVersion" bigint NOT NULL, + "status" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "dueAt" timestamp without time zone NOT NULL, + "decidedAt" timestamp without time zone, + "decidedBy" uuid, + "resolutionEncrypted" text, + "resolutionKeyVersion" bigint +); + +-- Indexes +CREATE INDEX "data_subject_requests_user_id_type_idx" ON "data_subject_requests" USING btree ("userId", "requestType"); + +-- +-- Class EnrollmentToken as table enrollment_tokens +-- +CREATE TABLE "enrollment_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "tokenHash" text NOT NULL, + "patientId" uuid NOT NULL, + "microAreaId" uuid NOT NULL, + "createdByAcsId" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "consumedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "enrollment_tokens_token_hash_key" ON "enrollment_tokens" USING btree ("tokenHash"); + +-- +-- Class MicroArea as table micro_areas +-- +CREATE TABLE "micro_areas" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "name" text NOT NULL, + "ubsId" uuid NOT NULL, + "geoJsonBoundary" text NOT NULL +); + +-- Indexes +CREATE INDEX "micro_areas_ubs_idx" ON "micro_areas" USING btree ("ubsId"); + +-- +-- Class OtpChallenge as table otp_challenges +-- +CREATE TABLE "otp_challenges" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "codeHash" text NOT NULL, + "attempts" bigint NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "expiresAt" timestamp without time zone NOT NULL, + "consumedAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "otp_challenges_user_id_created_at_idx" ON "otp_challenges" USING btree ("userId", "createdAt"); + +-- +-- Class Patient as table patients +-- +CREATE TABLE "patients" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "emergencyContactEncrypted" text NOT NULL DEFAULT ''::text, + "emergencyContactKeyVersion" bigint NOT NULL DEFAULT 1, + "isChronic" boolean NOT NULL, + "chronicConditionsEncrypted" text NOT NULL DEFAULT ''::text, + "chronicConditionsKeyVersion" bigint NOT NULL DEFAULT 1, + "lastLocationHash" text, + "lastTriageAt" timestamp without time zone +); + +-- +-- Class PushToken as table push_tokens +-- +CREATE TABLE "push_tokens" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "microAreaId" uuid, + "token" text NOT NULL, + "platform" text NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "push_tokens_token_key" ON "push_tokens" USING btree ("token"); +CREATE INDEX "push_tokens_user_id_idx" ON "push_tokens" USING btree ("userId"); + +-- +-- Class ScheduledNotice as table scheduled_notices +-- +CREATE TABLE "scheduled_notices" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "createdBy" uuid NOT NULL, + "microAreaId" uuid NOT NULL, + "audience" text NOT NULL, + "title" text NOT NULL, + "message" text NOT NULL, + "scheduledFor" timestamp without time zone NOT NULL, + "status" text NOT NULL, + "recipients" bigint, + "accepted" bigint, + "failureReason" text, + "createdAt" timestamp without time zone NOT NULL, + "sentAt" timestamp without time zone +); + +-- Indexes +CREATE INDEX "scheduled_notices_due_idx" ON "scheduled_notices" USING btree ("status", "scheduledFor"); +CREATE INDEX "scheduled_notices_area_idx" ON "scheduled_notices" USING btree ("microAreaId", "createdAt"); + +-- +-- Class StaffAccount as table staff_accounts +-- +CREATE TABLE "staff_accounts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "enrollmentId" text NOT NULL, + "active" boolean NOT NULL, + "ubsId" uuid, + "activationCodeHash" text, + "activationCodeExpiresAt" timestamp without time zone, + "activationCodeIssuedBy" text, + "activationCodeIssuedAt" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "staff_accounts_enrollment_id_key" ON "staff_accounts" USING btree ("enrollmentId"); + +-- +-- Class TermsVersion as table terms_versions +-- +CREATE TABLE "terms_versions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "version" text NOT NULL, + "summary" text NOT NULL, + "effectiveFrom" timestamp without time zone NOT NULL, + "createdBy" uuid NOT NULL, + "createdAt" timestamp without time zone NOT NULL, + "legallyReviewedAt" timestamp without time zone, + "legallyReviewedBy" uuid, + "publishedAt" timestamp without time zone, + "publishedBy" uuid +); + +-- Indexes +CREATE UNIQUE INDEX "terms_versions_version_key" ON "terms_versions" USING btree ("version"); + +-- +-- Class TriageSession as table triage_sessions +-- +CREATE TABLE "triage_sessions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "answersEncrypted" text NOT NULL DEFAULT ''::text, + "answersKeyVersion" bigint NOT NULL DEFAULT 1, + "resultRiskEncrypted" text NOT NULL DEFAULT ''::text, + "resultRiskKeyVersion" bigint NOT NULL DEFAULT 1, + "createdAt" timestamp without time zone NOT NULL, + "deviceId" text NOT NULL +); + +-- +-- Class Ubs as table ubs +-- +CREATE TABLE "ubs" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "name" text NOT NULL, + "address" text NOT NULL, + "city" text NOT NULL, + "state" text NOT NULL, + "contactPhone" text +); + +-- +-- Class UserCredential as table user_credentials +-- +CREATE TABLE "user_credentials" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "userId" uuid NOT NULL, + "passwordHash" text NOT NULL, + "passwordSalt" text NOT NULL, + "memoryKb" bigint NOT NULL, + "iterations" bigint NOT NULL, + "parallelism" bigint NOT NULL, + "failedAttempts" bigint NOT NULL, + "lockedUntil" timestamp without time zone, + "lockStreak" bigint NOT NULL DEFAULT 0, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL, + "totpSecretEncrypted" text, + "totpKeyVersion" bigint, + "totpEnabledAt" timestamp without time zone, + "totpLastStep" bigint +); + +-- Indexes +CREATE UNIQUE INDEX "user_credentials_user_id_key" ON "user_credentials" USING btree ("userId"); + +-- +-- Class User as table users +-- +CREATE TABLE "users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "cpfHash" text NOT NULL, + "name" text NOT NULL, + "birthDate" timestamp without time zone NOT NULL, + "role" text NOT NULL, + "microAreaId" uuid, + "createdAt" timestamp without time zone NOT NULL, + "updatedAt" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "users_cpf_hash_key" ON "users" USING btree ("cpfHash"); +CREATE INDEX "users_micro_area_idx" ON "users" USING btree ("microAreaId"); + +-- +-- Class Visit as table visits +-- +CREATE TABLE "visits" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(), + "patientId" uuid NOT NULL, + "acsId" uuid, + "authorship" text NOT NULL DEFAULT 'acs'::text, + "originDeviceId" text, + "scheduledAt" timestamp without time zone NOT NULL, + "startedAt" timestamp without time zone, + "completedAt" timestamp without time zone, + "status" text NOT NULL, + "riskLevelBeforeEncrypted" text NOT NULL DEFAULT ''::text, + "riskLevelBeforeKeyVersion" bigint NOT NULL DEFAULT 1, + "riskLevelAfterEncrypted" text NOT NULL DEFAULT ''::text, + "riskLevelAfterKeyVersion" bigint NOT NULL DEFAULT 1, + "notesEncrypted" text NOT NULL DEFAULT ''::text, + "notesKeyVersion" bigint NOT NULL DEFAULT 1, + "syncStatus" text NOT NULL, + "arrivalMethod" text NOT NULL DEFAULT 'manual'::text, + "localId" uuid NOT NULL, + "syncAt" timestamp without time zone, + "version" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "visits_local_id_key" ON "visits" USING btree ("localId"); + +-- +-- Class CloudStorageEntry as table serverpod_cloud_storage +-- +CREATE TABLE "serverpod_cloud_storage" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "addedTime" timestamp without time zone NOT NULL, + "expiration" timestamp without time zone, + "byteData" bytea NOT NULL, + "verified" boolean NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_path_idx" ON "serverpod_cloud_storage" USING btree ("storageId", "path"); +CREATE INDEX "serverpod_cloud_storage_expiration" ON "serverpod_cloud_storage" USING btree ("expiration"); + +-- +-- Class CloudStorageDirectUploadEntry as table serverpod_cloud_storage_direct_upload +-- +CREATE TABLE "serverpod_cloud_storage_direct_upload" ( + "id" bigserial PRIMARY KEY, + "storageId" text NOT NULL, + "path" text NOT NULL, + "expiration" timestamp without time zone NOT NULL, + "authKey" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_cloud_storage_direct_upload_storage_path" ON "serverpod_cloud_storage_direct_upload" USING btree ("storageId", "path"); + +-- +-- Class FutureCallEntry as table serverpod_future_call +-- +CREATE TABLE "serverpod_future_call" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "serializedObject" text, + "serverId" text NOT NULL, + "identifier" text +); + +-- Indexes +CREATE INDEX "serverpod_future_call_time_idx" ON "serverpod_future_call" USING btree ("time"); +CREATE INDEX "serverpod_future_call_serverId_idx" ON "serverpod_future_call" USING btree ("serverId"); +CREATE INDEX "serverpod_future_call_identifier_idx" ON "serverpod_future_call" USING btree ("identifier"); + +-- +-- Class ServerHealthConnectionInfo as table serverpod_health_connection_info +-- +CREATE TABLE "serverpod_health_connection_info" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "active" bigint NOT NULL, + "closing" bigint NOT NULL, + "idle" bigint NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_connection_info_timestamp_idx" ON "serverpod_health_connection_info" USING btree ("timestamp", "serverId", "granularity"); + +-- +-- Class ServerHealthMetric as table serverpod_health_metric +-- +CREATE TABLE "serverpod_health_metric" ( + "id" bigserial PRIMARY KEY, + "name" text NOT NULL, + "serverId" text NOT NULL, + "timestamp" timestamp without time zone NOT NULL, + "isHealthy" boolean NOT NULL, + "value" double precision NOT NULL, + "granularity" bigint NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_health_metric_timestamp_idx" ON "serverpod_health_metric" USING btree ("timestamp", "serverId", "name", "granularity"); + +-- +-- Class LogEntry as table serverpod_log +-- +CREATE TABLE "serverpod_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "reference" text, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "logLevel" bigint NOT NULL, + "message" text NOT NULL, + "error" text, + "stackTrace" text, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_log_sessionLogId_idx" ON "serverpod_log" USING btree ("sessionLogId"); + +-- +-- Class MessageLogEntry as table serverpod_message_log +-- +CREATE TABLE "serverpod_message_log" ( + "id" bigserial PRIMARY KEY, + "sessionLogId" bigint NOT NULL, + "serverId" text NOT NULL, + "messageId" bigint NOT NULL, + "endpoint" text NOT NULL, + "messageName" text NOT NULL, + "duration" double precision NOT NULL, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- +-- Class MethodInfo as table serverpod_method +-- +CREATE TABLE "serverpod_method" ( + "id" bigserial PRIMARY KEY, + "endpoint" text NOT NULL, + "method" text NOT NULL +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_method_endpoint_method_idx" ON "serverpod_method" USING btree ("endpoint", "method"); + +-- +-- Class DatabaseMigrationVersion as table serverpod_migrations +-- +CREATE TABLE "serverpod_migrations" ( + "id" bigserial PRIMARY KEY, + "module" text NOT NULL, + "version" text NOT NULL, + "timestamp" timestamp without time zone +); + +-- Indexes +CREATE UNIQUE INDEX "serverpod_migrations_ids" ON "serverpod_migrations" USING btree ("module"); + +-- +-- Class QueryLogEntry as table serverpod_query_log +-- +CREATE TABLE "serverpod_query_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "sessionLogId" bigint NOT NULL, + "messageId" bigint, + "query" text NOT NULL, + "duration" double precision NOT NULL, + "numRows" bigint, + "error" text, + "stackTrace" text, + "slow" boolean NOT NULL, + "order" bigint NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_query_log_sessionLogId_idx" ON "serverpod_query_log" USING btree ("sessionLogId"); + +-- +-- Class ReadWriteTestEntry as table serverpod_readwrite_test +-- +CREATE TABLE "serverpod_readwrite_test" ( + "id" bigserial PRIMARY KEY, + "number" bigint NOT NULL +); + +-- +-- Class RuntimeSettings as table serverpod_runtime_settings +-- +CREATE TABLE "serverpod_runtime_settings" ( + "id" bigserial PRIMARY KEY, + "logSettings" json NOT NULL, + "logSettingsOverrides" json NOT NULL, + "logServiceCalls" boolean NOT NULL, + "logMalformedCalls" boolean NOT NULL +); + +-- +-- Class SessionLogEntry as table serverpod_session_log +-- +CREATE TABLE "serverpod_session_log" ( + "id" bigserial PRIMARY KEY, + "serverId" text NOT NULL, + "time" timestamp without time zone NOT NULL, + "module" text, + "endpoint" text, + "method" text, + "duration" double precision, + "numQueries" bigint, + "slow" boolean, + "error" text, + "stackTrace" text, + "authenticatedUserId" bigint, + "userId" text, + "isOpen" boolean, + "touched" timestamp without time zone NOT NULL +); + +-- Indexes +CREATE INDEX "serverpod_session_log_serverid_idx" ON "serverpod_session_log" USING btree ("serverId"); +CREATE INDEX "serverpod_session_log_time_idx" ON "serverpod_session_log" USING btree ("time"); +CREATE INDEX "serverpod_session_log_touched_idx" ON "serverpod_session_log" USING btree ("touched"); +CREATE INDEX "serverpod_session_log_isopen_idx" ON "serverpod_session_log" USING btree ("isOpen"); + +-- +-- Foreign relations for "acs_refresh_tokens" table +-- +ALTER TABLE ONLY "acs_refresh_tokens" + ADD CONSTRAINT "acs_refresh_tokens_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "acs_upload_tokens" table +-- +ALTER TABLE ONLY "acs_upload_tokens" + ADD CONSTRAINT "acs_upload_tokens_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_deliveries" table +-- +ALTER TABLE ONLY "alert_deliveries" + ADD CONSTRAINT "alert_deliveries_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "alert_deliveries" + ADD CONSTRAINT "alert_deliveries_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_idempotency_keys" table +-- +ALTER TABLE ONLY "alert_idempotency_keys" + ADD CONSTRAINT "alert_idempotency_keys_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alert_outbox" table +-- +ALTER TABLE ONLY "alert_outbox" + ADD CONSTRAINT "alert_outbox_fk_0" + FOREIGN KEY("alertId") + REFERENCES "alerts"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "alerts" table +-- +ALTER TABLE ONLY "alerts" + ADD CONSTRAINT "alerts_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "alerts" + ADD CONSTRAINT "alerts_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "audit_exports" table +-- +ALTER TABLE ONLY "audit_exports" + ADD CONSTRAINT "audit_exports_fk_0" + FOREIGN KEY("requestedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "audit_logs" table +-- +ALTER TABLE ONLY "audit_logs" + ADD CONSTRAINT "audit_logs_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "consent_logs" table +-- +ALTER TABLE ONLY "consent_logs" + ADD CONSTRAINT "consent_logs_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "data_subject_requests" table +-- +ALTER TABLE ONLY "data_subject_requests" + ADD CONSTRAINT "data_subject_requests_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "data_subject_requests" + ADD CONSTRAINT "data_subject_requests_fk_1" + FOREIGN KEY("decidedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "enrollment_tokens" table +-- +ALTER TABLE ONLY "enrollment_tokens" + ADD CONSTRAINT "enrollment_tokens_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "enrollment_tokens" + ADD CONSTRAINT "enrollment_tokens_fk_1" + FOREIGN KEY("createdByAcsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "otp_challenges" table +-- +ALTER TABLE ONLY "otp_challenges" + ADD CONSTRAINT "otp_challenges_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "push_tokens" table +-- +ALTER TABLE ONLY "push_tokens" + ADD CONSTRAINT "push_tokens_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "scheduled_notices" table +-- +ALTER TABLE ONLY "scheduled_notices" + ADD CONSTRAINT "scheduled_notices_fk_0" + FOREIGN KEY("createdBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "scheduled_notices" + ADD CONSTRAINT "scheduled_notices_fk_1" + FOREIGN KEY("microAreaId") + REFERENCES "micro_areas"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "terms_versions" table +-- +ALTER TABLE ONLY "terms_versions" + ADD CONSTRAINT "terms_versions_fk_0" + FOREIGN KEY("createdBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "terms_versions" + ADD CONSTRAINT "terms_versions_fk_1" + FOREIGN KEY("legallyReviewedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "terms_versions" + ADD CONSTRAINT "terms_versions_fk_2" + FOREIGN KEY("publishedBy") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "triage_sessions" table +-- +ALTER TABLE ONLY "triage_sessions" + ADD CONSTRAINT "triage_sessions_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "user_credentials" table +-- +ALTER TABLE ONLY "user_credentials" + ADD CONSTRAINT "user_credentials_fk_0" + FOREIGN KEY("userId") + REFERENCES "users"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "visits" table +-- +ALTER TABLE ONLY "visits" + ADD CONSTRAINT "visits_fk_0" + FOREIGN KEY("patientId") + REFERENCES "patients"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; +ALTER TABLE ONLY "visits" + ADD CONSTRAINT "visits_fk_1" + FOREIGN KEY("acsId") + REFERENCES "acs"("id") + ON DELETE NO ACTION + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_log" table +-- +ALTER TABLE ONLY "serverpod_log" + ADD CONSTRAINT "serverpod_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_message_log" table +-- +ALTER TABLE ONLY "serverpod_message_log" + ADD CONSTRAINT "serverpod_message_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + +-- +-- Foreign relations for "serverpod_query_log" table +-- +ALTER TABLE ONLY "serverpod_query_log" + ADD CONSTRAINT "serverpod_query_log_fk_0" + FOREIGN KEY("sessionLogId") + REFERENCES "serverpod_session_log"("id") + ON DELETE CASCADE + ON UPDATE NO ACTION; + + +-- +-- MIGRATION VERSION FOR sinalacs +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('sinalacs', '20261016155336452', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261016155336452', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260129180959368', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260129180959368', "timestamp" = now(); + + +COMMIT; diff --git a/backend/sinalacs_server/migrations/20261016155336452/definition_project.json b/backend/sinalacs_server/migrations/20261016155336452/definition_project.json new file mode 100644 index 00000000..ee4d3bdb --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016155336452/definition_project.json @@ -0,0 +1,3193 @@ +{ + "__className__": "serverpod.DatabaseDefinition", + "moduleName": "sinalacs", + "tables": [ + { + "__className__": "serverpod.TableDefinition", + "name": "acs", + "dartName": "Acs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSyncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_refresh_tokens", + "dartName": "AcsRefreshToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "familyId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "idleExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "absoluteExpiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rotatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_refresh_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_family_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "familyId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_refresh_tokens_user_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "acs_upload_tokens", + "dartName": "AcsUploadToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "issuedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "revokedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "acs_upload_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "acs_upload_tokens_user_device_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "deviceId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_deliveries", + "dartName": "AlertDeliveryRecord", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_deliveries_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_deliveries_alert_acs_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "alertId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "acsId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_idempotency_keys", + "dartName": "AlertIdempotencyKey", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "key", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_idempotency_keys_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_idempotency_keys_key_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "key" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alert_outbox", + "dartName": "AlertOutboxEntry", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "alertId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "topic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "payload", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "nextAttemptAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastError", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alert_outbox_fk_0", + "columns": [ + "alertId" + ], + "referenceTable": "alerts", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alert_outbox_pending_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "publishedAt" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "nextAttemptAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "alerts", + "dartName": "Alert", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "triggeredAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "receivedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "respondedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acknowledgedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolvedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevel", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:RiskLevel" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "locationCell", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:AlertStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "mqttTopic", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "retryCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalatedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "escalationReason", + "columnType": 0, + "isNullable": true, + "dartType": "protocol:EscalationReason?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "alerts_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "alerts_micro_area_status_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "triggeredAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_chain_checkpoints", + "dartName": "AuditChainCheckpoint", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_chain_checkpoints_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_exports", + "dartName": "AuditExport", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "filtersJson", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "format", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "rowCount", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "firstSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastSequence", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contentSha256", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chainState", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_exports_fk_0", + "columns": [ + "requestedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_exports_requested_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestedAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "audit_logs", + "dartName": "AuditLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "actionType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceType", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resourceId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "result", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sequence", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "previousHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "entryHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "audit_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_timestamp_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "timestamp" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "audit_logs_user_sequence_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "sequence" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "consent_logs", + "dartName": "ConsentLog", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "purpose", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "action", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "timestamp", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ipHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userAgent", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "signature", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "consent_logs_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "consent_logs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "data_subject_requests", + "dartName": "DataSubjectRequest", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "requestType", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestType" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsEncrypted", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "detailsKeyVersion", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:DataSubjectRequestStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "dueAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "decidedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolutionKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "data_subject_requests_fk_1", + "columns": [ + "decidedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "data_subject_requests_user_id_type_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "requestType" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "enrollment_tokens", + "dartName": "EnrollmentToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "tokenHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdByAcsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "enrollment_tokens_fk_1", + "columns": [ + "createdByAcsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "enrollment_tokens_token_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "tokenHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "micro_areas", + "dartName": "MicroArea", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "geoJsonBoundary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "micro_areas_ubs_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "ubsId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "otp_challenges", + "dartName": "OtpChallenge", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "codeHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "attempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "expiresAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "consumedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "otp_challenges_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "otp_challenges_user_id_created_at_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "patients", + "dartName": "Patient", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "emergencyContactKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "isChronic", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "chronicConditionsKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastLocationHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lastTriageAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "patients_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "push_tokens", + "dartName": "PushToken", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "token", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "platform", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "push_tokens_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_token_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "token" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "push_tokens_user_id_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "scheduled_notices", + "dartName": "ScheduledNotice", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "audience", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "title", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "message", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledFor", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:ScheduledNoticeStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "recipients", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "accepted", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failureReason", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "sentAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "scheduled_notices_fk_1", + "columns": [ + "microAreaId" + ], + "referenceTable": "micro_areas", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_due_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "status" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "scheduledFor" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "scheduled_notices_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + }, + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "createdAt" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "staff_accounts", + "dartName": "StaffAccount", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "enrollmentId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "active", + "columnType": 1, + "isNullable": false, + "dartType": "bool" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "ubsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeHash", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeExpiresAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedBy", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "activationCodeIssuedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "staff_accounts_enrollment_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "enrollmentId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "terms_versions", + "dartName": "TermsVersion", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "summary", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "effectiveFrom", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdBy", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "legallyReviewedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "publishedBy", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_0", + "columns": [ + "createdBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_1", + "columns": [ + "legallyReviewedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "terms_versions_fk_2", + "columns": [ + "publishedBy" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "terms_versions_version_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "version" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "triage_sessions", + "dartName": "TriageSession", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "answersKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "resultRiskKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "deviceId", + "columnType": 0, + "isNullable": false, + "dartType": "String" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "triage_sessions_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "triage_sessions_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "ubs", + "dartName": "Ubs", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "address", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "city", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "state", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "contactPhone", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "ubs_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "user_credentials", + "dartName": "UserCredential", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "userId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "passwordSalt", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "memoryKb", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "iterations", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "parallelism", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "failedAttempts", + "columnType": 6, + "isNullable": false, + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockedUntil", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "lockStreak", + "columnType": 6, + "isNullable": false, + "columnDefault": "0", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpSecretEncrypted", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpKeyVersion", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpEnabledAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "totpLastStep", + "columnType": 6, + "isNullable": true, + "dartType": "int?" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "user_credentials_fk_0", + "columns": [ + "userId" + ], + "referenceTable": "users", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "user_credentials_user_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "userId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "users", + "dartName": "User", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "cpfHash", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "name", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "birthDate", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "role", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:UserRole" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "microAreaId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "createdAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "updatedAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + } + ], + "foreignKeys": [], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_cpf_hash_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "cpfHash" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "users_micro_area_idx", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "microAreaId" + } + ], + "type": "btree", + "isUnique": false, + "isPrimary": false + } + ], + "managed": true + }, + { + "__className__": "serverpod.TableDefinition", + "name": "visits", + "dartName": "Visit", + "module": "sinalacs", + "schema": "public", + "columns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "id", + "columnType": 7, + "isNullable": false, + "columnDefault": "gen_random_uuid()", + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "patientId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "acsId", + "columnType": 7, + "isNullable": true, + "dartType": "UuidValue?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "authorship", + "columnType": 0, + "isNullable": false, + "columnDefault": "'acs'::text", + "dartType": "protocol:VisitAuthorship" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "originDeviceId", + "columnType": 0, + "isNullable": true, + "dartType": "String?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "scheduledAt", + "columnType": 4, + "isNullable": false, + "dartType": "DateTime" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "startedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "completedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "status", + "columnType": 0, + "isNullable": false, + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelBeforeKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "riskLevelAfterKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesEncrypted", + "columnType": 0, + "isNullable": false, + "columnDefault": "''::text", + "dartType": "String" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "notesKeyVersion", + "columnType": 6, + "isNullable": false, + "columnDefault": "1", + "dartType": "int" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncStatus", + "columnType": 0, + "isNullable": false, + "dartType": "protocol:SyncStatus" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "arrivalMethod", + "columnType": 0, + "isNullable": false, + "columnDefault": "'manual'::text", + "dartType": "protocol:ArrivalMethod" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "localId", + "columnType": 7, + "isNullable": false, + "dartType": "UuidValue" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "syncAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + }, + { + "__className__": "serverpod.ColumnDefinition", + "name": "version", + "columnType": 6, + "isNullable": false, + "dartType": "int" + } + ], + "foreignKeys": [ + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_0", + "columns": [ + "patientId" + ], + "referenceTable": "patients", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + }, + { + "__className__": "serverpod.ForeignKeyDefinition", + "constraintName": "visits_fk_1", + "columns": [ + "acsId" + ], + "referenceTable": "acs", + "referenceTableSchema": "public", + "referenceColumns": [ + "id" + ], + "onUpdate": 3, + "onDelete": 3 + } + ], + "indexes": [ + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_pkey", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "id" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": true + }, + { + "__className__": "serverpod.IndexDefinition", + "indexName": "visits_local_id_key", + "elements": [ + { + "__className__": "serverpod.IndexElementDefinition", + "type": 0, + "definition": "localId" + } + ], + "type": "btree", + "isUnique": true, + "isPrimary": false + } + ], + "managed": true + } + ], + "installedModules": [ + { + "__className__": "serverpod.DatabaseMigrationVersion", + "module": "serverpod", + "version": "20260129180959368" + } + ], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20261016155336452/migration.json b/backend/sinalacs_server/migrations/20261016155336452/migration.json new file mode 100644 index 00000000..ff36197a --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016155336452/migration.json @@ -0,0 +1,32 @@ +{ + "__className__": "serverpod.DatabaseMigration", + "actions": [ + { + "__className__": "serverpod.DatabaseMigrationAction", + "type": "alterTable", + "alterTable": { + "__className__": "serverpod.TableMigration", + "name": "alerts", + "schema": "public", + "addColumns": [ + { + "__className__": "serverpod.ColumnDefinition", + "name": "resolvedAt", + "columnType": 4, + "isNullable": true, + "dartType": "DateTime?" + } + ], + "deleteColumns": [], + "modifyColumns": [], + "addIndexes": [], + "deleteIndexes": [], + "addForeignKeys": [], + "deleteForeignKeys": [], + "warnings": [] + } + } + ], + "warnings": [], + "migrationApiVersion": 1 +} \ No newline at end of file diff --git a/backend/sinalacs_server/migrations/20261016155336452/migration.sql b/backend/sinalacs_server/migrations/20261016155336452/migration.sql new file mode 100644 index 00000000..cb66365e --- /dev/null +++ b/backend/sinalacs_server/migrations/20261016155336452/migration.sql @@ -0,0 +1,25 @@ +BEGIN; + +-- +-- ACTION ALTER TABLE +-- +ALTER TABLE "alerts" ADD COLUMN "resolvedAt" timestamp without time zone; + +-- +-- MIGRATION VERSION FOR sinalacs +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('sinalacs', '20261016155336452', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20261016155336452', "timestamp" = now(); + +-- +-- MIGRATION VERSION FOR serverpod +-- +INSERT INTO "serverpod_migrations" ("module", "version", "timestamp") + VALUES ('serverpod', '20260129180959368', now()) + ON CONFLICT ("module") + DO UPDATE SET "version" = '20260129180959368', "timestamp" = now(); + + +COMMIT; diff --git a/backend/sinalacs_server/migrations/migration_registry.txt b/backend/sinalacs_server/migrations/migration_registry.txt index 4a874d61..68791af4 100644 --- a/backend/sinalacs_server/migrations/migration_registry.txt +++ b/backend/sinalacs_server/migrations/migration_registry.txt @@ -32,3 +32,4 @@ 20261014155304871 20261016135918697 20261016140159775 +20261016155336452 diff --git a/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart b/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart index e591634a..3af94761 100644 --- a/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart +++ b/backend/sinalacs_server/test/integration/red_alert_cycle_test.dart @@ -364,6 +364,187 @@ void main() { expect(statusDepoisDoAck.acknowledgedAt, isNotNull); }); + group('concluir o atendimento (#76)', () { + Future<({String alertId, String acs})> alertaConfirmado() async { + await _seed(sessionBuilder.build()); + final paciente = + await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + final criado = await endpoints.alerts.createRedAlert( + sessionBuilder, + accessToken: paciente.accessToken, + idempotencyKey: 'resolve-${DateTime.now().microsecondsSinceEpoch}', + locationHash: 'hash-sintetico', + ); + final acs = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); + await endpoints.alerts.acknowledge( + sessionBuilder, + accessToken: acs.accessToken, + alertId: criado.alertId, + ); + return (alertId: criado.alertId, acs: acs.accessToken); + } + + Future> resultadosDaTrilha(String alertId) async { + final linhas = await AuditLog.db.find( + sessionBuilder.build(), + where: (t) => + t.resourceType.equals('alert') & + t.resourceId.equals(UuidValue.fromString(alertId)), + orderBy: (t) => t.sequence, + ); + return linhas.map((l) => l.result).toList(); + } + + test('leva a resolved, o paciente vê e repetir preserva o resolvedAt', () async { + final (:alertId, :acs) = await alertaConfirmado(); + + final r = await endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs, + alertId: alertId, + ); + expect(r.resolved, isTrue); + expect(r.status, AlertStatus.resolved); + expect(r.resolvedAt, isNotNull); + + final paciente = + await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + final status = await endpoints.alerts.statusFor( + sessionBuilder, + accessToken: paciente.accessToken, + ); + expect(status.status, AlertStatus.resolved); + expect(status.resolvedAt, r.resolvedAt); + expect(status.acknowledgedAt, isNotNull); + + final deNovo = await endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs, + alertId: alertId, + ); + expect(deNovo.resolved, isTrue); + expect(deNovo.resolvedAt, r.resolvedAt); + + expect(await resultadosDaTrilha(alertId), + ['acknowledged', 'resolved', 'already_resolved']); + }); + + test('alerta ainda pending é recusado e segue pending', () async { + await _seed(sessionBuilder.build()); + final paciente = + await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + final criado = await endpoints.alerts.createRedAlert( + sessionBuilder, + accessToken: paciente.accessToken, + idempotencyKey: 'resolve-pending', + locationHash: 'hash-sintetico', + ); + final acs = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); + + await expectLater( + endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs.accessToken, + alertId: criado.alertId, + ), + throwsA(isA().having( + (e) => e.message, + 'message', + RedAlertService.notAcknowledgedMessage, + )), + ); + + final linha = await Alert.db.findById( + sessionBuilder.build(), + UuidValue.fromString(criado.alertId), + ); + expect(linha!.status, AlertStatus.pending); + expect(linha.resolvedAt, isNull); + expect(await resultadosDaTrilha(criado.alertId), ['denied_not_acknowledged']); + }); + + test('confirmar de novo depois de concluído não volta a acknowledged', () async { + final (:alertId, :acs) = await alertaConfirmado(); + await endpoints.alerts.resolve(sessionBuilder, accessToken: acs, alertId: alertId); + + final ack = await endpoints.alerts.acknowledge( + sessionBuilder, + accessToken: acs, + alertId: alertId, + ); + expect(ack.acknowledged, isTrue); + + final linha = await Alert.db.findById( + sessionBuilder.build(), + UuidValue.fromString(alertId), + ); + expect(linha!.status, AlertStatus.resolved); + expect(linha.resolvedAt, isNotNull); + }); + + test('alerta de outra microárea: resolved false, nada muda nem é auditado', () async { + await _seed(sessionBuilder.build()); + final session = sessionBuilder.build(); + final outro = await Alert.db.insertRow( + session, + Alert( + patientId: UuidValue.fromString(_patientId), + microAreaId: UuidValue.fromString('00000000-0000-4000-8000-0000000076aa'), + triggeredAt: DateTime.now().toUtc(), + acknowledgedAt: DateTime.now().toUtc(), + riskLevel: RiskLevel.red, + locationHash: 'hash-sintetico', + status: AlertStatus.acknowledged, + mqttTopic: 'sinalacs/v1/microareas/outra/alerts', + deviceId: 'device-sintetico', + retryCount: 0, + version: 1, + ), + ); + final acs = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); + + final r = await endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs.accessToken, + alertId: outro.id!.uuid, + ); + + expect(r.resolved, isFalse); + expect(r.status, isNull); + expect((await Alert.db.findById(session, outro.id!))!.status, + AlertStatus.acknowledged); + expect(await resultadosDaTrilha(outro.id!.uuid), isEmpty); + }); + + test('identificador que não é UUID devolve resolved false', () async { + await _seed(sessionBuilder.build()); + final acs = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); + + final r = await endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs.accessToken, + alertId: 'nao-e-uuid', + ); + + expect(r.resolved, isFalse); + }); + + test('paciente não conclui atendimento', () async { + final (:alertId, acs: _) = await alertaConfirmado(); + final paciente = + await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + + await expectLater( + endpoints.alerts.resolve( + sessionBuilder, + accessToken: paciente.accessToken, + alertId: alertId, + ), + throwsA(isA()), + ); + }); + }); + test('um ACS não pode consultar alerts.statusFor', () async { await _seed(sessionBuilder.build()); final tokenAcs = diff --git a/backend/sinalacs_server/test/integration/test_tools/serverpod_test_tools.dart b/backend/sinalacs_server/test/integration/test_tools/serverpod_test_tools.dart index ac72e1cc..2b95c2ed 100644 --- a/backend/sinalacs_server/test/integration/test_tools/serverpod_test_tools.dart +++ b/backend/sinalacs_server/test/integration/test_tools/serverpod_test_tools.dart @@ -53,37 +53,39 @@ import 'package:sinalacs_server/src/generated/api/red_alert_result.dart' as _i25; import 'package:sinalacs_server/src/generated/api/alert_ack_result.dart' as _i26; -import 'package:sinalacs_server/src/generated/api/alert_status_result.dart' +import 'package:sinalacs_server/src/generated/api/alert_resolve_result.dart' as _i27; -import 'package:sinalacs_server/src/generated/api/development_login_result.dart' +import 'package:sinalacs_server/src/generated/api/alert_status_result.dart' as _i28; -import 'package:sinalacs_server/src/generated/api/totp_enrollment_start.dart' +import 'package:sinalacs_server/src/generated/api/development_login_result.dart' as _i29; -import 'package:sinalacs_server/src/generated/api/service_health.dart' as _i30; +import 'package:sinalacs_server/src/generated/api/totp_enrollment_start.dart' + as _i30; +import 'package:sinalacs_server/src/generated/api/service_health.dart' as _i31; import 'package:sinalacs_server/src/generated/api/notice_send_result.dart' - as _i31; -import 'package:sinalacs_server/src/generated/api/enrollment_token_result.dart' as _i32; -import 'package:sinalacs_server/src/generated/api/enrollment_result.dart' +import 'package:sinalacs_server/src/generated/api/enrollment_token_result.dart' as _i33; -import 'package:sinalacs_server/src/generated/api/micro_area_patient.dart' +import 'package:sinalacs_server/src/generated/api/enrollment_result.dart' as _i34; -import 'package:sinalacs_server/src/generated/api/patient_data_overview.dart' +import 'package:sinalacs_server/src/generated/api/micro_area_patient.dart' as _i35; -import 'package:sinalacs_server/src/generated/api/patient_consent_record.dart' +import 'package:sinalacs_server/src/generated/api/patient_data_overview.dart' as _i36; -import 'package:sinalacs_server/src/generated/enums/consent_purpose.dart' +import 'package:sinalacs_server/src/generated/api/patient_consent_record.dart' as _i37; -import 'package:sinalacs_server/src/generated/api/terms_change_notice.dart' +import 'package:sinalacs_server/src/generated/enums/consent_purpose.dart' as _i38; -import 'package:sinalacs_server/src/generated/api/patient_data_subject_request_record.dart' +import 'package:sinalacs_server/src/generated/api/terms_change_notice.dart' as _i39; -import 'package:sinalacs_server/src/generated/api/triage_result.dart' as _i40; -import 'package:sinalacs_server/src/generated/api/ubs_contact.dart' as _i41; +import 'package:sinalacs_server/src/generated/api/patient_data_subject_request_record.dart' + as _i40; +import 'package:sinalacs_server/src/generated/api/triage_result.dart' as _i41; +import 'package:sinalacs_server/src/generated/api/ubs_contact.dart' as _i42; import 'package:sinalacs_server/src/generated/api/visit_sync_result.dart' - as _i42; -import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' as _i43; +import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' + as _i44; import 'package:sinalacs_server/src/generated/protocol.dart'; import 'package:sinalacs_server/src/generated/endpoints.dart'; export 'package:serverpod_test/serverpod_test_public_exports.dart'; @@ -1397,7 +1399,42 @@ class _AlertsEndpoint { }); } - _i3.Future<_i27.AlertStatusResult> statusFor( + _i3.Future<_i27.AlertResolveResult> resolve( + _i1.TestSessionBuilder sessionBuilder, { + required String accessToken, + required String alertId, + }) async { + return _i1.callAwaitableFunctionAndHandleExceptions(() async { + var _localUniqueSession = + (sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild( + endpoint: 'alerts', + method: 'resolve', + ); + try { + var _localCallContext = await _endpointDispatch.getMethodCallContext( + createSessionCallback: (_) => _localUniqueSession, + endpointPath: 'alerts', + methodName: 'resolve', + parameters: _i1.testObjectToJson({ + 'accessToken': accessToken, + 'alertId': alertId, + }), + serializationManager: _serializationManager, + ); + var _localReturnValue = + await (_localCallContext.method.call( + _localUniqueSession, + _localCallContext.arguments, + ) + as _i3.Future<_i27.AlertResolveResult>); + return _localReturnValue; + } finally { + await _localUniqueSession.close(); + } + }); + } + + _i3.Future<_i28.AlertStatusResult> statusFor( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -1420,7 +1457,7 @@ class _AlertsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i27.AlertStatusResult>); + as _i3.Future<_i28.AlertStatusResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1439,7 +1476,7 @@ class _AuthEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i28.DevelopmentLoginResult> developmentLogin( + _i3.Future<_i29.DevelopmentLoginResult> developmentLogin( _i1.TestSessionBuilder sessionBuilder, { required String role, }) async { @@ -1462,7 +1499,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i28.DevelopmentLoginResult>); + as _i3.Future<_i29.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1470,7 +1507,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i28.DevelopmentLoginResult> loginInstitutional( + _i3.Future<_i29.DevelopmentLoginResult> loginInstitutional( _i1.TestSessionBuilder sessionBuilder, { required String matricula, required String password, @@ -1501,7 +1538,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i28.DevelopmentLoginResult>); + as _i3.Future<_i29.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1509,7 +1546,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i28.DevelopmentLoginResult> loginStaff( + _i3.Future<_i29.DevelopmentLoginResult> loginStaff( _i1.TestSessionBuilder sessionBuilder, { required String matricula, required String password, @@ -1538,7 +1575,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i28.DevelopmentLoginResult>); + as _i3.Future<_i29.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1546,7 +1583,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i29.TotpEnrollmentStart> beginStaffTotpEnrollment( + _i3.Future<_i30.TotpEnrollmentStart> beginStaffTotpEnrollment( _i1.TestSessionBuilder sessionBuilder, { required String matricula, required String password, @@ -1575,7 +1612,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i29.TotpEnrollmentStart>); + as _i3.Future<_i30.TotpEnrollmentStart>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1622,7 +1659,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i28.DevelopmentLoginResult> refreshSession( + _i3.Future<_i29.DevelopmentLoginResult> refreshSession( _i1.TestSessionBuilder sessionBuilder, { required String refreshToken, required String deviceId, @@ -1649,7 +1686,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i28.DevelopmentLoginResult>); + as _i3.Future<_i29.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1688,7 +1725,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i29.TotpEnrollmentStart> beginTotpEnrollment( + _i3.Future<_i30.TotpEnrollmentStart> beginTotpEnrollment( _i1.TestSessionBuilder sessionBuilder, { required String matricula, required String password, @@ -1715,7 +1752,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i29.TotpEnrollmentStart>); + as _i3.Future<_i30.TotpEnrollmentStart>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1795,7 +1832,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i28.DevelopmentLoginResult> verifyOtp( + _i3.Future<_i29.DevelopmentLoginResult> verifyOtp( _i1.TestSessionBuilder sessionBuilder, { required String cpf, required String code, @@ -1824,7 +1861,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i28.DevelopmentLoginResult>); + as _i3.Future<_i29.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1891,7 +1928,7 @@ class _HealthEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i30.ServiceHealth> check( + _i3.Future<_i31.ServiceHealth> check( _i1.TestSessionBuilder sessionBuilder, ) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { @@ -1913,7 +1950,7 @@ class _HealthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i30.ServiceHealth>); + as _i3.Future<_i31.ServiceHealth>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1932,7 +1969,7 @@ class _NoticesEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i31.NoticeSendResult> sendSegmented( + _i3.Future<_i32.NoticeSendResult> sendSegmented( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required String title, @@ -1963,7 +2000,7 @@ class _NoticesEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i31.NoticeSendResult>); + as _i3.Future<_i32.NoticeSendResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1982,7 +2019,7 @@ class _OnboardingEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i32.EnrollmentTokenResult> generateEnrollmentToken( + _i3.Future<_i33.EnrollmentTokenResult> generateEnrollmentToken( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required String patientId, @@ -2009,7 +2046,7 @@ class _OnboardingEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i32.EnrollmentTokenResult>); + as _i3.Future<_i33.EnrollmentTokenResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2017,7 +2054,7 @@ class _OnboardingEndpoint { }); } - _i3.Future<_i33.EnrollmentResult> completeEnrollment( + _i3.Future<_i34.EnrollmentResult> completeEnrollment( _i1.TestSessionBuilder sessionBuilder, { required String token, required bool healthDataConsent, @@ -2050,7 +2087,7 @@ class _OnboardingEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i33.EnrollmentResult>); + as _i3.Future<_i34.EnrollmentResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2069,7 +2106,7 @@ class _PatientsEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future> listMicroArea( + _i3.Future> listMicroArea( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2092,7 +2129,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future>); + as _i3.Future>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2166,7 +2203,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i35.PatientDataOverview> myData( + _i3.Future<_i36.PatientDataOverview> myData( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2189,7 +2226,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i35.PatientDataOverview>); + as _i3.Future<_i36.PatientDataOverview>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2197,10 +2234,10 @@ class _PatientsEndpoint { }); } - _i3.Future<_i36.PatientConsentRecord> updateConsent( + _i3.Future<_i37.PatientConsentRecord> updateConsent( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, - required _i37.ConsentPurpose purpose, + required _i38.ConsentPurpose purpose, required bool granted, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { @@ -2226,7 +2263,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i36.PatientConsentRecord>); + as _i3.Future<_i37.PatientConsentRecord>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2234,7 +2271,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i36.PatientConsentRecord> acceptTermsOfUse( + _i3.Future<_i37.PatientConsentRecord> acceptTermsOfUse( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2257,7 +2294,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i36.PatientConsentRecord>); + as _i3.Future<_i37.PatientConsentRecord>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2299,7 +2336,7 @@ class _PatientsEndpoint { _i3.Future hasGrantedConsent( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, - required _i37.ConsentPurpose purpose, + required _i38.ConsentPurpose purpose, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = @@ -2331,7 +2368,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i38.TermsChangeNotice?> termsChangeNotice( + _i3.Future<_i39.TermsChangeNotice?> termsChangeNotice( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2354,7 +2391,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i38.TermsChangeNotice?>); + as _i3.Future<_i39.TermsChangeNotice?>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2362,7 +2399,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i39.PatientDataSubjectRequestRecord> requestDataDeletion( + _i3.Future<_i40.PatientDataSubjectRequestRecord> requestDataDeletion( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2385,7 +2422,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i39.PatientDataSubjectRequestRecord>); + as _i3.Future<_i40.PatientDataSubjectRequestRecord>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2393,7 +2430,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i39.PatientDataSubjectRequestRecord> requestDataCorrection( + _i3.Future<_i40.PatientDataSubjectRequestRecord> requestDataCorrection( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required String details, @@ -2420,7 +2457,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i39.PatientDataSubjectRequestRecord>); + as _i3.Future<_i40.PatientDataSubjectRequestRecord>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2439,7 +2476,7 @@ class _TriageEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i40.TriageResult> evaluate( + _i3.Future<_i41.TriageResult> evaluate( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required bool chestPain, @@ -2476,7 +2513,7 @@ class _TriageEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i40.TriageResult>); + as _i3.Future<_i41.TriageResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2495,7 +2532,7 @@ class _UbsEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i41.UbsContact> myContact( + _i3.Future<_i42.UbsContact> myContact( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2518,7 +2555,7 @@ class _UbsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i41.UbsContact>); + as _i3.Future<_i42.UbsContact>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2537,10 +2574,10 @@ class _VisitsEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future> sync( + _i3.Future> sync( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, - required List<_i43.VisitSyncEntry> visits, + required List<_i44.VisitSyncEntry> visits, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = @@ -2564,7 +2601,7 @@ class _VisitsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future>); + as _i3.Future>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2572,11 +2609,11 @@ class _VisitsEndpoint { }); } - _i3.Future> syncLegacy( + _i3.Future> syncLegacy( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required String deviceId, - required List<_i43.VisitSyncEntry> visits, + required List<_i44.VisitSyncEntry> visits, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = @@ -2601,7 +2638,7 @@ class _VisitsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future>); + as _i3.Future>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2609,11 +2646,11 @@ class _VisitsEndpoint { }); } - _i3.Future> syncDeferred( + _i3.Future> syncDeferred( _i1.TestSessionBuilder sessionBuilder, { required String uploadToken, required String deviceId, - required List<_i43.VisitSyncEntry> visits, + required List<_i44.VisitSyncEntry> visits, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = @@ -2638,7 +2675,7 @@ class _VisitsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future>); + as _i3.Future>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2677,7 +2714,7 @@ class _VisitsEndpoint { }); } - _i3.Future> pull( + _i3.Future> pull( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required DateTime since, @@ -2704,7 +2741,7 @@ class _VisitsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future>); + as _i3.Future>); return _localReturnValue; } finally { await _localUniqueSession.close(); diff --git a/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart b/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart index e716ca88..5303f21d 100644 --- a/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart +++ b/backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart @@ -50,7 +50,8 @@ final _mutationPattern = RegExp( const _allowedWriters = { 'infrastructure/database/orm_alert_store.dart': 'save() insere o alerta como pending; acknowledge() leva a acknowledged ' - '(única transição de status hoje)', + '(e preserva resolved); resolve() leva acknowledged → resolved, sob ' + 'trava e auditado na mesma transação (#76)', 'infrastructure/database/orm_retention_store.dart': 'expurgo LGPD: DELETE FROM alerts por triggeredAt — hoje SEM guarda de ' 'status/risco (buraco, corrigido na Task 4)', From fa549b26c3f0fd37434f0a6360ccea4b79bd6e1f Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 11:59:18 -0400 Subject: [PATCH 3/7] =?UTF-8?q?fix(backend):=20pulso=20do=20backoffice=20a?= =?UTF-8?q?nda=20com=20a=20conclus=C3=A3o=20do=20alerta=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../database/orm_admin_read_store.dart | 3 ++- .../admin_monitoring_store_test.dart | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_admin_read_store.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_admin_read_store.dart index 2b51af81..6e5c40bd 100644 --- a/backend/sinalacs_server/lib/src/infrastructure/database/orm_admin_read_store.dart +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_admin_read_store.dart @@ -95,7 +95,8 @@ class OrmAdminReadStore implements AdminReadStore, AuditChainScopeStore { count(*) FILTER (WHERE $alertOverdueSql), max(GREATEST(a."triggeredAt", COALESCE(a."acknowledgedAt", a."triggeredAt"), - COALESCE(a."escalatedAt", a."triggeredAt"))) + COALESCE(a."escalatedAt", a."triggeredAt"), + COALESCE(a."resolvedAt", a."triggeredAt"))) FROM alerts a LEFT JOIN micro_areas m ON m.id = a."microAreaId" WHERE $_escopo diff --git a/backend/sinalacs_server/test/integration/admin_monitoring_store_test.dart b/backend/sinalacs_server/test/integration/admin_monitoring_store_test.dart index f4d9ae05..9ad4c39c 100644 --- a/backend/sinalacs_server/test/integration/admin_monitoring_store_test.dart +++ b/backend/sinalacs_server/test/integration/admin_monitoring_store_test.dart @@ -220,6 +220,27 @@ void main() { expect(depois.overdueRed, 0); }); + test('maxChangedAt anda com a conclusão do atendimento (#76)', () async { + final a = await _alerta( + session, + em: _t.subtract(const Duration(hours: 1)), + ); + final ack = _t.subtract(const Duration(minutes: 10)); + final concluido = _t.subtract(const Duration(minutes: 2)); + await Alert.db.updateRow( + session, + a.copyWith( + status: AlertStatus.resolved, + acknowledgedAt: ack, + resolvedAt: concluido, + ), + ); + + final r = await pulso(const AdminScope.ubs(_ubsA)); + expect(r.maxChangedAt, concluido); + expect(r.overdueRed, 0); + }); + test( 'maxChangedAt anda com o escalonamento, e escalonar tira o alerta de overdueRed', () async { From 29dab8d8c758ebf058f8910ef375a894d1738241 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 12:11:39 -0400 Subject: [PATCH 4/7] feat(acs): concluir o atendimento do alerta confirmado (#76) --- .../integration_test/full_journey_e2e.dart | 3 + apps/acs/lib/app/app.dart | 99 ++++++++-- apps/acs/lib/core/network/backend_client.dart | 17 ++ apps/acs/lib/core/services/alert_queue.dart | 21 ++- apps/acs/test/alert_resolve_test.dart | 170 ++++++++++++++++++ apps/acs/test/support/fakes.dart | 20 +++ 6 files changed, 318 insertions(+), 12 deletions(-) create mode 100644 apps/acs/test/alert_resolve_test.dart diff --git a/apps/acs/integration_test/full_journey_e2e.dart b/apps/acs/integration_test/full_journey_e2e.dart index bbeaafb4..535bd5e0 100644 --- a/apps/acs/integration_test/full_journey_e2e.dart +++ b/apps/acs/integration_test/full_journey_e2e.dart @@ -153,6 +153,9 @@ class _EnvioDeVisitasForaDoAr implements AcsBackend { @override Future acknowledge({required String alertId}) => _inner.acknowledge(alertId: alertId); @override + Future resolveAlert({required String alertId}) => + _inner.resolveAlert(alertId: alertId); + @override Future> syncVisits(List visits, {String? expectedUserId}) => _rede(() => _inner.syncVisits(visits, expectedUserId: expectedUserId)); @override diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index 082d2015..b281b853 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -1625,6 +1625,56 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver } } + /// Trava o toque duplo do "Concluir atendimento". + final Set _concluindo = {}; + + /// "Concluir atendimento" (#76): confirma com o ACS, porque o paciente passa + /// a ver "Atendido" e o servidor não volta atrás. + Future _resolve(PrioritizedAlert alert) async { + if (_concluindo.contains(alert.alertId)) return; + final messenger = ScaffoldMessenger.maybeOf(context); + void aviso(String texto) => messenger + ?..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(texto))); + final confirmou = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Concluir o atendimento?'), + content: const Text( + 'O paciente verá "Atendido" no aplicativo. Conclua só depois de atender o paciente.', + ), + actions: [ + TextButton( + key: const Key('cancelar_conclusao'), + onPressed: () => Navigator.of(dialog).pop(false), + style: TextButton.styleFrom(minimumSize: const Size(48, 52)), + child: const Text('Cancelar'), + ), + FilledButton( + key: const Key('confirmar_conclusao'), + onPressed: () => Navigator.of(dialog).pop(true), + style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), + child: const Text('Concluir'), + ), + ], + ), + ); + if (confirmou != true || !mounted) return; + _concluindo.add(alert.alertId); + try { + final result = await BackendScope.of(context).resolveAlert(alertId: alert.alertId); + if (result.resolved && mounted) _queue.markResolved(alert.alertId); + aviso(result.resolved + ? 'Atendimento concluído. O paciente já vê "Atendido".' + : 'A central não reconheceu este alerta.'); + } on BackendFailure catch (failure) { + // Nunca engolir: o alerta segue confirmado e por concluir. + aviso(failure.message); + } finally { + _concluindo.remove(alert.alertId); + } + } + bool _saindo = false; bool _limpando = false; @@ -1964,6 +2014,7 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver onRetryFeed: _feedErrorIsTransient ? _retryFeedNow : null, storageError: _storageNotice, onAcknowledge: _acknowledge, + onResolve: _resolve, onEscalate: (alert) => setState(() { _selected = alert; destination = AcsDestination.escalation; }), onVisit: (alert) => setState(() { _selected = alert; destination = AcsDestination.visit; }), ), @@ -2169,6 +2220,7 @@ class DashboardScreen extends StatelessWidget { this.onRetryFeed, this.storageError, this.onAcknowledge, + this.onResolve, this.onEscalate, this.onVisit, }); @@ -2189,6 +2241,7 @@ class DashboardScreen extends StatelessWidget { /// descobre sozinho — que as visitas do dia não estão sendo salvas. final InfraNotice? storageError; final void Function(PrioritizedAlert alert)? onAcknowledge; + final void Function(PrioritizedAlert alert)? onResolve; final void Function(PrioritizedAlert alert)? onEscalate; final void Function(PrioritizedAlert alert)? onVisit; @@ -2229,6 +2282,7 @@ class DashboardScreen extends StatelessWidget { child: _AlertCard( alert: alert, onAcknowledge: onAcknowledge, + onResolve: onResolve, onEscalate: onEscalate, onVisit: onVisit, ), @@ -2282,10 +2336,11 @@ class _InfraBanner extends StatelessWidget { } class _AlertCard extends StatelessWidget { - const _AlertCard({required this.alert, this.onAcknowledge, this.onEscalate, this.onVisit}); + const _AlertCard({required this.alert, this.onAcknowledge, this.onEscalate, this.onVisit, this.onResolve}); final PrioritizedAlert alert; final void Function(PrioritizedAlert alert)? onAcknowledge; + final void Function(PrioritizedAlert alert)? onResolve; final void Function(PrioritizedAlert alert)? onEscalate; final void Function(PrioritizedAlert alert)? onVisit; @@ -2326,7 +2381,8 @@ class _AlertCard extends StatelessWidget { container: true, label: 'Paciente ${alert.patientId.substring(0, 8)}, $label' '${alert.escalated ? ', reforço da coordenação' : ''}' - '${alert.acknowledged ? ', recebimento confirmado' : ''}, ' + '${alert.acknowledged ? ', recebimento confirmado' : ''}' + '${alert.resolved ? ', atendimento concluído' : ''}, ' 'recebido às ${_time(alert.triggeredAt)}', excludeSemantics: true, child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -2349,7 +2405,16 @@ class _AlertCard extends StatelessWidget { ), const SizedBox(height: 6), Text('Recebido às ${_time(alert.triggeredAt)} • local ${alert.locationHash}'), - if (alert.acknowledged) Padding( + if (alert.resolved) Padding( + padding: const EdgeInsets.only(top: 8), + child: Row(children: [ + Icon(Icons.task_alt, size: 16, color: context.acsRisk.greenOnSurface), + const SizedBox(width: 6), + Text('Atendimento concluído', + key: Key('resolved_${alert.alertId}'), + style: TextStyle(color: context.acsRisk.greenOnSurface)), + ]), + ) else if (alert.acknowledged) Padding( padding: const EdgeInsets.only(top: 8), child: Row(children: [ Icon(Icons.check_circle, size: 16, color: context.acsRisk.greenOnSurface), @@ -2369,17 +2434,29 @@ class _AlertCard extends StatelessWidget { child: const Text('Confirmar recebimento'), ), ), - const SizedBox(height: 8), - SizedBox( + if (alert.acknowledged && !alert.resolved) SizedBox( width: double.infinity, - child: OutlinedButton( - onPressed: isRed - ? (onEscalate == null ? null : () => onEscalate!(alert)) - : (onVisit == null ? null : () => onVisit!(alert)), - style: OutlinedButton.styleFrom(minimumSize: const Size(48, 52)), - child: Text(isRed ? 'Acionar SAMU / Atender' : 'Iniciar rota de visita'), + child: FilledButton.icon( + key: Key('resolve_${alert.alertId}'), + onPressed: onResolve == null ? null : () => onResolve!(alert), + style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), + icon: const Icon(Icons.task_alt), + label: const Text('Concluir atendimento'), ), ), + if (!alert.resolved) ...[ + const SizedBox(height: 8), + SizedBox( + width: double.infinity, + child: OutlinedButton( + onPressed: isRed + ? (onEscalate == null ? null : () => onEscalate!(alert)) + : (onVisit == null ? null : () => onVisit!(alert)), + style: OutlinedButton.styleFrom(minimumSize: const Size(48, 52)), + child: Text(isRed ? 'Acionar SAMU / Atender' : 'Iniciar rota de visita'), + ), + ), + ], ]), ), ); diff --git a/apps/acs/lib/core/network/backend_client.dart b/apps/acs/lib/core/network/backend_client.dart index e3a77b90..1c426fd7 100644 --- a/apps/acs/lib/core/network/backend_client.dart +++ b/apps/acs/lib/core/network/backend_client.dart @@ -127,6 +127,10 @@ abstract class AcsBackend { Future acknowledge({required String alertId}); + /// Conclui o atendimento de um alerta já confirmado (#76): o paciente passa + /// a ver "Atendido". + Future resolveAlert({required String alertId}); + /// [expectedUserId], quando presente, é o dono do lote: se a sessão que vai /// assinar a chamada — conferida DEPOIS de o token ser resolvido (talvez por /// renovação) e imediatamente antes da rede — não é dele, lança @@ -249,6 +253,8 @@ class MisconfiguredBackend implements AcsBackend { @override Future acknowledge({required String alertId}) async => _recusar(); + @override + Future resolveAlert({required String alertId}) async => _recusar(); @override Future> syncVisits(List visits, {String? expectedUserId}) async => @@ -722,6 +728,17 @@ class BackendClient implements AcsBackend { ); } + /// Conclui o atendimento (#76). Alerta inexistente ou de outra microárea + /// volta `resolved: false`; alerta ainda não confirmado é recusado pelo + /// servidor com texto pronto, que o `_guard` repassa. + @override + Future resolveAlert({required String alertId}) async { + final token = await _requireToken(); + return _guard( + () => _client.alerts.resolve(accessToken: token, alertId: alertId), + ); + } + /// Envia as visitas registradas offline. /// /// O lote inteiro roda em uma transação no servidor, e cada visita volta com diff --git a/apps/acs/lib/core/services/alert_queue.dart b/apps/acs/lib/core/services/alert_queue.dart index f175f1ed..c60fe27d 100644 --- a/apps/acs/lib/core/services/alert_queue.dart +++ b/apps/acs/lib/core/services/alert_queue.dart @@ -14,6 +14,7 @@ class PrioritizedAlert { required this.triggeredAt, this.acknowledged = false, this.escalated = false, + this.resolved = false, }); factory PrioritizedAlert.fromMqtt(ReceivedMqttAlert alert) { @@ -49,7 +50,10 @@ class PrioritizedAlert { /// gravado no aparelho nem carrega quem escalonou. final bool escalated; - PrioritizedAlert copyWith({bool? acknowledged, bool? escalated}) => PrioritizedAlert( + /// O ACS concluiu o atendimento (#76). Implica [acknowledged]. + final bool resolved; + + PrioritizedAlert copyWith({bool? acknowledged, bool? escalated, bool? resolved}) => PrioritizedAlert( alertId: alertId, patientId: patientId, microAreaId: microAreaId, @@ -59,6 +63,7 @@ class PrioritizedAlert { triggeredAt: triggeredAt, acknowledged: acknowledged ?? this.acknowledged, escalated: escalated ?? this.escalated, + resolved: resolved ?? this.resolved, ); /// Peso da ordenação. Menor vem primeiro. @@ -147,10 +152,24 @@ class AlertQueue extends ChangeNotifier { return true; } + /// Marca o atendimento como concluído (e, por consequência, confirmado). + /// Devolve `false` se o alerta não está na fila. + bool markResolved(String alertId) { + final index = _alerts.indexWhere((item) => item.alertId == alertId); + if (index < 0) return false; + + _alerts[index] = _alerts[index].copyWith(acknowledged: true, resolved: true); + _sort(); + notifyListeners(); + return true; + } + void _sort() { _alerts.sort((a, b) { // Confirmados descem, para não competir com quem ainda espera. if (a.acknowledged != b.acknowledged) return a.acknowledged ? 1 : -1; + // Concluídos descem abaixo dos confirmados: já não pedem ação. + if (a.resolved != b.resolved) return a.resolved ? 1 : -1; final byRisk = a._riskRank.compareTo(b._riskRank); if (byRisk != 0) return byRisk; // Reforço da coordenação vem à frente dos demais do mesmo risco. diff --git a/apps/acs/test/alert_resolve_test.dart b/apps/acs/test/alert_resolve_test.dart new file mode 100644 index 00000000..d3db1af6 --- /dev/null +++ b/apps/acs/test/alert_resolve_test.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_acs/app/acs_theme.dart'; +import 'package:sinalacs_acs/app/app.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; + +import 'support/fakes.dart'; + +/// Mesmo caminho de `entrar` em `login_flow_test.dart` (não importável entre +/// arquivos de teste). Credenciais sintéticas. +Future entrar(WidgetTester tester) async { + await tester.enterText(find.byKey(const Key('matricula_field')), 'ACS-001'); + await tester.enterText(find.byKey(const Key('senha_field')), 'senha-sintetica'); + await tester.tap(find.byKey(const Key('login_button'))); + await tester.pumpAndSettle(); +} + +/// "Concluir atendimento" (#76): a ação do ACS que faz o paciente ver +/// "Atendido". +void main() { + group('AlertQueue.markResolved', () { + test('marca concluído, implica confirmado e desce abaixo dos confirmados', () { + final queue = AlertQueue(microAreaId: seedMicroAreaId) + ..upsert(testAlert(alertId: 'a', triggeredAt: DateTime.utc(2026, 10, 16, 8))) + ..upsert(testAlert(alertId: 'b', triggeredAt: DateTime.utc(2026, 10, 16, 9))) + ..markAcknowledged('a') + ..markAcknowledged('b'); + + expect(queue.markResolved('a'), isTrue); + + expect(queue.alerts.map((x) => x.alertId), ['b', 'a']); + expect(queue.alerts.last.resolved, isTrue); + expect(queue.alerts.last.acknowledged, isTrue); + expect(queue.unacknowledgedCount, 0); + }); + + test('alerta fora da fila devolve false', () { + final queue = AlertQueue(microAreaId: seedMicroAreaId); + expect(queue.markResolved('nao-existe'), isFalse); + }); + + test('reentrega de um alerta concluído não o reabre', () { + final queue = AlertQueue(microAreaId: seedMicroAreaId) + ..upsert(testAlert(alertId: 'a')) + ..markAcknowledged('a') + ..markResolved('a'); + + expect(queue.upsert(testAlert(alertId: 'a')), isFalse); + expect(queue.upsert(testAlert(alertId: 'a', escalated: true)), isFalse); + expect(queue.alerts.single.resolved, isTrue); + }); + }); + + group('cartão do alerta', () { + Future pintar(WidgetTester tester, AlertQueue queue, {void Function(PrioritizedAlert)? onResolve}) => + tester.pumpWidget(MaterialApp( + theme: buildAcsDarkTheme(), + home: Scaffold(body: DashboardScreen(queue: queue, onResolve: onResolve)), + )); + + testWidgets('só o alerta confirmado oferece "Concluir atendimento"', (tester) async { + final queue = AlertQueue(microAreaId: seedMicroAreaId) + ..upsert(testAlert(alertId: 'novo')) + ..upsert(testAlert(alertId: 'visto')) + ..markAcknowledged('visto'); + await pintar(tester, queue, onResolve: (_) {}); + + expect(find.byKey(const Key('resolve_novo')), findsNothing); + expect(find.byKey(const Key('resolve_visto')), findsOneWidget); + final altura = tester.getSize(find.byKey(const Key('resolve_visto'))).height; + expect(altura, greaterThanOrEqualTo(48)); + }); + + testWidgets('concluído mostra rótulo e ícone, sem botões de ação', (tester) async { + final handle = tester.ensureSemantics(); + final queue = AlertQueue(microAreaId: seedMicroAreaId) + ..upsert(testAlert(alertId: 'a1')) + ..markAcknowledged('a1') + ..markResolved('a1'); + await pintar(tester, queue, onResolve: (_) {}); + + expect(find.byKey(const Key('resolved_a1')), findsOneWidget); + expect(find.byKey(const Key('resolve_a1')), findsNothing); + expect(find.byKey(const Key('ack_a1')), findsNothing); + expect(find.text('Acionar SAMU / Atender'), findsNothing); + expect(find.text('Iniciar rota de visita'), findsNothing); + expect(find.bySemanticsLabel(RegExp('atendimento concluído')), findsOneWidget); + final texto = tester.widget(find.byKey(const Key('resolved_a1'))); + expect(texto.style?.color, AcsRiskColors.dark.greenOnSurface); + handle.dispose(); + }); + }); + + group('fluxo no painel', () { + Future abrirComAlertaConfirmado(WidgetTester tester, FakeAcsBackend backend) async { + late FakeAlertFeed feed; + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + )); + await entrar(tester); + await tester.pumpAndSettle(); + feed.deliver(testAlert(alertId: 'alerta-1')); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('ack_alerta-1'))); + await tester.pumpAndSettle(); + return feed; + } + + testWidgets('confirma no diálogo, chama o servidor e marca concluído', (tester) async { + final backend = FakeAcsBackend(); + await abrirComAlertaConfirmado(tester, backend); + + await tester.tap(find.byKey(const Key('resolve_alerta-1'))); + await tester.pumpAndSettle(); + expect(find.text('Concluir o atendimento?'), findsOneWidget); + await tester.tap(find.byKey(const Key('confirmar_conclusao'))); + await tester.pumpAndSettle(); + + expect(backend.resolvedAlertIds, ['alerta-1']); + expect(find.byKey(const Key('resolved_alerta-1')), findsOneWidget); + expect(find.text('Atendimento concluído. O paciente já vê "Atendido".'), findsOneWidget); + }); + + testWidgets('cancelar o diálogo não chama o servidor', (tester) async { + final backend = FakeAcsBackend(); + await abrirComAlertaConfirmado(tester, backend); + + await tester.tap(find.byKey(const Key('resolve_alerta-1'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('cancelar_conclusao'))); + await tester.pumpAndSettle(); + + expect(backend.resolvedAlertIds, isEmpty); + expect(find.byKey(const Key('resolve_alerta-1')), findsOneWidget); + }); + + testWidgets('recusa do servidor aparece e o alerta segue por concluir', (tester) async { + final backend = FakeAcsBackend() + ..resolveFailure = const BackendFailure( + 'Confirme o recebimento antes de concluir o atendimento.', + isRecoverable: false, + ); + await abrirComAlertaConfirmado(tester, backend); + + await tester.tap(find.byKey(const Key('resolve_alerta-1'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('confirmar_conclusao'))); + await tester.pumpAndSettle(); + + expect(find.text('Confirme o recebimento antes de concluir o atendimento.'), findsOneWidget); + expect(find.byKey(const Key('resolve_alerta-1')), findsOneWidget); + expect(find.byKey(const Key('resolved_alerta-1')), findsNothing); + }); + + testWidgets('alerta que a central não encontra não é marcado concluído', (tester) async { + final backend = FakeAcsBackend()..resolved = false; + await abrirComAlertaConfirmado(tester, backend); + + await tester.tap(find.byKey(const Key('resolve_alerta-1'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('confirmar_conclusao'))); + await tester.pumpAndSettle(); + + expect(find.text('A central não reconheceu este alerta.'), findsOneWidget); + expect(find.byKey(const Key('resolved_alerta-1')), findsNothing); + }); + }); +} diff --git a/apps/acs/test/support/fakes.dart b/apps/acs/test/support/fakes.dart index 4b9e1346..6c0ae305 100644 --- a/apps/acs/test/support/fakes.dart +++ b/apps/acs/test/support/fakes.dart @@ -41,6 +41,13 @@ class FakeAcsBackend implements AcsBackend { int listPatientsCount = 0; final List acknowledgedAlertIds = []; + final List resolvedAlertIds = []; + + /// `false` simula alerta que a central não encontra na microárea. + bool resolved = true; + + /// Quando presente, `resolveAlert` lança esta falha (ex.: alerta pending). + BackendFailure? resolveFailure; int loginCount = 0; AuthSession? _session; @@ -211,6 +218,19 @@ class FakeAcsBackend implements AcsBackend { ); } + @override + Future resolveAlert({required String alertId}) async { + final falha = resolveFailure; + if (falha != null) throw falha; + resolvedAlertIds.add(alertId); + return AlertResolveResult( + alertId: alertId, + resolved: resolved, + status: resolved ? AlertStatus.resolved : null, + resolvedAt: resolved ? DateTime.utc(2026, 10, 16, 14) : null, + ); + } + final List> syncedVisitBatches = >[]; /// Falha da chamada inteira, como uma queda de rede. From 65e13640aa34788df51bbe2f614cc9b2ee16dcd3 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 12:14:43 -0400 Subject: [PATCH 5/7] feat(patient): status mostra Atendido quando a equipe conclui (#76) --- apps/patient/lib/app/app.dart | 10 ++++++++- apps/patient/test/patient_app_mvp_test.dart | 24 +++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/patient/lib/app/app.dart b/apps/patient/lib/app/app.dart index 86e2b7c0..0758fad8 100644 --- a/apps/patient/lib/app/app.dart +++ b/apps/patient/lib/app/app.dart @@ -2518,12 +2518,13 @@ class _StatusCard extends StatelessWidget { final statusLabel = switch (status.status) { AlertStatus.pending => 'Enviado — aguardando confirmação da equipe', AlertStatus.acknowledged => 'Recebido pela equipe de saúde', - AlertStatus.resolved => 'Atendimento concluído', + AlertStatus.resolved => 'Atendido pela equipe de saúde', AlertStatus.escalated => 'Encaminhado para o SAMU', null => 'Sem status', }; final triggeredAt = status.triggeredAt; final acknowledgedAt = status.acknowledgedAt; + final resolvedAt = status.resolvedAt; return Card( child: Padding( @@ -2550,6 +2551,13 @@ class _StatusCard extends StatelessWidget { const SizedBox(height: 4), Text('Confirmado pela equipe às ${_formatTime(acknowledgedAt)}'), ], + if (resolvedAt != null) ...[ + const SizedBox(height: 4), + Text( + key: const Key('status_resolved_at'), + 'Atendimento concluído às ${_formatTime(resolvedAt)}', + ), + ], ], ), ), diff --git a/apps/patient/test/patient_app_mvp_test.dart b/apps/patient/test/patient_app_mvp_test.dart index 10cab8a2..d19dab0e 100644 --- a/apps/patient/test/patient_app_mvp_test.dart +++ b/apps/patient/test/patient_app_mvp_test.dart @@ -1564,6 +1564,30 @@ void main() { expect(find.text('Recebido pela equipe de saúde'), findsOneWidget); }); + testWidgets('alerta concluído pela equipe aparece como "Atendido" (#76)', (tester) async { + final backend = FakePatientBackend() + ..statusResult = AlertStatusResult( + found: true, + alertId: 'alerta-1', + riskLevel: RiskLevel.red, + status: AlertStatus.resolved, + triggeredAt: DateTime.utc(2026, 9, 18, 9), + acknowledgedAt: DateTime.utc(2026, 9, 18, 9, 5), + resolvedAt: DateTime.utc(2026, 9, 18, 10, 40), + ); + await tester.pumpWidget(SinalAcsApp(backend: backend)); + await login(tester); + + await tester.tap(find.text('Status')); + await tester.pumpAndSettle(); + + expect(find.text('Atendido pela equipe de saúde'), findsOneWidget); + expect(find.byKey(const Key('status_resolved_at')), findsOneWidget); + expect(find.textContaining('Atendimento concluído às'), findsOneWidget); + // A confirmação continua visível: é a etapa "Visualizado" do PRD §6.1. + expect(find.textContaining('Confirmado pela equipe às'), findsOneWidget); + }); + testWidgets('uma falha ao consultar mostra o aviso, sem travar o botão de tentar de novo', (tester) async { final backend = FakePatientBackend() ..statusFailure = const BackendFailure('Sem conexão com o servidor.'); From dfda86f1b32e7d592c8d4f0a7abf34270e6379a4 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 12:31:30 -0400 Subject: [PATCH 6/7] docs: registra o estado Atendido do alerta (#76) --- CLAUDE.md | 2 +- PROGRESS.md | 8 +- backend/CLAUDE.md | 2 +- docs/auditoria-prd/2026-10-15-lacunas-prd.md | 8 +- .../2026-10-16-issue-76-alerta-atendido.md | 1487 +++++++++++++++++ spec/lgpd_data_audit.md | 1 + 6 files changed, 1501 insertions(+), 7 deletions(-) create mode 100644 docs/superpowers/plans/2026-10-16-issue-76-alerta-atendido.md diff --git a/CLAUDE.md b/CLAUDE.md index b6ce6241..64f6fa30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,7 +12,7 @@ Two core flows: A third app, `apps/admin`, is the backoffice (Indicadores, Microáreas, Alertas, Auditoria, Pedidos do titular), reading real data from the backend since #40/#41, managing accounts since #43 (ACS create/link/activate/deactivate, password and MFA reset, staff MFA reset, audit scoped to the coordinator's UBS) and handling LGPD data-subject requests since #42 (a coordinator/admin starts review, completes or rejects them). Since #44 the Alertas and Indicadores screens refresh by a 30 s polling pulse and a coordinator/admin can escalate a red alert unacknowledged past `RED_ALERT_ESCALATION_SECONDS` (default 300 s; the alert stays `pending`, the ACS gets a reinforcement; the emulator e2e passed end to end on 2026-10-12). Since #45 the Auditoria screen filters and pages the trail, shows a per-row seal and a global chain verification computed by the server (with a tail checkpoint), and the admin (not the coordinator) can export a nine-column CSV with the export itself recorded in the same transaction; the emulator e2e, including a deliberate tampering detected on screen and by `audit_chain_check`, passed on 2026-10-13/14. Since #46 the backoffice has an Avisos tab (schedule, cancel and history of community notices; a coordinator only for his UBS) and, for the admin only, a Termos sub-tab (draft, legal review, publication of terms versions; publication is blocked until the legal review is recorded and requires an effective date at least 15 days ahead). Proved: backend unit/integration tests, widget tests, a hermetic emulator test of the tab, and a push e2e in which a scheduled notice reaches the emulator through Gorush and FCM; the legal review of the real terms text and the manual switch of the in-force version remain outside the code (`PROGRESS.md`, section #46). -Main RPC endpoints (`backend/sinalacs_server/lib/src/endpoints/`): `auth` (`loginInstitutional`, `requestOtp`, `verifyOtp`, `developmentLogin` — the last only with `ENABLE_DEV_LOGIN`), `onboarding`, `triage.evaluate`, `alerts` (`createRedAlert`, `acknowledge`, `statusFor`), `patients` (`listMicroArea`, `myData`, chronic conditions, `updateConsent`, `requestDataDeletion`, `requestDataCorrection`), `visits` (`sync` e `pull` com a sessão do ACS; `syncLegacy`, que usa a sessão só como transporte das visitas legadas sem dono; `syncDeferred` e `revokeUploadToken`, autenticados pelo token de envio diferido do dono, não pelo JWT), `admin` (`indicators`, `microAreas`, `alerts` e `auditLogs` — leitura do backoffice escopada por papel/UBS desde #40; desde a #43 a gestão de contas: `acs`/`staff`, `createAcs`, `setAcsMicroArea`, `setAcsActive`, `resetAcsPassword`, `resetAcsMfa` e `resetStaffMfa`; desde a #44 `escalateAlert`, `alertsPulse` e `endMonitoringSession`; desde a #46 `scheduleNotice`, `cancelScheduledNotice`, `noticeHistory`, `termsVersions`, `draftTermsVersion`, `markTermsReviewed` e `publishTermsVersion` — detalhe em `backend/CLAUDE.md`), `health.check`. +Main RPC endpoints (`backend/sinalacs_server/lib/src/endpoints/`): `auth` (`loginInstitutional`, `requestOtp`, `verifyOtp`, `developmentLogin` — the last only with `ENABLE_DEV_LOGIN`), `onboarding`, `triage.evaluate`, `alerts` (`createRedAlert`, `acknowledge`, `resolve`, `statusFor`), `patients` (`listMicroArea`, `myData`, chronic conditions, `updateConsent`, `requestDataDeletion`, `requestDataCorrection`), `visits` (`sync` e `pull` com a sessão do ACS; `syncLegacy`, que usa a sessão só como transporte das visitas legadas sem dono; `syncDeferred` e `revokeUploadToken`, autenticados pelo token de envio diferido do dono, não pelo JWT), `admin` (`indicators`, `microAreas`, `alerts` e `auditLogs` — leitura do backoffice escopada por papel/UBS desde #40; desde a #43 a gestão de contas: `acs`/`staff`, `createAcs`, `setAcsMicroArea`, `setAcsActive`, `resetAcsPassword`, `resetAcsMfa` e `resetStaffMfa`; desde a #44 `escalateAlert`, `alertsPulse` e `endMonitoringSession`; desde a #46 `scheduleNotice`, `cancelScheduledNotice`, `noticeHistory`, `termsVersions`, `draftTermsVersion`, `markTermsReviewed` e `publishTermsVersion` — detalhe em `backend/CLAUDE.md`), `health.check`. `PROGRESS.md` holds milestone status and the open items with owners. `spec/validation_report.md` is stale in both directions (items it lists as open are closed, and its test counts are far below the real ones) — verify against the code before trusting it. diff --git a/PROGRESS.md b/PROGRESS.md index 283d11b1..ed2f703b 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1652,7 +1652,7 @@ Plano: `docs/superpowers/plans/2026-10-14-issue-46-avisos-e-termos.md`; achados Relatório item a item de `spec/PRD_system.md` contra o código, com evidência medida a partir de `develop` em `92106d1`: [docs/auditoria-prd/2026-10-15-lacunas-prd.md](docs/auditoria-prd/2026-10-15-lacunas-prd.md). -Contagem dos 64 IDs do PRD: 24 entregue, 27 parcial, 4 ausente, 9 externo (mais 57 linhas sem ID próprio, para SLOs, STRIDE, criptografia, acessibilidade, telemetria, métricas do §1.3 e módulos dos apps). +Contagem dos 64 IDs do PRD: 26 entregue, 25 parcial, 4 ausente, 9 externo (mais 57 linhas sem ID próprio, para SLOs, STRIDE, criptografia, acessibilidade, telemetria, métricas do §1.3 e módulos dos apps). Os três P0 do backlog: colunas de risco em texto plano (INV-04), o estado "Atendido" que o paciente nunca vê (RF05) e o SMS real do OTP (RF01). Backlog priorizado na seção final do relatório; conferência por `python3 scripts/qa/cobertura_prd.py --final`. @@ -1662,3 +1662,9 @@ Backlog priorizado na seção final do relatório; conferência por `python3 scr - Decisão: `alerts.riskLevel` permanece legível (só `red` é gravado; chave dos filtros SQL da INV-03). Registrada no PRD (INV-04) e em `spec/lgpd_data_audit.md` §2.3; amarrada pelo teste `INV-04 (#75)` de `test/unit/red_alert_discard_paths_test.dart`. - Provas: `test/unit/encrypted_risk_level_test.dart`; testes `#75` em `test/integration/health_data_encryption_test.dart` (linha bruta por `row_to_json` sem risco legível, ida e volta, `After` nulo e conclusão, linha antiga fora do `pull` e `rejected` só para ela no `sync`) e em `test/integration/patient_data_overview_endpoint_test.dart`. - Aberto: migrações sem backfill (volume antigo precisa ser recriado); rotação de chave continua inexistente (`keyVersion` só marcador), como nas demais colunas cifradas. + +## Estado "Atendido" do alerta (#76) — 2026-10-16 + +- Entregue: `alerts.resolve` (só ACS do território, só de `acknowledged`, idempotente, auditado na mesma transação: `resolved`, `already_resolved`, `denied_not_acknowledged`); coluna `alerts.resolvedAt` (migração aditiva); o ACK trava a linha e não rebaixa `resolved`; `alerts.statusFor` devolve `resolvedAt`; o pulso do backoffice anda com a conclusão. ACS: botão "Concluir atendimento" no cartão confirmado, com diálogo. Paciente: "Atendido pela equipe de saúde" e a hora da conclusão. +- Provas: `test/unit/red_alert_service_test.dart` (grupo `resolve (#76)`), `test/integration/red_alert_cycle_test.dart` (grupo `concluir o atendimento (#76)`), `test/integration/admin_monitoring_store_test.dart`, `apps/acs/test/alert_resolve_test.dart`, `apps/patient/test/patient_app_mvp_test.dart`. +- Aberto: sem prova no emulador (o e2e do paciente não tem ACS concluindo); o contador "reconhecidos" do backoffice (`acknowledgedRedAlerts`) conta só `acknowledged`, então um alerta concluído sai dele (o TMRAV não muda); a conclusão vive só na fila em memória do ACS, como o ACK. diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 5d158147..99a7fdca 100644 --- a/backend/CLAUDE.md +++ b/backend/CLAUDE.md @@ -101,7 +101,7 @@ Key pattern: application services depend on abstract interfaces (`AlertPublisher **MQTT do backend e outbox (#56).** `MqttAlertDispatcher` roda cada `client.connect` numa zona própria por cliente, derivada de `Zone.root`: o erro assíncrono do `Socket.done` do TLS (que antes ia sem dono para a zona do `connect` e matava o processo, saída 255) vira queda de conexão tratada, com reconexão e backoff. Política: `SocketException`, `TlsException`, `TimeoutException`, `ConnectionException`, `NoConnectionException` e `StateError('socket is null')` são **transitórios** (descarta o cliente; `publish` lança `MqttUnavailableException`); qualquer outro é **fatal** (registra só o tipo e chama `onFatal`, padrão `exit(70)`). Injetáveis nos testes: `initialBackoff`, `maxBackoff`, `publishAckTimeout`, `onFatal`, `log`. `AlertPublisher.publish` é `Future` e só completa no PUBACK do broker (timeout 10 s; PINGRESP também 10 s); o outbox só grava `publishedAt` depois disso. `bin/main.dart` tem um `runZonedGuarded` global (`runtime/uncaught_error_policy.dart`) como defesa em profundidade, e o `serverpod` do Compose tem `restart: unless-stopped`. Logs só com tipo e `errno` (VAZ-01). Testes: `dart test -t resiliencia` (broker falso em TCP real). QA manual e opcional, fora da CI, no Docker/TLS: `./scripts/qa/mqtt_resiliencia_e2e.sh` (base, B reinício do broker, C partição; o A, `docker pause` do backend, só com `--com-pausa-do-backend`). Evidências, decisões e riscos residuais: `docs/superpowers/plans/2026-10-12-issue-56-evidencias.md`. -**Serverpod is RPC, not REST**, so there are no URL routes to match: the generated client calls methods. Endpoints: `health.check` (returns `{status, mqttConnected, dbConnected}`; answers as soon as the server is up, independent of MQTT/DB state), `auth.developmentLogin` (throws `EndpointDisabledException` unless `ENABLE_DEV_LOGIN=true`, preserving the old 404-not-403 semantics), `auth.loginInstitutional` (o login real do ACS, RF07: matrícula + senha verificadas com Argon2id contra `user_credentials`, sem guardar a senha em nenhuma forma — só o hash com seu salt e os parâmetros de custo; bloqueia a conta após 5 tentativas falhas — 15 minutos na primeira rodada e o dobro a cada rodada seguinte sem um login válido no meio, até 24 h (`lockStreak`, `InstitutionalAuthService.lockDurationFor`) —, que é o rate limiting do achado F6 de `spec/security_assessment.md`, e audita cada desfecho em `audit_logs`; matrícula inexistente e senha errada devolvem a mesma mensagem), `auth.refreshSession`/`auth.logout` (o refresh token do ACS, LGPD-RT06; `loginInstitutional` só emite `refreshToken` quando o cliente envia um `deviceId` não-branco — sem ele o resultado traz `refreshToken` nulo e o JWT de 15 minutos segue valendo sozinho. O token guarda-se em `acs_refresh_tokens` **só como hash SHA-256**, com família, aparelho, `rotatedAt`/`revokedAt` — o `deviceId` é o identificador da instalação (UUID aleatório), não uma atestação de hardware: protege contra token vazado sem ele, não contra comprometimento do aparelho; constantes em `RefreshTokenService`: janela ociosa de 2 h (`idleWindow`), teto absoluto de 8 h contado do login por senha + TOTP (`absoluteWindow`; a rotação não o estende) e tolerância de reuso de 30 s (`reuseGrace`, para a resposta perdida em rede: reapresentar o pai dentro dela devolve um filho e audita `refresh_granted_grace`). Reuso fora da tolerância, aparelho diferente, conta inativa ou sem microárea **revogam a família inteira**, e toda recusa devolve a mesma `SessionExpiredException` com a mensagem única `Sessão expirada. Entre novamente.` (sem dizer o motivo). A microárea do JWT novo é relida do banco a cada refresh, nunca copiada do token antigo. A rotação é um INSERT condicional atômico e a revogação da família roda em duas passagens, para que dois refreshes concorrentes não gerem dois filhos vivos. Cada desfecho vira uma linha `session_refresh` em `audit_logs` (`refresh_granted`, `refresh_granted_grace`, `denied_*`). Os dois métodos são públicos por desenho — o token de acesso já venceu quando o primeiro é chamado — e estão na allowlist fina de `endpoint_auth_posture_test.dart`. `logout` revoga a família, audita `logout` e ignora em silêncio um token que não conhece. O login só poda os tokens expirados do próprio usuário: não há limpeza periódica da tabela), `auth.requestOtp`/`auth.verifyOtp` (o login passwordless do paciente, RF01: CPF validado por dígito verificador e hasheado no servidor em `users.cpfHash` com `CPF_HASH_PEPPER`, mais a data de nascimento; código de 6 dígitos, com TTL de 5 minutos, teto de 5 verificações e 60 s entre pedidos, guardado em `otp_challenges` **como HMAC** — o código em claro existe só entre a geração e o envio — e uma linha de auditoria por desfecho. `requestOtp` responde a mesma coisa exista ou não o CPF — mesmo payload, mesmo status, sem linha de desafio e sem SMS na recusa —, mas **não** equaliza o *tempo* de resposta: as lacunas do RF01 estão no `PROGRESS.md`. `verifyOtp` emite a sessão do paciente com `patientSessionLifetime` (**1 hora**, LGPD-RT06) e não com os 15 minutos padrão, que o ACS mantém porque a renovação dele é feita pelo refresh token (abaixo) — o paciente não tem refresh token e um código OTP não pode ser reapresentado; o caminho de onboarding emite o mesmo TTL — o defeito dos 15 minutos do RF02 foi corrigido, ver `PROGRESS.md`), `onboarding.generateEnrollmentToken`/`onboarding.completeEnrollment` (o convite de uso único que o ACS gera e a conclusão dele pelo paciente; `completeEnrollment` também emite sessão, com `patientSessionLifetime`), `patients.acceptTermsOfUse` (aceite do Termo de Uso e da Política de Privacidade vigentes por quem entrou por OTP sem onboarding, LGPD-RF18; única via de escrita de `termsOfUse` fora do cadastro; idempotente — com o aceite vigente já gravado devolve a linha existente) e `patients.hasAcceptedCurrentTerms` (o `bool` que o login consulta, sem ler o painel nem auditar leitura), `patients.hasGrantedConsent` (o mesmo desenho para uma finalidade: `true` se a decisão mais recente é `granted`; o app a consulta a cada login antes de pedir o token de push ao FCM); a criação do pedido de exclusão (`patients.requestDataDeletion`) é serializada por `pg_advisory_xact_lock` por titular (o aceite do termo também, e o registro de token de push por token: `lockPerSubject` em `infrastructure/database/subject_lock.dart`, forma de duas chaves `(namespace, hashtext)`, que não colide com a chave única da cadeia de auditoria), `notices.sendSegmented` (RF14: só ACS, a microárea vem do token; a consulta SQL em `OrmNoticeRecipientStore` usa o consentimento `segmentedPush` **mais recente** de cada titular, não a existência do token; entrega a lista ao Gorush por `GorushClient` — `dart:io`, tempo limite de 5 s no total e de 2 s só na **conexão** (estourar na conexão é "inacessível, tente de novo", porque nada saiu; estourar depois de o pedido sair é "resultado desconhecido", e corpo 2xx ilegível idem), um único cliente por processo encerrado em `overrideConfig`, `GORUSH_URL` vazio desliga o envio e a chamada falha com `NoticeDeliveryException`; tokens que o provedor declara inválidos são apagados; audita `community_notice` com a microárea e nunca o texto), `patients.termsChangeNotice` (aviso de mudança dos termos com 15 dias de antecedência, LGPD-RF18: só paciente e sem auditoria; desde a #46 lê as versões publicadas de `terms_versions` (`TermsVersionStore.currentPublished`), não faz mais "sem I/O"; a constante `upcomingTermsChange` em `terms_change_schedule.dart` é só um espelho `null` mantido por dois testes, não é mais a agenda, e `TermsChangeSchedule` se recusa (com `ArgumentError`, que vale em release, não `assert`) a existir com menos de 15 dias entre as datas de publicação e de vigência declaradas — não prova que o aviso chegou a alguém, um `publishedAt` retroativo passa — para anunciar uma mudança: (1) escreva o texto novo em `upcomingLegalDocuments` no app e publique essa versão do app, (2) só então troque `upcomingTermsChange` aqui — um teste do app (`upcoming_legal_documents_test.dart`) falha se as duas pontas divergirem —, (3) na vigência mude `consentPolicyVersion` e `legalDocumentsVersion`, mova o texto para `privacyPolicy`/`termsOfUse` e volte as duas constantes para `null`; um app antigo, sem o texto embarcado, vê só "Ler os termos atuais" durante os 15 dias), `devices.registerPushToken` (RF14: só paciente, só com o consentimento `segmentedPush` vigente, uma linha por token em `push_tokens`; revogar o consentimento apaga os tokens do titular na MESMA transação do `denied`, sob o lock por titular; no máximo 10 tokens por titular, o mais antigo sai; só a troca de dono do token é auditada, com uma linha para o novo dono e outra para o anterior; a poda apaga por id **e** titular e poupa o token recém-gravado; o consentimento mais recente desempata por `id`), `alerts.createRedAlert` (idempotency key is a method parameter, not a header; throws `AlertDispatchUnavailableException` if the MQTT dispatcher isn't connected), `alerts.acknowledge`, `triage.evaluate` (exige `accessToken`; classifica pelo `TriageEngine` determinístico, grava a sessão em `triage_sessions` com o `patientId` vindo do token — nunca de parâmetro, INV-05 — e escreve uma linha `write`/`triage_session` em `audit_logs`; só o papel `patient` é aceito, um ACS recebe `AlertPermissionException`), `visits.sync` (batch upload of visits registered offline by the ACS; deduplicated by the device-generated `localId`, which has a unique index on `visits`, version-checked — a mismatched `version` returns `SyncStatus.conflict` and never overwrites — and territory-checked against the patient's own micro-area, not just the caller's; a malformed identifier, a territory mismatch, or a visit owned by another ACS all return the terminal `SyncStatus.rejected`, distinct from the retryable `SyncStatus.error` used for things like an unknown patient, so the device queue knows which failures are worth retrying; a visit with no author — `acsId` null, `authorship = legacyUnclaimed` — is also `rejected` there, never claimed), `visits.syncLegacy` (D4 of the 2026-10-03 plan: uploads the device's pre-owner legacy visits with **unknown authorship** — the ACS session is only the transport, territory-checked like `sync`, idempotent by `localId` from the same `originDeviceId`, refuses an inactive ACS, never writes the transporter as `acsId`, at most `VisitSyncService.maxLegacyBatch` (200) visits per call, one `visit_legacy_sync` audit row per batch plus one `visit_legacy_sync_item` row per visit actually created or changed (only the visit id, no clinical content); a re-send of an authored `localId` at the same version is a no-op `synced`, at another version `rejected`), `visits.syncDeferred`/`visits.revokeUploadToken` (D7 of the 2026-10-03 plan: the **deferred upload token** of `UploadTokenService` (`application/auth/upload_token_service.dart`, store `OrmUploadTokenStore`) lets the device upload the pending visits of an ACS who already logged out, **with that ACS's authorship** and no session. `auth.loginInstitutional` issues it as `uploadToken` next to the refresh token, under the same rule — only with a non-blank `deviceId`, otherwise `null`; patient logins and `developmentLogin` never return it. 256 random bits, stored in `acs_upload_tokens` **only as SHA-256** (helpers shared with the refresh token in `opaque_token.dart`), bound to user + device, fixed 7-day life (`UploadTokenService.lifetime`), no rotation, independent of the refresh family (logout does not touch it); a new login on the same (user, device) revokes the previous one, inside a transaction serialized by `lockPerSubject` (namespace 5). `resolve` returns the **token owner** as ACS with the micro-area **re-read from the database** on every call, and every refusal — unknown, expired, revoked, other device, inactive account, no micro-area — is the same `SessionExpiredException` with `UploadTokenService.deniedMessage` (`Envio não autorizado. Entre novamente.`); other device and inactive/no micro-area also revoke the token. `syncDeferred` checks the batch cap `VisitSyncService.maxLegacyBatch` (200) before anything else (`AlertValidationException`), resolves the token outside the batch transaction and runs the **same** `VisitSyncService.sync` as `visits.sync` — so author = owner, territory = owner's current one, another ACS's `localId` `rejected`. Audit rows in `audit_logs`, never with the token: `upload_token` / `upload_token_issued`, `upload_token_denied_{revoked,device,expired,inactive}` (unknown token → no row, no subject), `upload_token_revoked`, and one `visit_deferred` / `visit_deferred_sync` per committed batch. Both methods are public by design for `authenticate(...)` and sit in the allowlist of `endpoint_auth_posture_test.dart`. Still open, by design or deferred (`PROGRESS.md`, section "Fila de visitas por dono e corrida do login"): `visits.sync`/`pull` accept an inactive ACS until the JWT expires (`isActiveAcs` is only checked by `syncLegacy`; `syncDeferred` re-checks it through the token); no database CHECK ties a null `acsId` to `authorship = legacyUnclaimed`; the existing `visits.sync` has no batch cap; a `localId` existence oracle remains, identical to `visits.sync`'s; `uploadToken`/`refreshToken` are plain RPC arguments, so any future argument logging must scrub them, see VAZ-03 in `spec/lgpd_data_audit.md`), and `patients.listMicroArea` (the ACS's routine-visit patient picker; the micro-area comes from the caller's token, never a parameter, and every call is written to `audit_logs`, whose rows are hash-chained — `AuditChain`/`AuditChainVerifier` in `application/audit/`, keyed by `AUDIT_CHAIN_SECRET` — so tampering with a row is detectable even by someone with direct Postgres write access; `bin/audit_chain_check.dart` verifies the chain on demand). `bin/seed_e2e_fixtures.dart` seeds the e2e stack (`docker-compose.e2e.yml`, see `apps/CLAUDE.md`): synthetic UUIDs/CPFs generated per run by `lib/src/infrastructure/testing/e2e_fixtures.dart`, written **only** to the database `sinalacs_e2e` (it refuses any other name and any `APP_ENV` but `development`); `development.sql` and its `*-seed` services stay for the plain development stack. Errors are typed exceptions declared in `.spy.yaml` and serialized to the client, replacing HTTP status codes. MQTT connects in the background after boot (non-blocking) with exponential-backoff auto-reconnect, so the server stays responsive even if the broker is unreachable — this matters on free-tier hosts that sleep/hibernate. Toda decisão de papel e de presença de território no token passa por uma regra +**Serverpod is RPC, not REST**, so there are no URL routes to match: the generated client calls methods. Endpoints: `health.check` (returns `{status, mqttConnected, dbConnected}`; answers as soon as the server is up, independent of MQTT/DB state), `auth.developmentLogin` (throws `EndpointDisabledException` unless `ENABLE_DEV_LOGIN=true`, preserving the old 404-not-403 semantics), `auth.loginInstitutional` (o login real do ACS, RF07: matrícula + senha verificadas com Argon2id contra `user_credentials`, sem guardar a senha em nenhuma forma — só o hash com seu salt e os parâmetros de custo; bloqueia a conta após 5 tentativas falhas — 15 minutos na primeira rodada e o dobro a cada rodada seguinte sem um login válido no meio, até 24 h (`lockStreak`, `InstitutionalAuthService.lockDurationFor`) —, que é o rate limiting do achado F6 de `spec/security_assessment.md`, e audita cada desfecho em `audit_logs`; matrícula inexistente e senha errada devolvem a mesma mensagem), `auth.refreshSession`/`auth.logout` (o refresh token do ACS, LGPD-RT06; `loginInstitutional` só emite `refreshToken` quando o cliente envia um `deviceId` não-branco — sem ele o resultado traz `refreshToken` nulo e o JWT de 15 minutos segue valendo sozinho. O token guarda-se em `acs_refresh_tokens` **só como hash SHA-256**, com família, aparelho, `rotatedAt`/`revokedAt` — o `deviceId` é o identificador da instalação (UUID aleatório), não uma atestação de hardware: protege contra token vazado sem ele, não contra comprometimento do aparelho; constantes em `RefreshTokenService`: janela ociosa de 2 h (`idleWindow`), teto absoluto de 8 h contado do login por senha + TOTP (`absoluteWindow`; a rotação não o estende) e tolerância de reuso de 30 s (`reuseGrace`, para a resposta perdida em rede: reapresentar o pai dentro dela devolve um filho e audita `refresh_granted_grace`). Reuso fora da tolerância, aparelho diferente, conta inativa ou sem microárea **revogam a família inteira**, e toda recusa devolve a mesma `SessionExpiredException` com a mensagem única `Sessão expirada. Entre novamente.` (sem dizer o motivo). A microárea do JWT novo é relida do banco a cada refresh, nunca copiada do token antigo. A rotação é um INSERT condicional atômico e a revogação da família roda em duas passagens, para que dois refreshes concorrentes não gerem dois filhos vivos. Cada desfecho vira uma linha `session_refresh` em `audit_logs` (`refresh_granted`, `refresh_granted_grace`, `denied_*`). Os dois métodos são públicos por desenho — o token de acesso já venceu quando o primeiro é chamado — e estão na allowlist fina de `endpoint_auth_posture_test.dart`. `logout` revoga a família, audita `logout` e ignora em silêncio um token que não conhece. O login só poda os tokens expirados do próprio usuário: não há limpeza periódica da tabela), `auth.requestOtp`/`auth.verifyOtp` (o login passwordless do paciente, RF01: CPF validado por dígito verificador e hasheado no servidor em `users.cpfHash` com `CPF_HASH_PEPPER`, mais a data de nascimento; código de 6 dígitos, com TTL de 5 minutos, teto de 5 verificações e 60 s entre pedidos, guardado em `otp_challenges` **como HMAC** — o código em claro existe só entre a geração e o envio — e uma linha de auditoria por desfecho. `requestOtp` responde a mesma coisa exista ou não o CPF — mesmo payload, mesmo status, sem linha de desafio e sem SMS na recusa —, mas **não** equaliza o *tempo* de resposta: as lacunas do RF01 estão no `PROGRESS.md`. `verifyOtp` emite a sessão do paciente com `patientSessionLifetime` (**1 hora**, LGPD-RT06) e não com os 15 minutos padrão, que o ACS mantém porque a renovação dele é feita pelo refresh token (abaixo) — o paciente não tem refresh token e um código OTP não pode ser reapresentado; o caminho de onboarding emite o mesmo TTL — o defeito dos 15 minutos do RF02 foi corrigido, ver `PROGRESS.md`), `onboarding.generateEnrollmentToken`/`onboarding.completeEnrollment` (o convite de uso único que o ACS gera e a conclusão dele pelo paciente; `completeEnrollment` também emite sessão, com `patientSessionLifetime`), `patients.acceptTermsOfUse` (aceite do Termo de Uso e da Política de Privacidade vigentes por quem entrou por OTP sem onboarding, LGPD-RF18; única via de escrita de `termsOfUse` fora do cadastro; idempotente — com o aceite vigente já gravado devolve a linha existente) e `patients.hasAcceptedCurrentTerms` (o `bool` que o login consulta, sem ler o painel nem auditar leitura), `patients.hasGrantedConsent` (o mesmo desenho para uma finalidade: `true` se a decisão mais recente é `granted`; o app a consulta a cada login antes de pedir o token de push ao FCM); a criação do pedido de exclusão (`patients.requestDataDeletion`) é serializada por `pg_advisory_xact_lock` por titular (o aceite do termo também, e o registro de token de push por token: `lockPerSubject` em `infrastructure/database/subject_lock.dart`, forma de duas chaves `(namespace, hashtext)`, que não colide com a chave única da cadeia de auditoria), `notices.sendSegmented` (RF14: só ACS, a microárea vem do token; a consulta SQL em `OrmNoticeRecipientStore` usa o consentimento `segmentedPush` **mais recente** de cada titular, não a existência do token; entrega a lista ao Gorush por `GorushClient` — `dart:io`, tempo limite de 5 s no total e de 2 s só na **conexão** (estourar na conexão é "inacessível, tente de novo", porque nada saiu; estourar depois de o pedido sair é "resultado desconhecido", e corpo 2xx ilegível idem), um único cliente por processo encerrado em `overrideConfig`, `GORUSH_URL` vazio desliga o envio e a chamada falha com `NoticeDeliveryException`; tokens que o provedor declara inválidos são apagados; audita `community_notice` com a microárea e nunca o texto), `patients.termsChangeNotice` (aviso de mudança dos termos com 15 dias de antecedência, LGPD-RF18: só paciente e sem auditoria; desde a #46 lê as versões publicadas de `terms_versions` (`TermsVersionStore.currentPublished`), não faz mais "sem I/O"; a constante `upcomingTermsChange` em `terms_change_schedule.dart` é só um espelho `null` mantido por dois testes, não é mais a agenda, e `TermsChangeSchedule` se recusa (com `ArgumentError`, que vale em release, não `assert`) a existir com menos de 15 dias entre as datas de publicação e de vigência declaradas — não prova que o aviso chegou a alguém, um `publishedAt` retroativo passa — para anunciar uma mudança: (1) escreva o texto novo em `upcomingLegalDocuments` no app e publique essa versão do app, (2) só então troque `upcomingTermsChange` aqui — um teste do app (`upcoming_legal_documents_test.dart`) falha se as duas pontas divergirem —, (3) na vigência mude `consentPolicyVersion` e `legalDocumentsVersion`, mova o texto para `privacyPolicy`/`termsOfUse` e volte as duas constantes para `null`; um app antigo, sem o texto embarcado, vê só "Ler os termos atuais" durante os 15 dias), `devices.registerPushToken` (RF14: só paciente, só com o consentimento `segmentedPush` vigente, uma linha por token em `push_tokens`; revogar o consentimento apaga os tokens do titular na MESMA transação do `denied`, sob o lock por titular; no máximo 10 tokens por titular, o mais antigo sai; só a troca de dono do token é auditada, com uma linha para o novo dono e outra para o anterior; a poda apaga por id **e** titular e poupa o token recém-gravado; o consentimento mais recente desempata por `id`), `alerts.createRedAlert` (idempotency key is a method parameter, not a header; throws `AlertDispatchUnavailableException` if the MQTT dispatcher isn't connected), `alerts.acknowledge`, `alerts.resolve` (#76: só de `acknowledged` para `resolved`, sob trava, no território do token, auditado na mesma transação; `resolved: false` para inexistente/fora do território; o ACK não rebaixa `resolved`), `triage.evaluate` (exige `accessToken`; classifica pelo `TriageEngine` determinístico, grava a sessão em `triage_sessions` com o `patientId` vindo do token — nunca de parâmetro, INV-05 — e escreve uma linha `write`/`triage_session` em `audit_logs`; só o papel `patient` é aceito, um ACS recebe `AlertPermissionException`), `visits.sync` (batch upload of visits registered offline by the ACS; deduplicated by the device-generated `localId`, which has a unique index on `visits`, version-checked — a mismatched `version` returns `SyncStatus.conflict` and never overwrites — and territory-checked against the patient's own micro-area, not just the caller's; a malformed identifier, a territory mismatch, or a visit owned by another ACS all return the terminal `SyncStatus.rejected`, distinct from the retryable `SyncStatus.error` used for things like an unknown patient, so the device queue knows which failures are worth retrying; a visit with no author — `acsId` null, `authorship = legacyUnclaimed` — is also `rejected` there, never claimed), `visits.syncLegacy` (D4 of the 2026-10-03 plan: uploads the device's pre-owner legacy visits with **unknown authorship** — the ACS session is only the transport, territory-checked like `sync`, idempotent by `localId` from the same `originDeviceId`, refuses an inactive ACS, never writes the transporter as `acsId`, at most `VisitSyncService.maxLegacyBatch` (200) visits per call, one `visit_legacy_sync` audit row per batch plus one `visit_legacy_sync_item` row per visit actually created or changed (only the visit id, no clinical content); a re-send of an authored `localId` at the same version is a no-op `synced`, at another version `rejected`), `visits.syncDeferred`/`visits.revokeUploadToken` (D7 of the 2026-10-03 plan: the **deferred upload token** of `UploadTokenService` (`application/auth/upload_token_service.dart`, store `OrmUploadTokenStore`) lets the device upload the pending visits of an ACS who already logged out, **with that ACS's authorship** and no session. `auth.loginInstitutional` issues it as `uploadToken` next to the refresh token, under the same rule — only with a non-blank `deviceId`, otherwise `null`; patient logins and `developmentLogin` never return it. 256 random bits, stored in `acs_upload_tokens` **only as SHA-256** (helpers shared with the refresh token in `opaque_token.dart`), bound to user + device, fixed 7-day life (`UploadTokenService.lifetime`), no rotation, independent of the refresh family (logout does not touch it); a new login on the same (user, device) revokes the previous one, inside a transaction serialized by `lockPerSubject` (namespace 5). `resolve` returns the **token owner** as ACS with the micro-area **re-read from the database** on every call, and every refusal — unknown, expired, revoked, other device, inactive account, no micro-area — is the same `SessionExpiredException` with `UploadTokenService.deniedMessage` (`Envio não autorizado. Entre novamente.`); other device and inactive/no micro-area also revoke the token. `syncDeferred` checks the batch cap `VisitSyncService.maxLegacyBatch` (200) before anything else (`AlertValidationException`), resolves the token outside the batch transaction and runs the **same** `VisitSyncService.sync` as `visits.sync` — so author = owner, territory = owner's current one, another ACS's `localId` `rejected`. Audit rows in `audit_logs`, never with the token: `upload_token` / `upload_token_issued`, `upload_token_denied_{revoked,device,expired,inactive}` (unknown token → no row, no subject), `upload_token_revoked`, and one `visit_deferred` / `visit_deferred_sync` per committed batch. Both methods are public by design for `authenticate(...)` and sit in the allowlist of `endpoint_auth_posture_test.dart`. Still open, by design or deferred (`PROGRESS.md`, section "Fila de visitas por dono e corrida do login"): `visits.sync`/`pull` accept an inactive ACS until the JWT expires (`isActiveAcs` is only checked by `syncLegacy`; `syncDeferred` re-checks it through the token); no database CHECK ties a null `acsId` to `authorship = legacyUnclaimed`; the existing `visits.sync` has no batch cap; a `localId` existence oracle remains, identical to `visits.sync`'s; `uploadToken`/`refreshToken` are plain RPC arguments, so any future argument logging must scrub them, see VAZ-03 in `spec/lgpd_data_audit.md`), and `patients.listMicroArea` (the ACS's routine-visit patient picker; the micro-area comes from the caller's token, never a parameter, and every call is written to `audit_logs`, whose rows are hash-chained — `AuditChain`/`AuditChainVerifier` in `application/audit/`, keyed by `AUDIT_CHAIN_SECRET` — so tampering with a row is detectable even by someone with direct Postgres write access; `bin/audit_chain_check.dart` verifies the chain on demand). `bin/seed_e2e_fixtures.dart` seeds the e2e stack (`docker-compose.e2e.yml`, see `apps/CLAUDE.md`): synthetic UUIDs/CPFs generated per run by `lib/src/infrastructure/testing/e2e_fixtures.dart`, written **only** to the database `sinalacs_e2e` (it refuses any other name and any `APP_ENV` but `development`); `development.sql` and its `*-seed` services stay for the plain development stack. Errors are typed exceptions declared in `.spy.yaml` and serialized to the client, replacing HTTP status codes. MQTT connects in the background after boot (non-blocking) with exponential-backoff auto-reconnect, so the server stays responsive even if the broker is unreachable — this matters on free-tier hosts that sleep/hibernate. Toda decisão de papel e de presença de território no token passa por uma regra única, `Authorization.require` (`lib/src/application/auth/authorization.dart`): o chamador decide o que lançar (`StateError` nos serviços territoriais, `TriageAuthorizationException` na triagem), e é isso que preserva a tradução diff --git a/docs/auditoria-prd/2026-10-15-lacunas-prd.md b/docs/auditoria-prd/2026-10-15-lacunas-prd.md index d09c7326..92909977 100644 --- a/docs/auditoria-prd/2026-10-15-lacunas-prd.md +++ b/docs/auditoria-prd/2026-10-15-lacunas-prd.md @@ -21,7 +21,7 @@ próprio; o conferidor exige apenas os IDs do PRD. | RF02 | Onboarding via QR Code | **entregue** | Convite de uso único em `onboarding_endpoint.dart:21` (`generateEnrollmentToken`) e `:38` (`completeEnrollment`); `qr_flutter` no ACS, `mobile_scanner` no paciente (`apps/patient/lib/app/qr_scanner.dart`); `onboarding_service_test.dart` recusa outra microárea; `qr_scanner_test`, `onboarding_flow_test`, `enrollment_qr_test` passam (36 testes, 2026-10-15). | | | RF03 | Botão de alerta de urgência (MQTT, GPS) | **entregue** | Botão `panic_button` 208x208 dp (`apps/patient/lib/app/app.dart:1380`); localização por `GeolocatorLocationReader` vira geocélula arredondada + hash antes do envio (`app.dart:1300-1320`); `alerts.createRedAlert` publica via outbox MQTT; ciclo provado em `red_alert_cycle_test.dart` (integração) e entrega p95 212 ms medida por `scripts/qa/measure_latency.dart` (2026-10-16 UTC). | | | RF04 | Triagem estruturada (árvore de decisão) | **entregue** | Seis perguntas sim/não em `TriageSymptom` (`apps/patient/lib/app/app.dart:1429`) mapeadas 1:1 para `TriageEngine.evaluate` (`backend/sinalacs_server/lib/src/application/triage/triage_engine.dart`); risco calculado só no servidor. | O §6.1 fala em 3 perguntas e o código usa 6: atualizar o texto do PRD (correção do PRD, não do código). | -| RF05 | Painel de status da solicitação | **parcial** | `alerts.statusFor` + tela de status (`apps/patient/lib/app/app.dart:2518`) mostram Enviado (pending) e Recebido pela equipe (acknowledged); sincronização periódica testada em `patient_app_mvp_test.dart`. | Nenhum código leva o alerta a `resolved` (só existe no enum `alert_status.spy.yaml:8`), então o paciente nunca vê Atendido: falta a ação do ACS de encerrar o atendimento e a transição no backend. | +| RF05 | Painel de status da solicitação | **entregue** | `alerts.resolve` (só ACS do território, só de `acknowledged`, auditado) + `alerts.statusFor` com `resolvedAt`; ACS "Concluir atendimento"; paciente "Atendido pela equipe de saúde". Provas: `red_alert_service_test.dart` (`resolve (#76)`), `red_alert_cycle_test.dart` (`concluir o atendimento (#76)`), `alert_resolve_test.dart`, `patient_app_mvp_test.dart`. | — | | RF06 | Lembretes de saúde (notificação local) | **entregue** | `flutter_local_notifications` + `apps/patient/lib/core/reminders/` (agendador e store `sqflite`); testes em `apps/patient/test/reminders/` passam. Sem endpoint por decisão §2.2.1 (§3.1). | | | RF18 | Modo escuro nativo | **parcial** | Paciente e ACS seguem o tema do sistema (`ThemeController` com `ThemeMode.system`, `darkTheme:` em `apps/patient/lib/app/app.dart:164` e `apps/acs/lib/app/app.dart:554`), com testes de contraste nos dois temas (`contrast_tokens_test.dart`, `contrast_tokens_light_test.dart`). | O backoffice (`apps/admin/lib/app/admin_theme.dart:48`) só tem tema escuro fixo (`Brightness.dark`), sem tema claro nem `themeMode`. | @@ -170,7 +170,7 @@ próprio; o conferidor exige apenas os IDs do PRD. | M1.3 | Motor de triagem (MCDC, 50 cenários) | **entregue** | Ver RF16: 64 combinações (tabela-verdade completa) + 8 cenários nomeados. | Sem relatório MCDC por ferramenta; a tabela-verdade completa o torna redundante para seis booleanos. | | M1.4 | FSM de sincronização | **entregue** | `backend/sinalacs_server/lib/src/application/sync/sync_fsm.dart` e a FSM do aparelho em `offline_visit_queue.dart`; testes de transição, conflito e erro passam na linha de base. | | | M1.5 | SQLCipher local | **entregue** | SQLCipher no ACS (`apps/acs/lib/core/database/encrypted_database.dart`, `integration_test/encrypted_storage_test.dart`). O paciente usa `sqflite` sem cifra, de propósito, para lembretes e preferências de consentimento. | Lembretes de saúde do paciente em texto livre sem cifra (ver STRIDE-I). | -| M2.1 | App Paciente MVP | **parcial** | Login, alerta, triagem e status no app do paciente (RF01, RF03, RF04, RF05). | SMS real (RF01) e o estado Atendido do status (RF05). | +| M2.1 | App Paciente MVP | **parcial** | Login, alerta, triagem e status no app do paciente (RF01, RF03, RF04, RF05). | SMS real (RF01). | | M2.2 | App ACS MVP | **entregue** | Login, territorialização, dashboard e registro no ACS (RF07, RF08, RF09, RF11). | | | M2.3 | MQTT com TLS (WSS) | **parcial** | MQTT sobre TLS com mTLS na 8883 (`mtls_invariants.sh` OK). | O critério fala em WSS, e WebSockets (9001) não é publicado por decisão; o broker aceita TLS 1.2 (ver RNF04). | | M2.4 | 100 registros sincronizam em < 5 s | **parcial** | 100 visitas por lote em p95 112 ms, 10/10 (meta < 5 s), `measure_latency.dart` contra a stack local (2026-10-16 UTC); `offline_burst_test.dart`. | Só stack local, 1 cliente sequencial, 10 amostras (p95 de 10 amostras é quase o máximo): repetir em produção, com carga e mais amostras. | @@ -189,7 +189,7 @@ próprio; o conferidor exige apenas os IDs do PRD. | MVP-LOGIN-60S | 10 pacientes logam em < 60 s | **parcial** | Fluxo OTP funciona (RF01). | SMS real e medição com 10 pacientes. | | MVP-ALERTA-2S | Alerta chega ao ACS em < 2 s | **parcial** | p95 212 ms na stack local (RNF01). | Só stack local, 1 cliente sequencial, 10 amostras (p95 de 10 amostras é quase o máximo): repetir em produção, com carga e mais amostras. | | MVP-TRIAGEM-3P | Triagem de 3 perguntas | **entregue** | Seis perguntas, classificação determinística (RF04). | Corrigir 3 para 6 no texto do PRD. | -| MVP-STATUS | Enviado → Visualizado → Atendido | **parcial** | Enviado e Recebido aparecem (RF05). | Atendido nunca aparece (RF05). | +| MVP-STATUS | Enviado → Visualizado → Atendido | **entregue** | Enviado, Recebido e Atendido aparecem (RF05, #76): `alerts.resolve` (só ACS do território, só de `acknowledged`, auditado) + `alerts.statusFor` com `resolvedAt`; ACS "Concluir atendimento"; paciente "Atendido pela equipe de saúde". | — | | MVP-LOGIN-ACS | ACS loga com matrícula e senha | **entregue** | Ver RF07. | | | MVP-TERRITORIO-30S | Microárea carrega em < 30 s | **parcial** | Cache da microárea entregue (RF08). | Medir o download (< 30 s). | | MVP-FILA-ORDEM | Lista Vermelho → Amarelo → Verde | **entregue** | Ver RF09. | | @@ -226,7 +226,7 @@ Critério: **P0** toca invariante ou o núcleo do MVP (§6.1) sem critério atin | # | Prioridade | Lacuna | IDs | Tipo | Justificativa | |---|---|---|---|---|---| | 1 | P0 | **Resolvido (2026-10-16, #75).** Colunas de risco da triagem e do alerta em texto plano (`triage_sessions.resultRisk`/`resultDisplay`, `visits.riskLevelBefore`/`riskLevelAfter`, `alerts.riskLevel`) | INV-04, RNF03, LGPD-RF09 | código | Viola uma invariante; ou cifrar, ou registrar no PRD por que o risco precisa ficar legível para a fila. | -| 2 | P0 | Paciente nunca vê Atendido: nenhum caminho leva o alerta a `resolved` | RF05, MVP-STATUS, M2.1 | código | Critério do núcleo do MVP (§6.1) não atingido; precisa da ação do ACS de encerrar o atendimento. | +| 2 | P0 | **Resolvido (2026-10-16, #76).** Paciente nunca vê Atendido: nenhum caminho leva o alerta a `resolved` | RF05, MVP-STATUS, M2.1 | código | Critério do núcleo do MVP (§6.1) não atingido; precisa da ação do ACS de encerrar o atendimento. | | 3 | P0 | Envio de OTP por SMS real (provedor + telefone do paciente) | RF01, MVP-LOGIN-60S, M2.1, R6 | código + externo | Sem SMS o paciente só entra com o ACS presente gerando um QR (R6): não há login por conta própria fora do ambiente de dev; a escolha de provedor/contrato é de produto. | | 4 | P1 | Broker MQTT aceita TLS 1.2; Traefik → backend em HTTP | RNF04, M2.3, CRIPTO-TRAEFIK-BACKEND | infra | Risco Crítico; `tls_version tlsv1.3` no `mosquitto.conf` e o caso no `tls_invariants.sh` é pequeno; mTLS interno exige decisão. | | 5 | P1 | Resolução de conflito de visita no ACS (comparar e escolher versão) | R1, DADOS-VERSION-OTIMISTA | código | Risco de impacto 5; hoje a visita em conflito fica parada na fila para sempre. | diff --git a/docs/superpowers/plans/2026-10-16-issue-76-alerta-atendido.md b/docs/superpowers/plans/2026-10-16-issue-76-alerta-atendido.md new file mode 100644 index 00000000..830461dc --- /dev/null +++ b/docs/superpowers/plans/2026-10-16-issue-76-alerta-atendido.md @@ -0,0 +1,1487 @@ +# Issue #76 — Paciente vê "Atendido" (alerta chega a `resolved`) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Dar ao ACS a ação "Concluir atendimento", que leva o alerta vermelho de `acknowledged` a `resolved` no backend, para o paciente ver "Atendido" no painel de status (RF05, MVP-STATUS, M2.1). + +**Architecture:** Novo método RPC `alerts.resolve` → `RedAlertService.resolve` → `AlertStore.resolve` (ORM com `SELECT … FOR UPDATE`, auditoria na mesma transação, fail-closed), coluna nova `alerts.resolvedAt` (migração aditiva). O ACK passa a travar a linha e nunca rebaixa `resolved` para `acknowledged`. O app do ACS ganha o botão no cartão já confirmado (com diálogo de confirmação); o app do paciente rotula `resolved` como "Atendido" e mostra a hora. O pulso do backoffice passa a enxergar a conclusão. + +**Tech Stack:** Serverpod 3.4.13 (Dart), PostgreSQL, Flutter (apps `acs` e `patient`), `flutter_test`, `package:test` + harness `withServerpod`. + +**Spec:** issue #76 (`gh issue view 76`), item 2 (P0) de `docs/auditoria-prd/2026-10-15-lacunas-prd.md`; requisito em `spec/PRD_system.md` §2.2 (RF05) e §6.1 ("Paciente visualiza 'Enviado → Visualizado → Atendido'"). + +## Global Constraints + +- Idioma: código comentado, mensagens e documentação em português, como o resto do repositório. +- Nunca editar à mão `lib/src/generated/` nem `migrations/`: `serverpod generate` + `serverpod create-migration` (de `backend/sinalacs_server`). +- INV-01: o ACS só conclui alerta da própria microárea; a microárea vem do token, nunca de parâmetro. +- INV-03: alerta vermelho nunca é descartado em silêncio — `resolve` só sai de `acknowledged`; um `pending` **não** pode ser concluído (precisa de ACK antes), e toda mudança de status é auditada na mesma transação (fail-closed). +- Toda escrita em `alerts` fica em arquivo listado em `_allowedWriters` de `test/unit/red_alert_discard_paths_test.dart`. +- Cor é sinal clínico: textos/ícones sobre `Card` usam os tokens `*OnSurface` (`context.acsRisk.greenOnSurface`, `context.patientRisk.accentOnSurface`). +- Alvos de toque ≥ 48×52 dp (`minimumSize: const Size(48, 52)`). +- Sem dado real de paciente em teste, log ou captura; ids sintéticos. +- Commits sem atribuição de IA (regra do `CLAUDE.md` da raiz, que prevalece sobre o lembrete de atribuição). +- Testes de integração do backend: `docker compose --profile test up -d postgres-test` e `dart test -j 1`. + +## Review Focus + +1. **ACK depois da conclusão** (reentrega MQTT, toque atrasado em outro aparelho): o alerta deve continuar `resolved`; o ACK antigo sobrescrevia o status sem reler. Pinado na Task 2 (`confirmar de novo depois de concluído não volta a acknowledged`). +2. **Concluir um alerta ainda `pending`** (ACS pula a confirmação, ou o ACK falhou em silêncio): recusa com mensagem clara, alerta segue `pending` e a recusa entra na trilha. Pinado nas Tasks 1 e 2. +3. **Concluir duas vezes** (toque duplo, retry de rede): resposta idempotente, `resolvedAt` original preservado, sem segunda transição. Pinado nas Tasks 1 e 2 (`already_resolved`); a trava `_concluindo` da Task 4 é só defesa, e o servidor é a garantia. +4. **Alerta de outra microárea ou id que não é UUID**: `resolved: false`, nada muda, nada é gravado (mesma resposta do ACK para inexistente). Pinado na Task 2. +5. **Backoffice não percebe a conclusão** (o pulso só olhava `triggeredAt`/`acknowledgedAt`/`escalatedAt`): a lista de Alertas ficaria mostrando "Reconhecido" até outro evento. Pinado na Task 3. + +--- + +## File Structure + +| Arquivo | Responsabilidade | +|---|---| +| `backend/sinalacs_server/lib/src/application/alerts/red_alert_service.dart` | `AlertResolveOutcome`, `AlertResolveAttempt`, `AlertStore.resolve`, `RedAlertService.resolve`, `AlertStatusSnapshot.resolvedAt` | +| `backend/sinalacs_server/lib/src/models/alert.spy.yaml` | coluna `resolvedAt` | +| `backend/sinalacs_server/lib/src/models/api/alert_status_result.spy.yaml` | campo `resolvedAt` | +| `backend/sinalacs_server/lib/src/models/api/alert_resolve_result.spy.yaml` (novo) | resultado do `alerts.resolve` | +| `backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart` | `resolve` com trava; ACK trava e preserva `resolved`; `latestForPatient` com `resolvedAt` | +| `backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart` | `alerts.resolve`; `statusFor` devolve `resolvedAt` | +| `backend/sinalacs_server/lib/src/infrastructure/database/orm_admin_read_store.dart` | `pulse.maxChangedAt` inclui `resolvedAt` | +| `apps/acs/lib/core/network/backend_client.dart` | `AcsBackend.resolveAlert` | +| `apps/acs/lib/core/services/alert_queue.dart` | `PrioritizedAlert.resolved`, `AlertQueue.markResolved`, ordenação | +| `apps/acs/lib/app/app.dart` | botão "Concluir atendimento", diálogo, `_resolve` | +| `apps/patient/lib/app/app.dart` | rótulo "Atendido" e hora da conclusão | +| docs (`PROGRESS.md`, `spec/lgpd_data_audit.md`, `docs/auditoria-prd/2026-10-15-lacunas-prd.md`, `CLAUDE.md`, `backend/CLAUDE.md`) | registro | + +--- + +### Task 1: Regra de conclusão no serviço (hermético) + +**Files:** +- Modify: `backend/sinalacs_server/lib/src/application/alerts/red_alert_service.dart` +- Modify (fakes): `backend/sinalacs_server/test/unit/red_alert_service_test.dart`, `test/unit/red_alert_discard_paths_test.dart` (`_MemoryStore`), `test/unit/red_alert_lifecycle_test.dart` (`RecordingStore`) +- Test: `backend/sinalacs_server/test/unit/red_alert_service_test.dart` + +**Interfaces:** +- Consumes: `Authorization.require`, `AuditEvent`, `AuthenticatedUser`, `UserRole` (existentes). +- Produces: + - `enum AlertResolveOutcome { notFound, notAcknowledged, resolved, alreadyResolved }` + - `class AlertResolveAttempt { const AlertResolveAttempt(this.outcome, {this.resolvedAt}); final AlertResolveOutcome outcome; final DateTime? resolvedAt; }` + - `AlertStore.resolve({required String alertId, required String microAreaId, required DateTime now, required AuditEvent Function(AlertResolveOutcome outcome) auditFor}) → Future` + - `RedAlertService.resolve({required AuthenticatedUser user, required String alertId}) → Future` (lança `StateError` para papel/território, `ArgumentError` para id vazio e para `notAcknowledged`) + - `RedAlertService.notAcknowledgedMessage = 'Confirme o recebimento antes de concluir o atendimento.'` + - `AlertStatusSnapshot.resolvedAt` (`DateTime?`, parâmetro nomeado opcional) + +- [ ] **Step 1: Escrever os testes que falham** + +Em `test/unit/red_alert_service_test.dart`, acrescentar ao `FakeAlertStore` (logo depois de `latestForPatient`): + +```dart + /// Desfecho que o "banco" decide para o próximo `resolve` (#76). + AlertResolveOutcome resolveOutcome = AlertResolveOutcome.resolved; + final List<({String alertId, String microAreaId, DateTime now})> resolveCalls = []; + + @override + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }) async { + resolveCalls.add((alertId: alertId, microAreaId: microAreaId, now: now)); + // Mesma regra do store real: inexistente/fora do território não grava. + if (resolveOutcome != AlertResolveOutcome.notFound) { + audits.add(auditFor(resolveOutcome)); + } + final carimbo = switch (resolveOutcome) { + AlertResolveOutcome.resolved || AlertResolveOutcome.alreadyResolved => now, + _ => null, + }; + return AlertResolveAttempt(resolveOutcome, resolvedAt: carimbo); + } +``` + +E, antes de `group('statusFor (RF05)'`, o grupo novo: + +```dart + group('resolve (#76)', () { + const acs = AuthenticatedUser( + id: 'acs-001', + role: UserRole.acs, + microAreaId: 'area-12', + deviceId: 'device-001', + ); + final agora = DateTime.utc(2026, 10, 16, 14); + + RedAlertService servico(FakeAlertStore store) => RedAlertService( + store: store, + outbox: FakeAlertOutbox(), + clock: () => agora, + ); + + test('conclui no território do token e audita resolved', () async { + final store = FakeAlertStore(); + + final r = await servico(store).resolve(user: acs, alertId: 'alerta-1'); + + expect(r.outcome, AlertResolveOutcome.resolved); + expect(r.resolvedAt, agora); + expect(store.resolveCalls.single.microAreaId, 'area-12'); + expect(store.resolveCalls.single.now, agora); + final audit = store.audits.single; + expect(audit.userId, 'acs-001'); + expect(audit.actionType, 'write'); + expect(audit.resourceType, 'alert'); + expect(audit.resourceId, 'alerta-1'); + expect(audit.result, 'resolved'); + }); + + test('repetir a conclusão é idempotente e audita already_resolved', () async { + final store = FakeAlertStore()..resolveOutcome = AlertResolveOutcome.alreadyResolved; + + final r = await servico(store).resolve(user: acs, alertId: 'alerta-1'); + + expect(r.outcome, AlertResolveOutcome.alreadyResolved); + expect(store.audits.single.result, 'already_resolved'); + }); + + test('alerta ainda pending é recusado com a mensagem e a recusa é auditada', () async { + final store = FakeAlertStore()..resolveOutcome = AlertResolveOutcome.notAcknowledged; + + await expectLater( + servico(store).resolve(user: acs, alertId: 'alerta-1'), + throwsA(isA().having( + (e) => e.message, + 'message', + RedAlertService.notAcknowledgedMessage, + )), + ); + expect(store.audits.single.result, 'denied_not_acknowledged'); + }); + + test('alerta inexistente ou de outra microárea devolve notFound sem auditar', () async { + final store = FakeAlertStore()..resolveOutcome = AlertResolveOutcome.notFound; + + final r = await servico(store).resolve(user: acs, alertId: 'alerta-1'); + + expect(r.outcome, AlertResolveOutcome.notFound); + expect(r.resolvedAt, isNull); + expect(store.audits, isEmpty); + }); + + test('paciente não conclui atendimento', () async { + final store = FakeAlertStore(); + const paciente = AuthenticatedUser( + id: 'paciente-1', + role: UserRole.patient, + microAreaId: 'area-12', + deviceId: 'device-1', + ); + + await expectLater( + servico(store).resolve(user: paciente, alertId: 'alerta-1'), + throwsA(isA()), + ); + expect(store.resolveCalls, isEmpty); + }); + + test('ACS sem microárea não conclui atendimento', () async { + final store = FakeAlertStore(); + const semArea = AuthenticatedUser( + id: 'acs-002', + role: UserRole.acs, + microAreaId: null, + deviceId: 'device-2', + ); + + await expectLater( + servico(store).resolve(user: semArea, alertId: 'alerta-1'), + throwsA(isA()), + ); + expect(store.resolveCalls, isEmpty); + }); + + test('identificador vazio é recusado antes do store', () async { + final store = FakeAlertStore(); + + await expectLater( + servico(store).resolve(user: acs, alertId: ' '), + throwsA(isA()), + ); + expect(store.resolveCalls, isEmpty); + }); + }); +``` + +Nos outros dois fakes, só para compilar (nenhum teste deles conclui alerta): + +`test/unit/red_alert_discard_paths_test.dart`, dentro de `_MemoryStore`: + +```dart + @override + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }) async { + if (status[alertId] != 'acknowledged') { + return const AlertResolveAttempt(AlertResolveOutcome.notAcknowledged); + } + audits.add(auditFor(AlertResolveOutcome.resolved)); + status[alertId] = 'resolved'; + return AlertResolveAttempt(AlertResolveOutcome.resolved, resolvedAt: now); + } +``` + +`test/unit/red_alert_lifecycle_test.dart`, dentro de `RecordingStore`: + +```dart + @override + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }) async { + throw UnimplementedError('não usado nos testes deste arquivo'); + } +``` + +(Se `red_alert_lifecycle_test.dart` não importar `audit_trail.dart`, acrescente `import 'package:sinalacs_server/src/application/audit/audit_trail.dart';`.) + +- [ ] **Step 2: Rodar e ver falhar** + +Run: `cd backend/sinalacs_server && dart test test/unit/red_alert_service_test.dart` +Expected: FAIL de compilação — `AlertResolveOutcome`/`AlertResolveAttempt` não definidos. + +- [ ] **Step 3: Implementar** + +Em `lib/src/application/alerts/red_alert_service.dart`: + +1. Na interface `AlertStore`, depois de `acknowledge`: + +```dart + /// Conclui o atendimento (#76): leva o alerta de `acknowledged` a + /// `resolved` no território do ACS. O desfecho é decidido sob trava da + /// linha, e `auditFor(desfecho)` é gravado na MESMA transação (fail-closed) + /// — exceto [AlertResolveOutcome.notFound], que não grava nada, como o ACK. + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }); +``` + +2. Depois da classe `RedAlertRecord`: + +```dart +/// Desfecho de [AlertStore.resolve] (#76). +enum AlertResolveOutcome { + /// Inexistente ou de outra microárea: a mesma resposta, nada gravado. + notFound, + + /// Ainda `pending`: o recebimento precisa ser confirmado antes (INV-03 — + /// concluir sem ACK apagaria do painel um vermelho que ninguém confirmou). + notAcknowledged, + + /// `acknowledged` → `resolved` agora. + resolved, + + /// Já estava `resolved`: nada muda e o `resolvedAt` original volta. + alreadyResolved, +} + +class AlertResolveAttempt { + const AlertResolveAttempt(this.outcome, {this.resolvedAt}); + + final AlertResolveOutcome outcome; + + /// Quando o atendimento foi concluído; nulo em [AlertResolveOutcome.notFound] + /// e [AlertResolveOutcome.notAcknowledged]. + final DateTime? resolvedAt; +} +``` + +3. Em `AlertStatusSnapshot`, acrescentar `this.resolvedAt,` ao construtor e o campo: + +```dart + /// Conclusão do atendimento pelo ACS (#76); nulo até lá. + final DateTime? resolvedAt; +``` + +4. Em `RedAlertService`, depois de `acknowledge`: + +```dart + /// Texto da recusa de concluir um alerta que ninguém confirmou (#76). + static const notAcknowledgedMessage = + 'Confirme o recebimento antes de concluir o atendimento.'; + + /// Conclui o atendimento (#76, RF05): é a transição que faz o paciente ver + /// "Atendido". Só o ACS do território do alerta, e só depois do ACK. + /// Cada ato que encontra o alerta é auditado, inclusive a repetição e a + /// recusa por falta de ACK; a recusa vira [ArgumentError] DEPOIS de a + /// trilha gravar. + Future resolve({ + required AuthenticatedUser user, + required String alertId, + }) async { + Authorization.require( + user, + roles: {UserRole.acs}, + onDenied: () => + StateError('Somente ACS territorializados podem concluir atendimentos.'), + ); + if (alertId.trim().isEmpty) { + throw ArgumentError('O identificador do alerta é obrigatório.'); + } + final attempt = await _store.resolve( + alertId: alertId, + microAreaId: user.microAreaId!, + now: _clock().toUtc(), + auditFor: (outcome) => AuditEvent( + userId: user.id, + actionType: 'write', + resourceType: 'alert', + resourceId: alertId, + result: switch (outcome) { + AlertResolveOutcome.resolved => 'resolved', + AlertResolveOutcome.alreadyResolved => 'already_resolved', + AlertResolveOutcome.notAcknowledged => 'denied_not_acknowledged', + // O store não grava este; o texto existe só para o switch ser total. + AlertResolveOutcome.notFound => 'denied_not_found', + }, + ), + ); + if (attempt.outcome == AlertResolveOutcome.notAcknowledged) { + throw ArgumentError(notAcknowledgedMessage); + } + return attempt; + } +``` + +5. Em `lib/src/infrastructure/database/orm_alert_store.dart`, um stub temporário só para compilar (a Task 2 o substitui; não faça commit sem a Task 2 se preferir commits verdes ponta a ponta — o stub mantém `dart analyze` limpo): + +```dart + @override + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }) => + throw UnimplementedError('Task 2 (#76)'); +``` + +- [ ] **Step 4: Rodar e ver passar** + +Run: `cd backend/sinalacs_server && dart test test/unit && dart analyze` +Expected: PASS (todos os unitários, incluindo os 7 novos de `resolve (#76)`); analyze sem issues. + +- [ ] **Step 5: Commit** + +```bash +git add backend/sinalacs_server/lib/src/application/alerts/red_alert_service.dart \ + backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart \ + backend/sinalacs_server/test/unit/red_alert_service_test.dart \ + backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart \ + backend/sinalacs_server/test/unit/red_alert_lifecycle_test.dart +git commit -m "feat(backend): regra de concluir o atendimento do alerta (#76)" +``` + +--- + +### Task 2: Coluna, store ORM, ACK que não rebaixa e `alerts.resolve` + +**Files:** +- Modify: `backend/sinalacs_server/lib/src/models/alert.spy.yaml` +- Modify: `backend/sinalacs_server/lib/src/models/api/alert_status_result.spy.yaml` +- Create: `backend/sinalacs_server/lib/src/models/api/alert_resolve_result.spy.yaml` +- Modify: `backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart` +- Modify: `backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart` +- Modify: `backend/sinalacs_server/test/unit/red_alert_discard_paths_test.dart` (só o texto de `_allowedWriters`) +- Generated: `lib/src/generated/**`, `backend/sinalacs_client/**`, `test/integration/test_tools/serverpod_test_tools.dart`, `migrations//` +- Test: `backend/sinalacs_server/test/integration/red_alert_cycle_test.dart` + +**Interfaces:** +- Consumes: tudo o que a Task 1 produz. +- Produces: + - Coluna `alerts."resolvedAt"` (`timestamp without time zone`, nula). + - `AlertStatusResult.resolvedAt: DateTime?`. + - `AlertResolveResult { String alertId; bool resolved; AlertStatus? status; DateTime? resolvedAt; }` (no cliente gerado também). + - RPC `alerts.resolve(accessToken: String, alertId: String) → AlertResolveResult`; lança `AlertValidationException` (id vazio, `notAcknowledged`) e `AlertPermissionException` (papel/território do token). + +- [ ] **Step 1: Modelos** + +`lib/src/models/alert.spy.yaml`, logo depois de `acknowledgedAt: DateTime?`: + +```yaml + ### Atendimento concluído pelo ACS (#76): carimbado uma única vez, na + ### transição acknowledged → resolved. Nulo enquanto não foi atendido. + resolvedAt: DateTime? +``` + +`lib/src/models/api/alert_status_result.spy.yaml`, ao fim de `fields:`: + +```yaml + resolvedAt: DateTime? +``` + +Criar `lib/src/models/api/alert_resolve_result.spy.yaml`: + +```yaml +### Resultado de alerts.resolve (#76). +### +### Mesmo desenho de AlertAckResult: resolved: false significa que não havia +### alerta com esse id na microárea do ACS. Concluir duas vezes devolve +### resolved: true com o resolvedAt da primeira conclusão. +class: AlertResolveResult +fields: + alertId: String + resolved: bool + status: AlertStatus? + resolvedAt: DateTime? +``` + +- [ ] **Step 2: Gerar código e migração** + +```bash +cd backend/sinalacs_server +serverpod generate +serverpod create-migration +``` + +Expected: novo diretório em `migrations/` cujo `migration.sql` contém só `ALTER TABLE "alerts" ADD COLUMN "resolvedAt" timestamp without time zone;` (aditiva, sem `DROP`). Confira: `rtk proxy grep -c DROP migrations/$(ls migrations | grep -v registry | tail -1)/migration.sql` deve dar `0`. + +- [ ] **Step 3: Escrever os testes de integração que falham** + +Em `test/integration/red_alert_cycle_test.dart`, dentro de `withServerpod('Dado o ciclo de alerta vermelho', …)`, depois do teste `'paciente consulta o status do próprio alerta mais recente'`: + +```dart + group('concluir o atendimento (#76)', () { + Future<({String alertId, String acs})> alertaConfirmado() async { + await _seed(sessionBuilder.build()); + final paciente = + await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + final criado = await endpoints.alerts.createRedAlert( + sessionBuilder, + accessToken: paciente.accessToken, + idempotencyKey: 'resolve-${DateTime.now().microsecondsSinceEpoch}', + locationHash: 'hash-sintetico', + ); + final acs = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); + await endpoints.alerts.acknowledge( + sessionBuilder, + accessToken: acs.accessToken, + alertId: criado.alertId, + ); + return (alertId: criado.alertId, acs: acs.accessToken); + } + + Future> resultadosDaTrilha(String alertId) async { + final linhas = await AuditLog.db.find( + sessionBuilder.build(), + where: (t) => + t.resourceType.equals('alert') & + t.resourceId.equals(UuidValue.fromString(alertId)), + orderBy: (t) => t.sequence, + ); + return linhas.map((l) => l.result).toList(); + } + + test('leva a resolved, o paciente vê e repetir preserva o resolvedAt', () async { + final (:alertId, :acs) = await alertaConfirmado(); + + final r = await endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs, + alertId: alertId, + ); + expect(r.resolved, isTrue); + expect(r.status, AlertStatus.resolved); + expect(r.resolvedAt, isNotNull); + + final paciente = + await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + final status = await endpoints.alerts.statusFor( + sessionBuilder, + accessToken: paciente.accessToken, + ); + expect(status.status, AlertStatus.resolved); + expect(status.resolvedAt, r.resolvedAt); + expect(status.acknowledgedAt, isNotNull); + + final deNovo = await endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs, + alertId: alertId, + ); + expect(deNovo.resolved, isTrue); + expect(deNovo.resolvedAt, r.resolvedAt); + + expect(await resultadosDaTrilha(alertId), + ['acknowledged', 'resolved', 'already_resolved']); + }); + + test('alerta ainda pending é recusado e segue pending', () async { + await _seed(sessionBuilder.build()); + final paciente = + await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + final criado = await endpoints.alerts.createRedAlert( + sessionBuilder, + accessToken: paciente.accessToken, + idempotencyKey: 'resolve-pending', + locationHash: 'hash-sintetico', + ); + final acs = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); + + await expectLater( + endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs.accessToken, + alertId: criado.alertId, + ), + throwsA(isA().having( + (e) => e.message, + 'message', + RedAlertService.notAcknowledgedMessage, + )), + ); + + final linha = await Alert.db.findById( + sessionBuilder.build(), + UuidValue.fromString(criado.alertId), + ); + expect(linha!.status, AlertStatus.pending); + expect(linha.resolvedAt, isNull); + expect(await resultadosDaTrilha(criado.alertId), ['denied_not_acknowledged']); + }); + + test('confirmar de novo depois de concluído não volta a acknowledged', () async { + final (:alertId, :acs) = await alertaConfirmado(); + await endpoints.alerts.resolve(sessionBuilder, accessToken: acs, alertId: alertId); + + final ack = await endpoints.alerts.acknowledge( + sessionBuilder, + accessToken: acs, + alertId: alertId, + ); + expect(ack.acknowledged, isTrue); + + final linha = await Alert.db.findById( + sessionBuilder.build(), + UuidValue.fromString(alertId), + ); + expect(linha!.status, AlertStatus.resolved); + expect(linha.resolvedAt, isNotNull); + }); + + test('alerta de outra microárea: resolved false, nada muda nem é auditado', () async { + await _seed(sessionBuilder.build()); + final session = sessionBuilder.build(); + final outro = await Alert.db.insertRow( + session, + Alert( + patientId: UuidValue.fromString(_patientId), + microAreaId: UuidValue.fromString('00000000-0000-4000-8000-0000000076aa'), + triggeredAt: DateTime.now().toUtc(), + acknowledgedAt: DateTime.now().toUtc(), + riskLevel: RiskLevel.red, + locationHash: 'hash-sintetico', + status: AlertStatus.acknowledged, + mqttTopic: 'sinalacs/v1/microareas/outra/alerts', + deviceId: 'device-sintetico', + retryCount: 0, + version: 1, + ), + ); + final acs = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); + + final r = await endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs.accessToken, + alertId: outro.id!.uuid, + ); + + expect(r.resolved, isFalse); + expect(r.status, isNull); + expect((await Alert.db.findById(session, outro.id!))!.status, + AlertStatus.acknowledged); + expect(await resultadosDaTrilha(outro.id!.uuid), isEmpty); + }); + + test('identificador que não é UUID devolve resolved false', () async { + await _seed(sessionBuilder.build()); + final acs = await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs'); + + final r = await endpoints.alerts.resolve( + sessionBuilder, + accessToken: acs.accessToken, + alertId: 'nao-e-uuid', + ); + + expect(r.resolved, isFalse); + }); + + test('paciente não conclui atendimento', () async { + final (:alertId, acs: _) = await alertaConfirmado(); + final paciente = + await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + + await expectLater( + endpoints.alerts.resolve( + sessionBuilder, + accessToken: paciente.accessToken, + alertId: alertId, + ), + throwsA(isA()), + ); + }); + }); +``` + +Se o arquivo ainda não importar `red_alert_service.dart`, ele já importa (linha 3); `AuditLog` vem de `generated/protocol.dart`. + +- [ ] **Step 4: Rodar e ver falhar** + +Run: `docker compose --profile test up -d postgres-test && cd backend/sinalacs_server && dart test -j 1 test/integration/red_alert_cycle_test.dart` +Expected: FAIL — `UnimplementedError('Task 2 (#76)')` (o endpoint `resolve` ainda não existe → também falha de compilação; qualquer um dos dois serve como "vermelho"). + +- [ ] **Step 5: Implementar o store** + +Em `lib/src/infrastructure/database/orm_alert_store.dart`: + +1. Import: `import 'package:uuid/uuid.dart';` (já dependência do servidor — `red_alert_service.dart` a usa). + +2. Substituir o stub de `resolve` por: + +```dart + @override + Future resolve({ + required String alertId, + required String microAreaId, + required DateTime now, + required AuditEvent Function(AlertResolveOutcome outcome) auditFor, + }) async { + // Id que não é UUID é "não existe", não erro de servidor: mesma resposta + // de um alerta de outra microárea. + if (!Uuid.isValidUUID(fromString: alertId) || + !Uuid.isValidUUID(fromString: microAreaId)) { + return const AlertResolveAttempt(AlertResolveOutcome.notFound); + } + final session = _session(); + + Future run(Transaction transaction) async { + // Escopo e trava numa instrução. O predicado de microárea é a barreira + // de territorialização (INV-01); o FOR UPDATE serializa com o ACK e com + // o escalonamento, que releem o status sob a mesma trava. + final linhas = await session.db.unsafeQuery( + ''' + SELECT status, "resolvedAt" + FROM alerts + WHERE id = @id::uuid AND "microAreaId" = @ma::uuid + FOR UPDATE + ''', + parameters: QueryParameters.named({'id': alertId, 'ma': microAreaId}), + transaction: transaction, + ); + if (linhas.isEmpty) { + return const AlertResolveAttempt(AlertResolveOutcome.notFound); + } + + final status = linhas.single[0] as String; + final AlertResolveAttempt attempt; + if (status == AlertStatus.resolved.name) { + attempt = AlertResolveAttempt( + AlertResolveOutcome.alreadyResolved, + resolvedAt: (linhas.single[1] as DateTime?)?.toUtc(), + ); + } else if (status != AlertStatus.acknowledged.name) { + attempt = const AlertResolveAttempt(AlertResolveOutcome.notAcknowledged); + } else { + final agora = now.toUtc(); + // Só status e resolvedAt: as colunas do ACK e do escalonamento ficam. + await session.db.unsafeExecute( + ''' + UPDATE alerts + SET status = @resolvido, "resolvedAt" = @agora::timestamp + WHERE id = @id::uuid + ''', + parameters: QueryParameters.named({ + 'id': alertId, + 'resolvido': AlertStatus.resolved.name, + 'agora': agora, + }), + transaction: transaction, + ); + attempt = AlertResolveAttempt(AlertResolveOutcome.resolved, resolvedAt: agora); + } + + // Fail-closed e por último, como no ACK: a trava da cadeia nunca + // precede outra. + await _appendAudit(session, transaction, auditFor(attempt.outcome)); + return attempt; + } + + final existing = _transaction; + return existing != null ? run(existing) : session.db.transaction(run); + } +``` + +3. Em `acknowledge`, dentro de `run`, trocar a leitura sem trava + +```dart + final alert = await Alert.db.findFirstRow( + session, + where: (t) => + t.id.equals(alertUuid) & t.microAreaId.equals(microAreaUuid), + transaction: transaction, + ); + if (alert == null) return false; +``` + +por + +```dart + // Trava antes de ler (#76): um `resolve` concorrente que já gravou + // `resolved` é visto aqui, em vez de ser sobrescrito por um ACK que leu + // a linha antes dele. + final travado = await session.db.unsafeQuery( + ''' + SELECT id FROM alerts + WHERE id = @id::uuid AND "microAreaId" = @ma::uuid + FOR UPDATE + ''', + parameters: QueryParameters.named({'id': alertId, 'ma': microAreaId}), + transaction: transaction, + ); + if (travado.isEmpty) return false; + final alert = + (await Alert.db.findById(session, alertUuid, transaction: transaction))!; +``` + +e, no `copyWith` do `updateRow`, trocar `status: AlertStatus.acknowledged,` por: + +```dart + // ACK depois da conclusão (reentrega, outro aparelho) não rebaixa + // o atendimento já concluído (#76). + status: alert.status == AlertStatus.resolved + ? AlertStatus.resolved + : AlertStatus.acknowledged, +``` + +4. Em `latestForPatient`, acrescentar `resolvedAt: row.resolvedAt,` ao `AlertStatusSnapshot(...)`. + +- [ ] **Step 6: Implementar o endpoint** + +Em `lib/src/endpoints/alerts_endpoint.dart`: + +1. Depois de `acknowledge`: + +```dart + /// Conclui o atendimento de um alerta já confirmado (#76, RF05): é o que + /// faz o paciente ver "Atendido". Alerta inexistente ou de outra microárea + /// devolve `resolved: false`, como o ACK; alerta ainda não confirmado é + /// [AlertValidationException] com o texto de + /// `RedAlertService.notAcknowledgedMessage`. + Future resolve( + Session session, { + required String accessToken, + required String alertId, + }) async { + final user = authenticate(accessToken); + final service = AlertRuntime.instance.serviceFor(session); + + try { + final attempt = await service.resolve(user: user, alertId: alertId); + final found = attempt.outcome != AlertResolveOutcome.notFound; + return AlertResolveResult( + alertId: alertId, + resolved: found, + status: found ? AlertStatus.resolved : null, + resolvedAt: attempt.resolvedAt, + ); + } on ArgumentError catch (error) { + throw AlertValidationException(message: '${error.message}'); + } on StateError catch (error) { + throw AlertPermissionException(message: error.message); + } + } +``` + +e o import `import 'package:sinalacs_server/src/application/alerts/red_alert_service.dart';` (para `AlertResolveOutcome`). + +2. Em `statusFor`, acrescentar `resolvedAt: snapshot.resolvedAt,` ao `AlertStatusResult(...)`. + +3. Em `test/unit/red_alert_discard_paths_test.dart`, atualizar o motivo de `orm_alert_store.dart` em `_allowedWriters`: + +```dart + 'infrastructure/database/orm_alert_store.dart': + 'save() insere o alerta como pending; acknowledge() leva a acknowledged ' + '(e preserva resolved); resolve() leva acknowledged → resolved, sob ' + 'trava e auditado na mesma transação (#76)', +``` + +- [ ] **Step 7: Regenerar e rodar** + +```bash +cd backend/sinalacs_server +serverpod generate # o método novo entra no cliente e no test_tools +dart test -j 1 test/integration/red_alert_cycle_test.dart test/integration/red_alert_discard_paths_db_test.dart test/unit +dart analyze +``` + +Expected: PASS em todos (os 6 novos de `concluir o atendimento (#76)` incluídos); analyze limpo. Se `endpoint_auth_posture_test.dart` reclamar, confira que `resolve` chama `authenticate(accessToken)` (é o que ele exige). + +- [ ] **Step 8: Commit** + +```bash +git add backend/sinalacs_server/lib backend/sinalacs_server/migrations \ + backend/sinalacs_server/test backend/sinalacs_client +git commit -m "feat(backend): alerts.resolve leva o alerta confirmado a resolved (#76)" +``` + +--- + +### Task 3: Pulso do backoffice enxerga a conclusão + +**Files:** +- Modify: `backend/sinalacs_server/lib/src/infrastructure/database/orm_admin_read_store.dart` (SQL de `pulse`, ~linha 95) +- Test: `backend/sinalacs_server/test/integration/admin_monitoring_store_test.dart` + +**Interfaces:** +- Consumes: coluna `alerts."resolvedAt"` (Task 2). +- Produces: `AlertsPulseCounts.maxChangedAt` também avança com `resolvedAt`. + +- [ ] **Step 1: Escrever o teste que falha** + +Em `test/integration/admin_monitoring_store_test.dart`, logo depois de `test('maxChangedAt anda com o ACK do alerta', …)`: + +```dart + test('maxChangedAt anda com a conclusão do atendimento (#76)', () async { + final a = await _alerta( + session, + em: _t.subtract(const Duration(hours: 1)), + ); + final ack = _t.subtract(const Duration(minutes: 10)); + final concluido = _t.subtract(const Duration(minutes: 2)); + await Alert.db.updateRow( + session, + a.copyWith( + status: AlertStatus.resolved, + acknowledgedAt: ack, + resolvedAt: concluido, + ), + ); + + final r = await pulso(const AdminScope.ubs(_ubsA)); + expect(r.maxChangedAt, concluido); + expect(r.overdueRed, 0); + }); +``` + +- [ ] **Step 2: Rodar e ver falhar** + +Run: `cd backend/sinalacs_server && dart test -j 1 test/integration/admin_monitoring_store_test.dart` +Expected: FAIL — `maxChangedAt` é `ack`, não `concluido`. + +- [ ] **Step 3: Implementar** + +No SQL de `pulse`, trocar + +```sql + max(GREATEST(a."triggeredAt", + COALESCE(a."acknowledgedAt", a."triggeredAt"), + COALESCE(a."escalatedAt", a."triggeredAt"))) +``` + +por + +```sql + max(GREATEST(a."triggeredAt", + COALESCE(a."acknowledgedAt", a."triggeredAt"), + COALESCE(a."escalatedAt", a."triggeredAt"), + COALESCE(a."resolvedAt", a."triggeredAt"))) +``` + +- [ ] **Step 4: Rodar e ver passar** + +Run: `cd backend/sinalacs_server && dart test -j 1 test/integration/admin_monitoring_store_test.dart test/integration/admin_read_store_test.dart` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add backend/sinalacs_server/lib/src/infrastructure/database/orm_admin_read_store.dart \ + backend/sinalacs_server/test/integration/admin_monitoring_store_test.dart +git commit -m "fix(backend): pulso do backoffice anda com a conclusão do alerta (#76)" +``` + +--- + +### Task 4: App do ACS — "Concluir atendimento" + +**Files:** +- Modify: `apps/acs/lib/core/network/backend_client.dart` (interface `AcsBackend` ~linha 128, recusador ~linha 251, `BackendClient` ~linha 718) +- Modify: `apps/acs/lib/core/services/alert_queue.dart` +- Modify: `apps/acs/lib/app/app.dart` (`_acknowledge` ~linha 1610, `onAcknowledge:` ~1966, `DashboardScreen` ~2171, `_AlertCard` ~2285) +- Modify: `apps/acs/test/support/fakes.dart` (`FakeAcsBackend`) +- Modify: `apps/acs/integration_test/full_journey_e2e.dart` (`_EnvioDeVisitasForaDoAr`, só repasse) +- Create: `apps/acs/test/alert_resolve_test.dart` + +**Interfaces:** +- Consumes: `AlertResolveResult` e `endpoint alerts.resolve` do cliente gerado (Task 2). +- Produces: + - `AcsBackend.resolveAlert({required String alertId}) → Future` + - `PrioritizedAlert.resolved` (`bool`, padrão `false`); `copyWith({bool? acknowledged, bool? escalated, bool? resolved})` + - `AlertQueue.markResolved(String alertId) → bool` + - `DashboardScreen(onResolve: void Function(PrioritizedAlert)?)`; chaves `resolve_`, `confirmar_conclusao`, `cancelar_conclusao`, `resolved_` + +- [ ] **Step 1: Fakes** + +`FakeAcsBackend` (`test/support/fakes.dart`), ao lado de `acknowledge`: + +```dart + final List resolvedAlertIds = []; + + /// `false` simula alerta que a central não encontra na microárea. + bool resolved = true; + + /// Quando presente, `resolveAlert` lança esta falha (ex.: alerta pending). + BackendFailure? resolveFailure; + + @override + Future resolveAlert({required String alertId}) async { + final falha = resolveFailure; + if (falha != null) throw falha; + resolvedAlertIds.add(alertId); + return AlertResolveResult( + alertId: alertId, + resolved: resolved, + status: resolved ? AlertStatus.resolved : null, + resolvedAt: resolved ? DateTime.utc(2026, 10, 16, 14) : null, + ); + } +``` + +`integration_test/full_journey_e2e.dart`, em `_EnvioDeVisitasForaDoAr`, seguindo o padrão dos outros repasses da classe: + +```dart + @override + Future resolveAlert({required String alertId}) => + _inner.resolveAlert(alertId: alertId); +``` + +- [ ] **Step 2: Escrever os testes que falham** + +Criar `apps/acs/test/alert_resolve_test.dart`: + +```dart +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_acs/app/acs_theme.dart'; +import 'package:sinalacs_acs/app/app.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; + +import 'support/fakes.dart'; + +/// "Concluir atendimento" (#76): a ação do ACS que faz o paciente ver +/// "Atendido". +void main() { + group('AlertQueue.markResolved', () { + test('marca concluído, implica confirmado e desce abaixo dos confirmados', () { + final queue = AlertQueue(microAreaId: seedMicroAreaId) + ..upsert(testAlert(alertId: 'a', triggeredAt: DateTime.utc(2026, 10, 16, 8))) + ..upsert(testAlert(alertId: 'b', triggeredAt: DateTime.utc(2026, 10, 16, 9))) + ..markAcknowledged('a') + ..markAcknowledged('b'); + + expect(queue.markResolved('a'), isTrue); + + expect(queue.alerts.map((x) => x.alertId), ['b', 'a']); + expect(queue.alerts.last.resolved, isTrue); + expect(queue.alerts.last.acknowledged, isTrue); + expect(queue.unacknowledgedCount, 0); + }); + + test('alerta fora da fila devolve false', () { + final queue = AlertQueue(microAreaId: seedMicroAreaId); + expect(queue.markResolved('nao-existe'), isFalse); + }); + + test('reentrega de um alerta concluído não o reabre', () { + final queue = AlertQueue(microAreaId: seedMicroAreaId) + ..upsert(testAlert(alertId: 'a')) + ..markAcknowledged('a') + ..markResolved('a'); + + expect(queue.upsert(testAlert(alertId: 'a')), isFalse); + expect(queue.upsert(testAlert(alertId: 'a', escalated: true)), isFalse); + expect(queue.alerts.single.resolved, isTrue); + }); + }); + + group('cartão do alerta', () { + Future pintar(WidgetTester tester, AlertQueue queue, {void Function(PrioritizedAlert)? onResolve}) => + tester.pumpWidget(MaterialApp( + theme: buildAcsDarkTheme(), + home: Scaffold(body: DashboardScreen(queue: queue, onResolve: onResolve)), + )); + + testWidgets('só o alerta confirmado oferece "Concluir atendimento"', (tester) async { + final queue = AlertQueue(microAreaId: seedMicroAreaId) + ..upsert(testAlert(alertId: 'novo')) + ..upsert(testAlert(alertId: 'visto')) + ..markAcknowledged('visto'); + await pintar(tester, queue, onResolve: (_) {}); + + expect(find.byKey(const Key('resolve_novo')), findsNothing); + expect(find.byKey(const Key('resolve_visto')), findsOneWidget); + final altura = tester.getSize(find.byKey(const Key('resolve_visto'))).height; + expect(altura, greaterThanOrEqualTo(48)); + }); + + testWidgets('concluído mostra rótulo e ícone, sem botões de ação', (tester) async { + final handle = tester.ensureSemantics(); + final queue = AlertQueue(microAreaId: seedMicroAreaId) + ..upsert(testAlert(alertId: 'a1')) + ..markAcknowledged('a1') + ..markResolved('a1'); + await pintar(tester, queue, onResolve: (_) {}); + + expect(find.byKey(const Key('resolved_a1')), findsOneWidget); + expect(find.byKey(const Key('resolve_a1')), findsNothing); + expect(find.byKey(const Key('ack_a1')), findsNothing); + expect(find.text('Acionar SAMU / Atender'), findsNothing); + expect(find.text('Iniciar rota de visita'), findsNothing); + expect(find.bySemanticsLabel(RegExp('atendimento concluído')), findsOneWidget); + final texto = tester.widget(find.byKey(const Key('resolved_a1'))); + expect(texto.style?.color, AcsRiskColors.dark.greenOnSurface); + handle.dispose(); + }); + }); + + group('fluxo no painel', () { + Future abrirComAlertaConfirmado(WidgetTester tester, FakeAcsBackend backend) async { + late FakeAlertFeed feed; + await tester.pumpWidget(SinalAcsApp( + backend: backend, + feedBuilder: (queue) => feed = FakeAlertFeed(queue), + )); + await entrar(tester); + await tester.pumpAndSettle(); + feed.deliver(testAlert(alertId: 'alerta-1')); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('ack_alerta-1'))); + await tester.pumpAndSettle(); + return feed; + } + + testWidgets('confirma no diálogo, chama o servidor e marca concluído', (tester) async { + final backend = FakeAcsBackend(); + await abrirComAlertaConfirmado(tester, backend); + + await tester.tap(find.byKey(const Key('resolve_alerta-1'))); + await tester.pumpAndSettle(); + expect(find.text('Concluir o atendimento?'), findsOneWidget); + await tester.tap(find.byKey(const Key('confirmar_conclusao'))); + await tester.pumpAndSettle(); + + expect(backend.resolvedAlertIds, ['alerta-1']); + expect(find.byKey(const Key('resolved_alerta-1')), findsOneWidget); + expect(find.text('Atendimento concluído. O paciente já vê "Atendido".'), findsOneWidget); + }); + + testWidgets('cancelar o diálogo não chama o servidor', (tester) async { + final backend = FakeAcsBackend(); + await abrirComAlertaConfirmado(tester, backend); + + await tester.tap(find.byKey(const Key('resolve_alerta-1'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('cancelar_conclusao'))); + await tester.pumpAndSettle(); + + expect(backend.resolvedAlertIds, isEmpty); + expect(find.byKey(const Key('resolve_alerta-1')), findsOneWidget); + }); + + testWidgets('recusa do servidor aparece e o alerta segue por concluir', (tester) async { + final backend = FakeAcsBackend() + ..resolveFailure = const BackendFailure( + 'Confirme o recebimento antes de concluir o atendimento.', + isRecoverable: false, + ); + await abrirComAlertaConfirmado(tester, backend); + + await tester.tap(find.byKey(const Key('resolve_alerta-1'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('confirmar_conclusao'))); + await tester.pumpAndSettle(); + + expect(find.text('Confirme o recebimento antes de concluir o atendimento.'), findsOneWidget); + expect(find.byKey(const Key('resolve_alerta-1')), findsOneWidget); + expect(find.byKey(const Key('resolved_alerta-1')), findsNothing); + }); + + testWidgets('alerta que a central não encontra não é marcado concluído', (tester) async { + final backend = FakeAcsBackend()..resolved = false; + await abrirComAlertaConfirmado(tester, backend); + + await tester.tap(find.byKey(const Key('resolve_alerta-1'))); + await tester.pumpAndSettle(); + await tester.tap(find.byKey(const Key('confirmar_conclusao'))); + await tester.pumpAndSettle(); + + expect(find.text('A central não reconheceu este alerta.'), findsOneWidget); + expect(find.byKey(const Key('resolved_alerta-1')), findsNothing); + }); + }); +} +``` + +(`testAlert`, `seedMicroAreaId`, `FakeAlertFeed` e `entrar` já existem em `test/support/`; confira o import de `entrar` usado por `test/login_flow_test.dart` e replique-o se ele vier de outro arquivo de suporte.) + +- [ ] **Step 3: Rodar e ver falhar** + +Run: `cd apps/acs && flutter test test/alert_resolve_test.dart` +Expected: FAIL de compilação — `resolveAlert`, `markResolved`, `onResolve` não existem. + +- [ ] **Step 4: Implementar a rede** + +`lib/core/network/backend_client.dart`: + +Interface, depois de `acknowledge`: + +```dart + /// Conclui o atendimento de um alerta já confirmado (#76): o paciente passa + /// a ver "Atendido". + Future resolveAlert({required String alertId}); +``` + +Recusador (classe do `_recusar()`, ~linha 251): + +```dart + @override + Future resolveAlert({required String alertId}) async => _recusar(); +``` + +`BackendClient`, depois de `acknowledge`: + +```dart + /// Conclui o atendimento (#76). Alerta inexistente ou de outra microárea + /// volta `resolved: false`; alerta ainda não confirmado é recusado pelo + /// servidor com texto pronto, que o `_guard` repassa. + @override + Future resolveAlert({required String alertId}) async { + final token = await _requireToken(); + return _guard( + () => _client.alerts.resolve(accessToken: token, alertId: alertId), + ); + } +``` + +- [ ] **Step 5: Implementar a fila** + +`lib/core/services/alert_queue.dart`: + +- Construtor de `PrioritizedAlert`: `this.resolved = false,` depois de `this.escalated = false,`. +- Campo: + +```dart + /// O ACS concluiu o atendimento (#76). Implica [acknowledged]. + final bool resolved; +``` + +- `copyWith({bool? acknowledged, bool? escalated, bool? resolved})` com `resolved: resolved ?? this.resolved,`. +- Depois de `markAcknowledged`: + +```dart + /// Marca o atendimento como concluído (e, por consequência, confirmado). + /// Devolve `false` se o alerta não está na fila. + bool markResolved(String alertId) { + final index = _alerts.indexWhere((item) => item.alertId == alertId); + if (index < 0) return false; + + _alerts[index] = _alerts[index].copyWith(acknowledged: true, resolved: true); + _sort(); + notifyListeners(); + return true; + } +``` + +- Em `_sort`, depois da linha dos confirmados: + +```dart + // Concluídos descem abaixo dos confirmados: já não pedem ação. + if (a.resolved != b.resolved) return a.resolved ? 1 : -1; +``` + +(`upsert` já devolve `false` para alerta confirmado, o que cobre o concluído.) + +- [ ] **Step 6: Implementar a tela** + +`lib/app/app.dart`: + +1. Depois de `_acknowledge`: + +```dart + /// Trava o toque duplo do "Concluir atendimento". + final Set _concluindo = {}; + + /// "Concluir atendimento" (#76): confirma com o ACS, porque o paciente passa + /// a ver "Atendido" e o servidor não volta atrás. + Future _resolve(PrioritizedAlert alert) async { + if (_concluindo.contains(alert.alertId)) return; + final messenger = ScaffoldMessenger.maybeOf(context); + void aviso(String texto) => messenger + ?..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(texto))); + final confirmou = await showDialog( + context: context, + builder: (dialog) => AlertDialog( + title: const Text('Concluir o atendimento?'), + content: const Text( + 'O paciente verá "Atendido" no aplicativo. Conclua só depois de atender o paciente.', + ), + actions: [ + TextButton( + key: const Key('cancelar_conclusao'), + onPressed: () => Navigator.of(dialog).pop(false), + style: TextButton.styleFrom(minimumSize: const Size(48, 52)), + child: const Text('Cancelar'), + ), + FilledButton( + key: const Key('confirmar_conclusao'), + onPressed: () => Navigator.of(dialog).pop(true), + style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), + child: const Text('Concluir'), + ), + ], + ), + ); + if (confirmou != true || !mounted) return; + _concluindo.add(alert.alertId); + try { + final result = await BackendScope.of(context).resolveAlert(alertId: alert.alertId); + if (result.resolved && mounted) _queue.markResolved(alert.alertId); + aviso(result.resolved + ? 'Atendimento concluído. O paciente já vê "Atendido".' + : 'A central não reconheceu este alerta.'); + } on BackendFailure catch (failure) { + // Nunca engolir: o alerta segue confirmado e por concluir. + aviso(failure.message); + } finally { + _concluindo.remove(alert.alertId); + } + } +``` + +2. Onde o painel monta `DashboardScreen` (~linha 1966), ao lado de `onAcknowledge: _acknowledge,`: `onResolve: _resolve,`. + +3. `DashboardScreen`: novo parâmetro `this.onResolve,`, campo `final void Function(PrioritizedAlert alert)? onResolve;` e repasse `onResolve: onResolve,` ao `_AlertCard` (~linha 2231), seguindo exatamente o desenho de `onAcknowledge`. + +4. `_AlertCard`: construtor `const _AlertCard({required this.alert, this.onAcknowledge, this.onEscalate, this.onVisit, this.onResolve});` e campo `final void Function(PrioritizedAlert alert)? onResolve;`. + +- No `label` do `Semantics`, depois do trecho de `acknowledged`: + +```dart + '${alert.resolved ? ', atendimento concluído' : ''}' +``` + +- Substituir o bloco `if (alert.acknowledged) Padding(… 'Recebimento confirmado' …)` por: + +```dart + if (alert.resolved) Padding( + padding: const EdgeInsets.only(top: 8), + child: Row(children: [ + Icon(Icons.task_alt, size: 16, color: context.acsRisk.greenOnSurface), + const SizedBox(width: 6), + Text('Atendimento concluído', + key: Key('resolved_${alert.alertId}'), + style: TextStyle(color: context.acsRisk.greenOnSurface)), + ]), + ) else if (alert.acknowledged) Padding( + padding: const EdgeInsets.only(top: 8), + child: Row(children: [ + Icon(Icons.check_circle, size: 16, color: context.acsRisk.greenOnSurface), + const SizedBox(width: 6), + Text('Recebimento confirmado', style: TextStyle(color: context.acsRisk.greenOnSurface)), + ]), + ), +``` + +- Depois do bloco `if (!alert.acknowledged) SizedBox(… ack …)`, acrescentar: + +```dart + if (alert.acknowledged && !alert.resolved) SizedBox( + width: double.infinity, + child: FilledButton.icon( + key: Key('resolve_${alert.alertId}'), + onPressed: onResolve == null ? null : () => onResolve!(alert), + style: FilledButton.styleFrom(minimumSize: const Size(48, 52)), + icon: const Icon(Icons.task_alt), + label: const Text('Concluir atendimento'), + ), + ), +``` + +- Envolver o `const SizedBox(height: 8)` e o `SizedBox(… OutlinedButton … 'Acionar SAMU / Atender' …)` finais em `if (!alert.resolved) ...[ … ]`. + +- [ ] **Step 7: Rodar e ver passar** + +```bash +cd apps/acs +flutter test test/alert_resolve_test.dart +flutter test +flutter analyze +``` + +Expected: PASS (os 9 novos e a suíte inteira, inclusive `text_scale_test.dart`, `login_flow_test.dart`, `alert_card_escalation_test.dart`); analyze limpo. + +- [ ] **Step 8: Commit** + +```bash +git add apps/acs/lib apps/acs/test apps/acs/integration_test/full_journey_e2e.dart +git commit -m "feat(acs): concluir o atendimento do alerta confirmado (#76)" +``` + +--- + +### Task 5: App do paciente — "Atendido" + +**Files:** +- Modify: `apps/patient/lib/app/app.dart` (`_StatusCard`, ~linha 2503) +- Test: `apps/patient/test/patient_app_mvp_test.dart` (grupo do status, ~linha 1547) + +**Interfaces:** +- Consumes: `AlertStatusResult.resolvedAt` (cliente gerado, Task 2). +- Produces: chave `status_resolved_at`. + +- [ ] **Step 1: Escrever o teste que falha** + +Depois de `testWidgets('mostra o status real devolvido pelo servidor', …)`: + +```dart + testWidgets('alerta concluído pela equipe aparece como "Atendido" (#76)', (tester) async { + final backend = FakePatientBackend() + ..statusResult = AlertStatusResult( + found: true, + alertId: 'alerta-1', + riskLevel: RiskLevel.red, + status: AlertStatus.resolved, + triggeredAt: DateTime.utc(2026, 9, 18, 9), + acknowledgedAt: DateTime.utc(2026, 9, 18, 9, 5), + resolvedAt: DateTime.utc(2026, 9, 18, 10, 40), + ); + await tester.pumpWidget(SinalAcsApp(backend: backend)); + await login(tester); + + await tester.tap(find.text('Status')); + await tester.pumpAndSettle(); + + expect(find.text('Atendido pela equipe de saúde'), findsOneWidget); + expect(find.byKey(const Key('status_resolved_at')), findsOneWidget); + expect(find.textContaining('Atendimento concluído às'), findsOneWidget); + // A confirmação continua visível: é a etapa "Visualizado" do PRD §6.1. + expect(find.textContaining('Confirmado pela equipe às'), findsOneWidget); + }); +``` + +- [ ] **Step 2: Rodar e ver falhar** + +Run: `cd apps/patient && flutter test test/patient_app_mvp_test.dart --plain-name 'Atendido'` +Expected: FAIL — o rótulo atual é "Atendimento concluído" e não há `status_resolved_at`. + +- [ ] **Step 3: Implementar** + +Em `_StatusCard.build`: + +- Trocar `AlertStatus.resolved => 'Atendimento concluído',` por `AlertStatus.resolved => 'Atendido pela equipe de saúde',`. +- Depois de `final acknowledgedAt = status.acknowledgedAt;`: `final resolvedAt = status.resolvedAt;`. +- Depois do bloco `if (acknowledgedAt != null) ...[ … ]`: + +```dart + if (resolvedAt != null) ...[ + const SizedBox(height: 4), + Text( + key: const Key('status_resolved_at'), + 'Atendimento concluído às ${_formatTime(resolvedAt)}', + ), + ], +``` + +- [ ] **Step 4: Rodar e ver passar** + +```bash +cd apps/patient +flutter test +flutter analyze +``` + +Expected: PASS; analyze limpo. + +- [ ] **Step 5: Commit** + +```bash +git add apps/patient/lib/app/app.dart apps/patient/test/patient_app_mvp_test.dart +git commit -m "feat(patient): status mostra Atendido quando a equipe conclui (#76)" +``` + +--- + +### Task 6: Documentação, verificação completa e grafo + +**Files:** +- Modify: `PROGRESS.md`, `spec/lgpd_data_audit.md` (tabela `alerts`, ~linha 75), `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (linhas RF05 ~24, M2.1 ~173, MVP-STATUS ~192, backlog item 2 ~229), `CLAUDE.md` (lista de endpoints: `alerts`), `backend/CLAUDE.md` (lista de endpoints, ao lado de `alerts.acknowledge`) + +- [ ] **Step 1: Atualizar os documentos** + +- `spec/lgpd_data_audit.md`, na tabela de `alerts`, logo depois da linha de `acknowledgedAt`: + +```markdown +| | `resolvedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de conclusão | Conclusão do atendimento pelo ACS (#76); é o que o paciente vê como "Atendido" (RF05). Sem dado pessoal. | +``` + +- `PROGRESS.md`, nova seção depois de "Colunas de risco cifradas (#75)": + +```markdown +## Estado "Atendido" do alerta (#76) — 2026-10-16 + +- Entregue: `alerts.resolve` (só ACS do território, só de `acknowledged`, idempotente, auditado na mesma transação: `resolved`, `already_resolved`, `denied_not_acknowledged`); coluna `alerts.resolvedAt` (migração aditiva); o ACK trava a linha e não rebaixa `resolved`; `alerts.statusFor` devolve `resolvedAt`; o pulso do backoffice anda com a conclusão. ACS: botão "Concluir atendimento" no cartão confirmado, com diálogo. Paciente: "Atendido pela equipe de saúde" e a hora da conclusão. +- Provas: `test/unit/red_alert_service_test.dart` (grupo `resolve (#76)`), `test/integration/red_alert_cycle_test.dart` (grupo `concluir o atendimento (#76)`), `test/integration/admin_monitoring_store_test.dart`, `apps/acs/test/alert_resolve_test.dart`, `apps/patient/test/patient_app_mvp_test.dart`. +- Aberto: sem prova no emulador (o e2e do paciente não tem ACS concluindo); o contador "reconhecidos" do backoffice (`acknowledgedRedAlerts`) conta só `acknowledged`, então um alerta concluído sai dele (o TMRAV não muda); a conclusão vive só na fila em memória do ACS, como o ACK. +``` + +- `docs/auditoria-prd/2026-10-15-lacunas-prd.md`: RF05 e MVP-STATUS para **entregue** com a evidência (`alerts.resolve`, os testes acima) e falta `—`; M2.1 continua **parcial**, com a falta reduzida a "SMS real (RF01)"; no backlog, item 2 prefixado com `**Resolvido (2026-10-16, #76).**`, como o item 1. +- `CLAUDE.md` (raiz): na lista de endpoints, `alerts` (`createRedAlert`, `acknowledge`, `resolve`, `statusFor`). +- `backend/CLAUDE.md`: depois de `` `alerts.acknowledge`, `` acrescentar `` `alerts.resolve` (#76: só de `acknowledged` para `resolved`, sob trava, no território do token, auditado na mesma transação; `resolved: false` para inexistente/fora do território; o ACK não rebaixa `resolved`), ``. + +- [ ] **Step 2: Conferir a contagem do relatório** + +Run: `cd /home/rock/Documents/Dev/APPs/SinalACS && python3 scripts/qa/cobertura_prd.py --final` +Expected: sai com 0. Se acusar divergência de contagem, ajuste o cabeçalho de contagem do relatório (e a linha correspondente em `PROGRESS.md`, seção "Auditoria de lacunas do PRD") para os números que o script imprimir. + +- [ ] **Step 3: Verificação completa** + +```bash +cd /home/rock/Documents/Dev/APPs/SinalACS/backend && dart analyze +cd sinalacs_server && dart test -j 1 +cd ../../apps/acs && flutter analyze && flutter test +cd ../patient && flutter analyze && flutter test +cd ../admin && flutter analyze && flutter test +``` + +Expected: tudo verde. O admin não muda de código, mas consome o cliente regenerado — por isso entra na verificação. Anote as contagens de testes impressas para o PR. + +- [ ] **Step 4: Atualizar o grafo** + +Run: `cd /home/rock/Documents/Dev/APPs/SinalACS && graphify update .` +Expected: termina sem erro. + +- [ ] **Step 5: Commit** + +```bash +git add PROGRESS.md spec/lgpd_data_audit.md docs/auditoria-prd/2026-10-15-lacunas-prd.md CLAUDE.md backend/CLAUDE.md graphify-out +git commit -m "docs: registra o estado Atendido do alerta (#76)" +``` + +(Se `graphify-out/` não for versionado — confira com `git check-ignore graphify-out` —, tire-o do `git add`.) diff --git a/spec/lgpd_data_audit.md b/spec/lgpd_data_audit.md index 1a544c0e..44469114 100644 --- a/spec/lgpd_data_audit.md +++ b/spec/lgpd_data_audit.md @@ -74,6 +74,7 @@ A tabela abaixo consolida o mapeamento exaustivo de dados persistidos pelo backe | | `receivedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de entrega | Telemetria de entrega no dispositivo do agente comunitário. | | | `respondedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de ação | Telemetria de início de resposta do ACS. | | | `acknowledgedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de confirmação | Registro formal de visualização do alerta vermelho (garantia da INV-03). | +| | `resolvedAt` | `timestamp without time zone` | Metadado Operacional | Timestamp de conclusão | Conclusão do atendimento pelo ACS (#76); é o que o paciente vê como "Atendido" (RF05). Sem dado pessoal. | | | `escalatedAt` | `timestamp without time zone` (anulável) | Metadado Operacional | Timestamp do escalonamento (relógio do servidor) | Quando a coordenação escalonou o vermelho vencido pelo backoffice (issue #44); nulo enquanto não escalonado. Escalonar não muda o `status` (o alerta segue `pending`). | | | `escalatedBy` | `uuid` (anulável) | Pseudonimizado (dado pessoal do profissional) | Chave para `users.id` do staff (coordenador/administrador) que escalonou | Identifica o **profissional**, não o titular, como `decidedBy` de `data_subject_requests`; nunca vai para o payload MQTT do reforço nem para a resposta do RPC. Nulo enquanto não escalonado. | | | `escalationReason` | `text` (anulável) | Metadado Operacional | Enum fechado `EscalationReason` (`noAckTimeout`, `acsUnreachable`, `patientRecontact`) | Motivo escolhido numa lista fechada, **sem texto livre**: não há campo em que o operador possa digitar dado do paciente. Nulo enquanto não escalonado. | From a24c502d878341248fa4d0ab9084c78fc00067a8 Mon Sep 17 00:00:00 2001 From: Herbert Rocha Date: Fri, 16 Oct 2026 13:05:51 -0400 Subject: [PATCH 7/7] =?UTF-8?q?docs(code):=20corrige=20coment=C3=A1rios=20?= =?UTF-8?q?da=20trava=20do=20ACK=20e=20da=20conclus=C3=A3o=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/acs/lib/app/app.dart | 4 +++- .../lib/src/infrastructure/database/orm_alert_store.dart | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index b281b853..502eb638 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -1625,7 +1625,9 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver } } - /// Trava o toque duplo do "Concluir atendimento". + /// Alertas com a conclusão em voo. O diálogo modal já impede dois toques; + /// este conjunto impede uma segunda conclusão do mesmo alerta enquanto a + /// chamada ao servidor não volta (o servidor também é idempotente). final Set _concluindo = {}; /// "Concluir atendimento" (#76): confirma com o ACS, porque o paciente passa diff --git a/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart b/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart index 9062cfee..689151e8 100644 --- a/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart +++ b/backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart @@ -145,9 +145,9 @@ class OrmAlertStore implements AlertStore { ? AlertStatus.resolved : AlertStatus.acknowledged, ), - // Só as colunas do ACK: a linha foi lida sem lock, e um escalonamento - // (#44) gravado entre a leitura e esta escrita seria apagado por um - // UPDATE da linha inteira com os valores velhos. + // A linha agora é lida sob FOR UPDATE (acima). Restringir às colunas + // do ACK fica como defesa extra: um UPDATE da linha inteira não pode + // apagar o escalonamento (#44) nem a conclusão (#76). columns: (t) => [t.acsId, t.receivedAt, t.acknowledgedAt, t.status], transaction: transaction, );