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)]
563mod tests {
564 use super::*;
565
566 fn test_date(days: i64) -> NaiveDate {
567 NaiveDate::from_ymd_opt(2024, 1, 1).unwrap() + chrono::Duration::days(days)
568 }
569
570 #[test]
571 fn test_entity_registration() {
572 let mut registry = EntityRegistry::new();
573
574 let entity_id = EntityId::vendor("V-001");
575 let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(0));
576
577 registry.register(record);
578
579 assert!(registry.exists(&entity_id));
580 assert_eq!(registry.count_by_type(EntityType::Vendor), 1);
581 }
582
583 #[test]
584 fn test_entity_validity() {
585 let mut registry = EntityRegistry::new();
586
587 let entity_id = EntityId::vendor("V-001");
588 let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(10))
589 .with_validity(test_date(10), Some(test_date(100)));
590
591 registry.register(record);
592
593 assert!(!registry.is_valid(&entity_id, test_date(5)));
595
596 assert!(registry.is_valid(&entity_id, test_date(50)));
598
599 assert!(!registry.is_valid(&entity_id, test_date(150)));
601 }
602
603 #[test]
604 fn test_entity_blocking() {
605 let mut registry = EntityRegistry::new();
606
607 let entity_id = EntityId::vendor("V-001");
608 let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(0));
609
610 registry.register(record);
611
612 assert!(registry.can_transact(&entity_id, test_date(5)));
614
615 registry.block(&entity_id, test_date(10));
617
618 assert!(!registry.can_transact(&entity_id, test_date(15)));
620
621 registry.unblock(&entity_id, test_date(20));
623
624 assert!(registry.can_transact(&entity_id, test_date(25)));
626 }
627
628 #[test]
629 fn test_entity_timeline() {
630 let mut registry = EntityRegistry::new();
631
632 let entity1 = EntityId::vendor("V-001");
633 let entity2 = EntityId::vendor("V-002");
634
635 registry.register(EntityRecord::new(entity1.clone(), "Vendor 1", test_date(0)));
636 registry.register(EntityRecord::new(entity2.clone(), "Vendor 2", test_date(5)));
637
638 let events_day0 = registry.get_events_on(test_date(0));
639 assert_eq!(events_day0.len(), 1);
640
641 let events_range = registry.get_events_in_range(test_date(0), test_date(10));
642 assert_eq!(events_range.len(), 2);
643 }
644
645 #[test]
646 fn test_company_index() {
647 let mut registry = EntityRegistry::new();
648
649 let entity1 = EntityId::vendor("V-001");
650 let entity2 = EntityId::vendor("V-002");
651 let entity3 = EntityId::customer("C-001");
652
653 registry.register(
654 EntityRecord::new(entity1.clone(), "Vendor 1", test_date(0)).with_company_code("1000"),
655 );
656 registry.register(
657 EntityRecord::new(entity2.clone(), "Vendor 2", test_date(0)).with_company_code("2000"),
658 );
659 registry.register(
660 EntityRecord::new(entity3.clone(), "Customer 1", test_date(0))
661 .with_company_code("1000"),
662 );
663
664 let company_1000_entities = registry.get_by_company("1000");
665 assert_eq!(company_1000_entities.len(), 2);
666 }
667
668 #[test]
669 fn test_validate_reference() {
670 let mut registry = EntityRegistry::new();
671
672 let entity_id = EntityId::vendor("V-001");
673 let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(10))
674 .with_validity(test_date(10), Some(test_date(100)));
675
676 registry.register(record);
677
678 assert!(registry
680 .validate_reference(&entity_id, test_date(5))
681 .is_err());
682
683 assert!(registry
685 .validate_reference(&entity_id, test_date(50))
686 .is_ok());
687
688 assert!(registry
690 .validate_reference(&entity_id, test_date(150))
691 .is_err());
692
693 let fake_id = EntityId::vendor("V-999");
695 assert!(registry
696 .validate_reference(&fake_id, test_date(50))
697 .is_err());
698 }
699}