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}