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}