1use chrono::{DateTime, NaiveDate, Utc};
7use rust_decimal::Decimal;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ObjectInstance {
15 pub object_id: Uuid,
17 pub object_type_id: String,
19 pub external_id: String,
21 pub current_state: String,
23 pub created_at: DateTime<Utc>,
25 pub completed_at: Option<DateTime<Utc>>,
27 pub company_code: String,
29 pub attributes: HashMap<String, ObjectAttributeValue>,
31 pub is_anomaly: bool,
33 pub anomaly_type: Option<String>,
35}
36
37impl ObjectInstance {
38 pub fn new(object_type_id: &str, external_id: &str, company_code: &str) -> Self {
40 Self {
41 object_id: Uuid::new_v4(),
42 object_type_id: object_type_id.into(),
43 external_id: external_id.into(),
44 current_state: "created".into(),
45 created_at: Utc::now(),
46 completed_at: None,
47 company_code: company_code.into(),
48 attributes: HashMap::new(),
49 is_anomaly: false,
50 anomaly_type: None,
51 }
52 }
53
54 pub fn with_id(mut self, id: Uuid) -> Self {
56 self.object_id = id;
57 self
58 }
59
60 pub fn with_state(mut self, state: &str) -> Self {
62 self.current_state = state.into();
63 self
64 }
65
66 pub fn with_created_at(mut self, created_at: DateTime<Utc>) -> Self {
68 self.created_at = created_at;
69 self
70 }
71
72 pub fn with_attribute(mut self, key: &str, value: ObjectAttributeValue) -> Self {
74 self.attributes.insert(key.into(), value);
75 self
76 }
77
78 pub fn complete(&mut self, terminal_state: &str) {
80 self.current_state = terminal_state.into();
81 self.completed_at = Some(Utc::now());
82 }
83
84 pub fn transition(&mut self, new_state: &str) {
86 self.current_state = new_state.into();
87 }
88
89 pub fn mark_anomaly(&mut self, anomaly_type: &str) {
91 self.is_anomaly = true;
92 self.anomaly_type = Some(anomaly_type.into());
93 }
94
95 pub fn is_completed(&self) -> bool {
97 self.completed_at.is_some()
98 }
99
100 pub fn get_string(&self, key: &str) -> Option<&str> {
102 match self.attributes.get(key) {
103 Some(ObjectAttributeValue::String(s)) => Some(s.as_str()),
104 _ => None,
105 }
106 }
107
108 pub fn get_decimal(&self, key: &str) -> Option<Decimal> {
110 match self.attributes.get(key) {
111 Some(ObjectAttributeValue::Decimal(d)) => Some(*d),
112 _ => None,
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119#[serde(untagged)]
120pub enum ObjectAttributeValue {
121 String(String),
123 Integer(i64),
125 Decimal(Decimal),
127 Date(NaiveDate),
129 DateTime(DateTime<Utc>),
131 Boolean(bool),
133 Reference(Uuid),
135 Null,
137}
138
139impl From<String> for ObjectAttributeValue {
140 fn from(s: String) -> Self {
141 Self::String(s)
142 }
143}
144
145impl From<&str> for ObjectAttributeValue {
146 fn from(s: &str) -> Self {
147 Self::String(s.into())
148 }
149}
150
151impl From<i64> for ObjectAttributeValue {
152 fn from(i: i64) -> Self {
153 Self::Integer(i)
154 }
155}
156
157impl From<Decimal> for ObjectAttributeValue {
158 fn from(d: Decimal) -> Self {
159 Self::Decimal(d)
160 }
161}
162
163impl From<NaiveDate> for ObjectAttributeValue {
164 fn from(d: NaiveDate) -> Self {
165 Self::Date(d)
166 }
167}
168
169impl From<DateTime<Utc>> for ObjectAttributeValue {
170 fn from(dt: DateTime<Utc>) -> Self {
171 Self::DateTime(dt)
172 }
173}
174
175impl From<bool> for ObjectAttributeValue {
176 fn from(b: bool) -> Self {
177 Self::Boolean(b)
178 }
179}
180
181impl From<Uuid> for ObjectAttributeValue {
182 fn from(id: Uuid) -> Self {
183 Self::Reference(id)
184 }
185}
186
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
189pub struct ObjectGraph {
190 pub objects: HashMap<Uuid, ObjectInstance>,
192 objects_by_type: HashMap<String, Vec<Uuid>>,
194 objects_by_external_id: HashMap<String, Uuid>,
196}
197
198impl ObjectGraph {
199 pub fn new() -> Self {
201 Self::default()
202 }
203
204 pub fn add_object(&mut self, object: ObjectInstance) {
206 let object_id = object.object_id;
207 let type_id = object.object_type_id.clone();
208 let external_id = object.external_id.clone();
209
210 self.objects.insert(object_id, object);
211
212 self.objects_by_type
213 .entry(type_id)
214 .or_default()
215 .push(object_id);
216
217 self.objects_by_external_id.insert(external_id, object_id);
218 }
219
220 pub fn get(&self, object_id: Uuid) -> Option<&ObjectInstance> {
222 self.objects.get(&object_id)
223 }
224
225 pub fn get_mut(&mut self, object_id: Uuid) -> Option<&mut ObjectInstance> {
227 self.objects.get_mut(&object_id)
228 }
229
230 pub fn get_by_external_id(&self, external_id: &str) -> Option<&ObjectInstance> {
232 self.objects_by_external_id
233 .get(external_id)
234 .and_then(|id| self.objects.get(id))
235 }
236
237 pub fn get_by_type(&self, type_id: &str) -> Vec<&ObjectInstance> {
239 self.objects_by_type
240 .get(type_id)
241 .map(|ids| ids.iter().filter_map(|id| self.objects.get(id)).collect())
242 .unwrap_or_default()
243 }
244
245 pub fn len(&self) -> usize {
247 self.objects.len()
248 }
249
250 pub fn is_empty(&self) -> bool {
252 self.objects.is_empty()
253 }
254
255 pub fn iter(&self) -> impl Iterator<Item = &ObjectInstance> {
257 self.objects.values()
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn test_object_instance_creation() {
267 let obj = ObjectInstance::new("purchase_order", "PO-001", "1000");
268 assert_eq!(obj.object_type_id, "purchase_order");
269 assert_eq!(obj.external_id, "PO-001");
270 assert_eq!(obj.company_code, "1000");
271 assert!(!obj.is_anomaly);
272 }
273
274 #[test]
275 fn test_object_graph() {
276 let mut graph = ObjectGraph::new();
277
278 let po1 = ObjectInstance::new("purchase_order", "PO-001", "1000");
279 let po2 = ObjectInstance::new("purchase_order", "PO-002", "1000");
280 let gr1 = ObjectInstance::new("goods_receipt", "GR-001", "1000");
281
282 graph.add_object(po1);
283 graph.add_object(po2);
284 graph.add_object(gr1);
285
286 assert_eq!(graph.len(), 3);
287 assert_eq!(graph.get_by_type("purchase_order").len(), 2);
288 assert_eq!(graph.get_by_type("goods_receipt").len(), 1);
289 assert!(graph.get_by_external_id("PO-001").is_some());
290 }
291}