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