Skip to main content

datasynth_core/models/
entity_registry.rs

1//! Entity registry for centralized master data management.
2//!
3//! Provides a central registry tracking all master data entities with
4//! temporal validity, ensuring referential integrity across transactions.
5
6use chrono::NaiveDate;
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, HashMap};
9
10/// Unique identifier for any entity in the system.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct EntityId {
13    /// Type of the entity
14    pub entity_type: EntityType,
15    /// Unique identifier within the type
16    pub id: String,
17}
18
19impl EntityId {
20    /// Create a new entity ID.
21    pub fn new(entity_type: EntityType, id: impl Into<String>) -> Self {
22        Self {
23            entity_type,
24            id: id.into(),
25        }
26    }
27
28    /// Create a vendor entity ID.
29    pub fn vendor(id: impl Into<String>) -> Self {
30        Self::new(EntityType::Vendor, id)
31    }
32
33    /// Create a customer entity ID.
34    pub fn customer(id: impl Into<String>) -> Self {
35        Self::new(EntityType::Customer, id)
36    }
37
38    /// Create a material entity ID.
39    pub fn material(id: impl Into<String>) -> Self {
40        Self::new(EntityType::Material, id)
41    }
42
43    /// Create a fixed asset entity ID.
44    pub fn fixed_asset(id: impl Into<String>) -> Self {
45        Self::new(EntityType::FixedAsset, id)
46    }
47
48    /// Create an employee entity ID.
49    pub fn employee(id: impl Into<String>) -> Self {
50        Self::new(EntityType::Employee, id)
51    }
52
53    /// Create a cost center entity ID.
54    pub fn cost_center(id: impl Into<String>) -> Self {
55        Self::new(EntityType::CostCenter, id)
56    }
57
58    /// Create a profit center entity ID.
59    pub fn profit_center(id: impl Into<String>) -> Self {
60        Self::new(EntityType::ProfitCenter, id)
61    }
62
63    /// Create a GL account entity ID.
64    pub fn gl_account(id: impl Into<String>) -> Self {
65        Self::new(EntityType::GlAccount, id)
66    }
67}
68
69impl std::fmt::Display for EntityId {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        write!(f, "{}:{}", self.entity_type, self.id)
72    }
73}
74
75/// Types of entities that can be registered.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum EntityType {
79    /// Vendor/Supplier
80    Vendor,
81    /// Customer
82    Customer,
83    /// Material/Product
84    Material,
85    /// Fixed Asset
86    FixedAsset,
87    /// Employee
88    Employee,
89    /// Cost Center
90    CostCenter,
91    /// Profit Center
92    ProfitCenter,
93    /// GL Account
94    GlAccount,
95    /// Company Code
96    CompanyCode,
97    /// Business Partner (generic)
98    BusinessPartner,
99    /// Project/WBS Element
100    Project,
101    /// Internal Order
102    InternalOrder,
103}
104
105impl std::fmt::Display for EntityType {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        let name = match self {
108            Self::Vendor => "VENDOR",
109            Self::Customer => "CUSTOMER",
110            Self::Material => "MATERIAL",
111            Self::FixedAsset => "FIXED_ASSET",
112            Self::Employee => "EMPLOYEE",
113            Self::CostCenter => "COST_CENTER",
114            Self::ProfitCenter => "PROFIT_CENTER",
115            Self::GlAccount => "GL_ACCOUNT",
116            Self::CompanyCode => "COMPANY_CODE",
117            Self::BusinessPartner => "BUSINESS_PARTNER",
118            Self::Project => "PROJECT",
119            Self::InternalOrder => "INTERNAL_ORDER",
120        };
121        write!(f, "{}", name)
122    }
123}
124
125/// Status of an entity at a point in time.
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum EntityStatus {
129    /// Entity is active and can be used in transactions
130    #[default]
131    Active,
132    /// Entity is blocked for new transactions
133    Blocked,
134    /// Entity is marked for deletion
135    MarkedForDeletion,
136    /// Entity has been archived
137    Archived,
138}
139
140/// Record of an entity in the registry.
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct EntityRecord {
143    /// Entity identifier
144    pub entity_id: EntityId,
145    /// Human-readable name/description
146    pub name: String,
147    /// Company code this entity belongs to (if applicable)
148    pub company_code: Option<String>,
149    /// Date the entity was created
150    pub created_date: NaiveDate,
151    /// Date the entity becomes valid (may differ from created)
152    pub valid_from: NaiveDate,
153    /// Date the entity is valid until (None = indefinite)
154    pub valid_to: Option<NaiveDate>,
155    /// Current status
156    pub status: EntityStatus,
157    /// Date status last changed
158    pub status_changed_date: Option<NaiveDate>,
159    /// Additional attributes as key-value pairs
160    pub attributes: HashMap<String, String>,
161}
162
163impl EntityRecord {
164    /// Create a new entity record.
165    pub fn new(entity_id: EntityId, name: impl Into<String>, created_date: NaiveDate) -> Self {
166        Self {
167            entity_id,
168            name: name.into(),
169            company_code: None,
170            created_date,
171            valid_from: created_date,
172            valid_to: None,
173            status: EntityStatus::Active,
174            status_changed_date: None,
175            attributes: HashMap::new(),
176        }
177    }
178
179    /// Set company code.
180    pub fn with_company_code(mut self, company_code: impl Into<String>) -> Self {
181        self.company_code = Some(company_code.into());
182        self
183    }
184
185    /// Set validity period.
186    pub fn with_validity(mut self, from: NaiveDate, to: Option<NaiveDate>) -> Self {
187        self.valid_from = from;
188        self.valid_to = to;
189        self
190    }
191
192    /// Add an attribute.
193    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
194        self.attributes.insert(key.into(), value.into());
195        self
196    }
197
198    /// Check if the entity is valid on a given date.
199    pub fn is_valid_on(&self, date: NaiveDate) -> bool {
200        date >= self.valid_from
201            && self.valid_to.map_or(true, |to| date <= to)
202            && self.status == EntityStatus::Active
203    }
204
205    /// Check if the entity can be used in transactions on a given date.
206    pub fn can_transact_on(&self, date: NaiveDate) -> bool {
207        self.is_valid_on(date) && self.status == EntityStatus::Active
208    }
209}
210
211/// Event in an entity's lifecycle.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct EntityEvent {
214    /// Entity this event relates to
215    pub entity_id: EntityId,
216    /// Type of event
217    pub event_type: EntityEventType,
218    /// Date the event occurred
219    pub event_date: NaiveDate,
220    /// Description of the event
221    pub description: Option<String>,
222}
223
224/// Types of entity lifecycle events.
225#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
226#[serde(rename_all = "snake_case")]
227pub enum EntityEventType {
228    /// Entity was created
229    Created,
230    /// Entity was activated
231    Activated,
232    /// Entity was blocked
233    Blocked,
234    /// Entity was unblocked
235    Unblocked,
236    /// Entity was marked for deletion
237    MarkedForDeletion,
238    /// Entity was archived
239    Archived,
240    /// Entity validity period changed
241    ValidityChanged,
242    /// Entity was transferred to another company
243    Transferred,
244    /// Entity attributes were modified
245    Modified,
246}
247
248/// Central registry for all master data entities.
249///
250/// Ensures referential integrity by tracking entity existence and validity
251/// over time. All transaction generators should check this registry before
252/// using any master data reference.
253#[derive(Debug, Clone, Default, Serialize, Deserialize)]
254pub struct EntityRegistry {
255    /// All registered entities
256    entities: HashMap<EntityId, EntityRecord>,
257    /// Index by entity type
258    by_type: HashMap<EntityType, Vec<EntityId>>,
259    /// Index by company code
260    by_company: HashMap<String, Vec<EntityId>>,
261    /// Timeline of entity events
262    entity_timeline: BTreeMap<NaiveDate, Vec<EntityEvent>>,
263}
264
265impl EntityRegistry {
266    /// Create a new empty registry.
267    pub fn new() -> Self {
268        Self::default()
269    }
270
271    /// Register a new entity.
272    pub fn register(&mut self, record: EntityRecord) {
273        let entity_id = record.entity_id.clone();
274        let entity_type = entity_id.entity_type;
275        let company_code = record.company_code.clone();
276        let created_date = record.created_date;
277
278        // Add to main storage
279        self.entities.insert(entity_id.clone(), record);
280
281        // Update type index
282        self.by_type
283            .entry(entity_type)
284            .or_default()
285            .push(entity_id.clone());
286
287        // Update company index
288        if let Some(cc) = company_code {
289            self.by_company
290                .entry(cc)
291                .or_default()
292                .push(entity_id.clone());
293        }
294
295        // Record creation event
296        let event = EntityEvent {
297            entity_id,
298            event_type: EntityEventType::Created,
299            event_date: created_date,
300            description: Some("Entity created".to_string()),
301        };
302        self.entity_timeline
303            .entry(created_date)
304            .or_default()
305            .push(event);
306    }
307
308    /// Get an entity by ID.
309    pub fn get(&self, entity_id: &EntityId) -> Option<&EntityRecord> {
310        self.entities.get(entity_id)
311    }
312
313    /// Get a mutable reference to an entity.
314    pub fn get_mut(&mut self, entity_id: &EntityId) -> Option<&mut EntityRecord> {
315        self.entities.get_mut(entity_id)
316    }
317
318    /// Check if an entity exists.
319    pub fn exists(&self, entity_id: &EntityId) -> bool {
320        self.entities.contains_key(entity_id)
321    }
322
323    /// Check if an entity exists and is valid on a given date.
324    pub fn is_valid(&self, entity_id: &EntityId, date: NaiveDate) -> bool {
325        self.entities
326            .get(entity_id)
327            .is_some_and(|r| r.is_valid_on(date))
328    }
329
330    /// Check if an entity can be used in transactions on a given date.
331    pub fn can_transact(&self, entity_id: &EntityId, date: NaiveDate) -> bool {
332        self.entities
333            .get(entity_id)
334            .is_some_and(|r| r.can_transact_on(date))
335    }
336
337    /// Get all entities of a given type.
338    pub fn get_by_type(&self, entity_type: EntityType) -> Vec<&EntityRecord> {
339        self.by_type
340            .get(&entity_type)
341            .map(|ids| ids.iter().filter_map(|id| self.entities.get(id)).collect())
342            .unwrap_or_default()
343    }
344
345    /// Get all entities of a given type that are valid on a date.
346    pub fn get_valid_by_type(
347        &self,
348        entity_type: EntityType,
349        date: NaiveDate,
350    ) -> Vec<&EntityRecord> {
351        self.get_by_type(entity_type)
352            .into_iter()
353            .filter(|r| r.is_valid_on(date))
354            .collect()
355    }
356
357    /// Get all entities for a company code.
358    pub fn get_by_company(&self, company_code: &str) -> Vec<&EntityRecord> {
359        self.by_company
360            .get(company_code)
361            .map(|ids| ids.iter().filter_map(|id| self.entities.get(id)).collect())
362            .unwrap_or_default()
363    }
364
365    /// Get all entity IDs of a given type.
366    pub fn get_ids_by_type(&self, entity_type: EntityType) -> Vec<&EntityId> {
367        self.by_type
368            .get(&entity_type)
369            .map(|ids| ids.iter().collect())
370            .unwrap_or_default()
371    }
372
373    /// Get count of entities by type.
374    pub fn count_by_type(&self, entity_type: EntityType) -> usize {
375        self.by_type.get(&entity_type).map_or(0, |ids| ids.len())
376    }
377
378    /// Get total count of all entities.
379    pub fn total_count(&self) -> usize {
380        self.entities.len()
381    }
382
383    /// Update entity status.
384    pub fn update_status(
385        &mut self,
386        entity_id: &EntityId,
387        new_status: EntityStatus,
388        date: NaiveDate,
389    ) -> bool {
390        if let Some(record) = self.entities.get_mut(entity_id) {
391            let old_status = record.status;
392            record.status = new_status;
393            record.status_changed_date = Some(date);
394
395            // Record status change event
396            let event_type = match new_status {
397                EntityStatus::Active if old_status == EntityStatus::Blocked => {
398                    EntityEventType::Unblocked
399                }
400                EntityStatus::Active => EntityEventType::Activated,
401                EntityStatus::Blocked => EntityEventType::Blocked,
402                EntityStatus::MarkedForDeletion => EntityEventType::MarkedForDeletion,
403                EntityStatus::Archived => EntityEventType::Archived,
404            };
405
406            let event = EntityEvent {
407                entity_id: entity_id.clone(),
408                event_type,
409                event_date: date,
410                description: Some(format!(
411                    "Status changed from {:?} to {:?}",
412                    old_status, new_status
413                )),
414            };
415            self.entity_timeline.entry(date).or_default().push(event);
416
417            true
418        } else {
419            false
420        }
421    }
422
423    /// Block an entity for new transactions.
424    pub fn block(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
425        self.update_status(entity_id, EntityStatus::Blocked, date)
426    }
427
428    /// Unblock an entity.
429    pub fn unblock(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
430        self.update_status(entity_id, EntityStatus::Active, date)
431    }
432
433    /// Get events that occurred on a specific date.
434    pub fn get_events_on(&self, date: NaiveDate) -> &[EntityEvent] {
435        self.entity_timeline
436            .get(&date)
437            .map(|v| v.as_slice())
438            .unwrap_or(&[])
439    }
440
441    /// Get events in a date range.
442    pub fn get_events_in_range(&self, from: NaiveDate, to: NaiveDate) -> Vec<&EntityEvent> {
443        self.entity_timeline
444            .range(from..=to)
445            .flat_map(|(_, events)| events.iter())
446            .collect()
447    }
448
449    /// Get the timeline entry dates.
450    pub fn timeline_dates(&self) -> impl Iterator<Item = &NaiveDate> {
451        self.entity_timeline.keys()
452    }
453
454    /// Validate that an entity reference can be used on a transaction date.
455    /// Returns an error message if invalid.
456    pub fn validate_reference(
457        &self,
458        entity_id: &EntityId,
459        transaction_date: NaiveDate,
460    ) -> Result<(), String> {
461        match self.entities.get(entity_id) {
462            None => Err(format!("Entity {} does not exist", entity_id)),
463            Some(record) => {
464                if transaction_date < record.valid_from {
465                    Err(format!(
466                        "Entity {} is not valid until {} (transaction date: {})",
467                        entity_id, record.valid_from, transaction_date
468                    ))
469                } else if let Some(valid_to) = record.valid_to {
470                    if transaction_date > valid_to {
471                        Err(format!(
472                            "Entity {} validity expired on {} (transaction date: {})",
473                            entity_id, valid_to, transaction_date
474                        ))
475                    } else if record.status != EntityStatus::Active {
476                        Err(format!(
477                            "Entity {} has status {:?} (not active)",
478                            entity_id, record.status
479                        ))
480                    } else {
481                        Ok(())
482                    }
483                } else if record.status != EntityStatus::Active {
484                    Err(format!(
485                        "Entity {} has status {:?} (not active)",
486                        entity_id, record.status
487                    ))
488                } else {
489                    Ok(())
490                }
491            }
492        }
493    }
494
495    /// Rebuild all indices (call after deserialization).
496    pub fn rebuild_indices(&mut self) {
497        self.by_type.clear();
498        self.by_company.clear();
499
500        for (entity_id, record) in &self.entities {
501            self.by_type
502                .entry(entity_id.entity_type)
503                .or_default()
504                .push(entity_id.clone());
505
506            if let Some(cc) = &record.company_code {
507                self.by_company
508                    .entry(cc.clone())
509                    .or_default()
510                    .push(entity_id.clone());
511            }
512        }
513    }
514
515    // === Backward compatibility aliases ===
516
517    /// Alias for `register` - registers a new entity.
518    pub fn register_entity(&mut self, record: EntityRecord) {
519        self.register(record);
520    }
521
522    /// Record an event for an entity.
523    pub fn record_event(&mut self, event: EntityEvent) {
524        self.entity_timeline
525            .entry(event.event_date)
526            .or_default()
527            .push(event);
528    }
529
530    /// Check if an entity is valid on a given date.
531    /// Alias for `is_valid`.
532    pub fn is_valid_on(&self, entity_id: &EntityId, date: NaiveDate) -> bool {
533        self.is_valid(entity_id, date)
534    }
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    fn test_date(days: i64) -> NaiveDate {
542        NaiveDate::from_ymd_opt(2024, 1, 1).unwrap() + chrono::Duration::days(days)
543    }
544
545    #[test]
546    fn test_entity_registration() {
547        let mut registry = EntityRegistry::new();
548
549        let entity_id = EntityId::vendor("V-001");
550        let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(0));
551
552        registry.register(record);
553
554        assert!(registry.exists(&entity_id));
555        assert_eq!(registry.count_by_type(EntityType::Vendor), 1);
556    }
557
558    #[test]
559    fn test_entity_validity() {
560        let mut registry = EntityRegistry::new();
561
562        let entity_id = EntityId::vendor("V-001");
563        let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(10))
564            .with_validity(test_date(10), Some(test_date(100)));
565
566        registry.register(record);
567
568        // Before validity period
569        assert!(!registry.is_valid(&entity_id, test_date(5)));
570
571        // During validity period
572        assert!(registry.is_valid(&entity_id, test_date(50)));
573
574        // After validity period
575        assert!(!registry.is_valid(&entity_id, test_date(150)));
576    }
577
578    #[test]
579    fn test_entity_blocking() {
580        let mut registry = EntityRegistry::new();
581
582        let entity_id = EntityId::vendor("V-001");
583        let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(0));
584
585        registry.register(record);
586
587        // Initially can transact
588        assert!(registry.can_transact(&entity_id, test_date(5)));
589
590        // Block the entity
591        registry.block(&entity_id, test_date(10));
592
593        // Cannot transact after blocking
594        assert!(!registry.can_transact(&entity_id, test_date(15)));
595
596        // Unblock
597        registry.unblock(&entity_id, test_date(20));
598
599        // Can transact again
600        assert!(registry.can_transact(&entity_id, test_date(25)));
601    }
602
603    #[test]
604    fn test_entity_timeline() {
605        let mut registry = EntityRegistry::new();
606
607        let entity1 = EntityId::vendor("V-001");
608        let entity2 = EntityId::vendor("V-002");
609
610        registry.register(EntityRecord::new(entity1.clone(), "Vendor 1", test_date(0)));
611        registry.register(EntityRecord::new(entity2.clone(), "Vendor 2", test_date(5)));
612
613        let events_day0 = registry.get_events_on(test_date(0));
614        assert_eq!(events_day0.len(), 1);
615
616        let events_range = registry.get_events_in_range(test_date(0), test_date(10));
617        assert_eq!(events_range.len(), 2);
618    }
619
620    #[test]
621    fn test_company_index() {
622        let mut registry = EntityRegistry::new();
623
624        let entity1 = EntityId::vendor("V-001");
625        let entity2 = EntityId::vendor("V-002");
626        let entity3 = EntityId::customer("C-001");
627
628        registry.register(
629            EntityRecord::new(entity1.clone(), "Vendor 1", test_date(0)).with_company_code("1000"),
630        );
631        registry.register(
632            EntityRecord::new(entity2.clone(), "Vendor 2", test_date(0)).with_company_code("2000"),
633        );
634        registry.register(
635            EntityRecord::new(entity3.clone(), "Customer 1", test_date(0))
636                .with_company_code("1000"),
637        );
638
639        let company_1000_entities = registry.get_by_company("1000");
640        assert_eq!(company_1000_entities.len(), 2);
641    }
642
643    #[test]
644    fn test_validate_reference() {
645        let mut registry = EntityRegistry::new();
646
647        let entity_id = EntityId::vendor("V-001");
648        let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(10))
649            .with_validity(test_date(10), Some(test_date(100)));
650
651        registry.register(record);
652
653        // Before validity
654        assert!(registry
655            .validate_reference(&entity_id, test_date(5))
656            .is_err());
657
658        // During validity
659        assert!(registry
660            .validate_reference(&entity_id, test_date(50))
661            .is_ok());
662
663        // After validity
664        assert!(registry
665            .validate_reference(&entity_id, test_date(150))
666            .is_err());
667
668        // Non-existent entity
669        let fake_id = EntityId::vendor("V-999");
670        assert!(registry
671            .validate_reference(&fake_id, test_date(50))
672            .is_err());
673    }
674}