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)]
21pub 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
278pub fn router() -> Router {
285 router_with_state(
286 VACCINE_DOCUMENT_STATE
287 .get_or_init(VaccineDocumentState::default)
288 .clone(),
289 )
290}
291
292pub 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}