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}