From 7da266ee4f3372be138abe8c45b51109bbab5b4f Mon Sep 17 00:00:00 2001 From: Dominik Date: Fri, 3 May 2024 13:50:28 +0200 Subject: [PATCH] feat: use a custom builder pattern --- README.md | 60 +++---- src/address.rs | 58 +++++-- src/biller.rs | 42 ++++- src/contact.rs | 42 ++++- src/details.rs | 200 ++++++++++++++---------- src/identification.rs | 8 +- src/invoice.rs | 79 ++++++---- src/invoice_recipient.rs | 47 +++++- src/lib.rs | 157 ++++++++----------- src/order_reference.rs | 25 ++- src/payment_method.rs | 278 ++++++++++++++++++++++++--------- src/reduction_and_surcharge.rs | 155 ++++++++++-------- src/tax.rs | 23 ++- 13 files changed, 756 insertions(+), 418 deletions(-) diff --git a/README.md b/README.md index 98ae1d7..896f98d 100644 --- a/README.md +++ b/README.md @@ -20,44 +20,30 @@ ## Example ```rust -Invoice { - generating_system: "test", - invoice_currency: "EUR", - document_title: "An invoice", - language: "de", - invoice_number: "993433000298", - invoice_date: "2020-01-01", - biller: Biller { - vat_identification_number: "ATU51507409", - ..Default::default() - }, - invoice_recipient: InvoiceRecipient { - vat_identification_number: "ATU18708634", - ..Default::default() - }, - details: Details { - items: vec![ - DetailsItem { - description: vec!["Schraubenzieher"], - quantity: dec!(100), - unit: "STK", - unit_price: dec!(10.20), - tax_item: TaxItem { - tax_percent: dec!(20), - tax_category: TaxCategory::S, - }, - ..Default::default() - }, - ], - }, - ..Default::default() -} +Invoice::new( + "test", + "EUR", + "993433000298", + "2020-01-01", + Biller::new("ATU51507409"), + InvoiceRecipient::new("ATU18708634"), +) +.with_item( + DetailsItem::new( + dec!(100), + "STK", + dec!(10.20), + TaxItem::new(dec!(20), TaxCategory::S), + ) + .with_description("Schraubenzieher") +) +.with_document_title("An invoice") +.with_language("de") .with_payment_method( - PaymentMethodPaymentCard { - primary_account_number: "123456*4321", - card_holder_name: Some("Name"), - }, - Some("Comment"), + PaymentMethod::payment_card( + PaymentMethodPaymentCard::new("123456*4321").with_card_holder_name("Name"), + ) + .with_comment("Comment"), ) .to_xml_string() .unwrap(); // returns "..." diff --git a/src/address.rs b/src/address.rs index cb60c28..332af5e 100644 --- a/src/address.rs +++ b/src/address.rs @@ -2,17 +2,57 @@ use crate::xml::XmlElement; #[derive(Default)] pub struct Address<'a> { - pub name: &'a str, - pub street: Option<&'a str>, - pub town: &'a str, - pub zip: &'a str, - pub country: &'a str, - pub country_code: Option<&'a str>, - pub phone: Option>, - pub email: Option>, + name: &'a str, + street: Option<&'a str>, + town: &'a str, + zip: &'a str, + country: &'a str, + country_code: Option<&'a str>, + phone: Option>, + email: Option>, } -impl Address<'_> { +impl<'a> Address<'a> { + pub fn new(name: &'a str, town: &'a str, zip: &'a str, country: &'a str) -> Address<'a> { + Address { + name, + town, + zip, + country, + ..Default::default() + } + } + + pub fn with_street(mut self, street: &'a str) -> Self { + self.street = Some(street); + self + } + + pub fn with_country_code(mut self, country_code: &'a str) -> Self { + self.country_code = Some(country_code); + self + } + + pub fn with_phone(mut self, phone_number: &'a str) -> Self { + let mut phone_numbers = match self.phone { + Some(p) => p, + None => vec![], + }; + phone_numbers.push(phone_number); + self.phone = Some(phone_numbers); + self + } + + pub fn with_email(mut self, email_address: &'a str) -> Self { + let mut email_addresses = match self.email { + Some(e) => e, + None => vec![], + }; + email_addresses.push(email_address); + self.email = Some(email_addresses); + self + } + pub fn as_xml(&self) -> XmlElement { let mut e = XmlElement::new("Address").with_text_element("Name", self.name); diff --git a/src/biller.rs b/src/biller.rs index 15d294c..56c8f0c 100644 --- a/src/biller.rs +++ b/src/biller.rs @@ -5,14 +5,44 @@ use crate::{ #[derive(Default)] pub struct Biller<'a> { - pub vat_identification_number: &'a str, - pub further_identification: Option>>, - pub order_reference: Option>, - pub address: Option>, - pub contact: Option>, + vat_identification_number: &'a str, + further_identification: Option>>, + order_reference: Option>, + address: Option>, + contact: Option>, } -impl Biller<'_> { +impl<'a> Biller<'a> { + pub fn new(vat_identification_number: &str) -> Biller { + Biller { + vat_identification_number, + ..Default::default() + } + } + + pub fn with_further_identification( + mut self, + further_identification: FurtherIdentification<'a>, + ) -> Self { + let mut fi = match self.further_identification { + Some(fi) => fi, + None => vec![], + }; + fi.push(further_identification); + self.further_identification = Some(fi); + self + } + + pub fn with_address(mut self, address: Address<'a>) -> Self { + self.address = Some(address); + self + } + + pub fn with_contact(mut self, contact: Contact<'a>) -> Self { + self.contact = Some(contact); + self + } + pub fn as_xml(&self) -> XmlElement { let mut e = XmlElement::new("Biller") .with_text_element("VATIdentificationNumber", self.vat_identification_number); diff --git a/src/contact.rs b/src/contact.rs index 07d962c..d2677c5 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -2,13 +2,45 @@ use crate::xml::XmlElement; #[derive(Default)] pub struct Contact<'a> { - pub salutation: Option<&'a str>, - pub name: &'a str, - pub phone: Option>, - pub email: Option>, + salutation: Option<&'a str>, + name: &'a str, + phone: Option>, + email: Option>, } -impl Contact<'_> { +impl<'a> Contact<'a> { + pub fn new(name: &str) -> Contact { + Contact { + name, + ..Default::default() + } + } + + pub fn with_salutation(mut self, salutation: &'a str) -> Self { + self.salutation = Some(salutation); + self + } + + pub fn with_phone(mut self, phone_number: &'a str) -> Self { + let mut phone_numbers = match self.phone { + Some(p) => p, + None => vec![], + }; + phone_numbers.push(phone_number); + self.phone = Some(phone_numbers); + self + } + + pub fn with_email(mut self, email_address: &'a str) -> Self { + let mut email_addresses = match self.email { + Some(e) => e, + None => vec![], + }; + email_addresses.push(email_address); + self.email = Some(email_addresses); + self + } + pub fn as_xml(&self) -> XmlElement { let mut e = XmlElement::new("Contact"); diff --git a/src/details.rs b/src/details.rs index e5dc75e..4d5730f 100644 --- a/src/details.rs +++ b/src/details.rs @@ -1,23 +1,81 @@ use rust_decimal::Decimal; use crate::{ - decimal::CloneAndRescale, reduction_and_surcharge::ReductionAndSurchargeListLineItemDetails, - tax::TaxItem, xml::XmlElement, + decimal::CloneAndRescale, + reduction_and_surcharge::{ + ReductionAndSurchargeListLineItemDetails, ReductionListLineItem, SurchargeListLineItem, + }, + tax::{TaxCategory, TaxItem}, + xml::XmlElement, }; #[derive(Default)] pub struct DetailsItem<'a> { - pub position_number: Option, - pub description: Vec<&'a str>, - pub quantity: Decimal, - pub unit: &'a str, - pub unit_price: Decimal, - pub base_quantity: Option, - pub reduction_and_surcharge: Option>, - pub tax_item: TaxItem, + position_number: Option, + description: Vec<&'a str>, + quantity: Decimal, + unit: &'a str, + unit_price: Decimal, + base_quantity: Option, + reduction_and_surcharge: Option>, + tax_item: TaxItem, } -impl DetailsItem<'_> { +impl<'a> DetailsItem<'a> { + pub fn new( + quantity: Decimal, + unit: &'a str, + unit_price: Decimal, + tax_item: TaxItem, + ) -> DetailsItem { + DetailsItem { + quantity, + unit, + unit_price, + tax_item, + ..Default::default() + } + } + + pub fn with_position_number(mut self, position_number: u64) -> Self { + self.position_number = Some(position_number); + self + } + + pub fn with_description(mut self, description: &'a str) -> Self { + self.description.push(description); + self + } + + pub fn with_base_quantity(mut self, base_quantity: Decimal) -> Self { + self.base_quantity = Some(base_quantity); + self + } + + pub fn with_reduction(mut self, reduction: ReductionListLineItem<'a>) -> Self { + let mut ras = match self.reduction_and_surcharge { + Some(ras) => ras, + None => ReductionAndSurchargeListLineItemDetails::new(), + }; + ras = ras.with_reduction(reduction); + self.reduction_and_surcharge = Some(ras); + self + } + + pub fn with_surcharge(mut self, surcharge: SurchargeListLineItem<'a>) -> Self { + let mut ras = match self.reduction_and_surcharge { + Some(ras) => ras, + None => ReductionAndSurchargeListLineItemDetails::new(), + }; + ras = ras.with_surcharge(surcharge); + self.reduction_and_surcharge = Some(ras); + self + } + + pub fn tax_item_tuple(&self) -> (Decimal, TaxCategory) { + self.tax_item.tax_item_tuple() + } + pub fn line_item_amount(&self) -> Decimal { let base_quantity = match self.base_quantity { Some(bq) => bq, @@ -35,7 +93,7 @@ impl DetailsItem<'_> { pub fn line_item_total_gross_amount(&self) -> Decimal { self.line_item_amount() - * ((self.tax_item.tax_percent + Decimal::ONE_HUNDRED) / Decimal::ONE_HUNDRED) + * ((self.tax_item.percent() + Decimal::ONE_HUNDRED) / Decimal::ONE_HUNDRED) } pub fn as_xml(&self) -> XmlElement { @@ -112,9 +170,7 @@ mod tests { use rust_decimal_macros::dec; use crate::{ - reduction_and_surcharge::{ - ReductionAndSurchargeValue, ReductionListLineItem, SurchargeListLineItem, - }, + reduction_and_surcharge::{ReductionAndSurchargeValue, SurchargeListLineItem}, tax::TaxCategory, xml::XmlToString, }; @@ -124,17 +180,13 @@ mod tests { let quantity = dec!(0.005); let unit_price = dec!(0.005); - let result = DetailsItem { - description: vec!["Sand"], - quantity: quantity, - unit: "KGM", - unit_price: unit_price, - tax_item: TaxItem { - tax_percent: dec!(20), - tax_category: TaxCategory::S, - }, - ..Default::default() - } + let result = DetailsItem::new( + quantity, + "KGM", + unit_price, + TaxItem::new(dec!(20), TaxCategory::S), + ) + .with_description("Sand") .as_xml() .to_string(); @@ -149,17 +201,13 @@ mod tests { let quantity = dec!(100.123456); let unit_price = dec!(10.20005); - let result = DetailsItem { - description: vec!["Sand"], - quantity: quantity, - unit: "KGM", - unit_price: unit_price, - tax_item: TaxItem { - tax_percent: dec!(20), - tax_category: TaxCategory::S, - }, - ..Default::default() - } + let result = DetailsItem::new( + quantity, + "KGM", + unit_price, + TaxItem::new(dec!(20), TaxCategory::S), + ) + .with_description("Sand") .as_xml() .to_string(); @@ -171,25 +219,17 @@ mod tests { #[test] fn calculates_reduction_correctly() { - let result = DetailsItem { - description: vec!["Handbuch zur Schraube"], - quantity: dec!(1), - unit: "STK", - unit_price: dec!(5.00), - reduction_and_surcharge: Some(ReductionAndSurchargeListLineItemDetails { - reduction_list_line_items: Some(vec![ReductionListLineItem::new( - dec!(5), - ReductionAndSurchargeValue::Amount(dec!(2)), - None, - )]), - ..Default::default() - }), - tax_item: TaxItem { - tax_percent: dec!(10), - tax_category: TaxCategory::AA, - }, - ..Default::default() - } + let result = DetailsItem::new( + dec!(1), + "STK", + dec!(5.00), + TaxItem::new(dec!(10), TaxCategory::AA), + ) + .with_description("Handbuch zur Schraube") + .with_reduction(ReductionListLineItem::new( + dec!(5), + ReductionAndSurchargeValue::Amount(dec!(2)), + )) .as_xml() .to_string(); @@ -201,25 +241,17 @@ mod tests { #[test] fn calculates_surcharge_correctly() { - let result = DetailsItem { - description: vec!["Handbuch zur Schraube"], - quantity: dec!(1), - unit: "STK", - unit_price: dec!(5.00), - reduction_and_surcharge: Some(ReductionAndSurchargeListLineItemDetails { - surcharge_list_line_items: Some(vec![SurchargeListLineItem::new( - dec!(5), - ReductionAndSurchargeValue::Amount(dec!(2)), - None, - )]), - ..Default::default() - }), - tax_item: TaxItem { - tax_percent: dec!(10), - tax_category: TaxCategory::AA, - }, - ..Default::default() - } + let result = DetailsItem::new( + dec!(1), + "STK", + dec!(5.00), + TaxItem::new(dec!(10), TaxCategory::AA), + ) + .with_description("Handbuch zur Schraube") + .with_surcharge(SurchargeListLineItem::new( + dec!(5), + ReductionAndSurchargeValue::Amount(dec!(2)), + )) .as_xml() .to_string(); @@ -234,17 +266,13 @@ mod tests { let quantity = dec!(100.12344); let unit_price = dec!(10.20001); - let result = DetailsItem { - description: vec!["Sand"], - quantity: quantity, - unit: "KGM", - unit_price: unit_price, - tax_item: TaxItem { - tax_percent: dec!(20), - tax_category: TaxCategory::S, - }, - ..Default::default() - } + let result = DetailsItem::new( + quantity, + "KGM", + unit_price, + TaxItem::new(dec!(20), TaxCategory::S), + ) + .with_description("Sand") .as_xml() .to_string(); diff --git a/src/identification.rs b/src/identification.rs index 5e21d60..eed589e 100644 --- a/src/identification.rs +++ b/src/identification.rs @@ -40,11 +40,15 @@ impl FurtherIdentificationType { } pub struct FurtherIdentification<'a> { - pub id: &'a str, - pub id_type: FurtherIdentificationType, + id: &'a str, + id_type: FurtherIdentificationType, } impl FurtherIdentification<'_> { + pub fn new(id: &str, id_type: FurtherIdentificationType) -> FurtherIdentification { + FurtherIdentification { id, id_type } + } + pub fn as_xml(&self) -> XmlElement { XmlElement::new("FurtherIdentification") .with_attr("IdentificationType", self.id_type.as_str()) diff --git a/src/invoice.rs b/src/invoice.rs index 73a5d0b..016dbc0 100644 --- a/src/invoice.rs +++ b/src/invoice.rs @@ -4,37 +4,64 @@ use std::collections::HashMap; use crate::{ biller::Biller, decimal::CloneAndRescale, - details::Details, + details::{Details, DetailsItem}, invoice_recipient::InvoiceRecipient, - payment_method::{PaymentMethod, PaymentMethodType}, + payment_method::PaymentMethod, tax::{TaxCategory, TaxItem}, xml::{XmlElement, XmlToString}, }; #[derive(Default)] pub struct Invoice<'a> { - pub generating_system: &'a str, - pub invoice_currency: &'a str, - pub document_title: Option<&'a str>, - pub language: Option<&'a str>, - pub invoice_number: &'a str, - pub invoice_date: &'a str, - pub biller: Biller<'a>, - pub invoice_recipient: InvoiceRecipient<'a>, - pub details: Details<'a>, - pub payment_method: Option>, + generating_system: &'a str, + invoice_currency: &'a str, + document_title: Option<&'a str>, + language: Option<&'a str>, + invoice_number: &'a str, + invoice_date: &'a str, + biller: Biller<'a>, + invoice_recipient: InvoiceRecipient<'a>, + details: Details<'a>, + payment_method: Option>, } impl<'a> Invoice<'a> { - pub fn with_payment_method( - &mut self, - payment_method: impl PaymentMethodType<'a> + 'a, - comment: Option<&'a str>, - ) -> &Self { - self.payment_method = Some(PaymentMethod { - payment_method_type: Box::new(payment_method), - comment, - }); + pub fn new( + generating_system: &'a str, + invoice_currency: &'a str, + invoice_number: &'a str, + invoice_date: &'a str, + biller: Biller<'a>, + invoice_recipient: InvoiceRecipient<'a>, + ) -> Self { + Self { + generating_system, + invoice_currency, + invoice_number, + invoice_date, + biller, + invoice_recipient, + ..Default::default() + } + } + + pub fn with_document_title(mut self, document_title: &'a str) -> Self { + self.document_title = Some(document_title); + self + } + + pub fn with_language(mut self, language: &'a str) -> Self { + self.language = Some(language); + self + } + + pub fn with_item(mut self, item: DetailsItem<'a>) -> Self { + self.details.items.push(item); + self + } + + pub fn with_payment_method(&mut self, payment_method: PaymentMethod<'a>) -> &Self { + self.payment_method = Some(payment_method); self } @@ -43,7 +70,7 @@ impl<'a> Invoice<'a> { // Collect all taxes, grouped by tuples of tax_percent and tax_category. let mut tax_items: HashMap<(Decimal, TaxCategory), Decimal> = HashMap::new(); for i in &self.details.items { - let k = (i.tax_item.tax_percent, i.tax_item.tax_category); + let k = i.tax_item_tuple(); let s = match tax_items.get(&k) { Some(v) => v, None => &Decimal::ZERO, @@ -58,13 +85,7 @@ impl<'a> Invoice<'a> { let tax_item_xmls = sorted_tax_item_entries .iter() - .map(|e| { - TaxItem { - tax_percent: e.0 .0, - tax_category: e.0 .1, - } - .as_xml(&e.1) - }) + .map(|e| TaxItem::new(e.0 .0, e.0 .1).as_xml(&e.1)) .collect::>(); let mut tax = XmlElement::new("Tax"); diff --git a/src/invoice_recipient.rs b/src/invoice_recipient.rs index 9b53a31..b3fc346 100644 --- a/src/invoice_recipient.rs +++ b/src/invoice_recipient.rs @@ -5,14 +5,49 @@ use crate::{ #[derive(Default)] pub struct InvoiceRecipient<'a> { - pub vat_identification_number: &'a str, - pub further_identification: Option>>, - pub order_reference: Option>, - pub address: Option>, - pub contact: Option>, + vat_identification_number: &'a str, + further_identification: Option>>, + order_reference: Option>, + address: Option>, + contact: Option>, } -impl InvoiceRecipient<'_> { +impl<'a> InvoiceRecipient<'a> { + pub fn new(vat_identification_number: &str) -> InvoiceRecipient { + InvoiceRecipient { + vat_identification_number, + ..Default::default() + } + } + + pub fn with_further_identification( + mut self, + further_identification: FurtherIdentification<'a>, + ) -> Self { + let mut fi = match self.further_identification { + Some(fi) => fi, + None => vec![], + }; + fi.push(further_identification); + self.further_identification = Some(fi); + self + } + + pub fn with_order_reference(mut self, order_reference: OrderReference<'a>) -> Self { + self.order_reference = Some(order_reference); + self + } + + pub fn with_address(mut self, address: Address<'a>) -> Self { + self.address = Some(address); + self + } + + pub fn with_contact(mut self, contact: Contact<'a>) -> Self { + self.contact = Some(contact); + self + } + pub fn as_xml(&self) -> XmlElement { let mut e = XmlElement::new("InvoiceRecipient") .with_text_element("VATIdentificationNumber", self.vat_identification_number); diff --git a/src/lib.rs b/src/lib.rs index 7b44513..b51eb13 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,111 +21,76 @@ mod tests { use address::Address; use biller::Biller; use contact::Contact; - use details::{Details, DetailsItem}; + use details::DetailsItem; use identification::{FurtherIdentification, FurtherIdentificationType}; use invoice::Invoice; use invoice_recipient::InvoiceRecipient; use order_reference::OrderReference; - use payment_method::PaymentMethodPaymentCard; - use reduction_and_surcharge::{ - ReductionAndSurchargeListLineItemDetails, ReductionAndSurchargeValue, ReductionListLineItem, - }; + use payment_method::{PaymentMethod, PaymentMethodPaymentCard}; + use reduction_and_surcharge::{ReductionAndSurchargeValue, ReductionListLineItem}; use tax::{TaxCategory, TaxItem}; #[test] fn it_works() { - let result = Invoice { - generating_system: "test", - invoice_currency: "EUR", - document_title: Some("An invoice"), - language: Some("de"), - invoice_number: "993433000298", - invoice_date: "2020-01-01", - biller: Biller { - vat_identification_number: "ATU51507409", - further_identification: Some(vec![FurtherIdentification { - id: "0012345", - id_type: FurtherIdentificationType::DVR, - }]), - address: Some(Address { - name: "Schrauben Mustermann", - street: Some("Lassallenstraße 5"), - town: "Wien", - zip: "1020", - country: "Österreich", - country_code: Some("AT"), - phone: Some(vec!["+43 / 1 / 78 56 789"]), - email: Some(vec!["schrauben@mustermann.at"]), - }), - ..Default::default() - }, - invoice_recipient: InvoiceRecipient { - vat_identification_number: "ATU18708634", - order_reference: Some(OrderReference { - order_id: "test", - ..Default::default() - }), - address: Some(Address { - name: "Mustermann GmbH", - street: Some("Hauptstraße 10"), - town: "Graz", - zip: "8010", - country: "Österreich", - country_code: Some("AT"), - ..Default::default() - }), - contact: Some(Contact { - name: "Max Mustermann", - email: Some(vec!["schrauben@mustermann.at"]), - ..Default::default() - }), - ..Default::default() - }, - details: Details { - items: vec![ - DetailsItem { - position_number: Some(1), - description: vec!["Schraubenzieher"], - quantity: dec!(100), - unit: "STK", - unit_price: dec!(10.20), - base_quantity: Some(dec!(1)), - tax_item: TaxItem { - tax_percent: dec!(20), - tax_category: TaxCategory::S, - }, - ..Default::default() - }, - DetailsItem { - position_number: Some(2), - description: vec!["Handbuch zur Schraube"], - quantity: dec!(1), - unit: "STK", - unit_price: dec!(5.00), - base_quantity: Some(dec!(1)), - reduction_and_surcharge: Some(ReductionAndSurchargeListLineItemDetails { - reduction_list_line_items: Some(vec![ReductionListLineItem::new( - dec!(5), - ReductionAndSurchargeValue::Amount(dec!(2)), - Some("reduction"), - )]), - ..Default::default() - }), - tax_item: TaxItem { - tax_percent: dec!(10), - tax_category: TaxCategory::AA, - }, - }, - ], - }, - ..Default::default() - } + let result = Invoice::new( + "test", + "EUR", + "993433000298", + "2020-01-01", + Biller::new("ATU51507409") + .with_further_identification(FurtherIdentification::new( + "0012345", + FurtherIdentificationType::DVR, + )) + .with_address( + Address::new("Schrauben Mustermann", "Wien", "1020", "Österreich") + .with_street("Lassallenstraße 5") + .with_country_code("AT") + .with_phone("+43 / 1 / 78 56 789") + .with_email("schrauben@mustermann.at"), + ), + InvoiceRecipient::new("ATU18708634") + .with_order_reference(OrderReference::new("test")) + .with_address( + Address::new("Mustermann GmbH", "Graz", "8010", "Österreich") + .with_street("Hauptstraße 10") + .with_country_code("AT"), + ) + .with_contact(Contact::new("Max Mustermann").with_email("schrauben@mustermann.at")), + ) + .with_item( + DetailsItem::new( + dec!(100), + "STK", + dec!(10.20), + TaxItem::new(dec!(20), TaxCategory::S), + ) + .with_position_number(1) + .with_description("Schraubenzieher") + .with_base_quantity(dec!(1)), + ) + .with_item( + DetailsItem::new( + dec!(1), + "STK", + dec!(5.00), + TaxItem::new(dec!(10), TaxCategory::AA), + ) + .with_position_number(2) + .with_description("Handbuch zur Schraube") + .with_base_quantity(dec!(1)) + .with_reduction( + ReductionListLineItem::new(dec!(5), ReductionAndSurchargeValue::Amount(dec!(2))) + .with_comment("reduction"), + ), + ) + .with_document_title("An invoice") + .with_language("de") .with_payment_method( - PaymentMethodPaymentCard { - primary_account_number: "123456*4321", - card_holder_name: Some("Name"), - }, - Some("Comment"), + PaymentMethod::payment_card( + PaymentMethodPaymentCard::new("123456*4321").with_card_holder_name("Name"), + ) + .with_comment("Comment"), ) .to_xml_string() .unwrap(); diff --git a/src/order_reference.rs b/src/order_reference.rs index 22e56e1..8bc17f4 100644 --- a/src/order_reference.rs +++ b/src/order_reference.rs @@ -2,12 +2,29 @@ use crate::xml::XmlElement; #[derive(Default)] pub struct OrderReference<'a> { - pub order_id: &'a str, - pub reference_date: Option<&'a str>, - pub description: Option<&'a str>, + order_id: &'a str, + reference_date: Option<&'a str>, + description: Option<&'a str>, } -impl OrderReference<'_> { +impl<'a> OrderReference<'a> { + pub fn new(order_id: &str) -> OrderReference { + OrderReference { + order_id, + ..Default::default() + } + } + + pub fn with_reference_date(mut self, reference_date: &'a str) -> Self { + self.reference_date = Some(reference_date); + self + } + + pub fn with_description(mut self, description: &'a str) -> Self { + self.description = Some(description); + self + } + pub fn as_xml(&self) -> XmlElement { let mut e = XmlElement::new("OrderReference").with_text_element("OrderID", self.order_id); diff --git a/src/payment_method.rs b/src/payment_method.rs index d7b5ee9..dd17964 100644 --- a/src/payment_method.rs +++ b/src/payment_method.rs @@ -2,30 +2,85 @@ use regex::Regex; use crate::xml::XmlElement; -pub trait PaymentMethodType<'a> { - fn as_xml(&self) -> Result, String>; +#[derive(Default)] +enum PaymentMethodType<'a> { + #[default] + NoPayment, + SEPADirectDebit(PaymentMethodSEPADirectDebit<'a>), + UniversalBankTransactionBeneficiaryAccount( + PaymentMethodUniversalBankTransactionBeneficiaryAccount<'a>, + ), + UniversalBankTransaction(PaymentMethodUniversalBankTransaction<'a>), + PaymentCard(PaymentMethodPaymentCard<'a>), + OtherPayment, } -pub struct PaymentMethodNoPayment {} - -impl<'a> PaymentMethodType<'a> for PaymentMethodNoPayment { +impl<'a> PaymentMethodType<'a> { fn as_xml(&self) -> Result, String> { - Ok(XmlElement::new("NoPayment")) + match self { + PaymentMethodType::NoPayment => Ok(XmlElement::new("NoPayment")), + PaymentMethodType::SEPADirectDebit(p) => p.as_xml(), + PaymentMethodType::UniversalBankTransactionBeneficiaryAccount(p) => p.as_xml(), + PaymentMethodType::UniversalBankTransaction(p) => p.as_xml(), + PaymentMethodType::PaymentCard(p) => p.as_xml(), + PaymentMethodType::OtherPayment => Ok(XmlElement::new("OtherPayment")), + } } } #[derive(Default)] pub struct PaymentMethodSEPADirectDebit<'a> { - pub direct_debit_type: Option<&'a str>, - pub bic: Option<&'a str>, - pub iban: Option<&'a str>, - pub bank_account_owner: Option<&'a str>, - pub creditor_id: Option<&'a str>, - pub mandate_reference: Option<&'a str>, - pub debit_collection_date: Option<&'a str>, + direct_debit_type: Option<&'a str>, + bic: Option<&'a str>, + iban: Option<&'a str>, + bank_account_owner: Option<&'a str>, + creditor_id: Option<&'a str>, + mandate_reference: Option<&'a str>, + debit_collection_date: Option<&'a str>, } -impl<'a> PaymentMethodType<'a> for PaymentMethodSEPADirectDebit<'a> { +impl<'a> PaymentMethodSEPADirectDebit<'a> { + pub fn new() -> PaymentMethodSEPADirectDebit<'a> { + PaymentMethodSEPADirectDebit { + ..Default::default() + } + } + + pub fn with_direct_debit_type(mut self, direct_debit_type: &'a str) -> Self { + self.direct_debit_type = Some(direct_debit_type); + self + } + + pub fn with_bic(mut self, bic: &'a str) -> Self { + self.bic = Some(bic); + self + } + + pub fn with_iban(mut self, iban: &'a str) -> Self { + self.iban = Some(iban); + self + } + + pub fn with_bank_account_owner(mut self, bank_account_owner: &'a str) -> Self { + self.bank_account_owner = Some(bank_account_owner); + self + } + + pub fn with_creditor_id(mut self, creditor_id: &'a str) -> Self { + self.creditor_id = Some(creditor_id); + self + } + + pub fn with_mandate_reference(mut self, mandate_reference: &'a str) -> Self { + self.mandate_reference = Some(mandate_reference); + self + } + + pub fn with_debit_collection_date(mut self, debit_collection_date: &'a str) -> Self { + self.debit_collection_date = Some(debit_collection_date); + self + } + fn as_xml(&self) -> Result, String> { let mut e = XmlElement::new("SEPADirectDebit"); @@ -101,21 +156,72 @@ impl<'a> PaymentMethodType<'a> for PaymentMethodSEPADirectDebit<'a> { /// - bank_code_type: ISO 3166-1 Code pub struct PaymentMethodUniversalBankTransactionBeneficiaryAccountBankCode<'a> { - pub bank_code: i64, - pub bank_code_type: &'a str, + bank_code: i64, + bank_code_type: &'a str, +} + +impl<'a> PaymentMethodUniversalBankTransactionBeneficiaryAccountBankCode<'a> { + pub fn new( + bank_code: i64, + bank_code_type: &'a str, + ) -> PaymentMethodUniversalBankTransactionBeneficiaryAccountBankCode<'a> { + PaymentMethodUniversalBankTransactionBeneficiaryAccountBankCode { + bank_code, + bank_code_type, + } + } } #[derive(Default)] pub struct PaymentMethodUniversalBankTransactionBeneficiaryAccount<'a> { - pub bank_name: Option<&'a str>, - pub bank_code: Option>, - pub bic: Option<&'a str>, - pub bank_account_number: Option<&'a str>, - pub iban: Option<&'a str>, - pub bank_account_owner: Option<&'a str>, + bank_name: Option<&'a str>, + bank_code: Option>, + bic: Option<&'a str>, + bank_account_number: Option<&'a str>, + iban: Option<&'a str>, + bank_account_owner: Option<&'a str>, } -impl<'a> PaymentMethodType<'a> for PaymentMethodUniversalBankTransactionBeneficiaryAccount<'a> { +impl<'a> PaymentMethodUniversalBankTransactionBeneficiaryAccount<'a> { + pub fn new() -> PaymentMethodUniversalBankTransactionBeneficiaryAccount<'a> { + PaymentMethodUniversalBankTransactionBeneficiaryAccount { + ..Default::default() + } + } + + pub fn with_bank_name(mut self, bank_name: &'a str) -> Self { + self.bank_name = Some(bank_name); + self + } + + pub fn with_bank_code( + mut self, + bank_code: PaymentMethodUniversalBankTransactionBeneficiaryAccountBankCode<'a>, + ) -> Self { + self.bank_code = Some(bank_code); + self + } + + pub fn with_bic(mut self, bic: &'a str) -> Self { + self.bic = Some(bic); + self + } + + pub fn with_bank_account_number(mut self, bank_account_number: &'a str) -> Self { + self.bank_account_number = Some(bank_account_number); + self + } + + pub fn with_iban(mut self, iban: &'a str) -> Self { + self.iban = Some(iban); + self + } + + pub fn with_bank_account_owner(mut self, bank_account_owner: &'a str) -> Self { + self.bank_account_owner = Some(bank_account_owner); + self + } + fn as_xml(&self) -> Result, String> { let mut e = XmlElement::new("BeneficiaryAccount"); @@ -191,7 +297,7 @@ pub struct PaymentMethodUniversalBankTransaction<'a> { pub payment_reference_checksum: Option<&'a str>, } -impl<'a> PaymentMethodType<'a> for PaymentMethodUniversalBankTransaction<'a> { +impl<'a> PaymentMethodUniversalBankTransaction<'a> { fn as_xml(&self) -> Result, String> { let mut e = XmlElement::new("UniversalBankTransaction"); @@ -235,11 +341,23 @@ impl<'a> PaymentMethodType<'a> for PaymentMethodUniversalBankTransaction<'a> { /// - primary_account_number: Only provide at most the first 6 and last 4 digits, separated with a "*". #[derive(Default)] pub struct PaymentMethodPaymentCard<'a> { - pub primary_account_number: &'a str, - pub card_holder_name: Option<&'a str>, + primary_account_number: &'a str, + card_holder_name: Option<&'a str>, } -impl<'a> PaymentMethodType<'a> for PaymentMethodPaymentCard<'a> { +impl<'a> PaymentMethodPaymentCard<'a> { + pub fn new(primary_account_number: &'a str) -> PaymentMethodPaymentCard { + PaymentMethodPaymentCard { + primary_account_number, + ..Default::default() + } + } + + pub fn with_card_holder_name(mut self, card_holder_name: &'a str) -> Self { + self.card_holder_name = Some(card_holder_name); + self + } + fn as_xml(&self) -> Result, String> { let mut e = XmlElement::new("PaymentCard"); @@ -261,32 +379,65 @@ impl<'a> PaymentMethodType<'a> for PaymentMethodPaymentCard<'a> { } } -pub struct PaymentMethodOtherPayment {} - -impl<'a> PaymentMethodType<'a> for PaymentMethodOtherPayment { - fn as_xml(&self) -> Result, String> { - Ok(XmlElement::new("OtherPayment")) - } -} - #[derive(Default)] pub struct PaymentMethod<'a> { - pub comment: Option<&'a str>, - pub payment_method_type: Box + 'a>, + comment: Option<&'a str>, + method: PaymentMethodType<'a>, } -impl<'a> Default for Box> { - fn default() -> Box<(dyn PaymentMethodType<'a>)> { - Box::new(PaymentMethodNoPayment {}) +impl<'a> PaymentMethod<'a> { + pub fn no_payment() -> PaymentMethod<'a> { + PaymentMethod { + method: PaymentMethodType::NoPayment, + ..Default::default() + } } -} -impl<'a> PaymentMethod<'a> { - pub fn with_method_type( - mut self, - payment_method_type: impl PaymentMethodType<'a> + 'a, - ) -> Self { - self.payment_method_type = Box::new(payment_method_type); + pub fn sepa_direct_debit( + sepa_direct_debit: PaymentMethodSEPADirectDebit<'a>, + ) -> PaymentMethod<'a> { + PaymentMethod { + method: PaymentMethodType::SEPADirectDebit(sepa_direct_debit), + ..Default::default() + } + } + + pub fn universal_bank_transaction( + universal_bank_transaction: PaymentMethodUniversalBankTransaction<'a>, + ) -> PaymentMethod<'a> { + PaymentMethod { + method: PaymentMethodType::UniversalBankTransaction(universal_bank_transaction), + ..Default::default() + } + } + + pub fn universal_bank_transaction_beneficiary_account( + universal_bank_transaction_beneficiary_account: PaymentMethodUniversalBankTransactionBeneficiaryAccount<'a>, + ) -> PaymentMethod<'a> { + PaymentMethod { + method: PaymentMethodType::UniversalBankTransactionBeneficiaryAccount( + universal_bank_transaction_beneficiary_account, + ), + ..Default::default() + } + } + + pub fn payment_card(payment_card: PaymentMethodPaymentCard<'a>) -> PaymentMethod<'a> { + PaymentMethod { + method: PaymentMethodType::PaymentCard(payment_card), + ..Default::default() + } + } + + pub fn other_payment() -> PaymentMethod<'a> { + PaymentMethod { + method: PaymentMethodType::OtherPayment, + ..Default::default() + } + } + + pub fn with_comment(mut self, comment: &'a str) -> Self { + self.comment = Some(comment); self } @@ -297,7 +448,7 @@ impl<'a> PaymentMethod<'a> { e = e.with_text_element("Comment", comment); } - match self.payment_method_type.as_xml() { + match self.method.as_xml() { Ok(pmt) => { e = e.with_element(pmt); } @@ -330,13 +481,7 @@ mod tests { #[test] fn no_payment() { assert_eq!( - PaymentMethod { - ..Default::default() - } - .with_method_type(PaymentMethodNoPayment {}) - .as_xml() - .unwrap() - .to_string(), + PaymentMethod::no_payment().as_xml().unwrap().to_string(), "" ) } @@ -344,10 +489,7 @@ mod tests { #[test] fn sepa_direct_debit() { assert_eq!( - PaymentMethod { - ..Default::default() - } - .with_method_type(PaymentMethodSEPADirectDebit { + PaymentMethod::sepa_direct_debit(PaymentMethodSEPADirectDebit { direct_debit_type: Some("B2B"), bic: Some("BKAUATWW"), iban: Some("AT491200011111111111"), @@ -367,10 +509,7 @@ mod tests { #[test] fn universal_bank_transaction() { assert_eq!( - PaymentMethod { - ..Default::default() - } - .with_method_type(PaymentMethodUniversalBankTransaction { + PaymentMethod::universal_bank_transaction(PaymentMethodUniversalBankTransaction { consolidator_payable: Some(true), beneficiary_account: Some(vec![PaymentMethodUniversalBankTransactionBeneficiaryAccount { bank_name: Some("Bank"), @@ -396,10 +535,7 @@ mod tests { #[test] fn payment_card() { assert_eq!( - PaymentMethod { - ..Default::default() - } - .with_method_type(PaymentMethodPaymentCard { + PaymentMethod::payment_card(PaymentMethodPaymentCard { primary_account_number: "123456*4321", card_holder_name: Some("Name"), }) @@ -413,13 +549,7 @@ mod tests { #[test] fn other_payment() { assert_eq!( - PaymentMethod { - ..Default::default() - } - .with_method_type(PaymentMethodOtherPayment {}) - .as_xml() - .unwrap() - .to_string(), + PaymentMethod::other_payment().as_xml().unwrap().to_string(), "" ) } diff --git a/src/reduction_and_surcharge.rs b/src/reduction_and_surcharge.rs index 90485ac..1b6cea3 100644 --- a/src/reduction_and_surcharge.rs +++ b/src/reduction_and_surcharge.rs @@ -14,7 +14,18 @@ struct ReductionAndSurchargeListLineItemBase<'a> { comment: Option<&'a str>, } -impl ReductionAndSurchargeListLineItemBase<'_> { +impl<'a> ReductionAndSurchargeListLineItemBase<'a> { + pub fn new( + base_amount: Decimal, + value: ReductionAndSurchargeValue, + ) -> ReductionAndSurchargeListLineItemBase<'a> { + ReductionAndSurchargeListLineItemBase { + base_amount, + value, + comment: None, + } + } + fn sum(&self) -> Decimal { match self.value { ReductionAndSurchargeValue::Percentage(percentage) => { @@ -65,20 +76,17 @@ pub struct ReductionListLineItem<'a> { } impl<'a> ReductionListLineItem<'a> { - pub fn new( - base_amount: Decimal, - value: ReductionAndSurchargeValue, - comment: Option<&'a str>, - ) -> Self { + pub fn new(base_amount: Decimal, value: ReductionAndSurchargeValue) -> Self { ReductionListLineItem { - base: ReductionAndSurchargeListLineItemBase { - base_amount, - value, - comment, - }, + base: ReductionAndSurchargeListLineItemBase::new(base_amount, value), } } + pub fn with_comment(mut self, comment: &'a str) -> Self { + self.base.comment = Some(comment); + self + } + fn sum(&self) -> Decimal { self.base.sum() } @@ -99,20 +107,17 @@ pub struct SurchargeListLineItem<'a> { } impl<'a> SurchargeListLineItem<'a> { - pub fn new( - base_amount: Decimal, - value: ReductionAndSurchargeValue, - comment: Option<&'a str>, - ) -> Self { + pub fn new(base_amount: Decimal, value: ReductionAndSurchargeValue) -> Self { SurchargeListLineItem { - base: ReductionAndSurchargeListLineItemBase { - base_amount, - value, - comment, - }, + base: ReductionAndSurchargeListLineItemBase::new(base_amount, value), } } + pub fn with_comment(mut self, comment: &'a str) -> Self { + self.base.comment = Some(comment); + self + } + fn sum(&self) -> Decimal { self.base.sum() } @@ -130,11 +135,37 @@ impl<'a> SurchargeListLineItem<'a> { #[derive(Default)] pub struct ReductionAndSurchargeListLineItemDetails<'a> { - pub reduction_list_line_items: Option>>, - pub surcharge_list_line_items: Option>>, + reduction_list_line_items: Option>>, + surcharge_list_line_items: Option>>, } -impl ReductionAndSurchargeListLineItemDetails<'_> { +impl<'a> ReductionAndSurchargeListLineItemDetails<'a> { + pub fn new() -> ReductionAndSurchargeListLineItemDetails<'a> { + ReductionAndSurchargeListLineItemDetails { + ..Default::default() + } + } + + pub fn with_reduction(mut self, reduction: ReductionListLineItem<'a>) -> Self { + let mut reductions = match self.reduction_list_line_items { + Some(r) => r, + None => vec![], + }; + reductions.push(reduction); + self.reduction_list_line_items = Some(reductions); + self + } + + pub fn with_surcharge(mut self, surcharge: SurchargeListLineItem<'a>) -> Self { + let mut surcharges = match self.surcharge_list_line_items { + Some(s) => s, + None => vec![], + }; + surcharges.push(surcharge); + self.surcharge_list_line_items = Some(surcharges); + self + } + pub fn sum(&self) -> Decimal { let surcharge_sum = match &self.surcharge_list_line_items { Some(s) => s.iter().fold(Decimal::ZERO, |sum, s| sum + s.sum()), @@ -175,56 +206,56 @@ mod tests { #[test] fn generates_reduction_and_surcharge_list_line_item() { - let result = ReductionAndSurchargeListLineItemDetails { - reduction_list_line_items: Some(vec![ReductionListLineItem::new( - dec!(100), - ReductionAndSurchargeValue::Percentage(dec!(2)), - Some("reduction"), - )]), - surcharge_list_line_items: Some(vec![SurchargeListLineItem::new( - dec!(200), - ReductionAndSurchargeValue::Amount(dec!(3)), - Some("surcharge"), - )]), - } - .as_xml() - .to_string(); + let result = ReductionAndSurchargeListLineItemDetails::new() + .with_reduction( + ReductionListLineItem::new( + dec!(100), + ReductionAndSurchargeValue::Percentage(dec!(2)), + ) + .with_comment("reduction"), + ) + .with_surcharge( + SurchargeListLineItem::new(dec!(200), ReductionAndSurchargeValue::Amount(dec!(3))) + .with_comment("surcharge"), + ) + .as_xml() + .to_string(); assert_eq!( result, "100.002.00reduction200.003.00surcharge" ); - let result = ReductionAndSurchargeListLineItemDetails { - reduction_list_line_items: Some(vec![ReductionListLineItem::new( - dec!(100), - ReductionAndSurchargeValue::Amount(dec!(2)), - Some("reduction"), - )]), - surcharge_list_line_items: Some(vec![SurchargeListLineItem::new( - dec!(200), - ReductionAndSurchargeValue::Percentage(dec!(3)), - Some("surcharge"), - )]), - } - .as_xml() - .to_string(); + let result = ReductionAndSurchargeListLineItemDetails::new() + .with_reduction( + ReductionListLineItem::new(dec!(100), ReductionAndSurchargeValue::Amount(dec!(2))) + .with_comment("reduction"), + ) + .with_surcharge( + SurchargeListLineItem::new( + dec!(200), + ReductionAndSurchargeValue::Percentage(dec!(3)), + ) + .with_comment("surcharge"), + ) + .as_xml() + .to_string(); assert_eq!( result, "100.002.00reduction200.003.00surcharge" ); - let result = ReductionAndSurchargeListLineItemDetails { - reduction_list_line_items: Some(vec![ReductionListLineItem::new( - dec!(100), - ReductionAndSurchargeValue::PercentageAndAmount(dec!(2), dec!(3)), - Some("reduction"), - )]), - ..Default::default() - } - .as_xml() - .to_string(); + let result = ReductionAndSurchargeListLineItemDetails::new() + .with_reduction( + ReductionListLineItem::new( + dec!(100), + ReductionAndSurchargeValue::PercentageAndAmount(dec!(2), dec!(3)), + ) + .with_comment("reduction"), + ) + .as_xml() + .to_string(); assert_eq!( result, diff --git a/src/tax.rs b/src/tax.rs index 776cdf1..cd726ee 100644 --- a/src/tax.rs +++ b/src/tax.rs @@ -40,11 +40,30 @@ impl TaxCategory { #[derive(Default)] pub struct TaxItem { - pub tax_percent: Decimal, - pub tax_category: TaxCategory, + tax_percent: Decimal, + tax_category: TaxCategory, } impl TaxItem { + pub fn new(tax_percent: Decimal, tax_category: TaxCategory) -> TaxItem { + TaxItem { + tax_percent, + tax_category, + } + } + + pub fn percent(&self) -> Decimal { + self.tax_percent + } + + pub fn category(&self) -> TaxCategory { + self.tax_category + } + + pub fn tax_item_tuple(&self) -> (Decimal, TaxCategory) { + (self.tax_percent, self.tax_category) + } + pub fn as_xml<'a>(&self, taxable_amount: &Decimal) -> XmlElement<'a> { let tax_amount = taxable_amount * (self.tax_percent / Decimal::ONE_HUNDRED);