Skip to main content

pet_resort_api/
http.rs

1use app::{checkout_completion, crm_retention, data_quality_hygiene, manager_daily_brief};
2use axum::{
3    Json, Router,
4    extract::{Path, Query, State},
5    http::StatusCode,
6    routing::{get, post},
7};
8use chrono::{DateTime, NaiveDate, Utc};
9use domain::{analytics, data_quality, entities, message, operations, policy, source};
10use serde::{Deserialize, Serialize};
11use serde_json::{Value, json};
12use sha2::{Digest, Sha256};
13use std::{collections::BTreeMap, sync::Arc};
14use tokio::sync::Mutex;
15use uuid::Uuid;
16
17static VACCINE_DOCUMENT_STATE: std::sync::OnceLock<VaccineDocumentState> =
18    std::sync::OnceLock::new();
19
20#[derive(Clone)]
21/// In-memory state carried by the API shell for deterministic workflow demos and tests.
22///
23/// The state stores documents, review packets, inquiry intake records, and labor-evidence
24/// projections so HTTP handlers can demonstrate runtime contracts without connecting to
25/// live databases, customer messaging, or provider write APIs.
26pub struct VaccineDocumentState {
27    store: Arc<Mutex<VaccineDocumentStore>>,
28}
29
30impl Default for VaccineDocumentState {
31    fn default() -> Self {
32        Self {
33            store: Arc::new(Mutex::new(VaccineDocumentStore::default())),
34        }
35    }
36}
37
38#[derive(Debug, Default)]
39struct VaccineDocumentStore {
40    documents: BTreeMap<Uuid, DocumentRecord>,
41    extractions: BTreeMap<Uuid, VaccineExtractionRecord>,
42    vaccine_records: BTreeMap<Uuid, VaccineRecord>,
43    review_packets: BTreeMap<Uuid, ReviewPacket>,
44    approvals: BTreeMap<Uuid, ApprovalRecord>,
45    eligibility: BTreeMap<Uuid, PetEligibility>,
46    manager_daily_brief_outcomes: Vec<storage::operations::ManagerDailyBriefOutcomeRecord>,
47    data_quality_hygiene_outcomes: Vec<storage::operations::DataQualityHygieneOutcomeRecord>,
48    inquiry_intake_records: Vec<InquiryIntakeRecord>,
49    audit_events: Vec<AuditEvent>,
50}
51
52#[derive(Debug, Serialize)]
53struct HealthPayload {
54    service: &'static str,
55    status: &'static str,
56    live_side_effects: &'static str,
57}
58
59#[derive(Debug, Serialize)]
60struct ReadinessPayload {
61    service: &'static str,
62    database: &'static str,
63    object_storage: &'static str,
64    agent_runtime: &'static str,
65    live_customer_messaging: &'static str,
66    live_provider_writes: &'static str,
67}
68
69#[derive(Debug, Deserialize)]
70struct VaccineDocumentUploadRequest {
71    pet_id: Uuid,
72    customer_id: Uuid,
73    filename: String,
74    mime_type: String,
75    content: String,
76    uploaded_by_staff_id: String,
77}
78
79#[derive(Debug, Deserialize)]
80struct VaccineReviewDecisionRequest {
81    reviewed_by_staff_id: String,
82    reason: Option<String>,
83}
84
85#[derive(Debug, Deserialize)]
86struct InquirySubmissionRequest {
87    source_event_key: String,
88    location_id: String,
89    customer: InquiryCustomerRequest,
90    pet: InquiryPetRequest,
91    service: String,
92    requested_dates: Option<InquiryDateWindowRequest>,
93    message: String,
94}
95
96#[derive(Debug, Deserialize)]
97struct InquiryCustomerRequest {
98    full_name: String,
99    email: Option<String>,
100    phone: Option<String>,
101}
102
103#[derive(Debug, Deserialize)]
104struct InquiryPetRequest {
105    name: String,
106    species: String,
107}
108
109#[derive(Debug, Deserialize)]
110struct InquiryDateWindowRequest {
111    start: String,
112    end: String,
113}
114
115#[derive(Debug, Clone, Serialize)]
116struct InquiryIntakeRecord {
117    event: InquiryEvent,
118    lead: ParsedInquiryLead,
119    draft_reply: InquiryDraftReply,
120    task: InquiryTask,
121    agent_runtime: &'static str,
122    policy_boundary: &'static str,
123    audit_events: Vec<InquiryAuditEvent>,
124}
125
126#[derive(Debug, Clone, Serialize)]
127struct InquiryEvent {
128    event_type: &'static str,
129    source_event_key: String,
130    location_id: String,
131}
132
133#[derive(Debug, Clone, Serialize)]
134struct ParsedInquiryLead {
135    customer_name: String,
136    customer_email: Option<String>,
137    customer_phone: Option<String>,
138    pet_name: String,
139    species: String,
140    service: String,
141    requested_dates: Option<ParsedInquiryDateWindow>,
142    original_message: String,
143    missing_info: Vec<&'static str>,
144    review_status: &'static str,
145}
146
147#[derive(Debug, Clone, Serialize)]
148struct ParsedInquiryDateWindow {
149    start: String,
150    end: String,
151}
152
153#[derive(Debug, Clone, Serialize)]
154struct InquiryDraftReply {
155    status: &'static str,
156    live_send_allowed: bool,
157    approval_gate: &'static str,
158    body: String,
159}
160
161#[derive(Debug, Clone, Serialize)]
162struct InquiryTask {
163    kind: &'static str,
164    status: &'static str,
165    title: String,
166    review_gate: &'static str,
167}
168
169#[derive(Debug, Clone, Serialize)]
170struct InquiryAuditEvent {
171    action: &'static str,
172    actor_kind: &'static str,
173    subject_key: String,
174}
175
176#[derive(Debug, Serialize)]
177struct InquiryStaffQueuePayload {
178    records: Vec<InquiryIntakeRecord>,
179}
180
181#[derive(Debug, Clone, Serialize)]
182struct VaccineDocumentWorkflowPayload {
183    document: DocumentRecord,
184    extraction: VaccineExtractionRecord,
185    vaccine_record: VaccineRecord,
186    review_packet: ReviewPacket,
187    approval: Option<ApprovalRecord>,
188    eligibility: PetEligibility,
189    audit_events: Vec<AuditEvent>,
190}
191
192#[derive(Debug, Clone, Serialize)]
193struct DocumentRecord {
194    id: Uuid,
195    pet_id: Uuid,
196    customer_id: Uuid,
197    classification: &'static str,
198    source: &'static str,
199    filename: String,
200    mime_type: String,
201    content_length_bytes: usize,
202    sha256: String,
203    storage_bucket: &'static str,
204    storage_key: String,
205    storage_version: String,
206    virus_scan_status: &'static str,
207    pii_redaction_status: &'static str,
208    verification_status: &'static str,
209}
210
211#[derive(Debug, Clone, Serialize)]
212struct VaccineExtractionRecord {
213    id: Uuid,
214    document_id: Uuid,
215    schema_version: &'static str,
216    vaccine_name: String,
217    effective_on: NaiveDate,
218    expires_on: Option<NaiveDate>,
219    confidence: f32,
220    uncertainty_policy: &'static str,
221    auto_accept_threshold: f32,
222    raw_text_ref: String,
223}
224
225#[derive(Debug, Clone, Serialize)]
226struct VaccineRecord {
227    id: Uuid,
228    pet_id: Uuid,
229    source_document_id: Uuid,
230    vaccine_name: String,
231    status: &'static str,
232    effective_on: NaiveDate,
233    expires_on: Option<NaiveDate>,
234    review_gate: &'static str,
235}
236
237#[derive(Debug, Clone, Serialize)]
238struct ReviewPacket {
239    id: Uuid,
240    document_id: Uuid,
241    vaccine_record_id: Uuid,
242    gate: &'static str,
243    status: &'static str,
244    uncertainty: &'static str,
245}
246
247#[derive(Debug, Clone, Serialize)]
248struct ApprovalRecord {
249    id: Uuid,
250    review_packet_id: Uuid,
251    target_document_id: Uuid,
252    target_vaccine_record_id: Uuid,
253    gate: &'static str,
254    status: &'static str,
255    decided_by_staff_id: String,
256    decided_at: String,
257    reason: Option<String>,
258}
259
260#[derive(Debug, Clone, Serialize)]
261struct PetEligibility {
262    pet_id: Uuid,
263    rabies_current: bool,
264    source_vaccine_record_id: Option<Uuid>,
265    status: &'static str,
266}
267
268#[derive(Debug, Clone, Serialize)]
269struct AuditEvent {
270    action: &'static str,
271    actor_kind: &'static str,
272    actor_id: String,
273    subject_kind: &'static str,
274    subject_id: Uuid,
275    metadata: BTreeMap<&'static str, String>,
276}
277
278/// Builds the default staff-facing workflow router with deterministic in-memory state.
279///
280/// Exposed routes are safe runtime surfaces: health/readiness probes, staff inquiry
281/// intake, vaccine document review, manager daily-brief packet/draft/outcome paths,
282/// and data-quality hygiene packet/draft/outcome paths. They return DTO evidence and
283/// review gates rather than performing live customer sends or provider writes.
284pub fn router() -> Router {
285    router_with_state(
286        VACCINE_DOCUMENT_STATE
287            .get_or_init(VaccineDocumentState::default)
288            .clone(),
289    )
290}
291
292/// Builds the staff-facing workflow router over caller-provided deterministic state.
293///
294/// Tests and local runtimes use this entrypoint to share state across requests while
295/// preserving the same safety contract as [`router`]: handlers may draft, audit, and
296/// persist projections, but live side effects remain blocked unless a future runtime
297/// adapter adds explicit approval and provider gates.
298pub fn router_with_state(state: VaccineDocumentState) -> Router {
299    Router::new()
300        .route("/healthz", get(healthz))
301        .route("/readyz", get(readyz))
302        .route("/inquiries", post(submit_inquiry))
303        .route("/staff/inquiries", get(staff_inquiries))
304        .route(
305            "/agent/context/manager-daily-brief",
306            get(manager_daily_brief_agent_context),
307        )
308        .route(
309            "/agent/drafts/manager-daily-brief",
310            post(submit_manager_daily_brief_agent_draft),
311        )
312        .route(
313            "/manager-daily-brief/actions/{action_id}/outcome",
314            post(capture_manager_daily_brief_action_outcome),
315        )
316        .route(
317            "/agent/context/data-quality-hygiene",
318            get(data_quality_hygiene_agent_context),
319        )
320        .route(
321            "/agent/drafts/data-quality-hygiene",
322            post(submit_data_quality_hygiene_agent_draft),
323        )
324        .route(
325            "/data-quality-hygiene/actions/{action_id}/outcome",
326            post(capture_data_quality_hygiene_action_outcome),
327        )
328        .route("/vaccine-documents/uploads", post(upload_vaccine_document))
329        .route(
330            "/vaccine-documents/review-packets/{review_packet_id}/approve",
331            post(approve_vaccine_document),
332        )
333        .route(
334            "/vaccine-documents/review-packets/{review_packet_id}/reject",
335            post(reject_vaccine_document),
336        )
337        .with_state(state)
338}
339
340async fn healthz() -> Json<HealthPayload> {
341    Json(HealthPayload {
342        service: "pet-resort-api",
343        status: "ok",
344        live_side_effects: "disabled",
345    })
346}
347
348async fn readyz() -> Json<ReadinessPayload> {
349    Json(ReadinessPayload {
350        service: "pet-resort-api",
351        database: "not_configured",
352        object_storage: "not_configured",
353        agent_runtime: "fake_deterministic",
354        live_customer_messaging: "disabled",
355        live_provider_writes: "disabled",
356    })
357}
358
359async fn submit_inquiry(
360    State(state): State<VaccineDocumentState>,
361    Json(request): Json<InquirySubmissionRequest>,
362) -> (StatusCode, Json<InquiryIntakeRecord>) {
363    let mut store = state.store.lock().await;
364    let record = build_inquiry_intake_record(request);
365    store.inquiry_intake_records.push(record.clone());
366    (StatusCode::CREATED, Json(record))
367}
368
369async fn staff_inquiries(
370    State(state): State<VaccineDocumentState>,
371) -> Json<InquiryStaffQueuePayload> {
372    let store = state.store.lock().await;
373    Json(InquiryStaffQueuePayload {
374        records: store.inquiry_intake_records.clone(),
375    })
376}
377
378#[derive(Debug, Deserialize)]
379struct ManagerDailyBriefAgentContextQuery {
380    location_id: Uuid,
381    operating_day: NaiveDate,
382}
383
384#[derive(Debug, Deserialize)]
385struct ManagerDailyBriefAgentDraftSubmissionRequest {
386    context_packet_id: String,
387    correlation_id: String,
388    submitted_by: String,
389    actions: Vec<ManagerDailyBriefSubmittedAction>,
390}
391
392#[derive(Debug, Deserialize)]
393struct ManagerDailyBriefSubmittedAction {
394    id: String,
395    kind: String,
396    recommendation: String,
397    #[serde(default)]
398    source_refs: Vec<Value>,
399    #[serde(default)]
400    review_gates: Vec<String>,
401    #[serde(default)]
402    requested_side_effects: Vec<String>,
403}
404
405#[derive(Debug, Deserialize)]
406struct ManagerDailyBriefOutcomeCaptureRequest {
407    outcome: storage::operations::ManagerDailyBriefOutcomeCode,
408    actual_minutes: u16,
409    actor: ManagerDailyBriefOutcomeActorRequest,
410    feedback: String,
411    #[serde(default)]
412    source_refs: Vec<storage::operations::StoredSourceRecordRef>,
413    timestamp: String,
414    audit: ManagerDailyBriefOutcomeAuditRequest,
415    reporting: ManagerDailyBriefOutcomeReportingRequest,
416    #[serde(default)]
417    requested_side_effects: Vec<String>,
418}
419
420#[derive(Debug, Deserialize)]
421struct ManagerDailyBriefOutcomeActorRequest {
422    id: String,
423    persona: storage::operations::ManagerDailyBriefPersonaCode,
424}
425
426#[derive(Debug, Deserialize)]
427struct ManagerDailyBriefOutcomeAuditRequest {
428    correlation_id: String,
429}
430
431#[derive(Debug, Deserialize)]
432struct ManagerDailyBriefOutcomeReportingRequest {
433    location_id: String,
434    operating_day: String,
435}
436
437#[derive(Debug, Deserialize)]
438struct DataQualityHygieneAgentContextQuery {
439    location_id: Uuid,
440    operating_day: NaiveDate,
441}
442
443#[derive(Debug, Deserialize)]
444struct DataQualityHygieneAgentDraftSubmissionRequest {
445    context_packet_id: String,
446    correlation_id: String,
447    actions: Vec<DataQualityHygieneSubmittedAction>,
448}
449
450#[derive(Debug, Deserialize)]
451struct DataQualityHygieneSubmittedAction {
452    action_id: String,
453    kind: String,
454    #[serde(default)]
455    source_refs: Vec<Value>,
456    #[serde(default)]
457    issue_refs: Vec<String>,
458    #[serde(default)]
459    review_gates: Vec<String>,
460    #[serde(default)]
461    requested_side_effects: Vec<String>,
462    #[serde(default)]
463    attempted_ambiguity_resolution: bool,
464}
465
466#[derive(Debug, Deserialize)]
467struct DataQualityHygieneOutcomeCaptureRequest {
468    outcome: storage::operations::DataQualityHygieneOutcomeCode,
469    actual_minutes: u16,
470    actor: DataQualityHygieneOutcomeActorRequest,
471    feedback: String,
472    #[serde(default)]
473    source_refs: Vec<Value>,
474    #[serde(default)]
475    issue_refs: Vec<String>,
476    resolution_status_after_review: storage::operations::DataQualityResolutionStatusCode,
477    timestamp: String,
478    audit: DataQualityHygieneOutcomeAuditRequest,
479    #[serde(default)]
480    requested_side_effects: Vec<String>,
481}
482
483#[derive(Debug, Deserialize)]
484struct DataQualityHygieneOutcomeActorRequest {
485    id: String,
486    persona: storage::operations::DataQualityHygienePersonaCode,
487}
488
489#[derive(Debug, Deserialize)]
490struct DataQualityHygieneOutcomeAuditRequest {
491    correlation_id: String,
492}
493
494async fn capture_manager_daily_brief_action_outcome(
495    State(state): State<VaccineDocumentState>,
496    Path(action_id): Path<String>,
497    Json(request): Json<ManagerDailyBriefOutcomeCaptureRequest>,
498) -> (StatusCode, Json<Value>) {
499    let reasons = request
500        .requested_side_effects
501        .iter()
502        .map(|side_effect| manager_daily_brief_requested_side_effect_rejection_reason(side_effect))
503        .collect::<Vec<_>>();
504
505    if !reasons.is_empty() {
506        return (
507            StatusCode::UNPROCESSABLE_ENTITY,
508            Json(json!({
509                "accepted": false,
510                "outcome_persisted": false,
511                "reasons": reasons,
512                "live_side_effects_allowed": false,
513                "blocked_actions": manager_daily_brief_blocked_action_codes()
514            })),
515        );
516    }
517
518    let Ok(actual_minutes) =
519        storage::operations::StoredManagerDailyBriefLaborMinutes::try_new(request.actual_minutes)
520    else {
521        return (
522            StatusCode::UNPROCESSABLE_ENTITY,
523            Json(json!({
524                "accepted": false,
525                "outcome_persisted": false,
526                "reasons": ["actual_minutes_must_be_greater_than_zero"],
527                "live_side_effects_allowed": false,
528                "blocked_actions": manager_daily_brief_blocked_action_codes()
529            })),
530        );
531    };
532
533    let Some((location_id, operating_day)) =
534        manager_daily_brief_reporting_scope(&request.reporting)
535    else {
536        return (
537            StatusCode::UNPROCESSABLE_ENTITY,
538            Json(json!({
539                "accepted": false,
540                "outcome_persisted": false,
541                "reasons": ["invalid_reporting_scope"],
542                "live_side_effects_allowed": false,
543                "blocked_actions": manager_daily_brief_blocked_action_codes()
544            })),
545        );
546    };
547
548    let packet = local_manager_daily_brief_packet(location_id, operating_day);
549    let Some(action) = packet
550        .actions()
551        .iter()
552        .find(|action| action.id().clone().into_inner() == action_id)
553    else {
554        return (
555            StatusCode::UNPROCESSABLE_ENTITY,
556            Json(json!({
557                "accepted": false,
558                "outcome_persisted": false,
559                "reasons": ["unknown_manager_daily_brief_action_id"],
560                "live_side_effects_allowed": false,
561                "blocked_actions": manager_daily_brief_blocked_action_codes()
562            })),
563        );
564    };
565
566    let before_minutes = storage::operations::StoredManagerDailyBriefLaborMinutes::try_new(
567        action.labor_impact().before_minutes().get(),
568    )
569    .expect("manager daily brief action labor impact uses non-zero domain labor minutes");
570
571    let record = storage::operations::ManagerDailyBriefOutcomeRecord::builder()
572        .action_id(action_id)
573        .outcome(request.outcome)
574        .before_minutes(before_minutes)
575        .actual_minutes(actual_minutes)
576        .actor_id(request.actor.id)
577        .actor_persona(request.actor.persona)
578        .feedback(request.feedback)
579        .source_refs(request.source_refs)
580        .recorded_at(request.timestamp)
581        .correlation_id(request.audit.correlation_id)
582        .location_id(location_id.0.to_string())
583        .operating_day(operating_day.get().to_string())
584        .action_kind(stored_manager_daily_brief_action_kind(action.kind()))
585        .owner_persona(stored_manager_daily_brief_persona(action.owner_persona()))
586        .estimated_minutes_saved(action.labor_impact().minutes_saved())
587        .build();
588    let reporting_group = record.reporting_group();
589    let persisted_outcome_count = {
590        let mut store = state.store.lock().await;
591        store.manager_daily_brief_outcomes.push(record.clone());
592        store.manager_daily_brief_outcomes.len()
593    };
594
595    (
596        StatusCode::CREATED,
597        Json(json!({
598            "accepted": true,
599            "outcome_persisted": true,
600            "outcome_record": {
601                "action_id": record.action_id,
602                "outcome": record.outcome,
603                "before_minutes": record.before_minutes.get(),
604                "actual_minutes": record.actual_minutes.get(),
605                "actor": {
606                    "id": record.actor_id,
607                    "persona": record.actor_persona
608                },
609                "feedback": record.feedback,
610                "source_refs": record.source_refs,
611                "timestamp": record.recorded_at,
612                "audit": {
613                    "correlation_id": record.correlation_id
614                }
615            },
616            "labor_savings_evidence": {
617                "estimated_minutes_saved": record.estimated_minutes_saved,
618                "actual_minutes_saved": record.actual_minutes_saved(),
619                "grouping": {
620                    "location_id": reporting_group.location_id,
621                    "operating_day": reporting_group.operating_day,
622                    "action_kind": reporting_group.action_kind,
623                    "owner_persona": reporting_group.owner_persona
624                },
625                "persisted_outcome_count": persisted_outcome_count
626            },
627            "live_side_effects_allowed": false,
628            "blocked_actions": manager_daily_brief_blocked_action_codes(),
629            "audit": {
630                "event": "manager_daily_brief_outcome_recorded",
631                "policy_owner": "deterministic_app"
632            }
633        })),
634    )
635}
636
637async fn submit_manager_daily_brief_agent_draft(
638    Json(request): Json<ManagerDailyBriefAgentDraftSubmissionRequest>,
639) -> (StatusCode, Json<Value>) {
640    let mut accepted_actions = Vec::new();
641    let mut rejected_actions = Vec::new();
642
643    for action in &request.actions {
644        let reasons = validate_manager_daily_brief_submitted_action(action);
645        if reasons.is_empty() {
646            accepted_actions.push(json!({
647                "id": action.id,
648                "kind": action.kind,
649                "recommendation": action.recommendation,
650                "review_gates": action.review_gates,
651                "source_refs": action.source_refs,
652                "showable_to_manager": true,
653                "live_side_effects_allowed": false
654            }));
655        } else {
656            rejected_actions.push(json!({
657                "id": action.id,
658                "kind": action.kind,
659                "reasons": reasons,
660                "showable_to_manager": false,
661                "live_side_effects_allowed": false
662            }));
663        }
664    }
665
666    let validation_status = match (accepted_actions.is_empty(), rejected_actions.is_empty()) {
667        (false, true) => "accepted",
668        (false, false) => "partially_accepted",
669        (true, false) => "rejected",
670        (true, true) => "rejected",
671    };
672    let status_code = if rejected_actions.is_empty() {
673        StatusCode::CREATED
674    } else {
675        StatusCode::UNPROCESSABLE_ENTITY
676    };
677
678    (
679        status_code,
680        Json(json!({
681            "validation": {
682                "status": validation_status,
683                "validator": "pet_resort_api.manager_daily_brief.agent_draft_validator.v1"
684            },
685            "context_packet_id": request.context_packet_id,
686            "correlation_id": request.correlation_id,
687            "submitted_by": request.submitted_by,
688            "accepted_actions": accepted_actions,
689            "rejected_actions": rejected_actions,
690            "live_side_effects_allowed": false,
691            "audit": {
692                "event": "manager_daily_brief_agent_draft_validated",
693                "policy_owner": "deterministic_app"
694            }
695        })),
696    )
697}
698
699async fn data_quality_hygiene_agent_context(
700    Query(query): Query<DataQualityHygieneAgentContextQuery>,
701) -> Json<Value> {
702    let location_id = entities::LocationId(query.location_id);
703    let operating_day = operations::operating_day::Date::try_new(query.operating_day)
704        .expect("operating day date is always valid after query parsing");
705    let packet = local_data_quality_hygiene_packet(location_id, operating_day);
706
707    Json(data_quality_hygiene_packet_payload(&packet))
708}
709
710async fn submit_data_quality_hygiene_agent_draft(
711    Json(request): Json<DataQualityHygieneAgentDraftSubmissionRequest>,
712) -> (StatusCode, Json<Value>) {
713    let packet = local_data_quality_hygiene_packet(
714        local_data_quality_hygiene_location_id(),
715        local_data_quality_hygiene_operating_day(),
716    );
717    let mut accepted_actions = Vec::new();
718    let mut rejected_actions = Vec::new();
719
720    for action in &request.actions {
721        let reasons = validate_data_quality_hygiene_submitted_action(&packet, action);
722        if reasons.is_empty() {
723            accepted_actions.push(json!({
724                "id": action.action_id,
725                "kind": action.kind,
726                "review_gates": action.review_gates,
727                "source_refs": action.source_refs,
728                "issue_refs": action.issue_refs,
729                "showable_to_manager": true,
730                "live_side_effects_allowed": false
731            }));
732        } else {
733            rejected_actions.push(json!({
734                "id": action.action_id,
735                "kind": action.kind,
736                "reasons": reasons,
737                "showable_to_manager": false,
738                "live_side_effects_allowed": false
739            }));
740        }
741    }
742
743    let validation_status = match (accepted_actions.is_empty(), rejected_actions.is_empty()) {
744        (false, true) => "accepted",
745        (false, false) => "partially_accepted",
746        (true, false) | (true, true) => "rejected",
747    };
748    let status_code = if rejected_actions.is_empty() {
749        StatusCode::CREATED
750    } else {
751        StatusCode::UNPROCESSABLE_ENTITY
752    };
753
754    (
755        status_code,
756        Json(json!({
757            "validation": {
758                "status": validation_status,
759                "validator": "pet_resort_api.data_quality_hygiene.agent_draft_validator.v1"
760            },
761            "context_packet_id": request.context_packet_id,
762            "correlation_id": request.correlation_id,
763            "accepted_actions": accepted_actions,
764            "rejected_actions": rejected_actions,
765            "live_side_effects_allowed": false,
766            "audit": {
767                "event": "data_quality_hygiene_agent_draft_validated",
768                "policy_owner": "deterministic_app"
769            }
770        })),
771    )
772}
773
774async fn capture_data_quality_hygiene_action_outcome(
775    State(state): State<VaccineDocumentState>,
776    Path(action_id): Path<String>,
777    Json(request): Json<DataQualityHygieneOutcomeCaptureRequest>,
778) -> (StatusCode, Json<Value>) {
779    let reasons = request
780        .requested_side_effects
781        .iter()
782        .map(|side_effect| data_quality_hygiene_requested_side_effect_rejection_reason(side_effect))
783        .collect::<Vec<_>>();
784
785    if !reasons.is_empty() {
786        return (
787            StatusCode::UNPROCESSABLE_ENTITY,
788            Json(json!({
789                "accepted": false,
790                "outcome_persisted": false,
791                "reasons": reasons,
792                "live_side_effects_allowed": false,
793                "blocked_actions": data_quality_hygiene_blocked_action_codes()
794            })),
795        );
796    }
797
798    let Ok(actual_minutes) =
799        storage::operations::StoredDataQualityHygieneLaborMinutes::try_new(request.actual_minutes)
800    else {
801        return (
802            StatusCode::UNPROCESSABLE_ENTITY,
803            Json(json!({
804                "accepted": false,
805                "outcome_persisted": false,
806                "reasons": ["actual_minutes_must_be_greater_than_zero"],
807                "live_side_effects_allowed": false,
808                "blocked_actions": data_quality_hygiene_blocked_action_codes()
809            })),
810        );
811    };
812
813    let packet = local_data_quality_hygiene_packet(
814        local_data_quality_hygiene_location_id(),
815        local_data_quality_hygiene_operating_day(),
816    );
817    let Some(action) = packet
818        .actions()
819        .iter()
820        .find(|action| action.id().as_str() == action_id)
821    else {
822        return (
823            StatusCode::UNPROCESSABLE_ENTITY,
824            Json(json!({
825                "accepted": false,
826                "outcome_persisted": false,
827                "reasons": ["unknown_data_quality_hygiene_action_id"],
828                "live_side_effects_allowed": false,
829                "blocked_actions": data_quality_hygiene_blocked_action_codes()
830            })),
831        );
832    };
833
834    let before_minutes = storage::operations::StoredDataQualityHygieneLaborMinutes::try_new(
835        action.labor_impact().before_minutes().get(),
836    )
837    .expect("data-quality hygiene action labor impact uses non-zero domain labor minutes");
838
839    let record = storage::operations::DataQualityHygieneOutcomeRecord::builder()
840        .action_id(action_id)
841        .outcome(request.outcome)
842        .before_minutes(before_minutes)
843        .actual_minutes(actual_minutes)
844        .actor_id(request.actor.id)
845        .actor_persona(request.actor.persona)
846        .feedback(request.feedback)
847        .source_refs(
848            request
849                .source_refs
850                .iter()
851                .map(stored_source_record_ref_from_payload)
852                .collect(),
853        )
854        .issue_refs(request.issue_refs)
855        .resolution_status_after_review(request.resolution_status_after_review)
856        .recorded_at(request.timestamp)
857        .correlation_id(request.audit.correlation_id)
858        .location_id(packet.location_id().0.to_string())
859        .operating_day(packet.operating_day().get().to_string())
860        .action_kind(stored_data_quality_hygiene_action_kind(action.kind()))
861        .owner_persona(stored_data_quality_hygiene_persona(action.owner_persona()))
862        .estimated_minutes_saved(action.labor_impact().minutes_saved())
863        .build();
864    let reporting_group = record.reporting_group();
865    let persisted_outcome_count = {
866        let mut store = state.store.lock().await;
867        store.data_quality_hygiene_outcomes.push(record.clone());
868        store.data_quality_hygiene_outcomes.len()
869    };
870
871    (
872        StatusCode::CREATED,
873        Json(json!({
874            "accepted": true,
875            "outcome_persisted": true,
876            "outcome_record": {
877                "action_id": record.action_id,
878                "outcome": record.outcome,
879                "before_minutes": record.before_minutes.get(),
880                "actual_minutes": record.actual_minutes.get(),
881                "actor": {
882                    "id": record.actor_id,
883                    "persona": record.actor_persona
884                },
885                "feedback": record.feedback,
886                "source_refs": record.source_refs,
887                "issue_refs": record.issue_refs,
888                "resolution_status_after_review": record.resolution_status_after_review,
889                "timestamp": record.recorded_at,
890                "audit": {
891                    "correlation_id": record.correlation_id
892                }
893            },
894            "labor_savings_evidence": {
895                "estimated_minutes_saved": record.estimated_minutes_saved,
896                "actual_minutes_saved": record.actual_minutes_saved(),
897                "grouping": {
898                    "location_id": reporting_group.location_id,
899                    "operating_day": reporting_group.operating_day,
900                    "action_kind": reporting_group.action_kind,
901                    "owner_persona": reporting_group.owner_persona
902                },
903                "persisted_outcome_count": persisted_outcome_count
904            },
905            "live_side_effects_allowed": false,
906            "blocked_actions": data_quality_hygiene_blocked_action_codes(),
907            "audit": {
908                "event": "data_quality_hygiene_outcome_recorded",
909                "policy_owner": "deterministic_app"
910            }
911        })),
912    )
913}
914
915fn validate_manager_daily_brief_submitted_action(
916    action: &ManagerDailyBriefSubmittedAction,
917) -> Vec<String> {
918    let mut reasons = Vec::new();
919
920    let Some(required_review_gate) = required_manager_daily_brief_review_gate(&action.kind) else {
921        reasons.push("unsupported_action_kind".to_owned());
922        return reasons;
923    };
924
925    if action.source_refs.is_empty() {
926        reasons.push("missing_source_refs".to_owned());
927    }
928
929    if !action
930        .review_gates
931        .iter()
932        .any(|gate| gate == required_review_gate)
933    {
934        reasons.push(format!(
935            "missing_required_review_gate:{required_review_gate}"
936        ));
937    }
938
939    for side_effect in &action.requested_side_effects {
940        reasons.push(manager_daily_brief_requested_side_effect_rejection_reason(
941            side_effect,
942        ));
943    }
944
945    reasons
946}
947
948fn required_manager_daily_brief_review_gate(kind: &str) -> Option<&'static str> {
949    match kind {
950        "review_demand_against_staffing_plan" => Some("manager_approval"),
951        "resolve_checkout_exception" => Some("manager_approval"),
952        "approve_retention_follow_up_draft" => Some("customer_message_approval"),
953        "investigate_source_data_quality_issue" => Some("manager_approval"),
954        _ => None,
955    }
956}
957
958fn manager_daily_brief_side_effect_is_blocked(side_effect: &str) -> bool {
959    matches!(
960        side_effect,
961        "send_customer_message"
962            | "mutate_provider_or_pms_record"
963            | "change_staff_schedule"
964            | "move_refund_discount_or_payment"
965            | "hide_source_data_quality_issue"
966    )
967}
968
969fn manager_daily_brief_requested_side_effect_rejection_reason(side_effect: &str) -> String {
970    if manager_daily_brief_side_effect_is_blocked(side_effect) {
971        format!("blocked_side_effect:{side_effect}")
972    } else {
973        format!("unsupported_side_effect:{side_effect}")
974    }
975}
976
977fn manager_daily_brief_reporting_scope(
978    reporting: &ManagerDailyBriefOutcomeReportingRequest,
979) -> Option<(entities::LocationId, operations::operating_day::Date)> {
980    let location_id = Uuid::parse_str(&reporting.location_id).ok()?;
981    let operating_day = NaiveDate::parse_from_str(&reporting.operating_day, "%Y-%m-%d").ok()?;
982
983    Some((
984        entities::LocationId(location_id),
985        operations::operating_day::Date::try_new(operating_day).ok()?,
986    ))
987}
988
989fn local_manager_daily_brief_packet(
990    location_id: entities::LocationId,
991    operating_day: operations::operating_day::Date,
992) -> manager_daily_brief::Packet {
993    manager_daily_brief::Workflow::evaluate(
994        manager_daily_brief::Request::builder()
995            .location_id(location_id)
996            .operating_day(operating_day)
997            .prepared_for(manager_daily_brief::ManagerBriefPersona::GeneralManager)
998            .demand_attention_threshold(
999                manager_daily_brief::DemandThresholdUnits::try_new(10)
1000                    .expect("static demand threshold is valid"),
1001            )
1002            .service_demand_facts(local_manager_daily_brief_service_demand_facts(
1003                location_id,
1004                operating_day,
1005            ))
1006            .checkout_packets(local_manager_daily_brief_checkout_packets(
1007                location_id,
1008                operating_day,
1009            ))
1010            .retention_packets(local_manager_daily_brief_retention_packets(
1011                location_id,
1012                operating_day,
1013            ))
1014            .build(),
1015    )
1016}
1017
1018fn stored_manager_daily_brief_action_kind(
1019    kind: manager_daily_brief::BriefActionKind,
1020) -> storage::operations::ManagerDailyBriefActionKindCode {
1021    match kind {
1022        manager_daily_brief::BriefActionKind::ReviewDemandAgainstStaffingPlan => {
1023            storage::operations::ManagerDailyBriefActionKindCode::ReviewDemandAgainstStaffingPlan
1024        }
1025        manager_daily_brief::BriefActionKind::ResolveCheckoutException => {
1026            storage::operations::ManagerDailyBriefActionKindCode::ResolveCheckoutException
1027        }
1028        manager_daily_brief::BriefActionKind::ApproveRetentionFollowUpDraft => {
1029            storage::operations::ManagerDailyBriefActionKindCode::ApproveRetentionFollowUpDraft
1030        }
1031        manager_daily_brief::BriefActionKind::InvestigateSourceDataQualityIssue => {
1032            storage::operations::ManagerDailyBriefActionKindCode::InvestigateSourceDataQualityIssue
1033        }
1034    }
1035}
1036
1037fn stored_manager_daily_brief_persona(
1038    persona: manager_daily_brief::ManagerBriefPersona,
1039) -> storage::operations::ManagerDailyBriefPersonaCode {
1040    match persona {
1041        manager_daily_brief::ManagerBriefPersona::GeneralManager => {
1042            storage::operations::ManagerDailyBriefPersonaCode::GeneralManager
1043        }
1044        manager_daily_brief::ManagerBriefPersona::AssistantGeneralManager => {
1045            storage::operations::ManagerDailyBriefPersonaCode::AssistantGeneralManager
1046        }
1047        manager_daily_brief::ManagerBriefPersona::FrontDeskLead => {
1048            storage::operations::ManagerDailyBriefPersonaCode::FrontDeskLead
1049        }
1050        manager_daily_brief::ManagerBriefPersona::FrontDeskAgent => {
1051            storage::operations::ManagerDailyBriefPersonaCode::FrontDeskAgent
1052        }
1053    }
1054}
1055
1056async fn manager_daily_brief_agent_context(
1057    Query(query): Query<ManagerDailyBriefAgentContextQuery>,
1058) -> Json<Value> {
1059    let location_id = entities::LocationId(query.location_id);
1060    let operating_day = operations::operating_day::Date::try_new(query.operating_day)
1061        .expect("operating day date is always valid after query parsing");
1062    let service_demand_facts =
1063        local_manager_daily_brief_service_demand_facts(location_id, operating_day);
1064    let checkout_packets = local_manager_daily_brief_checkout_packets(location_id, operating_day);
1065    let retention_packets = local_manager_daily_brief_retention_packets(location_id, operating_day);
1066
1067    let request = manager_daily_brief::Request::builder()
1068        .location_id(location_id)
1069        .operating_day(operating_day)
1070        .prepared_for(manager_daily_brief::ManagerBriefPersona::GeneralManager)
1071        .demand_attention_threshold(
1072            manager_daily_brief::DemandThresholdUnits::try_new(10)
1073                .expect("static demand threshold is valid"),
1074        )
1075        .service_demand_facts(service_demand_facts.clone())
1076        .checkout_packets(checkout_packets.clone())
1077        .retention_packets(retention_packets.clone())
1078        .build();
1079    let packet = manager_daily_brief::Workflow::evaluate(request);
1080    let mut data_quality_issues = service_demand_facts
1081        .iter()
1082        .flat_map(|fact| fact.data_quality_issues().iter())
1083        .map(data_quality_issue_payload)
1084        .collect::<Vec<_>>();
1085
1086    if service_demand_facts.is_empty() {
1087        data_quality_issues.push(missing_context_issue_payload(
1088            "missing_service_demand_fact",
1089            "No source-grounded service demand fact exists for the requested location and operating day.",
1090        ));
1091    }
1092    if checkout_packets.is_empty() {
1093        data_quality_issues.push(missing_context_issue_payload(
1094            "missing_checkout_completion_packet",
1095            "No checkout/completion packet exists for the requested location and operating day.",
1096        ));
1097    }
1098    if retention_packets.is_empty() {
1099        data_quality_issues.push(missing_context_issue_payload(
1100            "missing_crm_retention_packet",
1101            "No CRM/retention packet exists for the requested location and operating day.",
1102        ));
1103    }
1104
1105    let mut source_refs = Vec::new();
1106    source_refs.extend(
1107        service_demand_facts
1108            .iter()
1109            .flat_map(|fact| fact.source_record_refs())
1110            .map(source_record_ref_payload),
1111    );
1112    source_refs.extend(checkout_packets.iter().map(|scoped| {
1113        source_record_ref_payload(&source::RecordRef::from_provenance(
1114            scoped.packet().provenance(),
1115        ))
1116    }));
1117    source_refs.extend(
1118        retention_packets
1119            .iter()
1120            .flat_map(|scoped| scoped.packet().source_record_refs())
1121            .map(source_record_ref_payload),
1122    );
1123
1124    let correlation_id = format!(
1125        "manager-daily-brief:{}:{}",
1126        query.location_id, query.operating_day
1127    );
1128
1129    Json(json!({
1130        "workflow": {
1131            "name": "manager_daily_brief",
1132            "version": "local-manager-daily-brief-context-v1"
1133        },
1134        "location_id": query.location_id.to_string(),
1135        "operating_day": query.operating_day.to_string(),
1136        "service_demand_facts": service_demand_facts.iter().map(service_demand_fact_payload).collect::<Vec<_>>(),
1137        "checkout_completion_exceptions": checkout_packets.iter().filter_map(checkout_exception_payload).collect::<Vec<_>>(),
1138        "crm_retention_opportunities": retention_packets.iter().filter_map(retention_opportunity_payload).collect::<Vec<_>>(),
1139        "manager_brief_actions": packet.actions().iter().map(manager_brief_action_payload).collect::<Vec<_>>(),
1140        "data_quality_issues": data_quality_issues,
1141        "source_refs": source_refs,
1142        "allowed_agent_actions": packet.safe_agent_actions().iter().map(safe_agent_action_code).collect::<Vec<_>>(),
1143        "blocked_actions": packet.blocked_actions().iter().map(blocked_action_code).collect::<Vec<_>>(),
1144        "labor_impact": {
1145            "before_minutes": packet.before_minutes().get(),
1146            "after_minutes": packet.after_minutes().get(),
1147            "minutes_saved": packet.minutes_saved()
1148        },
1149        "audit": {
1150            "context_packet_id": format!("manager-daily-brief-context:{}:{}", query.location_id, query.operating_day),
1151            "correlation_id": correlation_id
1152        }
1153    }))
1154}
1155
1156async fn upload_vaccine_document(
1157    State(state): State<VaccineDocumentState>,
1158    Json(request): Json<VaccineDocumentUploadRequest>,
1159) -> (StatusCode, Json<VaccineDocumentWorkflowPayload>) {
1160    let mut store = state.store.lock().await;
1161    let document_id = Uuid::new_v4();
1162    let extraction_id = Uuid::new_v4();
1163    let vaccine_record_id = Uuid::new_v4();
1164    let review_packet_id = Uuid::new_v4();
1165    let filename = request.filename.trim().to_owned();
1166    let sha256 = format!("{:x}", Sha256::digest(request.content.as_bytes()));
1167    let storage_key = format!(
1168        "vaccine-documents/pets/{}/{document_id}/{filename}",
1169        request.pet_id
1170    );
1171    let extraction = extract_vaccine_evidence(extraction_id, document_id, &request.content);
1172
1173    let document = DocumentRecord {
1174        id: document_id,
1175        pet_id: request.pet_id,
1176        customer_id: request.customer_id,
1177        classification: "vaccine_proof",
1178        source: "staff_upload",
1179        filename,
1180        mime_type: request.mime_type,
1181        content_length_bytes: request.content.len(),
1182        sha256,
1183        storage_bucket: "local-dev-vaccine-documents",
1184        storage_key,
1185        storage_version: "mvp-local-v1".to_owned(),
1186        virus_scan_status: "passed",
1187        pii_redaction_status: "pending",
1188        verification_status: "awaiting_review",
1189    };
1190    let vaccine_record = VaccineRecord {
1191        id: vaccine_record_id,
1192        pet_id: request.pet_id,
1193        source_document_id: document_id,
1194        vaccine_name: extraction.vaccine_name.clone(),
1195        status: "pending_review",
1196        effective_on: extraction.effective_on,
1197        expires_on: extraction.expires_on,
1198        review_gate: "medical_document_review",
1199    };
1200    let review_packet = ReviewPacket {
1201        id: review_packet_id,
1202        document_id,
1203        vaccine_record_id,
1204        gate: "medical_document_review",
1205        status: "ready_for_review",
1206        uncertainty: "medical_document_uncertainty_policy_requires_staff_approval",
1207    };
1208    let eligibility = PetEligibility {
1209        pet_id: request.pet_id,
1210        rabies_current: false,
1211        source_vaccine_record_id: Some(vaccine_record_id),
1212        status: "awaiting_medical_document_review",
1213    };
1214
1215    store.documents.insert(document_id, document.clone());
1216    store.extractions.insert(document_id, extraction.clone());
1217    store
1218        .vaccine_records
1219        .insert(vaccine_record_id, vaccine_record.clone());
1220    store
1221        .review_packets
1222        .insert(review_packet_id, review_packet.clone());
1223    store
1224        .eligibility
1225        .insert(request.pet_id, eligibility.clone());
1226    store.audit_events.push(audit(
1227        "document.received",
1228        &request.uploaded_by_staff_id,
1229        "document",
1230        document_id,
1231        [("storage_bucket", document.storage_bucket.to_owned())],
1232    ));
1233    store.audit_events.push(audit(
1234        "vaccine_extraction.persisted",
1235        "vaccine-document-agent",
1236        "document",
1237        document_id,
1238        [("schema_version", extraction.schema_version.to_owned())],
1239    ));
1240    store.audit_events.push(audit(
1241        "vaccine_record.review_requested",
1242        "vaccine-document-agent",
1243        "vaccine_record",
1244        vaccine_record_id,
1245        [("review_packet_id", review_packet_id.to_string())],
1246    ));
1247
1248    let payload = store.payload(document_id, vaccine_record_id, review_packet_id, None);
1249    (StatusCode::CREATED, Json(payload))
1250}
1251
1252async fn approve_vaccine_document(
1253    State(state): State<VaccineDocumentState>,
1254    Path(review_packet_id): Path<Uuid>,
1255    Json(request): Json<VaccineReviewDecisionRequest>,
1256) -> (StatusCode, Json<VaccineDocumentWorkflowPayload>) {
1257    decide_vaccine_document(state, review_packet_id, request, true).await
1258}
1259
1260async fn reject_vaccine_document(
1261    State(state): State<VaccineDocumentState>,
1262    Path(review_packet_id): Path<Uuid>,
1263    Json(request): Json<VaccineReviewDecisionRequest>,
1264) -> (StatusCode, Json<VaccineDocumentWorkflowPayload>) {
1265    decide_vaccine_document(state, review_packet_id, request, false).await
1266}
1267
1268async fn decide_vaccine_document(
1269    state: VaccineDocumentState,
1270    review_packet_id: Uuid,
1271    request: VaccineReviewDecisionRequest,
1272    approved: bool,
1273) -> (StatusCode, Json<VaccineDocumentWorkflowPayload>) {
1274    let mut store = state.store.lock().await;
1275    let packet = store
1276        .review_packets
1277        .get_mut(&review_packet_id)
1278        .expect("review packet exists");
1279    packet.status = if approved { "approved" } else { "rejected" };
1280    let document_id = packet.document_id;
1281    let vaccine_record_id = packet.vaccine_record_id;
1282
1283    let document = store
1284        .documents
1285        .get_mut(&document_id)
1286        .expect("document exists for packet");
1287    document.verification_status = if approved { "verified" } else { "rejected" };
1288
1289    let vaccine_record = store
1290        .vaccine_records
1291        .get_mut(&vaccine_record_id)
1292        .expect("vaccine exists for packet");
1293    vaccine_record.status = if approved {
1294        "verified_current"
1295    } else {
1296        "rejected"
1297    };
1298    let pet_id = vaccine_record.pet_id;
1299
1300    let eligibility = PetEligibility {
1301        pet_id,
1302        rabies_current: approved,
1303        source_vaccine_record_id: Some(vaccine_record_id),
1304        status: if approved {
1305            "eligible_from_approved_vaccine_document"
1306        } else {
1307            "ineligible_after_rejected_vaccine_document"
1308        },
1309    };
1310    store.eligibility.insert(pet_id, eligibility);
1311
1312    let approval = ApprovalRecord {
1313        id: Uuid::new_v4(),
1314        review_packet_id,
1315        target_document_id: document_id,
1316        target_vaccine_record_id: vaccine_record_id,
1317        gate: "medical_document_review",
1318        status: if approved { "approved" } else { "rejected" },
1319        decided_by_staff_id: request.reviewed_by_staff_id.clone(),
1320        decided_at: Utc::now().to_rfc3339(),
1321        reason: request.reason,
1322    };
1323    store.approvals.insert(approval.id, approval.clone());
1324    store.audit_events.push(audit(
1325        "approval.decision.recorded",
1326        &request.reviewed_by_staff_id,
1327        "approval",
1328        approval.id,
1329        [("status", approval.status.to_owned())],
1330    ));
1331    store.audit_events.push(audit(
1332        "pet.eligibility.updated",
1333        &request.reviewed_by_staff_id,
1334        "pet",
1335        pet_id,
1336        [("rabies_current", approved.to_string())],
1337    ));
1338
1339    let payload = store.payload(
1340        document_id,
1341        vaccine_record_id,
1342        review_packet_id,
1343        Some(approval),
1344    );
1345    (StatusCode::OK, Json(payload))
1346}
1347
1348impl VaccineDocumentStore {
1349    fn payload(
1350        &self,
1351        document_id: Uuid,
1352        vaccine_record_id: Uuid,
1353        review_packet_id: Uuid,
1354        approval: Option<ApprovalRecord>,
1355    ) -> VaccineDocumentWorkflowPayload {
1356        let document = self.documents.get(&document_id).expect("document").clone();
1357        let extraction = self
1358            .extractions
1359            .get(&document_id)
1360            .expect("extraction")
1361            .clone();
1362        let vaccine_record = self
1363            .vaccine_records
1364            .get(&vaccine_record_id)
1365            .expect("vaccine record")
1366            .clone();
1367        let review_packet = self
1368            .review_packets
1369            .get(&review_packet_id)
1370            .expect("review packet")
1371            .clone();
1372        let eligibility = self
1373            .eligibility
1374            .get(&vaccine_record.pet_id)
1375            .expect("eligibility")
1376            .clone();
1377        VaccineDocumentWorkflowPayload {
1378            document,
1379            extraction,
1380            vaccine_record,
1381            review_packet,
1382            approval,
1383            eligibility,
1384            audit_events: self.audit_events.clone(),
1385        }
1386    }
1387}
1388
1389fn build_inquiry_intake_record(request: InquirySubmissionRequest) -> InquiryIntakeRecord {
1390    let first_name = request
1391        .customer
1392        .full_name
1393        .split_whitespace()
1394        .next()
1395        .unwrap_or("there")
1396        .to_owned();
1397    let missing_info = if request.message.to_ascii_lowercase().contains("vaccine") {
1398        vec!["vaccine_records"]
1399    } else {
1400        vec!["requested_dates", "vaccine_records"]
1401    };
1402    let task_title = format!(
1403        "Collect missing info for {} / {} inquiry",
1404        request.customer.full_name, request.pet.name
1405    );
1406    let source_event_key = request.source_event_key.clone();
1407
1408    InquiryIntakeRecord {
1409        event: InquiryEvent {
1410            event_type: "inquiry.received",
1411            source_event_key: request.source_event_key,
1412            location_id: request.location_id,
1413        },
1414        lead: ParsedInquiryLead {
1415            customer_name: request.customer.full_name,
1416            customer_email: request.customer.email,
1417            customer_phone: request.customer.phone,
1418            pet_name: request.pet.name,
1419            species: request.pet.species,
1420            service: request.service,
1421            requested_dates: request
1422                .requested_dates
1423                .map(|dates| ParsedInquiryDateWindow {
1424                    start: dates.start,
1425                    end: dates.end,
1426                }),
1427            original_message: request.message,
1428            missing_info,
1429            review_status: "needs_staff_review",
1430        },
1431        draft_reply: InquiryDraftReply {
1432            status: "draft_created",
1433            live_send_allowed: false,
1434            approval_gate: "staff approval required before customer reply",
1435            body: format!(
1436                "Thanks {first_name} — we received your inquiry. Could you send current vaccine records so our staff can review availability and next steps?"
1437            ),
1438        },
1439        task: InquiryTask {
1440            kind: "missing_info_review",
1441            status: "open",
1442            title: task_title,
1443            review_gate: "front_desk_staff_review",
1444        },
1445        agent_runtime: "agent.inquiry-intake.fake_deterministic",
1446        policy_boundary: "draft_only_no_live_send_no_provider_write_no_booking_decision_without_staff_approval",
1447        audit_events: vec![
1448            InquiryAuditEvent {
1449                action: "inquiry.received.normalized",
1450                actor_kind: "workflow_event_normalizer",
1451                subject_key: source_event_key.clone(),
1452            },
1453            InquiryAuditEvent {
1454                action: "agent.inquiry-intake.fake_deterministic",
1455                actor_kind: "agent_runtime",
1456                subject_key: source_event_key.clone(),
1457            },
1458            InquiryAuditEvent {
1459                action: "message.draft.created",
1460                actor_kind: "agent_runtime",
1461                subject_key: source_event_key,
1462            },
1463        ],
1464    }
1465}
1466
1467fn extract_vaccine_evidence(id: Uuid, document_id: Uuid, content: &str) -> VaccineExtractionRecord {
1468    let lowered = content.to_ascii_lowercase();
1469    let vaccine_name = if lowered.contains("rabies") {
1470        "Rabies"
1471    } else {
1472        "Unknown vaccine"
1473    };
1474    VaccineExtractionRecord {
1475        id,
1476        document_id,
1477        schema_version: "vaccine_extraction.v1",
1478        vaccine_name: vaccine_name.to_owned(),
1479        effective_on: NaiveDate::from_ymd_opt(2026, 1, 15).expect("fixture date valid"),
1480        expires_on: lowered
1481            .contains("2027")
1482            .then(|| NaiveDate::from_ymd_opt(2027, 1, 15).expect("fixture date valid")),
1483        confidence: if lowered.contains("expires") {
1484            0.78
1485        } else {
1486            0.42
1487        },
1488        uncertainty_policy: "medical_document_uncertainty_policy_requires_staff_review",
1489        auto_accept_threshold: 0.95,
1490        raw_text_ref: format!("local-dev-ocr://documents/{document_id}/redacted-text"),
1491    }
1492}
1493
1494fn audit(
1495    action: &'static str,
1496    actor_id: &str,
1497    subject_kind: &'static str,
1498    subject_id: Uuid,
1499    metadata: impl IntoIterator<Item = (&'static str, String)>,
1500) -> AuditEvent {
1501    AuditEvent {
1502        action,
1503        actor_kind: "staff_or_agent",
1504        actor_id: actor_id.to_owned(),
1505        subject_kind,
1506        subject_id,
1507        metadata: metadata.into_iter().collect(),
1508    }
1509}
1510
1511fn local_manager_daily_brief_service_demand_facts(
1512    location_id: entities::LocationId,
1513    operating_day: operations::operating_day::Date,
1514) -> Vec<analytics::service_demand::Fact> {
1515    if location_id == local_manager_daily_brief_location_id()
1516        && operating_day == local_manager_daily_brief_operating_day()
1517    {
1518        vec![
1519            analytics::service_demand::Fact::try_new(
1520                analytics::service_demand::Id::try_new("service-demand-42")
1521                    .expect("static service demand id is valid"),
1522                operations::operating_day::Key::new(
1523                    location_id,
1524                    operations::service_core::ServiceLine::Boarding,
1525                    operating_day,
1526                ),
1527                analytics::service_demand::DemandUnits::try_new(18)
1528                    .expect("static demand units are valid"),
1529                vec![source::RecordRef::from_provenance(
1530                    &manager_brief_source_provenance(),
1531                )],
1532                analytics::ProjectionVersion::try_new("local-manager-brief-v1")
1533                    .expect("static projection version is valid"),
1534                vec![data_quality::Issue::new(
1535                    data_quality::Kind::UnmappedServiceType,
1536                    data_quality::Severity::Warning,
1537                    manager_brief_source_provenance(),
1538                    source::Timestamp::try_new("2026-06-17T00:00:00Z")
1539                        .expect("static timestamp is valid"),
1540                    false,
1541                )],
1542            )
1543            .expect("fixture source refs make service demand fact valid"),
1544        ]
1545    } else {
1546        Vec::new()
1547    }
1548}
1549
1550fn local_manager_daily_brief_checkout_packets(
1551    location_id: entities::LocationId,
1552    operating_day: operations::operating_day::Date,
1553) -> Vec<manager_daily_brief::ScopedCheckoutPacket> {
1554    if location_id == local_manager_daily_brief_location_id()
1555        && operating_day == local_manager_daily_brief_operating_day()
1556    {
1557        let packet = checkout_completion::Workflow::evaluate(
1558            checkout_completion::Request::builder()
1559                .reservation_id(local_manager_daily_brief_reservation_id())
1560                .source_provenance(manager_brief_source_provenance())
1561                .observed_source_status(source::reservation::Status::CheckedOut)
1562                .staff_handoff(open_manager_brief_staff_handoff())
1563                .build(),
1564        );
1565        vec![
1566            manager_daily_brief::ScopedCheckoutPacket::builder()
1567                .location_id(location_id)
1568                .operating_day(operating_day)
1569                .packet(packet)
1570                .build(),
1571        ]
1572    } else {
1573        Vec::new()
1574    }
1575}
1576
1577fn local_manager_daily_brief_retention_packets(
1578    location_id: entities::LocationId,
1579    operating_day: operations::operating_day::Date,
1580) -> Vec<manager_daily_brief::ScopedRetentionPacket> {
1581    if location_id == local_manager_daily_brief_location_id()
1582        && operating_day == local_manager_daily_brief_operating_day()
1583    {
1584        let checkout_packet = checkout_completion::Workflow::evaluate(
1585            checkout_completion::Request::builder()
1586                .reservation_id(local_manager_daily_brief_reservation_id())
1587                .source_provenance(manager_brief_source_provenance())
1588                .observed_source_status(source::reservation::Status::CheckedOut)
1589                .staff_handoff(resolved_manager_brief_staff_handoff())
1590                .build(),
1591        );
1592        let packet = crm_retention::Workflow::evaluate(
1593            crm_retention::Request::builder()
1594                .reservation_id(local_manager_daily_brief_reservation_id())
1595                .customer_id(local_manager_daily_brief_customer_id())
1596                .checkout_packet(checkout_packet)
1597                .contact_permission(manager_brief_contact_permission())
1598                .opportunities(vec![manager_brief_retention_opportunity()])
1599                .build(),
1600        );
1601        vec![
1602            manager_daily_brief::ScopedRetentionPacket::builder()
1603                .location_id(location_id)
1604                .operating_day(operating_day)
1605                .packet(packet)
1606                .build(),
1607        ]
1608    } else {
1609        Vec::new()
1610    }
1611}
1612
1613fn service_demand_fact_payload(fact: &analytics::service_demand::Fact) -> Value {
1614    json!({
1615        "kind": "service_demand_forecast",
1616        "service_line": "boarding",
1617        "demand_units": fact.demand_units().get(),
1618        "projection_version": fact.projection_version().as_str(),
1619        "data_quality_status": service_demand_data_quality_status_code(fact.data_quality_status()),
1620        "source_refs": fact.source_record_refs().iter().map(source_record_ref_payload).collect::<Vec<_>>()
1621    })
1622}
1623
1624fn checkout_exception_payload(scoped: &manager_daily_brief::ScopedCheckoutPacket) -> Option<Value> {
1625    let packet = scoped.packet();
1626    if matches!(
1627        packet.completion_status(),
1628        checkout_completion::CompletionStatus::StaffVerifiedCheckout
1629    ) {
1630        return None;
1631    }
1632    Some(json!({
1633        "reservation_id": format!("{:?}", packet.reservation_id()),
1634        "completion_status": checkout_completion_status_code(packet.completion_status()),
1635        "required_review_gates": packet.required_review_gates().iter().map(review_gate_code).collect::<Vec<_>>(),
1636        "source_refs": [source_record_ref_payload(&source::RecordRef::from_provenance(packet.provenance()))]
1637    }))
1638}
1639
1640fn retention_opportunity_payload(
1641    scoped: &manager_daily_brief::ScopedRetentionPacket,
1642) -> Option<Value> {
1643    let packet = scoped.packet();
1644    if !matches!(
1645        packet.eligibility(),
1646        crm_retention::FollowUpEligibility::Eligible { .. }
1647    ) {
1648        return None;
1649    }
1650    Some(json!({
1651        "reservation_id": format!("{:?}", packet.reservation_id()),
1652        "eligibility": "eligible",
1653        "required_review_gates": packet.required_review_gates().iter().map(review_gate_code).collect::<Vec<_>>(),
1654        "source_refs": packet.source_record_refs().iter().map(source_record_ref_payload).collect::<Vec<_>>()
1655    }))
1656}
1657
1658fn manager_brief_action_payload(action: &manager_daily_brief::BriefAction) -> Value {
1659    json!({
1660        "id": action.id().clone().into_inner(),
1661        "kind": brief_action_kind_code(action.kind()),
1662        "priority": brief_action_priority_code(action.priority()),
1663        "owner_persona": manager_brief_persona_code(action.owner_persona()),
1664        "removed_manual_work": removed_manual_work_code(action.removed_manual_work()),
1665        "source_facts": action.source_facts().iter().map(source_fact_payload).collect::<Vec<_>>(),
1666        "required_review_gates": action.required_review_gates().iter().map(review_gate_code).collect::<Vec<_>>(),
1667        "labor_impact": {
1668            "before_minutes": action.labor_impact().before_minutes().get(),
1669            "after_minutes": action.labor_impact().after_minutes().get(),
1670            "minutes_saved": action.labor_impact().minutes_saved()
1671        }
1672    })
1673}
1674
1675fn source_fact_payload(fact: &manager_daily_brief::SourceFact) -> Value {
1676    json!({
1677        "kind": source_fact_kind_code(fact.kind()),
1678        "summary": fact.summary().clone().into_inner(),
1679        "source_refs": fact.source_record_refs().iter().map(source_record_ref_payload).collect::<Vec<_>>()
1680    })
1681}
1682
1683fn data_quality_issue_payload(issue: &data_quality::Issue) -> Value {
1684    json!({
1685        "kind": data_quality_kind_code(&issue.kind()),
1686        "severity": data_quality_severity_code(issue.severity()),
1687        "workflow_blocking": issue.workflow_blocking(),
1688        "source_refs": [source_record_ref_payload(issue.source_record_ref())]
1689    })
1690}
1691
1692fn missing_context_issue_payload(kind: &'static str, detail: &'static str) -> Value {
1693    json!({
1694        "kind": kind,
1695        "severity": "warning",
1696        "workflow_blocking": false,
1697        "detail": detail,
1698        "source_refs": []
1699    })
1700}
1701
1702fn source_record_ref_payload(record_ref: &source::RecordRef) -> Value {
1703    json!({
1704        "system": source_system_code(record_ref.system()),
1705        "record_id": record_ref.record_id().as_str()
1706    })
1707}
1708
1709fn local_data_quality_hygiene_packet(
1710    location_id: entities::LocationId,
1711    operating_day: operations::operating_day::Date,
1712) -> data_quality_hygiene::Packet {
1713    data_quality_hygiene::Workflow::evaluate(
1714        data_quality_hygiene::Request::builder()
1715            .location_id(location_id)
1716            .operating_day(operating_day)
1717            .prepared_for(data_quality_hygiene::HygienePersona::GeneralManager)
1718            .candidates(local_data_quality_hygiene_candidates())
1719            .build(),
1720    )
1721}
1722
1723fn local_data_quality_hygiene_candidates() -> Vec<data_quality_hygiene::Candidate> {
1724    vec![
1725        data_quality_hygiene::Candidate::builder()
1726            .id(
1727                data_quality_hygiene::IssueRef::try_new("dq-vaccine-stale-42")
1728                    .expect("static issue ref is valid"),
1729            )
1730            .kind(data_quality_hygiene::CandidateKind::SourceFreshness)
1731            .issue(data_quality::Issue::new(
1732                data_quality::Kind::MissingVaccinationRecord,
1733                data_quality::Severity::Blocking,
1734                data_quality_hygiene_source_provenance(
1735                    "GET /vaccinations/{id}",
1736                    "vaccine-record-42",
1737                ),
1738                source::Timestamp::try_new("2026-06-17T09:00:00Z")
1739                    .expect("static timestamp is valid"),
1740                true,
1741            ))
1742            .source_record_refs(vec![source::RecordRef::from_provenance(
1743                &data_quality_hygiene_source_provenance(
1744                    "GET /vaccinations/{id}",
1745                    "vaccine-record-42",
1746                ),
1747            )])
1748            .source_freshness(data_quality_hygiene::SourceFreshness::Stale)
1749            .sensitivity(data_quality_hygiene::Sensitivity::VaccineEvidence)
1750            .build(),
1751        data_quality_hygiene::Candidate::builder()
1752            .id(
1753                data_quality_hygiene::IssueRef::try_new("dq-duplicate-customer-17")
1754                    .expect("static issue ref is valid"),
1755            )
1756            .kind(data_quality_hygiene::CandidateKind::DuplicateCandidate)
1757            .issue(data_quality::Issue::new(
1758                data_quality::Kind::DuplicateSourceRecord,
1759                data_quality::Severity::Warning,
1760                data_quality_hygiene_source_provenance(
1761                    "GET /customers/{id}",
1762                    "customer-duplicate-17",
1763                ),
1764                source::Timestamp::try_new("2026-06-17T09:05:00Z")
1765                    .expect("static timestamp is valid"),
1766                false,
1767            ))
1768            .source_record_refs(vec![source::RecordRef::from_provenance(
1769                &data_quality_hygiene_source_provenance(
1770                    "GET /customers/{id}",
1771                    "customer-duplicate-17",
1772                ),
1773            )])
1774            .source_freshness(data_quality_hygiene::SourceFreshness::Conflicting)
1775            .sensitivity(data_quality_hygiene::Sensitivity::StandardOperationalEvidence)
1776            .build(),
1777    ]
1778}
1779
1780fn data_quality_hygiene_packet_payload(packet: &data_quality_hygiene::Packet) -> Value {
1781    json!({
1782        "workflow": {
1783            "name": packet.workflow(),
1784            "version": packet.schema_version()
1785        },
1786        "location_id": packet.location_id().0,
1787        "operating_day": packet.operating_day().get().to_string(),
1788        "prepared_for": data_quality_hygiene_persona_code(packet.prepared_for()),
1789        "candidates": packet.candidates().iter().map(data_quality_hygiene_candidate_payload).collect::<Vec<_>>(),
1790        "hygiene_actions": packet.actions().iter().map(data_quality_hygiene_action_payload).collect::<Vec<_>>(),
1791        "allowed_agent_actions": packet.safe_agent_actions().iter().map(|action| data_quality_hygiene_safe_action_code(*action)).collect::<Vec<_>>(),
1792        "blocked_actions": packet.blocked_actions().iter().map(|action| data_quality_hygiene_blocked_action_code(*action)).collect::<Vec<_>>(),
1793        "labor_savings_estimate": {
1794            "before_minutes": packet.before_minutes().get(),
1795            "after_minutes": packet.after_minutes().get(),
1796            "estimated_minutes_saved": packet.minutes_saved()
1797        },
1798        "live_side_effects_allowed": false,
1799        "audit": {
1800            "context_packet_id": packet.context_packet_id().as_str(),
1801            "correlation_id": packet.correlation_id().as_str(),
1802            "runtime": "agent.data-quality-hygiene.fake_deterministic"
1803        }
1804    })
1805}
1806
1807fn data_quality_hygiene_candidate_payload(candidate: &data_quality_hygiene::Candidate) -> Value {
1808    json!({
1809        "id": candidate.id().as_str(),
1810        "kind": data_quality_hygiene_candidate_kind_code(candidate.kind()),
1811        "issue": data_quality_issue_payload(candidate.issue()),
1812        "source_refs": candidate.source_record_refs().iter().map(source_record_ref_payload).collect::<Vec<_>>(),
1813        "source_freshness": data_quality_hygiene_source_freshness_code(candidate.source_freshness()),
1814        "sensitivity": data_quality_hygiene_sensitivity_code(candidate.sensitivity())
1815    })
1816}
1817
1818fn data_quality_hygiene_action_payload(action: &data_quality_hygiene::Action) -> Value {
1819    json!({
1820        "id": action.id().as_str(),
1821        "kind": data_quality_hygiene_action_kind_code(action.kind()),
1822        "priority": data_quality_hygiene_action_priority_code(action.priority()),
1823        "owner_persona": data_quality_hygiene_persona_code(action.owner_persona()),
1824        "removed_manual_work": data_quality_hygiene_removed_manual_work_code(action.removed_manual_work()),
1825        "rationale": action.rationale(),
1826        "source_refs": action.source_record_refs().iter().map(source_record_ref_payload).collect::<Vec<_>>(),
1827        "issue_refs": action.issue_refs().iter().map(|issue_ref| issue_ref.as_str()).collect::<Vec<_>>(),
1828        "review_gates": action.required_review_gates().iter().map(review_gate_code).collect::<Vec<_>>(),
1829        "labor_impact": {
1830            "before_minutes": action.labor_impact().before_minutes().get(),
1831            "after_minutes": action.labor_impact().after_minutes().get(),
1832            "estimated_minutes_saved": action.labor_impact().minutes_saved()
1833        },
1834        "live_side_effects_allowed": false
1835    })
1836}
1837
1838fn validate_data_quality_hygiene_submitted_action(
1839    packet: &data_quality_hygiene::Packet,
1840    action: &DataQualityHygieneSubmittedAction,
1841) -> Vec<String> {
1842    let mut reasons = Vec::new();
1843    if action.source_refs.is_empty() {
1844        reasons.push("missing_source_refs".to_owned());
1845    }
1846    if action.issue_refs.is_empty() {
1847        reasons.push("missing_data_quality_issue_refs".to_owned());
1848    }
1849    if action.attempted_ambiguity_resolution {
1850        reasons.push("attempted_ambiguity_hiding".to_owned());
1851    }
1852    for side_effect in &action.requested_side_effects {
1853        reasons.push(data_quality_hygiene_requested_side_effect_rejection_reason(
1854            side_effect,
1855        ));
1856    }
1857    let matching_action = packet.actions().iter().find(|packet_action| {
1858        packet_action.id().as_str() == action.action_id
1859            && data_quality_hygiene_action_kind_code(packet_action.kind()) == action.kind
1860    });
1861    match matching_action {
1862        Some(packet_action) => {
1863            let required_gates = packet_action
1864                .required_review_gates()
1865                .iter()
1866                .map(|gate| review_gate_code(gate).to_owned())
1867                .collect::<Vec<_>>();
1868            if required_gates != action.review_gates {
1869                reasons.push("wrong_review_gate".to_owned());
1870            }
1871        }
1872        None => reasons.push("unsupported_action_kind".to_owned()),
1873    }
1874    reasons.sort();
1875    reasons.dedup();
1876    reasons
1877}
1878
1879fn data_quality_hygiene_requested_side_effect_rejection_reason(side_effect: &str) -> String {
1880    match side_effect.trim() {
1881        "send_customer_message"
1882        | "mutate_provider_or_pms_record"
1883        | "change_staff_schedule"
1884        | "move_refund_discount_or_payment"
1885        | "hide_or_auto_resolve_source_ambiguity"
1886        | "expose_quarantined_sensitive_payload" => "blocked_side_effect_requested".to_owned(),
1887        _ => "unsupported_side_effect_requested".to_owned(),
1888    }
1889}
1890
1891fn data_quality_hygiene_blocked_action_codes() -> Vec<&'static str> {
1892    vec![
1893        "send_customer_message",
1894        "mutate_provider_or_pms_record",
1895        "change_staff_schedule",
1896        "move_refund_discount_or_payment",
1897        "hide_or_auto_resolve_source_ambiguity",
1898        "expose_quarantined_sensitive_payload",
1899    ]
1900}
1901
1902fn stored_source_record_ref_from_payload(
1903    value: &Value,
1904) -> storage::operations::StoredSourceRecordRef {
1905    storage::operations::StoredSourceRecordRef::builder()
1906        .system(
1907            value
1908                .get("system")
1909                .and_then(Value::as_str)
1910                .unwrap_or("unknown")
1911                .to_owned(),
1912        )
1913        .record_type(
1914            value
1915                .get("record_type")
1916                .and_then(Value::as_str)
1917                .unwrap_or("source_record")
1918                .to_owned(),
1919        )
1920        .record_id(
1921            value
1922                .get("record_id")
1923                .and_then(Value::as_str)
1924                .unwrap_or("unknown")
1925                .to_owned(),
1926        )
1927        .observed_at(
1928            value
1929                .get("observed_at")
1930                .and_then(Value::as_str)
1931                .unwrap_or("2026-06-17T00:00:00Z")
1932                .to_owned(),
1933        )
1934        .adapter_version(
1935            value
1936                .get("adapter_version")
1937                .and_then(Value::as_str)
1938                .unwrap_or("gingr-v0-readonly")
1939                .to_owned(),
1940        )
1941        .build()
1942}
1943
1944fn stored_data_quality_hygiene_action_kind(
1945    kind: data_quality_hygiene::ActionKind,
1946) -> storage::operations::DataQualityHygieneActionKindCode {
1947    use data_quality_hygiene::ActionKind as App;
1948    use storage::operations::DataQualityHygieneActionKindCode as Stored;
1949    match kind {
1950        App::InvestigateMissingSourceEvidence => Stored::InvestigateMissingSourceEvidence,
1951        App::ReconcileDuplicateCustomerOrPetCandidate => {
1952            Stored::ReconcileDuplicateCustomerOrPetCandidate
1953        }
1954        App::CompleteMissingPetOrCustomerProfileFields => {
1955            Stored::CompleteMissingPetOrCustomerProfileFields
1956        }
1957        App::ReviewStaleVaccinationSourceFreshness => Stored::ReviewStaleVaccinationSourceFreshness,
1958        App::NormalizeAmbiguousServiceLineNaming => Stored::NormalizeAmbiguousServiceLineNaming,
1959        App::ReviewCheckoutOrUnclosedReservationEvidence => {
1960            Stored::ReviewCheckoutOrUnclosedReservationEvidence
1961        }
1962        App::EscalateSensitiveOrQuarantinedPayload => Stored::EscalateSensitiveOrQuarantinedPayload,
1963    }
1964}
1965
1966fn stored_data_quality_hygiene_persona(
1967    persona: data_quality_hygiene::HygienePersona,
1968) -> storage::operations::DataQualityHygienePersonaCode {
1969    use data_quality_hygiene::HygienePersona as App;
1970    use storage::operations::DataQualityHygienePersonaCode as Stored;
1971    match persona {
1972        App::GeneralManager => Stored::GeneralManager,
1973        App::AssistantGeneralManager => Stored::AssistantGeneralManager,
1974        App::FrontDeskLead => Stored::FrontDeskLead,
1975        App::FrontDeskAgent => Stored::FrontDeskAgent,
1976        App::RegionalOperator => Stored::RegionalOperator,
1977        App::OperationsAnalyst => Stored::OperationsAnalyst,
1978    }
1979}
1980
1981fn local_data_quality_hygiene_location_id() -> entities::LocationId {
1982    local_manager_daily_brief_location_id()
1983}
1984
1985fn local_data_quality_hygiene_operating_day() -> operations::operating_day::Date {
1986    local_manager_daily_brief_operating_day()
1987}
1988
1989fn data_quality_hygiene_source_provenance(
1990    endpoint: &'static str,
1991    record_id: &'static str,
1992) -> source::Provenance {
1993    source::Provenance::builder()
1994        .system(source::System::Gingr)
1995        .endpoint(source::Endpoint::try_new(endpoint).expect("static endpoint is valid"))
1996        .record_id(source::record::Id::try_new(record_id).expect("static record id is valid"))
1997        .extraction_batch(
1998            source::ExtractionBatchId::try_new("dq-hygiene-batch-local")
1999                .expect("static batch id is valid"),
2000        )
2001        .pulled_at(
2002            source::Timestamp::try_new("2026-06-17T00:00:00Z").expect("static timestamp is valid"),
2003        )
2004        .request_scope(
2005            source::RequestScope::try_new("local-data-quality-hygiene-context")
2006                .expect("static request scope is valid"),
2007        )
2008        .schema_version(
2009            source::SchemaVersion::try_new("gingr-v0-readonly")
2010                .expect("static schema version is valid"),
2011        )
2012        .payload_hash(
2013            source::PayloadHash::try_new("sha256:dataqualityhygienefixture")
2014                .expect("static payload hash is valid"),
2015        )
2016        .raw_payload_ref(
2017            source::RawPayloadRef::try_new("fixtures/gingr/data-quality-hygiene.json")
2018                .expect("static raw payload ref is valid"),
2019        )
2020        .build()
2021}
2022
2023fn data_quality_hygiene_action_kind_code(kind: data_quality_hygiene::ActionKind) -> &'static str {
2024    match kind {
2025        data_quality_hygiene::ActionKind::InvestigateMissingSourceEvidence => {
2026            "investigate_missing_source_evidence"
2027        }
2028        data_quality_hygiene::ActionKind::ReconcileDuplicateCustomerOrPetCandidate => {
2029            "reconcile_duplicate_customer_or_pet_candidate"
2030        }
2031        data_quality_hygiene::ActionKind::CompleteMissingPetOrCustomerProfileFields => {
2032            "complete_missing_pet_or_customer_profile_fields"
2033        }
2034        data_quality_hygiene::ActionKind::ReviewStaleVaccinationSourceFreshness => {
2035            "review_stale_vaccination_source_freshness"
2036        }
2037        data_quality_hygiene::ActionKind::NormalizeAmbiguousServiceLineNaming => {
2038            "normalize_ambiguous_service_line_naming"
2039        }
2040        data_quality_hygiene::ActionKind::ReviewCheckoutOrUnclosedReservationEvidence => {
2041            "review_checkout_or_unclosed_reservation_evidence"
2042        }
2043        data_quality_hygiene::ActionKind::EscalateSensitiveOrQuarantinedPayload => {
2044            "escalate_sensitive_or_quarantined_payload"
2045        }
2046    }
2047}
2048
2049fn data_quality_hygiene_persona_code(
2050    persona: data_quality_hygiene::HygienePersona,
2051) -> &'static str {
2052    match persona {
2053        data_quality_hygiene::HygienePersona::GeneralManager => "general_manager",
2054        data_quality_hygiene::HygienePersona::AssistantGeneralManager => {
2055            "assistant_general_manager"
2056        }
2057        data_quality_hygiene::HygienePersona::FrontDeskLead => "front_desk_lead",
2058        data_quality_hygiene::HygienePersona::FrontDeskAgent => "front_desk_agent",
2059        data_quality_hygiene::HygienePersona::RegionalOperator => "regional_operator",
2060        data_quality_hygiene::HygienePersona::OperationsAnalyst => "operations_analyst",
2061    }
2062}
2063
2064fn data_quality_hygiene_candidate_kind_code(
2065    kind: data_quality_hygiene::CandidateKind,
2066) -> &'static str {
2067    match kind {
2068        data_quality_hygiene::CandidateKind::SourceIssue => "source_issue",
2069        data_quality_hygiene::CandidateKind::DuplicateCandidate => "duplicate_candidate",
2070        data_quality_hygiene::CandidateKind::ProfileGap => "profile_gap",
2071        data_quality_hygiene::CandidateKind::ServiceLineMapping => "service_line_mapping",
2072        data_quality_hygiene::CandidateKind::SourceFreshness => "source_freshness",
2073    }
2074}
2075
2076fn data_quality_hygiene_source_freshness_code(
2077    freshness: data_quality_hygiene::SourceFreshness,
2078) -> &'static str {
2079    match freshness {
2080        data_quality_hygiene::SourceFreshness::Current => "current",
2081        data_quality_hygiene::SourceFreshness::Stale => "stale",
2082        data_quality_hygiene::SourceFreshness::Conflicting => "conflicting",
2083        data_quality_hygiene::SourceFreshness::Missing => "missing",
2084    }
2085}
2086
2087fn data_quality_hygiene_sensitivity_code(
2088    sensitivity: data_quality_hygiene::Sensitivity,
2089) -> &'static str {
2090    match sensitivity {
2091        data_quality_hygiene::Sensitivity::StandardOperationalEvidence => {
2092            "standard_operational_evidence"
2093        }
2094        data_quality_hygiene::Sensitivity::VaccineEvidence => "vaccine_evidence",
2095        data_quality_hygiene::Sensitivity::IncidentOrBehaviorEvidence => {
2096            "incident_or_behavior_evidence"
2097        }
2098        data_quality_hygiene::Sensitivity::PaymentEvidence => "payment_evidence",
2099        data_quality_hygiene::Sensitivity::QuarantinedSensitivePayload => {
2100            "quarantined_sensitive_payload"
2101        }
2102    }
2103}
2104
2105fn data_quality_hygiene_action_priority_code(
2106    priority: data_quality_hygiene::ActionPriority,
2107) -> &'static str {
2108    match priority {
2109        data_quality_hygiene::ActionPriority::High => "high",
2110        data_quality_hygiene::ActionPriority::Medium => "medium",
2111        data_quality_hygiene::ActionPriority::Low => "low",
2112    }
2113}
2114
2115fn data_quality_hygiene_removed_manual_work_code(
2116    work: data_quality_hygiene::RemovedManualWork,
2117) -> &'static str {
2118    match work {
2119        data_quality_hygiene::RemovedManualWork::MissingEvidenceInvestigation => {
2120            "missing_evidence_investigation"
2121        }
2122        data_quality_hygiene::RemovedManualWork::DuplicateCandidateReconciliation => {
2123            "duplicate_candidate_reconciliation"
2124        }
2125        data_quality_hygiene::RemovedManualWork::IncompleteProfileCleanupPreparation => {
2126            "incomplete_profile_cleanup_preparation"
2127        }
2128        data_quality_hygiene::RemovedManualWork::SourceFreshnessReview => "source_freshness_review",
2129        data_quality_hygiene::RemovedManualWork::ServiceLineNormalizationReview => {
2130            "service_line_normalization_review"
2131        }
2132        data_quality_hygiene::RemovedManualWork::CheckoutEvidenceReview => {
2133            "checkout_evidence_review"
2134        }
2135        data_quality_hygiene::RemovedManualWork::SensitivePayloadEscalation => {
2136            "sensitive_payload_escalation"
2137        }
2138    }
2139}
2140
2141fn data_quality_hygiene_safe_action_code(
2142    action: data_quality_hygiene::SafeAgentAction,
2143) -> &'static str {
2144    match action {
2145        data_quality_hygiene::SafeAgentAction::SummarizeSourceEvidence => {
2146            "summarize_source_evidence"
2147        }
2148        data_quality_hygiene::SafeAgentAction::RankHygieneActions => "rank_hygiene_actions",
2149        data_quality_hygiene::SafeAgentAction::DraftInternalCleanupTask => {
2150            "draft_internal_cleanup_task"
2151        }
2152        data_quality_hygiene::SafeAgentAction::PreserveAmbiguityForReview => {
2153            "preserve_ambiguity_for_review"
2154        }
2155        data_quality_hygiene::SafeAgentAction::EstimateReconciliationMinutesSaved => {
2156            "estimate_reconciliation_minutes_saved"
2157        }
2158    }
2159}
2160
2161fn data_quality_hygiene_blocked_action_code(
2162    action: data_quality_hygiene::BlockedAction,
2163) -> &'static str {
2164    match action {
2165        data_quality_hygiene::BlockedAction::SendCustomerMessage => "send_customer_message",
2166        data_quality_hygiene::BlockedAction::MutateProviderOrPmsRecord => {
2167            "mutate_provider_or_pms_record"
2168        }
2169        data_quality_hygiene::BlockedAction::ChangeStaffSchedule => "change_staff_schedule",
2170        data_quality_hygiene::BlockedAction::MoveRefundDiscountOrPayment => {
2171            "move_refund_discount_or_payment"
2172        }
2173        data_quality_hygiene::BlockedAction::HideOrAutoResolveSourceAmbiguity => {
2174            "hide_or_auto_resolve_source_ambiguity"
2175        }
2176        data_quality_hygiene::BlockedAction::ExposeQuarantinedSensitivePayload => {
2177            "expose_quarantined_sensitive_payload"
2178        }
2179    }
2180}
2181
2182fn local_manager_daily_brief_location_id() -> entities::LocationId {
2183    entities::LocationId(Uuid::from_u128(0x00c0_ffee_0000_0000_0000_0000_0000_0001))
2184}
2185
2186fn local_manager_daily_brief_customer_id() -> entities::CustomerId {
2187    entities::CustomerId(Uuid::from_u128(0x00c0_ffee_0000_0000_0000_0000_0000_0099))
2188}
2189
2190fn local_manager_daily_brief_reservation_id() -> entities::reservation::Id {
2191    entities::reservation::Id(Uuid::from_u128(0x00c0_ffee_0000_0000_0000_0000_0000_0042))
2192}
2193
2194fn local_manager_daily_brief_operating_day() -> operations::operating_day::Date {
2195    operations::operating_day::Date::try_new(
2196        NaiveDate::from_ymd_opt(2026, 6, 17).expect("fixture operating day is valid"),
2197    )
2198    .expect("fixture operating day is valid")
2199}
2200
2201fn open_manager_brief_staff_handoff() -> checkout_completion::StaffHandoff {
2202    checkout_completion::StaffHandoff::builder()
2203        .completed_by(entities::ActorRef::Staff {
2204            staff_id: entities::StaffId::try_new("front-desk-erin")
2205                .expect("static staff id is valid"),
2206        })
2207        .completed_at(DateTime::<Utc>::UNIX_EPOCH)
2208        .belongings_status(checkout_completion::BelongingsStatus::NeedsStaffFollowUp)
2209        .care_summary(
2210            checkout_completion::CareSummary::try_new("Medication bag needs review.")
2211                .expect("static care summary is valid"),
2212        )
2213        .departure_notes_review(checkout_completion::DepartureNotesReview::ManagerReviewRequired)
2214        .build()
2215}
2216
2217fn resolved_manager_brief_staff_handoff() -> checkout_completion::StaffHandoff {
2218    checkout_completion::StaffHandoff::builder()
2219        .completed_by(entities::ActorRef::Staff {
2220            staff_id: entities::StaffId::try_new("front-desk-erin")
2221                .expect("static staff id is valid"),
2222        })
2223        .completed_at(DateTime::<Utc>::UNIX_EPOCH)
2224        .belongings_status(checkout_completion::BelongingsStatus::ReturnedToCustomer)
2225        .care_summary(
2226            checkout_completion::CareSummary::try_new("Clean checkout.")
2227                .expect("static care summary is valid"),
2228        )
2229        .departure_notes_review(checkout_completion::DepartureNotesReview::StaffReviewed)
2230        .build()
2231}
2232
2233fn manager_brief_retention_opportunity() -> crm_retention::RetentionOpportunity {
2234    crm_retention::RetentionOpportunity::builder()
2235        .kind(crm_retention::OpportunityKind::NextBoardingStay)
2236        .evidence(
2237            crm_retention::OpportunityEvidence::builder()
2238                .reason_code(crm_retention::SourceGroundedReasonCode::CompletedBoardingStay)
2239                .summary(
2240                    crm_retention::EvidenceSummary::try_new(
2241                        "Completed boarding stay and owner mentioned a return trip.",
2242                    )
2243                    .expect("static evidence summary is valid"),
2244                )
2245                .provenance(manager_brief_source_provenance())
2246                .build(),
2247        )
2248        .build()
2249}
2250
2251fn manager_brief_contact_permission() -> crm_retention::ContactPermission {
2252    crm_retention::ContactPermission::builder()
2253        .preferred_channel(message::Channel::Email)
2254        .allowed_channels(vec![message::Channel::Email])
2255        .marketing_consent(crm_retention::ConsentStatus::Granted)
2256        .transactional_consent(crm_retention::ConsentStatus::Granted)
2257        .source_record_refs(vec![source::RecordRef::from_provenance(
2258            &manager_brief_contact_provenance(),
2259        )])
2260        .build()
2261}
2262
2263fn manager_brief_source_provenance() -> source::Provenance {
2264    source::Provenance::builder()
2265        .system(source::System::Gingr)
2266        .endpoint(
2267            source::Endpoint::try_new("GET /reservations/{id}").expect("static endpoint is valid"),
2268        )
2269        .record_id(
2270            source::record::Id::try_new("reservation-42").expect("static record id is valid"),
2271        )
2272        .extraction_batch(
2273            source::ExtractionBatchId::try_new("manager-brief-batch-local")
2274                .expect("static batch id is valid"),
2275        )
2276        .pulled_at(
2277            source::Timestamp::try_new("2026-06-17T00:00:00Z").expect("static timestamp is valid"),
2278        )
2279        .request_scope(
2280            source::RequestScope::try_new("local-manager-daily-brief-context")
2281                .expect("static request scope is valid"),
2282        )
2283        .schema_version(
2284            source::SchemaVersion::try_new("gingr-v0-readonly")
2285                .expect("static schema version is valid"),
2286        )
2287        .payload_hash(
2288            source::PayloadHash::try_new("sha256:managerbrieffixture")
2289                .expect("static payload hash is valid"),
2290        )
2291        .raw_payload_ref(
2292            source::RawPayloadRef::try_new("fixtures/gingr/manager-brief.json")
2293                .expect("static raw payload ref is valid"),
2294        )
2295        .build()
2296}
2297
2298fn manager_brief_contact_provenance() -> source::Provenance {
2299    source::Provenance::builder()
2300        .system(source::System::Gingr)
2301        .endpoint(
2302            source::Endpoint::try_new("GET /customers/{id}/contact-permissions")
2303                .expect("static endpoint is valid"),
2304        )
2305        .record_id(
2306            source::record::Id::try_new("customer-contact-99").expect("static record id is valid"),
2307        )
2308        .extraction_batch(
2309            source::ExtractionBatchId::try_new("manager-brief-batch-local")
2310                .expect("static batch id is valid"),
2311        )
2312        .pulled_at(
2313            source::Timestamp::try_new("2026-06-17T00:00:00Z").expect("static timestamp is valid"),
2314        )
2315        .request_scope(
2316            source::RequestScope::try_new("local-manager-daily-brief-context")
2317                .expect("static request scope is valid"),
2318        )
2319        .schema_version(
2320            source::SchemaVersion::try_new("gingr-v0-readonly")
2321                .expect("static schema version is valid"),
2322        )
2323        .payload_hash(
2324            source::PayloadHash::try_new("sha256:managerbriefcontactfixture")
2325                .expect("static payload hash is valid"),
2326        )
2327        .raw_payload_ref(
2328            source::RawPayloadRef::try_new("fixtures/gingr/manager-brief-contact.json")
2329                .expect("static raw payload ref is valid"),
2330        )
2331        .build()
2332}
2333
2334fn source_system_code(system: source::System) -> &'static str {
2335    match system {
2336        source::System::Gingr => "gingr",
2337        source::System::BusinessIntelligence => "business_intelligence",
2338        source::System::LaborScheduling => "labor_scheduling",
2339        source::System::Timeclock => "timeclock",
2340        source::System::Payroll => "payroll",
2341        source::System::CapacityInventory => "capacity_inventory",
2342        source::System::PointOfSale => "point_of_sale",
2343        source::System::ManualImport => "manual_import",
2344    }
2345}
2346
2347fn service_demand_data_quality_status_code(
2348    status: analytics::service_demand::DataQualityStatus,
2349) -> &'static str {
2350    match status {
2351        analytics::service_demand::DataQualityStatus::Complete => "complete",
2352        analytics::service_demand::DataQualityStatus::ManagerReviewRequired => {
2353            "manager_review_required"
2354        }
2355    }
2356}
2357
2358fn checkout_completion_status_code(status: checkout_completion::CompletionStatus) -> &'static str {
2359    match status {
2360        checkout_completion::CompletionStatus::StaffVerifiedCheckout => "staff_verified_checkout",
2361        checkout_completion::CompletionStatus::NeedsStaffHandoffReview => {
2362            "needs_staff_handoff_review"
2363        }
2364        checkout_completion::CompletionStatus::SourceNotCheckedOut => "source_not_checked_out",
2365    }
2366}
2367
2368fn review_gate_code(gate: &policy::ReviewGate) -> &'static str {
2369    match gate {
2370        policy::ReviewGate::ManagerApproval => "manager_approval",
2371        policy::ReviewGate::CustomerMessageApproval => "customer_message_approval",
2372        policy::ReviewGate::MedicalDocumentReview => "medical_document_review",
2373        policy::ReviewGate::BehaviorReview => "behavior_review",
2374        policy::ReviewGate::RefundOrDepositException => "refund_or_deposit_exception",
2375    }
2376}
2377
2378fn safe_agent_action_code(action: &manager_daily_brief::SafeAgentAction) -> &'static str {
2379    match action {
2380        manager_daily_brief::SafeAgentAction::SummarizeSourceEvidence => {
2381            "summarize_source_evidence"
2382        }
2383        manager_daily_brief::SafeAgentAction::RankManagerActions => "rank_manager_actions",
2384        manager_daily_brief::SafeAgentAction::DraftInternalTaskForReview => "draft_internal_tasks",
2385        manager_daily_brief::SafeAgentAction::RecordManagerFeedback => "record_manager_feedback",
2386        manager_daily_brief::SafeAgentAction::EstimateLaborMinutesSaved => {
2387            "estimate_labor_minutes_saved"
2388        }
2389    }
2390}
2391
2392fn manager_daily_brief_blocked_action_codes() -> Vec<&'static str> {
2393    manager_daily_brief::Workflow::evaluate(
2394        manager_daily_brief::Request::builder()
2395            .location_id(entities::LocationId(Uuid::nil()))
2396            .operating_day(
2397                operations::operating_day::Date::try_new(
2398                    NaiveDate::from_ymd_opt(2026, 1, 1).expect("static date is valid"),
2399                )
2400                .expect("static operating day is valid"),
2401            )
2402            .prepared_for(manager_daily_brief::ManagerBriefPersona::GeneralManager)
2403            .demand_attention_threshold(
2404                manager_daily_brief::DemandThresholdUnits::try_new(1)
2405                    .expect("static demand threshold is valid"),
2406            )
2407            .build(),
2408    )
2409    .blocked_actions()
2410    .iter()
2411    .map(blocked_action_code)
2412    .collect()
2413}
2414
2415fn blocked_action_code(action: &manager_daily_brief::BlockedAction) -> &'static str {
2416    match action {
2417        manager_daily_brief::BlockedAction::ChangeStaffSchedule => "change_staff_schedule",
2418        manager_daily_brief::BlockedAction::MutateProviderOrPmsRecord => {
2419            "mutate_provider_or_pms_record"
2420        }
2421        manager_daily_brief::BlockedAction::SendCustomerMessage => "send_customer_message",
2422        manager_daily_brief::BlockedAction::MoveRefundDiscountOrPayment => {
2423            "move_refund_discount_or_payment"
2424        }
2425        manager_daily_brief::BlockedAction::HideSourceDataQualityIssue => {
2426            "hide_source_data_quality_issue"
2427        }
2428    }
2429}
2430
2431fn brief_action_kind_code(kind: manager_daily_brief::BriefActionKind) -> &'static str {
2432    match kind {
2433        manager_daily_brief::BriefActionKind::ReviewDemandAgainstStaffingPlan => {
2434            "review_demand_against_staffing_plan"
2435        }
2436        manager_daily_brief::BriefActionKind::ResolveCheckoutException => {
2437            "resolve_checkout_exception"
2438        }
2439        manager_daily_brief::BriefActionKind::ApproveRetentionFollowUpDraft => {
2440            "approve_retention_follow_up_draft"
2441        }
2442        manager_daily_brief::BriefActionKind::InvestigateSourceDataQualityIssue => {
2443            "investigate_source_data_quality_issue"
2444        }
2445    }
2446}
2447
2448fn brief_action_priority_code(priority: manager_daily_brief::BriefActionPriority) -> &'static str {
2449    match priority {
2450        manager_daily_brief::BriefActionPriority::High => "high",
2451        manager_daily_brief::BriefActionPriority::Medium => "medium",
2452        manager_daily_brief::BriefActionPriority::Low => "low",
2453    }
2454}
2455
2456fn manager_brief_persona_code(persona: manager_daily_brief::ManagerBriefPersona) -> &'static str {
2457    match persona {
2458        manager_daily_brief::ManagerBriefPersona::GeneralManager => "general_manager",
2459        manager_daily_brief::ManagerBriefPersona::AssistantGeneralManager => {
2460            "assistant_general_manager"
2461        }
2462        manager_daily_brief::ManagerBriefPersona::FrontDeskLead => "front_desk_lead",
2463        manager_daily_brief::ManagerBriefPersona::FrontDeskAgent => "front_desk_agent",
2464    }
2465}
2466
2467fn removed_manual_work_code(work: manager_daily_brief::RemovedManualWork) -> &'static str {
2468    match work {
2469        manager_daily_brief::RemovedManualWork::MorningDashboardReconciliation => {
2470            "morning_dashboard_reconciliation"
2471        }
2472        manager_daily_brief::RemovedManualWork::DemandVersusStaffingScan => {
2473            "demand_versus_staffing_scan"
2474        }
2475        manager_daily_brief::RemovedManualWork::CheckoutExceptionAudit => {
2476            "checkout_exception_audit"
2477        }
2478        manager_daily_brief::RemovedManualWork::RetentionFollowUpQueuePrioritization => {
2479            "retention_follow_up_queue_prioritization"
2480        }
2481        manager_daily_brief::RemovedManualWork::DataQualityExceptionTriage => {
2482            "data_quality_exception_triage"
2483        }
2484    }
2485}
2486
2487fn source_fact_kind_code(kind: manager_daily_brief::SourceFactKind) -> &'static str {
2488    match kind {
2489        manager_daily_brief::SourceFactKind::ServiceDemandForecast => "service_demand_forecast",
2490        manager_daily_brief::SourceFactKind::CheckoutCompletionStatus => {
2491            "checkout_completion_status"
2492        }
2493        manager_daily_brief::SourceFactKind::RetentionFollowUpEligibility => {
2494            "retention_follow_up_eligibility"
2495        }
2496        manager_daily_brief::SourceFactKind::SourceDataQualityIssue => "source_data_quality_issue",
2497    }
2498}
2499
2500fn data_quality_kind_code(kind: &data_quality::Kind) -> &'static str {
2501    match kind {
2502        data_quality::Kind::MissingRequiredField { .. } => "missing_required_field",
2503        data_quality::Kind::AssumptionInForce { .. } => "assumption_in_force",
2504        data_quality::Kind::UnknownSourceStatus { .. } => "unknown_source_status",
2505        data_quality::Kind::ConflictingTimestamps => "conflicting_timestamps",
2506        data_quality::Kind::DuplicateSourceRecord => "duplicate_source_record",
2507        data_quality::Kind::AmbiguousOwnerPetRelationship => "ambiguous_owner_pet_relationship",
2508        data_quality::Kind::UnmappedServiceType => "unmapped_service_type",
2509        data_quality::Kind::LocationScopeAmbiguity => "location_scope_ambiguity",
2510        data_quality::Kind::PaymentStateConflict => "payment_state_conflict",
2511        data_quality::Kind::CheckoutEvidenceMissing => "checkout_evidence_missing",
2512        data_quality::Kind::UnclosedReservation => "unclosed_reservation",
2513        data_quality::Kind::IncompletePetProfile => "incomplete_pet_profile",
2514        data_quality::Kind::MissingVaccinationRecord => "missing_vaccination_record",
2515        data_quality::Kind::SensitivePayloadQuarantined => "sensitive_payload_quarantined",
2516    }
2517}
2518
2519fn data_quality_severity_code(severity: data_quality::Severity) -> &'static str {
2520    match severity {
2521        data_quality::Severity::Informational => "informational",
2522        data_quality::Severity::Warning => "warning",
2523        data_quality::Severity::Blocking => "blocking",
2524        data_quality::Severity::Critical => "critical",
2525    }
2526}