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 taxonomy for error in the local smoke-test workflow; each value carries operational meaning for source-grounded routing and review.
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 carried 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 taxonomy for stage in the local smoke-test workflow; each value carries operational meaning for source-grounded routing and review.
70pub enum Stage {
71    /// Represents inquiry in the local smoke decision model so the app can choose the correct evidence, review, or draft path without taking live action.
72    Inquiry,
73    /// Represents profile in the local smoke decision model so the app can choose the correct evidence, review, or draft path without taking live action.
74    Profile,
75    /// Represents vaccine docs in the local smoke decision model so the app can choose the correct evidence, review, or draft path without taking live action.
76    VaccineDocs,
77    /// Represents booking triage in the local smoke decision model so the app can choose the correct evidence, review, or draft path without taking live action.
78    BookingTriage,
79    /// Represents confirmation draft in the local smoke decision model so the app can choose the correct evidence, review, or draft path without taking live action.
80    ConfirmationDraft,
81    /// Represents check in today view in the local smoke decision model so the app can choose the correct evidence, review, or draft path without taking live action.
82    CheckInTodayView,
83    /// Represents staff note daily update draft in the local smoke decision model so the app can choose the correct evidence, review, or draft path without taking live action.
84    StaffNoteDailyUpdateDraft,
85    /// Represents checkout completion in the local smoke decision model so the app can choose the correct evidence, review, or draft path without taking live action.
86    CheckoutCompletion,
87    /// Represents follow up retention in the local smoke decision model so the app can choose the correct evidence, review, 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 boundaries carried 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 source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
127    pub const fn draft_only_ai(&self) -> bool {
128        self.draft_only_ai
129    }
130
131    /// Returns the blocks live customer sends source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
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 source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
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 source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
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 carried 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 carried 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 carried 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 carried 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 source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
198    pub fn reservation_labels(&self) -> &[ReservationLabel] {
199        &self.reservation_labels
200    }
201
202    /// Returns the status source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
203    pub const fn status(&self) -> &entities::reservation::Status {
204        &self.status
205    }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
209/// Checkout completion carried 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 source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
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 source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
223    pub const fn completion_status(&self) -> checkout_completion::CompletionStatus {
224        self.packet.completion_status()
225    }
226
227    /// Returns the required review gates source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
228    pub fn required_review_gates(&self) -> &[policy::ReviewGate] {
229        self.packet.required_review_gates()
230    }
231
232    /// Returns the blocked actions source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
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 taxonomy for retention next action in the local smoke-test workflow; each value carries operational meaning for source-grounded routing and review.
240pub enum RetentionNextAction {
241    /// Represents draft rebooking reminder for review in the local smoke decision model so the app can choose the correct evidence, review, or draft path without taking live action.
242    DraftRebookingReminderForReview,
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
246/// Retention follow up carried 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 source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
254    pub const fn next_action(&self) -> RetentionNextAction {
255        self.next_action
256    }
257
258    /// Returns the review gate source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
259    pub fn review_gate(&self) -> policy::ReviewGate {
260        self.review_gate.clone()
261    }
262}
263
264#[derive(Debug, Clone)]
265/// Full chain evidence carried 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 source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
287    pub const fn source_event_key(&self) -> &SourceEventKey {
288        &self.source_event_key
289    }
290
291    /// Returns the stage names source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
292    pub fn stage_names(&self) -> Vec<&'static str> {
293        self.stages.iter().map(|stage| stage.name()).collect()
294    }
295
296    /// Returns the boundaries source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
297    pub const fn boundaries(&self) -> &SmokeBoundaries {
298        &self.boundaries
299    }
300
301    /// Returns the booking packet source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
302    pub const fn booking_packet(&self) -> &booking_triage::StaffEvaluationPacket {
303        &self.booking_packet
304    }
305
306    /// Returns the confirmation draft source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
307    pub const fn confirmation_draft(&self) -> &SmokeConfirmationDraft {
308        &self.confirmation_draft
309    }
310
311    /// Returns the today view source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
312    pub const fn today_view(&self) -> &TodayView {
313        &self.today_view
314    }
315
316    /// Returns the daily update preview source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
317    pub const fn daily_update_preview(&self) -> &daily_update::MvpPreview {
318        &self.daily_update_preview
319    }
320
321    /// Returns the checkout completion source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
322    pub const fn checkout_completion(&self) -> &CheckoutCompletion {
323        &self.checkout_completion
324    }
325
326    /// Returns the retention follow up source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
327    pub const fn retention_follow_up(&self) -> &RetentionFollowUp {
328        &self.retention_follow_up
329    }
330
331    /// Returns the review gated evidence refs source evidence carried by this local smoke-test workflow artifact without changing provider, customer, payment, or schedule state.
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}