Skip to main content

domain/training/
mod.rs

1//! Training service-line contracts for enrollment readiness, trainer capacity, curriculum progress, package sessions, and parent-facing follow-up.
2//!
3//! Training programs are high-value operational upsells with stronger evidence requirements than simple appointment notes. This module keeps trainer availability, package balances, progress reports, outcome claims, and customer/parent-facing summaries as typed contracts so automation can draft assignments and follow-ups while trainer, manager, payment, and customer-message gates prevent unsupported live claims.
4
5use bon::Builder;
6use nutype::nutype;
7use serde::{Deserialize, Deserializer, Serialize};
8
9use crate::entities::{CustomerId, LocationId, PetId, StaffId};
10use crate::policy;
11
12macro_rules! positive_scalar {
13    ($name:ident, $primitive:ty, $error:ident, $message:literal) => {
14        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
15        /// Positive training quantity used for package/session counts where zero would invalidate labor and revenue tracking.
16        pub struct $name($primitive);
17
18        impl $name {
19            /// Promotes boundary input into a validated training domain value.
20            pub const fn try_new(value: $primitive) -> std::result::Result<Self, $error> {
21                if value == 0 {
22                    return Err($error::Zero);
23                }
24                Ok(Self(value))
25            }
26
27            /// Exposes the validated scalar for serialization and adapter boundaries.
28            pub const fn get(self) -> $primitive {
29                self.0
30            }
31        }
32
33        impl<'de> Deserialize<'de> for $name {
34            fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
35            where
36                D: Deserializer<'de>,
37            {
38                Self::try_new(<$primitive>::deserialize(deserializer)?)
39                    .map_err(serde::de::Error::custom)
40            }
41        }
42
43        #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
44        /// Training-domain validation failures that prevent unsupported reports, outcomes, or package usage from entering workflow state.
45        pub enum $error {
46            #[error($message)]
47            /// Rejects zero where the pet-resort workflow requires a positive quantity.
48            Zero,
49        }
50    };
51}
52
53positive_scalar!(
54    SessionCount,
55    u16,
56    SessionCountError,
57    "training package requires at least one session"
58);
59
60/// Training-program duration boundary for single-session and multi-week offerings.
61pub mod program {
62    use super::*;
63
64    #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
65    /// Positive number of weeks in a Stay-and-Study or other multi-week training program.
66    pub struct DurationWeeks(u8);
67
68    impl DurationWeeks {
69        /// Promotes boundary input into a validated training domain value.
70        pub const fn try_new(value: u8) -> std::result::Result<Self, DurationWeeksError> {
71            if value == 0 {
72                return Err(DurationWeeksError::ZeroWeeks);
73            }
74            Ok(Self(value))
75        }
76
77        /// Exposes the validated scalar for serialization and adapter boundaries.
78        pub const fn get(self) -> u8 {
79            self.0
80        }
81    }
82
83    impl<'de> Deserialize<'de> for DurationWeeks {
84        fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
85        where
86            D: Deserializer<'de>,
87        {
88            Self::try_new(u8::deserialize(deserializer)?).map_err(serde::de::Error::custom)
89        }
90    }
91
92    #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
93    /// Decision vocabulary for duration weeks error in training workflows.
94    pub enum DurationWeeksError {
95        #[error("training program duration requires at least one week")]
96        /// Zero weeks training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
97        ZeroWeeks,
98    }
99
100    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
101    /// Program duration shape used to plan trainer labor and customer expectations.
102    pub enum Duration {
103        /// Single session training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
104        SingleSession,
105        /// Weeks training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
106        Weeks(DurationWeeks),
107    }
108}
109
110/// Enrollment readiness boundary for deciding whether a training assignment can be drafted.
111pub mod enrollment {
112    use super::*;
113
114    #[nutype(
115        sanitize(trim),
116        validate(not_empty, len_char_max = 120),
117        derive(
118            Debug,
119            Clone,
120            PartialEq,
121            Eq,
122            PartialOrd,
123            Ord,
124            Hash,
125            Serialize,
126            Deserialize
127        )
128    )]
129    pub struct Id(String);
130
131    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132    /// Enrollment readiness state and the review gate that blocks assignment when data, behavior, care, or payment facts are incomplete.
133    pub enum Readiness {
134        /// Enrollment has enough source facts to draft trainer assignment.
135        Ready,
136        /// Source-derived gate carried by this training contract.
137        TrainerReviewRequired {
138            /// Gate value carried by this review or workflow variant.
139            gate: policy::ReviewGate,
140        },
141        /// Source-derived gate carried by this training contract.
142        BehaviorOrCareReviewRequired {
143            /// Gate value carried by this review or workflow variant.
144            gate: policy::ReviewGate,
145        },
146        /// Source-derived gate carried by this training contract.
147        PackageOrPaymentReviewRequired {
148            /// Gate value carried by this review or workflow variant.
149            gate: policy::ReviewGate,
150        },
151    }
152
153    impl Readiness {
154        /// Returns the blocking review gate recorded on this training contract.
155        pub fn blocking_gate(&self) -> Option<policy::ReviewGate> {
156            match self {
157                Self::Ready => None,
158                Self::TrainerReviewRequired { gate }
159                | Self::BehaviorOrCareReviewRequired { gate }
160                | Self::PackageOrPaymentReviewRequired { gate } => Some(gate.clone()),
161            }
162        }
163    }
164}
165
166/// Curriculum boundary for program units, milestones, and evidence-backed progress tracking.
167pub mod curriculum {
168    use super::*;
169
170    /// Milestone boundary for normalized trainer-observed progress states.
171    pub mod milestone {
172        use super::*;
173
174        #[nutype(
175            sanitize(trim),
176            validate(not_empty, len_char_max = 120),
177            derive(
178                Debug,
179                Clone,
180                PartialEq,
181                Eq,
182                PartialOrd,
183                Ord,
184                Hash,
185                Serialize,
186                Deserialize
187            )
188        )]
189        pub struct Id(String);
190
191        #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192        /// Normalized training milestone status observed from trainer notes or source-data ingestion.
193        pub enum Status {
194            /// Not started training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
195            NotStarted,
196            /// Introduced training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
197            Introduced,
198            /// Practicing training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
199            Practicing,
200            /// Generalized training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
201            Generalized,
202            /// Completed training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
203            Completed,
204            /// Deferred needs trainer note training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
205            DeferredNeedsTrainerNote,
206        }
207    }
208
209    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
210    /// Curriculum unit that defines what trainers should work on and report against.
211    pub enum Unit {
212        /// Puppy manners training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
213        PuppyManners,
214        /// Loose leash walking training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
215        LooseLeashWalking,
216        /// Recall training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
217        Recall,
218        /// Confidence building training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
219        ConfidenceBuilding,
220        /// Canine good citizen prep training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
221        CanineGoodCitizenPrep,
222    }
223
224    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
225    /// Evidence-backed milestone progress entry included in internal and parent-facing reports.
226    pub struct Progress {
227        /// Source-derived milestone id carried by this training contract.
228        pub milestone_id: milestone::Id,
229        /// Source-derived status carried by this training contract.
230        pub status: milestone::Status,
231    }
232
233    impl Progress {
234        /// Assembles this training value from already-validated domain parts.
235        pub const fn new(milestone_id: milestone::Id, status: milestone::Status) -> Self {
236            Self {
237                milestone_id,
238                status,
239            }
240        }
241    }
242}
243
244/// Trainer assignment boundary for matching programs to certified, named, or program-qualified trainers.
245pub mod trainer {
246    use super::*;
247
248    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
249    /// Trainer availability posture used to draft assignments or waitlists without inventing capacity.
250    pub enum Availability {
251        /// Any certified trainer training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
252        AnyCertifiedTrainer,
253        /// Named trainer required training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
254        NamedTrainerRequired,
255        /// Waitlist until trainer available training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
256        WaitlistUntilTrainerAvailable,
257    }
258
259    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
260    /// Trainer requirement that constrains who may deliver a program or session.
261    pub enum Requirement {
262        /// Any certified trainer training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
263        AnyCertifiedTrainer,
264        /// Source-derived trainer id carried by this training contract.
265        NamedTrainer {
266            /// Trainer id value carried by this review or workflow variant.
267            trainer_id: StaffId,
268        },
269        /// Source-derived program carried by this training contract.
270        ProgramQualified {
271            /// Program value carried by this review or workflow variant.
272            program: Program,
273        },
274    }
275
276    impl Requirement {
277        /// Reports whether trainer assignment must use a named or waitlisted trainer.
278        pub const fn requires_named_trainer(&self) -> bool {
279            matches!(self, Self::NamedTrainer { .. })
280        }
281    }
282
283    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
284    /// Qualification evidence used to explain why a trainer may own a program.
285    pub enum Qualification {
286        /// Certified trainer training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
287        CertifiedTrainer,
288        /// Program specialist training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
289        ProgramSpecialist,
290        /// Manager approved exception training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
291        ManagerApprovedException,
292    }
293}
294
295#[nutype(
296    sanitize(trim),
297    validate(not_empty, len_char_max = 120),
298    derive(
299        Debug,
300        Clone,
301        PartialEq,
302        Eq,
303        PartialOrd,
304        Ord,
305        Hash,
306        Serialize,
307        Deserialize
308    )
309)]
310pub struct SessionId(String);
311
312#[nutype(
313    sanitize(trim),
314    validate(not_empty, len_char_max = 120),
315    derive(
316        Debug,
317        Clone,
318        PartialEq,
319        Eq,
320        PartialOrd,
321        Ord,
322        Hash,
323        Serialize,
324        Deserialize
325    )
326)]
327pub struct SessionRef(String);
328
329#[nutype(
330    sanitize(trim),
331    validate(not_empty, len_char_max = 120),
332    derive(
333        Debug,
334        Clone,
335        PartialEq,
336        Eq,
337        PartialOrd,
338        Ord,
339        Hash,
340        Serialize,
341        Deserialize
342    )
343)]
344pub struct ProgressReportId(String);
345
346#[nutype(
347    sanitize(trim),
348    validate(not_empty, len_char_max = 120),
349    derive(
350        Debug,
351        Clone,
352        PartialEq,
353        Eq,
354        PartialOrd,
355        Ord,
356        Hash,
357        Serialize,
358        Deserialize
359    )
360)]
361pub struct EvidenceId(String);
362
363#[nutype(
364    sanitize(trim),
365    validate(not_empty, len_char_max = 120),
366    derive(
367        Debug,
368        Clone,
369        PartialEq,
370        Eq,
371        PartialOrd,
372        Ord,
373        Hash,
374        Serialize,
375        Deserialize
376    )
377)]
378pub struct OutcomeDocumentationId(String);
379
380#[nutype(
381    sanitize(trim),
382    validate(not_empty, len_char_max = 500),
383    derive(
384        Debug,
385        Clone,
386        PartialEq,
387        Eq,
388        PartialOrd,
389        Ord,
390        Hash,
391        Serialize,
392        Deserialize
393    )
394)]
395pub struct ProgressNote(String);
396
397#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
398/// Training-domain validation failures that prevent unsupported reports, outcomes, or package usage from entering workflow state.
399pub enum Error {
400    #[error("training progress report requires evidence before it can be reviewed")]
401    /// Progress evidence required training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
402    ProgressEvidenceRequired,
403    #[error("training outcome claim requires evidence for achieved/readiness claims")]
404    /// Outcome evidence required training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
405    OutcomeEvidenceRequired,
406    #[error("training outcome documentation requires at least one claim")]
407    /// Outcome claim required training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
408    OutcomeClaimRequired,
409    #[error("training package policy does not define a reusable session balance")]
410    /// Package has no reusable balance training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
411    PackageHasNoReusableBalance,
412}
413
414/// Result type returned by fallible training operations.
415pub type Result<T> = std::result::Result<T, Error>;
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
418/// Training program sold or fulfilled by the resort, used for capacity, package, and outcome planning.
419pub enum Program {
420    /// Source-derived duration carried by this training contract.
421    StayAndStudy {
422        /// Duration value carried by this review or workflow variant.
423        duration: program::DurationWeeks,
424    },
425    /// Tutor session training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
426    TutorSession,
427    /// Group class training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
428    GroupClass,
429    /// Puppy kindergarten training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
430    PuppyKindergarten,
431    /// Private lesson training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
432    PrivateLesson,
433    /// Akc canine good citizen prep training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
434    AkcCanineGoodCitizenPrep,
435}
436
437#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
438/// Required progress-recording depth for a training program.
439pub enum ProgressTracking {
440    /// Attendance only training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
441    AttendanceOnly,
442    /// Session notes and milestones training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
443    SessionNotesAndMilestones,
444    /// Trainer scorecard training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
445    TrainerScorecard,
446}
447#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
448/// Outcome claim vocabulary that must be backed by trainer evidence before customer-facing use.
449pub enum Outcome {
450    /// Basic manners training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
451    BasicManners,
452    /// Reduced reactivity training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
453    ReducedReactivity,
454    /// Canine good citizen readiness training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
455    CanineGoodCitizenReadiness,
456    /// Owner handling plan training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
457    OwnerHandlingPlan,
458}
459#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
460/// Follow-up cadence that determines whether a progress/homework/re-enrollment message is due.
461pub enum FollowUpCadence {
462    /// No additional workflow gate is required.
463    None,
464    /// After each session training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
465    AfterEachSession,
466    /// After program completion training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
467    AfterProgramCompletion,
468    /// Thirty days after completion training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
469    ThirtyDaysAfterCompletion,
470}
471
472#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
473/// Source evidence attached to progress reports and outcome claims.
474pub enum ProgressEvidence {
475    /// Trainer note training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
476    TrainerNote {
477        /// Source-derived evidence id carried by this training contract.
478        evidence_id: EvidenceId,
479        /// Source-derived note carried by this training contract.
480        note: ProgressNote,
481    },
482    /// Milestone observed training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
483    MilestoneObserved {
484        /// Source-derived evidence id carried by this training contract.
485        evidence_id: EvidenceId,
486        /// Source-derived milestone id carried by this training contract.
487        milestone_id: curriculum::milestone::Id,
488        /// Source-derived status carried by this training contract.
489        status: curriculum::milestone::Status,
490    },
491    /// Session completed training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
492    SessionCompleted {
493        /// Source-derived evidence id carried by this training contract.
494        evidence_id: EvidenceId,
495        /// Source-derived session id carried by this training contract.
496        session_id: SessionId,
497    },
498    /// Outcome candidate training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
499    OutcomeCandidate {
500        /// Source-derived evidence id carried by this training contract.
501        evidence_id: EvidenceId,
502        /// Source-derived outcome carried by this training contract.
503        outcome: Outcome,
504    },
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
508/// Approval state for progress reports before they become parent-facing summaries.
509pub enum ApprovalState {
510    /// Draft training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
511    Draft,
512    /// Trainer approved training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
513    TrainerApproved {
514        /// Source-derived trainer id carried by this training contract.
515        trainer_id: StaffId,
516    },
517    /// Manager approved training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
518    ManagerApproved {
519        /// Source-derived manager id carried by this training contract.
520        manager_id: crate::entities::ManagerId,
521    },
522    /// Rejected training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
523    Rejected {
524        /// Source-derived gate carried by this training contract.
525        gate: policy::ReviewGate,
526    },
527}
528
529#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
530/// Review state for outcome documentation before achievements are exposed to customers.
531pub enum OutcomeReviewState {
532    /// Draft training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
533    Draft,
534    /// Source-derived trainer id carried by this training contract.
535    TrainerApproved {
536        /// Trainer id value carried by this review or workflow variant.
537        trainer_id: StaffId,
538    },
539    /// Source-derived approved by carried by this training contract.
540    ApprovedForMemberFacingUse {
541        /// Approved by value carried by this review or workflow variant.
542        approved_by: StaffId,
543    },
544    /// Source-derived gate carried by this training contract.
545    Rejected {
546        /// Gate value carried by this review or workflow variant.
547        gate: policy::ReviewGate,
548    },
549}
550
551#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
552/// Boundary for whether a training report or outcome may be shown to a pet parent.
553pub enum MemberFacingBoundary {
554    /// Internal only training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
555    InternalOnly,
556    /// Source-derived gate carried by this training contract.
557    DraftRequiresApproval {
558        /// Gate value carried by this review or workflow variant.
559        gate: policy::ReviewGate,
560    },
561    /// Approved for member facing use training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
562    ApprovedForMemberFacingUse,
563}
564
565#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
566/// Remaining reusable session balance for a multi-session training package.
567pub struct SessionBalance(u16);
568
569impl SessionBalance {
570    /// Assembles this training value from already-validated domain parts.
571    pub const fn new(value: u16) -> Self {
572        Self(value)
573    }
574    /// Exposes the validated scalar for serialization and adapter boundaries.
575    pub const fn get(self) -> u16 {
576        self.0
577    }
578    /// Returns the remaining evidence recorded on this training contract.
579    pub const fn remaining(self) -> Self {
580        self
581    }
582    /// Returns the reserve one evidence recorded on this training contract.
583    pub const fn reserve_one(self) -> Self {
584        Self(self.0.saturating_sub(1))
585    }
586}
587
588/// Trainer availability evaluation boundary for assignment drafting and waitlisting.
589pub mod availability {
590    use super::*;
591
592    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
593    /// Decision vocabulary for capacity outcomes in training workflows.
594    pub enum CapacityDecision {
595        /// Available training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
596        Available,
597        /// Unavailable training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
598        Unavailable,
599        /// Estimate confidence is unknown and must be reviewed.
600        UnknownRequiresReview,
601    }
602
603    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
604    /// Assignment request combining enrollment readiness, trainer requirement, capacity evidence, and program details.
605    pub struct Request {
606        /// Source-derived enrollment id carried by this training contract.
607        pub enrollment_id: enrollment::Id,
608        /// Pet receiving the grooming or care service.
609        pub pet_id: PetId,
610        /// Source-derived program carried by this training contract.
611        pub program: Program,
612        /// Source-derived requirement carried by this training contract.
613        pub requirement: trainer::Requirement,
614        /// Source-derived capacity carried by this training contract.
615        pub capacity: CapacityDecision,
616        /// Source-derived readiness carried by this training contract.
617        pub readiness: enrollment::Readiness,
618    }
619
620    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
621    /// Assignment decision showing whether to draft, waitlist, or require review before mutating provider schedules.
622    pub enum Decision {
623        /// Assignment drafted training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
624        AssignmentDrafted,
625        /// Waitlist training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
626        Waitlist {
627            /// Business reason staff should review before proceeding.
628            reason: WaitlistReason,
629            /// Source-derived gate carried by this training contract.
630            gate: policy::ReviewGate,
631        },
632        /// Review required training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
633        ReviewRequired {
634            /// Business reason staff should review before proceeding.
635            reason: ReviewReason,
636            /// Source-derived gate carried by this training contract.
637            gate: policy::ReviewGate,
638        },
639    }
640
641    impl Decision {
642        /// Returns the provider mutation review gate recorded on this training contract.
643        pub fn provider_mutation_gate(&self) -> Option<policy::ReviewGate> {
644            match self {
645                Self::AssignmentDrafted => None,
646                Self::Waitlist { gate, .. } | Self::ReviewRequired { gate, .. } => {
647                    Some(gate.clone())
648                }
649            }
650        }
651    }
652
653    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
654    /// Decision vocabulary for waitlist reason in training workflows.
655    pub enum WaitlistReason {
656        /// Requested trainer unavailable training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
657        RequestedTrainerUnavailable,
658        /// Capacity snapshot unavailable training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
659        CapacitySnapshotUnavailable,
660    }
661
662    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
663    /// Decision vocabulary for review reason in training workflows.
664    pub enum ReviewReason {
665        /// Enrollment not ready training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
666        EnrollmentNotReady,
667        /// Capacity unknown training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
668        CapacityUnknown,
669    }
670
671    #[derive(Debug, Clone, Default)]
672    /// Training policy object that converts source facts into assignment, report, package, or follow-up decisions.
673    pub struct Policy;
674
675    impl Policy {
676        /// Evaluates the request into a draft assignment, waitlist, or review gate without inventing trainer capacity.
677        pub fn evaluate(&self, request: &Request) -> Decision {
678            if let Some(gate) = request.readiness.blocking_gate() {
679                return Decision::ReviewRequired {
680                    reason: ReviewReason::EnrollmentNotReady,
681                    gate,
682                };
683            }
684            match request.capacity {
685                CapacityDecision::Available => Decision::AssignmentDrafted,
686                CapacityDecision::Unavailable => Decision::Waitlist {
687                    reason: if request.requirement.requires_named_trainer() {
688                        WaitlistReason::RequestedTrainerUnavailable
689                    } else {
690                        WaitlistReason::CapacitySnapshotUnavailable
691                    },
692                    gate: policy::ReviewGate::ManagerApproval,
693                },
694                CapacityDecision::UnknownRequiresReview => Decision::ReviewRequired {
695                    reason: ReviewReason::CapacityUnknown,
696                    gate: policy::ReviewGate::ManagerApproval,
697                },
698            }
699        }
700    }
701}
702
703/// Progress-report boundary for evidence-backed trainer updates and parent-facing approval gates.
704pub mod progress {
705    use super::*;
706
707    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
708    /// Training progress report carrying session evidence, milestones, and approval state.
709    pub struct Report {
710        /// Source-derived report id carried by this training contract.
711        pub report_id: ProgressReportId,
712        /// Source-derived enrollment id carried by this training contract.
713        pub enrollment_id: enrollment::Id,
714        /// Source-derived session ref carried by this training contract.
715        pub session_ref: SessionRef,
716        evidence: Vec<ProgressEvidence>,
717        milestones: Vec<curriculum::Progress>,
718        approval: ApprovalState,
719    }
720
721    impl Report {
722        /// Starts a validated builder for this training documentation or progress packet.
723        pub fn builder() -> ReportBuilder {
724            ReportBuilder::default()
725        }
726        /// Reports whether the progress report includes trainer/source evidence.
727        pub fn has_evidence(&self) -> bool {
728            !self.evidence.is_empty()
729        }
730        /// Returns the milestones evidence recorded on this training contract.
731        pub fn milestones(&self) -> &[curriculum::Progress] {
732            &self.milestones
733        }
734        /// Returns the approval evidence recorded on this training contract.
735        pub fn approval(&self) -> &ApprovalState {
736            &self.approval
737        }
738        /// Returns the parent facing boundary evidence recorded on this training contract.
739        pub fn parent_facing_boundary(&self) -> MemberFacingBoundary {
740            match &self.approval {
741                ApprovalState::Draft | ApprovalState::TrainerApproved { .. } => {
742                    MemberFacingBoundary::DraftRequiresApproval {
743                        gate: policy::ReviewGate::CustomerMessageApproval,
744                    }
745                }
746                ApprovalState::ManagerApproved { .. } => {
747                    MemberFacingBoundary::ApprovedForMemberFacingUse
748                }
749                ApprovalState::Rejected { .. } => MemberFacingBoundary::InternalOnly,
750            }
751        }
752    }
753
754    #[derive(Default)]
755    /// Builder for progress reports that rejects reports without trainer/source evidence.
756    pub struct ReportBuilder {
757        report_id: Option<ProgressReportId>,
758        enrollment_id: Option<enrollment::Id>,
759        session_ref: Option<SessionRef>,
760        evidence: Vec<ProgressEvidence>,
761        milestones: Vec<curriculum::Progress>,
762        approval: Option<ApprovalState>,
763    }
764
765    impl ReportBuilder {
766        /// Sets the report id value on this training builder.
767        pub fn report_id(mut self, value: ProgressReportId) -> Self {
768            self.report_id = Some(value);
769            self
770        }
771        /// Sets the enrollment id value on this training builder.
772        pub fn enrollment_id(mut self, value: enrollment::Id) -> Self {
773            self.enrollment_id = Some(value);
774            self
775        }
776        /// Sets the session ref value on this training builder.
777        pub fn session_ref(mut self, value: SessionRef) -> Self {
778            self.session_ref = Some(value);
779            self
780        }
781        /// Returns the evidence recorded on this training contract.
782        pub fn evidence(mut self, value: Vec<ProgressEvidence>) -> Self {
783            self.evidence = value;
784            self
785        }
786        /// Sets the milestones value on this training builder.
787        pub fn milestones(mut self, value: Vec<curriculum::Progress>) -> Self {
788            self.milestones = value;
789            self
790        }
791        /// Sets the approval value on this training builder.
792        pub fn approval(mut self, value: ApprovalState) -> Self {
793            self.approval = Some(value);
794            self
795        }
796        /// Builds the report only when required evidence exists; missing IDs still indicate programmer misuse in tests/fixtures.
797        pub fn build(self) -> Result<Report> {
798            if self.evidence.is_empty() {
799                return Err(Error::ProgressEvidenceRequired);
800            }
801            Ok(Report {
802                report_id: self.report_id.expect("report_id is required"),
803                enrollment_id: self.enrollment_id.expect("enrollment_id is required"),
804                session_ref: self.session_ref.expect("session_ref is required"),
805                evidence: self.evidence,
806                milestones: self.milestones,
807                approval: self.approval.unwrap_or(ApprovalState::Draft),
808            })
809        }
810    }
811}
812
813/// Outcome-documentation boundary for claims like manners readiness or CGC readiness.
814pub mod outcome {
815    use super::*;
816
817    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
818    /// Decision vocabulary for claim status in training workflows.
819    pub enum ClaimStatus {
820        /// Achieved training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
821        Achieved,
822        /// Readiness training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
823        Readiness,
824        /// Deferred training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
825        Deferred,
826        /// Not assessed training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
827        NotAssessed,
828    }
829
830    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
831    /// Evidence bundle used to promote an outcome claim into reviewed documentation.
832    pub struct ClaimEvidence {
833        /// Source-derived outcome carried by this training contract.
834        pub outcome: Outcome,
835        /// Source-derived status carried by this training contract.
836        pub status: ClaimStatus,
837        /// Source-derived evidence carried by this training contract.
838        pub evidence: Vec<EvidenceId>,
839        /// Source-derived milestones carried by this training contract.
840        pub milestones: Vec<curriculum::milestone::Id>,
841    }
842
843    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
844    /// Outcome claim whose achieved/readiness status cannot exist without supporting evidence.
845    pub struct Claim {
846        /// Source-derived outcome carried by this training contract.
847        pub outcome: Outcome,
848        /// Source-derived status carried by this training contract.
849        pub status: ClaimStatus,
850        evidence: Vec<EvidenceId>,
851        milestones: Vec<curriculum::milestone::Id>,
852    }
853
854    impl Claim {
855        /// Builds this training value from evidence data.
856        pub fn from_evidence(value: ClaimEvidence) -> Result<Self> {
857            if matches!(value.status, ClaimStatus::Achieved | ClaimStatus::Readiness)
858                && value.evidence.is_empty()
859            {
860                return Err(Error::OutcomeEvidenceRequired);
861            }
862            Ok(Self {
863                outcome: value.outcome,
864                status: value.status,
865                evidence: value.evidence,
866                milestones: value.milestones,
867            })
868        }
869        /// Returns the evidence recorded on this training contract.
870        pub fn evidence(&self) -> &[EvidenceId] {
871            &self.evidence
872        }
873        /// Returns the milestones evidence recorded on this training contract.
874        pub fn milestones(&self) -> &[curriculum::milestone::Id] {
875            &self.milestones
876        }
877    }
878
879    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
880    /// Training outcome documentation packet for customer/account history and manager review.
881    pub struct Documentation {
882        /// Source-derived documentation id carried by this training contract.
883        pub documentation_id: OutcomeDocumentationId,
884        /// Source-derived enrollment id carried by this training contract.
885        pub enrollment_id: enrollment::Id,
886        /// Pet receiving the grooming or care service.
887        pub pet_id: PetId,
888        /// Source-derived location id carried by this training contract.
889        pub location_id: LocationId,
890        claims: Vec<Claim>,
891        review: OutcomeReviewState,
892    }
893
894    impl Documentation {
895        /// Starts a validated builder for this training documentation or progress packet.
896        pub fn builder() -> DocumentationBuilder {
897            DocumentationBuilder::default()
898        }
899        /// Returns the claims evidence recorded on this training contract.
900        pub fn claims(&self) -> &[Claim] {
901            &self.claims
902        }
903        /// Returns the review evidence recorded on this training contract.
904        pub fn review(&self) -> &OutcomeReviewState {
905            &self.review
906        }
907        /// Returns the member facing boundary evidence recorded on this training contract.
908        pub fn member_facing_boundary(&self) -> MemberFacingBoundary {
909            match &self.review {
910                OutcomeReviewState::ApprovedForMemberFacingUse { .. } => {
911                    MemberFacingBoundary::ApprovedForMemberFacingUse
912                }
913                OutcomeReviewState::Draft | OutcomeReviewState::TrainerApproved { .. } => {
914                    MemberFacingBoundary::DraftRequiresApproval {
915                        gate: policy::ReviewGate::CustomerMessageApproval,
916                    }
917                }
918                OutcomeReviewState::Rejected { .. } => MemberFacingBoundary::InternalOnly,
919            }
920        }
921    }
922
923    #[derive(Default)]
924    /// Builder for outcome documentation that requires at least one evidence-backed claim.
925    pub struct DocumentationBuilder {
926        documentation_id: Option<OutcomeDocumentationId>,
927        enrollment_id: Option<enrollment::Id>,
928        pet_id: Option<PetId>,
929        location_id: Option<LocationId>,
930        claims: Vec<Claim>,
931        review: Option<OutcomeReviewState>,
932    }
933
934    impl DocumentationBuilder {
935        /// Sets the documentation id value on this training builder.
936        pub fn documentation_id(mut self, value: OutcomeDocumentationId) -> Self {
937            self.documentation_id = Some(value);
938            self
939        }
940        /// Sets the enrollment id value on this training builder.
941        pub fn enrollment_id(mut self, value: enrollment::Id) -> Self {
942            self.enrollment_id = Some(value);
943            self
944        }
945        /// Sets the pet id value on this training builder.
946        pub fn pet_id(mut self, value: PetId) -> Self {
947            self.pet_id = Some(value);
948            self
949        }
950        /// Sets the location id value on this training builder.
951        pub fn location_id(mut self, value: LocationId) -> Self {
952            self.location_id = Some(value);
953            self
954        }
955        /// Sets the claims value on this training builder.
956        pub fn claims(mut self, value: Vec<Claim>) -> Self {
957            self.claims = value;
958            self
959        }
960        /// Sets the review value on this training builder.
961        pub fn review(mut self, value: OutcomeReviewState) -> Self {
962            self.review = Some(value);
963            self
964        }
965        /// Builds the report only when required evidence exists; missing IDs still indicate programmer misuse in tests/fixtures.
966        pub fn build(self) -> Result<Documentation> {
967            if self.claims.is_empty() {
968                return Err(Error::OutcomeClaimRequired);
969            }
970            Ok(Documentation {
971                documentation_id: self.documentation_id.expect("documentation_id is required"),
972                enrollment_id: self.enrollment_id.expect("enrollment_id is required"),
973                pet_id: self.pet_id.expect("pet_id is required"),
974                location_id: self.location_id.expect("location_id is required"),
975                claims: self.claims,
976                review: self.review.unwrap_or(OutcomeReviewState::Draft),
977            })
978        }
979    }
980}
981
982/// Package and session-ledger boundary for reserving, consuming, and reconciling training sessions.
983pub mod package {
984    use super::*;
985
986    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
987    /// Groomer-assignment policies used when booking grooming work.
988    pub enum Policy {
989        /// Pay per session training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
990        PayPerSession,
991        /// Source-derived sessions carried by this training contract.
992        MultiSessionPackage {
993            /// Sessions value carried by this review or workflow variant.
994            sessions: SessionCount,
995        },
996        /// Board and train bundle training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
997        BoardAndTrainBundle,
998    }
999
1000    #[nutype(
1001        sanitize(trim),
1002        validate(not_empty, len_char_max = 120),
1003        derive(
1004            Debug,
1005            Clone,
1006            PartialEq,
1007            Eq,
1008            PartialOrd,
1009            Ord,
1010            Hash,
1011            Serialize,
1012            Deserialize
1013        )
1014    )]
1015    pub struct Id(String);
1016
1017    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1018    /// Decision vocabulary for ledger entries in training workflows.
1019    pub enum LedgerEntry {
1020        /// Source-derived sessions carried by this training contract.
1021        Purchased {
1022            /// Sessions value carried by this review or workflow variant.
1023            sessions: SessionCount,
1024        },
1025        /// Source-derived session id carried by this training contract.
1026        Reserved {
1027            /// Session id value carried by this review or workflow variant.
1028            session_id: SessionId,
1029        },
1030        /// Source-derived session id carried by this training contract.
1031        Consumed {
1032            /// Session id value carried by this review or workflow variant.
1033            session_id: SessionId,
1034        },
1035        /// Source-derived session id carried by this training contract.
1036        Released {
1037            /// Session id value carried by this review or workflow variant.
1038            session_id: SessionId,
1039        },
1040    }
1041
1042    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1043    /// Source-derived opening ledger used to create a reusable multi-session package ledger.
1044    pub struct OpeningLedger {
1045        /// Source-derived package id carried by this training contract.
1046        pub package_id: Id,
1047        /// Source-derived customer id carried by this training contract.
1048        pub customer_id: CustomerId,
1049        /// Pet receiving the grooming or care service.
1050        pub pet_id: PetId,
1051        /// Source-derived policy carried by this training contract.
1052        pub policy: Policy,
1053        /// Source-derived entries carried by this training contract.
1054        pub entries: Vec<LedgerEntry>,
1055    }
1056
1057    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1058    /// Training package ledger used to compute remaining reusable sessions without raw counters.
1059    pub struct Ledger {
1060        package_id: Id,
1061        /// Source-derived customer id carried by this training contract.
1062        pub customer_id: CustomerId,
1063        /// Pet receiving the grooming or care service.
1064        pub pet_id: PetId,
1065        policy: Policy,
1066        entries: Vec<LedgerEntry>,
1067    }
1068
1069    impl Ledger {
1070        /// Opens a reusable package ledger after confirming the package policy has a session balance.
1071        pub fn open(opening: OpeningLedger) -> Result<Self> {
1072            if !matches!(opening.policy, Policy::MultiSessionPackage { .. }) {
1073                return Err(Error::PackageHasNoReusableBalance);
1074            }
1075            Ok(Self {
1076                package_id: opening.package_id,
1077                customer_id: opening.customer_id,
1078                pet_id: opening.pet_id,
1079                policy: opening.policy,
1080                entries: opening.entries,
1081            })
1082        }
1083        /// Returns the package id evidence recorded on this training contract.
1084        pub fn package_id(&self) -> &Id {
1085            &self.package_id
1086        }
1087        /// Returns the entries evidence recorded on this training contract.
1088        pub fn entries(&self) -> &[LedgerEntry] {
1089            &self.entries
1090        }
1091        /// Returns the balance evidence recorded on this training contract.
1092        pub fn balance(&self) -> SessionBalance {
1093            let Policy::MultiSessionPackage { sessions } = self.policy else {
1094                return SessionBalance::new(0);
1095            };
1096            let used = self.entries.iter().fold(0u16, |used, entry| match entry {
1097                LedgerEntry::Reserved { .. } | LedgerEntry::Consumed { .. } => {
1098                    used.saturating_add(1)
1099                }
1100                LedgerEntry::Released { .. } => used.saturating_sub(1),
1101                LedgerEntry::Purchased { .. } => used,
1102            });
1103            SessionBalance::new(sessions.get().saturating_sub(used))
1104        }
1105    }
1106
1107    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1108    /// Package usage decision for reserving the next session or escalating balance/reconciliation issues.
1109    pub enum UsageDecision {
1110        /// Reserve next session training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1111        ReserveNextSession {
1112            /// Source-derived package id carried by this training contract.
1113            package_id: Id,
1114            /// Source-derived remaining after reservation carried by this training contract.
1115            remaining_after_reservation: SessionBalance,
1116        },
1117        /// No remaining sessions training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1118        NoRemainingSessions {
1119            /// Source-derived package id carried by this training contract.
1120            package_id: Id,
1121            /// Source-derived gate carried by this training contract.
1122            gate: policy::ReviewGate,
1123        },
1124        /// Reconciliation required training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1125        ReconciliationRequired {
1126            /// Source-derived package id carried by this training contract.
1127            package_id: Id,
1128            /// Source-derived gate carried by this training contract.
1129            gate: policy::ReviewGate,
1130        },
1131    }
1132
1133    #[derive(Debug, Clone, Default)]
1134    /// Represents the usage policy concept as a typed training operational contract instead of a raw primitive.
1135    pub struct UsagePolicy;
1136
1137    impl UsagePolicy {
1138        /// Decides whether the next training session can be reserved or needs payment/reconciliation review.
1139        pub fn decide_usage(&self, ledger: &Ledger) -> UsageDecision {
1140            let balance = ledger.balance();
1141            if balance.get() == 0 {
1142                UsageDecision::NoRemainingSessions {
1143                    package_id: ledger.package_id().clone(),
1144                    gate: policy::ReviewGate::RefundOrDepositException,
1145                }
1146            } else {
1147                UsageDecision::ReserveNextSession {
1148                    package_id: ledger.package_id().clone(),
1149                    remaining_after_reservation: balance.reserve_one(),
1150                }
1151            }
1152        }
1153    }
1154}
1155
1156/// Follow-up boundary for progress updates, homework coaching, completion summaries, and re-enrollment prompts.
1157pub mod follow_up {
1158    use super::*;
1159
1160    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1161    /// Decision vocabulary for trigger in training workflows.
1162    pub enum Trigger {
1163        /// Source-derived session id carried by this training contract.
1164        SessionCompleted {
1165            /// Session id value carried by this review or workflow variant.
1166            session_id: SessionId,
1167        },
1168        /// Source-derived enrollment id carried by this training contract.
1169        ProgramCompleted {
1170            /// Enrollment id value carried by this review or workflow variant.
1171            enrollment_id: enrollment::Id,
1172        },
1173        /// Source-derived enrollment id carried by this training contract.
1174        LaterCadenceCheckpoint {
1175            /// Enrollment id value carried by this review or workflow variant.
1176            enrollment_id: enrollment::Id,
1177        },
1178    }
1179
1180    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1181    /// Decision vocabulary for purpose in training workflows.
1182    pub enum Purpose {
1183        /// Progress update training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1184        ProgressUpdate,
1185        /// Homework coaching training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1186        HomeworkCoaching,
1187        /// Program completion summary training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1188        ProgramCompletionSummary,
1189        /// Re enrollment prompt training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1190        ReEnrollmentPrompt,
1191    }
1192
1193    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1194    /// Decision vocabulary for evidence readiness in training workflows.
1195    pub enum EvidenceReadiness {
1196        /// Progress and homework ready training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1197        ProgressAndHomeworkReady,
1198        /// Needs trainer evidence training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1199        NeedsTrainerEvidence,
1200        /// Outcome disputed or ambiguous training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1201        OutcomeDisputedOrAmbiguous,
1202    }
1203
1204    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1205    /// Decision vocabulary for state in training workflows.
1206    pub enum State {
1207        /// Not due training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1208        NotDue,
1209        /// Source-derived gate carried by this training contract.
1210        TrainerEvidenceRequired {
1211            /// Gate value carried by this review or workflow variant.
1212            gate: policy::ReviewGate,
1213        },
1214        /// Source-derived gate carried by this training contract.
1215        DraftRequiresApproval {
1216            /// Gate value carried by this review or workflow variant.
1217            gate: policy::ReviewGate,
1218        },
1219        /// Suppressed training operational signal for enrollment, curriculum, progress, package, or follow-up handling.
1220        Suppressed,
1221    }
1222
1223    #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1224    /// Follow-up plan that separates due/not-due state from approval-gated customer messaging.
1225    pub struct Plan {
1226        /// Source-derived trigger carried by this training contract.
1227        pub trigger: Trigger,
1228        purpose: Purpose,
1229        state: State,
1230    }
1231
1232    impl Plan {
1233        /// Returns the purpose evidence recorded on this training contract.
1234        pub const fn purpose(&self) -> Purpose {
1235            self.purpose
1236        }
1237        /// Returns the state evidence recorded on this training contract.
1238        pub fn state(&self) -> State {
1239            self.state.clone()
1240        }
1241    }
1242
1243    #[derive(Debug, Clone, Default)]
1244    /// Training policy object that converts source facts into assignment, report, package, or follow-up decisions.
1245    pub struct Policy;
1246
1247    impl Policy {
1248        /// Builds a training follow-up plan from trigger, cadence, and evidence readiness.
1249        pub const fn plan(
1250            &self,
1251            trigger: Trigger,
1252            cadence: FollowUpCadence,
1253            evidence: EvidenceReadiness,
1254        ) -> Plan {
1255            let purpose = match trigger {
1256                Trigger::SessionCompleted { .. } => Purpose::ProgressUpdate,
1257                Trigger::ProgramCompleted { .. } => Purpose::ProgramCompletionSummary,
1258                Trigger::LaterCadenceCheckpoint { .. } => Purpose::ReEnrollmentPrompt,
1259            };
1260            let cadence_matches = matches!(
1261                (&trigger, cadence),
1262                (
1263                    Trigger::SessionCompleted { .. },
1264                    FollowUpCadence::AfterEachSession
1265                ) | (
1266                    Trigger::ProgramCompleted { .. },
1267                    FollowUpCadence::AfterProgramCompletion
1268                ) | (
1269                    Trigger::LaterCadenceCheckpoint { .. },
1270                    FollowUpCadence::ThirtyDaysAfterCompletion
1271                )
1272            );
1273            let state = if !cadence_matches || matches!(cadence, FollowUpCadence::None) {
1274                State::NotDue
1275            } else {
1276                match evidence {
1277                    EvidenceReadiness::ProgressAndHomeworkReady => State::DraftRequiresApproval {
1278                        gate: policy::ReviewGate::CustomerMessageApproval,
1279                    },
1280                    EvidenceReadiness::NeedsTrainerEvidence => State::TrainerEvidenceRequired {
1281                        gate: policy::ReviewGate::ManagerApproval,
1282                    },
1283                    EvidenceReadiness::OutcomeDisputedOrAmbiguous => {
1284                        State::TrainerEvidenceRequired {
1285                            gate: policy::ReviewGate::ManagerApproval,
1286                        }
1287                    }
1288                }
1289            };
1290            Plan {
1291                trigger,
1292                purpose,
1293                state,
1294            }
1295        }
1296    }
1297}
1298
1299#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
1300/// Location training contract tying program duration, curriculum, progress depth, outcomes, trainer availability, package policy, and follow-up cadence together.
1301pub struct Contract {
1302    /// Source-derived program duration carried by this training contract.
1303    pub program_duration: program::Duration,
1304    #[builder(default)]
1305    /// Source-derived curriculum carried by this training contract.
1306    pub curriculum: Vec<curriculum::Unit>,
1307    /// Source-derived progress carried by this training contract.
1308    pub progress: ProgressTracking,
1309    #[builder(default)]
1310    /// Source-derived outcomes carried by this training contract.
1311    pub outcomes: Vec<Outcome>,
1312    /// Source-derived trainer availability carried by this training contract.
1313    pub trainer_availability: trainer::Availability,
1314    /// Source-derived package carried by this training contract.
1315    pub package: package::Policy,
1316    /// Source-derived follow up carried by this training contract.
1317    pub follow_up: FollowUpCadence,
1318}
1319
1320impl Contract {
1321    /// Reports whether trainer assignment must use a named or waitlisted trainer.
1322    pub fn requires_named_trainer(&self) -> bool {
1323        matches!(
1324            self.trainer_availability,
1325            trainer::Availability::NamedTrainerRequired
1326                | trainer::Availability::WaitlistUntilTrainerAvailable
1327        )
1328    }
1329    /// Reports whether the training contract includes the requested outcome claim.
1330    pub fn has_outcome(&self, outcome: &Outcome) -> bool {
1331        self.outcomes.contains(outcome)
1332    }
1333    /// Builds a representative PetSuites-style training contract for docs/tests without claiming it is live policy.
1334    pub fn standard_petsuites() -> Self {
1335        Self::builder()
1336            .program_duration(program::Duration::Weeks(
1337                program::DurationWeeks::try_new(3).unwrap(),
1338            ))
1339            .curriculum(vec![
1340                curriculum::Unit::LooseLeashWalking,
1341                curriculum::Unit::Recall,
1342            ])
1343            .progress(ProgressTracking::SessionNotesAndMilestones)
1344            .outcomes(vec![Outcome::CanineGoodCitizenReadiness])
1345            .trainer_availability(trainer::Availability::NamedTrainerRequired)
1346            .package(package::Policy::MultiSessionPackage {
1347                sessions: SessionCount::try_new(6).unwrap(),
1348            })
1349            .follow_up(FollowUpCadence::AfterProgramCompletion)
1350            .build()
1351    }
1352}