1use 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::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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
79pub enum ReferenceFormat {
80 Sequential,
82 #[default]
84 YearPrefixed,
85 YearMonthPrefixed,
87 Random,
89 CompanyYearPrefixed,
91}
92
93#[derive(Debug, Clone)]
95pub struct ReferenceConfig {
96 pub prefix: String,
98 pub format: ReferenceFormat,
100 pub sequence_digits: usize,
102 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#[derive(Debug)]
119pub struct ReferenceGenerator {
120 configs: HashMap<ReferenceType, ReferenceConfig>,
122 counters: HashMap<(ReferenceType, Option<i32>), AtomicU64>,
124 default_year: i32,
126 company_code: String,
128}
129
130impl Default for ReferenceGenerator {
131 fn default() -> Self {
132 Self::new(2024, "1000")
133 }
134}
135
136impl ReferenceGenerator {
137 pub fn new(year: i32, company_code: &str) -> Self {
139 let mut configs = HashMap::new();
140
141 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 pub fn with_company_code(mut self, code: &str) -> Self {
176 self.company_code = code.to_string();
177 self
178 }
179
180 pub fn with_year(mut self, year: i32) -> Self {
182 self.default_year = year;
183 self
184 }
185
186 pub fn set_config(&mut self, ref_type: ReferenceType, config: ReferenceConfig) {
188 self.configs.insert(ref_type, config);
189 }
190
191 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 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 pub fn generate(&mut self, ref_type: ReferenceType) -> String {
213 self.generate_for_year(ref_type, self.default_year)
214 }
215
216 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 format!(
242 "{}-{}01-{:0width$}",
243 config.prefix,
244 year,
245 seq,
246 width = config.sequence_digits - 1
247 )
248 }
249 ReferenceFormat::Random => {
250 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 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 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 pub fn generate_external_reference(&self, rng: &mut impl Rng) -> String {
290 let formats = [
292 |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#[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 pub fn new() -> Self {
331 Self::default()
332 }
333
334 pub fn year(mut self, year: i32) -> Self {
336 self.year = Some(year);
337 self
338 }
339
340 pub fn company_code(mut self, code: &str) -> Self {
342 self.company_code = Some(code.to_string());
343 self
344 }
345
346 pub fn invoice_prefix(mut self, prefix: &str) -> Self {
348 self.invoice_prefix = Some(prefix.to_string());
349 self
350 }
351
352 pub fn po_prefix(mut self, prefix: &str) -> Self {
354 self.po_prefix = Some(prefix.to_string());
355 self
356 }
357
358 pub fn so_prefix(mut self, prefix: &str) -> Self {
360 self.so_prefix = Some(prefix.to_string());
361 self
362 }
363
364 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 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 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-")); assert!(p2p_ref.starts_with("PO-")); }
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}