domain/grooming/mod.rs
1//! Grooming service-line rules for pet-resort scheduling, no-show, rebooking, reminder, and review queues.
2//!
3//! Operators use this module to answer grooming queue questions without rereading notes by hand: how much groomer time a mini/full groom, bath, nail service, or coat/skin add-on should reserve; whether repeat no-show history requires a deposit or manager review; when a completed service should become a rebooking prompt; and whether a reminder draft is safe to prepare. The labor reduction is triage and evidence assembly, not unattended execution.
4//!
5//! Use it when the business question is "what grooming work can be prepared for staff or customer review, and what calendar, deposit, handling, or message approval still blocks live execution?" Next step: start with the location rules and `Service` for policy and request type, then follow `duration_estimate`, `no_show`, `rebooking`, `reminder`, or `calendar` depending on the queue you are trying to explain.
6//!
7//! The authoritative facts are the location rules, the requested `Service`, breed/coat facts on `EstimationRequest`, prior approved `history::ServiceHistoryEntry` records, pet/customer/location/staff identity from `domain::entities`, and shared `domain::policy::ReviewGate` approvals. Provider catalog names, adapter defaults, and AI suggestions must be promoted into these values or remain pending review evidence.
8//!
9//! This module must not book or move appointments, assign a live provider-calendar slot, send a customer message, charge or waive a deposit, or decide medical/handling safety on its own. `ReviewRequirement::calendar_execution_gate`, `no_show::Decision`, and `reminder::Plan::customer_message_gate` preserve the human review gates that protect pets, customers, groomers, and managers before app/storage/integration layers perform live work.
10
11use bon::Builder;
12use chrono::NaiveDate;
13use serde::{Deserialize, Deserializer, Serialize};
14
15use crate::entities::{CustomerId, LocationId, PetId, StaffId};
16
17macro_rules! positive_scalar {
18 ($name:ident, $primitive:ty, $error:ident, $message:literal) => {
19 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
20 /// Positive grooming quantity used where a zero-minute appointment or zero-length operational value would create impossible schedule math.
21 pub struct $name($primitive);
22
23 impl $name {
24 /// Rejects zero or unsupported grooming values before they affect groomer calendars, duration estimates, deposits, reminders, or rebooking prompts.
25 pub const fn try_new(value: $primitive) -> std::result::Result<Self, $error> {
26 if value == 0 {
27 return Err($error::Zero);
28 }
29 Ok(Self(value))
30 }
31
32 /// Returns the grooming number used by scheduling, estimate, reminder, or rebooking calculations.
33 pub const fn get(self) -> $primitive {
34 self.0
35 }
36 }
37
38 impl<'de> Deserialize<'de> for $name {
39 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
40 where
41 D: Deserializer<'de>,
42 {
43 Self::try_new(<$primitive>::deserialize(deserializer)?)
44 .map_err(serde::de::Error::custom)
45 }
46 }
47
48 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
49 /// Validation failures returned by grooming domain constructors.
50 pub enum $error {
51 #[error($message)]
52 /// Rejects zero where the pet-resort workflow requires a positive quantity.
53 Zero,
54 }
55 };
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59/// Grooming services and add-ons that drive groomer calendar load, checkout upsells, duration estimates, and follow-up reminders.
60pub enum Service {
61 /// Mini groom request that typically consumes less groomer time but still needs coat/history context.
62 MiniGroom,
63 /// Full groom request that drives the heaviest groomer labor estimate and style-history review.
64 FullGroom,
65 /// Bath offered before departure from boarding.
66 ExitBath,
67 /// Full bath appointment that may stand alone or attach to daycare/boarding checkout.
68 FullBath,
69 /// Premium bath that can justify product/style-note capture and higher checkout value.
70 PremiumBath,
71 /// Nail trim add-on that affects short-slot grooming capacity.
72 NailTrim,
73 /// Nail Dremel add-on that should respect pet handling notes and appointment timing.
74 NailDremel,
75 /// Ear-cleaning add-on whose care sensitivity may require staff review before customer claims.
76 EarCleaning,
77 /// Coat/skin product add-on that should remain a product recommendation unless care review approves stronger claims.
78 CoatSkinSpecificProduct,
79 /// First-time grooming offer used to convert new/lapsed guests without bypassing scheduling constraints.
80 FirstTimeGroomingOffer,
81}
82
83positive_scalar!(
84 AppointmentMinutes,
85 u16,
86 AppointmentMinutesError,
87 "grooming appointment estimate requires at least one minute"
88);
89
90/// Groomer-calendar policy for assigning grooming work without inventing availability.
91pub mod calendar {
92 use super::*;
93
94 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
95 /// Groomer-assignment policy used to decide whether a request can draft directly or needs manager/groomer review.
96 pub enum Policy {
97 /// Any qualified groomer may take the appointment if the schedule system shows capacity.
98 AnyQualifiedGroomer,
99 /// A specific groomer is required because of guest history, owner request, or service complexity.
100 GroomerSpecific,
101 /// First-available assignment is allowed only with a manager override when ordinary matching cannot satisfy demand.
102 FirstAvailableWithManagerOverride,
103 }
104}
105/// Breed/coat inputs for converting pet profile facts into labor-time estimates.
106pub mod breed_coat {
107 use super::*;
108
109 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110 /// Breed and coat groupings used to estimate grooming labor time.
111 pub enum BreedCategory {
112 /// Short-coat category with lower expected grooming labor when no history indicates otherwise.
113 ShortCoat,
114 /// Double-coat category that may require extra drying/deshedding time.
115 DoubleCoat,
116 /// Doodle or similar coat category where matting/style history often changes the estimate.
117 Doodle,
118 /// Cat guest, using cat-specific policy and accommodation rules.
119 Cat,
120 }
121
122 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
123 /// Coat condition signals that affect grooming time and review needs.
124 pub enum CoatCondition {
125 /// Maintained coat condition suitable for standard estimates.
126 Maintained,
127 /// Thick undercoat condition that increases labor estimate and may alter product recommendations.
128 ThickUndercoat,
129 /// Matted coat condition that requires groomer review before accepting a duration estimate.
130 Matted,
131 }
132
133 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
134 /// Duration estimate input derived from breed and coat facts for groomer calendar planning.
135 pub struct TimeEstimate {
136 /// Breed/coat class used to translate pet profile data into groomer labor demand.
137 pub breed: BreedCategory,
138 /// Coat condition that can raise confidence risk or trigger groomer review.
139 pub coat: CoatCondition,
140 minutes: AppointmentMinutes,
141 }
142
143 impl TimeEstimate {
144 /// Creates this grooming value from already-checked resort workflow inputs.
145 pub const fn new(
146 breed: BreedCategory,
147 coat: CoatCondition,
148 minutes: AppointmentMinutes,
149 ) -> Self {
150 Self {
151 breed,
152 coat,
153 minutes,
154 }
155 }
156
157 /// Returns the minutes value used by grooming schedule/rebooking review.
158 pub const fn minutes(&self) -> AppointmentMinutes {
159 self.minutes
160 }
161 }
162}
163#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
164/// Service-history retention requirement that protects rebooking quality and safe handling across visits.
165pub enum HistoryRequirement {
166 /// Preserve service notes so future estimates can cite source history rather than invent timing.
167 KeepServiceNotes,
168 /// Preserve style notes/photos so groomers can reproduce customer preferences at the next cadence.
169 KeepStyleNotesAndPhotos,
170 /// Preserve medical or handling notes and route sensitive interpretation through care review.
171 KeepMedicalHandlingNotes,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
175/// Grooming estimate request assembled from pet profile facts before staff propose any calendar change.
176pub struct EstimationRequest {
177 /// Pet receiving the grooming or care service.
178 pub pet_id: PetId,
179 /// Requested service that drives scheduling and labor estimates.
180 pub service: Service,
181 /// Breed/coat class used to translate pet profile data into groomer labor demand.
182 pub breed: breed_coat::BreedCategory,
183 /// Coat condition that can raise confidence risk or trigger groomer review.
184 pub coat: breed_coat::CoatCondition,
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
188/// Evidence basis that explains why a grooming duration was chosen for scheduling review.
189pub enum EstimateBasis {
190 /// Estimate came from the location breed/coat policy.
191 BreedCoatPolicy,
192 /// Estimate came from prior groomer history for this pet.
193 GroomerHistory,
194 /// Estimate fell back to a location default when stronger source facts were unavailable.
195 LocationDefault,
196 /// Estimate came from provider defaults and should not override local policy silently.
197 ProviderDefault,
198 /// Estimate was overridden by staff and should be auditable as a human-entered fact.
199 ManualStaffOverride,
200 /// Estimate was suggested by automation and must remain pending review before schedule use.
201 AiSuggestedPendingReview,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
205/// Confidence level assigned to a grooming duration estimate.
206pub enum EstimateConfidence {
207 /// Estimate is reliable enough for normal scheduling.
208 High,
209 /// Estimate is usable but should be treated with moderate uncertainty.
210 Medium,
211 /// Estimate is uncertain and may require staff confirmation.
212 Low,
213 /// Estimate confidence is unknown and must be reviewed.
214 UnknownRequiresReview,
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
218/// Review lane that determines whether a grooming estimate may be used for calendar execution.
219pub enum ReviewRequirement {
220 /// No additional workflow gate is required.
221 None,
222 /// General staff review is required before this estimate becomes actionable.
223 StaffReview,
224 /// Groomer review is required because coat/history/service complexity affects labor time.
225 GroomerReview,
226 /// Manager review is required before accepting an exceptional estimate or schedule choice.
227 ManagerReview,
228 /// Care/medical-document review is required before acting on sensitive handling information.
229 CareReview,
230}
231
232impl ReviewRequirement {
233 /// Maps the grooming review lane to the workflow gate that must approve scheduling.
234 pub const fn calendar_execution_gate(self) -> Option<crate::policy::ReviewGate> {
235 match self {
236 Self::None => None,
237 Self::StaffReview | Self::GroomerReview | Self::ManagerReview => {
238 Some(crate::policy::ReviewGate::ManagerApproval)
239 }
240 Self::CareReview => Some(crate::policy::ReviewGate::MedicalDocumentReview),
241 }
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246/// Grooming duration decision with evidence, confidence, and the review gate needed before calendar use.
247pub struct DurationEstimate {
248 minutes: AppointmentMinutes,
249 basis: EstimateBasis,
250 confidence: EstimateConfidence,
251 review: ReviewRequirement,
252}
253
254impl DurationEstimate {
255 const fn new(
256 minutes: AppointmentMinutes,
257 basis: EstimateBasis,
258 confidence: EstimateConfidence,
259 review: ReviewRequirement,
260 ) -> Self {
261 Self {
262 minutes,
263 basis,
264 confidence,
265 review,
266 }
267 }
268
269 /// Returns the minutes value used by grooming schedule/rebooking review.
270 pub const fn minutes(&self) -> AppointmentMinutes {
271 self.minutes
272 }
273
274 /// Returns the basis value used by grooming schedule/rebooking review.
275 pub const fn basis(&self) -> EstimateBasis {
276 self.basis
277 }
278
279 /// Returns the confidence value used by grooming schedule/rebooking review.
280 pub const fn confidence(&self) -> EstimateConfidence {
281 self.confidence
282 }
283
284 /// Returns the review value used by grooming schedule/rebooking review.
285 pub const fn review(&self) -> ReviewRequirement {
286 self.review
287 }
288
289 /// Maps the grooming review lane to the workflow gate that must approve scheduling.
290 pub const fn calendar_execution_gate(&self) -> Option<crate::policy::ReviewGate> {
291 self.review.calendar_execution_gate()
292 }
293}
294
295#[derive(Debug, Clone, Default)]
296/// Policy object that chooses a grooming duration from pet history first, then location breed/coat defaults.
297pub struct EstimationPolicy;
298
299impl EstimationPolicy {
300 /// Estimates appointment minutes from source history or local policy defaults and records any required review gate.
301 pub fn estimate(
302 &self,
303 request: EstimationRequest,
304 history: &[history::ServiceHistoryEntry],
305 contract: &Contract,
306 ) -> DurationEstimate {
307 if let Some(entry) = history
308 .iter()
309 .rev()
310 .find(|entry| entry.pet_id == request.pet_id && entry.duration().is_some())
311 {
312 return DurationEstimate::new(
313 entry.duration().expect("checked above"),
314 EstimateBasis::GroomerHistory,
315 EstimateConfidence::Medium,
316 if entry.requires_review() {
317 ReviewRequirement::GroomerReview
318 } else {
319 ReviewRequirement::None
320 },
321 );
322 }
323
324 let minutes = contract
325 .time_estimates
326 .iter()
327 .find(|estimate| estimate.breed == request.breed && estimate.coat == request.coat)
328 .or_else(|| {
329 contract
330 .time_estimates
331 .iter()
332 .find(|estimate| estimate.breed == request.breed)
333 })
334 .map(breed_coat::TimeEstimate::minutes)
335 .unwrap_or_else(|| {
336 AppointmentMinutes::try_new(60).expect("default estimate is positive")
337 });
338
339 let review = match request.coat {
340 breed_coat::CoatCondition::Matted => ReviewRequirement::GroomerReview,
341 breed_coat::CoatCondition::Maintained | breed_coat::CoatCondition::ThickUndercoat => {
342 ReviewRequirement::None
343 }
344 };
345 let confidence = if matches!(review, ReviewRequirement::None) {
346 EstimateConfidence::High
347 } else {
348 EstimateConfidence::Medium
349 };
350
351 DurationEstimate::new(minutes, EstimateBasis::BreedCoatPolicy, confidence, review)
352 }
353}
354
355/// No-show and late-cancel policy for protecting groomer capacity and rebooking decisions.
356pub mod no_show {
357 use super::*;
358
359 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
360 /// Rebooking rule that tells staff whether history only, deposit review, or manager review applies.
361 pub enum Rule {
362 /// Staff can see the note history only grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
363 NoteHistoryOnly,
364 /// Staff can see the require deposit for rebooking grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
365 RequireDepositForRebooking,
366 /// Staff can see the manager review before rebooking grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
367 ManagerReviewBeforeRebooking,
368 }
369
370 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
371 /// No-show count considered during grooming deposit and rebooking review.
372 pub struct Count(u16);
373
374 impl Count {
375 /// Rejects zero or unsupported grooming values before they affect groomer calendars, duration estimates, deposits, reminders, or rebooking prompts.
376 pub const fn try_new(value: u16) -> std::result::Result<Self, std::convert::Infallible> {
377 Ok(Self(value))
378 }
379
380 /// Returns the grooming number used by scheduling, estimate, reminder, or rebooking calculations.
381 pub const fn get(self) -> u16 {
382 self.0
383 }
384 }
385
386 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
387 /// Late-cancel count considered with no-shows during grooming rebooking review.
388 pub struct LateCancelCount(u16);
389
390 impl LateCancelCount {
391 /// Rejects zero or unsupported grooming values before they affect groomer calendars, duration estimates, deposits, reminders, or rebooking prompts.
392 pub const fn try_new(value: u16) -> std::result::Result<Self, std::convert::Infallible> {
393 Ok(Self(value))
394 }
395
396 /// Returns the grooming number used by scheduling, estimate, reminder, or rebooking calculations.
397 pub const fn get(self) -> u16 {
398 self.0
399 }
400 }
401
402 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
403 /// Repeat grooming history staff review before clearing a rebooking path.
404 pub struct History {
405 /// No shows from source or staff evidence used during grooming schedule/rebooking review; it does not authorize live changes by itself.
406 pub no_shows: Count,
407 /// Late cancels from source or staff evidence used during grooming schedule/rebooking review; it does not authorize live changes by itself.
408 pub late_cancels: LateCancelCount,
409 }
410
411 impl History {
412 /// Creates this grooming value from already-checked resort workflow inputs.
413 pub const fn new(no_shows: Count, late_cancels: LateCancelCount) -> Self {
414 Self {
415 no_shows,
416 late_cancels,
417 }
418 }
419
420 /// Returns the repeat behavior count value used by grooming schedule/rebooking review.
421 pub const fn repeat_behavior_count(&self) -> u16 {
422 self.no_shows.get().saturating_add(self.late_cancels.get())
423 }
424 }
425
426 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
427 /// Grooming rebooking outcome that tells staff whether to clear, collect a deposit, or seek manager review.
428 pub enum Decision {
429 /// Staff can see the clear to rebook grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
430 ClearToRebook,
431 /// Review gate that must clear before this grooming decision can trigger a live schedule, deposit, or message action.
432 DepositRequired {
433 /// Approval gate staff must clear before acting on this variant.
434 gate: crate::policy::ReviewGate,
435 },
436 /// Review gate that must clear before this grooming decision can trigger a live schedule, deposit, or message action.
437 ManagerReviewRequired {
438 /// Approval gate staff must clear before acting on this variant.
439 gate: crate::policy::ReviewGate,
440 },
441 }
442
443 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
444 /// Grooming rebooking evaluation packet tying customer, pet, and repeat-history facts together.
445 pub struct Evaluation {
446 /// Customer whose grooming reminder, deposit review, or rebooking packet is being prepared.
447 pub customer_id: CustomerId,
448 /// Pet receiving the grooming or care service.
449 pub pet_id: PetId,
450 /// No-show and late-cancel history staff review before choosing a rebooking path.
451 pub history: History,
452 }
453
454 #[derive(Debug, Clone)]
455 /// Grooming policy object that turns local rules into staff review decisions.
456 pub struct Policy {
457 rule: Rule,
458 }
459
460 impl Policy {
461 /// Creates this grooming value from already-checked resort workflow inputs.
462 pub const fn new(rule: Rule) -> Self {
463 Self { rule }
464 }
465
466 /// Evaluates grooming source facts into a rebooking or review decision.
467 pub fn evaluate(
468 &self,
469 customer_id: CustomerId,
470 pet_id: PetId,
471 history: History,
472 ) -> Decision {
473 let _evaluation = Evaluation {
474 customer_id,
475 pet_id,
476 history,
477 };
478 match self.rule {
479 Rule::NoteHistoryOnly => Decision::ClearToRebook,
480 Rule::RequireDepositForRebooking if history.repeat_behavior_count() > 0 => {
481 Decision::DepositRequired {
482 gate: crate::policy::ReviewGate::RefundOrDepositException,
483 }
484 }
485 Rule::RequireDepositForRebooking => Decision::ClearToRebook,
486 Rule::ManagerReviewBeforeRebooking => Decision::ManagerReviewRequired {
487 gate: crate::policy::ReviewGate::ManagerApproval,
488 },
489 }
490 }
491 }
492}
493
494/// History workflow gate for the grooming schedule, estimate, history, rebooking, reminder, or review workflow.
495pub mod history {
496 use super::*;
497
498 /// Style note workflow gate for the grooming schedule, estimate, history, rebooking, reminder, or review workflow.
499 pub mod style_note {
500 use nutype::nutype;
501
502 #[nutype(
503 sanitize(trim),
504 validate(not_empty, len_char_max = 500),
505 derive(
506 Debug,
507 Clone,
508 PartialEq,
509 Eq,
510 PartialOrd,
511 Ord,
512 Hash,
513 Serialize,
514 Deserialize
515 )
516 )]
517 pub struct StyleNote(String);
518 }
519
520 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
521 /// Care references that keep product, medical, or handling notes visible to groomer review.
522 pub enum CareReference {
523 /// Staff can see the sensitive skin product grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
524 SensitiveSkinProduct,
525 /// Staff can see the medicated product requires review grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
526 MedicatedProductRequiresReview,
527 /// Staff can see the handling or medical concern grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
528 HandlingOrMedicalConcern,
529 }
530
531 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
532 /// Service outcome recorded for grooming history, estimates, and rebooking prompts.
533 pub enum ServiceOutcome {
534 /// Staff can see the completed grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
535 Completed,
536 /// Staff can see the no show grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
537 NoShow,
538 /// Staff can see the late cancelled grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
539 LateCancelled,
540 /// Staff can see the needs follow up grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
541 NeedsFollowUp,
542 }
543
544 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
545 /// Approval state controlling whether grooming history can support future estimates or rebooking.
546 pub enum ApprovalState {
547 /// Staff can see the draft grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
548 Draft,
549 /// Review gate that must clear before this grooming decision can trigger a live schedule, deposit, or message action.
550 ReviewRequired {
551 /// Approval gate staff must clear before acting on this variant.
552 gate: crate::policy::ReviewGate,
553 },
554 /// Groomer who approved the history entry for later estimate, care, or rebooking review.
555 ApprovedByGroomer {
556 /// Groomer who approved this grooming history or review state.
557 groomer_id: StaffId,
558 },
559 /// Review gate that must clear before this grooming decision can trigger a live schedule, deposit, or message action.
560 Rejected {
561 /// Approval gate staff must clear before acting on this variant.
562 gate: crate::policy::ReviewGate,
563 },
564 }
565
566 impl ApprovalState {
567 /// Reports whether care-team review is needed before proceeding.
568 pub const fn requires_review(&self) -> bool {
569 matches!(self, Self::Draft | Self::ReviewRequired { .. })
570 }
571 }
572
573 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
574 /// Grooming history entry used for future duration estimates, style continuity, care review, and rebooking.
575 pub struct ServiceHistoryEntry {
576 /// Pet receiving the grooming or care service.
577 pub pet_id: PetId,
578 /// Resort location whose grooming history should be considered for this pet.
579 pub location_id: LocationId,
580 /// Requested service that drives scheduling and labor estimates.
581 pub service: super::Service,
582 /// Date the grooming outcome was completed or recorded for cadence and history review.
583 pub completed_on: NaiveDate,
584 /// Service outcome used to decide whether future estimates, rebooking prompts, or follow-up are appropriate.
585 pub outcome: ServiceOutcome,
586 /// Approval state that keeps sensitive grooming history out of automation until review clears.
587 pub approval: ApprovalState,
588 #[builder(default)]
589 style_notes: Vec<style_note::StyleNote>,
590 #[builder(default)]
591 care_refs: Vec<CareReference>,
592 duration: Option<AppointmentMinutes>,
593 }
594
595 impl ServiceHistoryEntry {
596 /// Returns the style notes value used by grooming schedule/rebooking review.
597 pub fn style_notes(&self) -> &[style_note::StyleNote] {
598 &self.style_notes
599 }
600
601 /// Returns the care refs value used by grooming schedule/rebooking review.
602 pub fn care_refs(&self) -> &[CareReference] {
603 &self.care_refs
604 }
605
606 /// Returns the duration value used by grooming schedule/rebooking review.
607 pub const fn duration(&self) -> Option<AppointmentMinutes> {
608 self.duration
609 }
610
611 /// Reports whether care-team review is needed before proceeding.
612 pub const fn requires_review(&self) -> bool {
613 self.approval.requires_review() || !self.care_refs.is_empty()
614 }
615 }
616}
617
618/// Rebooking cadence policy for identifying due, overdue, or history-insufficient grooming follow-up.
619pub mod rebooking {
620 use super::*;
621
622 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
623 /// Grooming cadence in weeks for due/overdue rebooking prompts.
624 pub struct CadenceWeeks(u8);
625
626 impl CadenceWeeks {
627 /// Rejects zero or unsupported grooming values before they affect groomer calendars, duration estimates, deposits, reminders, or rebooking prompts.
628 pub const fn try_new(value: u8) -> std::result::Result<Self, CadenceWeeksError> {
629 if value == 0 {
630 return Err(CadenceWeeksError::ZeroWeeks);
631 }
632 Ok(Self(value))
633 }
634
635 /// Returns the grooming number used by scheduling, estimate, reminder, or rebooking calculations.
636 pub const fn get(self) -> u8 {
637 self.0
638 }
639 }
640
641 impl<'de> Deserialize<'de> for CadenceWeeks {
642 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
643 where
644 D: Deserializer<'de>,
645 {
646 Self::try_new(u8::deserialize(deserializer)?).map_err(serde::de::Error::custom)
647 }
648 }
649
650 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
651 /// Cadence validation error for rebooking prompts that cannot use zero weeks.
652 pub enum CadenceWeeksError {
653 #[error("grooming cadence requires at least one week")]
654 /// Staff can see the zero weeks grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
655 ZeroWeeks,
656 }
657
658 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
659 /// Ordinary grooming cadence band used when staff expect repeat appointments every few weeks.
660 pub struct OrdinaryCadenceWeeks(u8);
661
662 impl OrdinaryCadenceWeeks {
663 /// Rejects zero or unsupported grooming values before they affect groomer calendars, duration estimates, deposits, reminders, or rebooking prompts.
664 pub const fn try_new(value: u8) -> std::result::Result<Self, OrdinaryCadenceWeeksError> {
665 if value < 2 || value > 8 {
666 return Err(OrdinaryCadenceWeeksError::OutsideOrdinaryGroomingBand);
667 }
668 Ok(Self(value))
669 }
670
671 /// Returns the grooming number used by scheduling, estimate, reminder, or rebooking calculations.
672 pub const fn get(self) -> u8 {
673 self.0
674 }
675 }
676
677 impl<'de> Deserialize<'de> for OrdinaryCadenceWeeks {
678 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
679 where
680 D: Deserializer<'de>,
681 {
682 Self::try_new(u8::deserialize(deserializer)?).map_err(serde::de::Error::custom)
683 }
684 }
685
686 #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
687 /// Cadence validation error for values outside the ordinary grooming rebooking band.
688 pub enum OrdinaryCadenceWeeksError {
689 #[error("ordinary grooming rebooking cadence must be between 2 and 8 weeks")]
690 /// Staff can see the outside ordinary grooming band grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
691 OutsideOrdinaryGroomingBand,
692 }
693
694 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
695 /// Rebooking cadence source used for due-date prompts and groomer-recommended follow-up.
696 pub enum Cadence {
697 /// Staff can see the every weeks grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
698 EveryWeeks(CadenceWeeks),
699 /// Staff can see the as needed grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
700 AsNeeded,
701 /// Staff can see the groomer recommended grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
702 GroomerRecommended,
703 /// Provider role or status could not be mapped confidently.
704 Unknown,
705 }
706
707 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
708 /// Normalized reservation states observed during source-data ingestion.
709 pub enum Status {
710 /// Staff can see the due later grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
711 DueLater,
712 /// Staff can see the due now grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
713 DueNow,
714 /// Staff can see the overdue grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
715 Overdue,
716 /// Staff can see the needs groomer recommendation grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
717 NeedsGroomerRecommendation,
718 }
719
720 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
721 /// Reason explaining why a grooming rebooking prompt is due or needs groomer input.
722 pub enum Rationale {
723 /// Staff can see the last completed service cadence grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
724 LastCompletedServiceCadence,
725 /// Staff can see the no completed history grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
726 NoCompletedHistory,
727 /// Staff can see the groomer recommended cadence required grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
728 GroomerRecommendedCadenceRequired,
729 }
730
731 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
732 /// Grooming rebooking recommendation staff can review before drafting customer follow-up.
733 pub struct Recommendation {
734 /// Pet receiving the grooming or care service.
735 pub pet_id: PetId,
736 /// Date when the next grooming reminder or rebooking prompt becomes due.
737 pub due_on: Option<NaiveDate>,
738 /// Rebooking status staff use to decide whether to prompt, wait, or request groomer input.
739 pub status: Status,
740 /// Reason explaining why the rebooking recommendation is due, overdue, or blocked for groomer input.
741 pub rationale: Rationale,
742 }
743
744 #[derive(Debug, Clone, Default)]
745 /// Grooming policy object that turns local rules into staff review decisions.
746 pub struct Policy;
747
748 impl Policy {
749 /// Returns the recommend from history value used by grooming schedule/rebooking review.
750 pub fn recommend_from_history(
751 &self,
752 pet_id: PetId,
753 history: &[history::ServiceHistoryEntry],
754 cadence: Cadence,
755 today: NaiveDate,
756 ) -> Recommendation {
757 let Some(last_completed) = history
758 .iter()
759 .filter(|entry| entry.pet_id == pet_id)
760 .filter(|entry| matches!(entry.outcome, history::ServiceOutcome::Completed))
761 .max_by_key(|entry| entry.completed_on)
762 else {
763 return Recommendation {
764 pet_id,
765 due_on: None,
766 status: Status::NeedsGroomerRecommendation,
767 rationale: Rationale::NoCompletedHistory,
768 };
769 };
770
771 let Cadence::EveryWeeks(weeks) = cadence else {
772 return Recommendation {
773 pet_id,
774 due_on: None,
775 status: Status::NeedsGroomerRecommendation,
776 rationale: Rationale::GroomerRecommendedCadenceRequired,
777 };
778 };
779
780 let due_on = last_completed
781 .completed_on
782 .checked_add_days(chrono::Days::new(u64::from(weeks.get()) * 7))
783 .expect("bounded grooming cadence should fit chrono date range");
784 let status = if today > due_on {
785 Status::Overdue
786 } else if today == due_on {
787 Status::DueNow
788 } else {
789 Status::DueLater
790 };
791
792 Recommendation {
793 pet_id,
794 due_on: Some(due_on),
795 status,
796 rationale: Rationale::LastCompletedServiceCadence,
797 }
798 }
799 }
800}
801
802/// Reminder policy for drafting appointment confirmations, prep instructions, and cadence winback messages.
803pub mod reminder {
804 use super::*;
805
806 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
807 /// Rebooking rule that tells staff whether history only, deposit review, or manager review applies.
808 pub enum Rule {
809 /// Staff can see the one week before grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
810 OneWeekBefore,
811 /// Staff can see the forty eight hours before grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
812 FortyEightHoursBefore,
813 /// Staff can see the morning of grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
814 MorningOf,
815 }
816
817 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
818 /// Reminder purpose for grooming confirmations, prep instructions, and cadence follow-up drafts.
819 pub enum Kind {
820 /// Staff can see the appointment confirmation grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
821 AppointmentConfirmation,
822 /// Staff can see the prep instructions grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
823 PrepInstructions,
824 /// Staff can see the morning of grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
825 MorningOf,
826 /// Staff can see the rebooking due grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
827 RebookingDue,
828 /// Staff can see the lapsed cadence winback grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
829 LapsedCadenceWinback,
830 }
831
832 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
833 /// Customer-message consent state used before grooming reminder drafts proceed.
834 pub enum Consent {
835 /// Staff can see the granted grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
836 Granted,
837 /// Staff can see the not granted grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
838 NotGranted,
839 }
840
841 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
842 /// Customer-message send status for grooming reminder plans.
843 pub enum SendBoundary {
844 /// Staff can see the draft requires approval grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
845 DraftRequiresApproval,
846 /// Staff can see the ready for approved send grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
847 ReadyForApprovedSend,
848 /// Staff can see the suppressed until consent grooming state during grooming scheduling, estimate, history, rebooking, reminder, or review work.
849 SuppressedUntilConsent,
850 }
851
852 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
853 /// Grooming reminder plan that separates message purpose from send approval.
854 pub struct Plan {
855 /// Customer whose grooming reminder, deposit review, or rebooking packet is being prepared.
856 pub customer_id: CustomerId,
857 /// Reminder purpose that controls whether the draft is confirmation, prep, same-day, or cadence follow-up copy.
858 pub kind: Kind,
859 boundary: SendBoundary,
860 }
861
862 impl Plan {
863 /// Returns the customer-message send gate value used by grooming schedule/rebooking review.
864 pub const fn send_boundary(&self) -> SendBoundary {
865 self.boundary
866 }
867
868 /// Returns the customer-message approval gate required before this grooming reminder is sent.
869 pub const fn customer_message_gate(&self) -> Option<crate::policy::ReviewGate> {
870 match self.boundary {
871 SendBoundary::DraftRequiresApproval => {
872 Some(crate::policy::ReviewGate::CustomerMessageApproval)
873 }
874 SendBoundary::ReadyForApprovedSend | SendBoundary::SuppressedUntilConsent => None,
875 }
876 }
877 }
878
879 #[derive(Debug, Clone, Default)]
880 /// Grooming policy object that turns local rules into staff review decisions.
881 pub struct Policy;
882
883 impl Policy {
884 /// Builds a grooming reminder plan from customer consent and reminder purpose.
885 pub const fn plan(&self, customer_id: CustomerId, kind: Kind, consent: Consent) -> Plan {
886 let boundary = match consent {
887 Consent::Granted => SendBoundary::DraftRequiresApproval,
888 Consent::NotGranted => SendBoundary::SuppressedUntilConsent,
889 };
890 Plan {
891 customer_id,
892 kind,
893 boundary,
894 }
895 }
896 }
897}
898
899#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Builder)]
900/// Location grooming ruleset tying calendar assignment, estimate policy, no-show rules, rebooking cadence, reminders, and history retention together.
901pub struct Contract {
902 /// Calendar-assignment rule staff honor before drafting or reviewing grooming work.
903 pub calendar: calendar::Policy,
904 #[builder(default)]
905 /// Breed/coat duration estimates staff use for groomer-calendar planning.
906 pub time_estimates: Vec<breed_coat::TimeEstimate>,
907 /// No-show rule that controls whether repeat history creates deposit or manager review.
908 pub no_show: no_show::Rule,
909 /// Rebooking cadence staff use when preparing due or overdue grooming prompts.
910 pub rebooking: rebooking::Cadence,
911 #[builder(default)]
912 /// Reminder timing options that can be drafted only through customer-message review.
913 pub reminders: Vec<reminder::Rule>,
914 /// No-show and late-cancel history staff review before choosing a rebooking path.
915 pub history: HistoryRequirement,
916}
917
918impl Contract {
919 /// Reports whether prior no-shows should trigger a deposit or manager review before rebooking.
920 pub fn requires_deposit_after_no_show(&self) -> bool {
921 matches!(
922 self.no_show,
923 no_show::Rule::RequireDepositForRebooking | no_show::Rule::ManagerReviewBeforeRebooking
924 )
925 }
926 /// Builds representative PetSuites-style grooming rules for docs/tests without claiming they are live policy.
927 pub fn standard_petsuites() -> Self {
928 Self::builder()
929 .calendar(calendar::Policy::GroomerSpecific)
930 .time_estimates(vec![breed_coat::TimeEstimate::new(
931 breed_coat::BreedCategory::Doodle,
932 breed_coat::CoatCondition::Matted,
933 AppointmentMinutes::try_new(180).unwrap(),
934 )])
935 .no_show(no_show::Rule::RequireDepositForRebooking)
936 .rebooking(rebooking::Cadence::EveryWeeks(
937 rebooking::CadenceWeeks::try_new(6).unwrap(),
938 ))
939 .reminders(vec![
940 reminder::Rule::FortyEightHoursBefore,
941 reminder::Rule::MorningOf,
942 ])
943 .history(HistoryRequirement::KeepStyleNotesAndPhotos)
944 .build()
945 }
946}
947
948/// Appointment-owned public vocabulary for grooming service requests.
949pub mod appointment {
950 pub use super::{EstimationRequest as Request, Service};
951}
952
953/// Duration-estimate decision vocabulary.
954pub mod duration_estimate {
955 pub use super::{
956 AppointmentMinutes, AppointmentMinutesError, DurationEstimate, EstimateBasis,
957 EstimateConfidence, EstimationPolicy as Policy, ReviewRequirement,
958 };
959}