1use crate::models::BusinessProcess;
7use rand::{Rng, RngExt};
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::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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
81pub enum ReferenceFormat {
82 Sequential,
84 #[default]
86 YearPrefixed,
87 YearMonthPrefixed,
89 Random,
91 CompanyYearPrefixed,
93}
94
95#[derive(Debug, Clone)]
97pub struct ReferenceConfig {
98 pub prefix: String,
100 pub format: ReferenceFormat,
102 pub sequence_digits: usize,
104 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#[derive(Debug)]
121pub struct ReferenceGenerator {
122 configs: HashMap<ReferenceType, ReferenceConfig>,
124 counters: HashMap<(ReferenceType, Option<i32>), AtomicU64>,
126 default_year: i32,
128 company_code: String,
130}
131
132impl Default for ReferenceGenerator {
133 fn default() -> Self {
134 Self::new(2024, "1000")
135 }
136}
137
138impl ReferenceGenerator {
139 pub fn new(year: i32, company_code: &str) -> Self {
141 let mut configs = HashMap::new();
142
143 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 pub fn with_company_code(mut self, code: &str) -> Self {
178 self.company_code = code.to_string();
179 self
180 }
181
182 pub fn with_year(mut self, year: i32) -> Self {
184 self.default_year = year;
185 self
186 }
187
188 pub fn set_config(&mut self, ref_type: ReferenceType, config: ReferenceConfig) {
190 self.configs.insert(ref_type, config);
191 }
192
193 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 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 pub fn generate(&mut self, ref_type: ReferenceType) -> String {
215 self.generate_for_year(ref_type, self.default_year)
216 }
217
218 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 format!(
244 "{}-{}01-{:0width$}",
245 config.prefix,
246 year,
247 seq,
248 width = config.sequence_digits - 1
249 )
250 }
251 ReferenceFormat::Random => {
252 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 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 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 pub fn generate_external_reference(&self, rng: &mut impl Rng) -> String {
297 let formats = [
299 |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#[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 pub fn new() -> Self {
338 Self::default()
339 }
340
341 pub fn year(mut self, year: i32) -> Self {
343 self.year = Some(year);
344 self
345 }
346
347 pub fn company_code(mut self, code: &str) -> Self {
349 self.company_code = Some(code.to_string());
350 self
351 }
352
353 pub fn invoice_prefix(mut self, prefix: &str) -> Self {
355 self.invoice_prefix = Some(prefix.to_string());
356 self
357 }
358
359 pub fn po_prefix(mut self, prefix: &str) -> Self {
361 self.po_prefix = Some(prefix.to_string());
362 self
363 }
364
365 pub fn so_prefix(mut self, prefix: &str) -> Self {
367 self.so_prefix = Some(prefix.to_string());
368 self
369 }
370
371 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 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 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-")); assert!(p2p_ref.starts_with("PO-")); }
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}