use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EntityId {
pub entity_type: EntityType,
pub id: String,
}
impl EntityId {
pub fn new(entity_type: EntityType, id: impl Into<String>) -> Self {
Self {
entity_type,
id: id.into(),
}
}
pub fn vendor(id: impl Into<String>) -> Self {
Self::new(EntityType::Vendor, id)
}
pub fn customer(id: impl Into<String>) -> Self {
Self::new(EntityType::Customer, id)
}
pub fn material(id: impl Into<String>) -> Self {
Self::new(EntityType::Material, id)
}
pub fn fixed_asset(id: impl Into<String>) -> Self {
Self::new(EntityType::FixedAsset, id)
}
pub fn employee(id: impl Into<String>) -> Self {
Self::new(EntityType::Employee, id)
}
pub fn cost_center(id: impl Into<String>) -> Self {
Self::new(EntityType::CostCenter, id)
}
pub fn profit_center(id: impl Into<String>) -> Self {
Self::new(EntityType::ProfitCenter, id)
}
pub fn gl_account(id: impl Into<String>) -> Self {
Self::new(EntityType::GlAccount, id)
}
}
impl std::fmt::Display for EntityId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.entity_type, self.id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntityType {
Vendor,
Customer,
Material,
FixedAsset,
Employee,
CostCenter,
ProfitCenter,
GlAccount,
CompanyCode,
BusinessPartner,
Project,
InternalOrder,
Company,
Department,
Contract,
Asset,
BankAccount,
PurchaseOrder,
SalesOrder,
Invoice,
Payment,
}
impl std::fmt::Display for EntityType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
Self::Vendor => "VENDOR",
Self::Customer => "CUSTOMER",
Self::Material => "MATERIAL",
Self::FixedAsset => "FIXED_ASSET",
Self::Employee => "EMPLOYEE",
Self::CostCenter => "COST_CENTER",
Self::ProfitCenter => "PROFIT_CENTER",
Self::GlAccount => "GL_ACCOUNT",
Self::CompanyCode => "COMPANY_CODE",
Self::BusinessPartner => "BUSINESS_PARTNER",
Self::Project => "PROJECT",
Self::InternalOrder => "INTERNAL_ORDER",
Self::Company => "COMPANY",
Self::Department => "DEPARTMENT",
Self::Contract => "CONTRACT",
Self::Asset => "ASSET",
Self::BankAccount => "BANK_ACCOUNT",
Self::PurchaseOrder => "PURCHASE_ORDER",
Self::SalesOrder => "SALES_ORDER",
Self::Invoice => "INVOICE",
Self::Payment => "PAYMENT",
};
write!(f, "{name}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntityStatus {
#[default]
Active,
Blocked,
MarkedForDeletion,
Archived,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityRecord {
pub entity_id: EntityId,
pub name: String,
pub company_code: Option<String>,
pub created_date: NaiveDate,
pub valid_from: NaiveDate,
pub valid_to: Option<NaiveDate>,
pub status: EntityStatus,
pub status_changed_date: Option<NaiveDate>,
pub attributes: HashMap<String, String>,
}
impl EntityRecord {
pub fn new(entity_id: EntityId, name: impl Into<String>, created_date: NaiveDate) -> Self {
Self {
entity_id,
name: name.into(),
company_code: None,
created_date,
valid_from: created_date,
valid_to: None,
status: EntityStatus::Active,
status_changed_date: None,
attributes: HashMap::new(),
}
}
pub fn with_company_code(mut self, company_code: impl Into<String>) -> Self {
self.company_code = Some(company_code.into());
self
}
pub fn with_validity(mut self, from: NaiveDate, to: Option<NaiveDate>) -> Self {
self.valid_from = from;
self.valid_to = to;
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.insert(key.into(), value.into());
self
}
pub fn is_valid_on(&self, date: NaiveDate) -> bool {
date >= self.valid_from
&& self.valid_to.is_none_or(|to| date <= to)
&& self.status == EntityStatus::Active
}
pub fn can_transact_on(&self, date: NaiveDate) -> bool {
self.is_valid_on(date) && self.status == EntityStatus::Active
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityEvent {
pub entity_id: EntityId,
pub event_type: EntityEventType,
pub event_date: NaiveDate,
pub description: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntityEventType {
Created,
Activated,
Blocked,
Unblocked,
MarkedForDeletion,
Archived,
ValidityChanged,
Transferred,
Modified,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EntityRegistry {
entities: HashMap<EntityId, EntityRecord>,
by_type: HashMap<EntityType, Vec<EntityId>>,
by_company: HashMap<String, Vec<EntityId>>,
entity_timeline: BTreeMap<NaiveDate, Vec<EntityEvent>>,
}
impl EntityRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, record: EntityRecord) {
let entity_id = record.entity_id.clone();
let entity_type = entity_id.entity_type;
let company_code = record.company_code.clone();
let created_date = record.created_date;
self.entities.insert(entity_id.clone(), record);
self.by_type
.entry(entity_type)
.or_default()
.push(entity_id.clone());
if let Some(cc) = company_code {
self.by_company
.entry(cc)
.or_default()
.push(entity_id.clone());
}
let event = EntityEvent {
entity_id,
event_type: EntityEventType::Created,
event_date: created_date,
description: Some("Entity created".to_string()),
};
self.entity_timeline
.entry(created_date)
.or_default()
.push(event);
}
pub fn get(&self, entity_id: &EntityId) -> Option<&EntityRecord> {
self.entities.get(entity_id)
}
pub fn get_mut(&mut self, entity_id: &EntityId) -> Option<&mut EntityRecord> {
self.entities.get_mut(entity_id)
}
pub fn exists(&self, entity_id: &EntityId) -> bool {
self.entities.contains_key(entity_id)
}
pub fn is_valid(&self, entity_id: &EntityId, date: NaiveDate) -> bool {
self.entities
.get(entity_id)
.is_some_and(|r| r.is_valid_on(date))
}
pub fn can_transact(&self, entity_id: &EntityId, date: NaiveDate) -> bool {
self.entities
.get(entity_id)
.is_some_and(|r| r.can_transact_on(date))
}
pub fn get_by_type(&self, entity_type: EntityType) -> Vec<&EntityRecord> {
self.by_type
.get(&entity_type)
.map(|ids| ids.iter().filter_map(|id| self.entities.get(id)).collect())
.unwrap_or_default()
}
pub fn get_valid_by_type(
&self,
entity_type: EntityType,
date: NaiveDate,
) -> Vec<&EntityRecord> {
self.get_by_type(entity_type)
.into_iter()
.filter(|r| r.is_valid_on(date))
.collect()
}
pub fn get_by_company(&self, company_code: &str) -> Vec<&EntityRecord> {
self.by_company
.get(company_code)
.map(|ids| ids.iter().filter_map(|id| self.entities.get(id)).collect())
.unwrap_or_default()
}
pub fn get_ids_by_type(&self, entity_type: EntityType) -> Vec<&EntityId> {
self.by_type
.get(&entity_type)
.map(|ids| ids.iter().collect())
.unwrap_or_default()
}
pub fn count_by_type(&self, entity_type: EntityType) -> usize {
self.by_type.get(&entity_type).map_or(0, std::vec::Vec::len)
}
pub fn total_count(&self) -> usize {
self.entities.len()
}
pub fn update_status(
&mut self,
entity_id: &EntityId,
new_status: EntityStatus,
date: NaiveDate,
) -> bool {
if let Some(record) = self.entities.get_mut(entity_id) {
let old_status = record.status;
record.status = new_status;
record.status_changed_date = Some(date);
let event_type = match new_status {
EntityStatus::Active if old_status == EntityStatus::Blocked => {
EntityEventType::Unblocked
}
EntityStatus::Active => EntityEventType::Activated,
EntityStatus::Blocked => EntityEventType::Blocked,
EntityStatus::MarkedForDeletion => EntityEventType::MarkedForDeletion,
EntityStatus::Archived => EntityEventType::Archived,
};
let event = EntityEvent {
entity_id: entity_id.clone(),
event_type,
event_date: date,
description: Some(format!(
"Status changed from {old_status:?} to {new_status:?}"
)),
};
self.entity_timeline.entry(date).or_default().push(event);
true
} else {
false
}
}
pub fn block(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
self.update_status(entity_id, EntityStatus::Blocked, date)
}
pub fn unblock(&mut self, entity_id: &EntityId, date: NaiveDate) -> bool {
self.update_status(entity_id, EntityStatus::Active, date)
}
pub fn get_events_on(&self, date: NaiveDate) -> &[EntityEvent] {
self.entity_timeline
.get(&date)
.map(std::vec::Vec::as_slice)
.unwrap_or(&[])
}
pub fn get_events_in_range(&self, from: NaiveDate, to: NaiveDate) -> Vec<&EntityEvent> {
self.entity_timeline
.range(from..=to)
.flat_map(|(_, events)| events.iter())
.collect()
}
pub fn timeline_dates(&self) -> impl Iterator<Item = &NaiveDate> {
self.entity_timeline.keys()
}
pub fn validate_reference(
&self,
entity_id: &EntityId,
transaction_date: NaiveDate,
) -> Result<(), String> {
match self.entities.get(entity_id) {
None => Err(format!("Entity {entity_id} does not exist")),
Some(record) => {
if transaction_date < record.valid_from {
Err(format!(
"Entity {} is not valid until {} (transaction date: {})",
entity_id, record.valid_from, transaction_date
))
} else if let Some(valid_to) = record.valid_to {
if transaction_date > valid_to {
Err(format!(
"Entity {entity_id} validity expired on {valid_to} (transaction date: {transaction_date})"
))
} else if record.status != EntityStatus::Active {
Err(format!(
"Entity {} has status {:?} (not active)",
entity_id, record.status
))
} else {
Ok(())
}
} else if record.status != EntityStatus::Active {
Err(format!(
"Entity {} has status {:?} (not active)",
entity_id, record.status
))
} else {
Ok(())
}
}
}
}
pub fn rebuild_indices(&mut self) {
self.by_type.clear();
self.by_company.clear();
for (entity_id, record) in &self.entities {
self.by_type
.entry(entity_id.entity_type)
.or_default()
.push(entity_id.clone());
if let Some(cc) = &record.company_code {
self.by_company
.entry(cc.clone())
.or_default()
.push(entity_id.clone());
}
}
}
pub fn register_entity(&mut self, record: EntityRecord) {
self.register(record);
}
pub fn record_event(&mut self, event: EntityEvent) {
self.entity_timeline
.entry(event.event_date)
.or_default()
.push(event);
}
pub fn is_valid_on(&self, entity_id: &EntityId, date: NaiveDate) -> bool {
self.is_valid(entity_id, date)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
fn test_date(days: i64) -> NaiveDate {
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap() + chrono::Duration::days(days)
}
#[test]
fn test_entity_registration() {
let mut registry = EntityRegistry::new();
let entity_id = EntityId::vendor("V-001");
let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(0));
registry.register(record);
assert!(registry.exists(&entity_id));
assert_eq!(registry.count_by_type(EntityType::Vendor), 1);
}
#[test]
fn test_entity_validity() {
let mut registry = EntityRegistry::new();
let entity_id = EntityId::vendor("V-001");
let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(10))
.with_validity(test_date(10), Some(test_date(100)));
registry.register(record);
assert!(!registry.is_valid(&entity_id, test_date(5)));
assert!(registry.is_valid(&entity_id, test_date(50)));
assert!(!registry.is_valid(&entity_id, test_date(150)));
}
#[test]
fn test_entity_blocking() {
let mut registry = EntityRegistry::new();
let entity_id = EntityId::vendor("V-001");
let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(0));
registry.register(record);
assert!(registry.can_transact(&entity_id, test_date(5)));
registry.block(&entity_id, test_date(10));
assert!(!registry.can_transact(&entity_id, test_date(15)));
registry.unblock(&entity_id, test_date(20));
assert!(registry.can_transact(&entity_id, test_date(25)));
}
#[test]
fn test_entity_timeline() {
let mut registry = EntityRegistry::new();
let entity1 = EntityId::vendor("V-001");
let entity2 = EntityId::vendor("V-002");
registry.register(EntityRecord::new(entity1.clone(), "Vendor 1", test_date(0)));
registry.register(EntityRecord::new(entity2.clone(), "Vendor 2", test_date(5)));
let events_day0 = registry.get_events_on(test_date(0));
assert_eq!(events_day0.len(), 1);
let events_range = registry.get_events_in_range(test_date(0), test_date(10));
assert_eq!(events_range.len(), 2);
}
#[test]
fn test_company_index() {
let mut registry = EntityRegistry::new();
let entity1 = EntityId::vendor("V-001");
let entity2 = EntityId::vendor("V-002");
let entity3 = EntityId::customer("C-001");
registry.register(
EntityRecord::new(entity1.clone(), "Vendor 1", test_date(0)).with_company_code("1000"),
);
registry.register(
EntityRecord::new(entity2.clone(), "Vendor 2", test_date(0)).with_company_code("2000"),
);
registry.register(
EntityRecord::new(entity3.clone(), "Customer 1", test_date(0))
.with_company_code("1000"),
);
let company_1000_entities = registry.get_by_company("1000");
assert_eq!(company_1000_entities.len(), 2);
}
#[test]
fn test_validate_reference() {
let mut registry = EntityRegistry::new();
let entity_id = EntityId::vendor("V-001");
let record = EntityRecord::new(entity_id.clone(), "Test Vendor", test_date(10))
.with_validity(test_date(10), Some(test_date(100)));
registry.register(record);
assert!(registry
.validate_reference(&entity_id, test_date(5))
.is_err());
assert!(registry
.validate_reference(&entity_id, test_date(50))
.is_ok());
assert!(registry
.validate_reference(&entity_id, test_date(150))
.is_err());
let fake_id = EntityId::vendor("V-999");
assert!(registry
.validate_reference(&fake_id, test_date(50))
.is_err());
}
}