Skip to main content

app/
local_smoke.rs

1use chrono::{DateTime, NaiveDate, Utc};
2use domain::{document, entities, pet, policy, source, vaccine, workflow};
3use serde::Deserialize;
4use uuid::Uuid;
5
6use crate::{booking_triage, checkout_completion, daily_update};
7
8#[derive(Debug, thiserror::Error)]
9/// Decision choices for error in the local smoke-test workflow; each value routes reviewed source facts to the right queue, draft, or staff gate.
10pub enum Error {
11    #[error("local smoke fixture is not valid JSON: {0}")]
12    /// Identifies invalid fixture as the reason the workflow must stop, retry, or request review.
13    InvalidFixture(#[from] serde_json::Error),
14    #[error("local smoke fixture is missing a required semantic value: {0}")]
15    /// Identifies invalid domain value as the reason the workflow must stop, retry, or request review.
16    InvalidDomainValue(String),
17    #[error("local smoke daily-update preview failed: {0}")]
18    /// Identifies daily update as the reason the workflow must stop, retry, or request review.
19    DailyUpdate(#[from] daily_update::Error),
20}
21
22/// Result type returned by fallible local smoke operations.
23pub type Result<T> = core::result::Result<T, Error>;
24
25#[derive(Debug, Deserialize)]
26struct InquiryFixture {
27    source_event_key: String,
28    customer: CustomerFixture,
29    pet: PetFixture,
30    requested_service: String,
31    message: String,
32}
33
34#[derive(Debug, Deserialize)]
35struct CustomerFixture {
36    name: String,
37    email: String,
38}
39
40#[derive(Debug, Deserialize)]
41struct PetFixture {
42    name: String,
43    species: String,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq)]
47/// Source event key used by the local smoke-test workflow; it exercises the local shell with deterministic fixtures and no external side effects.
48pub struct SourceEventKey(String);
49
50impl SourceEventKey {
51    fn parse(value: impl Into<String>) -> Result<Self> {
52        let value = value.into().trim().to_owned();
53        if value.is_empty() {
54            return Err(Error::InvalidDomainValue(
55                "source_event_key must be present for smoke provenance".to_owned(),
56            ));
57        }
58        Ok(Self(value))
59    }
60}
61
62impl AsRef<str> for SourceEventKey {
63    fn as_ref(&self) -> &str {
64        &self.0
65    }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69/// Decision choices for stage in the local smoke-test workflow; each value routes reviewed source facts to the right queue, draft, or staff gate.
70pub enum Stage {
71    /// Selects inquiry for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
72    Inquiry,
73    /// Selects profile for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
74    Profile,
75    /// Selects vaccine docs for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
76    VaccineDocs,
77    /// Selects booking triage for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
78    BookingTriage,
79    /// Selects confirmation draft for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
80    ConfirmationDraft,
81    /// Selects check in today view for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
82    CheckInTodayView,
83    /// Selects staff note daily update draft for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
84    StaffNoteDailyUpdateDraft,
85    /// Selects checkout completion for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
86    CheckoutCompletion,
87    /// Selects follow up retention for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
88    FollowUpRetention,
89}
90
91impl Stage {
92    const fn name(self) -> &'static str {
93        match self {
94            Self::Inquiry => "inquiry",
95            Self::Profile => "profile",
96            Self::VaccineDocs => "vaccine_docs",
97            Self::BookingTriage => "booking_triage",
98            Self::ConfirmationDraft => "confirmation_draft",
99            Self::CheckInTodayView => "check_in_today_view",
100            Self::StaffNoteDailyUpdateDraft => "staff_note_daily_update_draft",
101            Self::CheckoutCompletion => "checkout_completion",
102            Self::FollowUpRetention => "follow_up_retention",
103        }
104    }
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108/// Smoke gates used by the local smoke-test workflow; it exercises the local shell with deterministic fixtures and no external side effects.
109pub struct SmokeBoundaries {
110    draft_only_ai: bool,
111    blocks_live_customer_sends: bool,
112    blocks_provider_or_pms_mutations: bool,
113    blocks_payment_refund_or_discount_actions: bool,
114}
115
116impl SmokeBoundaries {
117    const fn local_demo() -> Self {
118        Self {
119            draft_only_ai: true,
120            blocks_live_customer_sends: true,
121            blocks_provider_or_pms_mutations: true,
122            blocks_payment_refund_or_discount_actions: true,
123        }
124    }
125
126    /// Returns the draft only ai evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
127    pub const fn draft_only_ai(&self) -> bool {
128        self.draft_only_ai
129    }
130
131    /// Returns the blocks live customer sends evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
132    pub const fn blocks_live_customer_sends(&self) -> bool {
133        self.blocks_live_customer_sends
134    }
135
136    /// Returns the blocks provider or pms mutations evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
137    pub const fn blocks_provider_or_pms_mutations(&self) -> bool {
138        self.blocks_provider_or_pms_mutations
139    }
140
141    /// Returns the blocks payment refund or discount actions evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
142    pub const fn blocks_payment_refund_or_discount_actions(&self) -> bool {
143        self.blocks_payment_refund_or_discount_actions
144    }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
148/// Review evidence ref used by the local smoke-test workflow; it exercises the local shell with deterministic fixtures and no external side effects.
149pub struct ReviewEvidenceRef(String);
150
151impl ReviewEvidenceRef {
152    fn new(value: impl Into<String>) -> Self {
153        Self(value.into())
154    }
155}
156
157impl AsRef<str> for ReviewEvidenceRef {
158    fn as_ref(&self) -> &str {
159        &self.0
160    }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
164/// Smoke confirmation draft used by the local smoke-test workflow; it exercises the local shell with deterministic fixtures and no external side effects.
165pub struct SmokeConfirmationDraft {
166    draft: booking_triage::ConfirmationDraft,
167}
168
169impl SmokeConfirmationDraft {
170    /// Reports whether the local smoke-test workflow satisfies the requires customer message approval safety condition.
171    pub const fn requires_customer_message_approval(&self) -> bool {
172        matches!(
173            self.draft.approval_gate(),
174            booking_triage::ApprovalGate::CustomerMessageApproval
175        )
176    }
177}
178
179#[derive(Debug, Clone, PartialEq, Eq)]
180/// Reservation label used by the local smoke-test workflow; it exercises the local shell with deterministic fixtures and no external side effects.
181pub struct ReservationLabel(String);
182
183impl AsRef<str> for ReservationLabel {
184    fn as_ref(&self) -> &str {
185        &self.0
186    }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq)]
190/// Today view used by the local smoke-test workflow; it exercises the local shell with deterministic fixtures and no external side effects.
191pub struct TodayView {
192    reservation_labels: Vec<ReservationLabel>,
193    status: entities::reservation::Status,
194}
195
196impl TodayView {
197    /// Returns the reservation labels evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
198    pub fn reservation_labels(&self) -> &[ReservationLabel] {
199        &self.reservation_labels
200    }
201
202    /// Returns the status evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
203    pub const fn status(&self) -> &entities::reservation::Status {
204        &self.status
205    }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
209/// Checkout completion used by the local smoke-test workflow; it exercises the local shell with deterministic fixtures and no external side effects.
210pub struct CheckoutCompletion {
211    packet: checkout_completion::Packet,
212}
213
214impl CheckoutCompletion {
215    /// Returns the status evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
216    pub fn status(&self) -> entities::reservation::Status {
217        self.packet
218            .suggested_reservation_status()
219            .expect("local smoke checkout completion should suggest checked-out status")
220    }
221
222    /// Returns the completion status evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
223    pub const fn completion_status(&self) -> checkout_completion::CompletionStatus {
224        self.packet.completion_status()
225    }
226
227    /// Returns the required review gates evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
228    pub fn required_review_gates(&self) -> &[policy::ReviewGate] {
229        self.packet.required_review_gates()
230    }
231
232    /// Returns the blocked actions evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
233    pub fn blocked_actions(&self) -> &[checkout_completion::BlockedAction] {
234        self.packet.blocked_actions()
235    }
236}
237
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239/// Decision choices for retention next action in the local smoke-test workflow; each value routes reviewed source facts to the right queue, draft, or staff gate.
240pub enum RetentionNextAction {
241    /// Selects draft rebooking reminder for review for the local smoke decision model so the app can choose a review, evidence, or draft path without taking live action.
242    DraftRebookingReminderForReview,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
246/// Retention follow up used by the local smoke-test workflow; it exercises the local shell with deterministic fixtures and no external side effects.
247pub struct RetentionFollowUp {
248    next_action: RetentionNextAction,
249    review_gate: policy::ReviewGate,
250}
251
252impl RetentionFollowUp {
253    /// Returns the next action evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
254    pub const fn next_action(&self) -> RetentionNextAction {
255        self.next_action
256    }
257
258    /// Returns the review gate evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
259    pub fn review_gate(&self) -> policy::ReviewGate {
260        self.review_gate.clone()
261    }
262}
263
264#[derive(Debug, Clone)]
265/// Full chain evidence used by the local smoke-test workflow; it exercises the local shell with deterministic fixtures and no external side effects.
266pub struct FullChainEvidence {
267    source_event_key: SourceEventKey,
268    stages: Vec<Stage>,
269    boundaries: SmokeBoundaries,
270    #[allow(dead_code)]
271    inquiry: InquiryRecord,
272    #[allow(dead_code)]
273    profile: ProfileEvidence,
274    #[allow(dead_code)]
275    vaccine_docs: VaccineDocumentEvidence,
276    booking_packet: booking_triage::StaffEvaluationPacket,
277    confirmation_draft: SmokeConfirmationDraft,
278    today_view: TodayView,
279    daily_update_preview: daily_update::MvpPreview,
280    checkout_completion: CheckoutCompletion,
281    retention_follow_up: RetentionFollowUp,
282    review_gated_evidence_refs: Vec<ReviewEvidenceRef>,
283}
284
285impl FullChainEvidence {
286    /// Returns the source event key evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
287    pub const fn source_event_key(&self) -> &SourceEventKey {
288        &self.source_event_key
289    }
290
291    /// Returns the stage names evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
292    pub fn stage_names(&self) -> Vec<&'static str> {
293        self.stages.iter().map(|stage| stage.name()).collect()
294    }
295
296    /// Returns the gates evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
297    pub const fn boundaries(&self) -> &SmokeBoundaries {
298        &self.boundaries
299    }
300
301    /// Returns the booking packet evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
302    pub const fn booking_packet(&self) -> &booking_triage::StaffEvaluationPacket {
303        &self.booking_packet
304    }
305
306    /// Returns the confirmation draft evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
307    pub const fn confirmation_draft(&self) -> &SmokeConfirmationDraft {
308        &self.confirmation_draft
309    }
310
311    /// Returns the today view evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
312    pub const fn today_view(&self) -> &TodayView {
313        &self.today_view
314    }
315
316    /// Returns the daily update preview evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
317    pub const fn daily_update_preview(&self) -> &daily_update::MvpPreview {
318        &self.daily_update_preview
319    }
320
321    /// Returns the checkout completion evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
322    pub const fn checkout_completion(&self) -> &CheckoutCompletion {
323        &self.checkout_completion
324    }
325
326    /// Returns the retention follow up evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
327    pub const fn retention_follow_up(&self) -> &RetentionFollowUp {
328        &self.retention_follow_up
329    }
330
331    /// Returns the review gated evidence refs evidence available to local smoke-test review while leaving provider, customer, payment, and schedule systems unchanged.
332    pub fn review_gated_evidence_refs(&self) -> &[ReviewEvidenceRef] {
333        &self.review_gated_evidence_refs
334    }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq)]
338struct InquiryRecord {
339    source_event_key: SourceEventKey,
340    requested_service: String,
341    message: String,
342}
343
344#[derive(Debug, Clone, PartialEq, Eq)]
345struct ProfileEvidence {
346    customer: entities::Customer,
347    pet: entities::Pet,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq)]
351struct VaccineDocumentEvidence {
352    document: entities::Document,
353    record: entities::VaccineRecord,
354}
355
356/// Runs the fixture scenario for the local smoke workflow.
357pub fn run_fixture(fixture_json: &str) -> Result<FullChainEvidence> {
358    let fixture: InquiryFixture = serde_json::from_str(fixture_json)?;
359    let source_event_key = SourceEventKey::parse(fixture.source_event_key)?;
360    let ids = SmokeIds::new();
361    let reservation_label = ReservationLabel(format!("REQ-{}", source_event_key.as_ref()));
362    let occurred_at = DateTime::<Utc>::UNIX_EPOCH;
363
364    let inquiry = InquiryRecord {
365        source_event_key: source_event_key.clone(),
366        requested_service: fixture.requested_service,
367        message: fixture.message,
368    };
369    let profile = build_profile(&fixture.customer, &fixture.pet, ids.customer_id, ids.pet_id)?;
370    let vaccine_docs = build_vaccine_document(ids, occurred_at)?;
371    let booking_packet = build_booking_packet(&reservation_label);
372    let confirmation_draft = SmokeConfirmationDraft {
373        draft: booking_packet.confirmation_draft().clone(),
374    };
375    let today_view = TodayView {
376        reservation_labels: vec![reservation_label],
377        status: entities::reservation::Status::CheckedIn,
378    };
379    let daily_update_preview = build_daily_update_preview(ids, &profile.pet.name, occurred_at)?;
380    let checkout_completion = build_checkout_completion(ids, occurred_at)?;
381    let retention_follow_up = RetentionFollowUp {
382        next_action: RetentionNextAction::DraftRebookingReminderForReview,
383        review_gate: policy::ReviewGate::CustomerMessageApproval,
384    };
385
386    let review_gated_evidence_refs = vec![
387        ReviewEvidenceRef::new("vaccine_docs:medical_document_review_required"),
388        ReviewEvidenceRef::new("confirmation:customer_message_approval_required"),
389        ReviewEvidenceRef::new("daily_update:send_stub_blocked_until_human_approval"),
390        ReviewEvidenceRef::new("checkout_completion:customer_message_approval_required"),
391    ];
392
393    assert!(vaccine_docs.document.requires_human_review_before_use());
394    assert!(
395        vaccine_docs
396            .record
397            .requires_human_review_before_compliance()
398    );
399
400    Ok(FullChainEvidence {
401        source_event_key,
402        stages: vec![
403            Stage::Inquiry,
404            Stage::Profile,
405            Stage::VaccineDocs,
406            Stage::BookingTriage,
407            Stage::ConfirmationDraft,
408            Stage::CheckInTodayView,
409            Stage::StaffNoteDailyUpdateDraft,
410            Stage::CheckoutCompletion,
411            Stage::FollowUpRetention,
412        ],
413        boundaries: SmokeBoundaries::local_demo(),
414        inquiry,
415        profile,
416        vaccine_docs,
417        booking_packet,
418        confirmation_draft,
419        today_view,
420        daily_update_preview,
421        checkout_completion,
422        retention_follow_up,
423        review_gated_evidence_refs,
424    })
425}
426
427#[derive(Debug, Clone, Copy)]
428struct SmokeIds {
429    location_id: entities::LocationId,
430    customer_id: entities::CustomerId,
431    pet_id: entities::PetId,
432    reservation_id: entities::reservation::Id,
433    document_id: entities::DocumentId,
434    vaccine_record_id: entities::VaccineRecordId,
435}
436
437impl SmokeIds {
438    fn new() -> Self {
439        Self {
440            location_id: entities::LocationId(Uuid::from_u128(
441                0x0051_0CA1_0000_0000_0000_0000_0000_0001,
442            )),
443            customer_id: entities::CustomerId(Uuid::from_u128(
444                0x0051_0CA1_0000_0000_0000_0000_0000_0002,
445            )),
446            pet_id: entities::PetId(Uuid::from_u128(0x0051_0CA1_0000_0000_0000_0000_0000_0003)),
447            reservation_id: entities::reservation::Id(Uuid::from_u128(
448                0x0051_0CA1_0000_0000_0000_0000_0000_0004,
449            )),
450            document_id: entities::DocumentId(Uuid::from_u128(
451                0x0051_0CA1_0000_0000_0000_0000_0000_0005,
452            )),
453            vaccine_record_id: entities::VaccineRecordId(Uuid::from_u128(
454                0x0051_0CA1_0000_0000_0000_0000_0000_0006,
455            )),
456        }
457    }
458}
459
460fn build_profile(
461    customer: &CustomerFixture,
462    pet: &PetFixture,
463    customer_id: entities::CustomerId,
464    pet_id: entities::PetId,
465) -> Result<ProfileEvidence> {
466    let species = match pet.species.trim().to_ascii_lowercase().as_str() {
467        "dog" => entities::Species::Dog,
468        "cat" => entities::Species::Cat,
469        other => entities::Species::Other(other.to_owned()),
470    };
471
472    Ok(ProfileEvidence {
473        customer: entities::Customer {
474            id: customer_id,
475            full_name: domain::customer::Name::try_new(customer.name.clone()).map_err(invalid)?,
476            email: Some(domain::customer::Email::try_new(customer.email.clone()).map_err(invalid)?),
477            mobile_phone: None,
478            preferred_contact: entities::ContactChannel::Email,
479            portal_account: None,
480        },
481        pet: entities::Pet {
482            id: pet_id,
483            customer_id,
484            name: pet::Name::try_new(pet.name.clone()).map_err(invalid)?,
485            species,
486            birth_date: None,
487            sex: None,
488            spay_neuter_status: entities::SpayNeuterStatus::Unknown,
489            temperament: entities::TemperamentProfile::default(),
490            care_profile: entities::CareProfile::default(),
491        },
492    })
493}
494
495fn build_vaccine_document(
496    ids: SmokeIds,
497    occurred_at: DateTime<Utc>,
498) -> Result<VaccineDocumentEvidence> {
499    let uploaded_by_actor = entities::ActorRef::Customer(ids.customer_id);
500    Ok(VaccineDocumentEvidence {
501        document: entities::Document::builder()
502            .id(ids.document_id)
503            .location_id(ids.location_id)
504            .subject(entities::DocumentSubject::Pet(ids.pet_id))
505            .classification(document::Classification::VaccineProof)
506            .source(document::Source::CustomerUpload)
507            .uploaded_by_actor(uploaded_by_actor)
508            .uploaded_at(occurred_at)
509            .original_file(
510                document::OriginalFile::builder()
511                    .filename(document::FileName::try_new("miso-rabies.pdf").map_err(invalid)?)
512                    .mime_type(document::MimeType::try_new("application/pdf").map_err(invalid)?)
513                    .content_length(document::ContentLengthBytes::try_new(42).map_err(invalid)?)
514                    .sha256(
515                        document::Sha256Digest::try_new(
516                            "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
517                        )
518                        .map_err(invalid)?,
519                    )
520                    .build(),
521            )
522            .storage_ref(
523                document::StorageRef::builder()
524                    .bucket(
525                        document::StorageBucket::try_new("local-smoke-documents")
526                            .map_err(invalid)?,
527                    )
528                    .key(
529                        document::StorageKey::try_new("fixtures/miso-rabies.pdf")
530                            .map_err(invalid)?,
531                    )
532                    .version(document::StorageVersion::try_new("v1").map_err(invalid)?)
533                    .build(),
534            )
535            .virus_scan_status(document::VirusScanStatus::Passed)
536            .pii_redaction_status(document::PiiRedactionStatus::NotRequired)
537            .verification_status(document::Status::AwaitingReview)
538            .build(),
539        record: entities::VaccineRecord::builder()
540            .id(ids.vaccine_record_id)
541            .pet_id(ids.pet_id)
542            .vaccine_name(policy::VaccineName::try_new("Rabies").map_err(invalid)?)
543            .source_document_id(ids.document_id)
544            .status(vaccine::Status::PendingReview)
545            .effective_on(NaiveDate::from_ymd_opt(2026, 1, 1).expect("static date is valid"))
546            .expires_on(NaiveDate::from_ymd_opt(2027, 1, 1).expect("static date is valid"))
547            .review_gate(policy::ReviewGate::MedicalDocumentReview)
548            .build(),
549    })
550}
551
552fn build_booking_packet(
553    reservation_label: &ReservationLabel,
554) -> booking_triage::StaffEvaluationPacket {
555    let evaluation = booking_triage::DeterministicResult::evaluate(vec![
556        booking_triage::rule::Evaluation::pass(
557            booking_triage::rule::Id::DateRangeAndServiceSupported,
558            vec![booking_triage::EvidenceRef::try_new("fixture:boarding-requested").unwrap()],
559        ),
560        booking_triage::rule::Evaluation::pass(
561            booking_triage::rule::Id::AccommodationAvailability,
562            vec![booking_triage::EvidenceRef::try_new("local-demo:capacity-open").unwrap()],
563        ),
564        booking_triage::rule::Evaluation::pass(
565            booking_triage::rule::Id::VaccineRequirements,
566            vec![
567                booking_triage::EvidenceRef::try_new("vaccine:rabies-pending-human-reviewed-path")
568                    .unwrap(),
569            ],
570        ),
571        booking_triage::rule::Evaluation::pass(
572            booking_triage::rule::Id::DepositAndPricingRequirements,
573            vec![booking_triage::EvidenceRef::try_new("payment:not-required-local-smoke").unwrap()],
574        ),
575    ]);
576
577    booking_triage::StaffEvaluationPacket::new(
578        booking_triage::Reservation::try_new(reservation_label.as_ref()).unwrap(),
579        evaluation,
580    )
581    .with_ai_recommendation(booking_triage::AiRecommendation::recommend_staff_confirmation(
582        booking_triage::RecommendationText::try_new(
583            "Draft-only local smoke: deterministic gates permit staff to review an offer without mutating PMS records.",
584        )
585        .unwrap(),
586    ))
587    .with_confirmation_draft(booking_triage::ConfirmationDraft::new(
588        booking_triage::CustomerMessageDraft::try_new(
589            "Draft only: staff can review this local demo booking confirmation before any customer send.",
590        )
591        .unwrap(),
592    ))
593}
594
595fn build_daily_update_preview(
596    ids: SmokeIds,
597    pet_name: &pet::Name,
598    occurred_at: DateTime<Utc>,
599) -> Result<daily_update::MvpPreview> {
600    let event = workflow::Event {
601        event_id: workflow::EventId(Uuid::from_u128(0x0051_0CA1_0000_0000_0000_0000_0000_0010)),
602        event_type: workflow::EventType::DailyNoteCreated,
603        occurred_at,
604        actor: entities::ActorRef::Staff {
605            staff_id: entities::StaffId::try_new("local-smoke-kennel").map_err(invalid)?,
606        },
607        location_id: ids.location_id,
608        subject: workflow::Subject::Reservation(ids.reservation_id),
609        policy_context: workflow::PolicyContext {
610            allowed_actions: vec![
611                workflow::AllowedAction::SummarizeCareNotes,
612                workflow::AllowedAction::DraftCustomerMessage,
613            ],
614            automation_level: policy::automation::Level::DraftOnly,
615            required_reviews: vec![policy::ReviewGate::CustomerMessageApproval],
616        },
617    };
618    let note = entities::CareNote::builder()
619        .id(entities::care_note::Id(Uuid::from_u128(
620            0x0051_0CA1_0000_0000_0000_0000_0000_0011,
621        )))
622        .subject(entities::care_note::Subject::Reservation(
623            ids.reservation_id,
624        ))
625        .kind(entities::care_note::Kind::General)
626        .visibility(entities::care_note::Visibility::CustomerVisibleAfterReview)
627        .body(
628            entities::care_note::Body::try_new(
629                "settled into suite, ate dinner, and enjoyed supervised play.",
630            )
631            .map_err(invalid)?,
632        )
633        .author(entities::ActorRef::Staff {
634            staff_id: entities::StaffId::try_new("local-smoke-kennel").map_err(invalid)?,
635        })
636        .recorded_at(occurred_at)
637        .build();
638
639    Ok(daily_update::build_mvp_preview(
640        daily_update::MvpPreviewRequest::builder()
641            .event(event)
642            .pet_name(pet_name.clone())
643            .owner_display_name(domain::customer::Name::try_new("Casey Local").map_err(invalid)?)
644            .policy_snapshot_id(
645                policy::Id::try_new("local-smoke-draft-only-policy").map_err(invalid)?,
646            )
647            .notes(vec![note])
648            .build(),
649    )?)
650}
651
652fn build_checkout_completion(
653    ids: SmokeIds,
654    occurred_at: DateTime<Utc>,
655) -> Result<CheckoutCompletion> {
656    let source_provenance = source::Provenance::builder()
657        .system(source::System::Gingr)
658        .endpoint(source::Endpoint::try_new("GET /reservations/{id}").map_err(invalid)?)
659        .record_id(source::record::Id::try_new("local-smoke-reservation-001").map_err(invalid)?)
660        .extraction_batch(
661            source::ExtractionBatchId::try_new("local-smoke-checkout").map_err(invalid)?,
662        )
663        .pulled_at(source::Timestamp::try_new("2026-06-17T00:00:00Z").map_err(invalid)?)
664        .request_scope(
665            source::RequestScope::try_new("local-smoke-readonly-checkout").map_err(invalid)?,
666        )
667        .schema_version(source::SchemaVersion::try_new("gingr-v0-readonly").map_err(invalid)?)
668        .payload_hash(source::PayloadHash::try_new("sha256:local-smoke-checkout").map_err(invalid)?)
669        .raw_payload_ref(
670            source::RawPayloadRef::try_new("fixtures/gingr/reservation-check-out.json")
671                .map_err(invalid)?,
672        )
673        .build();
674    let staff_handoff = checkout_completion::StaffHandoff::builder()
675        .completed_by(entities::ActorRef::Staff {
676            staff_id: entities::StaffId::try_new("local-smoke-front-desk").map_err(invalid)?,
677        })
678        .completed_at(occurred_at)
679        .belongings_status(checkout_completion::BelongingsStatus::ReturnedToCustomer)
680        .care_summary(
681            checkout_completion::CareSummary::try_new(
682                "Local smoke checkout handoff: belongings returned and care summary ready for review-gated follow-up.",
683            )
684            .map_err(invalid)?,
685        )
686        .departure_notes_review(checkout_completion::DepartureNotesReview::StaffReviewed)
687        .build();
688
689    Ok(CheckoutCompletion {
690        packet: checkout_completion::Workflow::evaluate(
691            checkout_completion::Request::builder()
692                .reservation_id(ids.reservation_id)
693                .source_provenance(source_provenance)
694                .observed_source_status(source::reservation::Status::CheckedOut)
695                .staff_handoff(staff_handoff)
696                .build(),
697        ),
698    })
699}
700
701fn invalid(error: impl ToString) -> Error {
702    Error::InvalidDomainValue(error.to_string())
703}