UO34MAAGCLHTUGNRZQUNIS32ZE354N72IMGZ76THUBKR7TDT6L2QC D6LJRTWXZZTEJPCUTKTHMIKR5R3UYZF4WKN2GV3P432HNHUJCVVAC VXPFYRI2BRQGUOIT3KNNSCEEJMCJLMRI4DG647JFLVNN3WQVH5DQC EYMZ5JWP6L3HVVAWYUGS6M6IWY6LOHIZPKTHYIZVNH7LOURQ3WRAC JUYXTWXP5Y52UAPROXCHVL4VL5G2DDRWGY4OX7ZXFE6GRMEV4EYQC 4WYI5U7YHCVEXBFCOY4M3JN7IZYSBBK6AOZJMBAQ6XWI2Y26IYXAC 6A5YLGWVK7F6DQWDTSVFRIPFQ5VLIUN3P6DW64QHDDE6RUARDEBQC XQHYMSDYDG3MFDUEGXADTNXA4B7AQRSYA5JCP3S3KQYL527DORHQC RCS5VP3AIMPZ5FQ2QWOQGZET726R2O2GPPEJMHLPXQG65KG7KFFQC NQ455YWRMOM3SUX3A7W2SR4VAI3DYBXBKBMYDGNQIGUICOVC2WOAC PCHAKXNMERZROCHMO6GWWES5JSN6MK4O2UAABLPETYYSRICWXTHAC GVEI7KNDSO5KHFV5GIT2FLNIF7I3UQNH6KHUDM4QLUSWBSMDJO6AC R5K55SCBMHEGYZTMITKMNV7JFM3246SSKVB75HNJEKMLLIMJRGNQC BDLVPDJZU5BPXRS5DTAUPJ5H6JWURRGS2NKKNCA43HGQGRFIKGIAC 362NCCMXCBQRRWBRE5G6YLINRAF472PJJF3WSF43B2SULDJBWEHQC KB7Y4PJIETGK43QVN5JTCQ2UD3JCXKYDULKTNQVR7WMH2JIODS7AC TSSWMQVUBZOA2PG4N2Z4IQRKOEM7BHS2S762HTTWGJGVDKSXQI3AC I2P2FTLEKLICJKHQ3FHOLRQRQYGZCJTCTU2MWXU2TMIRIKG6YFCQC 2JBFREZGJ2PST2DE3ZVDQADXAOFXBYPMSFTG7C65GDKLOZGETTGAC UESS5YZE6ZHPMVUL2P2OACW2Y2QLFLLLLC3F3JAENVH6A7REKLBAC TB2QGHXN2PTACZQG4M53QBG74IDGSDQPAFSTRZ7UNHHCAXRWQEZQC JQJTN6N53CMWHRUVXJ4K3HDNUYIM23W7GET7XTOBVCS57RCCT6ZQC ONRIF4V72HMVLO4BWDHI7ZOWYWTLTVYNH5TXUN5HB7BQUMV22NVAC 24CCPM5ORU7ONWBW4OU6BLAW666JMQ5VXWO43VEOD6VSFT6TCX2QC 2Z4EGCWQV2GF3CZC6IBGLTOS6JN3NMNBNQMUOZTIOCRO54VB6DSQC SLTVZLYXPHYVACXUOKWGVI3MV6IJBT22Y4NYRBQDVDYULCDSJ6SAC YDK6X6PPD42DMLFGF6OO2O3G7GA4Z2PCIDJIREHX6XNX2NYEBJSQC RI7HQBYA7OZBTINMECRKAM5JCCMI3HDLD2CBNYT2OVIHKZPZIWDAC #[serde(rename = "Buchungstag", with = "dmy")]booking_date: Date,#[serde(rename = "Waehrung")]currency: Currency,#[serde(rename = "Verwendungszweck")]purpose: &'r str,#[serde(rename = "IBAN Zahlungsbeteiligter")]transaction_partner_iban: &'r str,#[serde(rename = "Name Zahlungsbeteiligter")]transaction_partner_name: &'r str,#[serde(rename = "Valutadatum", with = "dmy")]value_date: Date,}
const DEPOT_ID_COLUMN_INDEX: usize = 1;const PRICE_COLUMN_INDEX: usize = 5;const TOTAL_COLUMN_INDEX: usize = 7;const TRANSACTION_KIND_COLUMN_INDEX: usize = 3;const UNIT_COLUMN_INDEX: usize = 8;const UNITS_COLUMN_INDEX: usize = 6;let context = TemplateContext::try_from(&record[DEPOT_ID_COLUMN_INDEX])?;
let context = TemplateContext::try_from(record.depot_id)?;
let currency: Commodity = self.lookup_currency(&record[UNIT_COLUMN_INDEX])?;let units = {let value: &str = &record[UNITS_COLUMN_INDEX];german_decimal::parse(value).map_err(|_| todo!())}?;let unit_cost = {let price = &record[PRICE_COLUMN_INDEX];german_decimal::parse(price).map_err(|_| todo!())}?;
let currency: Commodity = self.lookup_currency(record.currency)?;let units = record.shares;let unit_cost = record.unit_cost;
}}#[derive(Clone, Copy, Debug, Deserialize)]pub struct Record<'r> {#[serde(rename = "DATUM")]date: Date,#[serde(rename = "UNIONDEPOTNUMMER")]depot_id: DepotId<'r>,#[serde(rename = "TRANSAKTIONSART")]transaction_kind: TransactionKind,#[serde(rename = "FONDSPREIS", with = "german_decimal::serde")]unit_cost: Decimal,#[serde(rename = "ANTEILE", with = "german_decimal::serde")]shares: Decimal,#[serde(rename = "VOLUMEN", with = "german_decimal::serde")]total: Decimal,#[serde(rename = "EINHEIT")]currency: &'r str,}#[derive(Clone, Copy, Debug, Deserialize)]#[serde(try_from = "&str")]struct DepotId<'r> {depot: &'r str,position: &'r str,}impl<'i> TryFrom<&'i str> for DepotId<'i> {type Error = Error;fn try_from(value: &'i str) -> Result<Self, Self::Error> {let (depot, position) = value.split_once('/').ok_or_else(|| todo!())?;Ok(Self { depot, position })
fn extract_record(&self, record: &Record) -> Result<(Transaction, Balance), Error> {let date = parse_date(record).unwrap();let time =Time::parse(&record[1], format_description!("[hour]:[minute]:[second]")).unwrap();let tz = time_tz::timezones::get_by_name(&record[2]).unwrap();
fn extract_record(&self, record: Record) -> Result<(Transaction, Balance), Error> {dbg!(record);
let timestamp = date.with_time(time).assume_timezone(tz).unwrap();let commodity = Commodity::try_from(&record[4]).unwrap();
let tz = time_tz::timezones::get_by_name(record.timezone).unwrap();let timestamp = record.date.with_time(record.time).assume_timezone(tz).unwrap();
let transaction_id = &record[9];if matches!(kind, TransactionKind::Deposit) {transaction.add_link(Link::try_from(format!("^paypal.{transaction_id}")).unwrap());
if matches!(record.description, TransactionKind::Deposit) {transaction.add_link(Link::try_from(format!("^paypal.{}", record.transaction_id)).unwrap(),);
}#[derive(Clone, Copy, Debug, Deserialize)]pub struct Record<'r> {#[serde(rename = "Datum", with = "dmy")]date: Date,#[serde(rename = "Uhrzeit", with = "hms")]time: Time,#[serde(rename = "Zeitzone")]timezone: &'r str,#[serde(rename = "Beschreibung")]description: TransactionKind,#[serde(rename = "Währung")]currency: Commodity,#[serde(rename = "Brutto", with = "german_decimal::serde")]gross: Decimal,#[serde(rename = "Guthaben", with = "german_decimal::serde")]balance: Decimal,#[serde(rename = "Transaktionscode")]transaction_id: &'r str,#[serde(rename = "Absender E-Mail-Adresse")]sender_email: &'r str,#[serde(rename = "Name")]name: &'r str,#[serde(rename = "Bankkonto")]bank_account: &'r str,#[serde(rename = "Rechnungsnummer")]invoice_id: Option<&'r str>,#[serde(borrow, rename = "Zugehöriger Transaktionscode")]related_transaction_id: Option<&'r str>,
fn parse_date(record: &Record) -> Result<Date, Error> {use time::format_description::FormatItem;const DATE_COLUMN_INDEX: usize = 0;const DATE_FORMAT: &[FormatItem] = format_description!("[day].[month].[year]");Date::parse(&record[DATE_COLUMN_INDEX], DATE_FORMAT).map_err(|_| todo!())}
time::serde::format_description!(dmy, Date, "[day].[month].[year]");time::serde::format_description!(hms, Time, "[hour]:[minute]:[second]");
#[derive(Clone, Copy, Debug, Deserialize)]pub struct Record<'r> {#[serde(rename = "Datum", with = "dmy")]date: Date,#[serde(rename = "Beschreibung")]transaction_kind: TransactionKind<'r>,#[serde(rename = "Beschreibung2")]description: &'r str,#[serde(rename = "Wert", with = "german_decimal::serde")]amount: Decimal,}
fn parse_transaction_date(record: &csv::Record) -> Result<Date, Error> {use time::format_description::FormatItem;const DATE_COLUMN_INDEX: usize = 0;const DATE_FORMAT: &[FormatItem] = format_description!("[day].[month].[year]");Date::parse(&record[DATE_COLUMN_INDEX], DATE_FORMAT).map_err(|_| todo!())}
time::serde::format_description!(dmy, Date, "[day].[month].[year]");
mod dmy_short {pub fn deserialize<'de, D>(deserializer: D) -> Result<time::Date, D::Error>whereD: serde::Deserializer<'de>,{struct Visitor;impl serde::de::Visitor<'_> for Visitor {type Value = time::Date;fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {formatter.write_str("a date in dd.mm.yy format")}fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>whereE: serde::de::Error,{const BASE_YEAR: i32 = 2000;const DATE_FORMAT: &[time::format_description::FormatItem] =time::macros::format_description!("[day].[month].[year repr:last_two]");let mut parsed = time::parsing::Parsed::new();parsed.parse_items(v.as_bytes(), DATE_FORMAT).unwrap();let year_last_two = i32::from(parsed.year_last_two().unwrap());parsed.set_year(BASE_YEAR + year_last_two);parsed.try_into().map_err(E::custom)}}
const DEPOT_ID_COLUMN_INDEX: usize = 0;const DEPOT_POSITION_COLUMN_INDEX: usize = 1;const FUND_CURRENCY_COLUMN_INDEX: usize = 12;const FUND_NAME_COLUMN_INDEX: usize = 6;const ISIN_COLUMN_INDEX: usize = 7;const REFERENCE_COLUMN_INDEX: usize = 2;const SHARES_COLUMN_INDEX: usize = 10;const PRICE_DATE_COLUMN_INDEX: usize = 13;const PAYMENT_AMOUNT_COLUMN_INDEX: usize = 8;const PAYMENT_CURRENY_COLUMN_INDEX: usize = 9;const TRANSACTION_KIND_COLUMN_INDEX: usize = 4;const SHARE_PRICE_COLUMN_INDEX: usize = 11;const EXCHANGE_RATE_COLUMN_INDEX: usize = 14;
let kind = TransactionKind::try_from(&record[TRANSACTION_KIND_COLUMN_INDEX])?;let transaction_id = &record[REFERENCE_COLUMN_INDEX];let (reference, ..) = transaction_id.split_once('/').unwrap();
let reference = record.reference.transaction_id;
let fund_commodity = Commodity::try_from(&record[ISIN_COLUMN_INDEX]).unwrap();let depot_id = &record[DEPOT_ID_COLUMN_INDEX];let position = &record[DEPOT_POSITION_COLUMN_INDEX];
let depot_id = record.depot_id;let position = record.depot_position;
};let fund_currency = Commodity::try_from(&record[FUND_CURRENCY_COLUMN_INDEX]).unwrap();let share_price = Amount::new(german_decimal::parse(&record[SHARE_PRICE_COLUMN_INDEX]).unwrap(),fund_currency,);let shares_amount = Amount::new(german_decimal::parse(&record[SHARES_COLUMN_INDEX]).map_err(|_| -> Error { todo!() })?,fund_commodity,);let payment_currency = Commodity::try_from(&record[PAYMENT_CURRENY_COLUMN_INDEX]).map_err(|_| -> Error { todo!() })?;let payment_amount = {let amount = parse_decimal_with_precision(&record[PAYMENT_AMOUNT_COLUMN_INDEX], 2).map_err(|_| -> Error { todo!() })?;Amount::new(amount, payment_currency)
let investment_amount_payment = {let investment_amount = &record[15];let investment_amount = parse_decimal_with_precision(investment_amount, 2).map_err(|_| -> Error { todo!() })?;
let fund_currency = record.fund_currency;let share_price = Amount::new(record.share_price, fund_currency);
let investment_amount_fund = {let exchange_rate = Some(&record[EXCHANGE_RATE_COLUMN_INDEX]).filter(|value| !value.is_empty()).map(german_decimal::parse).transpose().map_err(|_| -> Error { todo!() })?;
let payment_amount = Amount::new(record.payment_amount, record.payment_curreny);
let fees = german_decimal::parse(&record[26]).unwrap().tap_mut(|amount| {amount.rescale(2);});
let investment_amount_fund = record.exchange_rate.map(|rate| {let mut amount = investment_amount_payment.amount * rate;amount.rescale(2);Amount::new(amount, fund_currency)});
.set_narration(kind.format_narration(&record[FUND_NAME_COLUMN_INDEX])).add_meta(common_keys::TRANSACTION_ID, transaction_id);
.set_narration(record.transaction_kind.format_narration(record.fund_name)).add_meta(common_keys::TRANSACTION_ID, record.reference.to_string());
#[derive(Clone, Copy, Debug, Deserialize)]pub struct Record<'r> {#[serde(rename = "Buchungsdatum", with = "dmy_short")]booking_date: Date,#[serde(rename = "Depotnummer")]depot_id: &'r str,#[serde(rename = "Depotposition")]depot_position: &'r str,#[serde(rename = "Devisenkurs (ZW/FW)", with = "german_decimal::serde::opt")]exchange_rate: Option<Decimal>,#[serde(rename = "Entgelt in ZW",with = "german_decimal::serde::with_precision::<2>")]fees: Decimal,
#[serde(rename = "Fondswährung (FW)")]fund_currency: Commodity,#[serde(rename = "Fonds")]fund_name: &'r str,#[serde(rename = "Anlagebetrag in ZW",with = "german_decimal::serde::with_precision::<2>")]investment_amount: Decimal,#[serde(rename = "ISIN")]isin: Commodity,#[serde(rename = "Zahlungsbetrag in ZW",with = "german_decimal::serde::with_precision::<2>")]payment_amount: Decimal,#[serde(rename = "Zahlungswährung (ZW)")]payment_curreny: Commodity,#[serde(rename = "Kursdatum", with = "dmy_short")]price_date: Date,#[serde(rename = "Ref. Nr.")]reference: TransactionReference<'r>,#[serde(rename = "Abrechnungskurs in FW", with = "german_decimal::serde")]share_price: Decimal,#[serde(rename = "Anteile", with = "german_decimal::serde")]shares: Decimal,#[serde(rename = "Umsatzart")]transaction_kind: TransactionKind,}
pub fn parse_transaction_date(record: &Record) -> Result<Date, Error> {const TRANSACTION_DATE_COLUMN_INDEX: usize = 3;parse_date(&record[TRANSACTION_DATE_COLUMN_INDEX])
#[derive(Clone, Copy, Debug)]struct TransactionReference<'r> {order_date: Date,transaction_id: &'r str,
pub fn parse_date(date: &str) -> Result<Date, Error> {const BASE_YEAR: i32 = 2000;const DATE_FORMAT: &[time::format_description::FormatItem] =format_description!("[day].[month].[year repr:last_two]");
impl<'de, 'r> serde::Deserialize<'de> for TransactionReference<'r>where'de: 'r,{fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>whereD: serde::Deserializer<'de>,{struct Visitor;impl<'de> serde::de::Visitor<'de> for Visitor {type Value = TransactionReference<'de>;fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {formatter.write_str("an ebase transaction reference number")}fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E>whereE: serde::de::Error,{let (transaction_id, order_date) = v.split_once('/').ok_or(E::custom("unexpected format for transaction reference"))?;
fn parse_decimal_with_precision(decimal: &str,precision: u32,) -> Result<Decimal, rust_decimal::Error> {german_decimal::parse(decimal).tap_ok_mut(|amount| {amount.rescale(precision);})
impl std::fmt::Display for TransactionReference<'_> {fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {let mut buffer: &mut [u8] = &mut [0u8; 8];f.write_str(self.transaction_id)?;f.write_char('/')?;self.order_date.format_into(&mut buffer, format_description!("[day][month][year]")).expect("valid formatting");f.write_str(core::str::from_utf8(buffer).expect("valid UTF-8"))}
const CURRENCY_COLUMN_INDEX: usize = 7;const DEPOT_ID_COLUMN_INDEX: usize = 0;const DEPOT_POSITION_COLUMN_INDEX: usize = 1;const ISIN_COLUMN_INDEX: usize = 3;const SHARE_PRICE_COLUMN_INDEX: usize = 6;const SHARES_COLUMN_INDEX: usize = 5;let date = parse_price_date(record)?;let depot_id = &record[DEPOT_ID_COLUMN_INDEX];let position = {let position = &record[DEPOT_POSITION_COLUMN_INDEX];let position = usize::from_str(position).context(PositionSnafu { position })?;format!("{:02}", position)};
let position = format!("{:02}", record.depot_position);
let share_price = {let share_price = &record[SHARE_PRICE_COLUMN_INDEX];let amount =german_decimal::parse(share_price).context(SharePriceSnafu { share_price })?;let currency = &record[CURRENCY_COLUMN_INDEX];let commodity = Commodity::try_from(currency).context(CurrencySnafu { currency })?;
let share_price = Amount::new(record.share_price, record.share_price_currency);let price = Price::new(record.share_price_date, isin, share_price);
#[derive(Clone, Copy, Debug, Deserialize)]pub struct Record<'r> {#[serde(rename = "Depotnummer")]depot_id: &'r str,#[serde(rename = "Position")]depot_position: usize,#[serde(rename = "ISIN")]isin: Commodity,#[serde(rename = "Anteile", with = "german_decimal::serde")]shares: Decimal,#[serde(rename = "Anteilswert", with = "german_decimal::serde")]share_price: Decimal,#[serde(rename = "Währung (Anteilswert)")]share_price_currency: Commodity,#[serde(rename = "Kursdatum", with = "dmy")]share_price_date: Date,}
self.fold(file,None,|record| inner.date(record).transpose(),|date, extracted| {if let Some(new_date) = extracted {let date = date.get_or_insert(new_date);*date = Date::max(*date, new_date);};},)
(|| {let mut date = None;let data = std::fs::read_to_string(file).context(ReadingSnafu { file })?;let mut reader = self.builder.from_reader(data.as_bytes());let headers = reader.headers().context(ParsingSnafu {})?.clone();let mut errors = Vec::new();let mut record = Record::new();while reader.read_record(&mut record).context(ParsingSnafu {})? {let position = record.position().expect("position is set by csv::Reader");let offset = SourceOffset::from((position.byte() + 1) as usize);let new_date = record.deserialize(Some(&headers)).map(|record| inner.date(record)).context(DeserializationSnafu { offset });match new_date {Ok(new_date) if errors.is_empty() => {let date = date.get_or_insert(new_date);*date = Date::max(*date, new_date);}// we can skip updating our state when we already have at least one errorOk(_) => {}Err(error) => {errors.push(error);}}}if errors.is_empty() {Ok(date)} else {ProcessingSnafu { errors, data }.fail()}})()
self.fold(file,Vec::new(),|record| {let position = record.position().expect("position is set by csv::Reader");inner.extract(existing, record).tap_ok_mut(|records| {records.iter_mut().for_each(|directive| {directive.add_meta(common_keys::IMPORTED_RECORD, position.record());});})},|directives, extracted| {directives.extend(extracted);},)}pub fn identify(&self, file: &Utf8Path, expected_headers: &[&str]) -> Result<bool, csv::Error> {if !matches!(file.extension(), Some(ext) if ext.eq_ignore_ascii_case("csv")) {return Ok(false);}let mut reader = self.builder.from_path(file)?;let headers = reader.headers()?;Ok(headers == expected_headers)}}impl Importer {fn fold<State, Err>(&self,file: &Utf8Path,mut state: State,extractor: impl Fn(&Record) -> Result<State, Err>,folder: impl Fn(&mut State, State),) -> Result<State, Error<Err>>whereErr: Diagnostic + Send + Sync,{
let mut directives = Vec::new();
{let mut record = Record::new();while reader.read_record(&mut record).context(ParsingSnafu {})? {let position = record.position().expect("position is set by csv::Reader");let offset = SourceOffset::from((position.byte() + 1) as usize);
let mut record = Record::new();while reader.read_record(&mut record).context(ParsingSnafu {})? {let position = record.position().expect("position is set by csv::Reader");let offset = SourceOffset::from((position.byte() + 1) as usize);
match extractor(&record).context(RecordSnafu { offset }) {Ok(new_state) if errors.is_empty() => {folder(&mut state, new_state);}
let extracted = record.deserialize(Some(&headers)).context(DeserializationSnafu { offset }).and_then(|record| {inner.extract(existing, record).context(ExtractionSnafu { offset })});match extracted {Ok(extracted) if errors.is_empty() => {directives.extend(extracted.into_iter().map(|mut directive| {directive.add_meta(common_keys::IMPORTED_RECORD, position.record());directive}));}
ExtractingSnafu { errors, data }.fail()
ProcessingSnafu { errors, data }.fail()}}pub fn identify(&self, file: &Utf8Path, expected_headers: &[&str]) -> Result<bool, csv::Error> {if !matches!(file.extension(), Some(ext) if ext.eq_ignore_ascii_case("csv")) {return Ok(false);
#[diagnostic_source]source: E,
#[snafu(display("error while deserializing CSV record"))]Deserialization {source: csv::Error,#[label("in this record")]offset: SourceSpan,},#[snafu(display("error while extracting record"))]Extraction {#[diagnostic_source]source: E,
const ORDER_ID_COLUMN_INDEX: usize = 3;let isin = Commodity::try_from(&record[0]).map_err(|_| todo!())?;let security_name = &record[1];let date = parse_order_date(record)?;let order_id = &record[ORDER_ID_COLUMN_INDEX];let transaction_kind = TransactionKind::from_str(&record[4])?;let units = german_decimal::parse(&record[5]).map_err(|_| todo!())?;let amount = Amount::new(units, isin);let security_price = parse_amount(&record[7])?;
let isin =Commodity::try_from(record.isin.to_string()).expect("ISINs are valid commodities");
TransactionKind::Buy => (amount, CostBasis::PerUnit(security_price), None, -total),TransactionKind::Sell => (-amount, CostBasis::Empty, Some(security_price), total),
TransactionKind::Buy => (Amount::new(record.shares, isin),CostBasis::PerUnit(record.unit_cost),None,-record.total_cost,),TransactionKind::Sell => (-Amount::new(record.shares, isin),CostBasis::Empty,Some(record.unit_cost),record.total_cost,),
}#[derive(Clone, Debug, Deserialize)]// TODO derive Copy once ISIN changes to support thatpub struct Record<'r> {#[serde(rename = "Datum", with = "dmy")]date: Date,#[serde(deserialize_with = "deserialize_isin", rename = "Isin")]isin: ISIN,#[serde(rename = "Ordernummer")]order_id: &'r str,#[serde(rename = "Wertpapierbezeichnung")]security_name: &'r str,#[serde(rename = "Stück", with = "german_decimal::serde")]shares: Decimal,#[serde(deserialize_with = "deserialize_german_amount", rename = "Kurswert")]total_cost: Amount,#[serde(rename = "Geschäftsart")]transaction_kind: TransactionKind,#[serde(deserialize_with = "deserialize_german_amount", rename = "Kurs")]unit_cost: Amount,
fn parse_amount(cost: &str) -> Result<Amount, Error> {let (amount, commodity) = cost.split_once(' ').ok_or_else(|| todo!())?;
time::serde::format_description!(dmy, Date, "[day].[month].[year]");fn deserialize_isin<'de, D>(deserializer: D) -> Result<ISIN, D::Error>whereD: serde::Deserializer<'de>,{struct Visitor;impl<'de> serde::de::Visitor<'de> for Visitor {type Value = ISIN;fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {formatter.write_str("an ISIN")}
let amount = german_decimal::parse(amount).map_err(|_| todo!())?;let commodity = Commodity::try_from(commodity).map_err(|_| todo!())?;
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>whereE: serde::de::Error,{isin::parse(v).map_err(E::custom)}}
fn parse_order_date(record: &csv::Record) -> Result<Date, Error> {use time::format_description::FormatItem;
impl<'de> serde::de::Visitor<'de> for Visitor {type Value = Amount;fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {formatter.write_str("an ISIN")}fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>whereE: serde::de::Error,{let Some((amount, currency)) = v.split_once(' ') else {return Err(E::custom("unexpected format"));};
const DATE_COLUMN_INDEX: usize = 2;const DATE_FORMAT: &[FormatItem] = format_description!("[day].[month].[year]");
let amount = german_decimal::parse(amount).map_err(E::custom)?;let currency = Commodity::try_from(currency).map_err(E::custom)?;Ok(Amount::new(amount, currency))}}
fn date(&self, record: &csv::Record) -> Option<Result<time::Date, Self::Error>> {Some(parse_order_timestamp(record).map(OffsetDateTime::date))
fn date(&self, record: Record) -> Date {Date::max(record.order_time.date(),record.invoice_date.unwrap_or(Date::MIN),)
const CURRENCY_COLUMN_INDEX: usize = 22;const ORDER_ID_COLUMN_INDEX: usize = 19;const PRODUCT_NAME_COLUMN_INDEX: usize = 4;const ITEM_PRICE_COLUMN_INDEX: usize = 8;let order_id = Some(&record[ORDER_ID_COLUMN_INDEX]).filter(|order_id| !order_id.is_empty());let timestamp = parse_order_timestamp(record)?;let commodity = Commodity::try_from(&record[CURRENCY_COLUMN_INDEX]).unwrap();let amount = {let amount = &record[ITEM_PRICE_COLUMN_INDEX];let (amount, _) = amount.split_once(' ').ok_or_else(|| todo!())?;let amount = german_decimal::parse(amount).map_err(|_| todo!())?;Amount::new(-amount, commodity)
let Some(order_id) = record.order_id else {return Ok(vec![]);
.set_payee(&self.config.payee).set_narration(product_name);
.set_payee(record.seller).set_narration(record.item_description).add_link(Link::try_from(format!("^apple.{order_id}")).expect("valid link")).add_meta(common_keys::TRANSACTION_ID, order_id);
.add_link(Link::try_from(format!("^apple.{order_id}")).expect("valid link")).add_meta(common_keys::TRANSACTION_ID, order_id);};
.add_meta(MetadataKey::try_from("invoice-date").expect("valid key"),invoice_date.to_string(),).add_meta(MetadataKey::try_from("invoice-id").expect("valid key"),invoice_id,);}
#[derive(Clone, Copy, Debug, Deserialize)]pub struct Record<'r> {#[serde(rename = "Currency")]currency: Currency,#[serde(rename = "Document Number")]document_number: Option<&'r str>,#[serde(rename = "Invoice Date", with = "mdy::option")]invoice_date: Option<Date>,#[serde(rename = "Item Description")]item_description: &'r str,#[serde(deserialize_with = "deserialize_german_euro_amount",rename = "Invoice Item Total")]item_total: Decimal,#[serde(rename = "Order Number")]order_id: Option<&'r str>,#[serde(rename = "Item Purchased Date", with = "time::serde::iso8601")]order_time: OffsetDateTime,#[serde(rename = "Seller")]seller: &'r str,}
fn parse_order_timestamp(record: &csv::Record) -> Result<OffsetDateTime, Error> {const ORDER_TIMESTAMP_COLUMN_INDEX: usize = 1;
fn deserialize_german_euro_amount<'de, D>(deserializer: D) -> Result<Decimal, D::Error>whereD: serde::Deserializer<'de>,{struct Visitor;
fn parse_timestamp(input: &str) -> Result<OffsetDateTime, Error> {let utc_timestamp = OffsetDateTime::parse(input, &Iso8601::PARSING).map_err(|_| todo!())?;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {formatter.write_str("an amount with trailing € symbol")}
let timezone = time_tz::system::get_timezone().expect("system timezone");let timestamp = utc_timestamp.to_timezone(timezone);
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>whereE: serde::de::Error,{let v = v.strip_suffix(" €").ok_or_else(|| E::custom("expected symbol suffix not found"))?.trim();
const AMOUNT_COLUMN_INDEX: usize = 3;const COUNTRY_COLUMN_INDEX: usize = 1;const ORDER_ID_COLUMN_INDEX: usize = 2;const TRANSACTION_KIND_COLUMN_INDEX: usize = 4;let timestamp = parse_record_timestamp(record)?;let country = &record[COUNTRY_COLUMN_INDEX];let commodity = match country {"DE" => Commodity::try_from("EUR").unwrap(),
let commodity = match record.country {Country::DE => Commodity::try_from("EUR").unwrap(),
let kind = TransactionKind::try_from(&record[TRANSACTION_KIND_COLUMN_INDEX])?;let amount = {let amount = &record[AMOUNT_COLUMN_INDEX];let amount = Decimal::from_str(amount).map_err(|_| todo!())?;Amount::new(amount, commodity)};
let kind = TransactionKind::try_from(record.transaction_type)?;let amount = Amount::new(record.amount, commodity);
}#[derive(Clone, Copy, Debug, Deserialize)]pub struct Record<'r> {#[serde(rename = "Amount")]amount: Decimal,#[serde(rename = "Country")]country: Country,#[serde(rename = "Order number")]order_number: &'r str,#[serde(rename = "Transaction date",deserialize_with = "deserialize_local_offset_date_time")]transaction_date: OffsetDateTime,#[serde(rename = "Transaction type")]transaction_type: &'r str,
fn parse_record_timestamp(record: &csv::Record) -> Result<OffsetDateTime, Error> {const DATE_COLUMN_INDEX: usize = 0;
fn deserialize_local_offset_date_time<'de, D>(deserializer: D) -> Result<OffsetDateTime, D::Error>whereD: serde::Deserializer<'de>,{use serde::de::Error as _;use time_tz::system;use time_tz::PrimitiveDateTimeExt as _;use time_tz::TimeZone as _;
let tz = system::get_timezone().map_err(|_| todo!())?;timestamp.assume_timezone(tz).take_first().ok_or_else(|| todo!())
let tz = system::get_timezone().map_err(D::Error::custom)?;timestamp.assume_timezone(tz).take_first().ok_or_else(|| {D::Error::custom(format!("invalid datetime {timestamp} in timezone {}",tz.name()))})
const PRIMITIVE_DATE_TIME_FORMAT: Iso8601<0> =time::format_description::well_known::Iso8601::PARSING;time::serde::format_description!(iso_primitivedatetime,PrimitiveDateTime,PRIMITIVE_DATE_TIME_FORMAT);
#[derive(Clone, Copy, Debug, Deserialize)]pub struct Record<'r> {#[serde(rename = "Order ID")]order_id: &'r str,#[serde(rename = "Order Date", with = "time::serde::iso8601")]order_timestamp: OffsetDateTime,#[serde(rename = "Currency")]currency: Commodity,#[serde(rename = "Total Owed")]total_owed: Decimal,
fn parse_timestamp(input: &str) -> Result<OffsetDateTime, Error> {let utc_timestamp = PrimitiveDateTime::parse(input, &Rfc3339).unwrap().assume_utc();let timezone = time_tz::system::get_timezone().expect("system timezone");let timestamp = utc_timestamp.to_timezone(timezone);Ok(timestamp)}
fn parse_record_date(record: &csv::Record) -> Result<Date, Error> {const DATE_COLUMN_INDEX: usize = 0;const DATE_FORMAT: &[time::format_description::FormatItem] =format_description!("[day] [month repr:long] [year]");
#[derive(Clone, Copy, Debug, Deserialize)]#[serde(rename_all = "PascalCase")]pub struct Record<'r> {#[serde(with = "dm_long_y")]date: Date,#[serde(rename = "Description")]kind: TransactionKind<'r>,
TransactionKind::Claim { .. } => f.write_str("Gift Card Claim"),TransactionKind::Payment { .. } => f.write_str("Payment"),TransactionKind::TopUp { .. } => f.write_str("Balance Top Up"),
TransactionKind::Claim { claim_code } => write!(f, "Claim gift card {claim_code}"),TransactionKind::Payment { order_id } => write!(f, "Payment towards order {order_id}"),TransactionKind::TopUp { order_id } => write!(f, "Balance top up in order {order_id}"),
const ASIN_COLUMN_INDEX: usize = 0;const CURRENCY_COLUMN_INDEX: usize = 14;const ORDER_ID_COLUMN_INDEX: usize = 2;const PRODUCT_NAME_COLUMN_INDEX: usize = 1;const TOTAL_OWED_COLUMN_INDEX: usize = 13;let date = parse_order_date(record)?;
let date = record.order_date;
let commodity = Commodity::try_from(&record[CURRENCY_COLUMN_INDEX]).unwrap();let amount = {let amount = Decimal::from_str(&record[TOTAL_OWED_COLUMN_INDEX]).unwrap().tap_mut(|amount| amount.rescale(2));Amount::new(amount, commodity)};
let amount = Amount::new(record.our_price_tax.tap_mut(|amount| amount.rescale(2)),record.our_price_tax_currency_code,);
use rust_decimal::Decimal;pub mod opt {use rust_decimal::Decimal;#[allow(non_camel_case_types)]pub struct with_precision<const PRECISION: u32>;impl<const PRECISION: u32> with_precision<PRECISION> {pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Decimal>, D::Error>whereD: serde::Deserializer<'de>,{struct Visitor<const PRECISION: u32>;impl<'de, const PRECISION: u32> serde::de::Visitor<'de> for Visitor<PRECISION> {type Value = Option<Decimal>;fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {formatter.write_str("an optional decimal number in german format")}fn visit_none<E>(self) -> Result<Self::Value, E>whereE: serde::de::Error,{Ok(None)}fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>whereD: serde::Deserializer<'de>,{super::with_precision::<PRECISION>::deserialize(deserializer).map(Some)}}deserializer.deserialize_option(Visitor::<PRECISION>)}}pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Decimal>, D::Error>whereD: serde::Deserializer<'de>,{struct Visitor;impl<'de> serde::de::Visitor<'de> for Visitor {type Value = Option<Decimal>;fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {formatter.write_str("an optional decimal number in german format")}fn visit_none<E>(self) -> Result<Self::Value, E>whereE: serde::de::Error,{Ok(None)}fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>whereD: serde::Deserializer<'de>,{super::deserialize(deserializer).map(Some)}}deserializer.deserialize_option(Visitor)}}#[allow(non_camel_case_types)]pub struct with_precision<const PRECISION: u32>;impl<const PRECISION: u32> with_precision<PRECISION> {pub fn deserialize<'de, D>(deserializer: D) -> Result<Decimal, D::Error>whereD: serde::Deserializer<'de>,{struct Visitor<const PRECISION: u32>;impl<'de, const PRECISION: u32> serde::de::Visitor<'de> for Visitor<PRECISION> {type Value = Decimal;fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {formatter.write_str("a decimal number in german format")}fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>whereE: serde::de::Error,{let mut amount = crate::parse(v).map_err(E::custom)?;amount.rescale(PRECISION);Ok(amount)}}deserializer.deserialize_str(Visitor::<PRECISION>)}}pub fn deserialize<'de, D>(deserializer: D) -> Result<Decimal, D::Error>whereD: serde::Deserializer<'de>,{struct Visitor;impl<'de> serde::de::Visitor<'de> for Visitor {type Value = Decimal;fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {formatter.write_str("a decimal number in german format")}fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>whereE: serde::de::Error,{crate::parse(v).map_err(E::custom)}}deserializer.deserialize_str(Visitor)}