use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use turnframe_core::ids::AttachmentId;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ClaimField {
Merchant,
Total,
ReceiptDate,
}
impl ClaimField {
pub const ALL: [Self; 3] = [Self::Merchant, Self::Total, Self::ReceiptDate];
#[must_use]
pub const fn key(self) -> &'static str {
match self {
Self::Merchant => "merchant",
Self::Total => "total",
Self::ReceiptDate => "receipt_date",
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Merchant => "Merchant",
Self::Total => "Total (cents)",
Self::ReceiptDate => "Receipt date",
}
}
#[must_use]
pub const fn label_it(self) -> &'static str {
match self {
Self::Merchant => "Esercente",
Self::Total => "Totale (centesimi)",
Self::ReceiptDate => "Data della ricevuta",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProposedField {
pub field: ClaimField,
pub value: String,
#[serde(default)]
pub edited: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Proposal {
pub attachment_id: AttachmentId,
pub fields: Vec<ProposedField>,
}
impl Proposal {
#[must_use]
pub fn new(attachment_id: AttachmentId, fields: Vec<ProposedField>) -> Self {
let mut fields = fields;
fields.sort_by_key(|proposed| proposed.field);
Self {
attachment_id,
fields,
}
}
#[must_use]
pub fn field(&self, field: ClaimField) -> Option<&ProposedField> {
self.fields.iter().find(|proposed| proposed.field == field)
}
#[must_use]
pub fn missing(&self) -> Vec<ClaimField> {
ClaimField::ALL
.into_iter()
.filter(|field| self.field(*field).is_none())
.collect()
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.missing().is_empty()
}
#[must_use]
pub fn edited_count(&self) -> usize {
self.fields
.iter()
.filter(|proposed| proposed.edited)
.count()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RecordedField {
pub field: ClaimField,
pub value: String,
pub from_attachment: AttachmentId,
pub corrected: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ClaimStatus {
#[default]
Draft,
Recorded,
Discarded,
}
impl ClaimStatus {
#[must_use]
pub const fn is_open(self) -> bool {
matches!(self, Self::Draft)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ClaimState {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reference: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attachment: Option<AttachmentId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proposal: Option<Proposal>,
#[serde(default)]
pub recorded: Vec<RecordedField>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub abandoned_from: Option<AttachmentId>,
pub status: ClaimStatus,
}
impl ClaimState {
#[must_use]
pub fn open_obligations(&self) -> Vec<ClaimObligation> {
if !self.status.is_open() {
return Vec::new();
}
let Some(attachment) = self.attachment.as_ref() else {
return vec![ClaimObligation::AttachReceipt];
};
let Some(proposal) = self.proposal.as_ref() else {
return vec![ClaimObligation::ExtractFields {
attachment_id: attachment.clone(),
}];
};
let mut obligations: Vec<ClaimObligation> = proposal
.fields
.iter()
.map(|proposed| ClaimObligation::ReviewProposedField {
field: proposed.field,
})
.collect();
obligations.extend(
proposal
.missing()
.into_iter()
.map(|field| ClaimObligation::ProvideField { field }),
);
obligations
}
#[must_use]
pub fn recorded_value(&self, field: ClaimField) -> Option<&str> {
self.recorded
.iter()
.find(|recorded| recorded.field == field)
.map(|recorded| recorded.value.as_str())
}
#[must_use]
pub const fn is_awaiting_review(&self) -> bool {
self.proposal.is_some() && self.status.is_open()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ClaimPhase {
PreDraft,
AwaitingDocument,
Extracting,
AwaitingReview,
Recorded,
Discarded,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ClaimObligation {
AttachReceipt,
ExtractFields {
attachment_id: AttachmentId,
},
ReviewProposedField {
field: ClaimField,
},
ProvideField {
field: ClaimField,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ClaimOutcome {
Recorded,
Discarded,
}