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.is_none_or(|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, std::vec::Vec::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 {old_status:?} to {new_status:?}"
439 )),
440 };
441 self.entity_timeline.entry(date).or_default().push(event);
442
443 true
444 } else {
445 false
446 }
447 }
448
449 pub fn block(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
451 self.update_status(entity_id, EntityStatus::Blocked, date)
452 }
453
454 pub fn unblock(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
456 self.update_status(entity_id, EntityStatus::Active, date)
457 }
458
459 pub fn get_events_on(&self, date: NaiveDate) -> &[EntityEvent] {
461 self.entity_timeline
462 .get(&date)
463 .map(std::vec::Vec::as_slice)
464 .unwrap_or(&[])
465 }
466
467 pub fn get_events_in_range(&self, from: NaiveDate, to: NaiveDate) -> Vec<&EntityEvent> {
469 self.entity_timeline
470 .range(from..=to)
471 .flat_map(|(_, events)| events.iter())
472 .collect()
473 }
474
475 pub fn timeline_dates(&self) -> impl Iterator<Item = &NaiveDate> {
477 self.entity_timeline.keys()
478 }
479
480 pub fn validate_reference(
483 &self,
484 entity_id: &EntityId,
485 transaction_date: NaiveDate,
486 ) -> Result<(), String> {
487 match self.entities.get(entity_id) {
488 None => Err(format!("Entity {entity_id} does not exist")),
489 Some(record) => {
490 if transaction_date < record.valid_from {
491 Err(format!(
492 "Entity {} is not valid until {} (transaction date: {})",
493 entity_id, record.valid_from, transaction_date
494 ))
495 } else if let Some(valid_to) = record.valid_to {
496 if transaction_date > valid_to {
497 Err(format!(
498 "Entity {entity_id} validity expired on {valid_to} (transaction date: {transaction_date})"
499 ))
500 } else if record.status != EntityStatus::Active {
501 Err(format!(
502 "Entity {} has status {:?} (not active)",
503 entity_id, record.status
504 ))
505 } else {
506 Ok(())
507 }
508 } else if record.status != EntityStatus::Active {
509 Err(format!(
510 "Entity {} has status {:?} (not active)",
511 entity_id, record.status
512 ))
513 } else {
514 Ok(())
515 }
516 }
517 }
518 }
519
520 pub fn rebuild_indices(&mut self) {
522 self.by_type.clear();
523 self.by_company.clear();
524
525 for (entity_id, record) in &self.entities {
526 self.by_type
527 .entry(entity_id.entity_type)
528 .or_default()
529 .push(entity_id.clone());
530
531 if let Some(cc) = &record.company_code {
532 self.by_company
533 .entry(cc.clone())
534 .or_default()
535 .push(entity_id.clone());
536 }
537 }
538 }
539
540 pub fn register_entity(&mut self, record: EntityRecord) {
544 self.register(record);
545 }
546
547 pub fn record_event(&mut self, event: EntityEvent) {
549 self.entity_timeline
550 .entry(event.event_date)
551 .or_default()
552 .push(event);
553 }
554
555 pub fn is_valid_on(&self, entity_id: &EntityId, date: NaiveDate) -> bool {
558 self.is_valid(entity_id, date)
559 }
560}
561
562#[cfg(test)]
563#[allow(clippy::unwrap_used)]
564mod tests {
565 use super::*;
566
567 fn test_date(days: i64) -> NaiveDate {
568 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap() + chrono::Duration::days(days)
569 }
570
571 #[test]
572 fn test_entity_registration() {
573 let mut registry = EntityRegistry::new();
574
575 let entity_id = EntityId::vendor("V-001");
576 let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(0));
577
578 registry.register(record);
579
580 assert!(registry.exists(&entity_id));
581 assert_eq!(registry.count_by_type(EntityType::Vendor), 1);
582 }
583
584 #[test]
585 fn test_entity_validity() {
586 let mut registry = EntityRegistry::new();
587
588 let entity_id = EntityId::vendor("V-001");
589 let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(10))
590 .with_validity(test_date(10), Some(test_date(100)));
591
592 registry.register(record);
593
594 assert!(!registry.is_valid(&entity_id, test_date(5)));
596
597 assert!(registry.is_valid(&entity_id, test_date(50)));
599
600 assert!(!registry.is_valid(&entity_id, test_date(150)));
602 }
603
604 #[test]
605 fn test_entity_blocking() {
606 let mut registry = EntityRegistry::new();
607
608 let entity_id = EntityId::vendor("V-001");
609 let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(0));
610
611 registry.register(record);
612
613 assert!(registry.can_transact(&entity_id, test_date(5)));
615
616 registry.block(&entity_id, test_date(10));
618
619 assert!(!registry.can_transact(&entity_id, test_date(15)));
621
622 registry.unblock(&entity_id, test_date(20));
624
625 assert!(registry.can_transact(&entity_id, test_date(25)));
627 }
628
629 #[test]
630 fn test_entity_timeline() {
631 let mut registry = EntityRegistry::new();
632
633 let entity1 = EntityId::vendor("V-001");
634 let entity2 = EntityId::vendor("V-002");
635
636 registry.register(EntityRecord::new(entity1.clone(), "Vendor 1", test_date(0)));
637 registry.register(EntityRecord::new(entity2.clone(), "Vendor 2", test_date(5)));
638
639 let events_day0 = registry.get_events_on(test_date(0));
640 assert_eq!(events_day0.len(), 1);
641
642 let events_range = registry.get_events_in_range(test_date(0), test_date(10));
643 assert_eq!(events_range.len(), 2);
644 }
645
646 #[test]
647 fn test_company_index() {
648 let mut registry = EntityRegistry::new();
649
650 let entity1 = EntityId::vendor("V-001");
651 let entity2 = EntityId::vendor("V-002");
652 let entity3 = EntityId::customer("C-001");
653
654 registry.register(
655 EntityRecord::new(entity1.clone(), "Vendor 1", test_date(0)).with_company_code("1000"),
656 );
657 registry.register(
658 EntityRecord::new(entity2.clone(), "Vendor 2", test_date(0)).with_company_code("2000"),
659 );
660 registry.register(
661 EntityRecord::new(entity3.clone(), "Customer 1", test_date(0))
662 .with_company_code("1000"),
663 );
664
665 let company_1000_entities = registry.get_by_company("1000");
666 assert_eq!(company_1000_entities.len(), 2);
667 }
668
669 #[test]
670 fn test_validate_reference() {
671 let mut registry = EntityRegistry::new();
672
673 let entity_id = EntityId::vendor("V-001");
674 let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(10))
675 .with_validity(test_date(10), Some(test_date(100)));
676
677 registry.register(record);
678
679 assert!(registry
681 .validate_reference(&entity_id, test_date(5))
682 .is_err());
683
684 assert!(registry
686 .validate_reference(&entity_id, test_date(50))
687 .is_ok());
688
689 assert!(registry
691 .validate_reference(&entity_id, test_date(150))
692 .is_err());
693
694 let fake_id = EntityId::vendor("V-999");
696 assert!(registry
697 .validate_reference(&fake_id, test_date(50))
698 .is_err());
699 }
700}