1use chrono::{DateTime, NaiveDateTime, Utc};
10use serde::{Deserialize, Serialize};
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct BiTemporal<T> {
33 pub data: T,
35
36 pub version_id: Uuid,
38
39 pub valid_from: NaiveDateTime,
42 pub valid_to: Option<NaiveDateTime>,
44
45 pub recorded_at: DateTime<Utc>,
48 pub superseded_at: Option<DateTime<Utc>>,
50
51 pub recorded_by: String,
54 pub change_reason: Option<String>,
56 pub previous_version_id: Option<Uuid>,
58 pub change_type: TemporalChangeType,
60}
61
62impl<T> BiTemporal<T> {
63 pub fn new(data: T) -> Self {
65 let now = Utc::now();
66 Self {
67 data,
68 version_id: Uuid::new_v4(),
69 valid_from: now.naive_utc(),
70 valid_to: None,
71 recorded_at: now,
72 superseded_at: None,
73 recorded_by: String::new(),
74 change_reason: None,
75 previous_version_id: None,
76 change_type: TemporalChangeType::Original,
77 }
78 }
79
80 pub fn with_valid_time(mut self, from: NaiveDateTime, to: Option<NaiveDateTime>) -> Self {
82 self.valid_from = from;
83 self.valid_to = to;
84 self
85 }
86
87 pub fn valid_from(mut self, from: NaiveDateTime) -> Self {
89 self.valid_from = from;
90 self
91 }
92
93 pub fn valid_to(mut self, to: NaiveDateTime) -> Self {
95 self.valid_to = Some(to);
96 self
97 }
98
99 pub fn with_recorded_at(mut self, recorded_at: DateTime<Utc>) -> Self {
101 self.recorded_at = recorded_at;
102 self
103 }
104
105 pub fn with_recorded_by(mut self, recorded_by: &str) -> Self {
107 self.recorded_by = recorded_by.into();
108 self
109 }
110
111 pub fn with_change_reason(mut self, reason: &str) -> Self {
113 self.change_reason = Some(reason.into());
114 self
115 }
116
117 pub fn with_change_type(mut self, change_type: TemporalChangeType) -> Self {
119 self.change_type = change_type;
120 self
121 }
122
123 pub fn with_previous_version(mut self, previous_id: Uuid) -> Self {
125 self.previous_version_id = Some(previous_id);
126 self
127 }
128
129 pub fn is_currently_valid(&self) -> bool {
131 let now = Utc::now().naive_utc();
132 self.valid_from <= now && self.valid_to.map_or(true, |to| to > now)
133 }
134
135 pub fn is_current_version(&self) -> bool {
137 self.superseded_at.is_none()
138 }
139
140 pub fn was_valid_at(&self, at: NaiveDateTime) -> bool {
142 self.valid_from <= at && self.valid_to.map_or(true, |to| to > at)
143 }
144
145 pub fn was_current_at(&self, at: DateTime<Utc>) -> bool {
147 self.recorded_at <= at && self.superseded_at.map_or(true, |sup| sup > at)
148 }
149
150 pub fn supersede(&mut self, superseded_at: DateTime<Utc>) {
152 self.superseded_at = Some(superseded_at);
153 }
154
155 pub fn correct(&self, new_data: T, corrected_by: &str, reason: &str) -> Self
157 where
158 T: Clone,
159 {
160 let now = Utc::now();
161 Self {
162 data: new_data,
163 version_id: Uuid::new_v4(),
164 valid_from: self.valid_from,
165 valid_to: self.valid_to,
166 recorded_at: now,
167 superseded_at: None,
168 recorded_by: corrected_by.into(),
169 change_reason: Some(reason.into()),
170 previous_version_id: Some(self.version_id),
171 change_type: TemporalChangeType::Correction,
172 }
173 }
174
175 pub fn reverse(&self, reversed_by: &str, reason: &str) -> Self
177 where
178 T: Clone,
179 {
180 let now = Utc::now();
181 Self {
182 data: self.data.clone(),
183 version_id: Uuid::new_v4(),
184 valid_from: now.naive_utc(),
185 valid_to: None,
186 recorded_at: now,
187 superseded_at: None,
188 recorded_by: reversed_by.into(),
189 change_reason: Some(reason.into()),
190 previous_version_id: Some(self.version_id),
191 change_type: TemporalChangeType::Reversal,
192 }
193 }
194
195 pub fn inner(&self) -> &T {
197 &self.data
198 }
199
200 pub fn inner_mut(&mut self) -> &mut T {
202 &mut self.data
203 }
204
205 pub fn into_inner(self) -> T {
207 self.data
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
213#[serde(rename_all = "snake_case")]
214pub enum TemporalChangeType {
215 #[default]
217 Original,
218 Correction,
220 Reversal,
222 Adjustment,
224 Reclassification,
226 LatePosting,
228}
229
230impl TemporalChangeType {
231 pub fn is_correction(&self) -> bool {
233 matches!(self, Self::Correction | Self::Reversal)
234 }
235
236 pub fn description(&self) -> &'static str {
238 match self {
239 Self::Original => "Original posting",
240 Self::Correction => "Error correction",
241 Self::Reversal => "Reversal entry",
242 Self::Adjustment => "Period adjustment",
243 Self::Reclassification => "Account reclassification",
244 Self::LatePosting => "Late posting",
245 }
246 }
247}
248
249#[derive(Debug, Clone, Default, Serialize, Deserialize)]
251pub struct TemporalQuery {
252 pub as_of_valid_time: Option<NaiveDateTime>,
254 pub as_of_system_time: Option<DateTime<Utc>>,
256 pub include_history: bool,
258}
259
260impl TemporalQuery {
261 pub fn current() -> Self {
263 Self::default()
264 }
265
266 pub fn as_of_valid(time: NaiveDateTime) -> Self {
268 Self {
269 as_of_valid_time: Some(time),
270 ..Default::default()
271 }
272 }
273
274 pub fn as_of_system(time: DateTime<Utc>) -> Self {
276 Self {
277 as_of_system_time: Some(time),
278 ..Default::default()
279 }
280 }
281
282 pub fn with_history() -> Self {
284 Self {
285 include_history: true,
286 ..Default::default()
287 }
288 }
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct TemporalVersionChain<T> {
294 pub entity_id: Uuid,
296 pub versions: Vec<BiTemporal<T>>,
298}
299
300impl<T> TemporalVersionChain<T> {
301 pub fn new(entity_id: Uuid, initial: BiTemporal<T>) -> Self {
303 Self {
304 entity_id,
305 versions: vec![initial],
306 }
307 }
308
309 pub fn current(&self) -> Option<&BiTemporal<T>> {
311 self.versions.iter().find(|v| v.is_current_version())
312 }
313
314 pub fn version_at(&self, at: DateTime<Utc>) -> Option<&BiTemporal<T>> {
316 self.versions.iter().find(|v| v.was_current_at(at))
317 }
318
319 pub fn add_version(&mut self, version: BiTemporal<T>) {
321 if let Some(current) = self.versions.iter_mut().find(|v| v.is_current_version()) {
323 current.supersede(version.recorded_at);
324 }
325 self.versions.push(version);
326 }
327
328 pub fn all_versions(&self) -> &[BiTemporal<T>] {
330 &self.versions
331 }
332
333 pub fn version_count(&self) -> usize {
335 self.versions.len()
336 }
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
341pub struct TemporalAuditEntry {
342 pub entry_id: Uuid,
344 pub entity_id: Uuid,
346 pub entity_type: String,
348 pub version_id: Uuid,
350 pub action: TemporalAction,
352 pub timestamp: DateTime<Utc>,
354 pub user_id: String,
356 pub reason: Option<String>,
358 pub previous_value: Option<String>,
360 pub new_value: Option<String>,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
366#[serde(rename_all = "snake_case")]
367pub enum TemporalAction {
368 Create,
369 Update,
370 Correct,
371 Reverse,
372 Delete,
373 Restore,
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
381 struct TestEntity {
382 name: String,
383 value: i32,
384 }
385
386 #[test]
387 fn test_bitemporal_creation() {
388 let entity = TestEntity {
389 name: "Test".into(),
390 value: 100,
391 };
392 let temporal = BiTemporal::new(entity).with_recorded_by("user001");
393
394 assert!(temporal.is_current_version());
395 assert!(temporal.is_currently_valid());
396 assert_eq!(temporal.inner().value, 100);
397 }
398
399 #[test]
400 fn test_bitemporal_correction() {
401 let original = TestEntity {
402 name: "Test".into(),
403 value: 100,
404 };
405 let temporal = BiTemporal::new(original).with_recorded_by("user001");
406
407 let corrected = TestEntity {
408 name: "Test".into(),
409 value: 150,
410 };
411 let correction = temporal.correct(corrected, "user002", "Amount was wrong");
412
413 assert_eq!(correction.change_type, TemporalChangeType::Correction);
414 assert_eq!(correction.previous_version_id, Some(temporal.version_id));
415 assert_eq!(correction.inner().value, 150);
416 }
417
418 #[test]
419 fn test_version_chain() {
420 let entity = TestEntity {
421 name: "Test".into(),
422 value: 100,
423 };
424 let v1 = BiTemporal::new(entity.clone()).with_recorded_by("user001");
425 let entity_id = Uuid::new_v4();
426
427 let mut chain = TemporalVersionChain::new(entity_id, v1);
428
429 let v2 = BiTemporal::new(TestEntity {
430 name: "Test".into(),
431 value: 200,
432 })
433 .with_recorded_by("user002")
434 .with_change_type(TemporalChangeType::Correction);
435
436 chain.add_version(v2);
437
438 assert_eq!(chain.version_count(), 2);
439 assert_eq!(chain.current().unwrap().inner().value, 200);
440 }
441
442 #[test]
443 fn test_temporal_change_type() {
444 assert!(TemporalChangeType::Correction.is_correction());
445 assert!(TemporalChangeType::Reversal.is_correction());
446 assert!(!TemporalChangeType::Original.is_correction());
447 }
448}