Skip to main content

domain/training/
mod.rs

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