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