rust_persian_tools/bill/
mod.rs

1//! Iranian Utility Bill tools (`bill` Cargo feature).
2//!
3//! The structure of Bill/Payment IDs and Barcode is based on [مستندات طرح هماهنگ پرداخت الکترونیکی قبوض - کمیسیون انفورماتیک بانک‌ها](https://core.pod.ir/nzh/file/?fileId=334917&hashCode=16e5a9ee893-0.22579290613133673)
4//!
5//! #### Example
6//!
7//! ##### Load from Barcode  
8//! A barcode must be 26 characters  
9//! if checksum of Bill or Payment ID separately or checksum of them together be invalid Result will return `Err::BillError`
10//!  
11//! ```rust
12//! use rust_persian_tools::bill::{Bill, BillID, PaymentID};
13//! use rust_persian_tools::bill::BillType;
14//! use rust_persian_tools::bill::CurrencyType;
15//! use std::str::FromStr;
16//!
17//! // This checks both bill/payment IDs and their relation match (checksum2)
18//! let bill = Bill::from_str("77483178001420000001770160").unwrap();
19//! assert_eq!(bill.get_bill_type(), BillType::Tel);   // Fixed Landline
20//! assert_eq!(bill.get_bill_id(), "7748317800142");
21//! assert_eq!(bill.get_payment_id(), "1770160");
22//! assert_eq!(bill.amount(CurrencyType::Rials), 17000);
23//! ```
24//!
25//! ##### Load from Bill and Payment IDs  
26//! Each can be any string with or without leading zeros  
27//! if checksum of Bill or Payment ID separately or checksum of them together be invalid Result will return `Err::BillError`
28//! ```rust  
29//! use rust_persian_tools::bill::{Bill, BillID, PaymentID};
30//! use rust_persian_tools::bill::CurrencyType;
31//! use rust_persian_tools::bill::BillType;
32//! use std::str::FromStr;
33//!
34//! let bill_id = BillID::from_str("7748317800142").unwrap();
35//! let payment_id = PaymentID::from_str("1770160").unwrap();
36//! let bill = Bill::new(bill_id, payment_id).unwrap();
37//! assert_eq!(bill.get_bill_type(), BillType::Tel);   // Fixed Landline
38//! assert_eq!(bill.get_bill_id(), "7748317800142");
39//! assert_eq!(bill.get_payment_id(), "1770160");
40//! assert_eq!(bill.amount(CurrencyType::Rials), 17000);
41//! ```
42//!
43//! ##### Generate IDs from scratch  
44//! This will calculate checksums automatically
45//! ```rust
46//! use rust_persian_tools::bill::{Bill, BillID, PaymentID};
47//! use rust_persian_tools::bill::CurrencyType;
48//! use rust_persian_tools::bill::BillType;
49//!
50//! let bill_id = BillID::new("77483178", "001", BillType::Tel).unwrap();
51//! let payment_id = PaymentID::new(17, 7, 1, &bill_id).unwrap();  // Thousands are omitted from amount
52//! let bill = Bill::new(bill_id, payment_id).unwrap();
53//! assert_eq!(bill.get_bill_type(), BillType::Tel);   // Fixed Landline
54//! assert_eq!(bill.get_bill_id(), "7748317800142");
55//! assert_eq!(bill.get_payment_id(), "1770160");
56//! assert_eq!(bill.amount(CurrencyType::Rials), 17000);
57//! assert_eq!(bill.amount(CurrencyType::Tomans), 1700);
58//! assert_eq!(bill.to_string(), "77483178001420000001770160");
59//! ```
60
61use 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/// Values Are based on the مستندات طرح هماهنگ پرداخت الکترونیکی قبوض - کمیسیون انفورماتیک بانک‌ها \
93/// It is possible to use `BillType` directly or from integer value:  
94/// ```rust
95/// use num::FromPrimitive;
96/// use rust_persian_tools::bill::BillType;
97/// assert_eq!(FromPrimitive::from_u8(2), Some(BillType::Electricity));
98/// ```
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100#[derive(FromPrimitive, Clone, Copy, PartialEq, Eq, Debug, Hash)]
101pub enum BillType {
102    /// آب  
103    /// Service Type Code: 1
104    Water = 1,
105    /// برق  
106    /// Service Type Code: 2
107    Electricity = 2,
108    /// گاز  
109    /// Service Type Code: 3
110    Gas = 3,
111    /// تلفن ثابت  
112    /// Service Type Code: 4
113    Tel = 4,
114    /// تلفن همراه  
115    /// Service Type Code: 5
116    Mobile = 5,
117    /// شهرداری  
118    /// Service Type Code: 6
119    Municipality = 6,
120    /// سازمان مالیات  
121    /// Service Type Code: 7
122    Tax = 7,
123    /// جریمه راهنمایی و رانندگی  
124    /// Service Type Code: 8
125    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/// Bill ID Structure based on مستندات طرح هماهنگ پرداخت الکترونیکی قبوض - کمیسیون انفورماتیک بانک‌ها \
136///
137/// | File ID<br/> Maximum Length: 8 | Company Code<br/>  Length: 3 | Service Type<br/>  Length: 1 | Checksum<br/>  Length: 1 |  
138/// |:-------------------------:|-------------------------|-------------------------|----------|  
139///
140/// Checksum is calculated via [ISSN Modulo 11 check digit](https://www.activebarcode.com/codes/checkdigit/modulo11)
141///
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143#[derive(Debug, PartialEq, Eq, Clone, Hash)]
144pub struct BillID {
145    /// Maximum 8-digit Company Internal File ID
146    pub file_id: String,
147    /// 3-digit Company Code
148    pub company_code: String,
149    /// Service Type
150    pub r#type: BillType,
151    pub checksum: u8,
152}
153impl FromStr for BillID {
154    type Err = BillError;
155    /// Loads a Bill ID from string representation \
156    /// String must be less than or equal size of 13 chars and more than or equal size of 6 chars \
157    /// Returns `Err(Bill::BillError)` on failure
158    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    /// Builds Bill ID from scratch and calculates checksum automatically
194    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    /// Returns String representation of Bill ID
207    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/// Payment Structure based on مستندات طرح هماهنگ پرداخت الکترونیکی قبوض - کمیسیون انفورماتیک بانک‌ها \
216///
217/// | Amount in thousands<br/> Maximum Length: 8 | Year Code<br/>  Length: 1 | Period Code<br/>  Length: 2 | Checksum1<br/>  Length: 1 | Checksum2<br/>  Length: 1 |    
218/// |:-------------------------:|-------------------------|-------------------------|----------|----------|  
219///
220/// Checksums are calculated via [ISSN Modulo 11 check digit](https://www.activebarcode.com/codes/checkdigit/modulo11)  \
221/// Checksum1 is the checksum for Payment ID itself and only checks digits in Payment ID  \
222/// Checksum2 is the checksum for Bill ID and Payment ID concatenated together and checks validity of relation between two IDs
223#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)]
225pub struct PaymentID {
226    /// Amount in scale 1000:1 (1000 will be 1)
227    amount: u64,
228    /// Company Internal code that represents bill year (Normally last digit of the year)
229    year: u8,
230    /// Company Internal payment period (Normally sequential increasing number)
231    period: u8,
232    checksum1: u8,
233    checksum2: u8,
234}
235impl std::str::FromStr for PaymentID {
236    type Err = BillError;
237
238    /// Loads a Payment ID from string representation \
239    /// String must be less than or equal size of 13 chars and more than or equal size of 6 chars \
240    /// Returns `Err(Bill::BillError)` on failure
241    /// Note: Only checksum1 is validated here
242    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    /// Returns String representation of Bill ID
289    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    /// Builds Bill ID from scratch and calculates checksum automatically
299    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/// Container for Both Bill and Payment IDs  \
331/// You must use this type to extract all information about the bill  
332#[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    /// Loads Bill ID and Payment ID form barcode  \
342    /// Barcode format is: \[Bill ID\]\[Payment ID\]  \
343    /// Barcode Must be exactly 26 chars  \
344    /// This also checks validity of the relation between Bill and Payment IDs (checksum2)
345    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    /// Generates Barcode from Bill struct
395    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        // The Payment ID from typescript version seems to be wrong
473        // https://github.com/persian-tools/persian-tools/blob/f47fc0261aa5680cde6e03b87905a1273c91de92/test/bill.spec.ts#L14
474        // Tested on AsanPardakht and Sadad to confirm
475        // Last digit of payment id is checksum of bill and payment id together
476        // with values 1117753200140 for bill id and 177016 for payment id the checksum will be 3
477        // so payment id must be 1770163 to be valid
478        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        // The Payment ID from typescript version seems to be wrong
523        // https://github.com/persian-tools/persian-tools/blob/f47fc0261aa5680cde6e03b87905a1273c91de92/test/bill.spec.ts#L14
524        // Tested on AsanPardakht and Sadad to confirm
525        // Last digit of payment id is checksum of bill and payment id together
526        // with values 9174639504124 for bill id and 1290819 for payment id the checksum will be 0
527        // so payment id must be 12908190 to be valid
528        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        // The Bill ID from typescript version seems to be wrong
547        // https://github.com/persian-tools/persian-tools/blob/f47fc0261aa5680cde6e03b87905a1273c91de92/test/bill.spec.ts#L36
548        // Bill ID 9100074409151 is invalid! checksum must be 3 so valid Bill ID is 9100074409153
549        // So Correct Payment ID should also be changed to 12908199 in turn
550        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        // Check first checksum (payment id verification)
572        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        // Check second checksum (Full bill and payment IDs validation)
583        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        // This test is supposed to test Payment ID so the Bill ID should be valid
599        // Bill id 2234322344613 from typescript version
600        // https://github.com/persian-tools/persian-tools/blob/f47fc0261aa5680cde6e03b87905a1273c91de92/test/bill.spec.ts#L79
601        // is not valid, so it is changed to 2234322344617 to be valid so only Payment ID is checked
602        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        // Based on the https://core.pod.ir/nzh/file/?fileId=334917&hashCode=16e5a9ee893-0.22579290613133673
612        // Barcode Musts be exactly 26 chars
613        // bill and payment IDs must be left padded by zero to fill 13 chars
614        // So the values from https://github.com/persian-tools/persian-tools/blob/f47fc0261aa5680cde6e03b87905a1273c91de92/test/bill.spec.ts#L89
615        // Are not correct
616        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        // 12908197 from typescript version must be 12908190
626        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        // Bill ID 2234322344613 from typescript version is not correct
645        // Correct Bill ID is 2234322344617
646        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        // Bill ID 2234322344613 from typescript version is incorrect, correct Bill ID is 2234322344617
660        // So correct payment ID will be 1070188
661        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}