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 #[serde(with = "crate::serde_timestamp::naive")]
42 pub valid_from: NaiveDateTime,
43 #[serde(default, with = "crate::serde_timestamp::naive::option")]
45 pub valid_to: Option<NaiveDateTime>,
46
47 #[serde(with = "crate::serde_timestamp::utc")]
50 pub recorded_at: DateTime<Utc>,
51 #[serde(default, with = "crate::serde_timestamp::utc::option")]
53 pub superseded_at: Option<DateTime<Utc>>,
54
55 pub recorded_by: String,
58 pub change_reason: Option<String>,
60 pub previous_version_id: Option<Uuid>,
62 pub change_type: TemporalChangeType,
64}
65
66impl<T> BiTemporal<T> {
67 pub fn new(data: T) -> Self {
69 let now = Utc::now();
70 Self {
71 data,
72 version_id: Uuid::new_v4(),
73 valid_from: now.naive_utc(),
74 valid_to: None,
75 recorded_at: now,
76 superseded_at: None,
77 recorded_by: String::new(),
78 change_reason: None,
79 previous_version_id: None,
80 change_type: TemporalChangeType::Original,
81 }
82 }
83
84 pub fn with_valid_time(mut self, from: NaiveDateTime, to: Option<NaiveDateTime>) -> Self {
86 self.valid_from = from;
87 self.valid_to = to;
88 self
89 }
90
91 pub fn valid_from(mut self, from: NaiveDateTime) -> Self {
93 self.valid_from = from;
94 self
95 }
96
97 pub fn valid_to(mut self, to: NaiveDateTime) -> Self {
99 self.valid_to = Some(to);
100 self
101 }
102
103 pub fn with_recorded_at(mut self, recorded_at: DateTime<Utc>) -> Self {
105 self.recorded_at = recorded_at;
106 self
107 }
108
109 pub fn with_recorded_by(mut self, recorded_by: &str) -> Self {
111 self.recorded_by = recorded_by.into();
112 self
113 }
114
115 pub fn with_change_reason(mut self, reason: &str) -> Self {
117 self.change_reason = Some(reason.into());
118 self
119 }
120
121 pub fn with_change_type(mut self, change_type: TemporalChangeType) -> Self {
123 self.change_type = change_type;
124 self
125 }
126
127 pub fn with_previous_version(mut self, previous_id: Uuid) -> Self {
129 self.previous_version_id = Some(previous_id);
130 self
131 }
132
133 pub fn is_currently_valid(&self) -> bool {
135 let now = Utc::now().naive_utc();
136 self.valid_from <= now && self.valid_to.is_none_or(|to| to > now)
137 }
138
139 pub fn is_current_version(&self) -> bool {
141 self.superseded_at.is_none()
142 }
143
144 pub fn was_valid_at(&self, at: NaiveDateTime) -> bool {
146 self.valid_from <= at && self.valid_to.is_none_or(|to| to > at)
147 }
148
149 pub fn was_current_at(&self, at: DateTime<Utc>) -> bool {
151 self.recorded_at <= at && self.superseded_at.is_none_or(|sup| sup > at)
152 }
153
154 pub fn supersede(&mut self, superseded_at: DateTime<Utc>) {
156 self.superseded_at = Some(superseded_at);
157 }
158
159 pub fn correct(&self, new_data: T, corrected_by: &str, reason: &str) -> Self
161 where
162 T: Clone,
163 {
164 let now = Utc::now();
165 Self {
166 data: new_data,
167 version_id: Uuid::new_v4(),
168 valid_from: self.valid_from,
169 valid_to: self.valid_to,
170 recorded_at: now,
171 superseded_at: None,
172 recorded_by: corrected_by.into(),
173 change_reason: Some(reason.into()),
174 previous_version_id: Some(self.version_id),
175 change_type: TemporalChangeType::Correction,
176 }
177 }
178
179 pub fn reverse(&self, reversed_by: &str, reason: &str) -> Self
181 where
182 T: Clone,
183 {
184 let now = Utc::now();
185 Self {
186 data: self.data.clone(),
187 version_id: Uuid::new_v4(),
188 valid_from: now.naive_utc(),
189 valid_to: None,
190 recorded_at: now,
191 superseded_at: None,
192 recorded_by: reversed_by.into(),
193 change_reason: Some(reason.into()),
194 previous_version_id: Some(self.version_id),
195 change_type: TemporalChangeType::Reversal,
196 }
197 }
198
199 pub fn inner(&self) -> &T {
201 &self.data
202 }
203
204 pub fn inner_mut(&mut self) -> &mut T {
206 &mut self.data
207 }
208
209 pub fn into_inner(self) -> T {
211 self.data
212 }
213}
214
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
217#[serde(rename_all = "snake_case")]
218pub enum TemporalChangeType {
219 #[default]
221 Original,
222 Correction,
224 Reversal,
226 Adjustment,
228 Reclassification,
230 LatePosting,
232}
233
234impl TemporalChangeType {
235 pub fn is_correction(&self) -> bool {
237 matches!(self, Self::Correction | Self::Reversal)
238 }
239
240 pub fn description(&self) -> &'static str {
242 match self {
243 Self::Original => "Original posting",
244 Self::Correction => "Error correction",
245 Self::Reversal => "Reversal entry",
246 Self::Adjustment => "Period adjustment",
247 Self::Reclassification => "Account reclassification",
248 Self::LatePosting => "Late posting",
249 }
250 }
251}
252
253#[derive(Debug, Clone, Default, Serialize, Deserialize)]
255pub struct TemporalQuery {
256 #[serde(default, with = "crate::serde_timestamp::naive::option")]
258 pub as_of_valid_time: Option<NaiveDateTime>,
259 #[serde(default, with = "crate::serde_timestamp::utc::option")]
261 pub as_of_system_time: Option<DateTime<Utc>>,
262 pub include_history: bool,
264}
265
266impl TemporalQuery {
267 pub fn current() -> Self {
269 Self::default()
270 }
271
272 pub fn as_of_valid(time: NaiveDateTime) -> Self {
274 Self {
275 as_of_valid_time: Some(time),
276 ..Default::default()
277 }
278 }
279
280 pub fn as_of_system(time: DateTime<Utc>) -> Self {
282 Self {
283 as_of_system_time: Some(time),
284 ..Default::default()
285 }
286 }
287
288 pub fn with_history() -> Self {
290 Self {
291 include_history: true,
292 ..Default::default()
293 }
294 }
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct TemporalVersionChain<T> {
300 pub entity_id: Uuid,
302 pub versions: Vec<BiTemporal<T>>,
304}
305
306impl<T> TemporalVersionChain<T> {
307 pub fn new(entity_id: Uuid, initial: BiTemporal<T>) -> Self {
309 Self {
310 entity_id,
311 versions: vec![initial],
312 }
313 }
314
315 pub fn current(&self) -> Option<&BiTemporal<T>> {
317 self.versions.iter().find(|v| v.is_current_version())
318 }
319
320 pub fn version_at(&self, at: DateTime<Utc>) -> Option<&BiTemporal<T>> {
322 self.versions.iter().find(|v| v.was_current_at(at))
323 }
324
325 pub fn add_version(&mut self, version: BiTemporal<T>) {
327 if let Some(current) = self.versions.iter_mut().find(|v| v.is_current_version()) {
329 current.supersede(version.recorded_at);
330 }
331 self.versions.push(version);
332 }
333
334 pub fn all_versions(&self) -> &[BiTemporal<T>] {
336 &self.versions
337 }
338
339 pub fn version_count(&self) -> usize {
341 self.versions.len()
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct TemporalAuditEntry {
348 pub entry_id: Uuid,
350 pub entity_id: Uuid,
352 pub entity_type: String,
354 pub version_id: Uuid,
356 pub action: TemporalAction,
358 #[serde(with = "crate::serde_timestamp::utc")]
360 pub timestamp: DateTime<Utc>,
361 pub user_id: String,
363 pub reason: Option<String>,
365 pub previous_value: Option<String>,
367 pub new_value: Option<String>,
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
373#[serde(rename_all = "snake_case")]
374pub enum TemporalAction {
375 Create,
376 Update,
377 Correct,
378 Reverse,
379 Delete,
380 Restore,
381}
382
383#[cfg(test)]
384#[allow(clippy::unwrap_used)]
385mod tests {
386 use super::*;
387
388 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
389 struct TestEntity {
390 name: String,
391 value: i32,
392 }
393
394 #[test]
395 fn test_bitemporal_creation() {
396 let entity = TestEntity {
397 name: "Test".into(),
398 value: 100,
399 };
400 let temporal = BiTemporal::new(entity).with_recorded_by("user001");
401
402 assert!(temporal.is_current_version());
403 assert!(temporal.is_currently_valid());
404 assert_eq!(temporal.inner().value, 100);
405 }
406
407 #[test]
408 fn test_bitemporal_correction() {
409 let original = TestEntity {
410 name: "Test".into(),
411 value: 100,
412 };
413 let temporal = BiTemporal::new(original).with_recorded_by("user001");
414
415 let corrected = TestEntity {
416 name: "Test".into(),
417 value: 150,
418 };
419 let correction = temporal.correct(corrected, "user002", "Amount was wrong");
420
421 assert_eq!(correction.change_type, TemporalChangeType::Correction);
422 assert_eq!(correction.previous_version_id, Some(temporal.version_id));
423 assert_eq!(correction.inner().value, 150);
424 }
425
426 #[test]
427 fn test_version_chain() {
428 let entity = TestEntity {
429 name: "Test".into(),
430 value: 100,
431 };
432 let v1 = BiTemporal::new(entity.clone()).with_recorded_by("user001");
433 let entity_id = Uuid::new_v4();
434
435 let mut chain = TemporalVersionChain::new(entity_id, v1);
436
437 let v2 = BiTemporal::new(TestEntity {
438 name: "Test".into(),
439 value: 200,
440 })
441 .with_recorded_by("user002")
442 .with_change_type(TemporalChangeType::Correction);
443
444 chain.add_version(v2);
445
446 assert_eq!(chain.version_count(), 2);
447 assert_eq!(chain.current().unwrap().inner().value, 200);
448 }
449
450 #[test]
451 fn test_temporal_change_type() {
452 assert!(TemporalChangeType::Correction.is_correction());
453 assert!(TemporalChangeType::Reversal.is_correction());
454 assert!(!TemporalChangeType::Original.is_correction());
455 }
456}