use crate::models::BusinessProcess;
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReferenceType {
Invoice,
PurchaseOrder,
SalesOrder,
GoodsReceipt,
PaymentReference,
AssetTag,
ProjectNumber,
ExpenseReport,
ContractNumber,
BatchNumber,
InternalDocument,
}
impl ReferenceType {
pub fn default_prefix(&self) -> &'static str {
match self {
Self::Invoice => "INV",
Self::PurchaseOrder => "PO",
Self::SalesOrder => "SO",
Self::GoodsReceipt => "GR",
Self::PaymentReference => "PAY",
Self::AssetTag => "FA",
Self::ProjectNumber => "PRJ",
Self::ExpenseReport => "EXP",
Self::ContractNumber => "CTR",
Self::BatchNumber => "BATCH",
Self::InternalDocument => "DOC",
}
}
pub fn for_business_process(process: BusinessProcess) -> Self {
match process {
BusinessProcess::O2C => Self::SalesOrder,
BusinessProcess::P2P => Self::PurchaseOrder,
BusinessProcess::R2R => Self::InternalDocument,
BusinessProcess::H2R => Self::ExpenseReport,
BusinessProcess::A2R => Self::AssetTag,
BusinessProcess::S2C => Self::PurchaseOrder,
BusinessProcess::Mfg => Self::InternalDocument,
BusinessProcess::Bank => Self::PaymentReference,
BusinessProcess::Audit => Self::InternalDocument,
BusinessProcess::Treasury => Self::PaymentReference,
BusinessProcess::Tax => Self::InternalDocument,
BusinessProcess::Intercompany => Self::InternalDocument,
BusinessProcess::ProjectAccounting => Self::ProjectNumber,
BusinessProcess::Esg => Self::InternalDocument,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub enum ReferenceFormat {
Sequential,
#[default]
YearPrefixed,
YearMonthPrefixed,
Random,
CompanyYearPrefixed,
}
#[derive(Debug, Clone)]
pub struct ReferenceConfig {
pub prefix: String,
pub format: ReferenceFormat,
pub sequence_digits: usize,
pub start_sequence: u64,
}
impl Default for ReferenceConfig {
fn default() -> Self {
Self {
prefix: "REF".to_string(),
format: ReferenceFormat::YearPrefixed,
sequence_digits: 6,
start_sequence: 1,
}
}
}
#[derive(Debug)]
pub struct ReferenceGenerator {
configs: HashMap<ReferenceType, ReferenceConfig>,
counters: HashMap<(ReferenceType, Option<i32>), AtomicU64>,
default_year: i32,
company_code: String,
}
impl Default for ReferenceGenerator {
fn default() -> Self {
Self::new(2024, "1000")
}
}
impl ReferenceGenerator {
pub fn new(year: i32, company_code: &str) -> Self {
let mut configs = HashMap::new();
for ref_type in [
ReferenceType::Invoice,
ReferenceType::PurchaseOrder,
ReferenceType::SalesOrder,
ReferenceType::GoodsReceipt,
ReferenceType::PaymentReference,
ReferenceType::AssetTag,
ReferenceType::ProjectNumber,
ReferenceType::ExpenseReport,
ReferenceType::ContractNumber,
ReferenceType::BatchNumber,
ReferenceType::InternalDocument,
] {
configs.insert(
ref_type,
ReferenceConfig {
prefix: ref_type.default_prefix().to_string(),
format: ReferenceFormat::YearPrefixed,
sequence_digits: 6,
start_sequence: 1,
},
);
}
Self {
configs,
counters: HashMap::new(),
default_year: year,
company_code: company_code.to_string(),
}
}
pub fn with_company_code(mut self, code: &str) -> Self {
self.company_code = code.to_string();
self
}
pub fn with_year(mut self, year: i32) -> Self {
self.default_year = year;
self
}
pub fn set_config(&mut self, ref_type: ReferenceType, config: ReferenceConfig) {
self.configs.insert(ref_type, config);
}
pub fn set_prefix(&mut self, ref_type: ReferenceType, prefix: &str) {
if let Some(config) = self.configs.get_mut(&ref_type) {
config.prefix = prefix.to_string();
}
}
fn next_sequence(&mut self, ref_type: ReferenceType, year: Option<i32>) -> u64 {
let key = (ref_type, year);
let config = self.configs.get(&ref_type).cloned().unwrap_or_default();
let counter = self
.counters
.entry(key)
.or_insert_with(|| AtomicU64::new(config.start_sequence));
counter.fetch_add(1, Ordering::SeqCst)
}
pub fn generate(&mut self, ref_type: ReferenceType) -> String {
self.generate_for_year(ref_type, self.default_year)
}
pub fn generate_for_year(&mut self, ref_type: ReferenceType, year: i32) -> String {
let config = self.configs.get(&ref_type).cloned().unwrap_or_default();
let seq = self.next_sequence(ref_type, Some(year));
match config.format {
ReferenceFormat::Sequential => {
format!(
"{}-{:0width$}",
config.prefix,
seq,
width = config.sequence_digits
)
}
ReferenceFormat::YearPrefixed => {
format!(
"{}-{}-{:0width$}",
config.prefix,
year,
seq,
width = config.sequence_digits
)
}
ReferenceFormat::YearMonthPrefixed => {
format!(
"{}-{}01-{:0width$}",
config.prefix,
year,
seq,
width = config.sequence_digits - 1
)
}
ReferenceFormat::Random => {
let suffix: String = (0..config.sequence_digits)
.map(|i| {
let idx = ((seq as usize)
.wrapping_mul(7)
.wrapping_add(i)
.wrapping_mul(13)
.wrapping_add(17))
% 36;
if idx < 10 {
(b'0' + idx as u8) as char
} else {
(b'A' + idx as u8 - 10) as char
}
})
.collect();
format!("{}-{}", config.prefix, suffix)
}
ReferenceFormat::CompanyYearPrefixed => {
format!(
"{}-{}-{}-{:0width$}",
config.prefix,
self.company_code,
year,
seq,
width = config.sequence_digits
)
}
}
}
pub fn generate_for_process(&mut self, process: BusinessProcess) -> String {
let ref_type = ReferenceType::for_business_process(process);
self.generate(ref_type)
}
pub fn generate_for_process_year(&mut self, process: BusinessProcess, year: i32) -> String {
let ref_type = ReferenceType::for_business_process(process);
self.generate_for_year(ref_type, year)
}
pub fn generate_external_reference(&self, rng: &mut impl Rng) -> String {
let formats = [
|rng: &mut dyn rand::RngCore| {
format!("INV{:08}", rng.random_range(10000000u64..99999999))
},
|rng: &mut dyn rand::RngCore| {
format!("{:010}", rng.random_range(1000000000u64..9999999999))
},
|rng: &mut dyn rand::RngCore| {
format!(
"V{}-{:06}",
rng.random_range(100..999),
rng.random_range(1..999999)
)
},
|rng: &mut dyn rand::RngCore| {
format!(
"{}{:07}",
(b'A' + rng.random_range(0..26)) as char,
rng.random_range(1000000..9999999)
)
},
];
let idx = rng.random_range(0..formats.len());
formats[idx](rng)
}
}
#[derive(Debug, Clone, Default)]
pub struct ReferenceGeneratorBuilder {
year: Option<i32>,
company_code: Option<String>,
invoice_prefix: Option<String>,
po_prefix: Option<String>,
so_prefix: Option<String>,
}
impl ReferenceGeneratorBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn year(mut self, year: i32) -> Self {
self.year = Some(year);
self
}
pub fn company_code(mut self, code: &str) -> Self {
self.company_code = Some(code.to_string());
self
}
pub fn invoice_prefix(mut self, prefix: &str) -> Self {
self.invoice_prefix = Some(prefix.to_string());
self
}
pub fn po_prefix(mut self, prefix: &str) -> Self {
self.po_prefix = Some(prefix.to_string());
self
}
pub fn so_prefix(mut self, prefix: &str) -> Self {
self.so_prefix = Some(prefix.to_string());
self
}
pub fn build(self) -> ReferenceGenerator {
let year = self.year.unwrap_or(2024);
let company = self.company_code.as_deref().unwrap_or("1000");
let mut gen = ReferenceGenerator::new(year, company);
if let Some(prefix) = self.invoice_prefix {
gen.set_prefix(ReferenceType::Invoice, &prefix);
}
if let Some(prefix) = self.po_prefix {
gen.set_prefix(ReferenceType::PurchaseOrder, &prefix);
}
if let Some(prefix) = self.so_prefix {
gen.set_prefix(ReferenceType::SalesOrder, &prefix);
}
gen
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_sequential_generation() {
let mut gen = ReferenceGenerator::new(2024, "1000");
let ref1 = gen.generate(ReferenceType::Invoice);
let ref2 = gen.generate(ReferenceType::Invoice);
let ref3 = gen.generate(ReferenceType::Invoice);
assert!(ref1.starts_with("INV-2024-"));
assert!(ref2.starts_with("INV-2024-"));
assert!(ref3.starts_with("INV-2024-"));
assert_ne!(ref1, ref2);
assert_ne!(ref2, ref3);
}
#[test]
fn test_different_types() {
let mut gen = ReferenceGenerator::new(2024, "1000");
let inv = gen.generate(ReferenceType::Invoice);
let po = gen.generate(ReferenceType::PurchaseOrder);
let so = gen.generate(ReferenceType::SalesOrder);
assert!(inv.starts_with("INV-"));
assert!(po.starts_with("PO-"));
assert!(so.starts_with("SO-"));
}
#[test]
fn test_year_based_counters() {
let mut gen = ReferenceGenerator::new(2024, "1000");
let ref_2024 = gen.generate_for_year(ReferenceType::Invoice, 2024);
let ref_2025 = gen.generate_for_year(ReferenceType::Invoice, 2025);
assert!(ref_2024.contains("2024"));
assert!(ref_2025.contains("2025"));
assert!(ref_2024.ends_with("000001"));
assert!(ref_2025.ends_with("000001"));
}
#[test]
fn test_business_process_mapping() {
let mut gen = ReferenceGenerator::new(2024, "1000");
let o2c_ref = gen.generate_for_process(BusinessProcess::O2C);
let p2p_ref = gen.generate_for_process(BusinessProcess::P2P);
assert!(o2c_ref.starts_with("SO-")); assert!(p2p_ref.starts_with("PO-")); }
#[test]
fn test_custom_prefix() {
let mut gen = ReferenceGenerator::new(2024, "ACME");
gen.set_prefix(ReferenceType::Invoice, "ACME-INV");
let inv = gen.generate(ReferenceType::Invoice);
assert!(inv.starts_with("ACME-INV-"));
}
#[test]
fn test_builder() {
let mut gen = ReferenceGeneratorBuilder::new()
.year(2025)
.company_code("CORP")
.invoice_prefix("CORP-INV")
.build();
let inv = gen.generate(ReferenceType::Invoice);
assert!(inv.starts_with("CORP-INV-2025-"));
}
#[test]
fn test_external_reference() {
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
let gen = ReferenceGenerator::new(2024, "1000");
let mut rng = ChaCha8Rng::seed_from_u64(42);
let ext_ref = gen.generate_external_reference(&mut rng);
assert!(!ext_ref.is_empty());
}
}