1use rand::Rng;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Mutex;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
14#[serde(rename_all = "snake_case")]
15pub enum EnhancedReferenceFormat {
16 #[default]
18 Standard,
19 SapStyle,
21 OracleStyle,
23 NetSuiteStyle,
25 Alphanumeric,
27 ShortUuid,
29 DateBased,
31 VendorInvoice,
33 BankReference,
35 CheckNumber,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
41#[serde(rename_all = "snake_case")]
42pub enum ReferenceStyle {
43 #[default]
44 Modern,
45 Legacy,
46 Erp,
47 Simple,
48}
49
50#[derive(Debug)]
52pub struct EnhancedReferenceGenerator {
53 counters: Mutex<HashMap<(EnhancedReferenceFormat, i32), AtomicU64>>,
54 sap_counter: AtomicU64,
55 check_counter: AtomicU64,
56}
57
58impl Clone for EnhancedReferenceGenerator {
59 fn clone(&self) -> Self {
60 Self::new()
61 }
62}
63
64impl Default for EnhancedReferenceGenerator {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl EnhancedReferenceGenerator {
71 pub fn new() -> Self {
73 Self {
74 counters: Mutex::new(HashMap::new()),
75 sap_counter: AtomicU64::new(4500000001),
76 check_counter: AtomicU64::new(100001),
77 }
78 }
79
80 pub fn generate(
82 &self,
83 format: EnhancedReferenceFormat,
84 year: i32,
85 rng: &mut impl Rng,
86 ) -> String {
87 match format {
88 EnhancedReferenceFormat::Standard => self.generate_standard(year),
89 EnhancedReferenceFormat::SapStyle => self.generate_sap_style(),
90 EnhancedReferenceFormat::OracleStyle => self.generate_oracle_style(year),
91 EnhancedReferenceFormat::NetSuiteStyle => self.generate_netsuite_style(),
92 EnhancedReferenceFormat::Alphanumeric => self.generate_alphanumeric(rng),
93 EnhancedReferenceFormat::ShortUuid => self.generate_short_uuid(rng),
94 EnhancedReferenceFormat::DateBased => self.generate_date_based(year, rng),
95 EnhancedReferenceFormat::VendorInvoice => self.generate_vendor_invoice(rng),
96 EnhancedReferenceFormat::BankReference => self.generate_bank_reference(year, rng),
97 EnhancedReferenceFormat::CheckNumber => self.generate_check_number(),
98 }
99 }
100
101 pub fn generate_for_document(
103 &self,
104 doc_type: DocumentType,
105 year: i32,
106 _rng: &mut impl Rng,
107 ) -> String {
108 let prefix = doc_type.prefix();
109 let seq = self.next_sequence(EnhancedReferenceFormat::Standard, year);
110 format!("{}-{}-{:06}", prefix, year, seq)
111 }
112
113 pub fn generate_external(&self, rng: &mut impl Rng) -> String {
115 self.generate_vendor_invoice(rng)
116 }
117
118 fn generate_standard(&self, year: i32) -> String {
119 let seq = self.next_sequence(EnhancedReferenceFormat::Standard, year);
120 format!("DOC-{}-{:06}", year, seq)
121 }
122
123 fn generate_sap_style(&self) -> String {
124 let num = self.sap_counter.fetch_add(1, Ordering::Relaxed);
125 format!("{:010}", num)
126 }
127
128 fn generate_oracle_style(&self, year: i32) -> String {
129 let seq = self.next_sequence(EnhancedReferenceFormat::OracleStyle, year);
130 format!("ORG1-{}-{:05}", year, seq)
131 }
132
133 fn generate_netsuite_style(&self) -> String {
134 let seq = self.next_sequence(EnhancedReferenceFormat::NetSuiteStyle, 0);
135 format!("INV{:05}", seq)
136 }
137
138 fn generate_alphanumeric(&self, rng: &mut impl Rng) -> String {
139 let letters: String = (0..3)
140 .map(|_| (b'A' + rng.gen_range(0..26)) as char)
141 .collect();
142 let numbers = rng.gen_range(100000..999999);
143 let check = (b'A' + rng.gen_range(0..26)) as char;
144 format!("{}{:06}{}", letters, numbers, check)
145 }
146
147 fn generate_short_uuid(&self, rng: &mut impl Rng) -> String {
148 let chars: String = (0..8)
149 .map(|_| {
150 let idx = rng.gen_range(0..36);
151 if idx < 10 {
152 (b'0' + idx) as char
153 } else {
154 (b'A' + idx - 10) as char
155 }
156 })
157 .collect();
158 chars
159 }
160
161 fn generate_date_based(&self, year: i32, rng: &mut impl Rng) -> String {
162 let month = rng.gen_range(1..=12);
163 let day = rng.gen_range(1..=28);
164 let seq = rng.gen_range(1..=9999);
165 format!("{}{:02}{:02}-{:04}", year, month, day, seq)
166 }
167
168 fn generate_vendor_invoice(&self, rng: &mut impl Rng) -> String {
169 let style = rng.gen_range(0..8);
170 match style {
171 0 => {
172 format!("INV-{:08}", rng.gen_range(10000000..99999999))
174 }
175 1 => {
176 format!("{:010}", rng.gen_range(1000000000u64..9999999999))
178 }
179 2 => {
180 format!(
182 "V{:03}-{:06}",
183 rng.gen_range(100..999),
184 rng.gen_range(100000..999999)
185 )
186 }
187 3 => {
188 let letter = (b'A' + rng.gen_range(0..26)) as char;
190 format!("{}{:07}", letter, rng.gen_range(1000000..9999999))
191 }
192 4 => {
193 let year = rng.gen_range(2020..=2025);
195 format!("{}-{:06}", year, rng.gen_range(1..999999))
196 }
197 5 => {
198 format!("PO{:08}", rng.gen_range(10000000..99999999))
200 }
201 6 => {
202 let alpha: String = (0..2)
204 .map(|_| (b'A' + rng.gen_range(0..26)) as char)
205 .collect();
206 format!("{}{:06}", alpha, rng.gen_range(100000..999999))
207 }
208 _ => {
209 format!(
211 "{:04X}-{:04X}",
212 rng.gen_range(0..0xFFFF),
213 rng.gen_range(0..0xFFFF)
214 )
215 }
216 }
217 }
218
219 fn generate_bank_reference(&self, year: i32, rng: &mut impl Rng) -> String {
220 let month = rng.gen_range(1..=12);
221 let day = rng.gen_range(1..=28);
222 let seq = rng.gen_range(1..=999999);
223 format!("BNK{}{:02}{:02}{:06}", year, month, day, seq)
224 }
225
226 fn generate_check_number(&self) -> String {
227 let num = self.check_counter.fetch_add(1, Ordering::Relaxed);
228 format!("{:06}", num)
229 }
230
231 fn next_sequence(&self, format: EnhancedReferenceFormat, year: i32) -> u64 {
232 let mut counters = self.counters.lock().unwrap();
233 let counter = counters
234 .entry((format, year))
235 .or_insert_with(|| AtomicU64::new(1));
236 counter.fetch_add(1, Ordering::Relaxed)
237 }
238
239 pub fn reset(&self) {
241 self.counters.lock().unwrap().clear();
242 self.sap_counter.store(4500000001, Ordering::Relaxed);
243 self.check_counter.store(100001, Ordering::Relaxed);
244 }
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249pub enum DocumentType {
250 Invoice,
251 PurchaseOrder,
252 SalesOrder,
253 GoodsReceipt,
254 Payment,
255 JournalEntry,
256 CreditMemo,
257 DebitMemo,
258 Delivery,
259 Return,
260 Adjustment,
261 Transfer,
262}
263
264impl DocumentType {
265 pub fn prefix(&self) -> &'static str {
267 match self {
268 DocumentType::Invoice => "INV",
269 DocumentType::PurchaseOrder => "PO",
270 DocumentType::SalesOrder => "SO",
271 DocumentType::GoodsReceipt => "GR",
272 DocumentType::Payment => "PMT",
273 DocumentType::JournalEntry => "JE",
274 DocumentType::CreditMemo => "CM",
275 DocumentType::DebitMemo => "DM",
276 DocumentType::Delivery => "DL",
277 DocumentType::Return => "RET",
278 DocumentType::Adjustment => "ADJ",
279 DocumentType::Transfer => "TRF",
280 }
281 }
282
283 pub fn sap_code(&self) -> &'static str {
285 match self {
286 DocumentType::Invoice => "RE",
287 DocumentType::PurchaseOrder => "NB",
288 DocumentType::SalesOrder => "TA",
289 DocumentType::GoodsReceipt => "WE",
290 DocumentType::Payment => "ZP",
291 DocumentType::JournalEntry => "SA",
292 DocumentType::CreditMemo => "KR",
293 DocumentType::DebitMemo => "DR",
294 DocumentType::Delivery => "LF",
295 DocumentType::Return => "AF",
296 DocumentType::Adjustment => "AB",
297 DocumentType::Transfer => "UE",
298 }
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use rand::SeedableRng;
306 use rand_chacha::ChaCha8Rng;
307
308 #[test]
309 fn test_standard_format() {
310 let gen = EnhancedReferenceGenerator::new();
311 let mut rng = ChaCha8Rng::seed_from_u64(42);
312
313 let ref1 = gen.generate(EnhancedReferenceFormat::Standard, 2024, &mut rng);
314 assert!(ref1.starts_with("DOC-2024-"));
315 assert!(ref1.len() == 15);
316
317 let ref2 = gen.generate(EnhancedReferenceFormat::Standard, 2024, &mut rng);
318 assert_ne!(ref1, ref2); }
320
321 #[test]
322 fn test_sap_style_format() {
323 let gen = EnhancedReferenceGenerator::new();
324 let mut rng = ChaCha8Rng::seed_from_u64(42);
325
326 let ref1 = gen.generate(EnhancedReferenceFormat::SapStyle, 2024, &mut rng);
327 assert!(ref1.len() == 10);
328 assert!(ref1.starts_with("4500"));
329
330 let ref2 = gen.generate(EnhancedReferenceFormat::SapStyle, 2024, &mut rng);
331 assert_ne!(ref1, ref2);
332 }
333
334 #[test]
335 fn test_oracle_style_format() {
336 let gen = EnhancedReferenceGenerator::new();
337 let mut rng = ChaCha8Rng::seed_from_u64(42);
338
339 let ref1 = gen.generate(EnhancedReferenceFormat::OracleStyle, 2024, &mut rng);
340 assert!(ref1.starts_with("ORG1-2024-"));
341 }
342
343 #[test]
344 fn test_alphanumeric_format() {
345 let gen = EnhancedReferenceGenerator::new();
346 let mut rng = ChaCha8Rng::seed_from_u64(42);
347
348 let ref1 = gen.generate(EnhancedReferenceFormat::Alphanumeric, 2024, &mut rng);
349 assert!(ref1.len() == 10);
350 assert!(ref1.chars().take(3).all(|c| c.is_ascii_uppercase()));
351 assert!(ref1.chars().last().unwrap().is_ascii_uppercase());
352 }
353
354 #[test]
355 fn test_vendor_invoice_variety() {
356 let gen = EnhancedReferenceGenerator::new();
357 let mut rng = ChaCha8Rng::seed_from_u64(42);
358
359 let mut formats = std::collections::HashSet::new();
360 for _ in 0..100 {
361 let ref1 = gen.generate(EnhancedReferenceFormat::VendorInvoice, 2024, &mut rng);
362 let pattern: String = ref1.chars().take(3).collect();
364 formats.insert(pattern);
365 }
366
367 assert!(formats.len() > 3);
369 }
370
371 #[test]
372 fn test_document_type_generation() {
373 let gen = EnhancedReferenceGenerator::new();
374 let mut rng = ChaCha8Rng::seed_from_u64(42);
375
376 let inv = gen.generate_for_document(DocumentType::Invoice, 2024, &mut rng);
377 assert!(inv.starts_with("INV-2024-"));
378
379 let po = gen.generate_for_document(DocumentType::PurchaseOrder, 2024, &mut rng);
380 assert!(po.starts_with("PO-2024-"));
381
382 let je = gen.generate_for_document(DocumentType::JournalEntry, 2024, &mut rng);
383 assert!(je.starts_with("JE-2024-"));
384 }
385
386 #[test]
387 fn test_check_number_sequential() {
388 let gen = EnhancedReferenceGenerator::new();
389 let mut rng = ChaCha8Rng::seed_from_u64(42);
390
391 let check1 = gen.generate(EnhancedReferenceFormat::CheckNumber, 2024, &mut rng);
392 let check2 = gen.generate(EnhancedReferenceFormat::CheckNumber, 2024, &mut rng);
393 let check3 = gen.generate(EnhancedReferenceFormat::CheckNumber, 2024, &mut rng);
394
395 let num1: u64 = check1.parse().unwrap();
397 let num2: u64 = check2.parse().unwrap();
398 let num3: u64 = check3.parse().unwrap();
399
400 assert_eq!(num2, num1 + 1);
401 assert_eq!(num3, num2 + 1);
402 }
403
404 #[test]
405 fn test_document_type_prefixes() {
406 assert_eq!(DocumentType::Invoice.prefix(), "INV");
407 assert_eq!(DocumentType::PurchaseOrder.prefix(), "PO");
408 assert_eq!(DocumentType::JournalEntry.prefix(), "JE");
409 assert_eq!(DocumentType::Payment.prefix(), "PMT");
410 }
411
412 #[test]
413 fn test_sap_codes() {
414 assert_eq!(DocumentType::Invoice.sap_code(), "RE");
415 assert_eq!(DocumentType::PurchaseOrder.sap_code(), "NB");
416 assert_eq!(DocumentType::GoodsReceipt.sap_code(), "WE");
417 }
418
419 #[test]
420 fn test_reset_counters() {
421 let gen = EnhancedReferenceGenerator::new();
422 let mut rng = ChaCha8Rng::seed_from_u64(42);
423
424 let ref1 = gen.generate(EnhancedReferenceFormat::Standard, 2024, &mut rng);
425 gen.reset();
426 let mut rng2 = ChaCha8Rng::seed_from_u64(42);
427 let ref2 = gen.generate(EnhancedReferenceFormat::Standard, 2024, &mut rng2);
428
429 assert_eq!(ref1, ref2);
430 }
431}