datasynth_core/templates/
references.rs1use crate::models::BusinessProcess;
7use rand::Rng;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ReferenceType {
16 Invoice,
18 PurchaseOrder,
20 SalesOrder,
22 GoodsReceipt,
24 PaymentReference,
26 AssetTag,
28 ProjectNumber,
30 ExpenseReport,
32 ContractNumber,
34 BatchNumber,
36 InternalDocument,
38}
39
40impl ReferenceType {
41 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 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::Treasury => Self::PaymentReference,
67 BusinessProcess::Tax => Self::InternalDocument,
68 BusinessProcess::Intercompany => Self::InternalDocument,
69 }
70 }
71}
72
73#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub enum ReferenceFormat {
76 Sequential,
78 #[default]
80 YearPrefixed,
81 YearMonthPrefixed,
83 Random,
85 CompanyYearPrefixed,
87}
88
89#[derive(Debug, Clone)]
91pub struct ReferenceConfig {
92 pub prefix: String,
94 pub format: ReferenceFormat,
96 pub sequence_digits: usize,
98 pub start_sequence: u64,
100}
101
102impl Default for ReferenceConfig {
103 fn default() -> Self {
104 Self {
105 prefix: "REF".to_string(),
106 format: ReferenceFormat::YearPrefixed,
107 sequence_digits: 6,
108 start_sequence: 1,
109 }
110 }
111}
112
113#[derive(Debug)]
115pub struct ReferenceGenerator {
116 configs: HashMap<ReferenceType, ReferenceConfig>,
118 counters: HashMap<(ReferenceType, Option<i32>), AtomicU64>,
120 default_year: i32,
122 company_code: String,
124}
125
126impl Default for ReferenceGenerator {
127 fn default() -> Self {
128 Self::new(2024, "1000")
129 }
130}
131
132impl ReferenceGenerator {
133 pub fn new(year: i32, company_code: &str) -> Self {
135 let mut configs = HashMap::new();
136
137 for ref_type in [
139 ReferenceType::Invoice,
140 ReferenceType::PurchaseOrder,
141 ReferenceType::SalesOrder,
142 ReferenceType::GoodsReceipt,
143 ReferenceType::PaymentReference,
144 ReferenceType::AssetTag,
145 ReferenceType::ProjectNumber,
146 ReferenceType::ExpenseReport,
147 ReferenceType::ContractNumber,
148 ReferenceType::BatchNumber,
149 ReferenceType::InternalDocument,
150 ] {
151 configs.insert(
152 ref_type,
153 ReferenceConfig {
154 prefix: ref_type.default_prefix().to_string(),
155 format: ReferenceFormat::YearPrefixed,
156 sequence_digits: 6,
157 start_sequence: 1,
158 },
159 );
160 }
161
162 Self {
163 configs,
164 counters: HashMap::new(),
165 default_year: year,
166 company_code: company_code.to_string(),
167 }
168 }
169
170 pub fn with_company_code(mut self, code: &str) -> Self {
172 self.company_code = code.to_string();
173 self
174 }
175
176 pub fn with_year(mut self, year: i32) -> Self {
178 self.default_year = year;
179 self
180 }
181
182 pub fn set_config(&mut self, ref_type: ReferenceType, config: ReferenceConfig) {
184 self.configs.insert(ref_type, config);
185 }
186
187 pub fn set_prefix(&mut self, ref_type: ReferenceType, prefix: &str) {
189 if let Some(config) = self.configs.get_mut(&ref_type) {
190 config.prefix = prefix.to_string();
191 }
192 }
193
194 fn next_sequence(&mut self, ref_type: ReferenceType, year: Option<i32>) -> u64 {
196 let key = (ref_type, year);
197 let config = self.configs.get(&ref_type).cloned().unwrap_or_default();
198
199 let counter = self
200 .counters
201 .entry(key)
202 .or_insert_with(|| AtomicU64::new(config.start_sequence));
203
204 counter.fetch_add(1, Ordering::SeqCst)
205 }
206
207 pub fn generate(&mut self, ref_type: ReferenceType) -> String {
209 self.generate_for_year(ref_type, self.default_year)
210 }
211
212 pub fn generate_for_year(&mut self, ref_type: ReferenceType, year: i32) -> String {
214 let config = self.configs.get(&ref_type).cloned().unwrap_or_default();
215 let seq = self.next_sequence(ref_type, Some(year));
216
217 match config.format {
218 ReferenceFormat::Sequential => {
219 format!(
220 "{}-{:0width$}",
221 config.prefix,
222 seq,
223 width = config.sequence_digits
224 )
225 }
226 ReferenceFormat::YearPrefixed => {
227 format!(
228 "{}-{}-{:0width$}",
229 config.prefix,
230 year,
231 seq,
232 width = config.sequence_digits
233 )
234 }
235 ReferenceFormat::YearMonthPrefixed => {
236 format!(
238 "{}-{}01-{:0width$}",
239 config.prefix,
240 year,
241 seq,
242 width = config.sequence_digits - 1
243 )
244 }
245 ReferenceFormat::Random => {
246 let suffix: String = (0..config.sequence_digits)
248 .map(|_| {
249 let idx = rand::thread_rng().gen_range(0..36);
250 if idx < 10 {
251 (b'0' + idx) as char
252 } else {
253 (b'A' + idx - 10) as char
254 }
255 })
256 .collect();
257 format!("{}-{}", config.prefix, suffix)
258 }
259 ReferenceFormat::CompanyYearPrefixed => {
260 format!(
261 "{}-{}-{}-{:0width$}",
262 config.prefix,
263 self.company_code,
264 year,
265 seq,
266 width = config.sequence_digits
267 )
268 }
269 }
270 }
271
272 pub fn generate_for_process(&mut self, process: BusinessProcess) -> String {
274 let ref_type = ReferenceType::for_business_process(process);
275 self.generate(ref_type)
276 }
277
278 pub fn generate_for_process_year(&mut self, process: BusinessProcess, year: i32) -> String {
280 let ref_type = ReferenceType::for_business_process(process);
281 self.generate_for_year(ref_type, year)
282 }
283
284 pub fn generate_external_reference(&self, rng: &mut impl Rng) -> String {
286 let formats = [
288 |rng: &mut dyn rand::RngCore| format!("INV{:08}", rng.gen_range(10000000u64..99999999)),
290 |rng: &mut dyn rand::RngCore| {
291 format!("{:010}", rng.gen_range(1000000000u64..9999999999))
292 },
293 |rng: &mut dyn rand::RngCore| {
294 format!(
295 "V{}-{:06}",
296 rng.gen_range(100..999),
297 rng.gen_range(1..999999)
298 )
299 },
300 |rng: &mut dyn rand::RngCore| {
301 format!(
302 "{}{:07}",
303 (b'A' + rng.gen_range(0..26)) as char,
304 rng.gen_range(1000000..9999999)
305 )
306 },
307 ];
308
309 let idx = rng.gen_range(0..formats.len());
310 formats[idx](rng)
311 }
312}
313
314#[derive(Debug, Clone, Default)]
316pub struct ReferenceGeneratorBuilder {
317 year: Option<i32>,
318 company_code: Option<String>,
319 invoice_prefix: Option<String>,
320 po_prefix: Option<String>,
321 so_prefix: Option<String>,
322}
323
324impl ReferenceGeneratorBuilder {
325 pub fn new() -> Self {
327 Self::default()
328 }
329
330 pub fn year(mut self, year: i32) -> Self {
332 self.year = Some(year);
333 self
334 }
335
336 pub fn company_code(mut self, code: &str) -> Self {
338 self.company_code = Some(code.to_string());
339 self
340 }
341
342 pub fn invoice_prefix(mut self, prefix: &str) -> Self {
344 self.invoice_prefix = Some(prefix.to_string());
345 self
346 }
347
348 pub fn po_prefix(mut self, prefix: &str) -> Self {
350 self.po_prefix = Some(prefix.to_string());
351 self
352 }
353
354 pub fn so_prefix(mut self, prefix: &str) -> Self {
356 self.so_prefix = Some(prefix.to_string());
357 self
358 }
359
360 pub fn build(self) -> ReferenceGenerator {
362 let year = self.year.unwrap_or(2024);
363 let company = self.company_code.as_deref().unwrap_or("1000");
364
365 let mut gen = ReferenceGenerator::new(year, company);
366
367 if let Some(prefix) = self.invoice_prefix {
368 gen.set_prefix(ReferenceType::Invoice, &prefix);
369 }
370 if let Some(prefix) = self.po_prefix {
371 gen.set_prefix(ReferenceType::PurchaseOrder, &prefix);
372 }
373 if let Some(prefix) = self.so_prefix {
374 gen.set_prefix(ReferenceType::SalesOrder, &prefix);
375 }
376
377 gen
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_sequential_generation() {
387 let mut gen = ReferenceGenerator::new(2024, "1000");
388
389 let ref1 = gen.generate(ReferenceType::Invoice);
390 let ref2 = gen.generate(ReferenceType::Invoice);
391 let ref3 = gen.generate(ReferenceType::Invoice);
392
393 assert!(ref1.starts_with("INV-2024-"));
394 assert!(ref2.starts_with("INV-2024-"));
395 assert!(ref3.starts_with("INV-2024-"));
396
397 assert_ne!(ref1, ref2);
399 assert_ne!(ref2, ref3);
400 }
401
402 #[test]
403 fn test_different_types() {
404 let mut gen = ReferenceGenerator::new(2024, "1000");
405
406 let inv = gen.generate(ReferenceType::Invoice);
407 let po = gen.generate(ReferenceType::PurchaseOrder);
408 let so = gen.generate(ReferenceType::SalesOrder);
409
410 assert!(inv.starts_with("INV-"));
411 assert!(po.starts_with("PO-"));
412 assert!(so.starts_with("SO-"));
413 }
414
415 #[test]
416 fn test_year_based_counters() {
417 let mut gen = ReferenceGenerator::new(2024, "1000");
418
419 let ref_2024 = gen.generate_for_year(ReferenceType::Invoice, 2024);
420 let ref_2025 = gen.generate_for_year(ReferenceType::Invoice, 2025);
421
422 assert!(ref_2024.contains("2024"));
423 assert!(ref_2025.contains("2025"));
424
425 assert!(ref_2024.ends_with("000001"));
427 assert!(ref_2025.ends_with("000001"));
428 }
429
430 #[test]
431 fn test_business_process_mapping() {
432 let mut gen = ReferenceGenerator::new(2024, "1000");
433
434 let o2c_ref = gen.generate_for_process(BusinessProcess::O2C);
435 let p2p_ref = gen.generate_for_process(BusinessProcess::P2P);
436
437 assert!(o2c_ref.starts_with("SO-")); assert!(p2p_ref.starts_with("PO-")); }
440
441 #[test]
442 fn test_custom_prefix() {
443 let mut gen = ReferenceGenerator::new(2024, "ACME");
444 gen.set_prefix(ReferenceType::Invoice, "ACME-INV");
445
446 let inv = gen.generate(ReferenceType::Invoice);
447 assert!(inv.starts_with("ACME-INV-"));
448 }
449
450 #[test]
451 fn test_builder() {
452 let mut gen = ReferenceGeneratorBuilder::new()
453 .year(2025)
454 .company_code("CORP")
455 .invoice_prefix("CORP-INV")
456 .build();
457
458 let inv = gen.generate(ReferenceType::Invoice);
459 assert!(inv.starts_with("CORP-INV-2025-"));
460 }
461
462 #[test]
463 fn test_external_reference() {
464 use rand::SeedableRng;
465 use rand_chacha::ChaCha8Rng;
466
467 let gen = ReferenceGenerator::new(2024, "1000");
468 let mut rng = ChaCha8Rng::seed_from_u64(42);
469
470 let ext_ref = gen.generate_external_reference(&mut rng);
471 assert!(!ext_ref.is_empty());
472 }
473}