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}