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, RngExt};
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 deterministic alphanumeric suffix based on sequence number
253                let suffix: String = (0..config.sequence_digits)
254                    .map(|i| {
255                        let idx = ((seq as usize)
256                            .wrapping_mul(7)
257                            .wrapping_add(i)
258                            .wrapping_mul(13)
259                            .wrapping_add(17))
260                            % 36;
261                        if idx < 10 {
262                            (b'0' + idx as u8) as char
263                        } else {
264                            (b'A' + idx as u8 - 10) as char
265                        }
266                    })
267                    .collect();
268                format!("{}-{}", config.prefix, suffix)
269            }
270            ReferenceFormat::CompanyYearPrefixed => {
271                format!(
272                    "{}-{}-{}-{:0width$}",
273                    config.prefix,
274                    self.company_code,
275                    year,
276                    seq,
277                    width = config.sequence_digits
278                )
279            }
280        }
281    }
282
283    /// Generate a reference for a business process.
284    pub fn generate_for_process(&mut self, process: BusinessProcess) -> String {
285        let ref_type = ReferenceType::for_business_process(process);
286        self.generate(ref_type)
287    }
288
289    /// Generate a reference for a business process and year.
290    pub fn generate_for_process_year(&mut self, process: BusinessProcess, year: i32) -> String {
291        let ref_type = ReferenceType::for_business_process(process);
292        self.generate_for_year(ref_type, year)
293    }
294
295    /// Generate an external reference (vendor invoice, etc.) with random elements.
296    pub fn generate_external_reference(&self, rng: &mut impl Rng) -> String {
297        // External references often have different formats
298        let formats = [
299            // Vendor invoice formats
300            |rng: &mut dyn rand::Rng| format!("INV{:08}", rng.random_range(10000000u64..99999999)),
301            |rng: &mut dyn rand::Rng| {
302                format!("{:010}", rng.random_range(1000000000u64..9999999999))
303            },
304            |rng: &mut dyn rand::Rng| {
305                format!(
306                    "V{}-{:06}",
307                    rng.random_range(100..999),
308                    rng.random_range(1..999999)
309                )
310            },
311            |rng: &mut dyn rand::Rng| {
312                format!(
313                    "{}{:07}",
314                    (b'A' + rng.random_range(0..26)) as char,
315                    rng.random_range(1000000..9999999)
316                )
317            },
318        ];
319
320        let idx = rng.random_range(0..formats.len());
321        formats[idx](rng)
322    }
323}
324
325/// Builder for configuring reference generation.
326#[derive(Debug, Clone, Default)]
327pub struct ReferenceGeneratorBuilder {
328    year: Option<i32>,
329    company_code: Option<String>,
330    invoice_prefix: Option<String>,
331    po_prefix: Option<String>,
332    so_prefix: Option<String>,
333}
334
335impl ReferenceGeneratorBuilder {
336    /// Create a new builder.
337    pub fn new() -> Self {
338        Self::default()
339    }
340
341    /// Set the year.
342    pub fn year(mut self, year: i32) -> Self {
343        self.year = Some(year);
344        self
345    }
346
347    /// Set the company code.
348    pub fn company_code(mut self, code: &str) -> Self {
349        self.company_code = Some(code.to_string());
350        self
351    }
352
353    /// Set invoice prefix.
354    pub fn invoice_prefix(mut self, prefix: &str) -> Self {
355        self.invoice_prefix = Some(prefix.to_string());
356        self
357    }
358
359    /// Set PO prefix.
360    pub fn po_prefix(mut self, prefix: &str) -> Self {
361        self.po_prefix = Some(prefix.to_string());
362        self
363    }
364
365    /// Set SO prefix.
366    pub fn so_prefix(mut self, prefix: &str) -> Self {
367        self.so_prefix = Some(prefix.to_string());
368        self
369    }
370
371    /// Build the generator.
372    pub fn build(self) -> ReferenceGenerator {
373        let year = self.year.unwrap_or(2024);
374        let company = self.company_code.as_deref().unwrap_or("1000");
375
376        let mut gen = ReferenceGenerator::new(year, company);
377
378        if let Some(prefix) = self.invoice_prefix {
379            gen.set_prefix(ReferenceType::Invoice, &prefix);
380        }
381        if let Some(prefix) = self.po_prefix {
382            gen.set_prefix(ReferenceType::PurchaseOrder, &prefix);
383        }
384        if let Some(prefix) = self.so_prefix {
385            gen.set_prefix(ReferenceType::SalesOrder, &prefix);
386        }
387
388        gen
389    }
390}
391
392#[cfg(test)]
393#[allow(clippy::unwrap_used)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn test_sequential_generation() {
399        let mut gen = ReferenceGenerator::new(2024, "1000");
400
401        let ref1 = gen.generate(ReferenceType::Invoice);
402        let ref2 = gen.generate(ReferenceType::Invoice);
403        let ref3 = gen.generate(ReferenceType::Invoice);
404
405        assert!(ref1.starts_with("INV-2024-"));
406        assert!(ref2.starts_with("INV-2024-"));
407        assert!(ref3.starts_with("INV-2024-"));
408
409        // Should be sequential
410        assert_ne!(ref1, ref2);
411        assert_ne!(ref2, ref3);
412    }
413
414    #[test]
415    fn test_different_types() {
416        let mut gen = ReferenceGenerator::new(2024, "1000");
417
418        let inv = gen.generate(ReferenceType::Invoice);
419        let po = gen.generate(ReferenceType::PurchaseOrder);
420        let so = gen.generate(ReferenceType::SalesOrder);
421
422        assert!(inv.starts_with("INV-"));
423        assert!(po.starts_with("PO-"));
424        assert!(so.starts_with("SO-"));
425    }
426
427    #[test]
428    fn test_year_based_counters() {
429        let mut gen = ReferenceGenerator::new(2024, "1000");
430
431        let ref_2024 = gen.generate_for_year(ReferenceType::Invoice, 2024);
432        let ref_2025 = gen.generate_for_year(ReferenceType::Invoice, 2025);
433
434        assert!(ref_2024.contains("2024"));
435        assert!(ref_2025.contains("2025"));
436
437        // Different years should have independent counters
438        assert!(ref_2024.ends_with("000001"));
439        assert!(ref_2025.ends_with("000001"));
440    }
441
442    #[test]
443    fn test_business_process_mapping() {
444        let mut gen = ReferenceGenerator::new(2024, "1000");
445
446        let o2c_ref = gen.generate_for_process(BusinessProcess::O2C);
447        let p2p_ref = gen.generate_for_process(BusinessProcess::P2P);
448
449        assert!(o2c_ref.starts_with("SO-")); // Sales Order
450        assert!(p2p_ref.starts_with("PO-")); // Purchase Order
451    }
452
453    #[test]
454    fn test_custom_prefix() {
455        let mut gen = ReferenceGenerator::new(2024, "ACME");
456        gen.set_prefix(ReferenceType::Invoice, "ACME-INV");
457
458        let inv = gen.generate(ReferenceType::Invoice);
459        assert!(inv.starts_with("ACME-INV-"));
460    }
461
462    #[test]
463    fn test_builder() {
464        let mut gen = ReferenceGeneratorBuilder::new()
465            .year(2025)
466            .company_code("CORP")
467            .invoice_prefix("CORP-INV")
468            .build();
469
470        let inv = gen.generate(ReferenceType::Invoice);
471        assert!(inv.starts_with("CORP-INV-2025-"));
472    }
473
474    #[test]
475    fn test_external_reference() {
476        use rand::SeedableRng;
477        use rand_chacha::ChaCha8Rng;
478
479        let gen = ReferenceGenerator::new(2024, "1000");
480        let mut rng = ChaCha8Rng::seed_from_u64(42);
481
482        let ext_ref = gen.generate_external_reference(&mut rng);
483        assert!(!ext_ref.is_empty());
484    }
485}