Skip to main content

turnframe_test/workflows/claim/
command.rs

1//! Typed claim commands, events and model-facing argument shapes.
2//!
3//! The interesting line here is that [`ClaimCommand::ReviseProposedField`] and
4//! [`ClaimCommand::AcceptProposal`] are two commands with two operation keys
5//! and two policies. Editing a proposed value and answering the review are
6//! different acts, and keeping them apart in the command vocabulary is what
7//! makes them distinguishable everywhere downstream: in the plan, in the
8//! journal, in the receipt and in the replay record.
9
10use chrono::NaiveDate;
11use schemars::JsonSchema;
12use serde::{Deserialize, Serialize};
13use turnframe_core::ids::AttachmentId;
14use turnframe_core::operation::Money;
15
16use crate::workflows::claim::state::{ClaimField, ProposedField, RecordedField};
17
18/// Keys of the operations the interpreter may propose.
19///
20/// [`operations`] deliberately has no key for proposing fields: extraction is
21/// something the application does to a document, not something a user asks for
22/// in a sentence. The command exists, the operation does not, so the
23/// interpreter cannot invent a reading of a document that nobody performed.
24pub mod operations {
25    /// Start an claim case.
26    pub const CREATE_DRAFT: &str = "claim.create_draft";
27    /// Set the hand-typed reference, which no proposal covers.
28    pub const SET_REFERENCE: &str = "claim.set_reference";
29    /// Attach the document.
30    pub const ATTACH_RECEIPT: &str = "claim.attach_receipt";
31    /// Change one proposed value while the review is open.
32    pub const REVISE_PROPOSED_FIELD: &str = "claim.revise_proposed_field";
33    /// Accept the proposal: the proposed values become the record.
34    pub const ACCEPT_PROPOSAL: &str = "claim.accept_proposal";
35    /// Throw the reading away and keep the document.
36    pub const ABANDON_REVIEW: &str = "claim.abandon_review";
37    /// Throw the document away.
38    pub const DISCARD_RECEIPT: &str = "claim.discard_receipt";
39}
40
41/// One typed claim command.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44#[non_exhaustive]
45pub enum ClaimCommand {
46    /// Bring the case into existence.
47    CreateDraft,
48    /// Set the hand-typed reference.
49    SetReference {
50        /// The reference.
51        value: String,
52    },
53    /// Record that a document arrived.
54    AttachReceipt {
55        /// The document.
56        attachment_id: AttachmentId,
57    },
58    /// Offer values read from the attached document for review.
59    ///
60    /// Server-issued: it carries what an extractor found, so its origin is an
61    /// internal policy or a verified callback, never a user's sentence.
62    ProposeFields {
63        /// The document the values were read from.
64        attachment_id: AttachmentId,
65        /// What was read.
66        fields: Vec<ProposedField>,
67    },
68    /// Change one proposed value while the review is open.
69    ///
70    /// Not an answer to the review: the card stays open, the proposal stays a
71    /// proposal, and the value is marked as edited by a human.
72    ReviseProposedField {
73        /// Which field.
74        field: ClaimField,
75        /// The value the user wants instead.
76        value: String,
77    },
78    /// Answer the review by accepting it: the proposal becomes the record.
79    AcceptProposal,
80    /// Answer the review by throwing the reading away, keeping the document.
81    AbandonReview,
82    /// Throw the document away without recording anything.
83    DiscardReceipt,
84}
85
86/// One committed claim event.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89#[non_exhaustive]
90pub enum ClaimEvent {
91    /// The case came into existence.
92    DraftCreated,
93    /// The hand-typed reference changed.
94    ReferenceSet {
95        /// The new reference.
96        value: String,
97    },
98    /// A document arrived.
99    ReceiptAttached {
100        /// The document.
101        attachment_id: AttachmentId,
102    },
103    /// Values were read out of the document and offered for review.
104    FieldsProposed {
105        /// The document they were read from.
106        attachment_id: AttachmentId,
107        /// What was read.
108        fields: Vec<ProposedField>,
109    },
110    /// The user changed one proposed value.
111    ProposedFieldRevised {
112        /// Which field.
113        field: ClaimField,
114        /// What was proposed before, when the field had a value.
115        #[serde(default, skip_serializing_if = "Option::is_none")]
116        previous: Option<String>,
117        /// What the user put there.
118        value: String,
119    },
120    /// The review was accepted and the values became the record.
121    ProposalAccepted {
122        /// The document the values came from.
123        attachment_id: AttachmentId,
124        /// What was recorded.
125        fields: Vec<RecordedField>,
126    },
127    /// The reading was thrown away; the document stayed.
128    ProposalAbandoned {
129        /// The document whose reading was thrown away.
130        attachment_id: AttachmentId,
131    },
132    /// The document was thrown away.
133    ReceiptDiscarded,
134}
135
136impl ClaimEvent {
137    /// The stable event type label stored on the committed event.
138    #[must_use]
139    pub const fn event_type(&self) -> &'static str {
140        match self {
141            Self::DraftCreated => "claim.draft_created",
142            Self::ReferenceSet { .. } => "claim.reference_set",
143            Self::ReceiptAttached { .. } => "claim.receipt_attached",
144            Self::FieldsProposed { .. } => "claim.fields_proposed",
145            Self::ProposedFieldRevised { .. } => "claim.proposed_field_revised",
146            Self::ProposalAccepted { .. } => "claim.proposal_accepted",
147            Self::ProposalAbandoned { .. } => "claim.proposal_abandoned",
148            Self::ReceiptDiscarded => "claim.receipt_discarded",
149        }
150    }
151}
152
153/// Arguments of [`operations::SET_REFERENCE`].
154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
155#[serde(deny_unknown_fields)]
156pub struct ReferenceArgs {
157    /// The reference to store.
158    pub value: String,
159}
160
161/// Arguments of [`operations::ATTACH_RECEIPT`].
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
163#[serde(deny_unknown_fields)]
164pub struct AttachArgs {
165    /// The document that arrived with the turn.
166    pub attachment_id: String,
167}
168
169/// Arguments of [`operations::REVISE_PROPOSED_FIELD`].
170#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
171#[serde(deny_unknown_fields)]
172pub struct ReviseArgs {
173    /// Which proposed field the user is correcting.
174    pub field: ClaimField,
175    /// The merchant's name, when that is the field.
176    #[serde(default)]
177    pub merchant: Option<String>,
178    /// The total, when that is the field.
179    #[serde(default)]
180    pub total: Option<Money>,
181    /// The document's date, when that is the field.
182    #[serde(default)]
183    pub date: Option<NaiveDate>,
184}