use std::num::ParseIntError;
use either::Either;
use iban::Iban;
use time::{Date, Month};
#[cfg(test)]
mod tests;
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default)]
pub enum BarcodeVersion {
V4,
#[default]
V5,
}
#[derive(Debug, Clone)]
pub struct Barcode {
version: BarcodeVersion,
account_number: iban::Iban,
euros: u32,
cents: u8,
reference: String,
due_date: Option<Date>,
}
#[derive(Debug, Default)]
pub struct BarcodeBuilder {
version: BarcodeVersion,
account_number: Option<Either<Iban, String>>,
euros: u32,
cents: u8,
reference: Option<String>,
due_date: Option<Either<Date, (i32, u8, u8)>>,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum BuilderError {
#[error("No account number specified")]
NoAccount,
#[error("Failed to parse account number {0}")]
InvalidAccount(#[from] iban::ParseError),
#[error(
"A non FI IBAN provided. Bank barcode may only be printed for IBAN accounts starting with FI"
)]
AccountNotFinnish,
#[error("The total sum is too large (over 999 999,99)")]
SumTooLarge,
#[error("The amount of cents is invalid (not between 0..=99)")]
InvalidCents,
#[error(
"The reference number is too large (the limit is 20 digits for V4 and 23 digits for V5)"
)]
ReferenceTooLarge,
#[error("Invalid reference")]
InvalidReference(#[from] ParseIntError),
#[error("Malformed reference: The reference for V5 has to start with 'RF'")]
MalformedReference,
#[error("Invalid date provided {0}")]
InvalidDate(#[from] time::error::ComponentRange),
}
impl BarcodeBuilder {
pub fn build(self) -> Result<Barcode, BuilderError> {
let account_number = match self.account_number {
Some(Either::Left(iban)) => iban,
Some(Either::Right(number)) => number.parse()?,
None => return Err(BuilderError::NoAccount),
};
if account_number.country_code() != "FI" {
return Err(BuilderError::AccountNotFinnish);
}
if self.cents >= 100 {
return Err(BuilderError::InvalidCents);
}
if self.euros >= 999_999 {
return Err(BuilderError::SumTooLarge);
}
let reference = match self.version {
BarcodeVersion::V4 => {
if let Some(rn) = self.reference.as_ref() {
let _: u128 = rn.parse()?;
}
let reference = self.reference.unwrap_or("0".into());
if reference.len() > 20 {
Err(BuilderError::ReferenceTooLarge)
} else {
Ok(reference)
}
}
BarcodeVersion::V5 => {
let reference_rf = self.reference.unwrap_or("RF00".into());
if !reference_rf.starts_with("RF") {
return Err(BuilderError::MalformedReference);
}
let _: u128 = reference_rf[2..].parse()?;
let reference = reference_rf[2..].to_string();
if reference.len() > 23 {
Err(BuilderError::ReferenceTooLarge)
} else {
Ok(reference)
}
}
}?;
let due_date = match self.due_date {
Some(Either::Left(date)) => Some(date),
Some(Either::Right((year, month, day))) => Some(Date::from_calendar_date(
year,
Month::try_from(month)?,
day,
)?),
None => None,
};
Ok(Barcode {
version: self.version,
account_number,
euros: self.euros,
cents: self.cents,
reference,
due_date,
})
}
#[must_use]
pub fn v4() -> Self {
Self::default().version(BarcodeVersion::V4)
}
#[must_use]
pub fn v5() -> Self {
Self::default().version(BarcodeVersion::V5)
}
#[must_use]
pub fn version(self, version: BarcodeVersion) -> Self {
Self { version, ..self }
}
#[must_use]
#[allow(clippy::needless_pass_by_value)] pub fn account_number(self, account: impl ToString) -> Self {
Self {
account_number: Some(Either::Right(account.to_string())),
..self
}
}
#[must_use]
pub fn account_number_iban(self, account: iban::Iban) -> Self {
Self {
account_number: Some(Either::Left(account)),
..self
}
}
#[must_use]
pub fn euros(self, euros: u32) -> Self {
Self { euros, ..self }
}
#[must_use]
pub fn cents(self, cents: u8) -> Self {
Self { cents, ..self }
}
#[must_use]
pub fn sum(self, sum: u32) -> Self {
Self {
euros: sum / 100,
cents: (sum % 100) as u8,
..self
}
}
#[must_use]
#[allow(clippy::needless_pass_by_value)] pub fn reference(self, reference: impl ToString) -> Self {
Self {
reference: Some(reference.to_string()),
..self
}
}
#[must_use]
pub fn due_date(self, due_date: Date) -> Self {
Self {
due_date: Some(Either::Left(due_date)),
..self
}
}
#[must_use]
pub fn calendar_due_date(self, year: i32, month: u8, day: u8) -> Self {
Self {
due_date: Some(Either::Right((year, month, day))),
..self
}
}
}
impl Barcode {
#[must_use]
pub fn builder() -> BarcodeBuilder {
BarcodeBuilder::default()
}
}
impl std::fmt::Display for Barcode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use time::macros::format_description;
match self.version {
BarcodeVersion::V4 => {
write!(
f,
"4{}{:0>6}{:0>2}000{:0>20}{}",
&self.account_number.as_str()[2..],
self.euros,
self.cents,
self.reference,
self.due_date.map_or("000000".into(), |d| d
.format(format_description!(
version = 2,
"[year repr:last_two][month][day]"
))
.expect("bug: formatting failed"))
)
}
BarcodeVersion::V5 => {
let ref_str = self.reference.clone();
let ref_nro = if ref_str.len() < 2 {
format!("{:0<2}", self.reference)
} else {
ref_str
};
write!(
f,
"5{}{:0>6}{:0>2}{}{:0>21}{}",
&self.account_number.as_str()[2..],
self.euros,
self.cents,
&ref_nro[..2],
&ref_nro[2..],
self.due_date.map_or("000000".into(), |d| d
.format(format_description!(
version = 2,
"[year repr:last_two][month][day]"
))
.expect("bug: formatting failed"))
)
}
}
}
}