use turnframe_core::ids::AttachmentId;
use crate::explore::{SimulatedTransition, WorkflowModel};
use crate::workflows::claim::command::{ClaimCommand, ClaimEvent};
use crate::workflows::claim::definition::ClaimWorkflow;
use crate::workflows::claim::state::{
ClaimField, ClaimOutcome, ClaimState, ClaimStatus, Proposal, ProposedField,
};
use crate::workflows::simulate;
pub const SAMPLE_ATTACHMENT: &str = "att-receipt-1";
pub const SAMPLE_REFERENCE: &str = "PO-2026-0042";
pub const OTHER_REFERENCE: &str = "PO-2026-0043";
pub const EXTRACTED_MERCHANT: &str = "Hotel Tejo";
pub const EXTRACTED_TOTAL: &str = "125000";
pub const EXTRACTED_DATE: &str = "2026-09-01";
pub const CORRECTED_MERCHANT: &str = "Hotel Tejo Lisboa";
#[must_use]
pub fn sample_attachment() -> AttachmentId {
AttachmentId::from(SAMPLE_ATTACHMENT)
}
fn proposed(field: ClaimField, value: &str) -> ProposedField {
ProposedField {
field,
value: value.to_owned(),
edited: false,
}
}
#[must_use]
pub fn complete_proposal() -> Proposal {
Proposal::new(
sample_attachment(),
vec![
proposed(ClaimField::Merchant, EXTRACTED_MERCHANT),
proposed(ClaimField::Total, EXTRACTED_TOTAL),
proposed(ClaimField::ReceiptDate, EXTRACTED_DATE),
],
)
}
#[must_use]
pub fn partial_proposal() -> Proposal {
Proposal::new(
sample_attachment(),
vec![
proposed(ClaimField::Merchant, EXTRACTED_MERCHANT),
proposed(ClaimField::Total, EXTRACTED_TOTAL),
],
)
}
#[must_use]
pub fn awaiting_extraction() -> ClaimState {
ClaimState {
attachment: Some(sample_attachment()),
..ClaimState::default()
}
}
#[must_use]
pub fn under_review(proposal: Proposal) -> ClaimState {
ClaimState {
proposal: Some(proposal),
..awaiting_extraction()
}
}
#[must_use]
pub fn under_review_with_reference(proposal: Proposal, reference: &str) -> ClaimState {
ClaimState {
reference: Some(reference.to_owned()),
..under_review(proposal)
}
}
#[must_use]
pub fn abandoned() -> ClaimState {
ClaimState {
abandoned_from: Some(sample_attachment()),
..awaiting_extraction()
}
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct ClaimModel {
pub workflow: ClaimWorkflow,
}
impl ClaimModel {
#[must_use]
pub const fn new() -> Self {
Self {
workflow: ClaimWorkflow::new(),
}
}
}
impl WorkflowModel<ClaimWorkflow> for ClaimModel {
fn initial_states(&self) -> Vec<Option<ClaimState>> {
vec![None]
}
fn candidate_commands(&self, state: Option<&ClaimState>) -> Vec<ClaimCommand> {
let Some(state) = state else {
return vec![ClaimCommand::CreateDraft];
};
if state.status != ClaimStatus::Draft {
return Vec::new();
}
let mut commands = Vec::new();
if state.reference.is_none() {
commands.push(ClaimCommand::SetReference {
value: SAMPLE_REFERENCE.to_owned(),
});
}
match (state.attachment.as_ref(), state.proposal.as_ref()) {
(None, _) => {
commands.push(ClaimCommand::AttachReceipt {
attachment_id: sample_attachment(),
});
commands.push(ClaimCommand::AcceptProposal);
}
(Some(_), None) => {
for proposal in [complete_proposal(), partial_proposal()] {
commands.push(ClaimCommand::ProposeFields {
attachment_id: proposal.attachment_id.clone(),
fields: proposal.fields.clone(),
});
}
commands.push(ClaimCommand::AbandonReview);
}
(Some(_), Some(proposal)) => {
if proposal
.field(ClaimField::Merchant)
.is_some_and(|field| field.value != CORRECTED_MERCHANT)
{
commands.push(ClaimCommand::ReviseProposedField {
field: ClaimField::Merchant,
value: CORRECTED_MERCHANT.to_owned(),
});
}
for missing in proposal.missing() {
commands.push(ClaimCommand::ReviseProposedField {
field: missing,
value: EXTRACTED_DATE.to_owned(),
});
}
commands.push(ClaimCommand::AcceptProposal);
commands.push(ClaimCommand::AbandonReview);
commands.push(ClaimCommand::ProposeFields {
attachment_id: sample_attachment(),
fields: complete_proposal().fields,
});
}
}
commands.push(ClaimCommand::DiscardReceipt);
commands.push(ClaimCommand::SetReference {
value: " ".to_owned(),
});
commands
}
fn simulate(
&self,
state: Option<&ClaimState>,
command: &ClaimCommand,
) -> SimulatedTransition<ClaimState, ClaimEvent> {
simulate(&self.workflow, state, command)
}
fn declared_outcomes(&self) -> Vec<ClaimOutcome> {
vec![ClaimOutcome::Recorded, ClaimOutcome::Discarded]
}
}