1use chrono::NaiveDate;
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, HashMap};
9
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct EntityId {
13 pub entity_type: EntityType,
15 pub id: String,
17}
18
19impl EntityId {
20 pub fn new(entity_type: EntityType, id: impl Into<String>) -> Self {
22 Self {
23 entity_type,
24 id: id.into(),
25 }
26 }
27
28 pub fn vendor(id: impl Into<String>) -> Self {
30 Self::new(EntityType::Vendor, id)
31 }
32
33 pub fn customer(id: impl Into<String>) -> Self {
35 Self::new(EntityType::Customer, id)
36 }
37
38 pub fn material(id: impl Into<String>) -> Self {
40 Self::new(EntityType::Material, id)
41 }
42
43 pub fn fixed_asset(id: impl Into<String>) -> Self {
45 Self::new(EntityType::FixedAsset, id)
46 }
47
48 pub fn employee(id: impl Into<String>) -> Self {
50 Self::new(EntityType::Employee, id)
51 }
52
53 pub fn cost_center(id: impl Into<String>) -> Self {
55 Self::new(EntityType::CostCenter, id)
56 }
57
58 pub fn profit_center(id: impl Into<String>) -> Self {
60 Self::new(EntityType::ProfitCenter, id)
61 }
62
63 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum EntityType {
79 Vendor,
81 Customer,
83 Material,
85 FixedAsset,
87 Employee,
89 CostCenter,
91 ProfitCenter,
93 GlAccount,
95 CompanyCode,
97 BusinessPartner,
99 Project,
101 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum EntityStatus {
129 #[default]
131 Active,
132 Blocked,
134 MarkedForDeletion,
136 Archived,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct EntityRecord {
143 pub entity_id: EntityId,
145 pub name: String,
147 pub company_code: Option<String>,
149 pub created_date: NaiveDate,
151 pub valid_from: NaiveDate,
153 pub valid_to: Option<NaiveDate>,
155 pub status: EntityStatus,
157 pub status_changed_date: Option<NaiveDate>,
159 pub attributes: HashMap<String, String>,
161}
162
163impl EntityRecord {
164 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 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 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 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 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 pub fn can_transact_on(&self, date: NaiveDate) -> bool {
207 self.is_valid_on(date) && self.status == EntityStatus::Active
208 }
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct EntityEvent {
214 pub entity_id: EntityId,
216 pub event_type: EntityEventType,
218 pub event_date: NaiveDate,
220 pub description: Option<String>,
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
226#[serde(rename_all = "snake_case")]
227pub enum EntityEventType {
228 Created,
230 Activated,
232 Blocked,
234 Unblocked,
236 MarkedForDeletion,
238 Archived,
240 ValidityChanged,
242 Transferred,
244 Modified,
246}
247
248#[derive(Debug, Clone, Default, Serialize, Deserialize)]
254pub struct EntityRegistry {
255 entities: HashMap<EntityId, EntityRecord>,
257 by_type: HashMap<EntityType, Vec<EntityId>>,
259 by_company: HashMap<String, Vec<EntityId>>,
261 entity_timeline: BTreeMap<NaiveDate, Vec<EntityEvent>>,
263}
264
265impl EntityRegistry {
266 pub fn new() -> Self {
268 Self::default()
269 }
270
271 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 self.entities.insert(entity_id.clone(), record);
280
281 self.by_type
283 .entry(entity_type)
284 .or_default()
285 .push(entity_id.clone());
286
287 if let Some(cc) = company_code {
289 self.by_company
290 .entry(cc)
291 .or_default()
292 .push(entity_id.clone());
293 }
294
295 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 pub fn get(&self, entity_id: &EntityId) -> Option<&EntityRecord> {
310 self.entities.get(entity_id)
311 }
312
313 pub fn get_mut(&mut self, entity_id: &EntityId) -> Option<&mut EntityRecord> {
315 self.entities.get_mut(entity_id)
316 }
317
318 pub fn exists(&self, entity_id: &EntityId) -> bool {
320 self.entities.contains_key(entity_id)
321 }
322
323 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 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 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 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 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 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 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 pub fn total_count(&self) -> usize {
380 self.entities.len()
381 }
382
383 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 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 pub fn block(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
425 self.update_status(entity_id, EntityStatus::Blocked, date)
426 }
427
428 pub fn unblock(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
430 self.update_status(entity_id, EntityStatus::Active, date)
431 }
432
433 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 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 pub fn timeline_dates(&self) -> impl Iterator<Item = &NaiveDate> {
451 self.entity_timeline.keys()
452 }
453
454 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 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 pub fn register_entity(&mut self, record: EntityRecord) {
519 self.register(record);
520 }
521
522 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 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 assert!(!registry.is_valid(&entity_id, test_date(5)));
570
571 assert!(registry.is_valid(&entity_id, test_date(50)));
573
574 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 assert!(registry.can_transact(&entity_id, test_date(5)));
589
590 registry.block(&entity_id, test_date(10));
592
593 assert!(!registry.can_transact(&entity_id, test_date(15)));
595
596 registry.unblock(&entity_id, test_date(20));
598
599 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 assert!(registry
655 .validate_reference(&entity_id, test_date(5))
656 .is_err());
657
658 assert!(registry
660 .validate_reference(&entity_id, test_date(50))
661 .is_ok());
662
663 assert!(registry
665 .validate_reference(&entity_id, test_date(150))
666 .is_err());
667
668 let fake_id = EntityId::vendor("V-999");
670 assert!(registry
671 .validate_reference(&fake_id, test_date(50))
672 .is_err());
673 }
674}