use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GraphEntityType {
Company,
Vendor,
Customer,
Employee,
Department,
CostCenter,
Project,
Contract,
Asset,
BankAccount,
Material,
GlAccount,
PurchaseOrder,
SalesOrder,
Invoice,
Payment,
SourcingProject,
RfxEvent,
ProductionOrder,
BankReconciliation,
TaxJurisdiction,
TaxCode,
TaxLine,
TaxReturn,
TaxProvision,
WithholdingTaxRecord,
UncertainTaxPosition,
CashPosition,
CashForecast,
CashPool,
CashPoolSweep,
HedgingInstrument,
HedgeRelationship,
DebtInstrument,
DebtCovenant,
EmissionRecord,
EnergyConsumption,
WaterUsage,
WasteRecord,
WorkforceDiversityMetric,
PayEquityMetric,
SafetyIncident,
SafetyMetric,
GovernanceMetric,
SupplierEsgAssessment,
MaterialityAssessment,
EsgDisclosure,
ClimateScenario,
ProjectCostLine,
ProjectRevenue,
EarnedValueMetric,
ChangeOrder,
ProjectMilestone,
SupplierBid,
BidEvaluation,
ProcurementContract,
SupplierQualification,
PayrollRun,
TimeEntry,
ExpenseReport,
BenefitEnrollment,
QualityInspection,
CycleCount,
BomComponent,
InventoryMovement,
CosoComponent,
CosoPrinciple,
SoxAssertion,
AuditEngagement,
ProfessionalJudgment,
}
impl GraphEntityType {
pub fn code(&self) -> &'static str {
match self {
Self::Company => "CO",
Self::Vendor => "VN",
Self::Customer => "CU",
Self::Employee => "EM",
Self::Department => "DP",
Self::CostCenter => "CC",
Self::Project => "PJ",
Self::Contract => "CT",
Self::Asset => "AS",
Self::BankAccount => "BA",
Self::Material => "MT",
Self::GlAccount => "GL",
Self::PurchaseOrder => "PO",
Self::SalesOrder => "SO",
Self::Invoice => "IV",
Self::Payment => "PM",
Self::SourcingProject => "SP",
Self::RfxEvent => "RX",
Self::ProductionOrder => "PR",
Self::BankReconciliation => "BR",
Self::TaxJurisdiction => "TJ",
Self::TaxCode => "TC",
Self::TaxLine => "TL",
Self::TaxReturn => "TR",
Self::TaxProvision => "TP",
Self::WithholdingTaxRecord => "WH",
Self::UncertainTaxPosition => "UT",
Self::CashPosition => "CP",
Self::CashForecast => "CF",
Self::CashPool => "CL",
Self::CashPoolSweep => "CS",
Self::HedgingInstrument => "HI",
Self::HedgeRelationship => "HR",
Self::DebtInstrument => "DI",
Self::DebtCovenant => "DC",
Self::EmissionRecord => "ER",
Self::EnergyConsumption => "EC",
Self::WaterUsage => "WU",
Self::WasteRecord => "WR",
Self::WorkforceDiversityMetric => "WD",
Self::PayEquityMetric => "PE",
Self::SafetyIncident => "SI",
Self::SafetyMetric => "SM",
Self::GovernanceMetric => "GM",
Self::SupplierEsgAssessment => "SE",
Self::MaterialityAssessment => "MA",
Self::EsgDisclosure => "ED",
Self::ClimateScenario => "CZ",
Self::ProjectCostLine => "PL",
Self::ProjectRevenue => "PV",
Self::EarnedValueMetric => "EV",
Self::ChangeOrder => "CR",
Self::ProjectMilestone => "MS",
Self::SupplierBid => "BD",
Self::BidEvaluation => "BE",
Self::ProcurementContract => "PC",
Self::SupplierQualification => "SQ",
Self::PayrollRun => "PY",
Self::TimeEntry => "TE",
Self::ExpenseReport => "EX",
Self::BenefitEnrollment => "BN",
Self::QualityInspection => "QI",
Self::CycleCount => "CY",
Self::BomComponent => "BC",
Self::InventoryMovement => "IM",
Self::CosoComponent => "GC",
Self::CosoPrinciple => "GP",
Self::SoxAssertion => "SA",
Self::AuditEngagement => "AE",
Self::ProfessionalJudgment => "JD",
}
}
pub fn numeric_code(&self) -> u16 {
match self {
Self::Company => 100,
Self::Vendor => 101,
Self::Material => 102,
Self::Customer => 103,
Self::Employee => 104,
Self::InventoryMovement => 105,
Self::GlAccount => 106,
Self::Department => 107,
Self::CostCenter => 108,
Self::Project => 109,
Self::Contract => 110,
Self::Asset => 111,
Self::BankAccount => 112,
Self::PurchaseOrder => 200,
Self::SalesOrder => 201,
Self::Invoice => 202,
Self::Payment => 203,
Self::BankReconciliation => 210,
Self::SourcingProject => 320,
Self::RfxEvent => 321,
Self::SupplierBid => 322,
Self::BidEvaluation => 323,
Self::ProcurementContract => 324,
Self::SupplierQualification => 325,
Self::PayrollRun => 330,
Self::TimeEntry => 331,
Self::ExpenseReport => 332,
Self::BenefitEnrollment => 333,
Self::ProductionOrder => 340,
Self::QualityInspection => 341,
Self::CycleCount => 342,
Self::BomComponent => 343,
Self::TaxJurisdiction => 410,
Self::TaxCode => 411,
Self::TaxLine => 412,
Self::TaxReturn => 413,
Self::TaxProvision => 414,
Self::WithholdingTaxRecord => 415,
Self::UncertainTaxPosition => 416,
Self::CashPosition => 420,
Self::CashForecast => 421,
Self::CashPool => 422,
Self::CashPoolSweep => 423,
Self::HedgingInstrument => 424,
Self::HedgeRelationship => 425,
Self::DebtInstrument => 426,
Self::DebtCovenant => 427,
Self::EmissionRecord => 430,
Self::EnergyConsumption => 431,
Self::WaterUsage => 432,
Self::WasteRecord => 433,
Self::WorkforceDiversityMetric => 434,
Self::PayEquityMetric => 435,
Self::SafetyIncident => 436,
Self::SafetyMetric => 437,
Self::GovernanceMetric => 438,
Self::SupplierEsgAssessment => 439,
Self::MaterialityAssessment => 440,
Self::EsgDisclosure => 441,
Self::ClimateScenario => 442,
Self::ProjectCostLine => 451,
Self::ProjectRevenue => 452,
Self::EarnedValueMetric => 453,
Self::ChangeOrder => 454,
Self::ProjectMilestone => 455,
Self::CosoComponent => 500,
Self::CosoPrinciple => 501,
Self::SoxAssertion => 502,
Self::AuditEngagement => 360,
Self::ProfessionalJudgment => 365,
}
}
pub fn node_type_name(&self) -> &'static str {
match self {
Self::Company => "company",
Self::Vendor => "vendor",
Self::Customer => "customer",
Self::Employee => "employee",
Self::Department => "department",
Self::CostCenter => "cost_center",
Self::Project => "project",
Self::Contract => "contract",
Self::Asset => "asset",
Self::BankAccount => "bank_account",
Self::Material => "material",
Self::GlAccount => "gl_account",
Self::PurchaseOrder => "purchase_order",
Self::SalesOrder => "sales_order",
Self::Invoice => "invoice",
Self::Payment => "payment",
Self::SourcingProject => "sourcing_project",
Self::RfxEvent => "rfx_event",
Self::ProductionOrder => "production_order",
Self::BankReconciliation => "bank_reconciliation",
Self::TaxJurisdiction => "tax_jurisdiction",
Self::TaxCode => "tax_code",
Self::TaxLine => "tax_line",
Self::TaxReturn => "tax_return",
Self::TaxProvision => "tax_provision",
Self::WithholdingTaxRecord => "withholding_tax_record",
Self::UncertainTaxPosition => "uncertain_tax_position",
Self::CashPosition => "cash_position",
Self::CashForecast => "cash_forecast",
Self::CashPool => "cash_pool",
Self::CashPoolSweep => "cash_pool_sweep",
Self::HedgingInstrument => "hedging_instrument",
Self::HedgeRelationship => "hedge_relationship",
Self::DebtInstrument => "debt_instrument",
Self::DebtCovenant => "debt_covenant",
Self::EmissionRecord => "emission_record",
Self::EnergyConsumption => "energy_consumption",
Self::WaterUsage => "water_usage",
Self::WasteRecord => "waste_record",
Self::WorkforceDiversityMetric => "workforce_diversity_metric",
Self::PayEquityMetric => "pay_equity_metric",
Self::SafetyIncident => "safety_incident",
Self::SafetyMetric => "safety_metric",
Self::GovernanceMetric => "governance_metric",
Self::SupplierEsgAssessment => "supplier_esg_assessment",
Self::MaterialityAssessment => "materiality_assessment",
Self::EsgDisclosure => "esg_disclosure",
Self::ClimateScenario => "climate_scenario",
Self::ProjectCostLine => "project_cost_line",
Self::ProjectRevenue => "project_revenue",
Self::EarnedValueMetric => "earned_value_metric",
Self::ChangeOrder => "change_order",
Self::ProjectMilestone => "project_milestone",
Self::SupplierBid => "supplier_bid",
Self::BidEvaluation => "bid_evaluation",
Self::ProcurementContract => "procurement_contract",
Self::SupplierQualification => "supplier_qualification",
Self::PayrollRun => "payroll_run",
Self::TimeEntry => "time_entry",
Self::ExpenseReport => "expense_report",
Self::BenefitEnrollment => "benefit_enrollment",
Self::QualityInspection => "quality_inspection",
Self::CycleCount => "cycle_count",
Self::BomComponent => "bom_component",
Self::InventoryMovement => "inventory_movement",
Self::CosoComponent => "coso_component",
Self::CosoPrinciple => "coso_principle",
Self::SoxAssertion => "sox_assertion",
Self::AuditEngagement => "audit_engagement",
Self::ProfessionalJudgment => "professional_judgment",
}
}
pub fn from_numeric_code(code: u16) -> Option<Self> {
Self::all_types()
.iter()
.find(|t| t.numeric_code() == code)
.copied()
}
pub fn from_node_type_name(name: &str) -> Option<Self> {
Self::all_types()
.iter()
.find(|t| t.node_type_name() == name)
.copied()
}
pub fn all_types() -> &'static [GraphEntityType] {
&[
Self::Company,
Self::Vendor,
Self::Customer,
Self::Employee,
Self::Department,
Self::CostCenter,
Self::Project,
Self::Contract,
Self::Asset,
Self::BankAccount,
Self::Material,
Self::GlAccount,
Self::PurchaseOrder,
Self::SalesOrder,
Self::Invoice,
Self::Payment,
Self::SourcingProject,
Self::RfxEvent,
Self::ProductionOrder,
Self::BankReconciliation,
Self::TaxJurisdiction,
Self::TaxCode,
Self::TaxLine,
Self::TaxReturn,
Self::TaxProvision,
Self::WithholdingTaxRecord,
Self::UncertainTaxPosition,
Self::CashPosition,
Self::CashForecast,
Self::CashPool,
Self::CashPoolSweep,
Self::HedgingInstrument,
Self::HedgeRelationship,
Self::DebtInstrument,
Self::DebtCovenant,
Self::EmissionRecord,
Self::EnergyConsumption,
Self::WaterUsage,
Self::WasteRecord,
Self::WorkforceDiversityMetric,
Self::PayEquityMetric,
Self::SafetyIncident,
Self::SafetyMetric,
Self::GovernanceMetric,
Self::SupplierEsgAssessment,
Self::MaterialityAssessment,
Self::EsgDisclosure,
Self::ClimateScenario,
Self::ProjectCostLine,
Self::ProjectRevenue,
Self::EarnedValueMetric,
Self::ChangeOrder,
Self::ProjectMilestone,
Self::SupplierBid,
Self::BidEvaluation,
Self::ProcurementContract,
Self::SupplierQualification,
Self::PayrollRun,
Self::TimeEntry,
Self::ExpenseReport,
Self::BenefitEnrollment,
Self::QualityInspection,
Self::CycleCount,
Self::BomComponent,
Self::InventoryMovement,
Self::CosoComponent,
Self::CosoPrinciple,
Self::SoxAssertion,
Self::AuditEngagement,
Self::ProfessionalJudgment,
]
}
pub fn is_tax(&self) -> bool {
matches!(
self,
Self::TaxJurisdiction
| Self::TaxCode
| Self::TaxLine
| Self::TaxReturn
| Self::TaxProvision
| Self::WithholdingTaxRecord
| Self::UncertainTaxPosition
)
}
pub fn is_treasury(&self) -> bool {
matches!(
self,
Self::CashPosition
| Self::CashForecast
| Self::CashPool
| Self::CashPoolSweep
| Self::HedgingInstrument
| Self::HedgeRelationship
| Self::DebtInstrument
| Self::DebtCovenant
)
}
pub fn is_esg(&self) -> bool {
matches!(
self,
Self::EmissionRecord
| Self::EnergyConsumption
| Self::WaterUsage
| Self::WasteRecord
| Self::WorkforceDiversityMetric
| Self::PayEquityMetric
| Self::SafetyIncident
| Self::SafetyMetric
| Self::GovernanceMetric
| Self::SupplierEsgAssessment
| Self::MaterialityAssessment
| Self::EsgDisclosure
| Self::ClimateScenario
)
}
pub fn is_project(&self) -> bool {
matches!(
self,
Self::Project
| Self::ProjectCostLine
| Self::ProjectRevenue
| Self::EarnedValueMetric
| Self::ChangeOrder
| Self::ProjectMilestone
)
}
pub fn is_h2r(&self) -> bool {
matches!(
self,
Self::PayrollRun | Self::TimeEntry | Self::ExpenseReport | Self::BenefitEnrollment
)
}
pub fn is_mfg(&self) -> bool {
matches!(
self,
Self::ProductionOrder
| Self::QualityInspection
| Self::CycleCount
| Self::BomComponent
| Self::Material
| Self::InventoryMovement
)
}
pub fn is_governance(&self) -> bool {
matches!(
self,
Self::CosoComponent
| Self::CosoPrinciple
| Self::SoxAssertion
| Self::AuditEngagement
| Self::ProfessionalJudgment
)
}
pub fn is_master_data(&self) -> bool {
matches!(
self,
Self::Company
| Self::Vendor
| Self::Customer
| Self::Employee
| Self::Department
| Self::CostCenter
| Self::Material
| Self::GlAccount
| Self::TaxJurisdiction
| Self::TaxCode
| Self::BankAccount
)
}
pub fn is_transactional(&self) -> bool {
matches!(
self,
Self::PurchaseOrder
| Self::SalesOrder
| Self::Invoice
| Self::Payment
| Self::TaxLine
| Self::TaxReturn
| Self::CashPoolSweep
| Self::InventoryMovement
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RelationshipType {
BuysFrom,
SellsTo,
PaysTo,
ReceivesFrom,
SuppliesTo,
SourcesFrom,
ReportsTo,
Manages,
BelongsTo,
OwnedBy,
WorksIn,
ResponsibleFor,
ReferredBy,
PartnersWith,
AffiliatedWith,
Intercompany,
References,
ReferencedBy,
Fulfills,
FulfilledBy,
AppliesTo,
AppliedBy,
InventoryLink,
UsedIn,
SourcedVia,
AwardedTo,
GovernsOrder,
EvaluatedBy,
QualifiedAs,
ScoredBy,
SourcedThrough,
CatalogItemOf,
ProducedBy,
ReconciledWith,
PlacedWith,
MatchesOrder,
PaysInvoice,
PlacedBy,
BillsOrder,
RfxBelongsToProject,
RespondsTo,
AwardedFrom,
RecordedBy,
PayrollIncludes,
SubmittedBy,
EnrolledBy,
Produces,
Inspects,
PartOf,
TaxLineBelongsTo,
ProvisionAppliesTo,
WithheldFrom,
SweepsTo,
HedgesInstrument,
GovernsInstrument,
EmissionReportedBy,
AssessesSupplier,
CostChargedTo,
MilestoneOf,
ModifiesProject,
PrincipleUnder,
AssertionCovers,
JudgmentWithin,
}
impl RelationshipType {
pub fn code(&self) -> &'static str {
match self {
Self::BuysFrom => "BF",
Self::SellsTo => "ST",
Self::PaysTo => "PT",
Self::ReceivesFrom => "RF",
Self::SuppliesTo => "SP",
Self::SourcesFrom => "SF",
Self::ReportsTo => "RT",
Self::Manages => "MG",
Self::BelongsTo => "BT",
Self::OwnedBy => "OB",
Self::WorksIn => "WI",
Self::ResponsibleFor => "RS",
Self::ReferredBy => "RB",
Self::PartnersWith => "PW",
Self::AffiliatedWith => "AW",
Self::Intercompany => "IC",
Self::References => "REF",
Self::ReferencedBy => "RBY",
Self::Fulfills => "FL",
Self::FulfilledBy => "FLB",
Self::AppliesTo => "AP",
Self::AppliedBy => "APB",
Self::InventoryLink => "INV",
Self::UsedIn => "UI",
Self::SourcedVia => "SV",
Self::AwardedTo => "AT",
Self::GovernsOrder => "GO",
Self::EvaluatedBy => "EB",
Self::QualifiedAs => "QA",
Self::ScoredBy => "SB",
Self::SourcedThrough => "STH",
Self::CatalogItemOf => "CIO",
Self::ProducedBy => "PB",
Self::ReconciledWith => "RW",
Self::PlacedWith => "PWI",
Self::MatchesOrder => "MO",
Self::PaysInvoice => "PI",
Self::PlacedBy => "PLB",
Self::BillsOrder => "BO",
Self::RfxBelongsToProject => "RBP",
Self::RespondsTo => "RTO",
Self::AwardedFrom => "AFR",
Self::RecordedBy => "RCB",
Self::PayrollIncludes => "PYI",
Self::SubmittedBy => "SUB",
Self::EnrolledBy => "ENB",
Self::Produces => "PRD",
Self::Inspects => "INS",
Self::PartOf => "POF",
Self::TaxLineBelongsTo => "TLB",
Self::ProvisionAppliesTo => "PAT",
Self::WithheldFrom => "WHF",
Self::SweepsTo => "SWT",
Self::HedgesInstrument => "HDG",
Self::GovernsInstrument => "GVI",
Self::EmissionReportedBy => "ERB",
Self::AssessesSupplier => "ASS",
Self::CostChargedTo => "CCT",
Self::MilestoneOf => "MLO",
Self::ModifiesProject => "MPJ",
Self::PrincipleUnder => "PUN",
Self::AssertionCovers => "ACO",
Self::JudgmentWithin => "JWI",
}
}
pub fn inverse(&self) -> Self {
match self {
Self::BuysFrom => Self::SellsTo,
Self::SellsTo => Self::BuysFrom,
Self::PaysTo => Self::ReceivesFrom,
Self::ReceivesFrom => Self::PaysTo,
Self::SuppliesTo => Self::SourcesFrom,
Self::SourcesFrom => Self::SuppliesTo,
Self::ReportsTo => Self::Manages,
Self::Manages => Self::ReportsTo,
Self::BelongsTo => Self::OwnedBy,
Self::OwnedBy => Self::BelongsTo,
Self::References => Self::ReferencedBy,
Self::ReferencedBy => Self::References,
Self::Fulfills => Self::FulfilledBy,
Self::FulfilledBy => Self::Fulfills,
Self::AppliesTo => Self::AppliedBy,
Self::AppliedBy => Self::AppliesTo,
Self::WorksIn => Self::WorksIn,
Self::ResponsibleFor => Self::ResponsibleFor,
Self::ReferredBy => Self::ReferredBy,
Self::PartnersWith => Self::PartnersWith,
Self::AffiliatedWith => Self::AffiliatedWith,
Self::Intercompany => Self::Intercompany,
Self::InventoryLink => Self::InventoryLink,
Self::UsedIn => Self::UsedIn,
Self::SourcedVia => Self::SourcedVia,
Self::AwardedTo => Self::AwardedTo,
Self::GovernsOrder => Self::GovernsOrder,
Self::EvaluatedBy => Self::EvaluatedBy,
Self::QualifiedAs => Self::QualifiedAs,
Self::ScoredBy => Self::ScoredBy,
Self::SourcedThrough => Self::SourcedThrough,
Self::CatalogItemOf => Self::CatalogItemOf,
Self::ProducedBy => Self::ProducedBy,
Self::ReconciledWith => Self::ReconciledWith,
Self::PlacedWith => Self::PlacedWith,
Self::MatchesOrder => Self::MatchesOrder,
Self::PaysInvoice => Self::PaysInvoice,
Self::PlacedBy => Self::PlacedBy,
Self::BillsOrder => Self::BillsOrder,
Self::RfxBelongsToProject => Self::RfxBelongsToProject,
Self::RespondsTo => Self::RespondsTo,
Self::AwardedFrom => Self::AwardedFrom,
Self::RecordedBy => Self::RecordedBy,
Self::PayrollIncludes => Self::PayrollIncludes,
Self::SubmittedBy => Self::SubmittedBy,
Self::EnrolledBy => Self::EnrolledBy,
Self::Produces => Self::Produces,
Self::Inspects => Self::Inspects,
Self::PartOf => Self::PartOf,
Self::TaxLineBelongsTo => Self::TaxLineBelongsTo,
Self::ProvisionAppliesTo => Self::ProvisionAppliesTo,
Self::WithheldFrom => Self::WithheldFrom,
Self::SweepsTo => Self::SweepsTo,
Self::HedgesInstrument => Self::HedgesInstrument,
Self::GovernsInstrument => Self::GovernsInstrument,
Self::EmissionReportedBy => Self::EmissionReportedBy,
Self::AssessesSupplier => Self::AssessesSupplier,
Self::CostChargedTo => Self::CostChargedTo,
Self::MilestoneOf => Self::MilestoneOf,
Self::ModifiesProject => Self::ModifiesProject,
Self::PrincipleUnder => Self::PrincipleUnder,
Self::AssertionCovers => Self::AssertionCovers,
Self::JudgmentWithin => Self::JudgmentWithin,
}
}
pub fn is_transactional(&self) -> bool {
matches!(
self,
Self::BuysFrom
| Self::SellsTo
| Self::PaysTo
| Self::ReceivesFrom
| Self::SuppliesTo
| Self::SourcesFrom
)
}
pub fn is_organizational(&self) -> bool {
matches!(
self,
Self::ReportsTo
| Self::Manages
| Self::BelongsTo
| Self::OwnedBy
| Self::WorksIn
| Self::ResponsibleFor
)
}
pub fn is_document(&self) -> bool {
matches!(
self,
Self::References
| Self::ReferencedBy
| Self::Fulfills
| Self::FulfilledBy
| Self::AppliesTo
| Self::AppliedBy
)
}
pub fn constraint(&self) -> Option<EdgeConstraint> {
let c = |src: GraphEntityType, tgt: GraphEntityType, card: Cardinality| EdgeConstraint {
relationship_type: *self,
source_type: src,
target_type: tgt,
cardinality: card,
edge_properties: &[],
};
use Cardinality::*;
use GraphEntityType as E;
match self {
Self::PlacedWith => Some(c(E::PurchaseOrder, E::Vendor, ManyToOne)),
Self::MatchesOrder => Some(c(E::Invoice, E::PurchaseOrder, ManyToOne)),
Self::PaysInvoice => Some(c(E::Payment, E::Invoice, ManyToMany)),
Self::PlacedBy => Some(c(E::SalesOrder, E::Customer, ManyToOne)),
Self::BillsOrder => Some(c(E::Invoice, E::SalesOrder, ManyToOne)),
Self::RfxBelongsToProject => Some(c(E::RfxEvent, E::SourcingProject, ManyToOne)),
Self::RespondsTo => Some(c(E::SupplierBid, E::RfxEvent, ManyToOne)),
Self::AwardedFrom => Some(c(E::ProcurementContract, E::BidEvaluation, OneToOne)),
Self::RecordedBy => Some(c(E::TimeEntry, E::Employee, ManyToOne)),
Self::PayrollIncludes => Some(c(E::PayrollRun, E::Employee, ManyToMany)),
Self::SubmittedBy => Some(c(E::ExpenseReport, E::Employee, ManyToOne)),
Self::EnrolledBy => Some(c(E::BenefitEnrollment, E::Employee, ManyToOne)),
Self::Produces => Some(c(E::ProductionOrder, E::Material, ManyToOne)),
Self::Inspects => Some(c(E::QualityInspection, E::ProductionOrder, ManyToOne)),
Self::PartOf => Some(c(E::BomComponent, E::Material, ManyToOne)),
Self::TaxLineBelongsTo => Some(c(E::TaxLine, E::TaxReturn, ManyToOne)),
Self::ProvisionAppliesTo => Some(c(E::TaxProvision, E::TaxJurisdiction, ManyToOne)),
Self::WithheldFrom => Some(c(E::WithholdingTaxRecord, E::Vendor, ManyToOne)),
Self::SweepsTo => Some(c(E::CashPoolSweep, E::CashPool, ManyToOne)),
Self::HedgesInstrument => {
Some(c(E::HedgeRelationship, E::HedgingInstrument, ManyToOne))
}
Self::GovernsInstrument => Some(c(E::DebtCovenant, E::DebtInstrument, ManyToOne)),
Self::EmissionReportedBy => Some(c(E::EmissionRecord, E::Company, ManyToOne)),
Self::AssessesSupplier => Some(c(E::SupplierEsgAssessment, E::Vendor, ManyToOne)),
Self::CostChargedTo => Some(c(E::ProjectCostLine, E::Project, ManyToOne)),
Self::MilestoneOf => Some(c(E::ProjectMilestone, E::Project, ManyToOne)),
Self::ModifiesProject => Some(c(E::ChangeOrder, E::Project, ManyToOne)),
Self::PrincipleUnder => Some(c(E::CosoPrinciple, E::CosoComponent, ManyToOne)),
Self::AssertionCovers => Some(c(E::SoxAssertion, E::GlAccount, ManyToMany)),
Self::JudgmentWithin => Some(c(E::ProfessionalJudgment, E::AuditEngagement, ManyToOne)),
_ => None,
}
}
pub fn all_constraints() -> Vec<EdgeConstraint> {
let all_types = [
Self::PlacedWith,
Self::MatchesOrder,
Self::PaysInvoice,
Self::PlacedBy,
Self::BillsOrder,
Self::RfxBelongsToProject,
Self::RespondsTo,
Self::AwardedFrom,
Self::RecordedBy,
Self::PayrollIncludes,
Self::SubmittedBy,
Self::EnrolledBy,
Self::Produces,
Self::Inspects,
Self::PartOf,
Self::TaxLineBelongsTo,
Self::ProvisionAppliesTo,
Self::WithheldFrom,
Self::SweepsTo,
Self::HedgesInstrument,
Self::GovernsInstrument,
Self::EmissionReportedBy,
Self::AssessesSupplier,
Self::CostChargedTo,
Self::MilestoneOf,
Self::ModifiesProject,
Self::PrincipleUnder,
Self::AssertionCovers,
Self::JudgmentWithin,
];
all_types
.iter()
.filter_map(RelationshipType::constraint)
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Cardinality {
OneToOne,
ManyToOne,
ManyToMany,
}
#[derive(Debug, Clone)]
pub struct EdgeConstraint {
pub relationship_type: RelationshipType,
pub source_type: GraphEntityType,
pub target_type: GraphEntityType,
pub cardinality: Cardinality,
pub edge_properties: &'static [&'static str],
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct GraphEntityId {
pub entity_type: GraphEntityType,
pub id: String,
}
impl GraphEntityId {
pub fn new(entity_type: GraphEntityType, id: impl Into<String>) -> Self {
Self {
entity_type,
id: id.into(),
}
}
pub fn key(&self) -> String {
format!("{}:{}", self.entity_type.code(), self.id)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityNode {
pub entity_id: GraphEntityId,
pub name: String,
pub attributes: HashMap<String, String>,
pub created_date: NaiveDate,
pub is_active: bool,
pub company_code: Option<String>,
}
impl EntityNode {
pub fn new(entity_id: GraphEntityId, name: impl Into<String>, created_date: NaiveDate) -> Self {
Self {
entity_id,
name: name.into(),
attributes: HashMap::new(),
created_date,
is_active: true,
company_code: None,
}
}
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 with_company(mut self, company_code: impl Into<String>) -> Self {
self.company_code = Some(company_code.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelationshipEdge {
pub from_id: GraphEntityId,
pub to_id: GraphEntityId,
pub relationship_type: RelationshipType,
pub strength: f64,
pub start_date: NaiveDate,
pub end_date: Option<NaiveDate>,
pub attributes: HashMap<String, String>,
pub strength_components: Option<StrengthComponents>,
}
impl RelationshipEdge {
pub fn new(
from_id: GraphEntityId,
to_id: GraphEntityId,
relationship_type: RelationshipType,
start_date: NaiveDate,
) -> Self {
Self {
from_id,
to_id,
relationship_type,
strength: 0.5, start_date,
end_date: None,
attributes: HashMap::new(),
strength_components: None,
}
}
pub fn with_strength(mut self, strength: f64) -> Self {
self.strength = strength.clamp(0.0, 1.0);
self
}
pub fn with_strength_components(mut self, components: StrengthComponents) -> Self {
self.strength = components.total();
self.strength_components = Some(components);
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_active(&self) -> bool {
self.end_date.is_none()
}
pub fn key(&self) -> String {
format!(
"{}->{}:{}",
self.from_id.key(),
self.to_id.key(),
self.relationship_type.code()
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrengthComponents {
pub transaction_volume: f64,
pub transaction_count: f64,
pub duration: f64,
pub recency: f64,
pub mutual_connections: f64,
}
impl StrengthComponents {
pub fn new(
transaction_volume: f64,
transaction_count: f64,
duration: f64,
recency: f64,
mutual_connections: f64,
) -> Self {
Self {
transaction_volume: transaction_volume.clamp(0.0, 1.0),
transaction_count: transaction_count.clamp(0.0, 1.0),
duration: duration.clamp(0.0, 1.0),
recency: recency.clamp(0.0, 1.0),
mutual_connections: mutual_connections.clamp(0.0, 1.0),
}
}
pub fn total(&self) -> f64 {
self.total_weighted(RelationshipStrengthCalculator::default_weights())
}
pub fn total_weighted(&self, weights: &StrengthWeights) -> f64 {
let total = self.transaction_volume * weights.transaction_volume_weight
+ self.transaction_count * weights.transaction_count_weight
+ self.duration * weights.duration_weight
+ self.recency * weights.recency_weight
+ self.mutual_connections * weights.mutual_connections_weight;
total.clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StrengthWeights {
pub transaction_volume_weight: f64,
pub transaction_count_weight: f64,
pub duration_weight: f64,
pub recency_weight: f64,
pub mutual_connections_weight: f64,
}
impl Default for StrengthWeights {
fn default() -> Self {
Self {
transaction_volume_weight: 0.30,
transaction_count_weight: 0.25,
duration_weight: 0.20,
recency_weight: 0.15,
mutual_connections_weight: 0.10,
}
}
}
impl StrengthWeights {
pub fn validate(&self) -> Result<(), String> {
let sum = self.transaction_volume_weight
+ self.transaction_count_weight
+ self.duration_weight
+ self.recency_weight
+ self.mutual_connections_weight;
if (sum - 1.0).abs() > 0.01 {
Err(format!("Strength weights must sum to 1.0, got {sum}"))
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RelationshipStrengthCalculator {
pub weights: StrengthWeights,
pub recency_half_life_days: u32,
pub max_transaction_volume: Decimal,
pub max_transaction_count: u32,
pub max_duration_days: u32,
}
impl Default for RelationshipStrengthCalculator {
fn default() -> Self {
Self {
weights: StrengthWeights::default(),
recency_half_life_days: 90,
max_transaction_volume: Decimal::from(10_000_000),
max_transaction_count: 1000,
max_duration_days: 3650, }
}
}
impl RelationshipStrengthCalculator {
pub fn default_weights() -> &'static StrengthWeights {
static WEIGHTS: std::sync::OnceLock<StrengthWeights> = std::sync::OnceLock::new();
WEIGHTS.get_or_init(StrengthWeights::default)
}
pub fn calculate(
&self,
transaction_volume: Decimal,
transaction_count: u32,
relationship_days: u32,
days_since_last_transaction: u32,
mutual_connections: usize,
total_possible_connections: usize,
) -> StrengthComponents {
let volume_normalized = if transaction_volume > Decimal::ZERO
&& self.max_transaction_volume > Decimal::ZERO
{
let log_vol = (transaction_volume.to_string().parse::<f64>().unwrap_or(1.0) + 1.0).ln();
let log_max = (self
.max_transaction_volume
.to_string()
.parse::<f64>()
.unwrap_or(1.0)
+ 1.0)
.ln();
(log_vol / log_max).min(1.0)
} else {
0.0
};
let count_normalized = if self.max_transaction_count > 0 {
let sqrt_count = (transaction_count as f64).sqrt();
let sqrt_max = (self.max_transaction_count as f64).sqrt();
(sqrt_count / sqrt_max).min(1.0)
} else {
0.0
};
let duration_normalized = if self.max_duration_days > 0 {
(relationship_days as f64 / self.max_duration_days as f64).min(1.0)
} else {
0.0
};
let recency_normalized = if self.recency_half_life_days > 0 {
let decay_rate = 0.693 / self.recency_half_life_days as f64; (-decay_rate * days_since_last_transaction as f64).exp()
} else {
1.0
};
let mutual_normalized = if total_possible_connections > 0 {
mutual_connections as f64 / total_possible_connections as f64
} else {
0.0
};
StrengthComponents::new(
volume_normalized,
count_normalized,
duration_normalized,
recency_normalized,
mutual_normalized,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RelationshipStrength {
Strong,
Moderate,
Weak,
Dormant,
}
impl RelationshipStrength {
pub fn from_value(strength: f64) -> Self {
if strength >= 0.7 {
Self::Strong
} else if strength >= 0.4 {
Self::Moderate
} else if strength >= 0.1 {
Self::Weak
} else {
Self::Dormant
}
}
pub fn min_threshold(&self) -> f64 {
match self {
Self::Strong => 0.7,
Self::Moderate => 0.4,
Self::Weak => 0.1,
Self::Dormant => 0.0,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GraphIndexes {
pub outgoing_edges: HashMap<String, Vec<usize>>,
pub incoming_edges: HashMap<String, Vec<usize>>,
pub edges_by_type: HashMap<RelationshipType, Vec<usize>>,
pub nodes_by_type: HashMap<GraphEntityType, Vec<String>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EntityGraph {
pub nodes: HashMap<String, EntityNode>,
pub edges: Vec<RelationshipEdge>,
#[serde(skip)]
pub indexes: GraphIndexes,
pub metadata: GraphMetadata,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GraphMetadata {
pub company_code: Option<String>,
pub created_date: Option<NaiveDate>,
#[serde(with = "crate::serde_decimal")]
pub total_transaction_volume: Decimal,
pub date_range: Option<(NaiveDate, NaiveDate)>,
}
impl EntityGraph {
pub fn new() -> Self {
Self::default()
}
pub fn add_node(&mut self, node: EntityNode) {
let key = node.entity_id.key();
let entity_type = node.entity_id.entity_type;
self.nodes.insert(key.clone(), node);
self.indexes
.nodes_by_type
.entry(entity_type)
.or_default()
.push(key);
}
pub fn add_edge(&mut self, edge: RelationshipEdge) {
let edge_idx = self.edges.len();
let from_key = edge.from_id.key();
let to_key = edge.to_id.key();
let rel_type = edge.relationship_type;
self.indexes
.outgoing_edges
.entry(from_key)
.or_default()
.push(edge_idx);
self.indexes
.incoming_edges
.entry(to_key)
.or_default()
.push(edge_idx);
self.indexes
.edges_by_type
.entry(rel_type)
.or_default()
.push(edge_idx);
self.edges.push(edge);
}
pub fn get_node(&self, entity_id: &GraphEntityId) -> Option<&EntityNode> {
self.nodes.get(&entity_id.key())
}
pub fn get_outgoing_edges(&self, entity_id: &GraphEntityId) -> Vec<&RelationshipEdge> {
self.indexes
.outgoing_edges
.get(&entity_id.key())
.map(|indices| indices.iter().map(|&idx| &self.edges[idx]).collect())
.unwrap_or_default()
}
pub fn get_incoming_edges(&self, entity_id: &GraphEntityId) -> Vec<&RelationshipEdge> {
self.indexes
.incoming_edges
.get(&entity_id.key())
.map(|indices| indices.iter().map(|&idx| &self.edges[idx]).collect())
.unwrap_or_default()
}
pub fn get_edges_by_type(&self, rel_type: RelationshipType) -> Vec<&RelationshipEdge> {
self.indexes
.edges_by_type
.get(&rel_type)
.map(|indices| indices.iter().map(|&idx| &self.edges[idx]).collect())
.unwrap_or_default()
}
pub fn get_nodes_by_type(&self, entity_type: GraphEntityType) -> Vec<&EntityNode> {
self.indexes
.nodes_by_type
.get(&entity_type)
.map(|keys| keys.iter().filter_map(|k| self.nodes.get(k)).collect())
.unwrap_or_default()
}
pub fn get_neighbors(&self, entity_id: &GraphEntityId) -> Vec<&EntityNode> {
let mut neighbor_ids: HashSet<String> = HashSet::new();
for edge in self.get_outgoing_edges(entity_id) {
neighbor_ids.insert(edge.to_id.key());
}
for edge in self.get_incoming_edges(entity_id) {
neighbor_ids.insert(edge.from_id.key());
}
neighbor_ids
.iter()
.filter_map(|key| self.nodes.get(key))
.collect()
}
pub fn node_degree(&self, entity_id: &GraphEntityId) -> usize {
let key = entity_id.key();
let out_degree = self
.indexes
.outgoing_edges
.get(&key)
.map(std::vec::Vec::len)
.unwrap_or(0);
let in_degree = self
.indexes
.incoming_edges
.get(&key)
.map(std::vec::Vec::len)
.unwrap_or(0);
out_degree + in_degree
}
pub fn rebuild_indexes(&mut self) {
self.indexes = GraphIndexes::default();
for (key, node) in &self.nodes {
self.indexes
.nodes_by_type
.entry(node.entity_id.entity_type)
.or_default()
.push(key.clone());
}
for (idx, edge) in self.edges.iter().enumerate() {
self.indexes
.outgoing_edges
.entry(edge.from_id.key())
.or_default()
.push(idx);
self.indexes
.incoming_edges
.entry(edge.to_id.key())
.or_default()
.push(idx);
self.indexes
.edges_by_type
.entry(edge.relationship_type)
.or_default()
.push(idx);
}
}
pub fn statistics(&self) -> GraphStatistics {
let node_count = self.nodes.len();
let edge_count = self.edges.len();
let avg_degree = if node_count > 0 {
(2.0 * edge_count as f64) / node_count as f64
} else {
0.0
};
let avg_strength = if edge_count > 0 {
self.edges.iter().map(|e| e.strength).sum::<f64>() / edge_count as f64
} else {
0.0
};
let mut node_counts: HashMap<String, usize> = HashMap::new();
for node in self.nodes.values() {
*node_counts
.entry(format!("{:?}", node.entity_id.entity_type))
.or_insert(0) += 1;
}
let mut edge_counts: HashMap<String, usize> = HashMap::new();
for edge in &self.edges {
*edge_counts
.entry(format!("{:?}", edge.relationship_type))
.or_insert(0) += 1;
}
let mut strength_distribution: HashMap<String, usize> = HashMap::new();
for edge in &self.edges {
let classification = RelationshipStrength::from_value(edge.strength);
*strength_distribution
.entry(format!("{classification:?}"))
.or_insert(0) += 1;
}
GraphStatistics {
node_count,
edge_count,
avg_degree,
avg_strength,
node_counts,
edge_counts,
strength_distribution,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphStatistics {
pub node_count: usize,
pub edge_count: usize,
pub avg_degree: f64,
pub avg_strength: f64,
pub node_counts: HashMap<String, usize>,
pub edge_counts: HashMap<String, usize>,
pub strength_distribution: HashMap<String, usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossProcessLink {
pub material_id: String,
pub source_process: String,
pub source_document_id: String,
pub target_process: String,
pub target_document_id: String,
pub link_type: CrossProcessLinkType,
#[serde(with = "crate::serde_decimal")]
pub quantity: Decimal,
pub link_date: NaiveDate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CrossProcessLinkType {
InventoryMovement,
ReturnFlow,
PaymentReconciliation,
IntercompanyBilateral,
}
impl CrossProcessLink {
#[allow(clippy::too_many_arguments)]
pub fn new(
material_id: impl Into<String>,
source_process: impl Into<String>,
source_document_id: impl Into<String>,
target_process: impl Into<String>,
target_document_id: impl Into<String>,
link_type: CrossProcessLinkType,
quantity: Decimal,
link_date: NaiveDate,
) -> Self {
Self {
material_id: material_id.into(),
source_process: source_process.into(),
source_document_id: source_document_id.into(),
target_process: target_process.into(),
target_document_id: target_document_id.into(),
link_type,
quantity,
link_date,
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_entity_id() {
let id = GraphEntityId::new(GraphEntityType::Vendor, "V-001234");
assert_eq!(id.key(), "VN:V-001234");
}
#[test]
fn test_relationship_type_inverse() {
assert_eq!(
RelationshipType::BuysFrom.inverse(),
RelationshipType::SellsTo
);
assert_eq!(
RelationshipType::SellsTo.inverse(),
RelationshipType::BuysFrom
);
assert_eq!(
RelationshipType::ReportsTo.inverse(),
RelationshipType::Manages
);
}
#[test]
fn test_strength_weights_validation() {
let valid_weights = StrengthWeights::default();
assert!(valid_weights.validate().is_ok());
let invalid_weights = StrengthWeights {
transaction_volume_weight: 0.5,
transaction_count_weight: 0.5,
duration_weight: 0.5,
recency_weight: 0.5,
mutual_connections_weight: 0.5,
};
assert!(invalid_weights.validate().is_err());
}
#[test]
fn test_strength_calculator() {
let calc = RelationshipStrengthCalculator::default();
let components = calc.calculate(Decimal::from(100000), 50, 365, 30, 5, 20);
assert!(components.transaction_volume > 0.0);
assert!(components.transaction_count > 0.0);
assert!(components.duration > 0.0);
assert!(components.recency > 0.0);
assert!(components.mutual_connections > 0.0);
assert!(components.total() <= 1.0);
}
#[test]
fn test_relationship_strength_classification() {
assert_eq!(
RelationshipStrength::from_value(0.8),
RelationshipStrength::Strong
);
assert_eq!(
RelationshipStrength::from_value(0.5),
RelationshipStrength::Moderate
);
assert_eq!(
RelationshipStrength::from_value(0.2),
RelationshipStrength::Weak
);
assert_eq!(
RelationshipStrength::from_value(0.05),
RelationshipStrength::Dormant
);
}
#[test]
fn test_entity_graph() {
let mut graph = EntityGraph::new();
let vendor_id = GraphEntityId::new(GraphEntityType::Vendor, "V-001");
let customer_id = GraphEntityId::new(GraphEntityType::Customer, "C-001");
graph.add_node(EntityNode::new(
vendor_id.clone(),
"Acme Supplies",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
));
graph.add_node(EntityNode::new(
customer_id.clone(),
"Contoso Corp",
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
));
graph.add_edge(
RelationshipEdge::new(
vendor_id.clone(),
customer_id.clone(),
RelationshipType::SellsTo,
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)
.with_strength(0.7),
);
assert_eq!(graph.nodes.len(), 2);
assert_eq!(graph.edges.len(), 1);
let neighbors = graph.get_neighbors(&vendor_id);
assert_eq!(neighbors.len(), 1);
assert_eq!(neighbors[0].entity_id.id, "C-001");
assert_eq!(graph.node_degree(&vendor_id), 1);
assert_eq!(graph.node_degree(&customer_id), 1);
}
#[test]
fn test_graph_statistics() {
let mut graph = EntityGraph::new();
for i in 0..10 {
let id = GraphEntityId::new(GraphEntityType::Vendor, format!("V-{:03}", i));
graph.add_node(EntityNode::new(
id,
format!("Vendor {}", i),
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
));
}
for i in 0..5 {
let from_id = GraphEntityId::new(GraphEntityType::Vendor, format!("V-{:03}", i));
let to_id = GraphEntityId::new(GraphEntityType::Vendor, format!("V-{:03}", i + 5));
graph.add_edge(
RelationshipEdge::new(
from_id,
to_id,
RelationshipType::PartnersWith,
NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
)
.with_strength(0.6),
);
}
let stats = graph.statistics();
assert_eq!(stats.node_count, 10);
assert_eq!(stats.edge_count, 5);
assert!((stats.avg_degree - 1.0).abs() < 0.01);
assert!((stats.avg_strength - 0.6).abs() < 0.01);
}
#[test]
fn test_numeric_code_roundtrip() {
for &entity_type in GraphEntityType::all_types() {
let code = entity_type.numeric_code();
let recovered = GraphEntityType::from_numeric_code(code);
assert_eq!(
recovered,
Some(entity_type),
"Failed roundtrip for {:?} with code {}",
entity_type,
code
);
}
}
#[test]
fn test_node_type_name_roundtrip() {
for &entity_type in GraphEntityType::all_types() {
let name = entity_type.node_type_name();
let recovered = GraphEntityType::from_node_type_name(name);
assert_eq!(
recovered,
Some(entity_type),
"Failed roundtrip for {:?} with name {}",
entity_type,
name
);
}
}
#[test]
fn test_all_types_unique_codes() {
let mut codes = std::collections::HashSet::new();
for &entity_type in GraphEntityType::all_types() {
assert!(
codes.insert(entity_type.numeric_code()),
"Duplicate numeric code {} for {:?}",
entity_type.numeric_code(),
entity_type
);
}
}
#[test]
fn test_all_types_unique_names() {
let mut names = std::collections::HashSet::new();
for &entity_type in GraphEntityType::all_types() {
assert!(
names.insert(entity_type.node_type_name()),
"Duplicate name {} for {:?}",
entity_type.node_type_name(),
entity_type
);
}
}
#[test]
fn test_all_types_unique_letter_codes() {
let mut codes = std::collections::HashSet::new();
for &entity_type in GraphEntityType::all_types() {
assert!(
codes.insert(entity_type.code()),
"Duplicate letter code {} for {:?}",
entity_type.code(),
entity_type
);
}
}
#[test]
fn test_category_helpers() {
assert!(GraphEntityType::TaxJurisdiction.is_tax());
assert!(GraphEntityType::UncertainTaxPosition.is_tax());
assert!(!GraphEntityType::CashPosition.is_tax());
assert!(GraphEntityType::CashPosition.is_treasury());
assert!(GraphEntityType::DebtCovenant.is_treasury());
assert!(!GraphEntityType::EmissionRecord.is_treasury());
assert!(GraphEntityType::EmissionRecord.is_esg());
assert!(GraphEntityType::ClimateScenario.is_esg());
assert!(!GraphEntityType::TaxCode.is_esg());
assert!(GraphEntityType::Project.is_project());
assert!(GraphEntityType::ProjectMilestone.is_project());
assert!(GraphEntityType::PayrollRun.is_h2r());
assert!(GraphEntityType::BenefitEnrollment.is_h2r());
assert!(GraphEntityType::ProductionOrder.is_mfg());
assert!(GraphEntityType::BomComponent.is_mfg());
assert!(GraphEntityType::CosoComponent.is_governance());
assert!(GraphEntityType::ProfessionalJudgment.is_governance());
}
#[test]
fn test_edge_constraint_validity() {
let constraints = RelationshipType::all_constraints();
assert_eq!(
constraints.len(),
29,
"Expected 29 domain-specific edge constraints"
);
for constraint in &constraints {
assert!(constraint.source_type.numeric_code() > 0);
assert!(constraint.target_type.numeric_code() > 0);
}
}
#[test]
fn test_all_process_families_have_edges() {
let constraints = RelationshipType::all_constraints();
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::PlacedWith));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::PaysInvoice));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::PlacedBy));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::TaxLineBelongsTo));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::WithheldFrom));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::SweepsTo));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::GovernsInstrument));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::EmissionReportedBy));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::CostChargedTo));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::PrincipleUnder));
assert!(constraints
.iter()
.any(|c| c.relationship_type == RelationshipType::JudgmentWithin));
}
#[test]
fn test_edge_source_target_types() {
let placed_with = RelationshipType::PlacedWith.constraint().unwrap();
assert_eq!(placed_with.source_type, GraphEntityType::PurchaseOrder);
assert_eq!(placed_with.target_type, GraphEntityType::Vendor);
assert_eq!(placed_with.cardinality, Cardinality::ManyToOne);
let awarded_from = RelationshipType::AwardedFrom.constraint().unwrap();
assert_eq!(
awarded_from.source_type,
GraphEntityType::ProcurementContract
);
assert_eq!(awarded_from.target_type, GraphEntityType::BidEvaluation);
assert_eq!(awarded_from.cardinality, Cardinality::OneToOne);
let pays_invoice = RelationshipType::PaysInvoice.constraint().unwrap();
assert_eq!(pays_invoice.cardinality, Cardinality::ManyToMany);
}
#[test]
fn test_existing_types_no_constraint() {
assert!(RelationshipType::BuysFrom.constraint().is_none());
assert!(RelationshipType::SellsTo.constraint().is_none());
assert!(RelationshipType::ReportsTo.constraint().is_none());
}
#[test]
fn test_specific_entity_codes() {
assert_eq!(GraphEntityType::UncertainTaxPosition.numeric_code(), 416);
assert_eq!(
GraphEntityType::UncertainTaxPosition.node_type_name(),
"uncertain_tax_position"
);
assert_eq!(GraphEntityType::DebtCovenant.numeric_code(), 427);
assert_eq!(GraphEntityType::BenefitEnrollment.numeric_code(), 333);
assert_eq!(GraphEntityType::BomComponent.numeric_code(), 343);
}
#[test]
fn test_all_types_count() {
assert_eq!(GraphEntityType::all_types().len(), 70);
}
#[test]
fn test_cross_process_link() {
let link = CrossProcessLink::new(
"MAT-001",
"P2P",
"GR-12345",
"O2C",
"DEL-67890",
CrossProcessLinkType::InventoryMovement,
Decimal::from(100),
NaiveDate::from_ymd_opt(2024, 6, 1).unwrap(),
);
assert_eq!(link.material_id, "MAT-001");
assert_eq!(link.link_type, CrossProcessLinkType::InventoryMovement);
}
}