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 Company,
105 Department,
107 Contract,
109 Asset,
111 BankAccount,
113 PurchaseOrder,
115 SalesOrder,
117 Invoice,
119 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
154#[serde(rename_all = "snake_case")]
155pub enum EntityStatus {
156 #[default]
158 Active,
159 Blocked,
161 MarkedForDeletion,
163 Archived,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct EntityRecord {
170 pub entity_id: EntityId,
172 pub name: String,
174 pub company_code: Option<String>,
176 pub created_date: NaiveDate,
178 pub valid_from: NaiveDate,
180 pub valid_to: Option<NaiveDate>,
182 pub status: EntityStatus,
184 pub status_changed_date: Option<NaiveDate>,
186 pub attributes: HashMap<String, String>,
188}
189
190impl EntityRecord {
191 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 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 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 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 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 pub fn can_transact_on(&self, date: NaiveDate) -> bool {
234 self.is_valid_on(date) && self.status == EntityStatus::Active
235 }
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct EntityEvent {
241 pub entity_id: EntityId,
243 pub event_type: EntityEventType,
245 pub event_date: NaiveDate,
247 pub description: Option<String>,
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
253#[serde(rename_all = "snake_case")]
254pub enum EntityEventType {
255 Created,
257 Activated,
259 Blocked,
261 Unblocked,
263 MarkedForDeletion,
265 Archived,
267 ValidityChanged,
269 Transferred,
271 Modified,
273}
274
275#[derive(Debug, Clone, Default, Serialize, Deserialize)]
281pub struct EntityRegistry {
282 entities: HashMap<EntityId, EntityRecord>,
284 by_type: HashMap<EntityType, Vec<EntityId>>,
286 by_company: HashMap<String, Vec<EntityId>>,
288 entity_timeline: BTreeMap<NaiveDate, Vec<EntityEvent>>,
290}
291
292impl EntityRegistry {
293 pub fn new() -> Self {
295 Self::default()
296 }
297
298 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 self.entities.insert(entity_id.clone(), record);
307
308 self.by_type
310 .entry(entity_type)
311 .or_default()
312 .push(entity_id.clone());
313
314 if let Some(cc) = company_code {
316 self.by_company
317 .entry(cc)
318 .or_default()
319 .push(entity_id.clone());
320 }
321
322 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 pub fn get(&self, entity_id: &EntityId) -> Option<&EntityRecord> {
337 self.entities.get(entity_id)
338 }
339
340 pub fn get_mut(&mut self, entity_id: &EntityId) -> Option<&mut EntityRecord> {
342 self.entities.get_mut(entity_id)
343 }
344
345 pub fn exists(&self, entity_id: &EntityId) -> bool {
347 self.entities.contains_key(entity_id)
348 }
349
350 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 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 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 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 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 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 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 pub fn total_count(&self) -> usize {
407 self.entities.len()
408 }
409
410 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 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 pub fn block(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
452 self.update_status(entity_id, EntityStatus::Blocked, date)
453 }
454
455 pub fn unblock(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
457 self.update_status(entity_id, EntityStatus::Active, date)
458 }
459
460 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 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 pub fn timeline_dates(&self) -> impl Iterator<Item = &NaiveDate> {
478 self.entity_timeline.keys()
479 }
480
481 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 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 pub fn register_entity(&mut self, record: EntityRecord) {
546 self.register(record);
547 }
548
549 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 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 assert!(!registry.is_valid(&entity_id, test_date(5)));
597
598 assert!(registry.is_valid(&entity_id, test_date(50)));
600
601 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 assert!(registry.can_transact(&entity_id, test_date(5)));
616
617 registry.block(&entity_id, test_date(10));
619
620 assert!(!registry.can_transact(&entity_id, test_date(15)));
622
623 registry.unblock(&entity_id, test_date(20));
625
626 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 assert!(registry
682 .validate_reference(&entity_id, test_date(5))
683 .is_err());
684
685 assert!(registry
687 .validate_reference(&entity_id, test_date(50))
688 .is_ok());
689
690 assert!(registry
692 .validate_reference(&entity_id, test_date(150))
693 .is_err());
694
695 let fake_id = EntityId::vendor("V-999");
697 assert!(registry
698 .validate_reference(&fake_id, test_date(50))
699 .is_err());
700 }
701}