Skip to main content

app/
crm_retention.rs

1use chrono::{DateTime, Utc};
2use domain::{entities, message, policy, source};
3use nutype::nutype;
4use serde::{Deserialize, Serialize};
5
6use crate::checkout_completion;
7
8#[nutype(
9    sanitize(trim),
10    validate(not_empty, len_char_max = 1200),
11    derive(
12        Debug,
13        Clone,
14        PartialEq,
15        Eq,
16        PartialOrd,
17        Ord,
18        Hash,
19        Serialize,
20        Deserialize
21    )
22)]
23pub struct EvidenceSummary(String);
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
26/// Decision taxonomy for source grounded reason code in the retention follow-up workflow; each value carries operational meaning for source-grounded routing and review.
27pub enum SourceGroundedReasonCode {
28    /// Uses completed boarding stay as source-grounded evidence for the deterministic decision.
29    CompletedBoardingStay,
30    /// Uses completed daycare visit as source-grounded evidence for the deterministic decision.
31    CompletedDaycareVisit,
32    /// Uses completed grooming visit as source-grounded evidence for the deterministic decision.
33    CompletedGroomingVisit,
34    /// Uses customer asked about future stay as source-grounded evidence for the deterministic decision.
35    CustomerAskedAboutFutureStay,
36    /// Uses pet eligible for recurring care as source-grounded evidence for the deterministic decision.
37    PetEligibleForRecurringCare,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
41/// Decision taxonomy for opportunity kind in the retention follow-up workflow; each value carries operational meaning for source-grounded routing and review.
42pub enum OpportunityKind {
43    /// Represents next boarding stay in the retention follow-up decision model so the app can choose the correct evidence, review, or draft path without taking live action.
44    NextBoardingStay,
45    /// Represents recurring daycare in the retention follow-up decision model so the app can choose the correct evidence, review, or draft path without taking live action.
46    RecurringDaycare,
47    /// Represents grooming rebook in the retention follow-up decision model so the app can choose the correct evidence, review, or draft path without taking live action.
48    GroomingRebook,
49    /// Represents training consult in the retention follow-up decision model so the app can choose the correct evidence, review, or draft path without taking live action.
50    TrainingConsult,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
54/// Decision taxonomy for consent status in the retention follow-up workflow; each value carries operational meaning for source-grounded routing and review.
55pub enum ConsentStatus {
56    /// Labels work as granted for queueing, review, and downstream agent context.
57    Granted,
58    /// Labels work as missing for queueing, review, and downstream agent context.
59    Missing,
60    /// Labels work as opted out for queueing, review, and downstream agent context.
61    OptedOut,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65/// Decision taxonomy for eligibility reason in the retention follow-up workflow; each value carries operational meaning for source-grounded routing and review.
66pub enum EligibilityReason {
67    /// Explains that the workflow is source grounded retention opportunity when deciding whether an agent draft is allowed.
68    SourceGroundedRetentionOpportunity,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72/// Decision taxonomy for ineligibility reason in the retention follow-up workflow; each value carries operational meaning for source-grounded routing and review.
73pub enum IneligibilityReason {
74    /// Explains that the workflow is checkout not staff verified when deciding whether an agent draft is allowed.
75    CheckoutNotStaffVerified,
76    /// Explains that the workflow is no source grounded opportunity when deciding whether an agent draft is allowed.
77    NoSourceGroundedOpportunity,
78    /// Explains that the workflow is contact permission not source grounded when deciding whether an agent draft is allowed.
79    ContactPermissionNotSourceGrounded,
80    /// Explains that the workflow is contact consent missing when deciding whether an agent draft is allowed.
81    ContactConsentMissing,
82    /// Explains that the workflow is contact opted out when deciding whether an agent draft is allowed.
83    ContactOptedOut,
84    /// Explains that the workflow is preferred channel not allowed when deciding whether an agent draft is allowed.
85    PreferredChannelNotAllowed,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
89/// Outcome of the deterministic contact-safety check for retention follow-up.
90pub enum FollowUpEligibility {
91    /// Source-derived Reason retained for audit, reviewer explanation, or agent context; callers must not invent or mutate it.
92    Eligible {
93        /// Reason carried by this variant.
94        reason: EligibilityReason,
95    },
96    /// Source-derived Reason retained for audit, reviewer explanation, or agent context; callers must not invent or mutate it.
97    Ineligible {
98        /// Reason carried by this variant.
99        reason: IneligibilityReason,
100    },
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
104/// Review-safe agent tasks allowed to save staff time without crossing mutation or send boundaries.
105pub enum SafeAgentAction {
106    /// Allows agents to summarize retention evidence for staff review without mutating records or contacting customers.
107    SummarizeRetentionEvidence,
108    /// Allows agents to create internal staff review task for staff review without mutating records or contacting customers.
109    CreateInternalStaffReviewTask,
110    /// Allows agents to draft customer follow up for review for staff review without mutating records or contacting customers.
111    DraftCustomerFollowUpForReview,
112    /// Allows agents to record follow up outcome evidence for staff review without mutating records or contacting customers.
113    RecordFollowUpOutcomeEvidence,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
117/// Actions the agent must never perform without a human/operator system of record.
118pub enum BlockedAction {
119    /// Blocks agents from send customer message until staff or the system of record performs the action.
120    SendCustomerMessage,
121    /// Blocks agents from mutate provider or pms record until staff or the system of record performs the action.
122    MutateProviderOrPmsRecord,
123    /// Blocks agents from move refund discount or payment until staff or the system of record performs the action.
124    MoveRefundDiscountOrPayment,
125    /// Blocks agents from auto apply discount until staff or the system of record performs the action.
126    AutoApplyDiscount,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
130/// Decision taxonomy for follow up outcome in the retention follow-up workflow; each value carries operational meaning for source-grounded routing and review.
131pub enum FollowUpOutcome {
132    /// Records a booked next stay result so follow-up impact is auditable.
133    BookedNextStay,
134    /// Records a interested needs staff call result so follow-up impact is auditable.
135    InterestedNeedsStaffCall,
136    /// Records a not interested result so follow-up impact is auditable.
137    NotInterested,
138    /// Records a no response result so follow-up impact is auditable.
139    NoResponse,
140    /// Records a suppressed by staff result so follow-up impact is auditable.
141    SuppressedByStaff,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
145/// Opportunity evidence carried by the retention follow-up workflow; it turns source-grounded visit evidence into safe follow-up drafts without sending customer messages automatically.
146pub struct OpportunityEvidence {
147    reason_code: SourceGroundedReasonCode,
148    summary: EvidenceSummary,
149    provenance: source::Provenance,
150}
151
152impl OpportunityEvidence {
153    /// Returns the reason code source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
154    pub const fn reason_code(&self) -> SourceGroundedReasonCode {
155        self.reason_code
156    }
157
158    /// Returns the summary source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
159    pub const fn summary(&self) -> &EvidenceSummary {
160        &self.summary
161    }
162
163    /// Returns the provenance source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
164    pub const fn provenance(&self) -> &source::Provenance {
165        &self.provenance
166    }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
170/// Retention opportunity carried by the retention follow-up workflow; it turns source-grounded visit evidence into safe follow-up drafts without sending customer messages automatically.
171pub struct RetentionOpportunity {
172    kind: OpportunityKind,
173    evidence: OpportunityEvidence,
174}
175
176impl RetentionOpportunity {
177    /// Returns the kind source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
178    pub const fn kind(&self) -> OpportunityKind {
179        self.kind
180    }
181
182    /// Returns the evidence source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
183    pub const fn evidence(&self) -> &OpportunityEvidence {
184        &self.evidence
185    }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
189/// Contact permission carried by the retention follow-up workflow; it turns source-grounded visit evidence into safe follow-up drafts without sending customer messages automatically.
190pub struct ContactPermission {
191    preferred_channel: message::Channel,
192    #[builder(default)]
193    allowed_channels: Vec<message::Channel>,
194    marketing_consent: ConsentStatus,
195    transactional_consent: ConsentStatus,
196    #[builder(default)]
197    source_record_refs: Vec<source::RecordRef>,
198}
199
200impl ContactPermission {
201    /// Returns the preferred channel source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
202    pub const fn preferred_channel(&self) -> message::Channel {
203        self.preferred_channel
204    }
205
206    /// Returns the allowed channels source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
207    pub fn allowed_channels(&self) -> &[message::Channel] {
208        &self.allowed_channels
209    }
210
211    /// Returns the marketing consent source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
212    pub const fn marketing_consent(&self) -> ConsentStatus {
213        self.marketing_consent
214    }
215
216    /// Returns the transactional consent source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
217    pub const fn transactional_consent(&self) -> ConsentStatus {
218        self.transactional_consent
219    }
220
221    /// Returns the source record refs source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
222    pub fn source_record_refs(&self) -> &[source::RecordRef] {
223        &self.source_record_refs
224    }
225
226    /// Reports whether the retention follow-up workflow satisfies the has source evidence safety condition.
227    pub fn has_source_evidence(&self) -> bool {
228        !self.source_record_refs.is_empty()
229    }
230
231    fn retention_draft_channel(&self) -> Option<message::Channel> {
232        if !matches!(self.marketing_consent, ConsentStatus::Granted) {
233            return None;
234        }
235        if matches!(self.preferred_channel, message::Channel::Internal) {
236            return None;
237        }
238        self.allowed_channels
239            .contains(&self.preferred_channel)
240            .then_some(self.preferred_channel)
241    }
242
243    fn denial_reason(&self) -> IneligibilityReason {
244        match self.marketing_consent {
245            ConsentStatus::Granted => IneligibilityReason::PreferredChannelNotAllowed,
246            ConsentStatus::Missing => IneligibilityReason::ContactConsentMissing,
247            ConsentStatus::OptedOut => IneligibilityReason::ContactOptedOut,
248        }
249    }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
253/// Input contract for building the workflow packet from source-grounded records.
254pub struct Request {
255    reservation_id: entities::reservation::Id,
256    customer_id: entities::CustomerId,
257    checkout_packet: checkout_completion::Packet,
258    contact_permission: ContactPermission,
259    #[builder(default)]
260    opportunities: Vec<RetentionOpportunity>,
261}
262
263impl Request {
264    /// Returns the reservation id source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
265    pub const fn reservation_id(&self) -> entities::reservation::Id {
266        self.reservation_id
267    }
268
269    /// Returns the customer id source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
270    pub const fn customer_id(&self) -> entities::CustomerId {
271        self.customer_id
272    }
273
274    /// Returns the checkout packet source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
275    pub const fn checkout_packet(&self) -> &checkout_completion::Packet {
276        &self.checkout_packet
277    }
278
279    /// Returns the contact permission source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
280    pub const fn contact_permission(&self) -> &ContactPermission {
281        &self.contact_permission
282    }
283
284    /// Returns the opportunities source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
285    pub fn opportunities(&self) -> &[RetentionOpportunity] {
286        &self.opportunities
287    }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
291/// Staff-facing packet that explains the evidence, draft limits, and required review gates.
292pub struct StaffReviewPacket {
293    reservation_id: entities::reservation::Id,
294    customer_id: entities::CustomerId,
295    eligibility: FollowUpEligibility,
296    draft_channel: Option<message::Channel>,
297    staff_evidence: Vec<OpportunityEvidence>,
298    required_review_gates: Vec<policy::ReviewGate>,
299}
300
301impl StaffReviewPacket {
302    /// Returns the reservation id source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
303    pub const fn reservation_id(&self) -> entities::reservation::Id {
304        self.reservation_id
305    }
306
307    /// Returns the customer id source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
308    pub const fn customer_id(&self) -> entities::CustomerId {
309        self.customer_id
310    }
311
312    /// Returns the eligibility source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
313    pub const fn eligibility(&self) -> FollowUpEligibility {
314        self.eligibility
315    }
316
317    /// Returns the draft channel source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
318    pub const fn draft_channel(&self) -> Option<message::Channel> {
319        self.draft_channel
320    }
321
322    /// Returns the staff evidence source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
323    pub fn staff_evidence(&self) -> &[OpportunityEvidence] {
324        &self.staff_evidence
325    }
326
327    /// Returns the required review gates source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
328    pub fn required_review_gates(&self) -> &[policy::ReviewGate] {
329        &self.required_review_gates
330    }
331
332    /// Reports whether the retention follow-up workflow satisfies the requires human review safety condition.
333    pub fn requires_human_review(&self) -> bool {
334        !self.required_review_gates.is_empty()
335    }
336}
337
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339/// Reviewable packet handed to staff or agents with deterministic gates already applied.
340pub struct Packet {
341    reservation_id: entities::reservation::Id,
342    customer_id: entities::CustomerId,
343    eligibility: FollowUpEligibility,
344    draft_channel: Option<message::Channel>,
345    review_packet: StaffReviewPacket,
346    required_review_gates: Vec<policy::ReviewGate>,
347    safe_agent_actions: Vec<SafeAgentAction>,
348    blocked_actions: Vec<BlockedAction>,
349    source_record_refs: Vec<source::RecordRef>,
350}
351
352impl Packet {
353    /// Returns the reservation id source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
354    pub const fn reservation_id(&self) -> entities::reservation::Id {
355        self.reservation_id
356    }
357
358    /// Returns the customer id source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
359    pub const fn customer_id(&self) -> entities::CustomerId {
360        self.customer_id
361    }
362
363    /// Returns the eligibility source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
364    pub const fn eligibility(&self) -> FollowUpEligibility {
365        self.eligibility
366    }
367
368    /// Returns the draft channel source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
369    pub const fn draft_channel(&self) -> Option<message::Channel> {
370        self.draft_channel
371    }
372
373    /// Returns the review packet source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
374    pub const fn review_packet(&self) -> &StaffReviewPacket {
375        &self.review_packet
376    }
377
378    /// Returns the required review gates source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
379    pub fn required_review_gates(&self) -> &[policy::ReviewGate] {
380        &self.required_review_gates
381    }
382
383    /// Returns the safe agent actions source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
384    pub fn safe_agent_actions(&self) -> &[SafeAgentAction] {
385        &self.safe_agent_actions
386    }
387
388    /// Returns the blocked actions source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
389    pub fn blocked_actions(&self) -> &[BlockedAction] {
390        &self.blocked_actions
391    }
392
393    /// Returns the source record refs source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
394    pub fn source_record_refs(&self) -> &[source::RecordRef] {
395        &self.source_record_refs
396    }
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
400/// Workflow carried by the retention follow-up workflow; it turns source-grounded visit evidence into safe follow-up drafts without sending customer messages automatically.
401pub struct Workflow;
402
403impl Workflow {
404    /// Builds evaluate for the retention follow-up workflow contract from validated source facts while preserving review gates and draft-only side-effect boundaries.
405    pub fn evaluate(request: Request) -> Packet {
406        let draft_channel = request.contact_permission.retention_draft_channel();
407        let eligibility = eligibility_for(&request, draft_channel);
408        let required_review_gates = required_review_gates_for(eligibility);
409        let staff_evidence = request
410            .opportunities
411            .iter()
412            .map(|opportunity| opportunity.evidence.clone())
413            .collect::<Vec<_>>();
414        let review_packet = StaffReviewPacket {
415            reservation_id: request.reservation_id,
416            customer_id: request.customer_id,
417            eligibility,
418            draft_channel,
419            staff_evidence,
420            required_review_gates: required_review_gates.clone(),
421        };
422        let safe_agent_actions = safe_agent_actions_for(eligibility);
423        let blocked_actions = blocked_actions_for();
424        let mut source_record_refs = vec![source::RecordRef::from_provenance(
425            request.checkout_packet.provenance(),
426        )];
427        source_record_refs.extend(
428            request
429                .contact_permission
430                .source_record_refs()
431                .iter()
432                .cloned(),
433        );
434
435        Packet {
436            reservation_id: request.reservation_id,
437            customer_id: request.customer_id,
438            eligibility,
439            draft_channel,
440            review_packet,
441            required_review_gates,
442            safe_agent_actions,
443            blocked_actions,
444            source_record_refs,
445        }
446    }
447}
448
449fn eligibility_for(
450    request: &Request,
451    draft_channel: Option<message::Channel>,
452) -> FollowUpEligibility {
453    if !matches!(
454        request.checkout_packet.completion_status(),
455        checkout_completion::CompletionStatus::StaffVerifiedCheckout
456    ) {
457        return FollowUpEligibility::Ineligible {
458            reason: IneligibilityReason::CheckoutNotStaffVerified,
459        };
460    }
461
462    if request.opportunities.is_empty() {
463        return FollowUpEligibility::Ineligible {
464            reason: IneligibilityReason::NoSourceGroundedOpportunity,
465        };
466    }
467
468    if draft_channel.is_none() {
469        return FollowUpEligibility::Ineligible {
470            reason: request.contact_permission.denial_reason(),
471        };
472    }
473
474    if !request.contact_permission.has_source_evidence() {
475        return FollowUpEligibility::Ineligible {
476            reason: IneligibilityReason::ContactPermissionNotSourceGrounded,
477        };
478    }
479
480    FollowUpEligibility::Eligible {
481        reason: EligibilityReason::SourceGroundedRetentionOpportunity,
482    }
483}
484
485fn required_review_gates_for(eligibility: FollowUpEligibility) -> Vec<policy::ReviewGate> {
486    match eligibility {
487        FollowUpEligibility::Eligible { .. } => vec![policy::ReviewGate::CustomerMessageApproval],
488        FollowUpEligibility::Ineligible { .. } => vec![policy::ReviewGate::ManagerApproval],
489    }
490}
491
492fn safe_agent_actions_for(eligibility: FollowUpEligibility) -> Vec<SafeAgentAction> {
493    let mut actions = vec![
494        SafeAgentAction::SummarizeRetentionEvidence,
495        SafeAgentAction::CreateInternalStaffReviewTask,
496        SafeAgentAction::RecordFollowUpOutcomeEvidence,
497    ];
498    if matches!(eligibility, FollowUpEligibility::Eligible { .. }) {
499        actions.push(SafeAgentAction::DraftCustomerFollowUpForReview);
500    }
501    actions
502}
503
504fn blocked_actions_for() -> Vec<BlockedAction> {
505    vec![
506        BlockedAction::AutoApplyDiscount,
507        BlockedAction::MoveRefundDiscountOrPayment,
508        BlockedAction::MutateProviderOrPmsRecord,
509        BlockedAction::SendCustomerMessage,
510    ]
511}
512
513#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
514/// Outcome record carried by the retention follow-up workflow; it turns source-grounded visit evidence into safe follow-up drafts without sending customer messages automatically.
515pub struct OutcomeRecord {
516    reservation_id: entities::reservation::Id,
517    customer_id: entities::CustomerId,
518    recorded_by: entities::ActorRef,
519    recorded_at: DateTime<Utc>,
520    outcome: FollowUpOutcome,
521    source_provenance: source::Provenance,
522    #[builder(default)]
523    evidence: Vec<OpportunityEvidence>,
524}
525
526impl OutcomeRecord {
527    /// Returns the reservation id source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
528    pub const fn reservation_id(&self) -> entities::reservation::Id {
529        self.reservation_id
530    }
531
532    /// Returns the customer id source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
533    pub const fn customer_id(&self) -> entities::CustomerId {
534        self.customer_id
535    }
536
537    /// Returns the recorded by source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
538    pub const fn recorded_by(&self) -> &entities::ActorRef {
539        &self.recorded_by
540    }
541
542    /// Returns the recorded at source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
543    pub const fn recorded_at(&self) -> DateTime<Utc> {
544        self.recorded_at
545    }
546
547    /// Returns the outcome source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
548    pub const fn outcome(&self) -> FollowUpOutcome {
549        self.outcome
550    }
551
552    /// Returns the source provenance source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
553    pub const fn source_provenance(&self) -> &source::Provenance {
554        &self.source_provenance
555    }
556
557    /// Returns the evidence source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
558    pub fn evidence(&self) -> &[OpportunityEvidence] {
559        &self.evidence
560    }
561
562    /// Returns the records staff evidence only source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
563    pub const fn records_staff_evidence_only(&self) -> bool {
564        true
565    }
566
567    /// Returns the blocked actions source evidence carried by this retention follow-up workflow artifact without changing provider, customer, payment, or schedule state.
568    pub fn blocked_actions(&self) -> Vec<BlockedAction> {
569        blocked_actions_for()
570    }
571}