Skip to main content

datasynth_core/templates/
references.rs

1//! Reference number generation for journal entries.
2//!
3//! Generates realistic document reference numbers like invoice numbers,
4//! purchase orders, sales orders, etc.
5
6use crate::models::BusinessProcess;
7use rand::Rng;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12/// Types of reference numbers.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ReferenceType {
16    /// Invoice number (vendor or customer)
17    Invoice,
18    /// Purchase order number
19    PurchaseOrder,
20    /// Sales order number
21    SalesOrder,
22    /// Goods receipt number
23    GoodsReceipt,
24    /// Payment reference
25    PaymentReference,
26    /// Asset tag number
27    AssetTag,
28    /// Project number
29    ProjectNumber,
30    /// Expense report number
31    ExpenseReport,
32    /// Contract number
33    ContractNumber,
34    /// Batch number
35    BatchNumber,
36    /// Internal document number
37    InternalDocument,
38}
39
40impl ReferenceType {
41    /// Get the default prefix for this reference type.
42    pub fn default_prefix(&self) -> &'static str {
43        match self {
44            Self::Invoice => "INV",
45            Self::PurchaseOrder => "PO",
46            Self::SalesOrder => "SO",
47            Self::GoodsReceipt => "GR",
48            Self::PaymentReference => "PAY",
49            Self::AssetTag => "FA",
50            Self::ProjectNumber => "PRJ",
51            Self::ExpenseReport => "EXP",
52            Self::ContractNumber => "CTR",
53            Self::BatchNumber => "BATCH",
54            Self::InternalDocument => "DOC",
55        }
56    }
57
58    /// Get the typical reference type for a business process.
59    pub fn for_business_process(process: BusinessProcess) -> Self {
60        match process {
61            BusinessProcess::O2C => Self::SalesOrder,
62            BusinessProcess::P2P => Self::PurchaseOrder,
63            BusinessProcess::R2R => Self::InternalDocument,
64            BusinessProcess::H2R => Self::ExpenseReport,
65            BusinessProcess::A2R => Self::AssetTag,
66            BusinessProcess::S2C => Self::PurchaseOrder,
67            BusinessProcess::Mfg => Self::InternalDocument,
68            BusinessProcess::Bank => Self::PaymentReference,
69            BusinessProcess::Audit => Self::InternalDocument,
70            BusinessProcess::Treasury => Self::PaymentReference,
71            BusinessProcess::Tax => Self::InternalDocument,
72            BusinessProcess::Intercompany => Self::InternalDocument,
73            BusinessProcess::ProjectAccounting => Self::ProjectNumber,
74            BusinessProcess::Esg => Self::InternalDocument,
75        }
76    }
77}
78
79/// Format for reference numbers.
80#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub enum ReferenceFormat {
82    /// Simple sequential: PREFIX-000001
83    Sequential,
84    /// Year-prefixed: PREFIX-YYYY-000001
85    #[default]
86    YearPrefixed,
87    /// Year-month: PREFIX-YYYYMM-00001
88    YearMonthPrefixed,
89    /// Random alphanumeric: PREFIX-XXXXXX
90    Random,
91    /// Company-year: PREFIX-COMP-YYYY-00001
92    CompanyYearPrefixed,
93}
94
95/// Configuration for a reference type.
96#[derive(Debug, Clone)]
97pub struct ReferenceConfig {
98    /// Prefix for the reference
99    pub prefix: String,
100    /// Format to use
101    pub format: ReferenceFormat,
102    /// Number of digits in the sequence
103    pub sequence_digits: usize,
104    /// Starting sequence number
105    pub start_sequence: u64,
106}
107
108impl Default for ReferenceConfig {
109    fn default() -> Self {
110        Self {
111            prefix: "REF".to_string(),
112            format: ReferenceFormat::YearPrefixed,
113            sequence_digits: 6,
114            start_sequence: 1,
115        }
116    }
117}
118
119/// Generator for reference numbers.
120#[derive(Debug)]
121pub struct ReferenceGenerator {
122    /// Configuration by reference type
123    configs: HashMap<ReferenceType, ReferenceConfig>,
124    /// Counters by reference type and year
125    counters: HashMap<(ReferenceType, Option<i32>), AtomicU64>,
126    /// Default year for generation
127    default_year: i32,
128    /// Company code for company-prefixed formats
129    company_code: String,
130}
131
132impl Default for ReferenceGenerator {
133    fn default() -> Self {
134        Self::new(2024, "1000")
135    }
136}
137
138impl ReferenceGenerator {
139    /// Create a new reference generator.
140    pub fn new(year: i32, company_code: &str) -> Self {
141        let mut configs = HashMap::new();
142
143        // Set up default configurations for each type
144        for ref_type in [
145            ReferenceType::Invoice,
146            ReferenceType::PurchaseOrder,
147            ReferenceType::SalesOrder,
148            ReferenceType::GoodsReceipt,
149            ReferenceType::PaymentReference,
150            ReferenceType::AssetTag,
151            ReferenceType::ProjectNumber,
152            ReferenceType::ExpenseReport,
153            ReferenceType::ContractNumber,
154            ReferenceType::BatchNumber,
155            ReferenceType::InternalDocument,
156        ] {
157            configs.insert(
158                ref_type,
159                ReferenceConfig {
160                    prefix: ref_type.default_prefix().to_string(),
161                    format: ReferenceFormat::YearPrefixed,
162                    sequence_digits: 6,
163                    start_sequence: 1,
164                },
165            );
166        }
167
168        Self {
169            configs,
170            counters: HashMap::new(),
171            default_year: year,
172            company_code: company_code.to_string(),
173        }
174    }
175
176    /// Set the company code.
177    pub fn with_company_code(mut self, code: &str) -> Self {
178        self.company_code = code.to_string();
179        self
180    }
181
182    /// Set the default year.
183    pub fn with_year(mut self, year: i32) -> Self {
184        self.default_year = year;
185        self
186    }
187
188    /// Set configuration for a reference type.
189    pub fn set_config(&mut self, ref_type: ReferenceType, config: ReferenceConfig) {
190        self.configs.insert(ref_type, config);
191    }
192
193    /// Set a custom prefix for a reference type.
194    pub fn set_prefix(&mut self, ref_type: ReferenceType, prefix: &str) {
195        if let Some(config) = self.configs.get_mut(&ref_type) {
196            config.prefix = prefix.to_string();
197        }
198    }
199
200    /// Get the next sequence number for a reference type and optional year.
201    fn next_sequence(&mut self, ref_type: ReferenceType, year: Option<i32>) -> u64 {
202        let key = (ref_type, year);
203        let config = self.configs.get(&ref_type).cloned().unwrap_or_default();
204
205        let counter = self
206            .counters
207            .entry(key)
208            .or_insert_with(|| AtomicU64::new(config.start_sequence));
209
210        counter.fetch_add(1, Ordering::SeqCst)
211    }
212
213    /// Generate a reference number.
214    pub fn generate(&mut self, ref_type: ReferenceType) -> String {
215        self.generate_for_year(ref_type, self.default_year)
216    }
217
218    /// Generate a reference number for a specific year.
219    pub fn generate_for_year(&mut self, ref_type: ReferenceType, year: i32) -> String {
220        let config = self.configs.get(&ref_type).cloned().unwrap_or_default();
221        let seq = self.next_sequence(ref_type, Some(year));
222
223        match config.format {
224            ReferenceFormat::Sequential => {
225                format!(
226                    "{}-{:0width$}",
227                    config.prefix,
228                    seq,
229                    width = config.sequence_digits
230                )
231            }
232            ReferenceFormat::YearPrefixed => {
233                format!(
234                    "{}-{}-{:0width$}",
235                    config.prefix,
236                    year,
237                    seq,
238                    width = config.sequence_digits
239                )
240            }
241            ReferenceFormat::YearMonthPrefixed => {
242                // Use a default month; in practice, pass month as parameter
243                format!(
244                    "{}-{}01-{:0width$}",
245                    config.prefix,
246                    year,
247                    seq,
248                    width = config.sequence_digits - 1
249                )
250            }
251            ReferenceFormat::Random => {
252                // Generate random alphanumeric suffix
253                let suffix: String = (0..config.sequence_digits)
254                    .map(|_| {
255                        let idx = rand::rng().random_range(0..36);
256                        if idx < 10 {
257                            (b'0' + idx) as char
258                        } else {
259                            (b'A' + idx - 10) as char
260                        }
261                    })
262                    .collect();
263                format!("{}-{}", config.prefix, suffix)
264            }
265            ReferenceFormat::CompanyYearPrefixed => {
266                format!(
267                    "{}-{}-{}-{:0width$}",
268                    config.prefix,
269                    self.company_code,
270                    year,
271                    seq,
272                    width = config.sequence_digits
273                )
274            }
275        }
276    }
277
278    /// Generate a reference for a business process.
279    pub fn generate_for_process(&mut self, process: BusinessProcess) -> String {
280        let ref_type = ReferenceType::for_business_process(process);
281        self.generate(ref_type)
282    }
283
284    /// Generate a reference for a business process and year.
285    pub fn generate_for_process_year(&mut self, process: BusinessProcess, year: i32) -> String {
286        let ref_type = ReferenceType::for_business_process(process);
287        self.generate_for_year(ref_type, year)
288    }
289
290    /// Generate an external reference (vendor invoice, etc.) with random elements.
291    pub fn generate_external_reference(&self, rng: &mut impl Rng) -> String {
292        // External references often have different formats
293        let formats = [
294            // Vendor invoice formats
295            |rng: &mut dyn rand::RngCore| {
296                format!("INV{:08}", rng.random_range(10000000u64..99999999))
297            },
298            |rng: &mut dyn rand::RngCore| {
299                format!("{:010}", rng.random_range(1000000000u64..9999999999))
300            },
301            |rng: &mut dyn rand::RngCore| {
302                format!(
303                    "V{}-{:06}",
304                    rng.random_range(100..999),
305                    rng.random_range(1..999999)
306                )
307            },
308            |rng: &mut dyn rand::RngCore| {
309                format!(
310                    "{}{:07}",
311                    (b'A' + rng.random_range(0..26)) as char,
312                    rng.random_range(1000000..9999999)
313                )
314            },
315        ];
316
317        let idx = rng.random_range(0..formats.len());
318        formats[idx](rng)
319    }
320}
321
322/// Builder for configuring reference generation.
323#[derive(Debug, Clone, Default)]
324pub struct ReferenceGeneratorBuilder {
325    year: Option<i32>,
326    company_code: Option<String>,
327    invoice_prefix: Option<String>,
328    po_prefix: Option<String>,
329    so_prefix: Option<String>,
330}
331
332impl ReferenceGeneratorBuilder {
333    /// Create a new builder.
334    pub fn new() -> Self {
335        Self::default()
336    }
337
338    /// Set the year.
339    pub fn year(mut self, year: i32) -> Self {
340        self.year = Some(year);
341        self
342    }
343
344    /// Set the company code.
345    pub fn company_code(mut self, code: &str) -> Self {
346        self.company_code = Some(code.to_string());
347        self
348    }
349
350    /// Set invoice prefix.
351    pub fn invoice_prefix(mut self, prefix: &str) -> Self {
352        self.invoice_prefix = Some(prefix.to_string());
353        self
354    }
355
356    /// Set PO prefix.
357    pub fn po_prefix(mut self, prefix: &str) -> Self {
358        self.po_prefix = Some(prefix.to_string());
359        self
360    }
361
362    /// Set SO prefix.
363    pub fn so_prefix(mut self, prefix: &str) -> Self {
364        self.so_prefix = Some(prefix.to_string());
365        self
366    }
367
368    /// Build the generator.
369    pub fn build(self) -> ReferenceGenerator {
370        let year = self.year.unwrap_or(2024);
371        let company = self.company_code.as_deref().unwrap_or("1000");
372
373        let mut gen = ReferenceGenerator::new(year, company);
374
375        if let Some(prefix) = self.invoice_prefix {
376            gen.set_prefix(ReferenceType::Invoice, &prefix);
377        }
378        if let Some(prefix) = self.po_prefix {
379            gen.set_prefix(ReferenceType::PurchaseOrder, &prefix);
380        }
381        if let Some(prefix) = self.so_prefix {
382            gen.set_prefix(ReferenceType::SalesOrder, &prefix);
383        }
384
385        gen
386    }
387}
388
389#[cfg(test)]
390#[allow(clippy::unwrap_used)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_sequential_generation() {
396        let mut gen = ReferenceGenerator::new(2024, "1000");
397
398        let ref1 = gen.generate(ReferenceType::Invoice);
399        let ref2 = gen.generate(ReferenceType::Invoice);
400        let ref3 = gen.generate(ReferenceType::Invoice);
401
402        assert!(ref1.starts_with("INV-2024-"));
403        assert!(ref2.starts_with("INV-2024-"));
404        assert!(ref3.starts_with("INV-2024-"));
405
406        // Should be sequential
407        assert_ne!(ref1, ref2);
408        assert_ne!(ref2, ref3);
409    }
410
411    #[test]
412    fn test_different_types() {
413        let mut gen = ReferenceGenerator::new(2024, "1000");
414
415        let inv = gen.generate(ReferenceType::Invoice);
416        let po = gen.generate(ReferenceType::PurchaseOrder);
417        let so = gen.generate(ReferenceType::SalesOrder);
418
419        assert!(inv.starts_with("INV-"));
420        assert!(po.starts_with("PO-"));
421        assert!(so.starts_with("SO-"));
422    }
423
424    #[test]
425    fn test_year_based_counters() {
426        let mut gen = ReferenceGenerator::new(2024, "1000");
427
428        let ref_2024 = gen.generate_for_year(ReferenceType::Invoice, 2024);
429        let ref_2025 = gen.generate_for_year(ReferenceType::Invoice, 2025);
430
431        assert!(ref_2024.contains("2024"));
432        assert!(ref_2025.contains("2025"));
433
434        // Different years should have independent counters
435        assert!(ref_2024.ends_with("000001"));
436        assert!(ref_2025.ends_with("000001"));
437    }
438
439    #[test]
440    fn test_business_process_mapping() {
441        let mut gen = ReferenceGenerator::new(2024, "1000");
442
443        let o2c_ref = gen.generate_for_process(BusinessProcess::O2C);
444        let p2p_ref = gen.generate_for_process(BusinessProcess::P2P);
445
446        assert!(o2c_ref.starts_with("SO-")); // Sales Order
447        assert!(p2p_ref.starts_with("PO-")); // Purchase Order
448    }
449
450    #[test]
451    fn test_custom_prefix() {
452        let mut gen = ReferenceGenerator::new(2024, "ACME");
453        gen.set_prefix(ReferenceType::Invoice, "ACME-INV");
454
455        let inv = gen.generate(ReferenceType::Invoice);
456        assert!(inv.starts_with("ACME-INV-"));
457    }
458
459    #[test]
460    fn test_builder() {
461        let mut gen = ReferenceGeneratorBuilder::new()
462            .year(2025)
463            .company_code("CORP")
464            .invoice_prefix("CORP-INV")
465            .build();
466
467        let inv = gen.generate(ReferenceType::Invoice);
468        assert!(inv.starts_with("CORP-INV-2025-"));
469    }
470
471    #[test]
472    fn test_external_reference() {
473        use rand::SeedableRng;
474        use rand_chacha::ChaCha8Rng;
475
476        let gen = ReferenceGenerator::new(2024, "1000");
477        let mut rng = ChaCha8Rng::seed_from_u64(42);
478
479        let ext_ref = gen.generate_external_reference(&mut rng);
480        assert!(!ext_ref.is_empty());
481    }
482}