1use num_derive::FromPrimitive;
62use num_traits::FromPrimitive;
63use std::convert::From;
64use std::str::FromStr;
65use std::string::ToString;
66
67#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
68#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, thiserror::Error)]
69pub enum BillError {
70 #[error("Barcode length must be 26 chars")]
71 InvalidBarcodeLength,
72 #[error("Payment ID length must be >= 6 and <= 13")]
73 InvalidPaymentIDLength,
74 #[error("Payment ID is not valid")]
75 InvalidPaymentID,
76 #[error("Bill ID length must be >= 6 and <= 13")]
77 InvalidBillIDLength,
78 #[error("Bill Amount is not parsable")]
79 InvalidBillIDAmount,
80 #[error("Bill Year is not parsable")]
81 InvalidBillIDYear,
82 #[error("Bill Period is not parable")]
83 InvalidBillIDPeriod,
84 #[error("Checksum doesn't match")]
85 InvalidBillChecksum,
86 #[error("Bill Type is not parsable")]
87 InvalidBillType,
88 #[error("Cannot convert to digit")]
89 InvalidDigits,
90}
91
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100#[derive(FromPrimitive, Clone, Copy, PartialEq, Eq, Debug, Hash)]
101pub enum BillType {
102 Water = 1,
105 Electricity = 2,
108 Gas = 3,
111 Tel = 4,
114 Mobile = 5,
117 Municipality = 6,
120 Tax = 7,
123 DrivingOffense = 8,
126}
127
128#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
129#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
130pub enum CurrencyType {
131 Rials,
132 Tomans,
133}
134
135#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143#[derive(Debug, PartialEq, Eq, Clone, Hash)]
144pub struct BillID {
145 pub file_id: String,
147 pub company_code: String,
149 pub r#type: BillType,
151 pub checksum: u8,
152}
153impl FromStr for BillID {
154 type Err = BillError;
155 fn from_str(s: &str) -> Result<BillID, Self::Err> {
159 if s.len() < 6 || s.len() > 13 {
160 return Err(BillError::InvalidBillIDLength);
161 }
162 let checksum = s
163 .chars()
164 .nth_back(0)
165 .ok_or(Self::Err::InvalidBillChecksum)?
166 .to_digit(10)
167 .ok_or(Self::Err::InvalidBillChecksum)? as u8;
168
169 let calculated_checksum = base11_checksum(&s[..s.len() - 1])?;
170 if calculated_checksum != checksum {
171 return Err(BillError::InvalidBillChecksum);
172 }
173 let r#type = s
174 .chars()
175 .nth_back(1)
176 .ok_or(Self::Err::InvalidBillChecksum)?
177 .to_digit(10)
178 .ok_or(Self::Err::InvalidBillChecksum)? as u8;
179 let r#type: BillType = FromPrimitive::from_u8(r#type).ok_or(Self::Err::InvalidBillType)?;
180
181 let company_code = String::from(&s[s.len() - 5..s.len() - 2]);
182 let file_id = String::from(&s[..s.len() - 5]);
183 Ok(BillID {
184 file_id,
185 company_code,
186 r#type,
187 checksum,
188 })
189 }
190}
191
192impl BillID {
193 pub fn new(file_id: &str, company_code: &str, r#type: BillType) -> Result<Self, BillError> {
195 let s = format!("{}{:03}{}", file_id, company_code, r#type as u8);
196 let checksum = base11_checksum(&s)?;
197 Ok(BillID {
198 file_id: String::from(file_id),
199 company_code: String::from(company_code),
200 r#type,
201 checksum,
202 })
203 }
204}
205impl ToString for BillID {
206 fn to_string(&self) -> String {
208 format!(
209 "{}{:03}{:1}{:1}",
210 self.file_id, self.company_code, self.r#type as u8, self.checksum
211 )
212 }
213}
214
215#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
225pub struct PaymentID {
226 amount: u64,
228 year: u8,
230 period: u8,
232 checksum1: u8,
233 checksum2: u8,
234}
235impl std::str::FromStr for PaymentID {
236 type Err = BillError;
237
238 fn from_str(s: &str) -> Result<PaymentID, Self::Err> {
243 if s.len() < 6 || s.len() > 13 {
244 return Err(BillError::InvalidPaymentIDLength);
245 }
246 let checksum1 = s
247 .chars()
248 .nth_back(1)
249 .ok_or(Self::Err::InvalidBillIDYear)?
250 .to_digit(10)
251 .ok_or(Self::Err::InvalidBillChecksum)? as u8;
252 let calculated_checksum = base11_checksum(&s[..s.len() - 2])?;
253 if calculated_checksum != checksum1 {
254 return Err(BillError::InvalidBillChecksum);
255 }
256
257 let amount = (s[..s.len() - 5])
258 .parse::<u64>()
259 .map_err(|_| Self::Err::InvalidBillIDAmount)?;
260 let year = s
261 .chars()
262 .nth_back(4)
263 .ok_or(Self::Err::InvalidBillIDYear)?
264 .to_digit(10)
265 .ok_or(Self::Err::InvalidBillIDYear)? as u8;
266
267 let period = (s[s.len() - 4..s.len() - 2])
268 .parse::<u8>()
269 .map_err(|_| Self::Err::InvalidBillIDPeriod)?;
270 let checksum2 = s
271 .chars()
272 .nth_back(0)
273 .ok_or(Self::Err::InvalidBillIDYear)?
274 .to_digit(10)
275 .ok_or(Self::Err::InvalidBillChecksum)? as u8;
276
277 Ok(PaymentID {
278 amount,
279 year,
280 period,
281 checksum1,
282 checksum2,
283 })
284 }
285}
286
287impl ToString for PaymentID {
288 fn to_string(&self) -> String {
290 format!(
291 "{}{}{:02}{}{}",
292 self.amount, self.year, self.period, self.checksum1, self.checksum2
293 )
294 }
295}
296
297impl PaymentID {
298 pub fn new(amount: u64, year: u8, period: u8, bill_id: &BillID) -> Result<Self, BillError> {
300 let s = format!("{}{}{:02}", amount, year, period);
301 let checksum1 = base11_checksum(&s)?;
302 let s = format!("{}{}{}", bill_id.to_string(), s, checksum1);
303 let checksum2 = base11_checksum(&s)?;
304 Ok(PaymentID {
305 amount,
306 year,
307 period,
308 checksum1,
309 checksum2,
310 })
311 }
312
313 pub fn get_amount(&self) -> u64 {
314 self.amount
315 }
316 pub fn get_year(&self) -> u8 {
317 self.year
318 }
319 pub fn get_period(&self) -> u8 {
320 self.period
321 }
322 pub fn get_checksum1(&self) -> u8 {
323 self.checksum1
324 }
325 pub fn get_checksum2(&self) -> u8 {
326 self.checksum2
327 }
328}
329
330#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
333#[derive(Debug, PartialEq, Eq, Clone, Hash)]
334pub struct Bill {
335 pub bill_id: BillID,
336 pub payment_id: PaymentID,
337}
338
339impl std::str::FromStr for Bill {
340 type Err = BillError;
341 fn from_str(barcode: &str) -> std::result::Result<Bill, BillError> {
346 if barcode.len() != 26 {
347 return Err(BillError::InvalidBarcodeLength);
348 }
349 let bill_id = BillID::from_str(&barcode[..13])?;
350 let payment_id = PaymentID::from_str(&barcode[16..])?;
351 let bill = Bill::new(bill_id, payment_id)?;
352 Ok(bill)
353 }
354}
355
356impl Bill {
357 fn validate(&self) -> Result<(), BillError> {
358 let mut merged = self.bill_id.to_string();
359 merged.push_str(&self.payment_id.to_string());
360 let calculated_checksum = base11_checksum(&merged[..&merged.len() - 1])?;
361 if calculated_checksum != self.payment_id.checksum2 {
362 return Err(BillError::InvalidBillChecksum);
363 }
364 Ok(())
365 }
366 pub fn new(bill_id: BillID, payment_id: PaymentID) -> Result<Self, BillError> {
367 let bill = Bill {
368 bill_id,
369 payment_id,
370 };
371 bill.validate()?;
372 Ok(bill)
373 }
374 pub fn amount(&self, currency: CurrencyType) -> u64 {
375 match currency {
376 CurrencyType::Rials => self.payment_id.amount * 1000,
377 CurrencyType::Tomans => self.payment_id.amount * 100,
378 }
379 }
380 pub fn get_bill_type(&self) -> BillType {
381 self.bill_id.r#type
382 }
383
384 pub fn get_bill_id(&self) -> String {
385 self.bill_id.to_string()
386 }
387
388 pub fn get_payment_id(&self) -> String {
389 self.payment_id.to_string()
390 }
391}
392
393impl ToString for Bill {
394 fn to_string(&self) -> String {
396 format!(
397 "{:0>13}{:0>13}",
398 self.bill_id.to_string(),
399 self.payment_id.to_string()
400 )
401 }
402}
403fn base11_checksum(s: &str) -> Result<u8, BillError> {
404 let mut sum: u32 =
405 s.chars()
406 .rev()
407 .enumerate()
408 .try_fold(0u32, |acc, (i, v)| -> Result<u32, BillError> {
409 let multiplier = i as u32 % 6 + 2;
410 let digit = v.to_digit(10).ok_or(BillError::InvalidDigits)?;
411 Ok(acc + digit * multiplier)
412 })?;
413 sum %= 11;
414 if sum < 2 {
415 Ok(0u8)
416 } else {
417 Ok(11 - sum as u8)
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use std::str::FromStr;
424
425 use crate::bill::{Bill, BillID, BillType, CurrencyType, PaymentID};
426
427 #[test]
428 fn bill_load_from_barcode_test() {
429 let barcode = "11177532001400000012070160";
430 let bill = Bill::from_str(barcode);
431 assert!(bill.is_ok());
432 let bill = bill.unwrap();
433 assert_eq!(bill.amount(CurrencyType::Tomans), 12000);
434 assert_eq!(bill.amount(CurrencyType::Rials), 120000);
435 }
436
437 #[test]
438 fn bill_load_from_ids_test() {
439 let bill_id = BillID::from_str("1117753200140");
440 let payment_id = PaymentID::from_str("12070160");
441 assert!(bill_id.is_ok());
442 assert!(payment_id.is_ok());
443
444 let bill_id = bill_id.unwrap();
445 let payment_id = payment_id.unwrap();
446 let bill = Bill::new(bill_id, payment_id);
447 assert!(bill.is_ok());
448
449 let bill = bill.unwrap();
450 assert_eq!(bill.amount(CurrencyType::Tomans), 12000);
451 assert_eq!(bill.amount(CurrencyType::Rials), 120000);
452
453 assert_eq!(
454 Bill::new(
455 BillID::from_str("1177809000142").unwrap(),
456 PaymentID::from_str("570108").unwrap()
457 )
458 .unwrap()
459 .amount(CurrencyType::Rials),
460 5000,
461 );
462 assert_eq!(
463 Bill::new(
464 BillID::from_str("1177809000142").unwrap(),
465 PaymentID::from_str("570108").unwrap()
466 )
467 .unwrap()
468 .amount(CurrencyType::Tomans),
469 500,
470 );
471
472 assert_eq!(
479 Bill::new(
480 BillID::from_str("1117753200140").unwrap(),
481 PaymentID::from_str("1770163").unwrap()
482 )
483 .unwrap()
484 .amount(CurrencyType::Rials),
485 17000,
486 );
487 assert_eq!(
488 Bill::new(
489 BillID::from_str("1117753200140").unwrap(),
490 PaymentID::from_str("1770163").unwrap()
491 )
492 .unwrap()
493 .amount(CurrencyType::Tomans),
494 1700,
495 );
496 }
497
498 #[test]
499 fn bill_instantiate_test() {
500 let bill_id = BillID::new("11177532", "001", BillType::Tel).unwrap();
501 let payment_id = PaymentID::new(120, 7, 1, &bill_id).unwrap();
502 let bill = Bill {
503 bill_id,
504 payment_id,
505 };
506
507 assert_eq!(bill.bill_id.to_string(), "1117753200140");
508 assert_eq!(bill.payment_id.to_string(), "12070160");
509 }
510
511 #[test]
512 fn bill_bill_type_test() {
513 assert_eq!(
514 Bill::new(
515 BillID::from_str("7748317800142").unwrap(),
516 PaymentID::from_str("1770160").unwrap()
517 )
518 .unwrap()
519 .get_bill_type(),
520 BillType::Tel
521 );
522 assert_eq!(
529 Bill::new(
530 BillID::from_str("9174639504124").unwrap(),
531 PaymentID::from_str("12908190").unwrap()
532 )
533 .unwrap()
534 .get_bill_type(),
535 BillType::Electricity
536 );
537 assert_eq!(
538 Bill::new(
539 BillID::from_str("2050327604613").unwrap(),
540 PaymentID::from_str("1070189").unwrap()
541 )
542 .unwrap()
543 .get_bill_type(),
544 BillType::Water
545 );
546 assert_eq!(
551 Bill::new(
552 BillID::from_str("9100074409153").unwrap(),
553 PaymentID::from_str("12908199").unwrap()
554 )
555 .unwrap()
556 .get_bill_type(),
557 BillType::Mobile
558 );
559 }
560
561 #[test]
562 fn bill_bill_id_is_valid() {
563 assert!(BillID::from_str("7748317800142").is_ok());
564 assert!(BillID::from_str("9174639504124").is_ok());
565 assert!(BillID::from_str("2050327604613").is_ok());
566 assert!(BillID::from_str("2234322344613").is_err());
567 }
568
569 #[test]
570 fn bill_payment_id_is_valid() {
571 assert!(PaymentID::from_str("1770160").is_ok());
573 assert!(PaymentID::from_str("12908197").is_ok());
574 assert!(PaymentID::from_str("1070189").is_ok());
575 assert!(PaymentID::from_str("1070189").is_ok());
576
577 assert!(PaymentID::from_str("1770150").is_err());
578 assert!(PaymentID::from_str("12908117").is_err());
579 assert!(PaymentID::from_str("1070179").is_err());
580 assert!(PaymentID::from_str("1070169").is_err());
581
582 assert!(Bill::new(
584 BillID::from_str("7748317800142").unwrap(),
585 PaymentID::from_str("1770160").unwrap()
586 )
587 .is_ok());
588 assert!(Bill::new(
589 BillID::from_str("9174639504124").unwrap(),
590 PaymentID::from_str("12908197").unwrap()
591 )
592 .is_err());
593 assert!(Bill::new(
594 BillID::from_str("2050327604613").unwrap(),
595 PaymentID::from_str("1070189").unwrap()
596 )
597 .is_ok());
598 assert!(Bill::new(
603 BillID::from_str("2234322344617").unwrap(),
604 PaymentID::from_str("1070189").unwrap()
605 )
606 .is_err());
607 }
608
609 #[test]
610 fn bill_generate_barcode_test() {
611 assert_eq!(
617 Bill::new(
618 BillID::from_str("7748317800142").unwrap(),
619 PaymentID::from_str("1770160").unwrap()
620 )
621 .unwrap()
622 .to_string(),
623 "77483178001420000001770160"
624 );
625 assert_eq!(
627 Bill::new(
628 BillID::from_str("9174639504124").unwrap(),
629 PaymentID::from_str("12908190").unwrap()
630 )
631 .unwrap()
632 .to_string(),
633 "91746395041240000012908190"
634 );
635 assert_eq!(
636 Bill::new(
637 BillID::from_str("2050327604613").unwrap(),
638 PaymentID::from_str("1070189").unwrap()
639 )
640 .unwrap()
641 .to_string(),
642 "20503276046130000001070189"
643 );
644 assert_eq!(
647 Bill::new(
648 BillID::from_str("2234322344617").unwrap(),
649 PaymentID::from_str("1070188").unwrap()
650 )
651 .unwrap()
652 .to_string(),
653 "22343223446170000001070188"
654 );
655 }
656
657 #[test]
658 fn bill_load_from_barcode_parts_test() {
659 let bill = Bill::from_str("22343223446170000001070188").unwrap();
662
663 assert_eq!(bill.get_bill_id(), "2234322344617");
664 assert_eq!(bill.get_payment_id(), "1070188");
665 }
666
667 #[test]
668 fn bill_barcode_regeneration_test() {
669 let barcode = "33009590043100000385620969";
670 let bill = Bill::from_str(barcode).unwrap();
671 assert_eq!(bill.to_string(), barcode);
672 }
673}