turnframe_test/workflows/claim/
model.rs1use turnframe_core::ids::AttachmentId;
4
5use crate::explore::{SimulatedTransition, WorkflowModel};
6use crate::workflows::claim::command::{ClaimCommand, ClaimEvent};
7use crate::workflows::claim::definition::ClaimWorkflow;
8use crate::workflows::claim::state::{
9 ClaimField, ClaimOutcome, ClaimState, ClaimStatus, Proposal, ProposedField,
10};
11use crate::workflows::simulate;
12
13pub const SAMPLE_ATTACHMENT: &str = "att-receipt-1";
15
16pub const SAMPLE_REFERENCE: &str = "PO-2026-0042";
18
19pub const OTHER_REFERENCE: &str = "PO-2026-0043";
21
22pub const EXTRACTED_MERCHANT: &str = "Hotel Tejo";
24
25pub const EXTRACTED_TOTAL: &str = "125000";
27
28pub const EXTRACTED_DATE: &str = "2026-09-01";
30
31pub const CORRECTED_MERCHANT: &str = "Hotel Tejo Lisboa";
33
34#[must_use]
36pub fn sample_attachment() -> AttachmentId {
37 AttachmentId::from(SAMPLE_ATTACHMENT)
38}
39
40fn proposed(field: ClaimField, value: &str) -> ProposedField {
41 ProposedField {
42 field,
43 value: value.to_owned(),
44 edited: false,
45 }
46}
47
48#[must_use]
50pub fn complete_proposal() -> Proposal {
51 Proposal::new(
52 sample_attachment(),
53 vec![
54 proposed(ClaimField::Merchant, EXTRACTED_MERCHANT),
55 proposed(ClaimField::Total, EXTRACTED_TOTAL),
56 proposed(ClaimField::ReceiptDate, EXTRACTED_DATE),
57 ],
58 )
59}
60
61#[must_use]
63pub fn partial_proposal() -> Proposal {
64 Proposal::new(
65 sample_attachment(),
66 vec![
67 proposed(ClaimField::Merchant, EXTRACTED_MERCHANT),
68 proposed(ClaimField::Total, EXTRACTED_TOTAL),
69 ],
70 )
71}
72
73#[must_use]
75pub fn awaiting_extraction() -> ClaimState {
76 ClaimState {
77 attachment: Some(sample_attachment()),
78 ..ClaimState::default()
79 }
80}
81
82#[must_use]
84pub fn under_review(proposal: Proposal) -> ClaimState {
85 ClaimState {
86 proposal: Some(proposal),
87 ..awaiting_extraction()
88 }
89}
90
91#[must_use]
96pub fn under_review_with_reference(proposal: Proposal, reference: &str) -> ClaimState {
97 ClaimState {
98 reference: Some(reference.to_owned()),
99 ..under_review(proposal)
100 }
101}
102
103#[must_use]
105pub fn abandoned() -> ClaimState {
106 ClaimState {
107 abandoned_from: Some(sample_attachment()),
108 ..awaiting_extraction()
109 }
110}
111
112#[derive(Debug, Clone, Copy, Default)]
119#[non_exhaustive]
120pub struct ClaimModel {
121 pub workflow: ClaimWorkflow,
123}
124
125impl ClaimModel {
126 #[must_use]
128 pub const fn new() -> Self {
129 Self {
130 workflow: ClaimWorkflow::new(),
131 }
132 }
133}
134
135impl WorkflowModel<ClaimWorkflow> for ClaimModel {
136 fn initial_states(&self) -> Vec<Option<ClaimState>> {
137 vec![None]
138 }
139
140 fn candidate_commands(&self, state: Option<&ClaimState>) -> Vec<ClaimCommand> {
141 let Some(state) = state else {
142 return vec![ClaimCommand::CreateDraft];
143 };
144 if state.status != ClaimStatus::Draft {
145 return Vec::new();
146 }
147 let mut commands = Vec::new();
148 if state.reference.is_none() {
149 commands.push(ClaimCommand::SetReference {
150 value: SAMPLE_REFERENCE.to_owned(),
151 });
152 }
153 match (state.attachment.as_ref(), state.proposal.as_ref()) {
154 (None, _) => {
155 commands.push(ClaimCommand::AttachReceipt {
156 attachment_id: sample_attachment(),
157 });
158 commands.push(ClaimCommand::AcceptProposal);
160 }
161 (Some(_), None) => {
162 for proposal in [complete_proposal(), partial_proposal()] {
163 commands.push(ClaimCommand::ProposeFields {
164 attachment_id: proposal.attachment_id.clone(),
165 fields: proposal.fields.clone(),
166 });
167 }
168 commands.push(ClaimCommand::AbandonReview);
170 }
171 (Some(_), Some(proposal)) => {
172 if proposal
173 .field(ClaimField::Merchant)
174 .is_some_and(|field| field.value != CORRECTED_MERCHANT)
175 {
176 commands.push(ClaimCommand::ReviseProposedField {
177 field: ClaimField::Merchant,
178 value: CORRECTED_MERCHANT.to_owned(),
179 });
180 }
181 for missing in proposal.missing() {
184 commands.push(ClaimCommand::ReviseProposedField {
185 field: missing,
186 value: EXTRACTED_DATE.to_owned(),
187 });
188 }
189 commands.push(ClaimCommand::AcceptProposal);
190 commands.push(ClaimCommand::AbandonReview);
191 commands.push(ClaimCommand::ProposeFields {
193 attachment_id: sample_attachment(),
194 fields: complete_proposal().fields,
195 });
196 }
197 }
198 commands.push(ClaimCommand::DiscardReceipt);
199 commands.push(ClaimCommand::SetReference {
201 value: " ".to_owned(),
202 });
203 commands
204 }
205
206 fn simulate(
207 &self,
208 state: Option<&ClaimState>,
209 command: &ClaimCommand,
210 ) -> SimulatedTransition<ClaimState, ClaimEvent> {
211 simulate(&self.workflow, state, command)
212 }
213
214 fn declared_outcomes(&self) -> Vec<ClaimOutcome> {
215 vec![ClaimOutcome::Recorded, ClaimOutcome::Discarded]
216 }
217}