diff --git a/CLAUDE.md b/CLAUDE.md index 7770b0b..9763a25 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`, `resolve`, `statusFor`), `patients` (`listMicroArea`, `myData`, chronic conditions, `updateConsent`, `requestDataDeletion`, `requestDataCorrection`), `visits` (`sync`, `pull` e `resolveConflict` (#79) 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`, `openForMicroArea` — a reserva por HTTP do ACS quando o MQTT cai, #80), `patients` (`listMicroArea`, `myData`, chronic conditions, `updateConsent`, `requestDataDeletion`, `requestDataCorrection`), `visits` (`sync`, `pull` e `resolveConflict` (#79) 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 8d716c9..c6fedb6 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -1683,3 +1683,15 @@ Backlog priorizado na seção final do relatório; conferência por `python3 scr - Quando "Limpar este aparelho" é bloqueado por um conflito, a mensagem usa a frase genérica "precisa de revisão pelo ACS dono", sem dizer "conflito". - Screenshots da tela de conflito. - O conflito só aparece na tela de registro de visita (sem selo no painel), como as recusadas. + +## Reserva por HTTP dos alertas do ACS (#80) — 2026-10-17 + +- Entregue: `alerts.openForMicroArea` (só ACS com microárea, microárea do token) devolve os alertas `pending`/`escalated` e depois os `acknowledged` da microárea, do mais antigo ao mais novo, teto 200 (`RedAlertService.openAlertsLimit`), com `escalated` derivado de `escalatedAt` ou do status legado. App ACS: `AcsBackend.openAlerts`, `AlertHttpFallback` e `AlertQueue.mergeFromServer`; o `AcsHomeShell` consulta ao abrir o painel e ao voltar ao primeiro plano e, a cada 30 s, só enquanto o broker está fora e o app em primeiro plano; linha `alert_fallback_status` no painel. Sem migração. +- Decisões: D1 os confirmados voltam (o painel os mantém até a conclusão; reabrir o app depois de "Sair" os reconstrói), concluídos não; D2 sem linha de auditoria de leitura por consulta (repete a cada 30 s; mesmo conteúdo do MQTT, que não é auditado; ACK e conclusão seguem auditados — o e2e conferiu 0 `read` de `alert`); D3 consulta periódica só com o broker fora e em primeiro plano, intervalo fixo sem backoff; D4 o chip "Sem central" fica como está, quem explica a reserva é a linha de status; D5 nenhum alerta gravado no aparelho (a mitigação do PRD cita `sqflite`; a reserva reconstrói a fila a partir do servidor, sem dado de saúde a mais no SQLCipher). +- Prova: `./scripts/qa/acs_alert_fallback_e2e.sh` no `emulator-5554` contra o banco de teste, 2026-10-17: broker fora do alcance do aparelho, alerta inserido por SQL sem linha no `alert_outbox`, intervalo de produção de 30 s; o cartão apareceu sem toque em 29 633 ms, foi confirmado pela tela e seguiu confirmado e único depois de mais um tique; no banco, `acknowledged` com `acknowledgedAt`, 1 linha `acknowledged`, 0 leituras de alerta auditadas, 0 linhas no outbox. Log em `docs/superpowers/plans/2026-10-17-issue-80-e2e.log`. Testes: `test/unit/red_alert_service_test.dart` (grupo `openForAcs (#80)`), `test/integration/acs_open_alerts_test.dart` (8 casos), `apps/acs/test/alert_queue_merge_test.dart` (9), `alert_http_fallback_test.dart` (2), `alert_http_fallback_shell_test.dart` (9). +- Aberto: + - Sem consulta com o app em segundo plano (WorkManager fora de `spec/stack.md`): broker fora + app no bolso = o alerta aparece quando o ACS volta ao app. + - O app segue sem som, vibração ou notificação do SO para alerta novo, venha por MQTT ou pela reserva. + - O teto de 200 não pagina: numa microárea com mais de 200 abertos, os mais ANTIGOS de cada grupo ficam de fora (não confirmados primeiro; o corte nunca tira o pendente mais novo). + - Um alerta concluído em outro aparelho do mesmo ACS some da resposta (o servidor não devolve `resolved`) mas o cartão local não muda, porque a fusão nunca remove: segue contando até sair do app. +- Revisão final (2026-10-17), corrigido com teste que falhou antes: (1) com `autoReconnect`, a queda do broker DEPOIS da primeira conexão não chamava `onDisconnected` no `mqtt_client` 10.5.1 — o painel seguia "em linha" e a reserva nunca ligava; `MqttSecureClient` agora trata `onAutoReconnect` como queda e usa `disconnectOnNoResponsePeriod = 10` para a conexão meio aberta do 3G (`test/mqtt_drop_after_connect_test.dart`, broker falso em TCP real); (2) o teto de 200 cortava o pendente mais NOVO — agora cada grupo é lido do mais novo para trás (`acs_open_alerts_test.dart`); (3) ao armar, a reserva consulta na hora e zera o status anterior, em vez de esperar 30 s mostrando a hora de quando o MQTT funcionava (`alert_http_fallback_shell_test.dart`). diff --git a/apps/CLAUDE.md b/apps/CLAUDE.md index dc5739c..0440cd7 100644 --- a/apps/CLAUDE.md +++ b/apps/CLAUDE.md @@ -38,6 +38,8 @@ The ACS login is institutional now (RF07): `_enter` in `apps/acs/lib/app/app.dar **ACS: conflito de visita (#79, 2026-10-16).** O conflito sai da retentativa (D3): `VisitSyncResult.serverVisit` traz a cópia do servidor, `OfflineVisitQueue` guarda a visita em `_conflicts` com `conflictServerVersion`/`conflictServer` (schema v8, só `ADD COLUMN`), `resolveKeepLocal` (também usado por "Editar e enviar", com `outcome`/`notes`) chama `visits.resolveConflict` e `resolveKeepServer` só apaga a cópia local. Tela `resolve_conflicts` (chaves `conflict_*`). Detalhe e provas: `PROGRESS.md`, seção "Resolução de conflito de visita (#79)". +**ACS: reserva por HTTP dos alertas (#80, 2026-10-17).** `AlertHttpFallback` (`core/services/alert_http_fallback.dart`, sem `Timer`) chama `AcsBackend.openAlerts` (`alerts.openForMicroArea`) e funde em `AlertQueue.mergeFromServer`, que nunca remove cartão, ignora outra microárea, nunca desfaz ACK, conclusão ou reforço (só os ganha: um ACK feito em outro aparelho promove o cartão) e só notifica quando algo mudou. Queda do broker depois de conectar: com `autoReconnect` o `mqtt_client` 10.5.1 não chama `onDisconnected`, só `onAutoReconnect`, por isso `MqttSecureClient` trata os dois como queda (`test/mqtt_drop_after_connect_test.dart`) e usa `disconnectOnNoResponsePeriod = 10` contra a conexão meio aberta. O `AcsHomeShell` consulta sempre ao abrir o painel, ao voltar ao primeiro plano e no instante em que a reserva arma, e a cada `AcsHomeShell.defaultAlertFallbackInterval` (30 s; `alertFallbackInterval` nos testes) **só** enquanto o broker está desconectado e o app em primeiro plano; uma consulta por vez, e `close()` descarta a resposta que chega depois do `dispose`. Com a reserva ligada o painel mostra `Key('alert_fallback_status')` (`liveRegion`): "Recebendo alertas pelo servidor a cada 30 s. Última consulta às HH:MM." ou "…também falhou…"; o chip do cabeçalho continua "Sem central". Sessão vencida na consulta segue o `onSessionExpired` de sempre. Prova: `scripts/qa/acs_alert_fallback_e2e.sh` (broker fora do alcance do aparelho, alerta inserido por SQL sem `alert_outbox`, intervalo de produção; estrutura guardada por `acs_alert_fallback_e2e_test.sh`). Sem consulta em segundo plano e sem alertas gravados no aparelho. + **ACS: fila de visitas por dono, envio diferido e "Limpar este aparelho" (2026-10-04).** Plano `docs/superpowers/plans/2026-10-03-fila-de-visitas-por-dono-e-corrida-do-login.md` (D1–D9). `offline_visits` tem a coluna `owner` (schema v7, migração aditiva): `VisitStorage.forOwner(userId)` só lê, conta, grava e apaga `WHERE owner = ?` (o `save()` de B nunca toca nas linhas de A; dono vazio é `ArgumentError`; `localId` já existente em outro dono é `VisitLocalIdConflict`, unicidade global). Linhas do schema v6 ficam em **quarentena** (`owner IS NULL`): nenhum dono as vê; só `VisitStorage.legacy` (`LegacyVisitStore`) as lê e remove por `localId`. O cursor do pull é por `userId|microAreaId`; fila e pull são resolvidos por sessão (`visit_queue_factory.dart`, `visit_pull_service_factory.dart`) e recriados por dono, e `BackendVisitSynchronizer` recusa sessão ou token de outro dono (checagem no push e `expectedUserId` conferido depois de resolvido o token). Os testes que injetam `visitQueue`/`visitPullService` seguem como *seam*. `BackendClient._exclusive` (seção crítica serial, **não reentrante**: dentro dela só E/S local do Keystore e atribuições, nunca rede, `renewSession()` ou outra `_exclusive`) cobre login, `developmentLogin`, a leitura do token e a confirmação da renovação e o logout, e fecha a corrida em que uma renovação do usuário anterior sobrescrevia o login novo. `DeferredFlushService` (`core/services/deferred_flush_service.dart`) sobe o que não é do dono da sessão atual: o legado pela sessão atual como transporte (`syncLegacy`, "autoria desconhecida"), cada dono com token por `syncDeferred`; lotes de 100 (`deferredBatchSize`); a visita só sai do aparelho em `synced`; o token de envio é revogado no servidor e apagado do Keystore só com a lista do dono vazia; `flushAll()` é single-flight e não lança; o dono da sessão atual é pulado (a fila é do painel, por isso o "Sair" sincroniza a fila do painel **antes** de `flushAll`). `UploadTokenStore` (`core/security/secure_upload_token_store.dart`) guarda um token por dono no Keystore (`acs_upload_token|`) e um índice de donos; o envio também tenta o dono de toda linha no disco, então o índice é só otimização. Gatilhos em `app.dart`: painel aberto, retomada do app e conexão que volta (`connectivityChanges`, `connectivity_plus`). Rejeição "versão diferente" (`legacyVersionMismatchMessage`) não é erro de território: a visita fica em quarentena e pede revisão. "Limpar este aparelho" congela a área, espera o refresh em voo e chama `VisitDatabase.wipeAllData`, que numa transação recusa (`WipeBlocked`) se houver **qualquer** linha em `offline_visits` — recusada, legado ou de outro dono — ou fila só em RAM; sem linhas, apaga `offline_visits`, `sync_cursor` e o cache da microárea (arquivo e chave do Keystore ficam). Não há wipe forçado por supervisor (não existe login de supervisor) nem envio em segundo plano do SO (WorkManager fora de `spec/stack.md`); um token de envio vencido (7 dias) ou recusado deixa a fila do dono presa e bloqueia o wipe. Provas e limites no emulador: `PROGRESS.md`, seção "Fila de visitas por dono e corrida do login" (o offline do `acs_full_e2e.sh` é **simulado** por um decorator de `AcsBackend`, e o gatilho de conectividade vem de um `StreamController`). The patient login is passwordless now (RF01), and no longer uses `developmentLogin` either: the app sends the CPF and the birth date to `auth.requestOtp`, then the 6-digit code to `auth.verifyOtp`, and only that second call issues the session. **There is no silent renewal on this side, on purpose** — an OTP code cannot be replayed the way the ACS's password can, so a 15-minute token would mean a new SMS every 15 minutes. The session therefore lives **1 hour** (`AuthEndpoint.patientSessionLifetime`, applying LGPD-RT06), the single deliberate asymmetry with the ACS: when it expires the app does not renew, it shows "Sua sessão expirou. Entre novamente com o código de acesso." with a button back to the login (`reenter_button`), because that is the only way back in. The asymmetry is written down in `spec/lgpd_design.md` so the next reader does not read it as an oversight. Both patient session paths — the OTP login and `onboarding.completeEnrollment` — issue the same 1-hour token (`AuthEndpoint.patientSessionLifetime`). After the OTP login the app asks `patients.hasAcceptedCurrentTerms` (a `bool` — not the whole "Meus dados" panel, which would write a read-audit row on every login) with a 3 s timeout and, when the answer is `false`, shows `TermsAcceptanceScreen` (`legal_screens.dart`); it is a prompt, not a gate — "Agora não" and a failed or slow `hasAcceptedCurrentTerms` both go straight to the home, so the emergency alert never waits for an acceptance. `patients.acceptTermsOfUse` is the only writer of `termsOfUse` outside onboarding; `updateConsent` still refuses it. After login and onboarding, and when "Avisos da equipe" is granted, the app silently calls `devices.registerPushToken` with the token from `PushTokenSource` (`core/push/push_token_source.dart`); the token comes from `pushTokenSourceProvider` (the only `flutter_riverpod` use in the patient app; `SinalAcsApp(pushTokens:)` overrides it in tests, and without a `ProviderScope` the app falls back to `NoPushTokenSource`), whose real implementation `NativePushTokenSource` asks the `sinalacs/push_token` method channel — the Android side exists (`MainActivity.kt` with `firebase-messaging`; the Google Services plugin is applied **only** when the git-ignored `android/app/google-services.json` exists, so CI and other devs still build and the app degrades to no push), while **the iOS/Swift side does not exist**. The manifest turns FCM auto-init **off** (`firebase_messaging_auto_init_enabled=false`, guarded by `test/android_push_manifest_test.dart`): otherwise the device would contact Google on every app open, before login and without consent; the explicit `getToken()` still works. `registerPushDevice` asks the provider for the token only after `patients.hasGrantedConsent(segmentedPush)` answers `true` (a `bool`: no dossier, no read-audit row; 3 s ceiling, fail closed, late answer discarded; skipped when the source is `NoPushTokenSource`). Onboarding (only when the box is ticked) and the "Meus dados" switch pass `consentKnownGranted: true` because they just wrote the decision. `scripts/qa/push_e2e.sh` proves the real delivery on `emulator-5554`; it needs the FCM service-account key and is not run in CI, and the two push integration tests only run with `--dart-define=PUSH_E2E=1`, so `e2e.sh --full` stays green without credentials. A refusal or failure never blocks the home or the emergency alert. The home also fetches `patients.termsChangeNotice` once (3 s ceiling, failure or a shell without `BackendScope` = no card) and shows a dismissible `TermsChangeNoticeCard` — never a gate, and never on the urgency tab. When the app embeds the text of exactly the version the server announced (`upcomingLegalDocuments`, injected via `UpcomingDocumentsScope`), the card also offers "Ler o texto novo" (`LegalDocumentsScreen(upcoming:)`); otherwise only "Ler os termos atuais". `PushTokenScope.maybeOf` falls back to no push instead of throwing. In the ACS app, "Avisos à comunidade" (`NoticesScreen`) calls `AcsBackend.sendNotice` → `notices.sendSegmented`. diff --git a/apps/acs/integration_test/alert_http_fallback_e2e.dart b/apps/acs/integration_test/alert_http_fallback_e2e.dart new file mode 100644 index 0000000..17b587a --- /dev/null +++ b/apps/acs/integration_test/alert_http_fallback_e2e.dart @@ -0,0 +1,114 @@ +/// Reserva por HTTP dos alertas (#80, R2) no emulador, contra o BANCO DE TESTE. +/// +/// O broker fica inalcançável a partir do aparelho (o script remove o +/// `adb reverse` da 8883), enquanto o backend segue no ar. O alerta é inserido +/// por SQL pelo script, sem linha no `alert_outbox`: só pode chegar à tela pela +/// consulta de 30 s, o intervalo de produção (não encurtado de propósito). +/// +/// Só roda por `scripts/qa/acs_alert_fallback_e2e.sh` (sufixo `_e2e`: a +/// descoberta do `flutter test integration_test` não o encontra). Conversa com o +/// script por linhas marcadoras na saída: +/// E2E_DISPARAR_ALERTA id= → o script insere o alerta +/// E2E_ALERTA_VIA_HTTP atraso_ms=N → o cartão apareceu sem toque +/// E2E_ACK_OK / E2E_RESERVA_OK → confirmado, e a reserva não o desfez +/// +/// PRIVACIDADE: só fixtures sintéticas geradas por execução. +@Timeout(Duration(minutes: 5)) +library; + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:sinalacs_acs/app/app.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/network/backend_config.dart'; +import 'package:sinalacs_acs/core/security/biometric_gate.dart'; + +import 'support/e2e_acs.dart'; + +class _SemBiometria implements BiometricGate { + @override + Future get isAvailable async => false; + @override + Future authenticate({required String reason}) async => UnlockResult.unavailable; +} + +/// UUID v4 sintético, gerado no aparelho: o script o usa no INSERT. +String _uuidV4() { + final r = Random.secure(); + final b = List.generate(16, (_) => r.nextInt(256)); + b[6] = (b[6] & 0x0f) | 0x40; + b[8] = (b[8] & 0x3f) | 0x80; + final h = b.map((x) => x.toRadixString(16).padLeft(2, '0')).join(); + return '${h.substring(0, 8)}-${h.substring(8, 12)}-${h.substring(12, 16)}-${h.substring(16, 20)}-${h.substring(20)}'; +} + +Future _ate(WidgetTester tester, bool Function() feito, {required Duration teto, required String oQue}) async { + final fim = DateTime.now().add(teto); + while (!feito()) { + if (DateTime.now().isAfter(fim)) fail('$oQue: não aconteceu em ${teto.inSeconds} s'); + await tester.pump(const Duration(milliseconds: 250)); + } +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('com o MQTT fora, o alerta chega pela reserva HTTP e o ACK não é desfeito', (tester) async { + final ca = (await rootBundle.load(BackendConfig.rpcCaAsset)).buffer.asUint8List(); + final backend = BackendClient(trustedCaBytes: ca); + addTearDown(backend.close); + final cred = await acsCredentialFromRelay(); + + await tester.pumpWidget(SinalAcsApp(backend: backend, biometricGate: _SemBiometria())); + await tester.enterText(find.byKey(const Key('matricula_field')), cred.matricula); + await tester.enterText(find.byKey(const Key('senha_field')), cred.senha); + final entrar = find.byKey(const Key('login_button')); + await tester.ensureVisible(entrar); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(entrar); + await _ate(tester, () => entrar.evaluate().isEmpty, teto: const Duration(seconds: 30), oQue: 'login'); + + // O cenário só prova algo se o tempo real estiver mesmo fora. + await _ate( + tester, + () => find.byKey(const Key('alert_fallback_status')).evaluate().isNotEmpty, + teto: const Duration(seconds: 60), + oQue: 'reserva ligada (o MQTT conectou? o cenário não provaria nada)', + ); + expect(find.text('Sem central'), findsOneWidget, reason: 'o chip diz que o tempo real caiu'); + + final id = _uuidV4(); + final disparo = DateTime.now(); + // ignore: avoid_print + print('E2E_DISPARAR_ALERTA id=$id'); + + final cartao = find.byKey(Key('alert_$id'), skipOffstage: false); + await _ate(tester, () => cartao.evaluate().isNotEmpty, teto: const Duration(seconds: 45), oQue: 'alerta pela reserva'); + // ignore: avoid_print + print('E2E_ALERTA_VIA_HTTP atraso_ms=${DateTime.now().difference(disparo).inMilliseconds}'); + + final confirmar = find.descendant(of: cartao, matching: find.text('Confirmar recebimento')); + await Scrollable.ensureVisible(tester.element(confirmar), alignment: 0.5); + await tester.pump(const Duration(milliseconds: 300)); + await tester.tap(confirmar); + final confirmado = find.descendant(of: cartao, matching: find.text('Recebimento confirmado')); + await _ate(tester, () => confirmado.evaluate().isNotEmpty, teto: const Duration(seconds: 20), oQue: 'ACK pela tela'); + // ignore: avoid_print + print('E2E_ACK_OK'); + + // Um tique inteiro da reserva depois do ACK: segue confirmado e único. + final fim = DateTime.now().add(const Duration(seconds: 35)); + while (DateTime.now().isBefore(fim)) { + await tester.pump(const Duration(milliseconds: 500)); + } + expect(cartao, findsOneWidget); + expect(confirmado, findsOneWidget); + expect(find.descendant(of: cartao, matching: find.text('Confirmar recebimento')), findsNothing); + // ignore: avoid_print + print('E2E_RESERVA_OK'); + }); +} diff --git a/apps/acs/integration_test/full_journey_e2e.dart b/apps/acs/integration_test/full_journey_e2e.dart index 9f83077..3692d14 100644 --- a/apps/acs/integration_test/full_journey_e2e.dart +++ b/apps/acs/integration_test/full_journey_e2e.dart @@ -170,6 +170,8 @@ class _EnvioDeVisitasForaDoAr implements AcsBackend { @override Future acknowledge({required String alertId}) => _inner.acknowledge(alertId: alertId); @override + Future> openAlerts() => _inner.openAlerts(); + @override Future resolveAlert({required String alertId}) => _inner.resolveAlert(alertId: alertId); @override diff --git a/apps/acs/lib/app/app.dart b/apps/acs/lib/app/app.dart index da2ef0f..5d0ee87 100644 --- a/apps/acs/lib/app/app.dart +++ b/apps/acs/lib/app/app.dart @@ -21,6 +21,7 @@ import 'package:sinalacs_acs/core/security/database_key_store.dart'; import 'package:sinalacs_acs/core/security/session_token_store.dart'; import 'package:sinalacs_acs/core/security/upload_token_store.dart'; import 'package:sinalacs_acs/core/services/alert_feed.dart'; +import 'package:sinalacs_acs/core/services/alert_http_fallback.dart'; import 'package:sinalacs_acs/core/services/alert_queue.dart'; import 'package:sinalacs_acs/core/services/deferred_flush_service.dart'; import 'package:sinalacs_acs/core/services/emergency_dialer.dart'; @@ -47,6 +48,7 @@ class SinalAcsApp extends StatefulWidget { this.currentPosition, this.themeController, this.syncInterval, + this.alertFallbackInterval, this.biometricGate, this.navigatorKey, this.lockAfter, @@ -127,6 +129,10 @@ class SinalAcsApp extends StatefulWidget { /// — testes passam um valor curto para não esperar 5 minutos reais. final Duration? syncInterval; + /// Intervalo da consulta de alertas por HTTP enquanto o MQTT está fora + /// (#80). `null` usa `AcsHomeShell.defaultAlertFallbackInterval`. + final Duration? alertFallbackInterval; + @override State createState() => _SinalAcsAppState(); } @@ -599,6 +605,7 @@ class _SinalAcsAppState extends State with WidgetsBindingObserver i initialPosition: widget.currentPosition, themeController: _themeController, syncInterval: widget.syncInterval, + alertFallbackInterval: widget.alertFallbackInterval, biometricGate: _gate, ), ), @@ -643,6 +650,7 @@ class LoginScreen extends StatefulWidget { this.initialAlert, this.initialPosition, this.syncInterval, + this.alertFallbackInterval, this.aviso, this.reauthUserId, this.biometricGate, @@ -672,6 +680,7 @@ class LoginScreen extends StatefulWidget { final LatLng? initialPosition; final ThemeController themeController; final Duration? syncInterval; + final Duration? alertFallbackInterval; @override State createState() => _LoginScreenState(); @@ -916,6 +925,7 @@ class _LoginScreenState extends State { initialPosition: widget.initialPosition, themeController: widget.themeController, syncInterval: widget.syncInterval, + alertFallbackInterval: widget.alertFallbackInterval, ), ); if (widget.reauthUserId != null) { @@ -1052,6 +1062,7 @@ class AcsHomeShell extends StatefulWidget { this.initialAlert, this.initialPosition, this.syncInterval, + this.alertFallbackInterval, }); final String microAreaId; @@ -1071,6 +1082,15 @@ class AcsHomeShell extends StatefulWidget { /// agressivo que gasta bateria/dados em campo. static const defaultSyncInterval = Duration(minutes: 5); + /// Consulta de alertas por HTTP (#80, R2) enquanto o broker está fora. + /// `null` usa [defaultAlertFallbackInterval]. + final Duration? alertFallbackInterval; + + /// 30 s: com a consulta no meio do intervalo, um alerta chega em ~15 s e, + /// no pior caso, em 30 s — dentro dos 90 s do TMRAV, sem virar polling que + /// gasta bateria e dados em 3G. + static const defaultAlertFallbackInterval = Duration(seconds: 30); + /// Obrigatória: a tela de visita usava `widget.visitQueue ?? OfflineVisitQueue()`, /// e um dia em que o shell fosse construído sem fila voltaria a descartar a /// visita em silêncio. @@ -1115,6 +1135,27 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver /// plano, ou ainda não iniciado). Timer? _periodicSyncTimer; + /// Reserva por HTTP dos alertas (#80, R2). Criada com o backend, em + /// [didChangeDependencies]. + AlertHttpFallback? _alertFallback; + + /// Armado só enquanto o broker está fora e o app em primeiro plano; `null` + /// é "reserva desligada", e é isso que a linha de status do painel segue. + Timer? _alertFallbackTimer; + + /// Uma consulta por vez: um tique que chega com outra em voo é descartado. + bool _pollingAlerts = false; + + /// Hora da última resposta da reserva (sucesso ou falha); `null` antes da + /// primeira. + DateTime? _alertFallbackAt; + bool _alertFallbackFailed = false; + + bool _inBackground = false; + + Duration get _alertFallbackInterval => + widget.alertFallbackInterval ?? AcsHomeShell.defaultAlertFallbackInterval; + Duration get _syncInterval => widget.syncInterval ?? AcsHomeShell.defaultSyncInterval; /// `true` enquanto uma chamada a `visits.pull` está em andamento. @@ -1199,6 +1240,11 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver _backend!.onSessionExpired = _sessionExpired; _lockHooks = _AppLockScope.maybeOf(context)?..openPasswordReauth = _reauthForLock; _loadMicroAreaPatients(); + // Sempre uma vez ao abrir, com ou sem MQTT: a fila em memória começa + // vazia, e o "Sair" anterior pode ter levado embora alertas que seguem + // pendentes no servidor (#80). + _alertFallback = AlertHttpFallback(backend: _backend!, queue: _queue); + _pollAlerts(); } } @@ -1255,6 +1301,7 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver initialAlert: widget.initialAlert, initialPosition: widget.initialPosition, syncInterval: widget.syncInterval, + alertFallbackInterval: widget.alertFallbackInterval, aviso: aviso, reauthUserId: widget.acsId, ), @@ -1537,6 +1584,7 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver _feedError = null; _feedErrorIsTransient = false; }); + _syncAlertFallbackTimer(); } on AlertFeedFailure catch (failure, stackTrace) { developer.log( 'não foi possível assinar o tópico de alertas', @@ -1555,6 +1603,7 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver : failure.detail, ); }); + _syncAlertFallbackTimer(); if (failure.transient) _scheduleReconnect(); } catch (error, stackTrace) { // Defesa: hoje só um AlertFeed de teste chega aqui. Logar em vez de @@ -1577,6 +1626,7 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver 'Tentando reconectar automaticamente.', ); }); + _syncAlertFallbackTimer(); _scheduleReconnect(); } } @@ -1608,6 +1658,53 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver void _onBrokerConnectionChanged(bool connected) { if (!mounted) return; setState(() => _brokerConnected = connected); + _syncAlertFallbackTimer(); + } + + /// Liga a consulta periódica de alertas por HTTP enquanto o broker está + /// fora e o app em primeiro plano; desliga em qualquer outro caso (#80). + /// Intervalo fixo, sem backoff: 30 s já é o piso de custo, e uma falha só + /// aparece na linha de status. + void _syncAlertFallbackTimer() { + final wanted = mounted && !_brokerConnected && !_inBackground; + if (wanted == (_alertFallbackTimer != null)) return; + if (wanted) { + _alertFallbackTimer = Timer.periodic(_alertFallbackInterval, (_) => _pollAlerts()); + // Consulta já, sem esperar um intervalo inteiro, e esquece a resposta + // anterior: ela é de quando o tempo real funcionava (ou de outra queda), + // e a linha de status diria uma hora velha como se fosse desta reserva. + _alertFallbackAt = null; + _alertFallbackFailed = false; + _pollAlerts(); + } else { + _alertFallbackTimer?.cancel(); + _alertFallbackTimer = null; + } + if (mounted) setState(() {}); + } + + /// Uma consulta à reserva. A sessão vencida segue o caminho de sempre + /// (`onSessionExpired` do backend abre a reautenticação); aqui só se marca + /// a falha para a linha de status. + Future _pollAlerts() async { + final fallback = _alertFallback; + if (fallback == null || _pollingAlerts) return; + _pollingAlerts = true; + var failed = false; + try { + await fallback.poll(); + } on BackendFailure catch (failure) { + // Só o texto já traduzido pelo `_guard`: nada do paciente vai ao log. + failed = true; + developer.log(failure.message, name: 'sinalacs.acs.alert_fallback'); + } finally { + _pollingAlerts = false; + } + if (!mounted) return; + setState(() { + _alertFallbackAt = DateTime.now(); + _alertFallbackFailed = failed; + }); } @override @@ -1619,7 +1716,12 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver _reconnectTimer = null; _periodicSyncTimer?.cancel(); _periodicSyncTimer = null; + _inBackground = true; + _syncAlertFallbackTimer(); } else if (state == AppLifecycleState.resumed) { + _inBackground = false; + _pollAlerts(); + _syncAlertFallbackTimer(); if (_feedErrorIsTransient) { // O gatilho que mais importa na prática: o sinal costuma voltar com // a tela apagada, e o ACS tira o aparelho do bolso já esperando o @@ -1854,6 +1956,7 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver initialAlert: widget.initialAlert, initialPosition: widget.initialPosition, syncInterval: widget.syncInterval, + alertFallbackInterval: widget.alertFallbackInterval, aviso: aviso, )), (_) => false, @@ -2023,6 +2126,8 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver if (_lockHooks?.openPasswordReauth == _reauthForLock) _lockHooks!.openPasswordReauth = null; _reconnectTimer?.cancel(); _periodicSyncTimer?.cancel(); + _alertFallbackTimer?.cancel(); + _alertFallback?.close(); WidgetsBinding.instance.removeObserver(this); // Solta o callback ANTES de parar: `stop()` desconecta o MQTT, que avisa // `onConnectionChanged(false)`, e `mounted` ainda é `true` durante o @@ -2053,6 +2158,9 @@ class _AcsHomeShellState extends State with WidgetsBindingObserver queue: _queue, feedError: _feedError, onRetryFeed: _feedErrorIsTransient ? _retryFeedNow : null, + alertFallback: _alertFallbackTimer == null + ? null + : (at: _alertFallbackAt, failed: _alertFallbackFailed, every: _alertFallbackInterval), storageError: _storageNotice, onAcknowledge: _acknowledge, onResolve: _resolve, @@ -2259,6 +2367,7 @@ class DashboardScreen extends StatelessWidget { super.key, this.feedError, this.onRetryFeed, + this.alertFallback, this.storageError, this.onAcknowledge, this.onResolve, @@ -2269,6 +2378,11 @@ class DashboardScreen extends StatelessWidget { final AlertQueue queue; final InfraNotice? feedError; + /// Reserva por HTTP ativa (#80): o broker está fora e o painel consulta o + /// servidor a cada [every]. `at` é a última resposta (nula antes da + /// primeira) e `failed` diz se ela falhou. + final ({DateTime? at, bool failed, Duration every})? alertFallback; + /// Presente só quando [feedError] é uma falha transitória — dá ao ACS que já /// vê o sinal voltar a chance de não esperar o backoff automático. `null` /// numa falha permanente (senha ausente, CA ausente) evita oferecer um botão @@ -2302,6 +2416,10 @@ class DashboardScreen extends StatelessWidget { notice: feedError!, onRetry: onRetryFeed, ), + if (alertFallback != null) _AlertFallbackStatus( + key: const Key('alert_fallback_status'), + status: alertFallback!, + ), if (storageError != null) _InfraBanner( key: const Key('storage_error'), icon: Icons.sd_card_alert_outlined, @@ -2341,6 +2459,41 @@ class DashboardScreen extends StatelessWidget { /// /// A `Key` vem de fora e é o único identificador do aviso — pô-la também no /// `ListTile` faria `find.byKey` achar dois widgets. +/// Linha da reserva por HTTP (#80): diz ao ACS que os alertas seguem +/// chegando, pelo servidor e com atraso, enquanto o tempo real está fora. +class _AlertFallbackStatus extends StatelessWidget { + const _AlertFallbackStatus({required this.status, super.key}); + + final ({DateTime? at, bool failed, Duration every}) status; + + static String _hhmm(DateTime t) => + '${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}'; + + @override + Widget build(BuildContext context) { + final at = status.at; + final segundos = status.every.inSeconds; + final texto = at == null + ? 'Consultando alertas pelo servidor…' + : status.failed + ? 'A consulta de alertas pelo servidor também falhou às ${_hhmm(at)}. ' + 'Nova tentativa em $segundos s.' + : 'Recebendo alertas pelo servidor a cada $segundos s. ' + 'Última consulta às ${_hhmm(at)}.'; + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Semantics( + liveRegion: true, + child: Row(children: [ + Icon(status.failed ? Icons.sync_problem_outlined : Icons.sync_outlined, size: 18), + const SizedBox(width: 8), + Expanded(child: Text(texto)), + ]), + ), + ); + } +} + class _InfraBanner extends StatelessWidget { const _InfraBanner({required this.icon, required this.notice, super.key, this.onRetry}); diff --git a/apps/acs/lib/core/network/backend_client.dart b/apps/acs/lib/core/network/backend_client.dart index 17dcc26..cf02b6e 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}); + /// `alerts.openForMicroArea` (#80, R2): alertas não concluídos da microárea + /// da sessão, a reserva por HTTP para quando o MQTT não está conectado. + Future> openAlerts(); + /// Conclui o atendimento de um alerta já confirmado (#76): o paciente passa /// a ver "Atendido". Future resolveAlert({required String alertId}); @@ -264,6 +268,9 @@ class MisconfiguredBackend implements AcsBackend { @override Future acknowledge({required String alertId}) async => _recusar(); + + @override + Future> openAlerts() async => _recusar(); @override Future resolveAlert({required String alertId}) async => _recusar(); @@ -747,6 +754,15 @@ class BackendClient implements AcsBackend { ); } + /// Alertas abertos da microárea pelo RPC (#80): a microárea vem do token. + @override + Future> openAlerts() async { + final token = await _requireToken(); + return _guard( + () => _client.alerts.openForMicroArea(accessToken: token), + ); + } + /// 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. diff --git a/apps/acs/lib/core/services/alert_http_fallback.dart b/apps/acs/lib/core/services/alert_http_fallback.dart new file mode 100644 index 0000000..ee1d1c5 --- /dev/null +++ b/apps/acs/lib/core/services/alert_http_fallback.dart @@ -0,0 +1,30 @@ +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; + +/// Reserva por HTTP dos alertas (#80, R2): lê os alertas abertos da microárea +/// pelo RPC e os funde na mesma [AlertQueue] que o MQTT alimenta. +/// +/// Só a consulta mora aqui; nenhum `Timer`. Quem agenda é o dono do ciclo de +/// vida (`AcsHomeShell`), que sabe quando o broker caiu e quando o app foi +/// para o segundo plano — mesma divisão de `ReconnectSchedule`. +class AlertHttpFallback { + AlertHttpFallback({required AcsBackend backend, required AlertQueue queue}) + : _backend = backend, + _queue = queue; + + final AcsBackend _backend; + final AlertQueue _queue; + bool _closed = false; + + /// Devolve quantos cartões foram criados ou mudaram. Uma falha do backend + /// sobe como veio ([BackendFailure]) e não toca na fila. + Future poll() async { + final abertos = await _backend.openAlerts(); + if (_closed) return 0; + return _queue.mergeFromServer(abertos.map(PrioritizedAlert.fromServer)); + } + + /// Descarta a resposta de uma consulta ainda em voo: o dono da [AlertQueue] + /// a descarta no `dispose`, e fundir depois disso notificaria uma fila morta. + void close() => _closed = true; +} diff --git a/apps/acs/lib/core/services/alert_queue.dart b/apps/acs/lib/core/services/alert_queue.dart index c60fe27..0daea49 100644 --- a/apps/acs/lib/core/services/alert_queue.dart +++ b/apps/acs/lib/core/services/alert_queue.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:sinalacs_acs/core/services/mqtt_secure_client.dart'; +import 'package:sinalacs_client/sinalacs_client.dart' show AcsOpenAlert, AlertStatus; /// Um alerta na fila de priorização do ACS. @immutable @@ -30,6 +31,21 @@ class PrioritizedAlert { ); } + /// Alerta aberto lido por HTTP, a reserva para quando o MQTT cai (#80). + factory PrioritizedAlert.fromServer(AcsOpenAlert alert) { + return PrioritizedAlert( + alertId: alert.alertId, + patientId: alert.patientId, + microAreaId: alert.microAreaId, + riskLevel: alert.riskLevel.name, + locationHash: alert.locationHash, + locationCell: alert.locationCell, + triggeredAt: alert.triggeredAt, + acknowledged: alert.status == AlertStatus.acknowledged, + escalated: alert.escalated, + ); + } + final String alertId; final String patientId; final String microAreaId; @@ -141,6 +157,46 @@ class AlertQueue extends ChangeNotifier { return true; } + /// Funde a lista de alertas abertos lida por HTTP (#80, R2). + /// + /// Devolve quantos cartões foram criados ou mudaram e notifica uma vez só, + /// e só se algo mudou: a mesma resposta a cada 30 s não pode virar "alerta + /// novo". As regras são as de [upsert], vistas do lado do servidor: + /// + /// - outra microárea é ignorada; + /// - nada é removido — um cartão ausente da resposta fica (INV-03); + /// - confirmação e reforço só são ganhos, nunca perdidos: o servidor pode + /// promover um cartão local a confirmado (ACK de outro aparelho ou pelo + /// MQTT), mas um "pendente" do servidor nunca desfaz o ACK local; + /// - um cartão já concluído aqui não muda. + int mergeFromServer(Iterable alerts) { + var changed = 0; + for (final alert in alerts) { + if (alert.microAreaId != microAreaId) continue; + + final index = _alerts.indexWhere((item) => item.alertId == alert.alertId); + if (index < 0) { + _alerts.add(alert); + changed++; + continue; + } + + final existing = _alerts[index]; + if (existing.resolved) continue; + final acknowledged = existing.acknowledged || alert.acknowledged; + final escalated = existing.escalated || alert.escalated; + if (acknowledged == existing.acknowledged && escalated == existing.escalated) continue; + _alerts[index] = existing.copyWith(acknowledged: acknowledged, escalated: escalated); + changed++; + } + + if (changed > 0) { + _sort(); + notifyListeners(); + } + return changed; + } + /// Marca o alerta como confirmado. Devolve `false` se ele não está na fila. bool markAcknowledged(String alertId) { final index = _alerts.indexWhere((item) => item.alertId == alertId); diff --git a/apps/acs/lib/core/services/mqtt_secure_client.dart b/apps/acs/lib/core/services/mqtt_secure_client.dart index acd787e..1f366cf 100644 --- a/apps/acs/lib/core/services/mqtt_secure_client.dart +++ b/apps/acs/lib/core/services/mqtt_secure_client.dart @@ -301,6 +301,14 @@ class MqttSecureClient { ..autoReconnect = true ..resubscribeOnAutoReconnect = true ..onDisconnected = (() => onConnectionChanged?.call(false)) + // Com `autoReconnect`, a queda DEPOIS da primeira conexão não chama + // `onDisconnected` (o mqtt_client 10.5.1 entra direto no laço de + // reconexão e só chama `onAutoReconnect`). Sem isto o painel seguia + // "em linha" e nunca ligava a reserva por HTTP dos alertas (#80). + ..onAutoReconnect = (() => onConnectionChanged?.call(false)) + // Conexão meio aberta (comum em 3G): sem resposta ao PINGREQ por este + // tempo, o cliente a dá por caída. O padrão 0 nunca desconecta. + ..disconnectOnNoResponsePeriod = 10 ..onAutoReconnected = (() => onConnectionChanged?.call(true)) ..onConnected = (() => onConnectionChanged?.call(true)) ..connectionMessage = MqttConnectMessage() diff --git a/apps/acs/test/alert_http_fallback_shell_test.dart b/apps/acs/test/alert_http_fallback_shell_test.dart new file mode 100644 index 0000000..6d0c53d --- /dev/null +++ b/apps/acs/test/alert_http_fallback_shell_test.dart @@ -0,0 +1,197 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_acs/app/app.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/alert_feed.dart'; +import 'package:sinalacs_client/sinalacs_client.dart'; + +import 'support/fakes.dart'; +import 'support/layout_harness.dart' show assentar; + +/// Reserva por HTTP no painel (#80, R2): consulta ao abrir, a cada intervalo +/// só enquanto o broker está fora, e nunca duas ao mesmo tempo. +const _intervalo = Duration(seconds: 1); + +const _brokerFora = AlertFeedFailure( + AlertFeedFailureKind.unreachable, + title: 'Sem conexão com a central de alertas.', + detail: 'Novos alertas podem não estar chegando.', + transient: true, +); + +AcsOpenAlert _aberto(String id) => AcsOpenAlert( + alertId: id, + patientId: seedPatientId, + microAreaId: seedMicroAreaId, + riskLevel: RiskLevel.red, + locationHash: 'hash-sintetico', + triggeredAt: DateTime.utc(2026, 10, 17, 12), + status: AlertStatus.pending, + escalated: false, + ); + +void main() { + late FakeAcsBackend backend; + late FakeAlertFeed feed; + + Future abrir(WidgetTester tester, {AlertFeedFailure? falhaDoFeed}) async { + await tester.pumpWidget(SinalAcsApp( + backend: backend, + alertFallbackInterval: _intervalo, + feedBuilder: (q) => feed = FakeAlertFeed(q, failure: falhaDoFeed), + )); + 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 assentar(tester); + } + + /// Avança o relógio falso em passos do intervalo, deixando cada consulta + /// terminar antes do próximo tique. + Future avancar(WidgetTester tester, int tiques) async { + for (var i = 0; i < tiques; i++) { + await tester.pump(_intervalo); + await tester.pump(); + } + } + + setUp(() => backend = FakeAcsBackend()); + + testWidgets('ao abrir o painel consulta uma vez, mesmo com o broker conectado', (tester) async { + await abrir(tester); + expect(backend.openAlertsCalls, 1); + + await avancar(tester, 3); + expect(backend.openAlertsCalls, 1, reason: 'broker conectado: sem tique'); + expect(find.byKey(const Key('alert_fallback_status')), findsNothing); + }); + + testWidgets('broker fora: o alerta do servidor aparece sem toque e a linha de status mostra a reserva', + (tester) async { + await abrir(tester, falhaDoFeed: _brokerFora); + backend.openAlertsResult = [_aberto('via-http')]; + + await avancar(tester, 1); + + expect(find.byKey(const Key('alert_via-http')), findsOneWidget); + expect(find.byKey(const Key('alert_fallback_status')), findsOneWidget); + expect(find.textContaining('Recebendo alertas pelo servidor a cada 1 s.'), findsOneWidget); + }); + + testWidgets('queda depois de conectar liga a reserva', (tester) async { + await abrir(tester); + expect(backend.openAlertsCalls, 1); + + feed.onConnectionChanged!(false); + await avancar(tester, 2); + + expect(backend.openAlertsCalls, greaterThanOrEqualTo(3)); + expect(find.byKey(const Key('alert_fallback_status')), findsOneWidget); + }); + + testWidgets('ao armar, consulta na hora e não mostra a consulta de quando o MQTT estava em linha', + (tester) async { + final gate = Completer(); + await abrir(tester); + expect(backend.openAlertsCalls, 1, reason: 'a consulta da abertura, com o broker em linha'); + + // A próxima consulta fica em voo: a linha tem de dizer "Consultando…", + // não a hora da consulta de quando o tempo real ainda funcionava. + backend.openAlertsGate = gate; + feed.onConnectionChanged!(false); + await tester.pump(); + + expect(backend.openAlertsCalls, 2, reason: 'sem esperar um intervalo inteiro'); + expect(find.text('Consultando alertas pelo servidor…'), findsOneWidget); + gate.complete(); + await tester.pump(); + await tester.pump(); + expect(find.textContaining('Recebendo alertas pelo servidor'), findsOneWidget); + }); + + testWidgets('broker volta: o tique para', (tester) async { + await abrir(tester); + feed.onConnectionChanged!(false); + await avancar(tester, 1); + + feed.onConnectionChanged!(true); + await tester.pump(); + final chamadas = backend.openAlertsCalls; + await avancar(tester, 3); + + expect(backend.openAlertsCalls, chamadas); + expect(find.byKey(const Key('alert_fallback_status')), findsNothing); + }); + + testWidgets('MQTT e reserva entregam o mesmo alerta: um cartão', (tester) async { + await abrir(tester, falhaDoFeed: _brokerFora); + backend.openAlertsResult = [_aberto('mesmo')]; + await avancar(tester, 1); + + feed.deliver(testAlert(alertId: 'mesmo')); + await tester.pump(); + + expect(find.byKey(const Key('alert_mesmo')), findsOneWidget); + }); + + testWidgets('falha da consulta aparece na linha e não apaga a fila', (tester) async { + await abrir(tester, falhaDoFeed: _brokerFora); + backend.openAlertsResult = [_aberto('ja-na-fila')]; + await avancar(tester, 1); + + backend.openAlertsFailure = const BackendFailure('Sem conexão com o servidor.'); + await avancar(tester, 1); + + expect(find.textContaining('A consulta de alertas pelo servidor também falhou'), findsOneWidget); + expect(find.byKey(const Key('alert_ja-na-fila')), findsOneWidget); + }); + + testWidgets('consulta em voo não se sobrepõe e não quebra no dispose', (tester) async { + final gate = Completer(); + backend + ..openAlertsGate = gate + // Não vazia: a fusão notificaria a fila já descartada. + ..openAlertsResult = [_aberto('tarde-demais')]; + await abrir(tester, falhaDoFeed: _brokerFora); + expect(backend.openAlertsCalls, 1); + + await avancar(tester, 3); + expect(backend.openAlertsCalls, 1, reason: 'a primeira ainda está em voo'); + + await tester.pumpWidget(const SizedBox()); + gate.complete(); + await tester.pump(); + expect(tester.takeException(), isNull); + }); + + testWidgets('em segundo plano não consulta; ao voltar consulta na hora', (tester) async { + await abrir(tester, falhaDoFeed: _brokerFora); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.paused); + await tester.pump(); + final chamadas = backend.openAlertsCalls; + + await avancar(tester, 3); + expect(backend.openAlertsCalls, chamadas); + + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.hidden); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.inactive); + tester.binding.handleAppLifecycleStateChanged(AppLifecycleState.resumed); + await tester.pump(); + expect(backend.openAlertsCalls, chamadas + 1); + }); + + testWidgets('SessionExpired na reserva abre a reautenticação', (tester) async { + await abrir(tester, falhaDoFeed: _brokerFora); + backend.sessionExpired = true; + + await avancar(tester, 1); + await assentar(tester); + + expect(find.byKey(const Key('login_button')), findsOneWidget); + expect(find.text('Sua sessão expirou. Entre novamente com o código do autenticador.'), findsOneWidget); + }); +} diff --git a/apps/acs/test/alert_http_fallback_test.dart b/apps/acs/test/alert_http_fallback_test.dart new file mode 100644 index 0000000..2581026 --- /dev/null +++ b/apps/acs/test/alert_http_fallback_test.dart @@ -0,0 +1,40 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_acs/core/network/backend_client.dart'; +import 'package:sinalacs_acs/core/services/alert_http_fallback.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; +import 'package:sinalacs_client/sinalacs_client.dart'; + +import 'support/fakes.dart'; + +void main() { + AcsOpenAlert aberto(String id) => AcsOpenAlert( + alertId: id, + patientId: seedPatientId, + microAreaId: seedMicroAreaId, + riskLevel: RiskLevel.red, + locationHash: 'hash-sintetico', + triggeredAt: DateTime.utc(2026, 10, 17, 12), + status: AlertStatus.pending, + escalated: false, + ); + + test('poll funde a resposta do backend', () async { + final backend = FakeAcsBackend()..openAlertsResult = [aberto('x')]; + final queue = AlertQueue(microAreaId: seedMicroAreaId); + + expect(await AlertHttpFallback(backend: backend, queue: queue).poll(), 1); + expect(queue.alerts.single.alertId, 'x'); + expect(backend.openAlertsCalls, 1); + }); + + test('falha do backend sobe como BackendFailure e não mexe na fila', () async { + final backend = FakeAcsBackend()..openAlertsFailure = const BackendFailure('Sem conexão com o servidor.'); + final queue = AlertQueue(microAreaId: seedMicroAreaId); + + await expectLater( + AlertHttpFallback(backend: backend, queue: queue).poll(), + throwsA(isA()), + ); + expect(queue.isEmpty, isTrue); + }); +} diff --git a/apps/acs/test/alert_queue_merge_test.dart b/apps/acs/test/alert_queue_merge_test.dart new file mode 100644 index 0000000..d70f937 --- /dev/null +++ b/apps/acs/test/alert_queue_merge_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_acs/core/services/alert_queue.dart'; +import 'package:sinalacs_acs/core/services/mqtt_secure_client.dart'; +import 'package:sinalacs_client/sinalacs_client.dart'; + +import 'support/fakes.dart'; + +/// Fusão da reserva por HTTP na fila (#80, R2): nunca remove, nunca duplica, +/// nunca desfaz ACK nem reforço, e só notifica quando algo mudou. +AcsOpenAlert _aberto( + String id, { + AlertStatus status = AlertStatus.pending, + bool escalated = false, + String microAreaId = seedMicroAreaId, +}) => + AcsOpenAlert( + alertId: id, + patientId: seedPatientId, + microAreaId: microAreaId, + riskLevel: RiskLevel.red, + locationHash: 'hash-sintetico', + triggeredAt: DateTime.utc(2026, 10, 17, 12), + status: status, + acknowledgedAt: status == AlertStatus.acknowledged ? DateTime.utc(2026, 10, 17, 12, 1) : null, + escalated: escalated, + ); + +PrioritizedAlert _do(AcsOpenAlert a) => PrioritizedAlert.fromServer(a); + +void main() { + late AlertQueue queue; + late int notificacoes; + + setUp(() { + queue = AlertQueue(microAreaId: seedMicroAreaId); + notificacoes = 0; + queue.addListener(() => notificacoes++); + }); + + test('alerta novo do servidor entra na fila e conta 1', () { + expect(queue.mergeFromServer([_do(_aberto('a'))]), 1); + expect(queue.alerts.single.alertId, 'a'); + expect(queue.alerts.single.riskLevel, 'red'); + expect(notificacoes, 1); + }); + + test('mesma resposta de novo não muda nada nem notifica', () { + queue.mergeFromServer([_do(_aberto('a'))]); + expect(queue.mergeFromServer([_do(_aberto('a'))]), 0); + expect(notificacoes, 1); + }); + + test('outra microárea é ignorada', () { + expect(queue.mergeFromServer([_do(_aberto('a', microAreaId: otherMicroAreaId))]), 0); + expect(queue.isEmpty, isTrue); + expect(notificacoes, 0); + }); + + test('servidor confirmado promove cartão local pendente', () { + queue.upsert(_do(_aberto('a'))); + expect(queue.mergeFromServer([_do(_aberto('a', status: AlertStatus.acknowledged))]), 1); + expect(queue.alerts.single.acknowledged, isTrue); + }); + + test('servidor pendente não desfaz ACK local', () { + queue.upsert(_do(_aberto('a'))); + queue.markAcknowledged('a'); + expect(queue.mergeFromServer([_do(_aberto('a'))]), 0); + expect(queue.alerts.single.acknowledged, isTrue); + }); + + test('reforço do servidor marca escalated; ausência do marcador não o tira', () { + queue.upsert(_do(_aberto('a'))); + expect(queue.mergeFromServer([_do(_aberto('a', escalated: true))]), 1); + expect(queue.alerts.single.escalated, isTrue); + expect(queue.mergeFromServer([_do(_aberto('a'))]), 0); + expect(queue.alerts.single.escalated, isTrue); + }); + + test('cartão ausente da resposta não é removido', () { + queue.upsert(_do(_aberto('a'))); + expect(queue.mergeFromServer([]), 0); + expect(queue.alerts, hasLength(1)); + }); + + test('mergeFromServer depois de upsert do MQTT não duplica', () { + queue.upsert(PrioritizedAlert.fromMqtt(ReceivedMqttAlert( + alertId: 'x', + patientId: seedPatientId, + microAreaId: seedMicroAreaId, + riskLevel: 'red', + locationHash: 'hash-sintetico', + triggeredAt: DateTime.utc(2026, 10, 17, 12), + ))); + queue.mergeFromServer([_do(_aberto('x'))]); + expect(queue.alerts, hasLength(1)); + }); + + test('concluído localmente não volta a pendente', () { + queue.upsert(_do(_aberto('a'))); + queue.markResolved('a'); + expect(queue.mergeFromServer([_do(_aberto('a', status: AlertStatus.acknowledged))]), 0); + expect(queue.alerts.single.resolved, isTrue); + }); +} diff --git a/apps/acs/test/mqtt_drop_after_connect_test.dart b/apps/acs/test/mqtt_drop_after_connect_test.dart new file mode 100644 index 0000000..758757a --- /dev/null +++ b/apps/acs/test/mqtt_drop_after_connect_test.dart @@ -0,0 +1,64 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:sinalacs_acs/core/services/mqtt_secure_client.dart'; + +/// Queda do broker DEPOIS da primeira conexão (#80, achado da revisão final). +/// +/// Com `autoReconnect` ligado, o `mqtt_client` 10.5.1 não chama +/// `onDisconnected` nessa queda: dispara o laço de reconexão e chama só +/// `onAutoReconnect`. Sem tratar esse callback, `onConnectionChanged(false)` +/// nunca chegava ao painel, que seguia "em linha" e nunca armava a reserva por +/// HTTP. Broker falso em TCP real (sem TLS), para usar o cliente de verdade. +void main() { + test('o broker derruba o socket depois do CONNACK: onConnectionChanged(false) chega', () async { + final server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + addTearDown(server.close); + final conexoes = []; + server.listen((socket) { + conexoes.add(socket); + socket.listen((bytes) { + // CONNECT (0x10) → CONNACK aceito; SUBSCRIBE (0x82) → SUBACK QoS 1. + if (bytes.isNotEmpty && bytes.first == 0x10) { + socket.add([0x20, 0x02, 0x00, 0x00]); + } else if (bytes.isNotEmpty && bytes.first == 0x82 && bytes.length >= 4) { + socket.add([0x90, 0x03, bytes[2], bytes[3], 0x01]); + } + }, onError: (_) {}); + }); + + final client = MqttSecureClient( + config: SecureMqttConfig( + brokerHost: '127.0.0.1', + port: server.port, + clientId: 'acs-queda-teste', + topic: 'sinalacs/v1/microareas/area-teste/alerts', + useTls: false, + ), + ); + addTearDown(client.disconnect); + + final estados = []; + final caiu = Completer(); + await client.connect( + onAlert: (_) {}, + onConnectionChanged: (conectado) { + estados.add(conectado); + if (!conectado && !caiu.isCompleted) caiu.complete(); + }, + ); + expect(client.isConnected, isTrue); + + // O broker cai: fecha a conexão aceita e para de aceitar novas. + await server.close(); + for (final s in conexoes) { + s.destroy(); + } + + await caiu.future.timeout(const Duration(seconds: 5), onTimeout: () { + fail('a queda depois de conectar nunca virou onConnectionChanged(false): $estados'); + }); + expect(estados.last, isFalse); + }); +} diff --git a/apps/acs/test/support/fakes.dart b/apps/acs/test/support/fakes.dart index 50e66b4..0460266 100644 --- a/apps/acs/test/support/fakes.dart +++ b/apps/acs/test/support/fakes.dart @@ -201,6 +201,33 @@ class FakeAcsBackend implements AcsBackend { onSessionExpired?.call(); } + /// O que `openAlerts()` devolve (#80). + List openAlertsResult = const []; + + /// Quando presente, `openAlerts()` lança esta falha. + Object? openAlertsFailure; + + int openAlertsCalls = 0; + + /// Se definido, `openAlerts()` espera por ele (consulta lenta). + Completer? openAlertsGate; + + @override + Future> openAlerts() async { + openAlertsCalls++; + await openAlertsGate?.future; + if (sessionExpired) { + onSessionExpired?.call(); + throw const BackendFailure( + 'Sua sessão expirou. Entre novamente com o código do autenticador.', + isRecoverable: false, + ); + } + final falha = openAlertsFailure; + if (falha != null) throw falha; + return openAlertsResult; + } + @override Future acknowledge({required String alertId}) async { if (sessionExpired) { diff --git a/backend/CLAUDE.md b/backend/CLAUDE.md index 2d1577e..6ff57c2 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`, `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.resolveConflict` (#79, D2: compare-and-set sobre `expectedServerVersion` — igual grava o conteúdo enviado com `version + 1`, diferente devolve `conflict` com a cópia nova e não grava; mesmas recusas do `sync`; trilha `write`/`visit`/`conflict_resolved_local` com `recordSafely`; `entry.version` é ignorado). `VisitSyncResult.serverVisit` (D1) vem preenchido só no `conflict` de `sync`/`syncDeferred` (nunca `syncLegacy`), com `read`/`visit_conflict` na trilha, sem migração. `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`), `alerts.openForMicroArea` (#80, R2: a reserva por HTTP do ACS quando o MQTT do aparelho cai; só ACS com microárea, que vem do token; devolve `AcsOpenAlert` — o envelope MQTT mais `status`, `acknowledgedAt` e `escalated` (`escalatedAt` preenchido ou o `status: escalated` legado), sem `escalatedBy`/`escalationReason` — de `pending`/`escalated` e depois `acknowledged`, cada grupo do mais antigo ao mais novo, teto `RedAlertService.openAlertsLimit` (200) que corta os mais antigos de cada grupo, nunca o pendente mais novo; **sem** linha de auditoria de leitura, porque o app repete a consulta a cada 30 s com o broker fora — ACK e conclusão seguem auditados), `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.resolveConflict` (#79, D2: compare-and-set sobre `expectedServerVersion` — igual grava o conteúdo enviado com `version + 1`, diferente devolve `conflict` com a cópia nova e não grava; mesmas recusas do `sync`; trilha `write`/`visit`/`conflict_resolved_local` com `recordSafely`; `entry.version` é ignorado). `VisitSyncResult.serverVisit` (D1) vem preenchido só no `conflict` de `sync`/`syncDeferred` (nunca `syncLegacy`), com `read`/`visit_conflict` na trilha, sem migração. `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/backend/sinalacs_client/lib/src/protocol/api/acs_open_alert.dart b/backend/sinalacs_client/lib/src/protocol/api/acs_open_alert.dart new file mode 100644 index 0000000..6320818 --- /dev/null +++ b/backend/sinalacs_client/lib/src/protocol/api/acs_open_alert.dart @@ -0,0 +1,190 @@ +/* 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/risk_level.dart' as _i2; +import '../enums/alert_status.dart' as _i3; + +/// Alerta ainda não concluído da microárea do ACS, lido por HTTP (#80, R2): +/// a reserva para quando o MQTT do aparelho não está conectado. +/// +/// Os campos espelham o envelope MQTT (`AlertDelivery.toJson`) mais o estado +/// persistido (`status`, `acknowledgedAt`) e o marcador de reforço. Nada de +/// `escalatedBy`, `escalationReason` ou `deviceId`: nenhuma identidade a mais. +abstract class AcsOpenAlert implements _i1.SerializableModel { + AcsOpenAlert._({ + required this.alertId, + required this.patientId, + required this.microAreaId, + required this.riskLevel, + required this.locationHash, + this.locationCell, + required this.triggeredAt, + required this.status, + this.acknowledgedAt, + required this.escalated, + }); + + factory AcsOpenAlert({ + required String alertId, + required String patientId, + required String microAreaId, + required _i2.RiskLevel riskLevel, + required String locationHash, + String? locationCell, + required DateTime triggeredAt, + required _i3.AlertStatus status, + DateTime? acknowledgedAt, + required bool escalated, + }) = _AcsOpenAlertImpl; + + factory AcsOpenAlert.fromJson(Map jsonSerialization) { + return AcsOpenAlert( + alertId: jsonSerialization['alertId'] as String, + patientId: jsonSerialization['patientId'] as String, + microAreaId: jsonSerialization['microAreaId'] as String, + riskLevel: _i2.RiskLevel.fromJson( + (jsonSerialization['riskLevel'] as String), + ), + locationHash: jsonSerialization['locationHash'] as String, + locationCell: jsonSerialization['locationCell'] as String?, + triggeredAt: _i1.DateTimeJsonExtension.fromJson( + jsonSerialization['triggeredAt'], + ), + status: _i3.AlertStatus.fromJson((jsonSerialization['status'] as String)), + acknowledgedAt: jsonSerialization['acknowledgedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson( + jsonSerialization['acknowledgedAt'], + ), + escalated: _i1.BoolJsonExtension.fromJson(jsonSerialization['escalated']), + ); + } + + String alertId; + + String patientId; + + String microAreaId; + + _i2.RiskLevel riskLevel; + + String locationHash; + + String? locationCell; + + DateTime triggeredAt; + + _i3.AlertStatus status; + + DateTime? acknowledgedAt; + + bool escalated; + + /// Returns a shallow copy of this [AcsOpenAlert] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + AcsOpenAlert copyWith({ + String? alertId, + String? patientId, + String? microAreaId, + _i2.RiskLevel? riskLevel, + String? locationHash, + String? locationCell, + DateTime? triggeredAt, + _i3.AlertStatus? status, + DateTime? acknowledgedAt, + bool? escalated, + }); + @override + Map toJson() { + return { + '__className__': 'AcsOpenAlert', + 'alertId': alertId, + 'patientId': patientId, + 'microAreaId': microAreaId, + 'riskLevel': riskLevel.toJson(), + 'locationHash': locationHash, + if (locationCell != null) 'locationCell': locationCell, + 'triggeredAt': triggeredAt.toJson(), + 'status': status.toJson(), + if (acknowledgedAt != null) 'acknowledgedAt': acknowledgedAt?.toJson(), + 'escalated': escalated, + }; + } + + @override + String toString() { + return _i1.SerializationManager.encode(this); + } +} + +class _Undefined {} + +class _AcsOpenAlertImpl extends AcsOpenAlert { + _AcsOpenAlertImpl({ + required String alertId, + required String patientId, + required String microAreaId, + required _i2.RiskLevel riskLevel, + required String locationHash, + String? locationCell, + required DateTime triggeredAt, + required _i3.AlertStatus status, + DateTime? acknowledgedAt, + required bool escalated, + }) : super._( + alertId: alertId, + patientId: patientId, + microAreaId: microAreaId, + riskLevel: riskLevel, + locationHash: locationHash, + locationCell: locationCell, + triggeredAt: triggeredAt, + status: status, + acknowledgedAt: acknowledgedAt, + escalated: escalated, + ); + + /// Returns a shallow copy of this [AcsOpenAlert] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + @override + AcsOpenAlert copyWith({ + String? alertId, + String? patientId, + String? microAreaId, + _i2.RiskLevel? riskLevel, + String? locationHash, + Object? locationCell = _Undefined, + DateTime? triggeredAt, + _i3.AlertStatus? status, + Object? acknowledgedAt = _Undefined, + bool? escalated, + }) { + return AcsOpenAlert( + alertId: alertId ?? this.alertId, + patientId: patientId ?? this.patientId, + microAreaId: microAreaId ?? this.microAreaId, + riskLevel: riskLevel ?? this.riskLevel, + locationHash: locationHash ?? this.locationHash, + locationCell: locationCell is String? ? locationCell : this.locationCell, + triggeredAt: triggeredAt ?? this.triggeredAt, + status: status ?? this.status, + acknowledgedAt: acknowledgedAt is DateTime? + ? acknowledgedAt + : this.acknowledgedAt, + escalated: escalated ?? this.escalated, + ); + } +} diff --git a/backend/sinalacs_client/lib/src/protocol/client.dart b/backend/sinalacs_client/lib/src/protocol/client.dart index 772e26c..ee75f90 100644 --- a/backend/sinalacs_client/lib/src/protocol/client.dart +++ b/backend/sinalacs_client/lib/src/protocol/client.dart @@ -52,37 +52,38 @@ 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_resolve_result.dart' as _i26; +import 'package:sinalacs_client/src/protocol/api/acs_open_alert.dart' as _i27; import 'package:sinalacs_client/src/protocol/api/alert_status_result.dart' - as _i27; -import 'package:sinalacs_client/src/protocol/api/development_login_result.dart' as _i28; -import 'package:sinalacs_client/src/protocol/api/totp_enrollment_start.dart' +import 'package:sinalacs_client/src/protocol/api/development_login_result.dart' as _i29; -import 'package:sinalacs_client/src/protocol/api/service_health.dart' as _i30; +import 'package:sinalacs_client/src/protocol/api/totp_enrollment_start.dart' + as _i30; +import 'package:sinalacs_client/src/protocol/api/service_health.dart' as _i31; import 'package:sinalacs_client/src/protocol/api/notice_send_result.dart' - as _i31; -import 'package:sinalacs_client/src/protocol/api/enrollment_token_result.dart' as _i32; -import 'package:sinalacs_client/src/protocol/api/enrollment_result.dart' +import 'package:sinalacs_client/src/protocol/api/enrollment_token_result.dart' as _i33; -import 'package:sinalacs_client/src/protocol/api/micro_area_patient.dart' +import 'package:sinalacs_client/src/protocol/api/enrollment_result.dart' as _i34; -import 'package:sinalacs_client/src/protocol/api/patient_data_overview.dart' +import 'package:sinalacs_client/src/protocol/api/micro_area_patient.dart' as _i35; -import 'package:sinalacs_client/src/protocol/api/patient_consent_record.dart' +import 'package:sinalacs_client/src/protocol/api/patient_data_overview.dart' as _i36; -import 'package:sinalacs_client/src/protocol/enums/consent_purpose.dart' +import 'package:sinalacs_client/src/protocol/api/patient_consent_record.dart' as _i37; -import 'package:sinalacs_client/src/protocol/api/terms_change_notice.dart' +import 'package:sinalacs_client/src/protocol/enums/consent_purpose.dart' as _i38; -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 _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/patient_data_subject_request_record.dart' + as _i40; +import 'package:sinalacs_client/src/protocol/api/triage_result.dart' as _i41; +import 'package:sinalacs_client/src/protocol/api/ubs_contact.dart' as _i42; import 'package:sinalacs_client/src/protocol/api/visit_sync_result.dart' - as _i42; -import 'package:sinalacs_client/src/protocol/api/visit_sync_entry.dart' as _i43; -import 'protocol.dart' as _i44; + as _i43; +import 'package:sinalacs_client/src/protocol/api/visit_sync_entry.dart' as _i44; +import 'protocol.dart' as _i45; /// Backoffice, só para `coordinator` e `admin`: leitura (issue #40), gestão de /// contas (issue #43), atendimento de pedidos do titular (issue #42) e @@ -612,10 +613,23 @@ class EndpointAlerts extends EndpointAuthenticated { }, ); + /// Alertas não concluídos da microárea do ACS (#80, R2): a reserva por HTTP + /// para quando o MQTT do aparelho não está conectado. A microárea vem do + /// token (INV-01). Não grava linha de auditoria de leitura: o app repete a + /// consulta a cada 30 s com o broker fora, e o mesmo conteúdo chega pelo + /// MQTT sem auditoria; ACK e conclusão seguem auditados. + _i2.Future> openForMicroArea({ + required String accessToken, + }) => caller.callServerEndpoint>( + 'alerts', + 'openForMicroArea', + {'accessToken': accessToken}, + ); + /// Status do alerta mais recente do PRÓPRIO paciente (RF05, decisão §5). /// `patientId` nunca é parâmetro — vem do token (INV-05). - _i2.Future<_i27.AlertStatusResult> statusFor({required String accessToken}) => - caller.callServerEndpoint<_i27.AlertStatusResult>( + _i2.Future<_i28.AlertStatusResult> statusFor({required String accessToken}) => + caller.callServerEndpoint<_i28.AlertStatusResult>( 'alerts', 'statusFor', {'accessToken': accessToken}, @@ -637,9 +651,9 @@ class EndpointAuth extends _i1.EndpointRef { @override String get name => 'auth'; - _i2.Future<_i28.DevelopmentLoginResult> developmentLogin({ + _i2.Future<_i29.DevelopmentLoginResult> developmentLogin({ required String role, - }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i29.DevelopmentLoginResult>( 'auth', 'developmentLogin', {'role': role}, @@ -657,12 +671,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<_i28.DevelopmentLoginResult> loginInstitutional({ + _i2.Future<_i29.DevelopmentLoginResult> loginInstitutional({ required String matricula, required String password, String? deviceId, String? totpCode, - }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i29.DevelopmentLoginResult>( 'auth', 'loginInstitutional', { @@ -683,11 +697,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<_i28.DevelopmentLoginResult> loginStaff({ + _i2.Future<_i29.DevelopmentLoginResult> loginStaff({ required String matricula, required String password, String? totpCode, - }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i29.DevelopmentLoginResult>( 'auth', 'loginStaff', { @@ -702,11 +716,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<_i29.TotpEnrollmentStart> beginStaffTotpEnrollment({ + _i2.Future<_i30.TotpEnrollmentStart> beginStaffTotpEnrollment({ required String matricula, required String password, required String activationCode, - }) => caller.callServerEndpoint<_i29.TotpEnrollmentStart>( + }) => caller.callServerEndpoint<_i30.TotpEnrollmentStart>( 'auth', 'beginStaffTotpEnrollment', { @@ -736,10 +750,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<_i28.DevelopmentLoginResult> refreshSession({ + _i2.Future<_i29.DevelopmentLoginResult> refreshSession({ required String refreshToken, required String deviceId, - }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i29.DevelopmentLoginResult>( 'auth', 'refreshSession', { @@ -757,10 +771,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<_i29.TotpEnrollmentStart> beginTotpEnrollment({ + _i2.Future<_i30.TotpEnrollmentStart> beginTotpEnrollment({ required String matricula, required String password, - }) => caller.callServerEndpoint<_i29.TotpEnrollmentStart>( + }) => caller.callServerEndpoint<_i30.TotpEnrollmentStart>( 'auth', 'beginTotpEnrollment', { @@ -825,11 +839,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<_i28.DevelopmentLoginResult> verifyOtp({ + _i2.Future<_i29.DevelopmentLoginResult> verifyOtp({ required String cpf, required String code, String? deviceId, - }) => caller.callServerEndpoint<_i28.DevelopmentLoginResult>( + }) => caller.callServerEndpoint<_i29.DevelopmentLoginResult>( 'auth', 'verifyOtp', { @@ -896,8 +910,8 @@ class EndpointHealth extends _i1.EndpointRef { @override String get name => 'health'; - _i2.Future<_i30.ServiceHealth> check() => - caller.callServerEndpoint<_i30.ServiceHealth>( + _i2.Future<_i31.ServiceHealth> check() => + caller.callServerEndpoint<_i31.ServiceHealth>( 'health', 'check', {}, @@ -916,12 +930,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<_i31.NoticeSendResult> sendSegmented({ + _i2.Future<_i32.NoticeSendResult> sendSegmented({ required String accessToken, required String title, required String message, required String audience, - }) => caller.callServerEndpoint<_i31.NoticeSendResult>( + }) => caller.callServerEndpoint<_i32.NoticeSendResult>( 'notices', 'sendSegmented', { @@ -950,10 +964,10 @@ class EndpointOnboarding extends _i1.EndpointRef { String get name => 'onboarding'; /// Chamado pelo app do ACS. Exige sessão de ACS. - _i2.Future<_i32.EnrollmentTokenResult> generateEnrollmentToken({ + _i2.Future<_i33.EnrollmentTokenResult> generateEnrollmentToken({ required String accessToken, required String patientId, - }) => caller.callServerEndpoint<_i32.EnrollmentTokenResult>( + }) => caller.callServerEndpoint<_i33.EnrollmentTokenResult>( 'onboarding', 'generateEnrollmentToken', { @@ -964,13 +978,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<_i33.EnrollmentResult> completeEnrollment({ + _i2.Future<_i34.EnrollmentResult> completeEnrollment({ required String token, required bool healthDataConsent, required bool remindersConsent, required bool pushConsent, required bool termsAccepted, - }) => caller.callServerEndpoint<_i33.EnrollmentResult>( + }) => caller.callServerEndpoint<_i34.EnrollmentResult>( 'onboarding', 'completeEnrollment', { @@ -999,9 +1013,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}, @@ -1034,8 +1048,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<_i35.PatientDataOverview> myData({required String accessToken}) => - caller.callServerEndpoint<_i35.PatientDataOverview>( + _i2.Future<_i36.PatientDataOverview> myData({required String accessToken}) => + caller.callServerEndpoint<_i36.PatientDataOverview>( 'patients', 'myData', {'accessToken': accessToken}, @@ -1045,11 +1059,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<_i36.PatientConsentRecord> updateConsent({ + _i2.Future<_i37.PatientConsentRecord> updateConsent({ required String accessToken, - required _i37.ConsentPurpose purpose, + required _i38.ConsentPurpose purpose, required bool granted, - }) => caller.callServerEndpoint<_i36.PatientConsentRecord>( + }) => caller.callServerEndpoint<_i37.PatientConsentRecord>( 'patients', 'updateConsent', { @@ -1063,9 +1077,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<_i36.PatientConsentRecord> acceptTermsOfUse({ + _i2.Future<_i37.PatientConsentRecord> acceptTermsOfUse({ required String accessToken, - }) => caller.callServerEndpoint<_i36.PatientConsentRecord>( + }) => caller.callServerEndpoint<_i37.PatientConsentRecord>( 'patients', 'acceptTermsOfUse', {'accessToken': accessToken}, @@ -1089,7 +1103,7 @@ class EndpointPatients extends EndpointAuthenticated { /// push (RF14). _i2.Future hasGrantedConsent({ required String accessToken, - required _i37.ConsentPurpose purpose, + required _i38.ConsentPurpose purpose, }) => caller.callServerEndpoint( 'patients', 'hasGrantedConsent', @@ -1102,9 +1116,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<_i38.TermsChangeNotice?> termsChangeNotice({ + _i2.Future<_i39.TermsChangeNotice?> termsChangeNotice({ required String accessToken, - }) => caller.callServerEndpoint<_i38.TermsChangeNotice?>( + }) => caller.callServerEndpoint<_i39.TermsChangeNotice?>( 'patients', 'termsChangeNotice', {'accessToken': accessToken}, @@ -1112,9 +1126,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<_i39.PatientDataSubjectRequestRecord> requestDataDeletion({ + _i2.Future<_i40.PatientDataSubjectRequestRecord> requestDataDeletion({ required String accessToken, - }) => caller.callServerEndpoint<_i39.PatientDataSubjectRequestRecord>( + }) => caller.callServerEndpoint<_i40.PatientDataSubjectRequestRecord>( 'patients', 'requestDataDeletion', {'accessToken': accessToken}, @@ -1123,10 +1137,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<_i39.PatientDataSubjectRequestRecord> requestDataCorrection({ + _i2.Future<_i40.PatientDataSubjectRequestRecord> requestDataCorrection({ required String accessToken, required String details, - }) => caller.callServerEndpoint<_i39.PatientDataSubjectRequestRecord>( + }) => caller.callServerEndpoint<_i40.PatientDataSubjectRequestRecord>( 'patients', 'requestDataCorrection', { @@ -1153,7 +1167,7 @@ class EndpointTriage extends EndpointAuthenticated { @override String get name => 'triage'; - _i2.Future<_i40.TriageResult> evaluate({ + _i2.Future<_i41.TriageResult> evaluate({ required String accessToken, required bool chestPain, required bool difficultyBreathing, @@ -1161,7 +1175,7 @@ class EndpointTriage extends EndpointAuthenticated { required bool persistentVomiting, required bool bleeding, required bool severeWeakness, - }) => caller.callServerEndpoint<_i40.TriageResult>( + }) => caller.callServerEndpoint<_i41.TriageResult>( 'triage', 'evaluate', { @@ -1184,8 +1198,8 @@ class EndpointUbs extends EndpointAuthenticated { @override String get name => 'ubs'; - _i2.Future<_i41.UbsContact> myContact({required String accessToken}) => - caller.callServerEndpoint<_i41.UbsContact>( + _i2.Future<_i42.UbsContact> myContact({required String accessToken}) => + caller.callServerEndpoint<_i42.UbsContact>( 'ubs', 'myContact', {'accessToken': accessToken}, @@ -1208,10 +1222,10 @@ class EndpointVisits extends EndpointAuthenticated { @override String get name => 'visits'; - _i2.Future> sync({ + _i2.Future> sync({ required String accessToken, - required List<_i43.VisitSyncEntry> visits, - }) => caller.callServerEndpoint>( + required List<_i44.VisitSyncEntry> visits, + }) => caller.callServerEndpoint>( 'visits', 'sync', { @@ -1225,11 +1239,11 @@ class EndpointVisits extends EndpointAuthenticated { /// conflito. `visit.version` é ignorado. Ver /// `VisitSyncService.resolveConflict`. Mesma transação e mesma tradução de /// erros de [sync]. - _i2.Future<_i42.VisitSyncResult> resolveConflict({ + _i2.Future<_i43.VisitSyncResult> resolveConflict({ required String accessToken, - required _i43.VisitSyncEntry visit, + required _i44.VisitSyncEntry visit, required int expectedServerVersion, - }) => caller.callServerEndpoint<_i42.VisitSyncResult>( + }) => caller.callServerEndpoint<_i43.VisitSyncResult>( 'visits', 'resolveConflict', { @@ -1244,11 +1258,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<_i43.VisitSyncEntry> visits, - }) => caller.callServerEndpoint>( + required List<_i44.VisitSyncEntry> visits, + }) => caller.callServerEndpoint>( 'visits', 'syncLegacy', { @@ -1272,11 +1286,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<_i43.VisitSyncEntry> visits, - }) => caller.callServerEndpoint>( + required List<_i44.VisitSyncEntry> visits, + }) => caller.callServerEndpoint>( 'visits', 'syncDeferred', { @@ -1299,10 +1313,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', { @@ -1332,7 +1346,7 @@ class Client extends _i1.ServerpodClientShared { bool? disconnectStreamsOnLostInternetConnection, }) : super( host, - _i44.Protocol(), + _i45.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 efe04f9..2bc995e 100644 --- a/backend/sinalacs_client/lib/src/protocol/protocol.dart +++ b/backend/sinalacs_client/lib/src/protocol/protocol.dart @@ -19,105 +19,107 @@ import 'alert.dart' as _i5; import 'alert_delivery_record.dart' as _i6; import 'alert_idempotency_key.dart' as _i7; import 'alert_outbox_entry.dart' as _i8; -import 'api/admin_acs.dart' as _i9; -import 'api/admin_acs_creation_result.dart' as _i10; -import 'api/admin_alert.dart' as _i11; -import 'api/admin_alert_page.dart' as _i12; -import 'api/admin_alerts_pulse.dart' as _i13; -import 'api/admin_audit_chain_status.dart' as _i14; -import 'api/admin_audit_entry.dart' as _i15; -import 'api/admin_audit_export.dart' as _i16; -import 'api/admin_audit_filter.dart' as _i17; -import 'api/admin_audit_page.dart' as _i18; -import 'api/admin_data_subject_request.dart' as _i19; -import 'api/admin_data_subject_request_detail.dart' as _i20; -import 'api/admin_data_subject_request_page.dart' as _i21; -import 'api/admin_escalation_result.dart' as _i22; -import 'api/admin_indicators.dart' as _i23; -import 'api/admin_micro_area.dart' as _i24; -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_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 'api/acs_open_alert.dart' as _i9; +import 'api/admin_acs.dart' as _i10; +import 'api/admin_acs_creation_result.dart' as _i11; +import 'api/admin_alert.dart' as _i12; +import 'api/admin_alert_page.dart' as _i13; +import 'api/admin_alerts_pulse.dart' as _i14; +import 'api/admin_audit_chain_status.dart' as _i15; +import 'api/admin_audit_entry.dart' as _i16; +import 'api/admin_audit_export.dart' as _i17; +import 'api/admin_audit_filter.dart' as _i18; +import 'api/admin_audit_page.dart' as _i19; +import 'api/admin_data_subject_request.dart' as _i20; +import 'api/admin_data_subject_request_detail.dart' as _i21; +import 'api/admin_data_subject_request_page.dart' as _i22; +import 'api/admin_escalation_result.dart' as _i23; +import 'api/admin_indicators.dart' as _i24; +import 'api/admin_micro_area.dart' as _i25; +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_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_client/src/protocol/api/admin_micro_area.dart' as _i98; +import 'package:sinalacs_client/src/protocol/api/admin_acs.dart' as _i99; +import 'package:sinalacs_client/src/protocol/api/admin_staff.dart' as _i100; import 'package:sinalacs_client/src/protocol/api/terms_version_entry.dart' - as _i100; -import 'package:sinalacs_client/src/protocol/api/micro_area_patient.dart' as _i101; +import 'package:sinalacs_client/src/protocol/api/acs_open_alert.dart' as _i102; +import 'package:sinalacs_client/src/protocol/api/micro_area_patient.dart' + as _i103; import 'package:sinalacs_client/src/protocol/api/visit_sync_result.dart' - as _i102; + as _i104; import 'package:sinalacs_client/src/protocol/api/visit_sync_entry.dart' - as _i103; + as _i105; export 'acs.dart'; export 'acs_refresh_token.dart'; export 'acs_upload_token.dart'; @@ -125,6 +127,7 @@ export 'alert.dart'; export 'alert_delivery_record.dart'; export 'alert_idempotency_key.dart'; export 'alert_outbox_entry.dart'; +export 'api/acs_open_alert.dart'; export 'api/admin_acs.dart'; export 'api/admin_acs_creation_result.dart'; export 'api/admin_alert.dart'; @@ -270,269 +273,272 @@ class Protocol extends _i1.SerializationManager { if (t == _i8.AlertOutboxEntry) { return _i8.AlertOutboxEntry.fromJson(data) as T; } - if (t == _i9.AdminAcs) { - return _i9.AdminAcs.fromJson(data) as T; + if (t == _i9.AcsOpenAlert) { + return _i9.AcsOpenAlert.fromJson(data) as T; } - if (t == _i10.AdminAcsCreationResult) { - return _i10.AdminAcsCreationResult.fromJson(data) as T; + if (t == _i10.AdminAcs) { + return _i10.AdminAcs.fromJson(data) as T; } - if (t == _i11.AdminAlert) { - return _i11.AdminAlert.fromJson(data) as T; + if (t == _i11.AdminAcsCreationResult) { + return _i11.AdminAcsCreationResult.fromJson(data) as T; } - if (t == _i12.AdminAlertPage) { - return _i12.AdminAlertPage.fromJson(data) as T; + if (t == _i12.AdminAlert) { + return _i12.AdminAlert.fromJson(data) as T; } - if (t == _i13.AdminAlertsPulse) { - return _i13.AdminAlertsPulse.fromJson(data) as T; + if (t == _i13.AdminAlertPage) { + return _i13.AdminAlertPage.fromJson(data) as T; } - if (t == _i14.AdminAuditChainStatus) { - return _i14.AdminAuditChainStatus.fromJson(data) as T; + if (t == _i14.AdminAlertsPulse) { + return _i14.AdminAlertsPulse.fromJson(data) as T; } - if (t == _i15.AdminAuditEntry) { - return _i15.AdminAuditEntry.fromJson(data) as T; + if (t == _i15.AdminAuditChainStatus) { + return _i15.AdminAuditChainStatus.fromJson(data) as T; } - if (t == _i16.AdminAuditExport) { - return _i16.AdminAuditExport.fromJson(data) as T; + if (t == _i16.AdminAuditEntry) { + return _i16.AdminAuditEntry.fromJson(data) as T; } - if (t == _i17.AdminAuditFilter) { - return _i17.AdminAuditFilter.fromJson(data) as T; + if (t == _i17.AdminAuditExport) { + return _i17.AdminAuditExport.fromJson(data) as T; } - if (t == _i18.AdminAuditPage) { - return _i18.AdminAuditPage.fromJson(data) as T; + if (t == _i18.AdminAuditFilter) { + return _i18.AdminAuditFilter.fromJson(data) as T; } - if (t == _i19.AdminDataSubjectRequest) { - return _i19.AdminDataSubjectRequest.fromJson(data) as T; + if (t == _i19.AdminAuditPage) { + return _i19.AdminAuditPage.fromJson(data) as T; } - if (t == _i20.AdminDataSubjectRequestDetail) { - return _i20.AdminDataSubjectRequestDetail.fromJson(data) as T; + if (t == _i20.AdminDataSubjectRequest) { + return _i20.AdminDataSubjectRequest.fromJson(data) as T; } - if (t == _i21.AdminDataSubjectRequestPage) { - return _i21.AdminDataSubjectRequestPage.fromJson(data) as T; + if (t == _i21.AdminDataSubjectRequestDetail) { + return _i21.AdminDataSubjectRequestDetail.fromJson(data) as T; } - if (t == _i22.AdminEscalationResult) { - return _i22.AdminEscalationResult.fromJson(data) as T; + if (t == _i22.AdminDataSubjectRequestPage) { + return _i22.AdminDataSubjectRequestPage.fromJson(data) as T; } - if (t == _i23.AdminIndicators) { - return _i23.AdminIndicators.fromJson(data) as T; + if (t == _i23.AdminEscalationResult) { + return _i23.AdminEscalationResult.fromJson(data) as T; } - if (t == _i24.AdminMicroArea) { - return _i24.AdminMicroArea.fromJson(data) as T; + if (t == _i24.AdminIndicators) { + return _i24.AdminIndicators.fromJson(data) as T; } - if (t == _i25.AdminPasswordResetResult) { - return _i25.AdminPasswordResetResult.fromJson(data) as T; + if (t == _i25.AdminMicroArea) { + return _i25.AdminMicroArea.fromJson(data) as T; } - if (t == _i26.AdminStaff) { - return _i26.AdminStaff.fromJson(data) as T; + if (t == _i26.AdminPasswordResetResult) { + return _i26.AdminPasswordResetResult.fromJson(data) as T; } - if (t == _i27.AdminStaffMfaResetResult) { - return _i27.AdminStaffMfaResetResult.fromJson(data) as T; + if (t == _i27.AdminStaff) { + return _i27.AdminStaff.fromJson(data) as T; } - if (t == _i28.AlertAckResult) { - return _i28.AlertAckResult.fromJson(data) as T; + if (t == _i28.AdminStaffMfaResetResult) { + return _i28.AdminStaffMfaResetResult.fromJson(data) as T; } - if (t == _i29.AlertResolveResult) { - return _i29.AlertResolveResult.fromJson(data) as T; + 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<_i2.Acs?>()) { return (data != null ? _i2.Acs.fromJson(data) : null) as T; @@ -557,371 +563,374 @@ class Protocol extends _i1.SerializationManager { if (t == _i1.getType<_i8.AlertOutboxEntry?>()) { return (data != null ? _i8.AlertOutboxEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i9.AdminAcs?>()) { - return (data != null ? _i9.AdminAcs.fromJson(data) : null) as T; + if (t == _i1.getType<_i9.AcsOpenAlert?>()) { + return (data != null ? _i9.AcsOpenAlert.fromJson(data) : null) as T; + } + if (t == _i1.getType<_i10.AdminAcs?>()) { + return (data != null ? _i10.AdminAcs.fromJson(data) : null) as T; } - if (t == _i1.getType<_i10.AdminAcsCreationResult?>()) { - return (data != null ? _i10.AdminAcsCreationResult.fromJson(data) : null) + if (t == _i1.getType<_i11.AdminAcsCreationResult?>()) { + return (data != null ? _i11.AdminAcsCreationResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i11.AdminAlert?>()) { - return (data != null ? _i11.AdminAlert.fromJson(data) : null) as T; + if (t == _i1.getType<_i12.AdminAlert?>()) { + return (data != null ? _i12.AdminAlert.fromJson(data) : null) as T; } - if (t == _i1.getType<_i12.AdminAlertPage?>()) { - return (data != null ? _i12.AdminAlertPage.fromJson(data) : null) as T; + if (t == _i1.getType<_i13.AdminAlertPage?>()) { + return (data != null ? _i13.AdminAlertPage.fromJson(data) : null) as T; } - if (t == _i1.getType<_i13.AdminAlertsPulse?>()) { - return (data != null ? _i13.AdminAlertsPulse.fromJson(data) : null) as T; + if (t == _i1.getType<_i14.AdminAlertsPulse?>()) { + return (data != null ? _i14.AdminAlertsPulse.fromJson(data) : null) as T; } - if (t == _i1.getType<_i14.AdminAuditChainStatus?>()) { - return (data != null ? _i14.AdminAuditChainStatus.fromJson(data) : null) + if (t == _i1.getType<_i15.AdminAuditChainStatus?>()) { + return (data != null ? _i15.AdminAuditChainStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i15.AdminAuditEntry?>()) { - return (data != null ? _i15.AdminAuditEntry.fromJson(data) : null) as T; + if (t == _i1.getType<_i16.AdminAuditEntry?>()) { + return (data != null ? _i16.AdminAuditEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i16.AdminAuditExport?>()) { - return (data != null ? _i16.AdminAuditExport.fromJson(data) : null) as T; + if (t == _i1.getType<_i17.AdminAuditExport?>()) { + return (data != null ? _i17.AdminAuditExport.fromJson(data) : null) as T; } - if (t == _i1.getType<_i17.AdminAuditFilter?>()) { - return (data != null ? _i17.AdminAuditFilter.fromJson(data) : null) as T; + if (t == _i1.getType<_i18.AdminAuditFilter?>()) { + return (data != null ? _i18.AdminAuditFilter.fromJson(data) : null) as T; } - if (t == _i1.getType<_i18.AdminAuditPage?>()) { - return (data != null ? _i18.AdminAuditPage.fromJson(data) : null) as T; + if (t == _i1.getType<_i19.AdminAuditPage?>()) { + return (data != null ? _i19.AdminAuditPage.fromJson(data) : null) as T; } - if (t == _i1.getType<_i19.AdminDataSubjectRequest?>()) { - return (data != null ? _i19.AdminDataSubjectRequest.fromJson(data) : null) + if (t == _i1.getType<_i20.AdminDataSubjectRequest?>()) { + return (data != null ? _i20.AdminDataSubjectRequest.fromJson(data) : null) as T; } - if (t == _i1.getType<_i20.AdminDataSubjectRequestDetail?>()) { + if (t == _i1.getType<_i21.AdminDataSubjectRequestDetail?>()) { return (data != null - ? _i20.AdminDataSubjectRequestDetail.fromJson(data) + ? _i21.AdminDataSubjectRequestDetail.fromJson(data) : null) as T; } - if (t == _i1.getType<_i21.AdminDataSubjectRequestPage?>()) { + if (t == _i1.getType<_i22.AdminDataSubjectRequestPage?>()) { return (data != null - ? _i21.AdminDataSubjectRequestPage.fromJson(data) + ? _i22.AdminDataSubjectRequestPage.fromJson(data) : null) as T; } - if (t == _i1.getType<_i22.AdminEscalationResult?>()) { - return (data != null ? _i22.AdminEscalationResult.fromJson(data) : null) + if (t == _i1.getType<_i23.AdminEscalationResult?>()) { + return (data != null ? _i23.AdminEscalationResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i23.AdminIndicators?>()) { - return (data != null ? _i23.AdminIndicators.fromJson(data) : null) as T; + if (t == _i1.getType<_i24.AdminIndicators?>()) { + return (data != null ? _i24.AdminIndicators.fromJson(data) : null) as T; } - if (t == _i1.getType<_i24.AdminMicroArea?>()) { - return (data != null ? _i24.AdminMicroArea.fromJson(data) : null) as T; + if (t == _i1.getType<_i25.AdminMicroArea?>()) { + return (data != null ? _i25.AdminMicroArea.fromJson(data) : null) as T; } - if (t == _i1.getType<_i25.AdminPasswordResetResult?>()) { + if (t == _i1.getType<_i26.AdminPasswordResetResult?>()) { return (data != null - ? _i25.AdminPasswordResetResult.fromJson(data) + ? _i26.AdminPasswordResetResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i26.AdminStaff?>()) { - return (data != null ? _i26.AdminStaff.fromJson(data) : null) as T; + if (t == _i1.getType<_i27.AdminStaff?>()) { + return (data != null ? _i27.AdminStaff.fromJson(data) : null) as T; } - if (t == _i1.getType<_i27.AdminStaffMfaResetResult?>()) { + if (t == _i1.getType<_i28.AdminStaffMfaResetResult?>()) { return (data != null - ? _i27.AdminStaffMfaResetResult.fromJson(data) + ? _i28.AdminStaffMfaResetResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i28.AlertAckResult?>()) { - return (data != null ? _i28.AlertAckResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i29.AlertAckResult?>()) { + return (data != null ? _i29.AlertAckResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i29.AlertResolveResult?>()) { - return (data != null ? _i29.AlertResolveResult.fromJson(data) : null) + if (t == _i1.getType<_i30.AlertResolveResult?>()) { + return (data != null ? _i30.AlertResolveResult.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<_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<_i11.AdminAlert>) { - return (data as List).map((e) => deserialize<_i11.AdminAlert>(e)).toList() + if (t == List<_i12.AdminAlert>) { + return (data as List).map((e) => deserialize<_i12.AdminAlert>(e)).toList() as T; } - if (t == List<_i15.AdminAuditEntry>) { + if (t == List<_i16.AdminAuditEntry>) { return (data as List) - .map((e) => deserialize<_i15.AdminAuditEntry>(e)) + .map((e) => deserialize<_i16.AdminAuditEntry>(e)) .toList() as T; } - if (t == List<_i19.AdminDataSubjectRequest>) { + if (t == List<_i20.AdminDataSubjectRequest>) { return (data as List) - .map((e) => deserialize<_i19.AdminDataSubjectRequest>(e)) + .map((e) => deserialize<_i20.AdminDataSubjectRequest>(e)) .toList() as T; } 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; } @@ -931,44 +940,52 @@ class Protocol extends _i1.SerializationManager { ) 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<_i101.TermsVersionEntry>) { + return (data as List) + .map((e) => deserialize<_i101.TermsVersionEntry>(e)) + .toList() as T; } - if (t == List<_i100.TermsVersionEntry>) { + if (t == List<_i102.AcsOpenAlert>) { return (data as List) - .map((e) => deserialize<_i100.TermsVersionEntry>(e)) + .map((e) => deserialize<_i102.AcsOpenAlert>(e)) .toList() as T; } - if (t == List<_i101.MicroAreaPatient>) { + if (t == List<_i103.MicroAreaPatient>) { return (data as List) - .map((e) => deserialize<_i101.MicroAreaPatient>(e)) + .map((e) => deserialize<_i103.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<_i104.VisitSyncResult>) { return (data as List) - .map((e) => deserialize<_i102.VisitSyncResult>(e)) + .map((e) => deserialize<_i104.VisitSyncResult>(e)) .toList() as T; } - if (t == List<_i103.VisitSyncEntry>) { + if (t == List<_i105.VisitSyncEntry>) { return (data as List) - .map((e) => deserialize<_i103.VisitSyncEntry>(e)) + .map((e) => deserialize<_i105.VisitSyncEntry>(e)) .toList() as T; } @@ -984,95 +1001,96 @@ class Protocol extends _i1.SerializationManager { _i6.AlertDeliveryRecord => 'AlertDeliveryRecord', _i7.AlertIdempotencyKey => 'AlertIdempotencyKey', _i8.AlertOutboxEntry => 'AlertOutboxEntry', - _i9.AdminAcs => 'AdminAcs', - _i10.AdminAcsCreationResult => 'AdminAcsCreationResult', - _i11.AdminAlert => 'AdminAlert', - _i12.AdminAlertPage => 'AdminAlertPage', - _i13.AdminAlertsPulse => 'AdminAlertsPulse', - _i14.AdminAuditChainStatus => 'AdminAuditChainStatus', - _i15.AdminAuditEntry => 'AdminAuditEntry', - _i16.AdminAuditExport => 'AdminAuditExport', - _i17.AdminAuditFilter => 'AdminAuditFilter', - _i18.AdminAuditPage => 'AdminAuditPage', - _i19.AdminDataSubjectRequest => 'AdminDataSubjectRequest', - _i20.AdminDataSubjectRequestDetail => 'AdminDataSubjectRequestDetail', - _i21.AdminDataSubjectRequestPage => 'AdminDataSubjectRequestPage', - _i22.AdminEscalationResult => 'AdminEscalationResult', - _i23.AdminIndicators => 'AdminIndicators', - _i24.AdminMicroArea => 'AdminMicroArea', - _i25.AdminPasswordResetResult => 'AdminPasswordResetResult', - _i26.AdminStaff => 'AdminStaff', - _i27.AdminStaffMfaResetResult => 'AdminStaffMfaResetResult', - _i28.AlertAckResult => 'AlertAckResult', - _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 => + _i9.AcsOpenAlert => 'AcsOpenAlert', + _i10.AdminAcs => 'AdminAcs', + _i11.AdminAcsCreationResult => 'AdminAcsCreationResult', + _i12.AdminAlert => 'AdminAlert', + _i13.AdminAlertPage => 'AdminAlertPage', + _i14.AdminAlertsPulse => 'AdminAlertsPulse', + _i15.AdminAuditChainStatus => 'AdminAuditChainStatus', + _i16.AdminAuditEntry => 'AdminAuditEntry', + _i17.AdminAuditExport => 'AdminAuditExport', + _i18.AdminAuditFilter => 'AdminAuditFilter', + _i19.AdminAuditPage => 'AdminAuditPage', + _i20.AdminDataSubjectRequest => 'AdminDataSubjectRequest', + _i21.AdminDataSubjectRequestDetail => 'AdminDataSubjectRequestDetail', + _i22.AdminDataSubjectRequestPage => 'AdminDataSubjectRequestPage', + _i23.AdminEscalationResult => 'AdminEscalationResult', + _i24.AdminIndicators => 'AdminIndicators', + _i25.AdminMicroArea => 'AdminMicroArea', + _i26.AdminPasswordResetResult => 'AdminPasswordResetResult', + _i27.AdminStaff => 'AdminStaff', + _i28.AdminStaffMfaResetResult => 'AdminStaffMfaResetResult', + _i29.AlertAckResult => 'AlertAckResult', + _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, }; } @@ -1101,181 +1119,183 @@ class Protocol extends _i1.SerializationManager { return 'AlertIdempotencyKey'; case _i8.AlertOutboxEntry(): return 'AlertOutboxEntry'; - case _i9.AdminAcs(): + case _i9.AcsOpenAlert(): + return 'AcsOpenAlert'; + case _i10.AdminAcs(): return 'AdminAcs'; - case _i10.AdminAcsCreationResult(): + case _i11.AdminAcsCreationResult(): return 'AdminAcsCreationResult'; - case _i11.AdminAlert(): + case _i12.AdminAlert(): return 'AdminAlert'; - case _i12.AdminAlertPage(): + case _i13.AdminAlertPage(): return 'AdminAlertPage'; - case _i13.AdminAlertsPulse(): + case _i14.AdminAlertsPulse(): return 'AdminAlertsPulse'; - case _i14.AdminAuditChainStatus(): + case _i15.AdminAuditChainStatus(): return 'AdminAuditChainStatus'; - case _i15.AdminAuditEntry(): + case _i16.AdminAuditEntry(): return 'AdminAuditEntry'; - case _i16.AdminAuditExport(): + case _i17.AdminAuditExport(): return 'AdminAuditExport'; - case _i17.AdminAuditFilter(): + case _i18.AdminAuditFilter(): return 'AdminAuditFilter'; - case _i18.AdminAuditPage(): + case _i19.AdminAuditPage(): return 'AdminAuditPage'; - case _i19.AdminDataSubjectRequest(): + case _i20.AdminDataSubjectRequest(): return 'AdminDataSubjectRequest'; - case _i20.AdminDataSubjectRequestDetail(): + case _i21.AdminDataSubjectRequestDetail(): return 'AdminDataSubjectRequestDetail'; - case _i21.AdminDataSubjectRequestPage(): + case _i22.AdminDataSubjectRequestPage(): return 'AdminDataSubjectRequestPage'; - case _i22.AdminEscalationResult(): + case _i23.AdminEscalationResult(): return 'AdminEscalationResult'; - case _i23.AdminIndicators(): + case _i24.AdminIndicators(): return 'AdminIndicators'; - case _i24.AdminMicroArea(): + case _i25.AdminMicroArea(): return 'AdminMicroArea'; - case _i25.AdminPasswordResetResult(): + case _i26.AdminPasswordResetResult(): return 'AdminPasswordResetResult'; - case _i26.AdminStaff(): + case _i27.AdminStaff(): return 'AdminStaff'; - case _i27.AdminStaffMfaResetResult(): + case _i28.AdminStaffMfaResetResult(): return 'AdminStaffMfaResetResult'; - case _i28.AlertAckResult(): + case _i29.AlertAckResult(): return 'AlertAckResult'; - case _i29.AlertResolveResult(): + case _i30.AlertResolveResult(): return 'AlertResolveResult'; - case _i30.AlertStatusResult(): + 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'; } return null; @@ -1308,269 +1328,272 @@ class Protocol extends _i1.SerializationManager { if (dataClassName == 'AlertOutboxEntry') { return deserialize<_i8.AlertOutboxEntry>(data['data']); } + if (dataClassName == 'AcsOpenAlert') { + return deserialize<_i9.AcsOpenAlert>(data['data']); + } if (dataClassName == 'AdminAcs') { - return deserialize<_i9.AdminAcs>(data['data']); + return deserialize<_i10.AdminAcs>(data['data']); } if (dataClassName == 'AdminAcsCreationResult') { - return deserialize<_i10.AdminAcsCreationResult>(data['data']); + return deserialize<_i11.AdminAcsCreationResult>(data['data']); } if (dataClassName == 'AdminAlert') { - return deserialize<_i11.AdminAlert>(data['data']); + return deserialize<_i12.AdminAlert>(data['data']); } if (dataClassName == 'AdminAlertPage') { - return deserialize<_i12.AdminAlertPage>(data['data']); + return deserialize<_i13.AdminAlertPage>(data['data']); } if (dataClassName == 'AdminAlertsPulse') { - return deserialize<_i13.AdminAlertsPulse>(data['data']); + return deserialize<_i14.AdminAlertsPulse>(data['data']); } if (dataClassName == 'AdminAuditChainStatus') { - return deserialize<_i14.AdminAuditChainStatus>(data['data']); + return deserialize<_i15.AdminAuditChainStatus>(data['data']); } if (dataClassName == 'AdminAuditEntry') { - return deserialize<_i15.AdminAuditEntry>(data['data']); + return deserialize<_i16.AdminAuditEntry>(data['data']); } if (dataClassName == 'AdminAuditExport') { - return deserialize<_i16.AdminAuditExport>(data['data']); + return deserialize<_i17.AdminAuditExport>(data['data']); } if (dataClassName == 'AdminAuditFilter') { - return deserialize<_i17.AdminAuditFilter>(data['data']); + return deserialize<_i18.AdminAuditFilter>(data['data']); } if (dataClassName == 'AdminAuditPage') { - return deserialize<_i18.AdminAuditPage>(data['data']); + return deserialize<_i19.AdminAuditPage>(data['data']); } if (dataClassName == 'AdminDataSubjectRequest') { - return deserialize<_i19.AdminDataSubjectRequest>(data['data']); + return deserialize<_i20.AdminDataSubjectRequest>(data['data']); } if (dataClassName == 'AdminDataSubjectRequestDetail') { - return deserialize<_i20.AdminDataSubjectRequestDetail>(data['data']); + return deserialize<_i21.AdminDataSubjectRequestDetail>(data['data']); } if (dataClassName == 'AdminDataSubjectRequestPage') { - return deserialize<_i21.AdminDataSubjectRequestPage>(data['data']); + return deserialize<_i22.AdminDataSubjectRequestPage>(data['data']); } if (dataClassName == 'AdminEscalationResult') { - return deserialize<_i22.AdminEscalationResult>(data['data']); + return deserialize<_i23.AdminEscalationResult>(data['data']); } if (dataClassName == 'AdminIndicators') { - return deserialize<_i23.AdminIndicators>(data['data']); + return deserialize<_i24.AdminIndicators>(data['data']); } if (dataClassName == 'AdminMicroArea') { - return deserialize<_i24.AdminMicroArea>(data['data']); + return deserialize<_i25.AdminMicroArea>(data['data']); } if (dataClassName == 'AdminPasswordResetResult') { - return deserialize<_i25.AdminPasswordResetResult>(data['data']); + return deserialize<_i26.AdminPasswordResetResult>(data['data']); } if (dataClassName == 'AdminStaff') { - return deserialize<_i26.AdminStaff>(data['data']); + return deserialize<_i27.AdminStaff>(data['data']); } if (dataClassName == 'AdminStaffMfaResetResult') { - return deserialize<_i27.AdminStaffMfaResetResult>(data['data']); + return deserialize<_i28.AdminStaffMfaResetResult>(data['data']); } if (dataClassName == 'AlertAckResult') { - return deserialize<_i28.AlertAckResult>(data['data']); + return deserialize<_i29.AlertAckResult>(data['data']); } if (dataClassName == 'AlertResolveResult') { - return deserialize<_i29.AlertResolveResult>(data['data']); + 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']); } return super.deserializeByClassName(data); } 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 4c1ad6b..5b45332 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 @@ -53,6 +53,12 @@ abstract interface class AlertStore { /// Alerta mais recente do paciente, para RF05 (`alerts.statusFor`). `null` /// quando o paciente nunca disparou um alerta. Future latestForPatient(String patientId); + + /// Alertas não concluídos da microárea (#80, R2): primeiro os não + /// confirmados (`pending` e o `escalated` legado), depois os confirmados, + /// cada grupo do mais antigo ao mais novo, cortado em [limit] — o corte + /// descarta os mais ANTIGOS de cada grupo, nunca o mais novo. + Future> openInMicroArea(String microAreaId, {required int limit}); } /// Fila durável de entregas pendentes. @@ -144,6 +150,38 @@ class AlertStatusSnapshot { final DateTime? resolvedAt; } +/// Alerta aberto da microárea, para a reserva por HTTP do ACS (#80). +/// +/// Mesmos campos de `AcsOpenAlert`: o envelope MQTT mais o estado persistido. +class OpenAlertSnapshot { + const OpenAlertSnapshot({ + required this.alertId, + required this.patientId, + required this.microAreaId, + required this.riskLevel, + required this.locationHash, + this.locationCell, + required this.triggeredAt, + required this.status, + this.acknowledgedAt, + required this.escalated, + }); + + final String alertId; + final String patientId; + final String microAreaId; + final RiskLevel riskLevel; + final String locationHash; + final String? locationCell; + final DateTime triggeredAt; + final AlertStatus status; + final DateTime? acknowledgedAt; + + /// Reforçado pelo backoffice (#44): `escalatedAt` preenchido ou a linha + /// legada com `status: escalated`. + final bool escalated; +} + /// Formato produzido por `locationCellFrom()` (apps/patient/lib/core/privacy/location_cell.dart): /// `":"`, dois inteiros (a célula é `(coordenada / /// cellSizeDegrees).floor()`, então pode ser negativa) separados por `:`. @@ -309,6 +347,23 @@ class RedAlertService { return attempt; } + /// Teto de [openForAcs]: muito acima do que uma microárea tem aberto ao + /// mesmo tempo, e os não confirmados vêm antes, então o corte nunca troca um + /// pendente por um confirmado. + static const int openAlertsLimit = 200; + + /// Alertas abertos da microárea do ACS autenticado (#80, R2): a reserva por + /// HTTP quando o MQTT do aparelho não está conectado. A microárea vem do + /// token (INV-01), nunca de parâmetro. + Future> openForAcs({required AuthenticatedUser user}) async { + Authorization.require( + user, + roles: {UserRole.acs}, + onDenied: () => StateError('Somente ACS territorializados podem listar alertas.'), + ); + return _store.openInMicroArea(user.microAreaId!, limit: openAlertsLimit); + } + /// 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/endpoints/alerts_endpoint.dart b/backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart index 23372c1..a9ad34e 100644 --- a/backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart +++ b/backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart @@ -118,6 +118,40 @@ class AlertsEndpoint extends AuthenticatedEndpoint { } } + /// Alertas não concluídos da microárea do ACS (#80, R2): a reserva por HTTP + /// para quando o MQTT do aparelho não está conectado. A microárea vem do + /// token (INV-01). Não grava linha de auditoria de leitura: o app repete a + /// consulta a cada 30 s com o broker fora, e o mesmo conteúdo chega pelo + /// MQTT sem auditoria; ACK e conclusão seguem auditados. + Future> openForMicroArea( + Session session, { + required String accessToken, + }) async { + final user = authenticate(accessToken); + final service = AlertRuntime.instance.serviceFor(session); + + try { + final abertos = await service.openForAcs(user: user); + return [ + for (final a in abertos) + AcsOpenAlert( + alertId: a.alertId, + patientId: a.patientId, + microAreaId: a.microAreaId, + riskLevel: a.riskLevel, + locationHash: a.locationHash, + locationCell: a.locationCell, + triggeredAt: a.triggeredAt, + status: a.status, + acknowledgedAt: a.acknowledgedAt, + escalated: a.escalated, + ), + ]; + } 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( diff --git a/backend/sinalacs_server/lib/src/generated/api/acs_open_alert.dart b/backend/sinalacs_server/lib/src/generated/api/acs_open_alert.dart new file mode 100644 index 0000000..585724c --- /dev/null +++ b/backend/sinalacs_server/lib/src/generated/api/acs_open_alert.dart @@ -0,0 +1,208 @@ +/* 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/risk_level.dart' as _i2; +import '../enums/alert_status.dart' as _i3; + +/// Alerta ainda não concluído da microárea do ACS, lido por HTTP (#80, R2): +/// a reserva para quando o MQTT do aparelho não está conectado. +/// +/// Os campos espelham o envelope MQTT (`AlertDelivery.toJson`) mais o estado +/// persistido (`status`, `acknowledgedAt`) e o marcador de reforço. Nada de +/// `escalatedBy`, `escalationReason` ou `deviceId`: nenhuma identidade a mais. +abstract class AcsOpenAlert + implements _i1.SerializableModel, _i1.ProtocolSerialization { + AcsOpenAlert._({ + required this.alertId, + required this.patientId, + required this.microAreaId, + required this.riskLevel, + required this.locationHash, + this.locationCell, + required this.triggeredAt, + required this.status, + this.acknowledgedAt, + required this.escalated, + }); + + factory AcsOpenAlert({ + required String alertId, + required String patientId, + required String microAreaId, + required _i2.RiskLevel riskLevel, + required String locationHash, + String? locationCell, + required DateTime triggeredAt, + required _i3.AlertStatus status, + DateTime? acknowledgedAt, + required bool escalated, + }) = _AcsOpenAlertImpl; + + factory AcsOpenAlert.fromJson(Map jsonSerialization) { + return AcsOpenAlert( + alertId: jsonSerialization['alertId'] as String, + patientId: jsonSerialization['patientId'] as String, + microAreaId: jsonSerialization['microAreaId'] as String, + riskLevel: _i2.RiskLevel.fromJson( + (jsonSerialization['riskLevel'] as String), + ), + locationHash: jsonSerialization['locationHash'] as String, + locationCell: jsonSerialization['locationCell'] as String?, + triggeredAt: _i1.DateTimeJsonExtension.fromJson( + jsonSerialization['triggeredAt'], + ), + status: _i3.AlertStatus.fromJson((jsonSerialization['status'] as String)), + acknowledgedAt: jsonSerialization['acknowledgedAt'] == null + ? null + : _i1.DateTimeJsonExtension.fromJson( + jsonSerialization['acknowledgedAt'], + ), + escalated: _i1.BoolJsonExtension.fromJson(jsonSerialization['escalated']), + ); + } + + String alertId; + + String patientId; + + String microAreaId; + + _i2.RiskLevel riskLevel; + + String locationHash; + + String? locationCell; + + DateTime triggeredAt; + + _i3.AlertStatus status; + + DateTime? acknowledgedAt; + + bool escalated; + + /// Returns a shallow copy of this [AcsOpenAlert] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + AcsOpenAlert copyWith({ + String? alertId, + String? patientId, + String? microAreaId, + _i2.RiskLevel? riskLevel, + String? locationHash, + String? locationCell, + DateTime? triggeredAt, + _i3.AlertStatus? status, + DateTime? acknowledgedAt, + bool? escalated, + }); + @override + Map toJson() { + return { + '__className__': 'AcsOpenAlert', + 'alertId': alertId, + 'patientId': patientId, + 'microAreaId': microAreaId, + 'riskLevel': riskLevel.toJson(), + 'locationHash': locationHash, + if (locationCell != null) 'locationCell': locationCell, + 'triggeredAt': triggeredAt.toJson(), + 'status': status.toJson(), + if (acknowledgedAt != null) 'acknowledgedAt': acknowledgedAt?.toJson(), + 'escalated': escalated, + }; + } + + @override + Map toJsonForProtocol() { + return { + '__className__': 'AcsOpenAlert', + 'alertId': alertId, + 'patientId': patientId, + 'microAreaId': microAreaId, + 'riskLevel': riskLevel.toJson(), + 'locationHash': locationHash, + if (locationCell != null) 'locationCell': locationCell, + 'triggeredAt': triggeredAt.toJson(), + 'status': status.toJson(), + if (acknowledgedAt != null) 'acknowledgedAt': acknowledgedAt?.toJson(), + 'escalated': escalated, + }; + } + + @override + String toString() { + return _i1.SerializationManager.encode(this); + } +} + +class _Undefined {} + +class _AcsOpenAlertImpl extends AcsOpenAlert { + _AcsOpenAlertImpl({ + required String alertId, + required String patientId, + required String microAreaId, + required _i2.RiskLevel riskLevel, + required String locationHash, + String? locationCell, + required DateTime triggeredAt, + required _i3.AlertStatus status, + DateTime? acknowledgedAt, + required bool escalated, + }) : super._( + alertId: alertId, + patientId: patientId, + microAreaId: microAreaId, + riskLevel: riskLevel, + locationHash: locationHash, + locationCell: locationCell, + triggeredAt: triggeredAt, + status: status, + acknowledgedAt: acknowledgedAt, + escalated: escalated, + ); + + /// Returns a shallow copy of this [AcsOpenAlert] + /// with some or all fields replaced by the given arguments. + @_i1.useResult + @override + AcsOpenAlert copyWith({ + String? alertId, + String? patientId, + String? microAreaId, + _i2.RiskLevel? riskLevel, + String? locationHash, + Object? locationCell = _Undefined, + DateTime? triggeredAt, + _i3.AlertStatus? status, + Object? acknowledgedAt = _Undefined, + bool? escalated, + }) { + return AcsOpenAlert( + alertId: alertId ?? this.alertId, + patientId: patientId ?? this.patientId, + microAreaId: microAreaId ?? this.microAreaId, + riskLevel: riskLevel ?? this.riskLevel, + locationHash: locationHash ?? this.locationHash, + locationCell: locationCell is String? ? locationCell : this.locationCell, + triggeredAt: triggeredAt ?? this.triggeredAt, + status: status ?? this.status, + acknowledgedAt: acknowledgedAt is DateTime? + ? acknowledgedAt + : this.acknowledgedAt, + escalated: escalated ?? this.escalated, + ); + } +} diff --git a/backend/sinalacs_server/lib/src/generated/endpoints.dart b/backend/sinalacs_server/lib/src/generated/endpoints.dart index f7d32f8..2f40b43 100644 --- a/backend/sinalacs_server/lib/src/generated/endpoints.dart +++ b/backend/sinalacs_server/lib/src/generated/endpoints.dart @@ -1001,6 +1001,25 @@ class Endpoints extends _i1.EndpointDispatch { alertId: params['alertId'], ), ), + 'openForMicroArea': _i1.MethodConnector( + name: 'openForMicroArea', + params: { + 'accessToken': _i1.ParameterDescription( + name: 'accessToken', + type: _i1.getType(), + nullable: false, + ), + }, + call: + ( + _i1.Session session, + Map params, + ) async => + (endpoints['alerts'] as _i3.AlertsEndpoint).openForMicroArea( + session, + accessToken: params['accessToken'], + ), + ), '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 b2e9c24..3ea6fc7 100644 --- a/backend/sinalacs_server/lib/src/generated/protocol.dart +++ b/backend/sinalacs_server/lib/src/generated/protocol.dart @@ -20,106 +20,108 @@ import 'alert.dart' as _i6; import 'alert_delivery_record.dart' as _i7; import 'alert_idempotency_key.dart' as _i8; import 'alert_outbox_entry.dart' as _i9; -import 'api/admin_acs.dart' as _i10; -import 'api/admin_acs_creation_result.dart' as _i11; -import 'api/admin_alert.dart' as _i12; -import 'api/admin_alert_page.dart' as _i13; -import 'api/admin_alerts_pulse.dart' as _i14; -import 'api/admin_audit_chain_status.dart' as _i15; -import 'api/admin_audit_entry.dart' as _i16; -import 'api/admin_audit_export.dart' as _i17; -import 'api/admin_audit_filter.dart' as _i18; -import 'api/admin_audit_page.dart' as _i19; -import 'api/admin_data_subject_request.dart' as _i20; -import 'api/admin_data_subject_request_detail.dart' as _i21; -import 'api/admin_data_subject_request_page.dart' as _i22; -import 'api/admin_escalation_result.dart' as _i23; -import 'api/admin_indicators.dart' as _i24; -import 'api/admin_micro_area.dart' as _i25; -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_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 'api/acs_open_alert.dart' as _i10; +import 'api/admin_acs.dart' as _i11; +import 'api/admin_acs_creation_result.dart' as _i12; +import 'api/admin_alert.dart' as _i13; +import 'api/admin_alert_page.dart' as _i14; +import 'api/admin_alerts_pulse.dart' as _i15; +import 'api/admin_audit_chain_status.dart' as _i16; +import 'api/admin_audit_entry.dart' as _i17; +import 'api/admin_audit_export.dart' as _i18; +import 'api/admin_audit_filter.dart' as _i19; +import 'api/admin_audit_page.dart' as _i20; +import 'api/admin_data_subject_request.dart' as _i21; +import 'api/admin_data_subject_request_detail.dart' as _i22; +import 'api/admin_data_subject_request_page.dart' as _i23; +import 'api/admin_escalation_result.dart' as _i24; +import 'api/admin_indicators.dart' as _i25; +import 'api/admin_micro_area.dart' as _i26; +import 'api/admin_password_reset_result.dart' as _i27; +import 'api/admin_staff.dart' as _i28; +import 'api/admin_staff_mfa_reset_result.dart' as _i29; +import 'api/alert_ack_result.dart' as _i30; +import 'api/alert_resolve_result.dart' as _i31; +import 'api/alert_status_result.dart' as _i32; +import 'api/development_login_result.dart' as _i33; +import 'api/enrollment_result.dart' as _i34; +import 'api/enrollment_token_result.dart' as _i35; +import 'api/micro_area_patient.dart' as _i36; +import 'api/notice_history_entry.dart' as _i37; +import 'api/notice_history_page.dart' as _i38; +import 'api/notice_send_result.dart' as _i39; +import 'api/patient_consent_record.dart' as _i40; +import 'api/patient_data_overview.dart' as _i41; +import 'api/patient_data_subject_request_record.dart' as _i42; +import 'api/patient_risk_event.dart' as _i43; +import 'api/red_alert_result.dart' as _i44; +import 'api/service_health.dart' as _i45; +import 'api/terms_change_notice.dart' as _i46; +import 'api/terms_version_entry.dart' as _i47; +import 'api/totp_enrollment_start.dart' as _i48; +import 'api/triage_result.dart' as _i49; +import 'api/ubs_contact.dart' as _i50; +import 'api/visit_sync_entry.dart' as _i51; +import 'api/visit_sync_result.dart' as _i52; +import 'audit_chain_checkpoint.dart' as _i53; +import 'audit_export.dart' as _i54; +import 'audit_log.dart' as _i55; +import 'consent_log.dart' as _i56; +import 'data_subject_request.dart' as _i57; +import 'enrollment_token.dart' as _i58; +import 'enums/alert_status.dart' as _i59; +import 'enums/arrival_method.dart' as _i60; +import 'enums/audit_chain_reason.dart' as _i61; +import 'enums/audit_integrity_state.dart' as _i62; +import 'enums/audit_row_seal.dart' as _i63; +import 'enums/consent_purpose.dart' as _i64; +import 'enums/data_subject_request_status.dart' as _i65; +import 'enums/data_subject_request_type.dart' as _i66; +import 'enums/escalation_reason.dart' as _i67; +import 'enums/risk_level.dart' as _i68; +import 'enums/scheduled_notice_status.dart' as _i69; +import 'enums/sync_status.dart' as _i70; +import 'enums/user_role.dart' as _i71; +import 'enums/visit_authorship.dart' as _i72; +import 'exceptions/admin_invalid_request_exception.dart' as _i73; +import 'exceptions/alert_dispatch_unavailable_exception.dart' as _i74; +import 'exceptions/alert_permission_exception.dart' as _i75; +import 'exceptions/alert_validation_exception.dart' as _i76; +import 'exceptions/authentication_failed_exception.dart' as _i77; +import 'exceptions/data_rights_exception.dart' as _i78; +import 'exceptions/endpoint_disabled_exception.dart' as _i79; +import 'exceptions/enrollment_exception.dart' as _i80; +import 'exceptions/mfa_enrollment_required_exception.dart' as _i81; +import 'exceptions/mfa_required_exception.dart' as _i82; +import 'exceptions/notice_delivery_exception.dart' as _i83; +import 'exceptions/otp_request_exception.dart' as _i84; +import 'exceptions/session_expired_exception.dart' as _i85; +import 'micro_area.dart' as _i86; +import 'otp_challenge.dart' as _i87; +import 'patient.dart' as _i88; +import 'push_token.dart' as _i89; +import 'scheduled_notice.dart' as _i90; +import 'staff_account.dart' as _i91; +import 'terms_version.dart' as _i92; +import 'triage_answer.dart' as _i93; +import 'triage_session.dart' as _i94; +import 'ubs.dart' as _i95; +import 'user.dart' as _i96; +import 'user_credential.dart' as _i97; +import 'visit.dart' as _i98; import 'package:sinalacs_server/src/generated/api/admin_micro_area.dart' - 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; + as _i99; +import 'package:sinalacs_server/src/generated/api/admin_acs.dart' as _i100; +import 'package:sinalacs_server/src/generated/api/admin_staff.dart' as _i101; import 'package:sinalacs_server/src/generated/api/terms_version_entry.dart' - as _i101; -import 'package:sinalacs_server/src/generated/api/micro_area_patient.dart' as _i102; +import 'package:sinalacs_server/src/generated/api/acs_open_alert.dart' as _i103; +import 'package:sinalacs_server/src/generated/api/micro_area_patient.dart' + as _i104; import 'package:sinalacs_server/src/generated/api/visit_sync_result.dart' - as _i103; + as _i105; import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' - as _i104; + as _i106; export 'acs.dart'; export 'acs_refresh_token.dart'; export 'acs_upload_token.dart'; @@ -127,6 +129,7 @@ export 'alert.dart'; export 'alert_delivery_record.dart'; export 'alert_idempotency_key.dart'; export 'alert_outbox_entry.dart'; +export 'api/acs_open_alert.dart'; export 'api/admin_acs.dart'; export 'api/admin_acs_creation_result.dart'; export 'api/admin_alert.dart'; @@ -3030,269 +3033,272 @@ class Protocol extends _i1.SerializationManagerServer { if (t == _i9.AlertOutboxEntry) { return _i9.AlertOutboxEntry.fromJson(data) as T; } - if (t == _i10.AdminAcs) { - return _i10.AdminAcs.fromJson(data) as T; + if (t == _i10.AcsOpenAlert) { + return _i10.AcsOpenAlert.fromJson(data) as T; + } + if (t == _i11.AdminAcs) { + return _i11.AdminAcs.fromJson(data) as T; } - if (t == _i11.AdminAcsCreationResult) { - return _i11.AdminAcsCreationResult.fromJson(data) as T; + if (t == _i12.AdminAcsCreationResult) { + return _i12.AdminAcsCreationResult.fromJson(data) as T; } - if (t == _i12.AdminAlert) { - return _i12.AdminAlert.fromJson(data) as T; + if (t == _i13.AdminAlert) { + return _i13.AdminAlert.fromJson(data) as T; } - if (t == _i13.AdminAlertPage) { - return _i13.AdminAlertPage.fromJson(data) as T; + if (t == _i14.AdminAlertPage) { + return _i14.AdminAlertPage.fromJson(data) as T; } - if (t == _i14.AdminAlertsPulse) { - return _i14.AdminAlertsPulse.fromJson(data) as T; + if (t == _i15.AdminAlertsPulse) { + return _i15.AdminAlertsPulse.fromJson(data) as T; } - if (t == _i15.AdminAuditChainStatus) { - return _i15.AdminAuditChainStatus.fromJson(data) as T; + if (t == _i16.AdminAuditChainStatus) { + return _i16.AdminAuditChainStatus.fromJson(data) as T; } - if (t == _i16.AdminAuditEntry) { - return _i16.AdminAuditEntry.fromJson(data) as T; + if (t == _i17.AdminAuditEntry) { + return _i17.AdminAuditEntry.fromJson(data) as T; } - if (t == _i17.AdminAuditExport) { - return _i17.AdminAuditExport.fromJson(data) as T; + if (t == _i18.AdminAuditExport) { + return _i18.AdminAuditExport.fromJson(data) as T; } - if (t == _i18.AdminAuditFilter) { - return _i18.AdminAuditFilter.fromJson(data) as T; + if (t == _i19.AdminAuditFilter) { + return _i19.AdminAuditFilter.fromJson(data) as T; } - if (t == _i19.AdminAuditPage) { - return _i19.AdminAuditPage.fromJson(data) as T; + if (t == _i20.AdminAuditPage) { + return _i20.AdminAuditPage.fromJson(data) as T; } - if (t == _i20.AdminDataSubjectRequest) { - return _i20.AdminDataSubjectRequest.fromJson(data) as T; + if (t == _i21.AdminDataSubjectRequest) { + return _i21.AdminDataSubjectRequest.fromJson(data) as T; } - if (t == _i21.AdminDataSubjectRequestDetail) { - return _i21.AdminDataSubjectRequestDetail.fromJson(data) as T; + if (t == _i22.AdminDataSubjectRequestDetail) { + return _i22.AdminDataSubjectRequestDetail.fromJson(data) as T; } - if (t == _i22.AdminDataSubjectRequestPage) { - return _i22.AdminDataSubjectRequestPage.fromJson(data) as T; + if (t == _i23.AdminDataSubjectRequestPage) { + return _i23.AdminDataSubjectRequestPage.fromJson(data) as T; } - if (t == _i23.AdminEscalationResult) { - return _i23.AdminEscalationResult.fromJson(data) as T; + if (t == _i24.AdminEscalationResult) { + return _i24.AdminEscalationResult.fromJson(data) as T; } - if (t == _i24.AdminIndicators) { - return _i24.AdminIndicators.fromJson(data) as T; + if (t == _i25.AdminIndicators) { + return _i25.AdminIndicators.fromJson(data) as T; } - if (t == _i25.AdminMicroArea) { - return _i25.AdminMicroArea.fromJson(data) as T; + if (t == _i26.AdminMicroArea) { + return _i26.AdminMicroArea.fromJson(data) as T; } - if (t == _i26.AdminPasswordResetResult) { - return _i26.AdminPasswordResetResult.fromJson(data) as T; + if (t == _i27.AdminPasswordResetResult) { + return _i27.AdminPasswordResetResult.fromJson(data) as T; } - if (t == _i27.AdminStaff) { - return _i27.AdminStaff.fromJson(data) as T; + if (t == _i28.AdminStaff) { + return _i28.AdminStaff.fromJson(data) as T; } - if (t == _i28.AdminStaffMfaResetResult) { - return _i28.AdminStaffMfaResetResult.fromJson(data) as T; + if (t == _i29.AdminStaffMfaResetResult) { + return _i29.AdminStaffMfaResetResult.fromJson(data) as T; } - if (t == _i29.AlertAckResult) { - return _i29.AlertAckResult.fromJson(data) as T; + if (t == _i30.AlertAckResult) { + return _i30.AlertAckResult.fromJson(data) as T; } - if (t == _i30.AlertResolveResult) { - return _i30.AlertResolveResult.fromJson(data) as T; + if (t == _i31.AlertResolveResult) { + return _i31.AlertResolveResult.fromJson(data) as T; } - if (t == _i31.AlertStatusResult) { - return _i31.AlertStatusResult.fromJson(data) as T; + if (t == _i32.AlertStatusResult) { + return _i32.AlertStatusResult.fromJson(data) as T; } - if (t == _i32.DevelopmentLoginResult) { - return _i32.DevelopmentLoginResult.fromJson(data) as T; + if (t == _i33.DevelopmentLoginResult) { + return _i33.DevelopmentLoginResult.fromJson(data) as T; } - if (t == _i33.EnrollmentResult) { - return _i33.EnrollmentResult.fromJson(data) as T; + if (t == _i34.EnrollmentResult) { + return _i34.EnrollmentResult.fromJson(data) as T; } - if (t == _i34.EnrollmentTokenResult) { - return _i34.EnrollmentTokenResult.fromJson(data) as T; + if (t == _i35.EnrollmentTokenResult) { + return _i35.EnrollmentTokenResult.fromJson(data) as T; } - if (t == _i35.MicroAreaPatient) { - return _i35.MicroAreaPatient.fromJson(data) as T; + if (t == _i36.MicroAreaPatient) { + return _i36.MicroAreaPatient.fromJson(data) as T; } - if (t == _i36.NoticeHistoryEntry) { - return _i36.NoticeHistoryEntry.fromJson(data) as T; + if (t == _i37.NoticeHistoryEntry) { + return _i37.NoticeHistoryEntry.fromJson(data) as T; } - if (t == _i37.NoticeHistoryPage) { - return _i37.NoticeHistoryPage.fromJson(data) as T; + if (t == _i38.NoticeHistoryPage) { + return _i38.NoticeHistoryPage.fromJson(data) as T; } - if (t == _i38.NoticeSendResult) { - return _i38.NoticeSendResult.fromJson(data) as T; + if (t == _i39.NoticeSendResult) { + return _i39.NoticeSendResult.fromJson(data) as T; } - if (t == _i39.PatientConsentRecord) { - return _i39.PatientConsentRecord.fromJson(data) as T; + if (t == _i40.PatientConsentRecord) { + return _i40.PatientConsentRecord.fromJson(data) as T; } - if (t == _i40.PatientDataOverview) { - return _i40.PatientDataOverview.fromJson(data) as T; + if (t == _i41.PatientDataOverview) { + return _i41.PatientDataOverview.fromJson(data) as T; } - if (t == _i41.PatientDataSubjectRequestRecord) { - return _i41.PatientDataSubjectRequestRecord.fromJson(data) as T; + if (t == _i42.PatientDataSubjectRequestRecord) { + return _i42.PatientDataSubjectRequestRecord.fromJson(data) as T; } - if (t == _i42.PatientRiskEvent) { - return _i42.PatientRiskEvent.fromJson(data) as T; + if (t == _i43.PatientRiskEvent) { + return _i43.PatientRiskEvent.fromJson(data) as T; } - if (t == _i43.RedAlertResult) { - return _i43.RedAlertResult.fromJson(data) as T; + if (t == _i44.RedAlertResult) { + return _i44.RedAlertResult.fromJson(data) as T; } - if (t == _i44.ServiceHealth) { - return _i44.ServiceHealth.fromJson(data) as T; + if (t == _i45.ServiceHealth) { + return _i45.ServiceHealth.fromJson(data) as T; } - if (t == _i45.TermsChangeNotice) { - return _i45.TermsChangeNotice.fromJson(data) as T; + if (t == _i46.TermsChangeNotice) { + return _i46.TermsChangeNotice.fromJson(data) as T; } - if (t == _i46.TermsVersionEntry) { - return _i46.TermsVersionEntry.fromJson(data) as T; + if (t == _i47.TermsVersionEntry) { + return _i47.TermsVersionEntry.fromJson(data) as T; } - if (t == _i47.TotpEnrollmentStart) { - return _i47.TotpEnrollmentStart.fromJson(data) as T; + if (t == _i48.TotpEnrollmentStart) { + return _i48.TotpEnrollmentStart.fromJson(data) as T; } - if (t == _i48.TriageResult) { - return _i48.TriageResult.fromJson(data) as T; + if (t == _i49.TriageResult) { + return _i49.TriageResult.fromJson(data) as T; } - if (t == _i49.UbsContact) { - return _i49.UbsContact.fromJson(data) as T; + if (t == _i50.UbsContact) { + return _i50.UbsContact.fromJson(data) as T; } - if (t == _i50.VisitSyncEntry) { - return _i50.VisitSyncEntry.fromJson(data) as T; + if (t == _i51.VisitSyncEntry) { + return _i51.VisitSyncEntry.fromJson(data) as T; } - if (t == _i51.VisitSyncResult) { - return _i51.VisitSyncResult.fromJson(data) as T; + if (t == _i52.VisitSyncResult) { + return _i52.VisitSyncResult.fromJson(data) as T; } - if (t == _i52.AuditChainCheckpoint) { - return _i52.AuditChainCheckpoint.fromJson(data) as T; + if (t == _i53.AuditChainCheckpoint) { + return _i53.AuditChainCheckpoint.fromJson(data) as T; } - if (t == _i53.AuditExport) { - return _i53.AuditExport.fromJson(data) as T; + if (t == _i54.AuditExport) { + return _i54.AuditExport.fromJson(data) as T; } - if (t == _i54.AuditLog) { - return _i54.AuditLog.fromJson(data) as T; + if (t == _i55.AuditLog) { + return _i55.AuditLog.fromJson(data) as T; } - if (t == _i55.ConsentLog) { - return _i55.ConsentLog.fromJson(data) as T; + if (t == _i56.ConsentLog) { + return _i56.ConsentLog.fromJson(data) as T; } - if (t == _i56.DataSubjectRequest) { - return _i56.DataSubjectRequest.fromJson(data) as T; + if (t == _i57.DataSubjectRequest) { + return _i57.DataSubjectRequest.fromJson(data) as T; } - if (t == _i57.EnrollmentToken) { - return _i57.EnrollmentToken.fromJson(data) as T; + if (t == _i58.EnrollmentToken) { + return _i58.EnrollmentToken.fromJson(data) as T; } - if (t == _i58.AlertStatus) { - return _i58.AlertStatus.fromJson(data) as T; + if (t == _i59.AlertStatus) { + return _i59.AlertStatus.fromJson(data) as T; } - if (t == _i59.ArrivalMethod) { - return _i59.ArrivalMethod.fromJson(data) as T; + if (t == _i60.ArrivalMethod) { + return _i60.ArrivalMethod.fromJson(data) as T; } - if (t == _i60.AuditChainReason) { - return _i60.AuditChainReason.fromJson(data) as T; + if (t == _i61.AuditChainReason) { + return _i61.AuditChainReason.fromJson(data) as T; } - if (t == _i61.AuditIntegrityState) { - return _i61.AuditIntegrityState.fromJson(data) as T; + if (t == _i62.AuditIntegrityState) { + return _i62.AuditIntegrityState.fromJson(data) as T; } - if (t == _i62.AuditRowSeal) { - return _i62.AuditRowSeal.fromJson(data) as T; + if (t == _i63.AuditRowSeal) { + return _i63.AuditRowSeal.fromJson(data) as T; } - if (t == _i63.ConsentPurpose) { - return _i63.ConsentPurpose.fromJson(data) as T; + if (t == _i64.ConsentPurpose) { + return _i64.ConsentPurpose.fromJson(data) as T; } - if (t == _i64.DataSubjectRequestStatus) { - return _i64.DataSubjectRequestStatus.fromJson(data) as T; + if (t == _i65.DataSubjectRequestStatus) { + return _i65.DataSubjectRequestStatus.fromJson(data) as T; } - if (t == _i65.DataSubjectRequestType) { - return _i65.DataSubjectRequestType.fromJson(data) as T; + if (t == _i66.DataSubjectRequestType) { + return _i66.DataSubjectRequestType.fromJson(data) as T; } - if (t == _i66.EscalationReason) { - return _i66.EscalationReason.fromJson(data) as T; + if (t == _i67.EscalationReason) { + return _i67.EscalationReason.fromJson(data) as T; } - if (t == _i67.RiskLevel) { - return _i67.RiskLevel.fromJson(data) as T; + if (t == _i68.RiskLevel) { + return _i68.RiskLevel.fromJson(data) as T; } - if (t == _i68.ScheduledNoticeStatus) { - return _i68.ScheduledNoticeStatus.fromJson(data) as T; + if (t == _i69.ScheduledNoticeStatus) { + return _i69.ScheduledNoticeStatus.fromJson(data) as T; } - if (t == _i69.SyncStatus) { - return _i69.SyncStatus.fromJson(data) as T; + if (t == _i70.SyncStatus) { + return _i70.SyncStatus.fromJson(data) as T; } - if (t == _i70.UserRole) { - return _i70.UserRole.fromJson(data) as T; + if (t == _i71.UserRole) { + return _i71.UserRole.fromJson(data) as T; } - if (t == _i71.VisitAuthorship) { - return _i71.VisitAuthorship.fromJson(data) as T; + if (t == _i72.VisitAuthorship) { + return _i72.VisitAuthorship.fromJson(data) as T; } - if (t == _i72.AdminInvalidRequestException) { - return _i72.AdminInvalidRequestException.fromJson(data) as T; + if (t == _i73.AdminInvalidRequestException) { + return _i73.AdminInvalidRequestException.fromJson(data) as T; } - if (t == _i73.AlertDispatchUnavailableException) { - return _i73.AlertDispatchUnavailableException.fromJson(data) as T; + if (t == _i74.AlertDispatchUnavailableException) { + return _i74.AlertDispatchUnavailableException.fromJson(data) as T; } - if (t == _i74.AlertPermissionException) { - return _i74.AlertPermissionException.fromJson(data) as T; + if (t == _i75.AlertPermissionException) { + return _i75.AlertPermissionException.fromJson(data) as T; } - if (t == _i75.AlertValidationException) { - return _i75.AlertValidationException.fromJson(data) as T; + if (t == _i76.AlertValidationException) { + return _i76.AlertValidationException.fromJson(data) as T; } - if (t == _i76.AuthenticationFailedException) { - return _i76.AuthenticationFailedException.fromJson(data) as T; + if (t == _i77.AuthenticationFailedException) { + return _i77.AuthenticationFailedException.fromJson(data) as T; } - if (t == _i77.DataRightsException) { - return _i77.DataRightsException.fromJson(data) as T; + if (t == _i78.DataRightsException) { + return _i78.DataRightsException.fromJson(data) as T; } - if (t == _i78.EndpointDisabledException) { - return _i78.EndpointDisabledException.fromJson(data) as T; + if (t == _i79.EndpointDisabledException) { + return _i79.EndpointDisabledException.fromJson(data) as T; } - if (t == _i79.EnrollmentException) { - return _i79.EnrollmentException.fromJson(data) as T; + if (t == _i80.EnrollmentException) { + return _i80.EnrollmentException.fromJson(data) as T; } - if (t == _i80.MfaEnrollmentRequiredException) { - return _i80.MfaEnrollmentRequiredException.fromJson(data) as T; + if (t == _i81.MfaEnrollmentRequiredException) { + return _i81.MfaEnrollmentRequiredException.fromJson(data) as T; } - if (t == _i81.MfaRequiredException) { - return _i81.MfaRequiredException.fromJson(data) as T; + if (t == _i82.MfaRequiredException) { + return _i82.MfaRequiredException.fromJson(data) as T; } - if (t == _i82.NoticeDeliveryException) { - return _i82.NoticeDeliveryException.fromJson(data) as T; + if (t == _i83.NoticeDeliveryException) { + return _i83.NoticeDeliveryException.fromJson(data) as T; } - if (t == _i83.OtpRequestException) { - return _i83.OtpRequestException.fromJson(data) as T; + if (t == _i84.OtpRequestException) { + return _i84.OtpRequestException.fromJson(data) as T; } - if (t == _i84.SessionExpiredException) { - return _i84.SessionExpiredException.fromJson(data) as T; + if (t == _i85.SessionExpiredException) { + return _i85.SessionExpiredException.fromJson(data) as T; } - if (t == _i85.MicroArea) { - return _i85.MicroArea.fromJson(data) as T; + if (t == _i86.MicroArea) { + return _i86.MicroArea.fromJson(data) as T; } - if (t == _i86.OtpChallenge) { - return _i86.OtpChallenge.fromJson(data) as T; + if (t == _i87.OtpChallenge) { + return _i87.OtpChallenge.fromJson(data) as T; } - if (t == _i87.Patient) { - return _i87.Patient.fromJson(data) as T; + if (t == _i88.Patient) { + return _i88.Patient.fromJson(data) as T; } - if (t == _i88.PushToken) { - return _i88.PushToken.fromJson(data) as T; + if (t == _i89.PushToken) { + return _i89.PushToken.fromJson(data) as T; } - if (t == _i89.ScheduledNotice) { - return _i89.ScheduledNotice.fromJson(data) as T; + if (t == _i90.ScheduledNotice) { + return _i90.ScheduledNotice.fromJson(data) as T; } - if (t == _i90.StaffAccount) { - return _i90.StaffAccount.fromJson(data) as T; + if (t == _i91.StaffAccount) { + return _i91.StaffAccount.fromJson(data) as T; } - if (t == _i91.TermsVersion) { - return _i91.TermsVersion.fromJson(data) as T; + if (t == _i92.TermsVersion) { + return _i92.TermsVersion.fromJson(data) as T; } - if (t == _i92.TriageAnswer) { - return _i92.TriageAnswer.fromJson(data) as T; + if (t == _i93.TriageAnswer) { + return _i93.TriageAnswer.fromJson(data) as T; } - if (t == _i93.TriageSession) { - return _i93.TriageSession.fromJson(data) as T; + if (t == _i94.TriageSession) { + return _i94.TriageSession.fromJson(data) as T; } - if (t == _i94.Ubs) { - return _i94.Ubs.fromJson(data) as T; + if (t == _i95.Ubs) { + return _i95.Ubs.fromJson(data) as T; } - if (t == _i95.User) { - return _i95.User.fromJson(data) as T; + if (t == _i96.User) { + return _i96.User.fromJson(data) as T; } - if (t == _i96.UserCredential) { - return _i96.UserCredential.fromJson(data) as T; + if (t == _i97.UserCredential) { + return _i97.UserCredential.fromJson(data) as T; } - if (t == _i97.Visit) { - return _i97.Visit.fromJson(data) as T; + if (t == _i98.Visit) { + return _i98.Visit.fromJson(data) as T; } if (t == _i1.getType<_i3.Acs?>()) { return (data != null ? _i3.Acs.fromJson(data) : null) as T; @@ -3317,371 +3323,374 @@ class Protocol extends _i1.SerializationManagerServer { if (t == _i1.getType<_i9.AlertOutboxEntry?>()) { return (data != null ? _i9.AlertOutboxEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i10.AdminAcs?>()) { - return (data != null ? _i10.AdminAcs.fromJson(data) : null) as T; + if (t == _i1.getType<_i10.AcsOpenAlert?>()) { + return (data != null ? _i10.AcsOpenAlert.fromJson(data) : null) as T; + } + if (t == _i1.getType<_i11.AdminAcs?>()) { + return (data != null ? _i11.AdminAcs.fromJson(data) : null) as T; } - if (t == _i1.getType<_i11.AdminAcsCreationResult?>()) { - return (data != null ? _i11.AdminAcsCreationResult.fromJson(data) : null) + if (t == _i1.getType<_i12.AdminAcsCreationResult?>()) { + return (data != null ? _i12.AdminAcsCreationResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i12.AdminAlert?>()) { - return (data != null ? _i12.AdminAlert.fromJson(data) : null) as T; + if (t == _i1.getType<_i13.AdminAlert?>()) { + return (data != null ? _i13.AdminAlert.fromJson(data) : null) as T; } - if (t == _i1.getType<_i13.AdminAlertPage?>()) { - return (data != null ? _i13.AdminAlertPage.fromJson(data) : null) as T; + if (t == _i1.getType<_i14.AdminAlertPage?>()) { + return (data != null ? _i14.AdminAlertPage.fromJson(data) : null) as T; } - if (t == _i1.getType<_i14.AdminAlertsPulse?>()) { - return (data != null ? _i14.AdminAlertsPulse.fromJson(data) : null) as T; + if (t == _i1.getType<_i15.AdminAlertsPulse?>()) { + return (data != null ? _i15.AdminAlertsPulse.fromJson(data) : null) as T; } - if (t == _i1.getType<_i15.AdminAuditChainStatus?>()) { - return (data != null ? _i15.AdminAuditChainStatus.fromJson(data) : null) + if (t == _i1.getType<_i16.AdminAuditChainStatus?>()) { + return (data != null ? _i16.AdminAuditChainStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i16.AdminAuditEntry?>()) { - return (data != null ? _i16.AdminAuditEntry.fromJson(data) : null) as T; + if (t == _i1.getType<_i17.AdminAuditEntry?>()) { + return (data != null ? _i17.AdminAuditEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i17.AdminAuditExport?>()) { - return (data != null ? _i17.AdminAuditExport.fromJson(data) : null) as T; + if (t == _i1.getType<_i18.AdminAuditExport?>()) { + return (data != null ? _i18.AdminAuditExport.fromJson(data) : null) as T; } - if (t == _i1.getType<_i18.AdminAuditFilter?>()) { - return (data != null ? _i18.AdminAuditFilter.fromJson(data) : null) as T; + if (t == _i1.getType<_i19.AdminAuditFilter?>()) { + return (data != null ? _i19.AdminAuditFilter.fromJson(data) : null) as T; } - if (t == _i1.getType<_i19.AdminAuditPage?>()) { - return (data != null ? _i19.AdminAuditPage.fromJson(data) : null) as T; + if (t == _i1.getType<_i20.AdminAuditPage?>()) { + return (data != null ? _i20.AdminAuditPage.fromJson(data) : null) as T; } - if (t == _i1.getType<_i20.AdminDataSubjectRequest?>()) { - return (data != null ? _i20.AdminDataSubjectRequest.fromJson(data) : null) + if (t == _i1.getType<_i21.AdminDataSubjectRequest?>()) { + return (data != null ? _i21.AdminDataSubjectRequest.fromJson(data) : null) as T; } - if (t == _i1.getType<_i21.AdminDataSubjectRequestDetail?>()) { + if (t == _i1.getType<_i22.AdminDataSubjectRequestDetail?>()) { return (data != null - ? _i21.AdminDataSubjectRequestDetail.fromJson(data) + ? _i22.AdminDataSubjectRequestDetail.fromJson(data) : null) as T; } - if (t == _i1.getType<_i22.AdminDataSubjectRequestPage?>()) { + if (t == _i1.getType<_i23.AdminDataSubjectRequestPage?>()) { return (data != null - ? _i22.AdminDataSubjectRequestPage.fromJson(data) + ? _i23.AdminDataSubjectRequestPage.fromJson(data) : null) as T; } - if (t == _i1.getType<_i23.AdminEscalationResult?>()) { - return (data != null ? _i23.AdminEscalationResult.fromJson(data) : null) + if (t == _i1.getType<_i24.AdminEscalationResult?>()) { + return (data != null ? _i24.AdminEscalationResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i24.AdminIndicators?>()) { - return (data != null ? _i24.AdminIndicators.fromJson(data) : null) as T; + if (t == _i1.getType<_i25.AdminIndicators?>()) { + return (data != null ? _i25.AdminIndicators.fromJson(data) : null) as T; } - if (t == _i1.getType<_i25.AdminMicroArea?>()) { - return (data != null ? _i25.AdminMicroArea.fromJson(data) : null) as T; + if (t == _i1.getType<_i26.AdminMicroArea?>()) { + return (data != null ? _i26.AdminMicroArea.fromJson(data) : null) as T; } - if (t == _i1.getType<_i26.AdminPasswordResetResult?>()) { + if (t == _i1.getType<_i27.AdminPasswordResetResult?>()) { return (data != null - ? _i26.AdminPasswordResetResult.fromJson(data) + ? _i27.AdminPasswordResetResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i27.AdminStaff?>()) { - return (data != null ? _i27.AdminStaff.fromJson(data) : null) as T; + if (t == _i1.getType<_i28.AdminStaff?>()) { + return (data != null ? _i28.AdminStaff.fromJson(data) : null) as T; } - if (t == _i1.getType<_i28.AdminStaffMfaResetResult?>()) { + if (t == _i1.getType<_i29.AdminStaffMfaResetResult?>()) { return (data != null - ? _i28.AdminStaffMfaResetResult.fromJson(data) + ? _i29.AdminStaffMfaResetResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i29.AlertAckResult?>()) { - return (data != null ? _i29.AlertAckResult.fromJson(data) : null) as T; + if (t == _i1.getType<_i30.AlertAckResult?>()) { + return (data != null ? _i30.AlertAckResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i30.AlertResolveResult?>()) { - return (data != null ? _i30.AlertResolveResult.fromJson(data) : null) + if (t == _i1.getType<_i31.AlertResolveResult?>()) { + return (data != null ? _i31.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<_i32.AlertStatusResult?>()) { + return (data != null ? _i32.AlertStatusResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i32.DevelopmentLoginResult?>()) { - return (data != null ? _i32.DevelopmentLoginResult.fromJson(data) : null) + if (t == _i1.getType<_i33.DevelopmentLoginResult?>()) { + return (data != null ? _i33.DevelopmentLoginResult.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<_i34.EnrollmentResult?>()) { + return (data != null ? _i34.EnrollmentResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i34.EnrollmentTokenResult?>()) { - return (data != null ? _i34.EnrollmentTokenResult.fromJson(data) : null) + if (t == _i1.getType<_i35.EnrollmentTokenResult?>()) { + return (data != null ? _i35.EnrollmentTokenResult.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<_i36.MicroAreaPatient?>()) { + return (data != null ? _i36.MicroAreaPatient.fromJson(data) : null) as T; } - if (t == _i1.getType<_i36.NoticeHistoryEntry?>()) { - return (data != null ? _i36.NoticeHistoryEntry.fromJson(data) : null) + if (t == _i1.getType<_i37.NoticeHistoryEntry?>()) { + return (data != null ? _i37.NoticeHistoryEntry.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<_i38.NoticeHistoryPage?>()) { + return (data != null ? _i38.NoticeHistoryPage.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<_i39.NoticeSendResult?>()) { + return (data != null ? _i39.NoticeSendResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i39.PatientConsentRecord?>()) { - return (data != null ? _i39.PatientConsentRecord.fromJson(data) : null) + if (t == _i1.getType<_i40.PatientConsentRecord?>()) { + return (data != null ? _i40.PatientConsentRecord.fromJson(data) : null) as T; } - if (t == _i1.getType<_i40.PatientDataOverview?>()) { - return (data != null ? _i40.PatientDataOverview.fromJson(data) : null) + if (t == _i1.getType<_i41.PatientDataOverview?>()) { + return (data != null ? _i41.PatientDataOverview.fromJson(data) : null) as T; } - if (t == _i1.getType<_i41.PatientDataSubjectRequestRecord?>()) { + if (t == _i1.getType<_i42.PatientDataSubjectRequestRecord?>()) { return (data != null - ? _i41.PatientDataSubjectRequestRecord.fromJson(data) + ? _i42.PatientDataSubjectRequestRecord.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<_i43.PatientRiskEvent?>()) { + return (data != null ? _i43.PatientRiskEvent.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<_i44.RedAlertResult?>()) { + return (data != null ? _i44.RedAlertResult.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<_i45.ServiceHealth?>()) { + return (data != null ? _i45.ServiceHealth.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<_i46.TermsChangeNotice?>()) { + return (data != null ? _i46.TermsChangeNotice.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<_i47.TermsVersionEntry?>()) { + return (data != null ? _i47.TermsVersionEntry.fromJson(data) : null) as T; } - if (t == _i1.getType<_i47.TotpEnrollmentStart?>()) { - return (data != null ? _i47.TotpEnrollmentStart.fromJson(data) : null) + if (t == _i1.getType<_i48.TotpEnrollmentStart?>()) { + return (data != null ? _i48.TotpEnrollmentStart.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<_i49.TriageResult?>()) { + return (data != null ? _i49.TriageResult.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<_i50.UbsContact?>()) { + return (data != null ? _i50.UbsContact.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<_i51.VisitSyncEntry?>()) { + return (data != null ? _i51.VisitSyncEntry.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<_i52.VisitSyncResult?>()) { + return (data != null ? _i52.VisitSyncResult.fromJson(data) : null) as T; } - if (t == _i1.getType<_i52.AuditChainCheckpoint?>()) { - return (data != null ? _i52.AuditChainCheckpoint.fromJson(data) : null) + if (t == _i1.getType<_i53.AuditChainCheckpoint?>()) { + return (data != null ? _i53.AuditChainCheckpoint.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<_i54.AuditExport?>()) { + return (data != null ? _i54.AuditExport.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<_i55.AuditLog?>()) { + return (data != null ? _i55.AuditLog.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<_i56.ConsentLog?>()) { + return (data != null ? _i56.ConsentLog.fromJson(data) : null) as T; } - if (t == _i1.getType<_i56.DataSubjectRequest?>()) { - return (data != null ? _i56.DataSubjectRequest.fromJson(data) : null) + if (t == _i1.getType<_i57.DataSubjectRequest?>()) { + return (data != null ? _i57.DataSubjectRequest.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<_i58.EnrollmentToken?>()) { + return (data != null ? _i58.EnrollmentToken.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<_i59.AlertStatus?>()) { + return (data != null ? _i59.AlertStatus.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<_i60.ArrivalMethod?>()) { + return (data != null ? _i60.ArrivalMethod.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<_i61.AuditChainReason?>()) { + return (data != null ? _i61.AuditChainReason.fromJson(data) : null) as T; } - if (t == _i1.getType<_i61.AuditIntegrityState?>()) { - return (data != null ? _i61.AuditIntegrityState.fromJson(data) : null) + if (t == _i1.getType<_i62.AuditIntegrityState?>()) { + return (data != null ? _i62.AuditIntegrityState.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<_i63.AuditRowSeal?>()) { + return (data != null ? _i63.AuditRowSeal.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<_i64.ConsentPurpose?>()) { + return (data != null ? _i64.ConsentPurpose.fromJson(data) : null) as T; } - if (t == _i1.getType<_i64.DataSubjectRequestStatus?>()) { + if (t == _i1.getType<_i65.DataSubjectRequestStatus?>()) { return (data != null - ? _i64.DataSubjectRequestStatus.fromJson(data) + ? _i65.DataSubjectRequestStatus.fromJson(data) : null) as T; } - if (t == _i1.getType<_i65.DataSubjectRequestType?>()) { - return (data != null ? _i65.DataSubjectRequestType.fromJson(data) : null) + if (t == _i1.getType<_i66.DataSubjectRequestType?>()) { + return (data != null ? _i66.DataSubjectRequestType.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<_i67.EscalationReason?>()) { + return (data != null ? _i67.EscalationReason.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<_i68.RiskLevel?>()) { + return (data != null ? _i68.RiskLevel.fromJson(data) : null) as T; } - if (t == _i1.getType<_i68.ScheduledNoticeStatus?>()) { - return (data != null ? _i68.ScheduledNoticeStatus.fromJson(data) : null) + if (t == _i1.getType<_i69.ScheduledNoticeStatus?>()) { + return (data != null ? _i69.ScheduledNoticeStatus.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<_i70.SyncStatus?>()) { + return (data != null ? _i70.SyncStatus.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<_i71.UserRole?>()) { + return (data != null ? _i71.UserRole.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<_i72.VisitAuthorship?>()) { + return (data != null ? _i72.VisitAuthorship.fromJson(data) : null) as T; } - if (t == _i1.getType<_i72.AdminInvalidRequestException?>()) { + if (t == _i1.getType<_i73.AdminInvalidRequestException?>()) { return (data != null - ? _i72.AdminInvalidRequestException.fromJson(data) + ? _i73.AdminInvalidRequestException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i73.AlertDispatchUnavailableException?>()) { + if (t == _i1.getType<_i74.AlertDispatchUnavailableException?>()) { return (data != null - ? _i73.AlertDispatchUnavailableException.fromJson(data) + ? _i74.AlertDispatchUnavailableException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i74.AlertPermissionException?>()) { + if (t == _i1.getType<_i75.AlertPermissionException?>()) { return (data != null - ? _i74.AlertPermissionException.fromJson(data) + ? _i75.AlertPermissionException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i75.AlertValidationException?>()) { + if (t == _i1.getType<_i76.AlertValidationException?>()) { return (data != null - ? _i75.AlertValidationException.fromJson(data) + ? _i76.AlertValidationException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i76.AuthenticationFailedException?>()) { + if (t == _i1.getType<_i77.AuthenticationFailedException?>()) { return (data != null - ? _i76.AuthenticationFailedException.fromJson(data) + ? _i77.AuthenticationFailedException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i77.DataRightsException?>()) { - return (data != null ? _i77.DataRightsException.fromJson(data) : null) + if (t == _i1.getType<_i78.DataRightsException?>()) { + return (data != null ? _i78.DataRightsException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i78.EndpointDisabledException?>()) { + if (t == _i1.getType<_i79.EndpointDisabledException?>()) { return (data != null - ? _i78.EndpointDisabledException.fromJson(data) + ? _i79.EndpointDisabledException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i79.EnrollmentException?>()) { - return (data != null ? _i79.EnrollmentException.fromJson(data) : null) + if (t == _i1.getType<_i80.EnrollmentException?>()) { + return (data != null ? _i80.EnrollmentException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i80.MfaEnrollmentRequiredException?>()) { + if (t == _i1.getType<_i81.MfaEnrollmentRequiredException?>()) { return (data != null - ? _i80.MfaEnrollmentRequiredException.fromJson(data) + ? _i81.MfaEnrollmentRequiredException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i81.MfaRequiredException?>()) { - return (data != null ? _i81.MfaRequiredException.fromJson(data) : null) + if (t == _i1.getType<_i82.MfaRequiredException?>()) { + return (data != null ? _i82.MfaRequiredException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i82.NoticeDeliveryException?>()) { - return (data != null ? _i82.NoticeDeliveryException.fromJson(data) : null) + if (t == _i1.getType<_i83.NoticeDeliveryException?>()) { + return (data != null ? _i83.NoticeDeliveryException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i83.OtpRequestException?>()) { - return (data != null ? _i83.OtpRequestException.fromJson(data) : null) + if (t == _i1.getType<_i84.OtpRequestException?>()) { + return (data != null ? _i84.OtpRequestException.fromJson(data) : null) as T; } - if (t == _i1.getType<_i84.SessionExpiredException?>()) { - return (data != null ? _i84.SessionExpiredException.fromJson(data) : null) + if (t == _i1.getType<_i85.SessionExpiredException?>()) { + return (data != null ? _i85.SessionExpiredException.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<_i86.MicroArea?>()) { + return (data != null ? _i86.MicroArea.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<_i87.OtpChallenge?>()) { + return (data != null ? _i87.OtpChallenge.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<_i88.Patient?>()) { + return (data != null ? _i88.Patient.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<_i89.PushToken?>()) { + return (data != null ? _i89.PushToken.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<_i90.ScheduledNotice?>()) { + return (data != null ? _i90.ScheduledNotice.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<_i91.StaffAccount?>()) { + return (data != null ? _i91.StaffAccount.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<_i92.TermsVersion?>()) { + return (data != null ? _i92.TermsVersion.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<_i93.TriageAnswer?>()) { + return (data != null ? _i93.TriageAnswer.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<_i94.TriageSession?>()) { + return (data != null ? _i94.TriageSession.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<_i95.Ubs?>()) { + return (data != null ? _i95.Ubs.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<_i96.User?>()) { + return (data != null ? _i96.User.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<_i97.UserCredential?>()) { + return (data != null ? _i97.UserCredential.fromJson(data) : null) as T; } - if (t == _i1.getType<_i97.Visit?>()) { - return (data != null ? _i97.Visit.fromJson(data) : null) as T; + if (t == _i1.getType<_i98.Visit?>()) { + return (data != null ? _i98.Visit.fromJson(data) : null) as T; } - if (t == List<_i12.AdminAlert>) { - return (data as List).map((e) => deserialize<_i12.AdminAlert>(e)).toList() + if (t == List<_i13.AdminAlert>) { + return (data as List).map((e) => deserialize<_i13.AdminAlert>(e)).toList() as T; } - if (t == List<_i16.AdminAuditEntry>) { + if (t == List<_i17.AdminAuditEntry>) { return (data as List) - .map((e) => deserialize<_i16.AdminAuditEntry>(e)) + .map((e) => deserialize<_i17.AdminAuditEntry>(e)) .toList() as T; } - if (t == List<_i20.AdminDataSubjectRequest>) { + if (t == List<_i21.AdminDataSubjectRequest>) { return (data as List) - .map((e) => deserialize<_i20.AdminDataSubjectRequest>(e)) + .map((e) => deserialize<_i21.AdminDataSubjectRequest>(e)) .toList() as T; } if (t == List) { return (data as List).map((e) => deserialize(e)).toList() as T; } - if (t == List<_i36.NoticeHistoryEntry>) { + if (t == List<_i37.NoticeHistoryEntry>) { return (data as List) - .map((e) => deserialize<_i36.NoticeHistoryEntry>(e)) + .map((e) => deserialize<_i37.NoticeHistoryEntry>(e)) .toList() as T; } - if (t == List<_i39.PatientConsentRecord>) { + if (t == List<_i40.PatientConsentRecord>) { return (data as List) - .map((e) => deserialize<_i39.PatientConsentRecord>(e)) + .map((e) => deserialize<_i40.PatientConsentRecord>(e)) .toList() as T; } - if (t == List<_i42.PatientRiskEvent>) { + if (t == List<_i43.PatientRiskEvent>) { return (data as List) - .map((e) => deserialize<_i42.PatientRiskEvent>(e)) + .map((e) => deserialize<_i43.PatientRiskEvent>(e)) .toList() as T; } - if (t == List<_i41.PatientDataSubjectRequestRecord>) { + if (t == List<_i42.PatientDataSubjectRequestRecord>) { return (data as List) - .map((e) => deserialize<_i41.PatientDataSubjectRequestRecord>(e)) + .map((e) => deserialize<_i42.PatientDataSubjectRequestRecord>(e)) .toList() as T; } @@ -3691,46 +3700,52 @@ class Protocol extends _i1.SerializationManagerServer { ) as T; } - if (t == List<_i98.AdminMicroArea>) { + if (t == List<_i99.AdminMicroArea>) { return (data as List) - .map((e) => deserialize<_i98.AdminMicroArea>(e)) + .map((e) => deserialize<_i99.AdminMicroArea>(e)) .toList() as T; } - if (t == List<_i99.AdminAcs>) { - return (data as List).map((e) => deserialize<_i99.AdminAcs>(e)).toList() + if (t == List<_i100.AdminAcs>) { + return (data as List).map((e) => deserialize<_i100.AdminAcs>(e)).toList() as T; } - if (t == List<_i100.AdminStaff>) { + if (t == List<_i101.AdminStaff>) { return (data as List) - .map((e) => deserialize<_i100.AdminStaff>(e)) + .map((e) => deserialize<_i101.AdminStaff>(e)) .toList() as T; } - if (t == List<_i101.TermsVersionEntry>) { + if (t == List<_i102.TermsVersionEntry>) { return (data as List) - .map((e) => deserialize<_i101.TermsVersionEntry>(e)) + .map((e) => deserialize<_i102.TermsVersionEntry>(e)) .toList() as T; } - if (t == List<_i102.MicroAreaPatient>) { + if (t == List<_i103.AcsOpenAlert>) { return (data as List) - .map((e) => deserialize<_i102.MicroAreaPatient>(e)) + .map((e) => deserialize<_i103.AcsOpenAlert>(e)) + .toList() + as T; + } + if (t == List<_i104.MicroAreaPatient>) { + return (data as List) + .map((e) => deserialize<_i104.MicroAreaPatient>(e)) .toList() as T; } if (t == List) { return (data as List).map((e) => deserialize(e)).toList() as T; } - if (t == List<_i103.VisitSyncResult>) { + if (t == List<_i105.VisitSyncResult>) { return (data as List) - .map((e) => deserialize<_i103.VisitSyncResult>(e)) + .map((e) => deserialize<_i105.VisitSyncResult>(e)) .toList() as T; } - if (t == List<_i104.VisitSyncEntry>) { + if (t == List<_i106.VisitSyncEntry>) { return (data as List) - .map((e) => deserialize<_i104.VisitSyncEntry>(e)) + .map((e) => deserialize<_i106.VisitSyncEntry>(e)) .toList() as T; } @@ -3749,95 +3764,96 @@ class Protocol extends _i1.SerializationManagerServer { _i7.AlertDeliveryRecord => 'AlertDeliveryRecord', _i8.AlertIdempotencyKey => 'AlertIdempotencyKey', _i9.AlertOutboxEntry => 'AlertOutboxEntry', - _i10.AdminAcs => 'AdminAcs', - _i11.AdminAcsCreationResult => 'AdminAcsCreationResult', - _i12.AdminAlert => 'AdminAlert', - _i13.AdminAlertPage => 'AdminAlertPage', - _i14.AdminAlertsPulse => 'AdminAlertsPulse', - _i15.AdminAuditChainStatus => 'AdminAuditChainStatus', - _i16.AdminAuditEntry => 'AdminAuditEntry', - _i17.AdminAuditExport => 'AdminAuditExport', - _i18.AdminAuditFilter => 'AdminAuditFilter', - _i19.AdminAuditPage => 'AdminAuditPage', - _i20.AdminDataSubjectRequest => 'AdminDataSubjectRequest', - _i21.AdminDataSubjectRequestDetail => 'AdminDataSubjectRequestDetail', - _i22.AdminDataSubjectRequestPage => 'AdminDataSubjectRequestPage', - _i23.AdminEscalationResult => 'AdminEscalationResult', - _i24.AdminIndicators => 'AdminIndicators', - _i25.AdminMicroArea => 'AdminMicroArea', - _i26.AdminPasswordResetResult => 'AdminPasswordResetResult', - _i27.AdminStaff => 'AdminStaff', - _i28.AdminStaffMfaResetResult => 'AdminStaffMfaResetResult', - _i29.AlertAckResult => 'AlertAckResult', - _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 => + _i10.AcsOpenAlert => 'AcsOpenAlert', + _i11.AdminAcs => 'AdminAcs', + _i12.AdminAcsCreationResult => 'AdminAcsCreationResult', + _i13.AdminAlert => 'AdminAlert', + _i14.AdminAlertPage => 'AdminAlertPage', + _i15.AdminAlertsPulse => 'AdminAlertsPulse', + _i16.AdminAuditChainStatus => 'AdminAuditChainStatus', + _i17.AdminAuditEntry => 'AdminAuditEntry', + _i18.AdminAuditExport => 'AdminAuditExport', + _i19.AdminAuditFilter => 'AdminAuditFilter', + _i20.AdminAuditPage => 'AdminAuditPage', + _i21.AdminDataSubjectRequest => 'AdminDataSubjectRequest', + _i22.AdminDataSubjectRequestDetail => 'AdminDataSubjectRequestDetail', + _i23.AdminDataSubjectRequestPage => 'AdminDataSubjectRequestPage', + _i24.AdminEscalationResult => 'AdminEscalationResult', + _i25.AdminIndicators => 'AdminIndicators', + _i26.AdminMicroArea => 'AdminMicroArea', + _i27.AdminPasswordResetResult => 'AdminPasswordResetResult', + _i28.AdminStaff => 'AdminStaff', + _i29.AdminStaffMfaResetResult => 'AdminStaffMfaResetResult', + _i30.AlertAckResult => 'AlertAckResult', + _i31.AlertResolveResult => 'AlertResolveResult', + _i32.AlertStatusResult => 'AlertStatusResult', + _i33.DevelopmentLoginResult => 'DevelopmentLoginResult', + _i34.EnrollmentResult => 'EnrollmentResult', + _i35.EnrollmentTokenResult => 'EnrollmentTokenResult', + _i36.MicroAreaPatient => 'MicroAreaPatient', + _i37.NoticeHistoryEntry => 'NoticeHistoryEntry', + _i38.NoticeHistoryPage => 'NoticeHistoryPage', + _i39.NoticeSendResult => 'NoticeSendResult', + _i40.PatientConsentRecord => 'PatientConsentRecord', + _i41.PatientDataOverview => 'PatientDataOverview', + _i42.PatientDataSubjectRequestRecord => 'PatientDataSubjectRequestRecord', + _i43.PatientRiskEvent => 'PatientRiskEvent', + _i44.RedAlertResult => 'RedAlertResult', + _i45.ServiceHealth => 'ServiceHealth', + _i46.TermsChangeNotice => 'TermsChangeNotice', + _i47.TermsVersionEntry => 'TermsVersionEntry', + _i48.TotpEnrollmentStart => 'TotpEnrollmentStart', + _i49.TriageResult => 'TriageResult', + _i50.UbsContact => 'UbsContact', + _i51.VisitSyncEntry => 'VisitSyncEntry', + _i52.VisitSyncResult => 'VisitSyncResult', + _i53.AuditChainCheckpoint => 'AuditChainCheckpoint', + _i54.AuditExport => 'AuditExport', + _i55.AuditLog => 'AuditLog', + _i56.ConsentLog => 'ConsentLog', + _i57.DataSubjectRequest => 'DataSubjectRequest', + _i58.EnrollmentToken => 'EnrollmentToken', + _i59.AlertStatus => 'AlertStatus', + _i60.ArrivalMethod => 'ArrivalMethod', + _i61.AuditChainReason => 'AuditChainReason', + _i62.AuditIntegrityState => 'AuditIntegrityState', + _i63.AuditRowSeal => 'AuditRowSeal', + _i64.ConsentPurpose => 'ConsentPurpose', + _i65.DataSubjectRequestStatus => 'DataSubjectRequestStatus', + _i66.DataSubjectRequestType => 'DataSubjectRequestType', + _i67.EscalationReason => 'EscalationReason', + _i68.RiskLevel => 'RiskLevel', + _i69.ScheduledNoticeStatus => 'ScheduledNoticeStatus', + _i70.SyncStatus => 'SyncStatus', + _i71.UserRole => 'UserRole', + _i72.VisitAuthorship => 'VisitAuthorship', + _i73.AdminInvalidRequestException => 'AdminInvalidRequestException', + _i74.AlertDispatchUnavailableException => 'AlertDispatchUnavailableException', - _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', + _i75.AlertPermissionException => 'AlertPermissionException', + _i76.AlertValidationException => 'AlertValidationException', + _i77.AuthenticationFailedException => 'AuthenticationFailedException', + _i78.DataRightsException => 'DataRightsException', + _i79.EndpointDisabledException => 'EndpointDisabledException', + _i80.EnrollmentException => 'EnrollmentException', + _i81.MfaEnrollmentRequiredException => 'MfaEnrollmentRequiredException', + _i82.MfaRequiredException => 'MfaRequiredException', + _i83.NoticeDeliveryException => 'NoticeDeliveryException', + _i84.OtpRequestException => 'OtpRequestException', + _i85.SessionExpiredException => 'SessionExpiredException', + _i86.MicroArea => 'MicroArea', + _i87.OtpChallenge => 'OtpChallenge', + _i88.Patient => 'Patient', + _i89.PushToken => 'PushToken', + _i90.ScheduledNotice => 'ScheduledNotice', + _i91.StaffAccount => 'StaffAccount', + _i92.TermsVersion => 'TermsVersion', + _i93.TriageAnswer => 'TriageAnswer', + _i94.TriageSession => 'TriageSession', + _i95.Ubs => 'Ubs', + _i96.User => 'User', + _i97.UserCredential => 'UserCredential', + _i98.Visit => 'Visit', _ => null, }; } @@ -3866,181 +3882,183 @@ class Protocol extends _i1.SerializationManagerServer { return 'AlertIdempotencyKey'; case _i9.AlertOutboxEntry(): return 'AlertOutboxEntry'; - case _i10.AdminAcs(): + case _i10.AcsOpenAlert(): + return 'AcsOpenAlert'; + case _i11.AdminAcs(): return 'AdminAcs'; - case _i11.AdminAcsCreationResult(): + case _i12.AdminAcsCreationResult(): return 'AdminAcsCreationResult'; - case _i12.AdminAlert(): + case _i13.AdminAlert(): return 'AdminAlert'; - case _i13.AdminAlertPage(): + case _i14.AdminAlertPage(): return 'AdminAlertPage'; - case _i14.AdminAlertsPulse(): + case _i15.AdminAlertsPulse(): return 'AdminAlertsPulse'; - case _i15.AdminAuditChainStatus(): + case _i16.AdminAuditChainStatus(): return 'AdminAuditChainStatus'; - case _i16.AdminAuditEntry(): + case _i17.AdminAuditEntry(): return 'AdminAuditEntry'; - case _i17.AdminAuditExport(): + case _i18.AdminAuditExport(): return 'AdminAuditExport'; - case _i18.AdminAuditFilter(): + case _i19.AdminAuditFilter(): return 'AdminAuditFilter'; - case _i19.AdminAuditPage(): + case _i20.AdminAuditPage(): return 'AdminAuditPage'; - case _i20.AdminDataSubjectRequest(): + case _i21.AdminDataSubjectRequest(): return 'AdminDataSubjectRequest'; - case _i21.AdminDataSubjectRequestDetail(): + case _i22.AdminDataSubjectRequestDetail(): return 'AdminDataSubjectRequestDetail'; - case _i22.AdminDataSubjectRequestPage(): + case _i23.AdminDataSubjectRequestPage(): return 'AdminDataSubjectRequestPage'; - case _i23.AdminEscalationResult(): + case _i24.AdminEscalationResult(): return 'AdminEscalationResult'; - case _i24.AdminIndicators(): + case _i25.AdminIndicators(): return 'AdminIndicators'; - case _i25.AdminMicroArea(): + case _i26.AdminMicroArea(): return 'AdminMicroArea'; - case _i26.AdminPasswordResetResult(): + case _i27.AdminPasswordResetResult(): return 'AdminPasswordResetResult'; - case _i27.AdminStaff(): + case _i28.AdminStaff(): return 'AdminStaff'; - case _i28.AdminStaffMfaResetResult(): + case _i29.AdminStaffMfaResetResult(): return 'AdminStaffMfaResetResult'; - case _i29.AlertAckResult(): + case _i30.AlertAckResult(): return 'AlertAckResult'; - case _i30.AlertResolveResult(): + case _i31.AlertResolveResult(): return 'AlertResolveResult'; - case _i31.AlertStatusResult(): + case _i32.AlertStatusResult(): return 'AlertStatusResult'; - case _i32.DevelopmentLoginResult(): + case _i33.DevelopmentLoginResult(): return 'DevelopmentLoginResult'; - case _i33.EnrollmentResult(): + case _i34.EnrollmentResult(): return 'EnrollmentResult'; - case _i34.EnrollmentTokenResult(): + case _i35.EnrollmentTokenResult(): return 'EnrollmentTokenResult'; - case _i35.MicroAreaPatient(): + case _i36.MicroAreaPatient(): return 'MicroAreaPatient'; - case _i36.NoticeHistoryEntry(): + case _i37.NoticeHistoryEntry(): return 'NoticeHistoryEntry'; - case _i37.NoticeHistoryPage(): + case _i38.NoticeHistoryPage(): return 'NoticeHistoryPage'; - case _i38.NoticeSendResult(): + case _i39.NoticeSendResult(): return 'NoticeSendResult'; - case _i39.PatientConsentRecord(): + case _i40.PatientConsentRecord(): return 'PatientConsentRecord'; - case _i40.PatientDataOverview(): + case _i41.PatientDataOverview(): return 'PatientDataOverview'; - case _i41.PatientDataSubjectRequestRecord(): + case _i42.PatientDataSubjectRequestRecord(): return 'PatientDataSubjectRequestRecord'; - case _i42.PatientRiskEvent(): + case _i43.PatientRiskEvent(): return 'PatientRiskEvent'; - case _i43.RedAlertResult(): + case _i44.RedAlertResult(): return 'RedAlertResult'; - case _i44.ServiceHealth(): + case _i45.ServiceHealth(): return 'ServiceHealth'; - case _i45.TermsChangeNotice(): + case _i46.TermsChangeNotice(): return 'TermsChangeNotice'; - case _i46.TermsVersionEntry(): + case _i47.TermsVersionEntry(): return 'TermsVersionEntry'; - case _i47.TotpEnrollmentStart(): + case _i48.TotpEnrollmentStart(): return 'TotpEnrollmentStart'; - case _i48.TriageResult(): + case _i49.TriageResult(): return 'TriageResult'; - case _i49.UbsContact(): + case _i50.UbsContact(): return 'UbsContact'; - case _i50.VisitSyncEntry(): + case _i51.VisitSyncEntry(): return 'VisitSyncEntry'; - case _i51.VisitSyncResult(): + case _i52.VisitSyncResult(): return 'VisitSyncResult'; - case _i52.AuditChainCheckpoint(): + case _i53.AuditChainCheckpoint(): return 'AuditChainCheckpoint'; - case _i53.AuditExport(): + case _i54.AuditExport(): return 'AuditExport'; - case _i54.AuditLog(): + case _i55.AuditLog(): return 'AuditLog'; - case _i55.ConsentLog(): + case _i56.ConsentLog(): return 'ConsentLog'; - case _i56.DataSubjectRequest(): + case _i57.DataSubjectRequest(): return 'DataSubjectRequest'; - case _i57.EnrollmentToken(): + case _i58.EnrollmentToken(): return 'EnrollmentToken'; - case _i58.AlertStatus(): + case _i59.AlertStatus(): return 'AlertStatus'; - case _i59.ArrivalMethod(): + case _i60.ArrivalMethod(): return 'ArrivalMethod'; - case _i60.AuditChainReason(): + case _i61.AuditChainReason(): return 'AuditChainReason'; - case _i61.AuditIntegrityState(): + case _i62.AuditIntegrityState(): return 'AuditIntegrityState'; - case _i62.AuditRowSeal(): + case _i63.AuditRowSeal(): return 'AuditRowSeal'; - case _i63.ConsentPurpose(): + case _i64.ConsentPurpose(): return 'ConsentPurpose'; - case _i64.DataSubjectRequestStatus(): + case _i65.DataSubjectRequestStatus(): return 'DataSubjectRequestStatus'; - case _i65.DataSubjectRequestType(): + case _i66.DataSubjectRequestType(): return 'DataSubjectRequestType'; - case _i66.EscalationReason(): + case _i67.EscalationReason(): return 'EscalationReason'; - case _i67.RiskLevel(): + case _i68.RiskLevel(): return 'RiskLevel'; - case _i68.ScheduledNoticeStatus(): + case _i69.ScheduledNoticeStatus(): return 'ScheduledNoticeStatus'; - case _i69.SyncStatus(): + case _i70.SyncStatus(): return 'SyncStatus'; - case _i70.UserRole(): + case _i71.UserRole(): return 'UserRole'; - case _i71.VisitAuthorship(): + case _i72.VisitAuthorship(): return 'VisitAuthorship'; - case _i72.AdminInvalidRequestException(): + case _i73.AdminInvalidRequestException(): return 'AdminInvalidRequestException'; - case _i73.AlertDispatchUnavailableException(): + case _i74.AlertDispatchUnavailableException(): return 'AlertDispatchUnavailableException'; - case _i74.AlertPermissionException(): + case _i75.AlertPermissionException(): return 'AlertPermissionException'; - case _i75.AlertValidationException(): + case _i76.AlertValidationException(): return 'AlertValidationException'; - case _i76.AuthenticationFailedException(): + case _i77.AuthenticationFailedException(): return 'AuthenticationFailedException'; - case _i77.DataRightsException(): + case _i78.DataRightsException(): return 'DataRightsException'; - case _i78.EndpointDisabledException(): + case _i79.EndpointDisabledException(): return 'EndpointDisabledException'; - case _i79.EnrollmentException(): + case _i80.EnrollmentException(): return 'EnrollmentException'; - case _i80.MfaEnrollmentRequiredException(): + case _i81.MfaEnrollmentRequiredException(): return 'MfaEnrollmentRequiredException'; - case _i81.MfaRequiredException(): + case _i82.MfaRequiredException(): return 'MfaRequiredException'; - case _i82.NoticeDeliveryException(): + case _i83.NoticeDeliveryException(): return 'NoticeDeliveryException'; - case _i83.OtpRequestException(): + case _i84.OtpRequestException(): return 'OtpRequestException'; - case _i84.SessionExpiredException(): + case _i85.SessionExpiredException(): return 'SessionExpiredException'; - case _i85.MicroArea(): + case _i86.MicroArea(): return 'MicroArea'; - case _i86.OtpChallenge(): + case _i87.OtpChallenge(): return 'OtpChallenge'; - case _i87.Patient(): + case _i88.Patient(): return 'Patient'; - case _i88.PushToken(): + case _i89.PushToken(): return 'PushToken'; - case _i89.ScheduledNotice(): + case _i90.ScheduledNotice(): return 'ScheduledNotice'; - case _i90.StaffAccount(): + case _i91.StaffAccount(): return 'StaffAccount'; - case _i91.TermsVersion(): + case _i92.TermsVersion(): return 'TermsVersion'; - case _i92.TriageAnswer(): + case _i93.TriageAnswer(): return 'TriageAnswer'; - case _i93.TriageSession(): + case _i94.TriageSession(): return 'TriageSession'; - case _i94.Ubs(): + case _i95.Ubs(): return 'Ubs'; - case _i95.User(): + case _i96.User(): return 'User'; - case _i96.UserCredential(): + case _i97.UserCredential(): return 'UserCredential'; - case _i97.Visit(): + case _i98.Visit(): return 'Visit'; } className = _i2.Protocol().getClassNameForObject(data); @@ -4077,269 +4095,272 @@ class Protocol extends _i1.SerializationManagerServer { if (dataClassName == 'AlertOutboxEntry') { return deserialize<_i9.AlertOutboxEntry>(data['data']); } + if (dataClassName == 'AcsOpenAlert') { + return deserialize<_i10.AcsOpenAlert>(data['data']); + } if (dataClassName == 'AdminAcs') { - return deserialize<_i10.AdminAcs>(data['data']); + return deserialize<_i11.AdminAcs>(data['data']); } if (dataClassName == 'AdminAcsCreationResult') { - return deserialize<_i11.AdminAcsCreationResult>(data['data']); + return deserialize<_i12.AdminAcsCreationResult>(data['data']); } if (dataClassName == 'AdminAlert') { - return deserialize<_i12.AdminAlert>(data['data']); + return deserialize<_i13.AdminAlert>(data['data']); } if (dataClassName == 'AdminAlertPage') { - return deserialize<_i13.AdminAlertPage>(data['data']); + return deserialize<_i14.AdminAlertPage>(data['data']); } if (dataClassName == 'AdminAlertsPulse') { - return deserialize<_i14.AdminAlertsPulse>(data['data']); + return deserialize<_i15.AdminAlertsPulse>(data['data']); } if (dataClassName == 'AdminAuditChainStatus') { - return deserialize<_i15.AdminAuditChainStatus>(data['data']); + return deserialize<_i16.AdminAuditChainStatus>(data['data']); } if (dataClassName == 'AdminAuditEntry') { - return deserialize<_i16.AdminAuditEntry>(data['data']); + return deserialize<_i17.AdminAuditEntry>(data['data']); } if (dataClassName == 'AdminAuditExport') { - return deserialize<_i17.AdminAuditExport>(data['data']); + return deserialize<_i18.AdminAuditExport>(data['data']); } if (dataClassName == 'AdminAuditFilter') { - return deserialize<_i18.AdminAuditFilter>(data['data']); + return deserialize<_i19.AdminAuditFilter>(data['data']); } if (dataClassName == 'AdminAuditPage') { - return deserialize<_i19.AdminAuditPage>(data['data']); + return deserialize<_i20.AdminAuditPage>(data['data']); } if (dataClassName == 'AdminDataSubjectRequest') { - return deserialize<_i20.AdminDataSubjectRequest>(data['data']); + return deserialize<_i21.AdminDataSubjectRequest>(data['data']); } if (dataClassName == 'AdminDataSubjectRequestDetail') { - return deserialize<_i21.AdminDataSubjectRequestDetail>(data['data']); + return deserialize<_i22.AdminDataSubjectRequestDetail>(data['data']); } if (dataClassName == 'AdminDataSubjectRequestPage') { - return deserialize<_i22.AdminDataSubjectRequestPage>(data['data']); + return deserialize<_i23.AdminDataSubjectRequestPage>(data['data']); } if (dataClassName == 'AdminEscalationResult') { - return deserialize<_i23.AdminEscalationResult>(data['data']); + return deserialize<_i24.AdminEscalationResult>(data['data']); } if (dataClassName == 'AdminIndicators') { - return deserialize<_i24.AdminIndicators>(data['data']); + return deserialize<_i25.AdminIndicators>(data['data']); } if (dataClassName == 'AdminMicroArea') { - return deserialize<_i25.AdminMicroArea>(data['data']); + return deserialize<_i26.AdminMicroArea>(data['data']); } if (dataClassName == 'AdminPasswordResetResult') { - return deserialize<_i26.AdminPasswordResetResult>(data['data']); + return deserialize<_i27.AdminPasswordResetResult>(data['data']); } if (dataClassName == 'AdminStaff') { - return deserialize<_i27.AdminStaff>(data['data']); + return deserialize<_i28.AdminStaff>(data['data']); } if (dataClassName == 'AdminStaffMfaResetResult') { - return deserialize<_i28.AdminStaffMfaResetResult>(data['data']); + return deserialize<_i29.AdminStaffMfaResetResult>(data['data']); } if (dataClassName == 'AlertAckResult') { - return deserialize<_i29.AlertAckResult>(data['data']); + return deserialize<_i30.AlertAckResult>(data['data']); } if (dataClassName == 'AlertResolveResult') { - return deserialize<_i30.AlertResolveResult>(data['data']); + return deserialize<_i31.AlertResolveResult>(data['data']); } if (dataClassName == 'AlertStatusResult') { - return deserialize<_i31.AlertStatusResult>(data['data']); + return deserialize<_i32.AlertStatusResult>(data['data']); } if (dataClassName == 'DevelopmentLoginResult') { - return deserialize<_i32.DevelopmentLoginResult>(data['data']); + return deserialize<_i33.DevelopmentLoginResult>(data['data']); } if (dataClassName == 'EnrollmentResult') { - return deserialize<_i33.EnrollmentResult>(data['data']); + return deserialize<_i34.EnrollmentResult>(data['data']); } if (dataClassName == 'EnrollmentTokenResult') { - return deserialize<_i34.EnrollmentTokenResult>(data['data']); + return deserialize<_i35.EnrollmentTokenResult>(data['data']); } if (dataClassName == 'MicroAreaPatient') { - return deserialize<_i35.MicroAreaPatient>(data['data']); + return deserialize<_i36.MicroAreaPatient>(data['data']); } if (dataClassName == 'NoticeHistoryEntry') { - return deserialize<_i36.NoticeHistoryEntry>(data['data']); + return deserialize<_i37.NoticeHistoryEntry>(data['data']); } if (dataClassName == 'NoticeHistoryPage') { - return deserialize<_i37.NoticeHistoryPage>(data['data']); + return deserialize<_i38.NoticeHistoryPage>(data['data']); } if (dataClassName == 'NoticeSendResult') { - return deserialize<_i38.NoticeSendResult>(data['data']); + return deserialize<_i39.NoticeSendResult>(data['data']); } if (dataClassName == 'PatientConsentRecord') { - return deserialize<_i39.PatientConsentRecord>(data['data']); + return deserialize<_i40.PatientConsentRecord>(data['data']); } if (dataClassName == 'PatientDataOverview') { - return deserialize<_i40.PatientDataOverview>(data['data']); + return deserialize<_i41.PatientDataOverview>(data['data']); } if (dataClassName == 'PatientDataSubjectRequestRecord') { - return deserialize<_i41.PatientDataSubjectRequestRecord>(data['data']); + return deserialize<_i42.PatientDataSubjectRequestRecord>(data['data']); } if (dataClassName == 'PatientRiskEvent') { - return deserialize<_i42.PatientRiskEvent>(data['data']); + return deserialize<_i43.PatientRiskEvent>(data['data']); } if (dataClassName == 'RedAlertResult') { - return deserialize<_i43.RedAlertResult>(data['data']); + return deserialize<_i44.RedAlertResult>(data['data']); } if (dataClassName == 'ServiceHealth') { - return deserialize<_i44.ServiceHealth>(data['data']); + return deserialize<_i45.ServiceHealth>(data['data']); } if (dataClassName == 'TermsChangeNotice') { - return deserialize<_i45.TermsChangeNotice>(data['data']); + return deserialize<_i46.TermsChangeNotice>(data['data']); } if (dataClassName == 'TermsVersionEntry') { - return deserialize<_i46.TermsVersionEntry>(data['data']); + return deserialize<_i47.TermsVersionEntry>(data['data']); } if (dataClassName == 'TotpEnrollmentStart') { - return deserialize<_i47.TotpEnrollmentStart>(data['data']); + return deserialize<_i48.TotpEnrollmentStart>(data['data']); } if (dataClassName == 'TriageResult') { - return deserialize<_i48.TriageResult>(data['data']); + return deserialize<_i49.TriageResult>(data['data']); } if (dataClassName == 'UbsContact') { - return deserialize<_i49.UbsContact>(data['data']); + return deserialize<_i50.UbsContact>(data['data']); } if (dataClassName == 'VisitSyncEntry') { - return deserialize<_i50.VisitSyncEntry>(data['data']); + return deserialize<_i51.VisitSyncEntry>(data['data']); } if (dataClassName == 'VisitSyncResult') { - return deserialize<_i51.VisitSyncResult>(data['data']); + return deserialize<_i52.VisitSyncResult>(data['data']); } if (dataClassName == 'AuditChainCheckpoint') { - return deserialize<_i52.AuditChainCheckpoint>(data['data']); + return deserialize<_i53.AuditChainCheckpoint>(data['data']); } if (dataClassName == 'AuditExport') { - return deserialize<_i53.AuditExport>(data['data']); + return deserialize<_i54.AuditExport>(data['data']); } if (dataClassName == 'AuditLog') { - return deserialize<_i54.AuditLog>(data['data']); + return deserialize<_i55.AuditLog>(data['data']); } if (dataClassName == 'ConsentLog') { - return deserialize<_i55.ConsentLog>(data['data']); + return deserialize<_i56.ConsentLog>(data['data']); } if (dataClassName == 'DataSubjectRequest') { - return deserialize<_i56.DataSubjectRequest>(data['data']); + return deserialize<_i57.DataSubjectRequest>(data['data']); } if (dataClassName == 'EnrollmentToken') { - return deserialize<_i57.EnrollmentToken>(data['data']); + return deserialize<_i58.EnrollmentToken>(data['data']); } if (dataClassName == 'AlertStatus') { - return deserialize<_i58.AlertStatus>(data['data']); + return deserialize<_i59.AlertStatus>(data['data']); } if (dataClassName == 'ArrivalMethod') { - return deserialize<_i59.ArrivalMethod>(data['data']); + return deserialize<_i60.ArrivalMethod>(data['data']); } if (dataClassName == 'AuditChainReason') { - return deserialize<_i60.AuditChainReason>(data['data']); + return deserialize<_i61.AuditChainReason>(data['data']); } if (dataClassName == 'AuditIntegrityState') { - return deserialize<_i61.AuditIntegrityState>(data['data']); + return deserialize<_i62.AuditIntegrityState>(data['data']); } if (dataClassName == 'AuditRowSeal') { - return deserialize<_i62.AuditRowSeal>(data['data']); + return deserialize<_i63.AuditRowSeal>(data['data']); } if (dataClassName == 'ConsentPurpose') { - return deserialize<_i63.ConsentPurpose>(data['data']); + return deserialize<_i64.ConsentPurpose>(data['data']); } if (dataClassName == 'DataSubjectRequestStatus') { - return deserialize<_i64.DataSubjectRequestStatus>(data['data']); + return deserialize<_i65.DataSubjectRequestStatus>(data['data']); } if (dataClassName == 'DataSubjectRequestType') { - return deserialize<_i65.DataSubjectRequestType>(data['data']); + return deserialize<_i66.DataSubjectRequestType>(data['data']); } if (dataClassName == 'EscalationReason') { - return deserialize<_i66.EscalationReason>(data['data']); + return deserialize<_i67.EscalationReason>(data['data']); } if (dataClassName == 'RiskLevel') { - return deserialize<_i67.RiskLevel>(data['data']); + return deserialize<_i68.RiskLevel>(data['data']); } if (dataClassName == 'ScheduledNoticeStatus') { - return deserialize<_i68.ScheduledNoticeStatus>(data['data']); + return deserialize<_i69.ScheduledNoticeStatus>(data['data']); } if (dataClassName == 'SyncStatus') { - return deserialize<_i69.SyncStatus>(data['data']); + return deserialize<_i70.SyncStatus>(data['data']); } if (dataClassName == 'UserRole') { - return deserialize<_i70.UserRole>(data['data']); + return deserialize<_i71.UserRole>(data['data']); } if (dataClassName == 'VisitAuthorship') { - return deserialize<_i71.VisitAuthorship>(data['data']); + return deserialize<_i72.VisitAuthorship>(data['data']); } if (dataClassName == 'AdminInvalidRequestException') { - return deserialize<_i72.AdminInvalidRequestException>(data['data']); + return deserialize<_i73.AdminInvalidRequestException>(data['data']); } if (dataClassName == 'AlertDispatchUnavailableException') { - return deserialize<_i73.AlertDispatchUnavailableException>(data['data']); + return deserialize<_i74.AlertDispatchUnavailableException>(data['data']); } if (dataClassName == 'AlertPermissionException') { - return deserialize<_i74.AlertPermissionException>(data['data']); + return deserialize<_i75.AlertPermissionException>(data['data']); } if (dataClassName == 'AlertValidationException') { - return deserialize<_i75.AlertValidationException>(data['data']); + return deserialize<_i76.AlertValidationException>(data['data']); } if (dataClassName == 'AuthenticationFailedException') { - return deserialize<_i76.AuthenticationFailedException>(data['data']); + return deserialize<_i77.AuthenticationFailedException>(data['data']); } if (dataClassName == 'DataRightsException') { - return deserialize<_i77.DataRightsException>(data['data']); + return deserialize<_i78.DataRightsException>(data['data']); } if (dataClassName == 'EndpointDisabledException') { - return deserialize<_i78.EndpointDisabledException>(data['data']); + return deserialize<_i79.EndpointDisabledException>(data['data']); } if (dataClassName == 'EnrollmentException') { - return deserialize<_i79.EnrollmentException>(data['data']); + return deserialize<_i80.EnrollmentException>(data['data']); } if (dataClassName == 'MfaEnrollmentRequiredException') { - return deserialize<_i80.MfaEnrollmentRequiredException>(data['data']); + return deserialize<_i81.MfaEnrollmentRequiredException>(data['data']); } if (dataClassName == 'MfaRequiredException') { - return deserialize<_i81.MfaRequiredException>(data['data']); + return deserialize<_i82.MfaRequiredException>(data['data']); } if (dataClassName == 'NoticeDeliveryException') { - return deserialize<_i82.NoticeDeliveryException>(data['data']); + return deserialize<_i83.NoticeDeliveryException>(data['data']); } if (dataClassName == 'OtpRequestException') { - return deserialize<_i83.OtpRequestException>(data['data']); + return deserialize<_i84.OtpRequestException>(data['data']); } if (dataClassName == 'SessionExpiredException') { - return deserialize<_i84.SessionExpiredException>(data['data']); + return deserialize<_i85.SessionExpiredException>(data['data']); } if (dataClassName == 'MicroArea') { - return deserialize<_i85.MicroArea>(data['data']); + return deserialize<_i86.MicroArea>(data['data']); } if (dataClassName == 'OtpChallenge') { - return deserialize<_i86.OtpChallenge>(data['data']); + return deserialize<_i87.OtpChallenge>(data['data']); } if (dataClassName == 'Patient') { - return deserialize<_i87.Patient>(data['data']); + return deserialize<_i88.Patient>(data['data']); } if (dataClassName == 'PushToken') { - return deserialize<_i88.PushToken>(data['data']); + return deserialize<_i89.PushToken>(data['data']); } if (dataClassName == 'ScheduledNotice') { - return deserialize<_i89.ScheduledNotice>(data['data']); + return deserialize<_i90.ScheduledNotice>(data['data']); } if (dataClassName == 'StaffAccount') { - return deserialize<_i90.StaffAccount>(data['data']); + return deserialize<_i91.StaffAccount>(data['data']); } if (dataClassName == 'TermsVersion') { - return deserialize<_i91.TermsVersion>(data['data']); + return deserialize<_i92.TermsVersion>(data['data']); } if (dataClassName == 'TriageAnswer') { - return deserialize<_i92.TriageAnswer>(data['data']); + return deserialize<_i93.TriageAnswer>(data['data']); } if (dataClassName == 'TriageSession') { - return deserialize<_i93.TriageSession>(data['data']); + return deserialize<_i94.TriageSession>(data['data']); } if (dataClassName == 'Ubs') { - return deserialize<_i94.Ubs>(data['data']); + return deserialize<_i95.Ubs>(data['data']); } if (dataClassName == 'User') { - return deserialize<_i95.User>(data['data']); + return deserialize<_i96.User>(data['data']); } if (dataClassName == 'UserCredential') { - return deserialize<_i96.UserCredential>(data['data']); + return deserialize<_i97.UserCredential>(data['data']); } if (dataClassName == 'Visit') { - return deserialize<_i97.Visit>(data['data']); + return deserialize<_i98.Visit>(data['data']); } if (dataClassName.startsWith('serverpod.')) { data['className'] = dataClassName.substring(10); @@ -4371,42 +4392,42 @@ class Protocol extends _i1.SerializationManagerServer { return _i8.AlertIdempotencyKey.t; case _i9.AlertOutboxEntry: return _i9.AlertOutboxEntry.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; + case _i53.AuditChainCheckpoint: + return _i53.AuditChainCheckpoint.t; + case _i54.AuditExport: + return _i54.AuditExport.t; + case _i55.AuditLog: + return _i55.AuditLog.t; + case _i56.ConsentLog: + return _i56.ConsentLog.t; + case _i57.DataSubjectRequest: + return _i57.DataSubjectRequest.t; + case _i58.EnrollmentToken: + return _i58.EnrollmentToken.t; + case _i86.MicroArea: + return _i86.MicroArea.t; + case _i87.OtpChallenge: + return _i87.OtpChallenge.t; + case _i88.Patient: + return _i88.Patient.t; + case _i89.PushToken: + return _i89.PushToken.t; + case _i90.ScheduledNotice: + return _i90.ScheduledNotice.t; + case _i91.StaffAccount: + return _i91.StaffAccount.t; + case _i92.TermsVersion: + return _i92.TermsVersion.t; + case _i94.TriageSession: + return _i94.TriageSession.t; + case _i95.Ubs: + return _i95.Ubs.t; + case _i96.User: + return _i96.User.t; + case _i97.UserCredential: + return _i97.UserCredential.t; + case _i98.Visit: + return _i98.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 7d3c9fa..1d1ec43 100644 --- a/backend/sinalacs_server/lib/src/generated/protocol.yaml +++ b/backend/sinalacs_server/lib/src/generated/protocol.yaml @@ -32,6 +32,7 @@ alerts: - createRedAlert: - acknowledge: - resolve: + - openForMicroArea: - 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 689151e..97ae173 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 @@ -273,4 +273,49 @@ class OrmAlertStore implements AlertStore { resolvedAt: row.resolvedAt, ); } + + /// Duas consultas em vez de uma ordenação por status: os não confirmados + /// enchem o teto primeiro, e só o que sobra vai para os confirmados. Cada + /// grupo é lido do MAIS NOVO para trás e devolvido do mais antigo para a + /// frente: o corte cai sempre nos antigos, nunca num vermelho recém-chegado + /// a uma microárea com pendentes acumulados (INV-03). + @override + Future> openInMicroArea(String microAreaId, {required int limit}) async { + final area = UuidValue.fromString(microAreaId); + final naoConfirmados = await Alert.db.find( + _session(), + where: (t) => t.microAreaId.equals(area) & t.status.inSet({AlertStatus.pending, AlertStatus.escalated}), + orderBy: (t) => t.triggeredAt, + orderDescending: true, + limit: limit, + transaction: _transaction, + ); + final restante = limit - naoConfirmados.length; + final confirmados = restante <= 0 + ? const [] + : await Alert.db.find( + _session(), + where: (t) => t.microAreaId.equals(area) & t.status.equals(AlertStatus.acknowledged), + orderBy: (t) => t.triggeredAt, + orderDescending: true, + limit: restante, + transaction: _transaction, + ); + + return [ + for (final row in [...naoConfirmados.reversed, ...confirmados.reversed]) + OpenAlertSnapshot( + alertId: row.id!.uuid, + patientId: row.patientId.uuid, + microAreaId: microAreaId, + riskLevel: row.riskLevel, + locationHash: row.locationHash, + locationCell: row.locationCell, + triggeredAt: row.triggeredAt, + status: row.status, + acknowledgedAt: row.acknowledgedAt, + escalated: row.escalatedAt != null || row.status == AlertStatus.escalated, + ), + ]; + } } diff --git a/backend/sinalacs_server/lib/src/models/api/acs_open_alert.spy.yaml b/backend/sinalacs_server/lib/src/models/api/acs_open_alert.spy.yaml new file mode 100644 index 0000000..1e4df45 --- /dev/null +++ b/backend/sinalacs_server/lib/src/models/api/acs_open_alert.spy.yaml @@ -0,0 +1,18 @@ +### Alerta ainda não concluído da microárea do ACS, lido por HTTP (#80, R2): +### a reserva para quando o MQTT do aparelho não está conectado. +### +### Os campos espelham o envelope MQTT (`AlertDelivery.toJson`) mais o estado +### persistido (`status`, `acknowledgedAt`) e o marcador de reforço. Nada de +### `escalatedBy`, `escalationReason` ou `deviceId`: nenhuma identidade a mais. +class: AcsOpenAlert +fields: + alertId: String + patientId: String + microAreaId: String + riskLevel: RiskLevel + locationHash: String + locationCell: String? + triggeredAt: DateTime + status: AlertStatus + acknowledgedAt: DateTime? + escalated: bool diff --git a/backend/sinalacs_server/test/integration/acs_open_alerts_test.dart b/backend/sinalacs_server/test/integration/acs_open_alerts_test.dart new file mode 100644 index 0000000..875f8c6 --- /dev/null +++ b/backend/sinalacs_server/test/integration/acs_open_alerts_test.dart @@ -0,0 +1,273 @@ +import 'package:serverpod/serverpod.dart'; +import 'package:sinalacs_server/src/application/alerts/red_alert_service.dart'; +import 'package:sinalacs_server/src/config/app_config.dart'; +import 'package:sinalacs_server/src/domain/entities/alert_delivery.dart'; +import 'package:sinalacs_server/src/generated/protocol.dart'; +import 'package:sinalacs_server/src/runtime/alert_runtime.dart'; +import 'package:test/test.dart'; + +import '../support/health_data_fixtures.dart'; +import 'test_tools/serverpod_test_tools.dart'; + +/// Reserva por HTTP dos alertas do ACS (#80, R2): `alerts.openForMicroArea`. +/// +/// O caso que fecha o R2 no servidor é o primeiro: com o broker fora, o alerta +/// criado pelo paciente aparece para o ACS da microárea pelo RPC. +class _RecordingPublisher implements AlertPublisher { + final List published = []; + bool offline = false; + + @override + Future publish(AlertDelivery alert) async { + if (offline) throw Exception('broker indisponível'); + published.add(alert); + } +} + +/// Mesmos UUIDs sintéticos do seed de desenvolvimento, dos quais o dev-login +/// depende. Inseridos por caso porque o harness reverte o banco a cada teste. +const _patientId = '00000000-0000-4000-8000-000000000001'; +const _acsId = '00000000-0000-4000-8000-000000000002'; +const _microAreaId = '00000000-0000-4000-8000-000000000003'; +const _ubsId = '00000000-0000-4000-8000-000000000004'; +const _outraMicroAreaId = '00000000-0000-4000-8000-0000000000aa'; + +AppConfig _config() => AppConfig( + mqttBroker: 'localhost:1883', + jwtSecret: 'test-secret', + auditChainSecret: 'test-audit-chain-secret', + healthDataEncryptionKey: AppConfig.developmentHealthDataEncryptionKey, + cpfHashPepper: AppConfig.developmentCpfHashPepper, + smsGateway: 'log', + mqttUsername: null, + mqttPassword: null, + mqttUseTls: false, + mqttCaCertificatePath: null, + appEnv: 'development', + enableDevLogin: true, + ); + +Future _seed(Session session) async { + await Ubs.db.insertRow( + session, + Ubs( + id: UuidValue.fromString(_ubsId), + name: 'UBS Desenvolvimento', + address: 'Endereço local', + city: 'São Paulo', + state: 'SP', + ), + ); + for (final (id, nome) in [(_microAreaId, 'Microárea 12'), (_outraMicroAreaId, 'Microárea 13')]) { + await MicroArea.db.insertRow( + session, + MicroArea(id: UuidValue.fromString(id), name: nome, ubsId: UuidValue.fromString(_ubsId), geoJsonBoundary: '{}'), + ); + } + final now = DateTime.now().toUtc(); + await User.db.insert(session, [ + User( + id: UuidValue.fromString(_patientId), + cpfHash: 'development-patient', + name: 'Paciente de desenvolvimento', + birthDate: DateTime.utc(1990), + role: UserRole.patient, + microAreaId: UuidValue.fromString(_microAreaId), + createdAt: now, + updatedAt: now, + ), + User( + id: UuidValue.fromString(_acsId), + cpfHash: 'development-acs', + name: 'ACS de desenvolvimento', + birthDate: DateTime.utc(1980), + role: UserRole.acs, + microAreaId: UuidValue.fromString(_microAreaId), + createdAt: now, + updatedAt: now, + ), + ]); + await Patient.db.insertRow( + session, + await encryptedPatient(id: _patientId, emergencyContact: 'Contato de desenvolvimento', isChronic: false), + ); + await Acs.db.insertRow( + session, + Acs(id: UuidValue.fromString(_acsId), enrollmentId: 'ACS-001', ubsId: UuidValue.fromString(_ubsId), active: true), + ); +} + +/// Alerta inserido direto pelo ORM, para controlar `triggeredAt`, status e +/// microárea sem passar pelo relógio do serviço. +Future _alerta( + Session session, { + required DateTime triggeredAt, + AlertStatus status = AlertStatus.pending, + String microAreaId = _microAreaId, + DateTime? escalatedAt, +}) async { + final row = await Alert.db.insertRow( + session, + Alert( + patientId: UuidValue.fromString(_patientId), + acsId: UuidValue.fromString(_acsId), + microAreaId: UuidValue.fromString(microAreaId), + triggeredAt: triggeredAt, + acknowledgedAt: status == AlertStatus.acknowledged ? triggeredAt.add(const Duration(minutes: 1)) : null, + riskLevel: RiskLevel.red, + locationHash: 'hash-sintetico', + status: status, + mqttTopic: 'sinalacs/v1/microareas/$microAreaId/alerts', + deviceId: 'device-teste', + retryCount: 0, + version: 0, + escalatedAt: escalatedAt, + ), + ); + return row.id!.uuid; +} + +void main() { + withServerpod('Dada a reserva por HTTP dos alertas do ACS (#80)', (sessionBuilder, endpoints) { + late _RecordingPublisher publisher; + + setUp(() { + publisher = _RecordingPublisher(); + AlertRuntime.instance + ..overridePublisher(publisher) + ..overrideConfig(_config()); + }); + + tearDown(() { + AlertRuntime.instance + ..overridePublisher(null) + ..overrideConfig(null); + }); + + Future tokenDoAcs() async => + (await endpoints.auth.developmentLogin(sessionBuilder, role: 'acs')).accessToken; + + test('com o broker fora, o alerta criado aparece para o ACS da microárea', () async { + await _seed(sessionBuilder.build()); + publisher.offline = true; + final paciente = await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + final criado = await endpoints.alerts.createRedAlert( + sessionBuilder, + accessToken: paciente.accessToken, + idempotencyKey: 'reserva-http-1', + locationHash: 'hash-sintetico', + ); + expect(criado.published, isFalse); + + final abertos = await endpoints.alerts.openForMicroArea(sessionBuilder, accessToken: await tokenDoAcs()); + + expect(abertos, hasLength(1)); + final a = abertos.single; + expect(a.alertId, criado.alertId); + expect(a.status, AlertStatus.pending); + expect(a.riskLevel, RiskLevel.red); + expect(a.escalated, isFalse); + expect(a.microAreaId, _microAreaId); + expect(a.patientId, _patientId); + expect(a.locationHash, 'hash-sintetico'); + expect(a.acknowledgedAt, isNull); + }); + + test('confirmado continua listado; concluído sai', () 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: 'reserva-http-2', + locationHash: 'hash-sintetico', + ); + final acs = await tokenDoAcs(); + + await endpoints.alerts.acknowledge(sessionBuilder, accessToken: acs, alertId: criado.alertId); + final depoisDoAck = await endpoints.alerts.openForMicroArea(sessionBuilder, accessToken: acs); + expect(depoisDoAck.single.status, AlertStatus.acknowledged); + expect(depoisDoAck.single.acknowledgedAt, isNotNull); + + await endpoints.alerts.resolve(sessionBuilder, accessToken: acs, alertId: criado.alertId); + expect(await endpoints.alerts.openForMicroArea(sessionBuilder, accessToken: acs), isEmpty); + }); + + test('não confirmados vêm antes dos confirmados, cada grupo do mais antigo ao mais novo', () async { + final session = sessionBuilder.build(); + await _seed(session); + final t0 = DateTime.utc(2026, 10, 17, 10); + final confirmado = await _alerta(session, triggeredAt: t0, status: AlertStatus.acknowledged); + final maisNovo = await _alerta(session, triggeredAt: t0.add(const Duration(minutes: 2))); + final maisAntigo = await _alerta(session, triggeredAt: t0.add(const Duration(minutes: 1))); + + final abertos = await endpoints.alerts.openForMicroArea(sessionBuilder, accessToken: await tokenDoAcs()); + + expect(abertos.map((a) => a.alertId), [maisAntigo, maisNovo, confirmado]); + }); + + test('escalonado aparece com escalated: true', () async { + final session = sessionBuilder.build(); + await _seed(session); + final t0 = DateTime.utc(2026, 10, 17, 10); + await _alerta(session, triggeredAt: t0, escalatedAt: t0.add(const Duration(minutes: 5))); + await _alerta(session, triggeredAt: t0.add(const Duration(minutes: 1)), status: AlertStatus.escalated); + + final abertos = await endpoints.alerts.openForMicroArea(sessionBuilder, accessToken: await tokenDoAcs()); + + expect(abertos, hasLength(2)); + expect(abertos.every((a) => a.escalated), isTrue); + }); + + test('ACS de outra microárea não vê', () async { + final session = sessionBuilder.build(); + await _seed(session); + await _alerta(session, triggeredAt: DateTime.utc(2026, 10, 17, 10), microAreaId: _outraMicroAreaId); + + expect(await endpoints.alerts.openForMicroArea(sessionBuilder, accessToken: await tokenDoAcs()), isEmpty); + }); + + test('paciente é recusado', () async { + await _seed(sessionBuilder.build()); + final paciente = await endpoints.auth.developmentLogin(sessionBuilder, role: 'patient'); + + await expectLater( + endpoints.alerts.openForMicroArea(sessionBuilder, accessToken: paciente.accessToken), + throwsA(isA()), + ); + }); + + test('não grava linha de auditoria de leitura', () async { + final session = sessionBuilder.build(); + await _seed(session); + await _alerta(session, triggeredAt: DateTime.utc(2026, 10, 17, 10)); + final acs = await tokenDoAcs(); + final antes = await AuditLog.db.count(session); + + await endpoints.alerts.openForMicroArea(sessionBuilder, accessToken: acs); + + expect(await AuditLog.db.count(session), antes); + }); + + test('corta em 200 sem nunca deixar de fora o pendente mais novo', () async { + final session = sessionBuilder.build(); + await _seed(session); + final t0 = DateTime.utc(2026, 10, 17, 10); + final ids = []; + for (var i = 0; i <= RedAlertService.openAlertsLimit; i++) { + ids.add(await _alerta(session, triggeredAt: t0.add(Duration(seconds: i)))); + } + + final abertos = await endpoints.alerts.openForMicroArea(sessionBuilder, accessToken: await tokenDoAcs()); + + expect(abertos, hasLength(200)); + // O corte cai nos mais ANTIGOS: um vermelho novo numa microárea com + // pendentes acumulados é justamente o que a reserva existe para entregar. + expect(abertos.map((a) => a.alertId), contains(ids.last)); + expect(abertos.map((a) => a.alertId), isNot(contains(ids.first))); + // E a resposta segue do mais antigo ao mais novo. + expect(abertos.first.alertId, ids[1]); + expect(abertos.last.alertId, ids.last); + }); + }); +} 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 6cb0800..21b4fa3 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 @@ -55,37 +55,38 @@ import 'package:sinalacs_server/src/generated/api/alert_ack_result.dart' as _i26; import 'package:sinalacs_server/src/generated/api/alert_resolve_result.dart' as _i27; +import 'package:sinalacs_server/src/generated/api/acs_open_alert.dart' as _i28; import 'package:sinalacs_server/src/generated/api/alert_status_result.dart' - as _i28; -import 'package:sinalacs_server/src/generated/api/development_login_result.dart' as _i29; -import 'package:sinalacs_server/src/generated/api/totp_enrollment_start.dart' +import 'package:sinalacs_server/src/generated/api/development_login_result.dart' as _i30; -import 'package:sinalacs_server/src/generated/api/service_health.dart' as _i31; +import 'package:sinalacs_server/src/generated/api/totp_enrollment_start.dart' + as _i31; +import 'package:sinalacs_server/src/generated/api/service_health.dart' as _i32; import 'package:sinalacs_server/src/generated/api/notice_send_result.dart' - as _i32; -import 'package:sinalacs_server/src/generated/api/enrollment_token_result.dart' as _i33; -import 'package:sinalacs_server/src/generated/api/enrollment_result.dart' +import 'package:sinalacs_server/src/generated/api/enrollment_token_result.dart' as _i34; -import 'package:sinalacs_server/src/generated/api/micro_area_patient.dart' +import 'package:sinalacs_server/src/generated/api/enrollment_result.dart' as _i35; -import 'package:sinalacs_server/src/generated/api/patient_data_overview.dart' +import 'package:sinalacs_server/src/generated/api/micro_area_patient.dart' as _i36; -import 'package:sinalacs_server/src/generated/api/patient_consent_record.dart' +import 'package:sinalacs_server/src/generated/api/patient_data_overview.dart' as _i37; -import 'package:sinalacs_server/src/generated/enums/consent_purpose.dart' +import 'package:sinalacs_server/src/generated/api/patient_consent_record.dart' as _i38; -import 'package:sinalacs_server/src/generated/api/terms_change_notice.dart' +import 'package:sinalacs_server/src/generated/enums/consent_purpose.dart' as _i39; -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 _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/patient_data_subject_request_record.dart' + as _i41; +import 'package:sinalacs_server/src/generated/api/triage_result.dart' as _i42; +import 'package:sinalacs_server/src/generated/api/ubs_contact.dart' as _i43; import 'package:sinalacs_server/src/generated/api/visit_sync_result.dart' - as _i43; -import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' as _i44; +import 'package:sinalacs_server/src/generated/api/visit_sync_entry.dart' + as _i45; 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'; @@ -1434,7 +1435,38 @@ class _AlertsEndpoint { }); } - _i3.Future<_i28.AlertStatusResult> statusFor( + _i3.Future> openForMicroArea( + _i1.TestSessionBuilder sessionBuilder, { + required String accessToken, + }) async { + return _i1.callAwaitableFunctionAndHandleExceptions(() async { + var _localUniqueSession = + (sessionBuilder as _i1.InternalTestSessionBuilder).internalBuild( + endpoint: 'alerts', + method: 'openForMicroArea', + ); + try { + var _localCallContext = await _endpointDispatch.getMethodCallContext( + createSessionCallback: (_) => _localUniqueSession, + endpointPath: 'alerts', + methodName: 'openForMicroArea', + parameters: _i1.testObjectToJson({'accessToken': accessToken}), + serializationManager: _serializationManager, + ); + var _localReturnValue = + await (_localCallContext.method.call( + _localUniqueSession, + _localCallContext.arguments, + ) + as _i3.Future>); + return _localReturnValue; + } finally { + await _localUniqueSession.close(); + } + }); + } + + _i3.Future<_i29.AlertStatusResult> statusFor( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -1457,7 +1489,7 @@ class _AlertsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i28.AlertStatusResult>); + as _i3.Future<_i29.AlertStatusResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1476,7 +1508,7 @@ class _AuthEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i29.DevelopmentLoginResult> developmentLogin( + _i3.Future<_i30.DevelopmentLoginResult> developmentLogin( _i1.TestSessionBuilder sessionBuilder, { required String role, }) async { @@ -1499,7 +1531,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i29.DevelopmentLoginResult>); + as _i3.Future<_i30.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1507,7 +1539,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i29.DevelopmentLoginResult> loginInstitutional( + _i3.Future<_i30.DevelopmentLoginResult> loginInstitutional( _i1.TestSessionBuilder sessionBuilder, { required String matricula, required String password, @@ -1538,7 +1570,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i29.DevelopmentLoginResult>); + as _i3.Future<_i30.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1546,7 +1578,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i29.DevelopmentLoginResult> loginStaff( + _i3.Future<_i30.DevelopmentLoginResult> loginStaff( _i1.TestSessionBuilder sessionBuilder, { required String matricula, required String password, @@ -1575,7 +1607,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i29.DevelopmentLoginResult>); + as _i3.Future<_i30.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1583,7 +1615,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i30.TotpEnrollmentStart> beginStaffTotpEnrollment( + _i3.Future<_i31.TotpEnrollmentStart> beginStaffTotpEnrollment( _i1.TestSessionBuilder sessionBuilder, { required String matricula, required String password, @@ -1612,7 +1644,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i30.TotpEnrollmentStart>); + as _i3.Future<_i31.TotpEnrollmentStart>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1659,7 +1691,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i29.DevelopmentLoginResult> refreshSession( + _i3.Future<_i30.DevelopmentLoginResult> refreshSession( _i1.TestSessionBuilder sessionBuilder, { required String refreshToken, required String deviceId, @@ -1686,7 +1718,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i29.DevelopmentLoginResult>); + as _i3.Future<_i30.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1725,7 +1757,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i30.TotpEnrollmentStart> beginTotpEnrollment( + _i3.Future<_i31.TotpEnrollmentStart> beginTotpEnrollment( _i1.TestSessionBuilder sessionBuilder, { required String matricula, required String password, @@ -1752,7 +1784,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i30.TotpEnrollmentStart>); + as _i3.Future<_i31.TotpEnrollmentStart>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1832,7 +1864,7 @@ class _AuthEndpoint { }); } - _i3.Future<_i29.DevelopmentLoginResult> verifyOtp( + _i3.Future<_i30.DevelopmentLoginResult> verifyOtp( _i1.TestSessionBuilder sessionBuilder, { required String cpf, required String code, @@ -1861,7 +1893,7 @@ class _AuthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i29.DevelopmentLoginResult>); + as _i3.Future<_i30.DevelopmentLoginResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1928,7 +1960,7 @@ class _HealthEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i31.ServiceHealth> check( + _i3.Future<_i32.ServiceHealth> check( _i1.TestSessionBuilder sessionBuilder, ) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { @@ -1950,7 +1982,7 @@ class _HealthEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i31.ServiceHealth>); + as _i3.Future<_i32.ServiceHealth>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -1969,7 +2001,7 @@ class _NoticesEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i32.NoticeSendResult> sendSegmented( + _i3.Future<_i33.NoticeSendResult> sendSegmented( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required String title, @@ -2000,7 +2032,7 @@ class _NoticesEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i32.NoticeSendResult>); + as _i3.Future<_i33.NoticeSendResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2019,7 +2051,7 @@ class _OnboardingEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i33.EnrollmentTokenResult> generateEnrollmentToken( + _i3.Future<_i34.EnrollmentTokenResult> generateEnrollmentToken( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required String patientId, @@ -2046,7 +2078,7 @@ class _OnboardingEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i33.EnrollmentTokenResult>); + as _i3.Future<_i34.EnrollmentTokenResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2054,7 +2086,7 @@ class _OnboardingEndpoint { }); } - _i3.Future<_i34.EnrollmentResult> completeEnrollment( + _i3.Future<_i35.EnrollmentResult> completeEnrollment( _i1.TestSessionBuilder sessionBuilder, { required String token, required bool healthDataConsent, @@ -2087,7 +2119,7 @@ class _OnboardingEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i34.EnrollmentResult>); + as _i3.Future<_i35.EnrollmentResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2106,7 +2138,7 @@ class _PatientsEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future> listMicroArea( + _i3.Future> listMicroArea( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2129,7 +2161,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future>); + as _i3.Future>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2203,7 +2235,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i36.PatientDataOverview> myData( + _i3.Future<_i37.PatientDataOverview> myData( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2226,7 +2258,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i36.PatientDataOverview>); + as _i3.Future<_i37.PatientDataOverview>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2234,10 +2266,10 @@ class _PatientsEndpoint { }); } - _i3.Future<_i37.PatientConsentRecord> updateConsent( + _i3.Future<_i38.PatientConsentRecord> updateConsent( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, - required _i38.ConsentPurpose purpose, + required _i39.ConsentPurpose purpose, required bool granted, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { @@ -2263,7 +2295,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i37.PatientConsentRecord>); + as _i3.Future<_i38.PatientConsentRecord>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2271,7 +2303,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i37.PatientConsentRecord> acceptTermsOfUse( + _i3.Future<_i38.PatientConsentRecord> acceptTermsOfUse( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2294,7 +2326,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i37.PatientConsentRecord>); + as _i3.Future<_i38.PatientConsentRecord>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2336,7 +2368,7 @@ class _PatientsEndpoint { _i3.Future hasGrantedConsent( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, - required _i38.ConsentPurpose purpose, + required _i39.ConsentPurpose purpose, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = @@ -2368,7 +2400,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i39.TermsChangeNotice?> termsChangeNotice( + _i3.Future<_i40.TermsChangeNotice?> termsChangeNotice( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2391,7 +2423,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i39.TermsChangeNotice?>); + as _i3.Future<_i40.TermsChangeNotice?>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2399,7 +2431,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i40.PatientDataSubjectRequestRecord> requestDataDeletion( + _i3.Future<_i41.PatientDataSubjectRequestRecord> requestDataDeletion( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2422,7 +2454,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i40.PatientDataSubjectRequestRecord>); + as _i3.Future<_i41.PatientDataSubjectRequestRecord>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2430,7 +2462,7 @@ class _PatientsEndpoint { }); } - _i3.Future<_i40.PatientDataSubjectRequestRecord> requestDataCorrection( + _i3.Future<_i41.PatientDataSubjectRequestRecord> requestDataCorrection( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required String details, @@ -2457,7 +2489,7 @@ class _PatientsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i40.PatientDataSubjectRequestRecord>); + as _i3.Future<_i41.PatientDataSubjectRequestRecord>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2476,7 +2508,7 @@ class _TriageEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i41.TriageResult> evaluate( + _i3.Future<_i42.TriageResult> evaluate( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required bool chestPain, @@ -2513,7 +2545,7 @@ class _TriageEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i41.TriageResult>); + as _i3.Future<_i42.TriageResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2532,7 +2564,7 @@ class _UbsEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future<_i42.UbsContact> myContact( + _i3.Future<_i43.UbsContact> myContact( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, }) async { @@ -2555,7 +2587,7 @@ class _UbsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i42.UbsContact>); + as _i3.Future<_i43.UbsContact>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2574,10 +2606,10 @@ class _VisitsEndpoint { final _i2.SerializationManager _serializationManager; - _i3.Future> sync( + _i3.Future> sync( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, - required List<_i44.VisitSyncEntry> visits, + required List<_i45.VisitSyncEntry> visits, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = @@ -2601,7 +2633,7 @@ class _VisitsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future>); + as _i3.Future>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2609,10 +2641,10 @@ class _VisitsEndpoint { }); } - _i3.Future<_i43.VisitSyncResult> resolveConflict( + _i3.Future<_i44.VisitSyncResult> resolveConflict( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, - required _i44.VisitSyncEntry visit, + required _i45.VisitSyncEntry visit, required int expectedServerVersion, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { @@ -2638,7 +2670,7 @@ class _VisitsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future<_i43.VisitSyncResult>); + as _i3.Future<_i44.VisitSyncResult>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2646,11 +2678,11 @@ class _VisitsEndpoint { }); } - _i3.Future> syncLegacy( + _i3.Future> syncLegacy( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required String deviceId, - required List<_i44.VisitSyncEntry> visits, + required List<_i45.VisitSyncEntry> visits, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = @@ -2675,7 +2707,7 @@ class _VisitsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future>); + as _i3.Future>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2683,11 +2715,11 @@ class _VisitsEndpoint { }); } - _i3.Future> syncDeferred( + _i3.Future> syncDeferred( _i1.TestSessionBuilder sessionBuilder, { required String uploadToken, required String deviceId, - required List<_i44.VisitSyncEntry> visits, + required List<_i45.VisitSyncEntry> visits, }) async { return _i1.callAwaitableFunctionAndHandleExceptions(() async { var _localUniqueSession = @@ -2712,7 +2744,7 @@ class _VisitsEndpoint { _localUniqueSession, _localCallContext.arguments, ) - as _i3.Future>); + as _i3.Future>); return _localReturnValue; } finally { await _localUniqueSession.close(); @@ -2751,7 +2783,7 @@ class _VisitsEndpoint { }); } - _i3.Future> pull( + _i3.Future> pull( _i1.TestSessionBuilder sessionBuilder, { required String accessToken, required DateTime since, @@ -2778,7 +2810,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 5303f21..81cf1a2 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 @@ -120,6 +120,9 @@ class _MemoryStore implements AlertStore { @override Future latestForPatient(String patientId) async => null; + @override + Future> openInMicroArea(String microAreaId, {required int limit}) async => []; + @override Future resolve({ required String alertId, 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 06f339e..5c77170 100644 --- a/backend/sinalacs_server/test/unit/red_alert_lifecycle_test.dart +++ b/backend/sinalacs_server/test/unit/red_alert_lifecycle_test.dart @@ -44,6 +44,11 @@ class RecordingStore implements AlertStore { throw UnimplementedError('não usado nos testes deste arquivo'); } + @override + Future> openInMicroArea(String microAreaId, {required int limit}) async { + throw UnimplementedError('não usado nos testes deste arquivo'); + } + @override Future resolve({ required String alertId, 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 b4f43ec..ab5777b 100644 --- a/backend/sinalacs_server/test/unit/red_alert_service_test.dart +++ b/backend/sinalacs_server/test/unit/red_alert_service_test.dart @@ -40,6 +40,16 @@ class FakeAlertStore implements AlertStore { final Map statusByPatient = {}; + /// Última consulta de alertas abertos (#80): microárea e teto pedidos. + (String, int)? lastOpenQuery; + List openAlerts = []; + + @override + Future> openInMicroArea(String microAreaId, {required int limit}) async { + lastOpenQuery = (microAreaId, limit); + return openAlerts; + } + @override Future latestForPatient(String patientId) async => statusByPatient[patientId]; @@ -438,4 +448,47 @@ void main() { }, ); }); -} \ No newline at end of file + + group('openForAcs (#80)', () { + const acs = AuthenticatedUser(id: 'acs-001', role: UserRole.acs, microAreaId: 'area-12', deviceId: 'device-001'); + + test('lista pela microárea do token e com o teto de 200', () async { + final store = FakeAlertStore(); + final aberto = OpenAlertSnapshot( + alertId: 'alerta-1', + patientId: 'patient-001', + microAreaId: 'area-12', + riskLevel: RiskLevel.red, + locationHash: '6gyf4bf', + triggeredAt: DateTime.utc(2026, 10, 17, 12), + status: AlertStatus.pending, + escalated: false, + ); + store.openAlerts = [aberto]; + final service = RedAlertService(store: store, outbox: FakeAlertOutbox()); + + final lista = await service.openForAcs(user: acs); + + expect(store.lastOpenQuery, ('area-12', 200)); + expect(RedAlertService.openAlertsLimit, 200); + expect(lista, [aberto]); + }); + + test('paciente não lista alertas da microárea', () async { + final store = FakeAlertStore(); + final service = RedAlertService(store: store, outbox: FakeAlertOutbox()); + + expect(() => service.openForAcs(user: patient), throwsA(isA())); + expect(store.lastOpenQuery, isNull); + }); + + test('ACS sem microárea é recusado', () async { + final store = FakeAlertStore(); + final service = RedAlertService(store: store, outbox: FakeAlertOutbox()); + const semArea = AuthenticatedUser(id: 'acs-002', role: UserRole.acs, microAreaId: null, deviceId: 'device-002'); + + expect(() => service.openForAcs(user: semArea), throwsA(isA())); + expect(store.lastOpenQuery, isNull); + }); + }); +} diff --git a/docs/auditoria-prd/2026-10-15-lacunas-prd.md b/docs/auditoria-prd/2026-10-15-lacunas-prd.md index c3d526f..3d456bd 100644 --- a/docs/auditoria-prd/2026-10-15-lacunas-prd.md +++ b/docs/auditoria-prd/2026-10-15-lacunas-prd.md @@ -71,7 +71,7 @@ próprio; o conferidor exige apenas os IDs do PRD. |----|-----------|--------|-----------|-------------| | INV-01 | ACS só vê pacientes da própria microárea | **entregue** | Recusa de outra microárea testada por endpoint: `patients.listMicroArea` e `visits.pull` (`patient_directory_and_territory_test.dart`), `visits.sync` (`visit_sync_service_test.dart`, rejeição de território), `onboarding.generateEnrollmentToken` (`onboarding_service_test.dart:75`), `alerts.acknowledge` (`red_alert_discard_paths_test.dart`), `notices.sendSegmented` (`notices_endpoint_test.dart`), staff por UBS (`admin_scope_test.dart`); todos passam (2026-10-15). | | | INV-02 | Classificação de risco não alterável por humano | **entregue** | Nenhum endpoint recebe risco do cliente: `createRedAlert` aceita só `idempotencyKey`, `locationHash` e `locationCell` (`alerts_endpoint.dart`); `triage.evaluate` recebe os sinais e o servidor classifica; o app do paciente não calcula risco (`apps/patient/lib/app/app.dart:1424-1428`). | | -| INV-03 | Alerta vermelho nunca perdido | **entregue** | Outbox MQTT com ACK, retry e reforço; publicação confirmada por PUBACK (`mqtt_alert_dispatcher_resilience_test.dart`, #56), caminhos de descarte cobertos (`red_alert_discard_paths_test.dart`), `red_alert_cycle_test.dart`; e2e `scripts/qa/mqtt_resiliencia_e2e.sh`; 10/10 alertas entregues na medição de latência. | O ACS só recebe alerta por MQTT: não há reserva por HTTP se o broker ficar inalcançável (ver R2). | +| INV-03 | Alerta vermelho nunca perdido | **entregue** | Outbox MQTT com ACK, retry e reforço; publicação confirmada por PUBACK (`mqtt_alert_dispatcher_resilience_test.dart`, #56), caminhos de descarte cobertos (`red_alert_discard_paths_test.dart`), `red_alert_cycle_test.dart`; e2e `scripts/qa/mqtt_resiliencia_e2e.sh`; 10/10 alertas entregues na medição de latência. | Reserva por HTTP entregue na #80 (ver R2); com o app em segundo plano e o broker fora, o alerta só aparece quando o ACS volta ao app. | | INV-04 | Dado de saúde nunca em texto plano | **entregue** | Colunas clínicas cifradas em AES-256-GCM na aplicação (`*Encrypted`/`*KeyVersion` para condições crônicas, respostas de triagem e notas de visita); SQLCipher no ACS (`apps/acs/lib/core/database/encrypted_database.dart`). Desde 2026-10-16 (#75): riscos de triagem e visita cifrados (`resultRiskEncrypted`, `riskLevelBefore/AfterEncrypted`); `alerts.riskLevel` legível por decisão registrada no PRD (INV-04). | — | | INV-05 | Paciente nunca acessa dado de outro paciente | **entregue** | Token do paciente carrega o `patientId` e os endpoints de paciente leem só dele: `myData`, consentimentos e pedidos (`patient_data_overview_endpoint_test.dart`, `data_subject_rights_endpoint_test.dart`), `statusFor` (`authorization_test.dart`, `red_alert_cycle_test.dart`), token de push por dono (`push_token_endpoint_test.dart`); passam (2026-10-15). | | @@ -211,7 +211,7 @@ próprio; o conferidor exige apenas os IDs do PRD. | ID | Requisito | Status | Evidência | O que falta | |----|-----------|--------|-----------|-------------| | R1 | Colisão de sincronização | **entregue** | Conflito detectado sem sobrescrever (`visit_sync_service.dart`), guardado fora da retentativa com a cópia do servidor (schema v8, `offline_visit_queue.dart`), tela de comparação lado a lado e decisão do ACS (`resolve_conflicts`, `conflict_*`) e `visits.resolveConflict` com compare-and-set. Emulador: `acs_full_e2e.sh`, 6/6 em 2026-10-16 (`docs/superpowers/plans/2026-10-16-issue-79-e2e.log`). | Conflito de visita legada segue sem tela (quarentena); "manter a do servidor" não deixa trilha no servidor; ver abertos em `PROGRESS.md`, #79. | -| R2 | Falha do MQTT em rede 3G | **parcial** | Reconexão com backoff e sessão persistente no MQTT; outbox no servidor reentrega (`mqtt_resiliencia_e2e.sh`); visitas já vão por HTTP. | Sem reserva por HTTP para o ACS receber alertas quando o MQTT cai (o `backend_client.dart` do ACS não lista alertas pendentes). | +| R2 | Falha do MQTT em rede 3G | **entregue** (2026-10-17, #80) | Reconexão com backoff e sessão persistente no MQTT; outbox no servidor reentrega (`mqtt_resiliencia_e2e.sh`); visitas já vão por HTTP; reserva por HTTP dos alertas: `alerts.openForMicroArea` consultado pelo ACS a cada 30 s enquanto o broker está fora (`AlertHttpFallback`, `AlertQueue.mergeFromServer`); no `emulator-5554`, alerta sem outbox chegou à tela em 29,6 s e foi confirmado (`scripts/qa/acs_alert_fallback_e2e.sh`). | Consulta só em primeiro plano (sem WorkManager, fora de `spec/stack.md`); nenhum alerta gravado no aparelho (decisão D5 da #80). | | R3 | Vazamento de PII no dispositivo | **parcial** | Limpeza do aparelho pelo próprio ACS (`apps/acs/lib/app/app.dart:174`, `device_wipe_test.dart`); chave do SQLCipher no Keystore. | Remote wipe pela API (não existe login de supervisor para dispará-lo); rotação de chave. | | R4 | Rejeição do ACS | **externo** | Nada no código. | NPS semanal e treinamento (produto + UBS). | | R5 | Performance do PostgreSQL | **parcial** | 46 índices na migração `20261014155304871`, incluindo `alerts_micro_area_status_idx` e `users_micro_area_idx`. | Particionamento por microárea não existe; sem teste de carga que mostre necessidade (M3.3). | @@ -230,7 +230,7 @@ Critério: **P0** toca invariante ou o núcleo do MVP (§6.1) sem critério atin | 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~~ — resolvido na #78 (2026-10-16): broker só 1.3, Traefik → backend em TLS 1.3 verificado; mTLS interno bloqueado pelo Serverpod | 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; entregue em 2026-10-16 (#79); antes, a visita em conflito ficava parada na fila para sempre. | -| 6 | P1 | Reserva por HTTP para o ACS receber alertas quando o MQTT cai | R2, INV-03 | código | Risco de impacto 5 em rede 3G; o ACS não tem como listar alertas pendentes por HTTP. | +| 6 | P1 | ~~Reserva por HTTP para o ACS receber alertas quando o MQTT cai~~ — resolvido na #80 (2026-10-17): `alerts.openForMicroArea` consultado a cada 30 s com o broker fora | R2, INV-03 | código | Risco de impacto 5 em rede 3G; antes, o ACS não tinha como listar alertas pendentes por HTTP. | | 7 | P1 | Remote wipe e certificado de cliente por aparelho | R3, STRIDE-S, CRIPTO-APP-CACHE | código + infra | Risco de impacto 5; depende de login de supervisor e de PKI de produção. | | 8 | P1 | Auditoria append-only no banco (trigger/`REVOKE`) | RF17, STRIDE-R | código | A cadeia só detecta; o PRD pede logs imutáveis. | | 9 | P1 | Telemetria estruturada dos eventos do §1.3 | EVT-*, RNF02, H1, H2, H4, MOD-RELATORIOS | código | Sem os eventos, a North Star e três hipóteses não são mensuráveis no piloto. | diff --git a/docs/superpowers/plans/2026-10-17-issue-80-e2e.log b/docs/superpowers/plans/2026-10-17-issue-80-e2e.log new file mode 100644 index 0000000..0446ce3 --- /dev/null +++ b/docs/superpowers/plans/2026-10-17-issue-80-e2e.log @@ -0,0 +1,101 @@ +== stack de e2e (banco de teste) +aviso: o backend em execução (sinalacs-serverpod) será substituído pelo de e2e; ao final, rode `docker compose up -d` para voltar à stack de desenvolvimento. +NOTICE: database "sinalacs_e2e" does not exist, skipping +Fixtures de e2e gravadas (5 pacientes, 2 ACS, 1 admin, 1 coordenador) em ../../.e2e/fixtures.json. +== reserva por HTTP no emulador (intervalo de produção: 30 s) +Resolving dependencies... +Downloading packages... + async 2.11.0 (2.13.1 available) + boolean_selector 2.1.1 (2.1.2 available) + clock 1.1.2 (1.1.3 available) + connectivity_plus 7.3.1 (7.3.2 available) + dbus 0.7.12 (0.8.0 available) + ffi 2.1.3 (2.2.0 available) + flutter_lints 3.0.2 (6.0.0 available) + flutter_secure_storage 9.2.4 (11.2.0 available) + flutter_secure_storage_linux 1.2.3 (3.0.3 available) + flutter_secure_storage_macos 3.1.3 (4.0.0 available) + flutter_secure_storage_platform_interface 1.1.2 (2.1.1 available) + flutter_secure_storage_web 1.2.1 (2.1.1 available) + flutter_secure_storage_windows 3.1.2 (4.2.2 available) + geolocator 12.0.0 (14.1.1 available) + geolocator_android 4.6.2 (5.1.1+1 available) + geolocator_apple 2.3.14 (2.3.15 available) + geolocator_platform_interface 4.3.0 (4.4.0 available) + google_maps_flutter 2.18.0 (2.19.0 available) + google_maps_flutter_android 2.19.13 (2.21.0 available) + google_maps_flutter_ios 2.18.6 (2.18.7 available) + google_maps_flutter_platform_interface 2.16.1 (2.17.0 available) + google_maps_flutter_web 0.6.3+1 (0.6.4+1 available) + html 0.15.6 (0.15.7 available) + jni 1.0.3 (1.1.0 available) + jni_flutter 1.0.3 (1.0.4+1 available) + js 0.6.7 (0.7.2 available) + lints 3.0.0 (6.1.0 available) + local_auth_android 2.2.0 (2.3.0 available) + matcher 0.12.19 (0.12.20 available) + material_color_utilities 0.13.0 (0.13.1 available) + meta 1.18.0 (1.19.0 available) + mqtt_client 10.5.1 (10.12.0 available) + nm 0.5.0 (0.6.0 available) + path_provider_foundation 2.5.1 (2.6.0 available) + petitparser 6.0.2 (7.1.0 available) + platform 3.1.6 (3.2.0 available) + qr 3.0.2 (4.0.0 available) + serverpod_auth_core_client 3.4.13 (4.0.4 available) + serverpod_auth_idp_client 3.4.13 (4.0.4 available) + serverpod_client 3.4.13 (4.0.4 available) + serverpod_serialization 3.4.13 (4.0.4 available) + shared_preferences 2.5.5 (2.5.6 available) + shared_preferences_android 2.4.28 (2.5.0 available) + source_span 1.10.0 (1.10.2 available) + sqflite 2.4.1 (2.4.4+2 available) + sqflite_android 2.4.0 (2.4.4+1 available) + sqflite_common 2.5.4+6 (2.5.14 available) + sqflite_common_ffi 2.3.4+4 (2.4.4 available) + sqflite_darwin 2.4.1+1 (2.4.4+1 available) + sqflite_platform_interface 2.4.0 (2.4.2 available) + sqflite_sqlcipher 3.2.0 (3.4.1 available) + sqlite3 2.9.4 (3.7.1 available) + stack_trace 1.12.1 (1.12.2 available) + string_scanner 1.2.0 (1.4.1 available) + synchronized 3.3.0+3 (3.5.0 available) + term_glyph 1.2.1 (1.2.2 available) + test_api 0.7.11 (0.7.15 available) + universal_html 2.2.4 (2.3.0 available) + universal_io 2.2.2 (2.3.1 available) + url_launcher 6.3.2 (6.3.3 available) + vector_math 2.2.0 (2.4.3 available) + vm_service 14.2.5 (15.3.0 available) + win32 5.13.0 (6.4.0 available) + xml 6.5.0 (7.1.0 available) +Got dependencies! +64 packages have newer versions incompatible with dependency constraints. +Try `flutter pub outdated` for more information. +00:00 +0: loading /home/rock/Documents/Dev/APPs/SinalACS/apps/acs/integration_test/alert_http_fallback_e2e.dart +Running Gradle task 'assembleDebug'... +WARNING: A restricted method in java.lang.System has been called +WARNING: java.lang.System::load has been called by net.rubygrapefruit.platform.internal.NativeLibraryLoader in an unnamed module (file:/home/rock/.gradle/wrapper/dists/gradle-9.1.0-all/7wzd0jkjit61aq2p43wpjgij9/gradle-9.1.0/lib/native-platform-0.22-milestone-28.jar) +WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module +WARNING: Restricted methods will be blocked in a future release unless native access is enabled + +Running Gradle task 'assembleDebug'... 13.1s +✓ Built build/app/outputs/flutter-apk/app-debug.apk +Installing build/app/outputs/flutter-apk/app-debug.apk... 581ms +00:00 +0: com o MQTT fora, o alerta chega pela reserva HTTP e o ACK não é desfeito +E2E_DISPARAR_ALERTA id=7298ae4a-9c51-440b-bd43-3433b1806889 + [script] alerta 7298ae4a-9c51-440b-bd43-3433b1806889 inserido (09:53:30) +E2E_ALERTA_VIA_HTTP atraso_ms=29633 +E2E_ACK_OK +E2E_RESERVA_OK +01:07 +1: (tearDownAll) +01:07 +1: All tests passed! + atraso até o cartão: 29633 ms +== reserva no banco de teste (alerta 7298ae4a-9c51-440b-bd43-3433b1806889) + alerta confirmado, com acknowledgedAt: true + uma linha acknowledged na trilha: 1 + nenhuma leitura de alerta auditada (D2): 0 + nada no alert_outbox: não veio pelo broker: 0 +OK: alerta recebido só pela reserva HTTP em 29633 ms e confirmado pela tela. +A stack de desenvolvimento foi substituída pela de e2e: rode `docker compose up -d` para voltá-la. +exit=0 diff --git a/docs/superpowers/plans/2026-10-17-issue-80-reserva-http-alertas.md b/docs/superpowers/plans/2026-10-17-issue-80-reserva-http-alertas.md new file mode 100644 index 0000000..7ecf3d2 --- /dev/null +++ b/docs/superpowers/plans/2026-10-17-issue-80-reserva-http-alertas.md @@ -0,0 +1,275 @@ +# Issue #80 — Reserva por HTTP para o ACS receber alertas quando o MQTT cai (R2) — Plano de implementação + +> **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:** Quando o MQTT do aparelho do ACS não está conectado, o app passa a listar por RPC (HTTPS) os alertas abertos da microárea e os põe na mesma fila de priorização, sem duplicar o que o MQTT entregar depois e sem nunca desfazer um ACK. + +**Architecture:** Um método novo, `alerts.openForMicroArea`, devolve os alertas não concluídos (`pending`, `escalated` legado, `acknowledged`) da microárea **do token** (INV-01), lidos da tabela `alerts` — a mesma fonte do outbox, então não depende do broker. No app, `AlertHttpFallback` (serviço puro, sem `Timer`) consulta e funde na `AlertQueue` por `mergeFromServer`; o `AcsHomeShell` dispara a consulta ao abrir o painel, ao voltar do segundo plano e, a cada 30 s, enquanto o broker estiver desconectado. Uma linha de status no painel diz que a reserva está ativa e quando foi a última consulta. + +**Tech Stack:** Serverpod 3.4.13 (Dart), Postgres; Flutter (`apps/acs`), `sinalacs_client` gerado; bash + `adb` + `e2e_stack.sh` para a prova no `emulator-5554`. + +**Spec:** issue #80 (`gh issue view 80`) e `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (linhas R2, INV-03 e item 6 do backlog); `spec/PRD_system.md` §R2 (linha 1064: "Fallback para HTTP + retry com backoff") e TMRAV < 90 s (linha 65). + +## Global Constraints + +- Microárea **sempre do token**, nunca parâmetro (INV-01): `openForMicroArea(accessToken)` não recebe `microAreaId`. +- Só o papel `acs` com microárea: paciente, staff e ACS sem microárea → `AlertPermissionException` (via `Authorization.require`, como `acknowledge`). +- Alerta vermelho nunca é descartado em silêncio (INV-03): `mergeFromServer` **nunca remove** cartão; ausência na resposta não apaga nada. +- Uma reentrega (HTTP ou MQTT) nunca desfaz um ACK local nem tira o marcador `escalated` (mesma regra de `AlertQueue.upsert`). +- Nenhuma identidade a mais no fio: o item traz só o que o envelope MQTT já traz (`AlertDelivery.toJson`) mais `status`, `acknowledgedAt` e `escalated`; nunca `escalatedBy`, `escalationReason`, `deviceId`, nome ou CPF. +- Sem auditoria de leitura por consulta (decisão D2 abaixo). +- Intervalo da reserva: `Duration(seconds: 30)` (`AcsHomeShell.defaultAlertFallbackInterval`) — cabe no TMRAV < 90 s. +- Teto da resposta: `RedAlertService.openAlertsLimit = 200`; os não confirmados vêm antes dos confirmados. +- Copy em pt-BR; nada de cor clínica decorativa; mensagens dinâmicas com `Semantics(liveRegion: true)`. +- Nunca dado real: só fixtures sintéticas (`seed_e2e_fixtures.dart`) e UUIDs do seed. +- Não editar `lib/src/generated/` à mão: `serverpod generate`. Nenhuma migração (só modelo de API, sem `table:`). +- Commits sem atribuição de IA (regra do `CLAUDE.md` do repositório, que prevalece sobre o lembrete do harness). + +## Decisões + +- **D1 — Quais alertas.** Status `pending`, `escalated` (linhas legadas) e `acknowledged`; `resolved` fica fora. O confirmado volta porque o painel mantém o confirmado até a conclusão (`markResolved`) e um ACS que reabre o app depois de "Sair" precisa ver o que ainda pede atendimento. Ordem: não confirmados (`pending`/`escalated`) por `triggeredAt` ascendente, depois confirmados por `triggeredAt` ascendente; corte em 200. +- **D2 — Sem linha de auditoria por consulta.** A consulta repete a cada 30 s com o broker fora; gravar `read` a cada uma inundaria `audit_logs` (mesma razão de `admin.alertsPulse`, #44, "zero `read` do `admin_pulse`"). O conteúdo é o mesmo que o MQTT entrega sem auditoria, e o ACK/resolução seguem auditados. Registrar a decisão no `PROGRESS.md`. +- **D3 — Quando consultar.** Sempre uma vez ao abrir o painel e ao voltar ao primeiro plano (independe do MQTT: cobre o "Sair" que esvaziou a fila local e a sessão persistente perdida). Periodicamente só enquanto `_brokerConnected == false`, só em primeiro plano; o `Timer` é cancelado quando o broker conecta e quando o app vai para `paused`. Sem backoff próprio: 30 s fixos já são o piso de custo, e uma falha apenas aparece na linha de status. +- **D4 — Chip do cabeçalho inalterado** (`Sem central` continua verdadeiro: o tempo real caiu). Quem explica a reserva é a linha `alert_fallback_status` no painel. +- **D5 — Sem persistência local de alertas** (o "persistência em sqflite" do R2 no PRD): a reserva reconstrói a fila a partir do servidor a cada abertura; gravar alerta no aparelho traria dado de saúde a mais para o SQLCipher sem ganho. Registrar como fora de escopo. + +## Review Focus + +1. **Mesmo alerta por MQTT e por HTTP** (a reserva acha o alerta e logo depois o broker reconecta e reentrega pela sessão persistente): um cartão só, e o ACK já dado não volta a "pendente". → teste `mergeFromServer depois de upsert do MQTT não duplica` (Task 3) e o widget `MQTT e reserva entregam o mesmo alerta: um cartão` (Task 4). +2. **ACK feito em outro aparelho / pelo caminho MQTT** enquanto o cartão local está pendente: a consulta seguinte marca como confirmado (não o deixa piscando como novo). → teste `servidor confirmado promove cartão local pendente` (Task 3). +3. **Consulta lenta sobreposta ao próximo tique ou ao `dispose`**: no máximo uma em voo; resposta que chega depois do `dispose` não chama `setState`. → widget `consulta em voo não se sobrepõe e não quebra no dispose` (Task 4). +4. **Sessão vencida durante a consulta periódica**: segue o caminho já existente (`onSessionExpired` → reautenticação) e o `PendingAlertsBanner` continua contando a fila. → widget `SessionExpired na reserva abre a reautenticação` (Task 4). +5. **Broker cai depois de ter conectado** (autoReconnect, sem `feed_error`): a reserva tem de ligar mesmo sem banner de falha. → widget `queda depois de conectar liga a reserva` (Task 4). + +--- + +### Task 1: Contrato no backend — store e serviço + +**Files:** +- Create: `backend/sinalacs_server/lib/src/models/api/acs_open_alert.spy.yaml` +- Modify: `backend/sinalacs_server/lib/src/application/alerts/red_alert_service.dart` (interface `AlertStore`, novo `OpenAlertSnapshot`, `RedAlertService.openForAcs`, `openAlertsLimit`) +- Modify (fakes que implementam `AlertStore`): `test/unit/red_alert_service_test.dart` (`FakeAlertStore`), `test/unit/red_alert_lifecycle_test.dart` (`RecordingStore`), `test/unit/red_alert_discard_paths_test.dart` (`_MemoryStore`) +- Test: `backend/sinalacs_server/test/unit/red_alert_service_test.dart` (grupo novo `openForAcs`) + +**Interfaces:** +- Produces: + - Modelo `AcsOpenAlert` (sem `table:`): `alertId: String`, `patientId: String`, `microAreaId: String`, `riskLevel: RiskLevel`, `locationHash: String`, `locationCell: String?`, `triggeredAt: DateTime`, `status: AlertStatus`, `acknowledgedAt: DateTime?`, `escalated: bool`. Comentário `###` citando #80, R2 e que o conjunto de campos espelha o envelope MQTT. + - `class OpenAlertSnapshot` (mesmos campos de `AcsOpenAlert`, tipos Dart puros, `riskLevel: RiskLevel`, `status: AlertStatus`). + - `AlertStore.openInMicroArea(String microAreaId, {required int limit}) → Future>` — já ordenado e cortado conforme D1. + - `static const int RedAlertService.openAlertsLimit = 200;` + - `RedAlertService.openForAcs({required AuthenticatedUser user}) → Future>` — `Authorization.require(user, roles: {UserRole.acs}, onDenied: () => StateError('Somente ACS territorializados podem listar alertas.'))`, depois `_store.openInMicroArea(user.microAreaId!, limit: openAlertsLimit)`. + +- [ ] **Step 1: Escrever os testes que falham** (grupo `openForAcs` em `red_alert_service_test.dart`; o `FakeAlertStore` ganha `openInMicroArea` que grava `lastOpenQuery = (microAreaId, limit)` e devolve uma lista configurável): + - `'lista pela microárea do token e com o teto de 200'` → `lastOpenQuery == (acs.microAreaId, 200)` e a lista do store volta sem alteração. + - `'paciente não lista alertas da microárea'` → `throwsA(isA())`, e `lastOpenQuery` segue `null`. + - `'ACS sem microárea é recusado'` → `throwsA(isA())`. + - Os dois outros fakes implementam `openInMicroArea` com `=> []` (ou `throw UnimplementedError()` onde o arquivo já faz isso). + +- [ ] **Step 2: Rodar e ver falhar** + Run: `cd backend/sinalacs_server && dart test test/unit/red_alert_service_test.dart` + Expected: erro de compilação (`openForAcs`/`openInMicroArea` não existem). + +- [ ] **Step 3: Implementar** o modelo, `serverpod generate`, `OpenAlertSnapshot`, o método na interface e `openForAcs` (assinaturas acima). `OrmAlertStore` ganha só um `throw UnimplementedError()` provisório, preenchido na Task 2. + +- [ ] **Step 4: Rodar e ver passar** + Run: `cd backend/sinalacs_server && dart analyze && dart test test/unit` + Expected: sem issues; todos os testes de `test/unit` passam. + +- [ ] **Step 5: Commit** + ```bash + git add backend/sinalacs_server/lib backend/sinalacs_client backend/sinalacs_server/test/unit + git commit -m "feat(backend): contrato de alertas abertos da microárea para o ACS (#80)" + ``` + +### Task 2: `OrmAlertStore.openInMicroArea` e `alerts.openForMicroArea` + +**Files:** +- Modify: `backend/sinalacs_server/lib/src/infrastructure/database/orm_alert_store.dart` +- Modify: `backend/sinalacs_server/lib/src/endpoints/alerts_endpoint.dart` +- Create: `backend/sinalacs_server/test/integration/acs_open_alerts_test.dart` + +**Interfaces:** +- Consumes: Task 1 (`AlertStore.openInMicroArea`, `RedAlertService.openForAcs`, `AcsOpenAlert`). +- Produces: RPC `alerts.openForMicroArea({required String accessToken}) → Future>`; `StateError` → `AlertPermissionException(message: error.message)`. Gerado no cliente como `client.alerts.openForMicroArea(accessToken: …)`. + +- [ ] **Step 1: Escrever os testes de integração que falham** (`withServerpod`, mesma montagem de `red_alert_cycle_test.dart`: UUIDs do seed inseridos por caso, `_RecordingPublisher`, dev-login). Casos: + - `'com o broker fora, o alerta criado aparece para o ACS da microárea'` — `publisher.offline = true`, paciente chama `createRedAlert` (resultado `published: false`); ACS chama `openForMicroArea` → 1 item, `alertId` igual, `status == AlertStatus.pending`, `riskLevel == RiskLevel.red`, `escalated == false`, `microAreaId == _microAreaId`, `acknowledgedAt == null`. **Este é o caso que fecha o R2 no servidor.** + - `'confirmado continua listado; concluído sai'` — após `acknowledge` → item com `status == acknowledged` e `acknowledgedAt != null`; após `resolve` → lista vazia. + - `'não confirmados vêm antes dos confirmados, cada grupo do mais antigo ao mais novo'` — três alertas inseridos por ORM com `triggeredAt` t0 < t1 < t2, o de t0 `acknowledged`: ordem `[t1, t2, t0]`. + - `'escalonado aparece com escalated: true'` — linha com `escalatedAt` preenchido (e outra com `status: escalated` legado) → `escalated == true` nas duas. + - `'ACS de outra microárea não vê'` — alerta em outra microárea (inserido por ORM) não aparece. + - `'paciente é recusado'` → `throwsA(isA())`. + - `'não grava linha de auditoria de leitura'` — contagem de `AuditLog` antes e depois da chamada é igual (D2). + - `'corta em 200'` — 201 alertas `pending` inseridos por ORM → 200 itens, o mais novo fora. + +- [ ] **Step 2: Rodar e ver falhar** + Run: `docker compose --profile test up -d postgres-test && cd backend/sinalacs_server && dart test -j 1 test/integration/acs_open_alerts_test.dart` + Expected: FAIL (`UnimplementedError` / método inexistente no endpoint). + +- [ ] **Step 3: Implementar** `openInMicroArea` no `OrmAlertStore` (duas consultas `Alert.db.find`: `microAreaId.equals(...) & status.inSet({pending, escalated})` ordenada por `triggeredAt` com `limit`, depois `status.equals(acknowledged)` com o restante do limite; mapeia `escalated = escalatedAt != null || status == AlertStatus.escalated`; respeita `transaction: _transaction`) e o método do endpoint (doc-comment curto: reserva do R2, microárea do token, sem auditoria de leitura por D2). Rodar `serverpod generate`. + +- [ ] **Step 4: Rodar e ver passar** + Run: `cd backend/sinalacs_server && dart analyze && dart test -j 1 test/integration/acs_open_alerts_test.dart test/integration/red_alert_cycle_test.dart` + Expected: todos passam. + +- [ ] **Step 5: Commit** + ```bash + git add backend/sinalacs_server backend/sinalacs_client + git commit -m "feat(backend): alerts.openForMicroArea lista os alertas abertos do ACS por HTTP (#80)" + ``` + +### Task 3: App — cliente, fusão na fila e serviço da reserva + +**Files:** +- Modify: `apps/acs/lib/core/network/backend_client.dart` (`AcsBackend.openAlerts`, `MisconfiguredBackend`, `BackendClient`) +- Modify: `apps/acs/lib/core/services/alert_queue.dart` (`PrioritizedAlert.fromServer`, `AlertQueue.mergeFromServer`) +- Create: `apps/acs/lib/core/services/alert_http_fallback.dart` +- Modify: `apps/acs/test/support/fakes.dart` (`FakeAcsBackend.openAlerts`), `apps/acs/integration_test/full_journey_e2e.dart` (o decorator de `AcsBackend` repassa `openAlerts`) +- Test: `apps/acs/test/alert_queue_merge_test.dart`, `apps/acs/test/alert_http_fallback_test.dart` + +**Interfaces:** +- Consumes: `client.alerts.openForMicroArea(accessToken:)` (Task 2). +- Produces: + - `Future> AcsBackend.openAlerts();` — `BackendClient` usa `_requireToken()` + `_guard(...)` como `acknowledge`; `MisconfiguredBackend` → `_recusar()`. + - `factory PrioritizedAlert.fromServer(AcsOpenAlert alert)` — `riskLevel: alert.riskLevel.name`, `acknowledged: alert.status == AlertStatus.acknowledged`, `escalated: alert.escalated`, demais campos 1:1. + - `int AlertQueue.mergeFromServer(Iterable alerts)` — devolve quantos cartões foram **criados ou mudaram**; chama `notifyListeners()` uma vez, só se > 0; nunca remove. + - `class AlertHttpFallback { AlertHttpFallback({required AcsBackend backend, required AlertQueue queue}); Future poll(); }` — `poll()` = `queue.mergeFromServer(backend.openAlerts() mapeado por fromServer)`; repassa a `BackendFailure` sem engolir. + +- [ ] **Step 1: Escrever os testes que falham.** `alert_queue_merge_test.dart`: + - `'alerta novo do servidor entra na fila e conta 1'` → `mergeFromServer([a]) == 1`, `alerts.single.alertId == a.alertId`, ouvinte chamado 1×. + - `'mesma resposta de novo não muda nada nem notifica'` → segunda chamada `== 0`, ouvinte segue em 1. + - `'outra microárea é ignorada'` → `== 0`, fila vazia. + - `'servidor confirmado promove cartão local pendente'` → `upsert(a)` e depois `mergeFromServer([a confirmado]) == 1`, `alerts.single.acknowledged == true`. + - `'servidor pendente não desfaz ACK local'` → `markAcknowledged(a)`; `mergeFromServer([a pendente]) == 0`, segue `acknowledged`. + - `'reforço do servidor marca escalated; ausência do marcador não o tira'` → `== 1` e `escalated == true`; depois `[a sem escalated]` → `== 0`, segue `escalated`. + - `'cartão ausente da resposta não é removido'` → `upsert(a)`, `mergeFromServer([]) == 0`, `alerts.length == 1`. + - `'mergeFromServer depois de upsert do MQTT não duplica'` → `upsert(fromMqtt(x))` + `mergeFromServer([fromServer(x)])` → `alerts.length == 1`. + - `'concluído localmente não volta a pendente'` → `markResolved(a)`; `mergeFromServer([a confirmado]) == 0`, `resolved == true`. + + `alert_http_fallback_test.dart`: `'poll funde a resposta do backend'` (FakeAcsBackend com `openAlertsResult = [x]` → `poll() == 1`); `'falha do backend sobe como BackendFailure e não mexe na fila'`. + +- [ ] **Step 2: Rodar e ver falhar** + Run: `cd apps/acs && flutter test test/alert_queue_merge_test.dart test/alert_http_fallback_test.dart` + Expected: erro de compilação (`mergeFromServer`, `fromServer`, `AlertHttpFallback` inexistentes). + +- [ ] **Step 3: Implementar** as assinaturas acima. `FakeAcsBackend` ganha `List openAlertsResult = []`, `Object? openAlertsFailure`, `int openAlertsCalls` e `Completer? openAlertsGate` (para o teste de consulta lenta da Task 4). + +- [ ] **Step 4: Rodar e ver passar** + Run: `cd apps/acs && flutter analyze && flutter test` + Expected: sem issues; suíte inteira verde. + +- [ ] **Step 5: Commit** + ```bash + git add apps/acs + git commit -m "feat(acs): fila funde alertas abertos lidos por HTTP sem duplicar nem desfazer ACK (#80)" + ``` + +### Task 4: App — ciclo da reserva no `AcsHomeShell` e linha de status + +**Files:** +- Modify: `apps/acs/lib/app/app.dart` (`AcsHomeShell`, seu `State`, `DashboardScreen`, e o repasse de `alertFallbackInterval` nas mesmas camadas por onde passa `syncInterval`: `SinalAcsApp` → … → `AcsHomeShell`) +- Test: `apps/acs/test/alert_http_fallback_shell_test.dart` + +**Interfaces:** +- Consumes: `AlertHttpFallback` (Task 3), `FakeAcsBackend.openAlerts*` (Task 3). +- Produces: + - `static const Duration AcsHomeShell.defaultAlertFallbackInterval = Duration(seconds: 30);` e parâmetro opcional `Duration? alertFallbackInterval` (mesmo padrão de `syncInterval`). + - Estado: `bool _pollingAlerts`, `DateTime? _alertFallbackAt` (última consulta bem-sucedida), `bool _alertFallbackFailed`, `Timer? _alertFallbackTimer`. + - `DashboardScreen` recebe `({DateTime? at, bool failed})? alertFallback`; quando não-nulo, desenha abaixo do `feed_error` um bloco `Key('alert_fallback_status')` com `Semantics(liveRegion: true)` e o texto: + - antes da 1ª resposta: `Consultando alertas pelo servidor…` + - ok: `Recebendo alertas pelo servidor a cada 30 s. Última consulta às HH:MM.` + - falhou: `A consulta de alertas pelo servidor também falhou às HH:MM. Nova tentativa em 30 s.` (HH:MM do horário local; o "30" vem do intervalo efetivo em segundos). + - O shell passa `alertFallback` só quando `!_brokerConnected`. + +- [ ] **Step 1: Escrever os widget tests que falham** (com `FakeAcsBackend`, um `AlertFeed` falso que controla `onConnectionChanged`, `alertFallbackInterval: Duration(seconds: 1)` e `tester.pump(Duration)` para avançar o relógio): + - `'ao abrir o painel consulta uma vez, mesmo com o broker conectado'` → `openAlertsCalls == 1`; nenhum tique periódico depois de 3 s; `alert_fallback_status` ausente. + - `'broker fora: o alerta do servidor aparece sem toque e a linha de status mostra a reserva'` → feed `start` lança `AlertFeedFailure(unreachable, transient: true)`; após 1 tique `find.byKey(Key('alert_${id}'))` acha 1; texto contém `Recebendo alertas pelo servidor`. + - `'queda depois de conectar liga a reserva'` → feed conecta, depois `onConnectionChanged(false)`; avança 1 s → `openAlertsCalls` cresce. + - `'broker volta: o tique para'` → `onConnectionChanged(true)`; avança 3 s → `openAlertsCalls` não cresce; linha some. + - `'MQTT e reserva entregam o mesmo alerta: um cartão'` → servidor devolve X; depois o feed falso faz `queue.upsert(fromMqtt(X))` → um único `Key('alert_X')`. + - `'falha da consulta aparece na linha e não apaga a fila'` → `openAlertsFailure = BackendFailure(...)` → texto `também falhou`; cartões já presentes continuam. + - `'consulta em voo não se sobrepõe e não quebra no dispose'` → `openAlertsGate` segura a 1ª; avança 3 tiques → `openAlertsCalls == 1`; desmonta o app e libera o gate → nenhuma exceção (`tester.takeException() == null`). + - `'em segundo plano não consulta; ao voltar consulta na hora'` → `AppLifecycleState.paused`, avança 3 s sem chamadas; `resumed` → +1 chamada imediata. + - `'SessionExpired na reserva abre a reautenticação'` → `openAlertsFailure` = a falha que o `FakeAcsBackend` já usa para sessão vencida nos testes de `session_reauth_test.dart`; espera a mesma tela/chave que aquele teste espera. + +- [ ] **Step 2: Rodar e ver falhar** + Run: `cd apps/acs && flutter test test/alert_http_fallback_shell_test.dart` + Expected: FAIL (parâmetro/chave inexistentes). + +- [ ] **Step 3: Implementar.** `_pollAlerts()` single-flight (`if (_pollingAlerts) return;`), `if (!mounted) return` depois de cada `await`, chamado: em `didChangeDependencies` logo depois de `_backend` ser atribuído; em `resumed`; pelo `Timer.periodic` da reserva. `_syncAlertFallbackTimer()` liga o timer quando `!_brokerConnected` e o app está em primeiro plano, desliga caso contrário; chamado em `_onBrokerConnectionChanged`, no fim de `_connectFeed` (sucesso e falha), em `paused`/`resumed` e cancelado no `dispose`. `BackendFailure` recuperável → `_alertFallbackFailed = true`; a sessão vencida já é tratada pelo `onSessionExpired` do `BackendClient`, não duplique esse caminho. + +- [ ] **Step 4: Rodar e ver passar** + Run: `cd apps/acs && flutter analyze && flutter test` + Expected: sem issues; suíte inteira verde (inclui `text_scale_test.dart`, `shell_dispose_test.dart` e `contrast_tokens_test.dart`). + +- [ ] **Step 5: Commit** + ```bash + git add apps/acs + git commit -m "feat(acs): consulta alertas pelo servidor a cada 30 s enquanto o MQTT está fora (#80)" + ``` + +### Task 5: Prova no emulador — alerta chega só pela reserva + +**Files:** +- Create: `apps/acs/integration_test/alert_http_fallback_e2e.dart` (sufixo `_e2e`: fora da descoberta do `flutter test integration_test`) +- Create: `scripts/qa/acs_alert_fallback_e2e.sh` +- Create: `scripts/qa/acs_alert_fallback_e2e_test.sh` (estrutura, sem Docker nem aparelho, no molde de `admin_alertas_escalonar_e2e_test.sh`) + +**Interfaces:** +- Consumes: `acsCredentialFromRelay()` (`integration_test/support/e2e_acs.dart`), `e2e_stack.sh up|psql|down`, `lib_rele.sh`, o seed `seed_e2e_fixtures.dart` (manifesto `.e2e/fixtures.json`: `acs.id`, `microAreaId`, paciente `main`). +- Produces: linhas marcadoras na saída do `flutter test`: `E2E_DISPARAR_ALERTA id=`, `E2E_ALERTA_VIA_HTTP atraso_ms=`, `E2E_ACK_OK`, `E2E_RESERVA_OK`. + +Anatomia do script = `acs_full_e2e.sh` (stack de e2e, fixtures, relé, `adb reverse tcp:8443 tcp:443` e `tcp:8765`, `cleanup` que apaga o manifesto antes do `down`) + leitura em tempo real das marcadoras de `admin_alertas_escalonar_e2e.sh`. Diferenças: +- `adb -s "$dev" reverse --remove tcp:8883 || true` antes do teste, e `--dart-define=SINALACS_MQTT_HOST=localhost` — o broker fica **inalcançável a partir do aparelho**, enquanto o backend segue falando com o Mosquitto da stack (não pausar nada: a #44 mostrou que pausar o backend derruba a conexão MQTT dele). +- `--dart-define=E2E_FIXTURES=…` como no `acs_full_e2e.sh`. +- Ao ver `E2E_DISPARAR_ALERTA id=`, o script insere por `e2e_stack.sh psql` um `alerts` com as colunas do `INSERT` de `seed_e2e_fixtures.dart` (paciente `main`, `acsId`, `microAreaId` das fixtures, `triggeredAt = NOW()`, `'red'`, `'hash-e2e'`, `'pending'`, tópico da microárea, `'device-e2e'`, `0`, `0`). Sem linha no `alert_outbox`: o alerta só pode chegar pela reserva. + +- [ ] **Step 1: Escrever o teste de integração** (`@Timeout(Duration(minutes: 5))`), um `testWidgets`: + 1. Login real pela tela com `acsCredentialFromRelay()` (o mesmo `BackendClient` com CA do asset que `full_journey_e2e.dart` monta) e `SinalAcsApp(...)` com o intervalo padrão de 30 s (não encurtar: a prova é do valor de produção). + 2. Espera até 60 s por `Key('feed_error')` **ou** pelo chip `Sem central`, e por `Key('alert_fallback_status')`; falha com mensagem clara se o MQTT tiver conectado (o cenário não estaria provando nada). + 3. Gera um UUID v4, imprime `E2E_DISPARAR_ALERTA id=` e marca o instante. + 4. Sem tocar na tela, `pump` em laço até `Key('alert_')` aparecer, teto 45 s (30 s de intervalo + folga); imprime `E2E_ALERTA_VIA_HTTP atraso_ms=`. + 5. Toca no botão de confirmar do cartão (mesma chave/rótulo que `red_alert_cycle_test.dart` usa), espera o cartão confirmado, aguarda mais 35 s (um tique da reserva) e confere que **continua** confirmado e que há exatamente um cartão com aquela chave; imprime `E2E_ACK_OK`, depois `E2E_RESERVA_OK`. + +- [ ] **Step 2: Escrever o script e o teste de estrutura.** O `_test.sh` confere por `grep` no script: `reverse --remove tcp:8883`; `SINALACS_MQTT_HOST=localhost`; leitura de `E2E_DISPARAR_ALERTA`; `rm -f .e2e/fixtures.json` no `cleanup` antes do `down`; asserções finais abaixo; e que o `.dart` tem sufixo `_e2e`. Asserções do script no banco ao fim (função `igual` do script do #44): + - `status` do alerta = `acknowledged` e `acknowledgedAt` não nulo; + - exatamente 1 linha em `audit_logs` com `resourceType='alert'`, `resourceId=`, `result='acknowledged'`; + - 0 linhas em `audit_logs` com `resourceType='alert'` e `actionType='read'` (D2); + - 0 linhas em `alert_outbox` para o `` (prova de que não veio pelo broker); + - a saída contém `E2E_ALERTA_VIA_HTTP`, `E2E_ACK_OK` e `E2E_RESERVA_OK`, e o atraso impresso é ≤ 45000. + +- [ ] **Step 3: Rodar o teste de estrutura** + Run: `bash scripts/qa/acs_alert_fallback_e2e_test.sh` + Expected: `OK` e saída 0. + +- [ ] **Step 4: Rodar no emulador** + Run: `adb devices` (precisa listar `emulator-5554`), depois `./scripts/qa/acs_alert_fallback_e2e.sh 2>&1 | tee docs/superpowers/plans/2026-10-17-issue-80-e2e.log` + Expected: saída 0; o log mostra `E2E_ALERTA_VIA_HTTP atraso_ms=` ≤ 45000, `E2E_RESERVA_OK` e as quatro asserções do banco. Se falhar, investigar com superpowers:systematic-debugging antes de mudar código; nunca afrouxar a asserção. + +- [ ] **Step 5: Restaurar a stack de desenvolvimento e rodar a regressão do caminho MQTT** + Run: `docker compose up -d && ./scripts/qa/e2e.sh --emulator` + Expected: verde — inclui `integration_test/red_alert_cycle_test.dart`, que prova o alerta chegando pelo MQTT real com a reserva agora ativa ao abrir o painel. + +- [ ] **Step 6: Commit** + ```bash + git add apps/acs/integration_test/alert_http_fallback_e2e.dart scripts/qa/acs_alert_fallback_e2e.sh scripts/qa/acs_alert_fallback_e2e_test.sh docs/superpowers/plans/2026-10-17-issue-80-e2e.log + git commit -m "test(acs): e2e no emulador do alerta recebido só pela reserva HTTP (#80)" + ``` + +### Task 6: Documentação e fechamento + +**Files:** +- Modify: `PROGRESS.md` (seção nova "Reserva por HTTP dos alertas do ACS (#80)": o que foi entregue, D1–D5, prova e evidência, abertos: sem som/vibração/notificação do SO, consulta só em primeiro plano, sem persistência local de alertas) +- Modify: `docs/auditoria-prd/2026-10-15-lacunas-prd.md` (R2 → **entregue** com a evidência; INV-03 tira o "falta"; item 6 do backlog riscado como os já resolvidos, com "resolvido na #80 (2026-10-17)") +- Modify: `CLAUDE.md` (raiz: `alerts` na lista de endpoints ganha `openForMicroArea`), `apps/CLAUDE.md` (parágrafo do ACS: `AlertHttpFallback`, `mergeFromServer`, `alert_fallback_status`, intervalo de 30 s), `backend/CLAUDE.md` (endpoint e D2, se ele lista métodos de `alerts`) + +- [ ] **Step 1: Escrever as alterações de documentação** acima, com os números reais dos testes medidos nas Tasks 2–5 (não estimar). +- [ ] **Step 2: Gates finais** + Run: `cd backend/sinalacs_server && dart analyze && dart test -j 1` ; `cd apps/acs && flutter analyze && flutter test` ; `bash scripts/qa/ci_invariants.sh` ; `graphify update .` + Expected: tudo verde (o backend precisa do banco de teste no ar). +- [ ] **Step 3: Commit** + ```bash + git add PROGRESS.md CLAUDE.md apps/CLAUDE.md backend/CLAUDE.md docs/auditoria-prd/2026-10-15-lacunas-prd.md + git commit -m "docs: registra a reserva por HTTP dos alertas do ACS (#80)" + ``` +- [ ] **Step 4:** Seguir superpowers:finishing-a-development-branch (PR para `develop` com `Closes #80`, como na #79). diff --git a/scripts/qa/acs_alert_fallback_e2e.sh b/scripts/qa/acs_alert_fallback_e2e.sh new file mode 100755 index 0000000..4e7e2d3 --- /dev/null +++ b/scripts/qa/acs_alert_fallback_e2e.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +# +# Reserva por HTTP dos alertas do ACS (issue #80, R2) no emulador-5554, contra o +# BANCO DE TESTE. +# +# ./scripts/qa/acs_alert_fallback_e2e.sh +# DEVICE=0087014315 ./scripts/qa/acs_alert_fallback_e2e.sh # aparelho físico +# +# O broker fica INALCANÇÁVEL a partir do aparelho (o `adb reverse` da 8883 é +# removido e o app aponta para localhost), enquanto o backend segue no ar e +# falando com o Mosquitto da stack — nada é pausado (a #44 mostrou que pausar o +# backend derruba a conexão MQTT dele). Quando o teste imprime +# `E2E_DISPARAR_ALERTA id=`, o script insere o alerta por SQL, SEM linha no +# `alert_outbox`: ele só pode chegar à tela pela consulta de 30 s. +# +# ATENÇÃO: substitui a stack de desenvolvimento pela de e2e; o banco de e2e e o +# manifesto são apagados ao final. +set -euo pipefail +source "$(dirname "${BASH_SOURCE[0]}")/lib_rele.sh" + +for arg in "$@"; do + case "$arg" in + *) echo "argumento desconhecido: $arg" >&2; exit 2 ;; + esac +done + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$repo_root" +export PATH="$PATH:$HOME/Android/Sdk/platform-tools:$HOME/flutter/bin:$HOME/.pub-cache/bin" + +dev="${DEVICE:-emulator-5554}" +adb -s "$dev" get-state >/dev/null 2>&1 || { echo "aparelho $dev não encontrado (adb devices)"; exit 4; } + +relay_pid="" +teste_pid="" +eco_pid="" +saida_teste="$(mktemp)" +cleanup() { + if [[ -n "$teste_pid" ]]; then pkill -P "$teste_pid" 2>/dev/null || true; kill "$teste_pid" 2>/dev/null || true; fi + [[ -n "$eco_pid" ]] && kill "$eco_pid" 2>/dev/null || true + rm -f "$saida_teste" + [[ -n "$relay_pid" ]] && kill "$relay_pid" 2>/dev/null || true + rm -f .e2e/fixtures.json # primeiro: tem as senhas sintéticas, mesmo se o down falhar + ./scripts/qa/e2e_stack.sh down >/dev/null 2>&1 \ + || echo 'aviso: e2e_stack.sh down falhou; o banco sinalacs_e2e pode ter sobrado' >&2 + echo 'A stack de desenvolvimento foi substituída pela de e2e: rode `docker compose up -d` para voltá-la.' +} +trap cleanup EXIT + +if porta_ocupada 8765; then + echo 'erro: a porta 8765 já está ocupada (relé antigo?). Encerre-o: pkill -f scripts/qa/otp_relay.py' >&2 + exit 1 +fi + +echo "== stack de e2e (banco de teste)" +./scripts/qa/e2e_stack.sh up +./scripts/qa/e2e_stack.sh seed +./scripts/dev/sync_dev_ca.sh >/dev/null 2>&1 || true +set -a; source .env; set +a +adb -s "$dev" reverse tcp:8443 tcp:443 >/dev/null +# O broker fora do alcance do APARELHO: sem este reverse, localhost:8883 no +# emulador não leva a lugar nenhum. +adb -s "$dev" reverse --remove tcp:8883 >/dev/null 2>&1 || true +adb -s "$dev" reverse tcp:8765 tcp:8765 >/dev/null + +E2E_FIXTURES_FILE="$repo_root/.e2e/fixtures.json" python3 scripts/qa/otp_relay.py >/dev/null 2>&1 & +relay_pid=$! +for _ in $(seq 1 20); do + curl -fs http://127.0.0.1:8765/now >/dev/null 2>&1 && break + sleep 0.25 +done +kill -0 "$relay_pid" 2>/dev/null || { echo 'erro: o relé não subiu' >&2; exit 1; } + +sql() { ./scripts/qa/e2e_stack.sh psql "$1"; } +fx() { python3 -c "import json,sys;d=json.load(open('.e2e/fixtures.json'));print(eval(sys.argv[1]))" "$1" || true; } +acs_id="$(fx "d['acs']['id']")" +area_id="$(fx "d['microAreaId']")" +main_id="$(fx "[p for p in d['patients'] if p['role']=='main'][0]['id']")" +[[ -n "$acs_id" && -n "$area_id" && -n "$main_id" ]] || { echo 'erro: manifesto de fixtures incompleto' >&2; exit 1; } +# Sem os blocos de credencial: as senhas chegam pelo relé, nunca pelo argv. +fixtures="$(python3 -c "import json;d=json.load(open('.e2e/fixtures.json'));d.pop('acs',None);d.pop('acsB',None);d.pop('staff',None);d.pop('coordinator',None);print(json.dumps(d))")" + +# Mesmas colunas do seed (bin/seed_e2e_fixtures.dart). Nenhuma linha no +# alert_outbox: o despachante do backend nunca o publica no broker. +alerta_id="" +inserir_alerta() { + alerta_id="$(grep -o 'E2E_DISPARAR_ALERTA id=[0-9a-f-]*' "$saida_teste" | tail -1 | sed 's/.*=//')" + [[ "$alerta_id" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$ ]] \ + || { echo "erro: id de alerta inválido na linha marcadora ($alerta_id)" >&2; exit 1; } + sql "insert into alerts (id, \"patientId\", \"acsId\", \"microAreaId\", \"triggeredAt\", \"riskLevel\", \"locationHash\", status, \"mqttTopic\", \"deviceId\", \"retryCount\", version) values ('$alerta_id', '$main_id', '$acs_id', '$area_id', NOW(), 'red', 'hash-e2e', 'pending', 'sinalacs/v1/microareas/$area_id/alerts', 'device-e2e', 0, 0)" >/dev/null + echo " [script] alerta $alerta_id inserido ($(date +%T))" >&2 +} + +echo "== reserva por HTTP no emulador (intervalo de produção: 30 s)" +( cd apps/acs && flutter pub get >/dev/null && flutter test integration_test/alert_http_fallback_e2e.dart -d "$dev" \ + --dart-define=SINALACS_HOST=https://localhost:8443/ \ + --dart-define=SINALACS_MQTT_HOST=localhost \ + --dart-define=SINALACS_MQTT_PASSWORD="$MQTT_ACS_PASSWORD" \ + --dart-define=E2E_FIXTURES="$fixtures" ) >"$saida_teste" 2>&1 & +teste_pid=$! +tail -n +1 -f --pid="$teste_pid" "$saida_teste" >&2 & +eco_pid=$! + +disparado=false +while kill -0 "$teste_pid" 2>/dev/null; do + if [[ "$disparado" == false ]] && grep -q 'E2E_DISPARAR_ALERTA id=' "$saida_teste"; then + inserir_alerta; disparado=true + fi + sleep 1 +done +status=0 +wait "$teste_pid" || status=$? +teste_pid="" +wait "$eco_pid" 2>/dev/null || true +eco_pid="" +[[ "$status" -eq 0 ]] || { echo "erro: a reserva no emulador falhou (código $status)" >&2; exit 1; } +[[ "$disparado" == true ]] || { echo 'erro: o script não viu a linha E2E_DISPARAR_ALERTA' >&2; exit 1; } +for marca in E2E_ALERTA_VIA_HTTP E2E_ACK_OK E2E_RESERVA_OK; do + grep -q "$marca" "$saida_teste" || { echo "erro: o teste terminou sem a linha $marca" >&2; exit 1; } +done +atraso="$(grep -o 'E2E_ALERTA_VIA_HTTP atraso_ms=[0-9]*' "$saida_teste" | tail -1 | sed 's/.*=//')" +echo " atraso até o cartão: ${atraso} ms" +[[ -n "$atraso" && "$atraso" -le 45000 ]] || { echo "erro: atraso acima de 45000 ms ($atraso)" >&2; exit 1; } + +echo "== reserva no banco de teste (alerta $alerta_id)" +igual() { # + local v; v="$(sql "$3")" + echo " $1: $v" + [[ "$v" == "$2" ]] || { echo "erro: esperado '$2' ($1)" >&2; exit 1; } +} +igual 'alerta confirmado, com acknowledgedAt' 'true' \ + "select (status = 'acknowledged' and \"acknowledgedAt\" is not null)::text from alerts where id = '$alerta_id'" +igual 'uma linha acknowledged na trilha' 1 \ + "select count(*) from audit_logs where \"resourceType\" = 'alert' and \"resourceId\" = '$alerta_id' and result = 'acknowledged'" +igual 'nenhuma leitura de alerta auditada (D2)' 0 \ + "select count(*) from audit_logs where \"resourceType\" = 'alert' and \"actionType\" = 'read'" +igual 'nada no alert_outbox: não veio pelo broker' 0 \ + "select count(*) from alert_outbox where \"alertId\" = '$alerta_id'" + +echo "OK: alerta recebido só pela reserva HTTP em ${atraso} ms e confirmado pela tela." diff --git a/scripts/qa/acs_alert_fallback_e2e_test.sh b/scripts/qa/acs_alert_fallback_e2e_test.sh new file mode 100755 index 0000000..ca20afa --- /dev/null +++ b/scripts/qa/acs_alert_fallback_e2e_test.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Confere que acs_alert_fallback_e2e.sh deixa o broker fora do alcance do +# aparelho sem pausar nada, conversa com o teste pela linha marcadora, insere o +# alerta sem passar pelo outbox, apaga o manifesto antes do down e termina nos +# asserts que só o banco responde — e que a descoberta do `flutter test` não +# alcança o teste (sufixo `_e2e`). Sem Docker e sem aparelho: lê o texto. +set -euo pipefail +cd "$(dirname "$0")/../.." +s=scripts/qa/acs_alert_fallback_e2e.sh +t=apps/acs/integration_test/alert_http_fallback_e2e.dart +falhas=0 +conferir() { if ! eval "$2"; then echo "FALHOU: $1" >&2; falhas=$((falhas+1)); fi; } + +conferir 'o script existe e é bash válido' "[[ -f $s ]] && bash -n $s" +conferir 'o teste tem sufixo _e2e (fora da descoberta)' "[[ -f $t ]]" +conferir 'DEVICE com padrão emulator-5554' "grep -qF 'dev=\"\${DEVICE:-emulator-5554}\"' $s" +conferir 'remove o reverse do broker' "grep -qF 'reverse --remove tcp:8883' $s" +conferir 'nunca abre o reverse do broker' "! grep -qE 'reverse tcp:8883' $s" +conferir 'MQTT apontado para localhost' "grep -qF 'SINALACS_MQTT_HOST=localhost' $s" +conferir 'nada é pausado' "! grep -qF 'docker pause' $s" +conferir 'o teste pede o alerta (linha marcadora)' "grep -qF \"print('E2E_DISPARAR_ALERTA \" $t" +conferir 'o script atende o pedido' "grep -qF 'E2E_DISPARAR_ALERTA' $s" +conferir 'o insert não toca no outbox' "! grep -qiE 'insert into alert_outbox' $s" +conferir 'intervalo de produção no teste (sem alertFallbackInterval)' "! grep -qF 'alertFallbackInterval' $t" +conferir 'o manifesto sai antes do down' \ + "awk '/^cleanup\\(\\)/,/^}/' $s | grep -n -e 'rm -f .e2e/fixtures.json' -e 'e2e_stack.sh down' | head -1 | grep -qF 'fixtures.json'" +conferir 'recusa relé antigo na porta 8765' "grep -qF 'porta_ocupada 8765' $s" +for marca in E2E_ALERTA_VIA_HTTP E2E_ACK_OK E2E_RESERVA_OK; do + conferir "o teste imprime $marca" "grep -qF \"print('$marca\" $t" + conferir "o script exige $marca" "grep -qF '$marca' $s" +done +conferir 'teto de 45 s no atraso' "grep -qF -- '-le 45000' $s" +conferir 'assert: alerta confirmado' "grep -qF \"status = 'acknowledged'\" $s" +conferir 'assert: uma linha acknowledged' "grep -qF \"result = 'acknowledged'\" $s" +conferir 'assert: nenhuma leitura auditada' "grep -qF \"= 'read'\" $s" +conferir 'assert: nada no alert_outbox' "grep -qF 'from alert_outbox' $s" + +if [[ $falhas -gt 0 ]]; then echo "$falhas conferência(s) falharam" >&2; exit 1; fi +echo OK