edit in importers/vr-bank/src/lib.rs at line 21
+ use iso_currency::Currency;
edit in importers/vr-bank/src/lib.rs at line 24
+ use rust_decimal::Decimal;
edit in importers/vr-bank/src/lib.rs at line 29
− use time::macros::format_description;
edit in importers/vr-bank/src/lib.rs at line 153
+ type Record<'de> = Record<'de>;
replacement in importers/vr-bank/src/lib.rs at line 155
[7.4096]→[7.4096:4221](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
− Some(parse_transaction_date(record))
+ fn date(&self, record: Record) -> Date {
+ Date::max(record.booking_date, record.value_date)
replacement in importers/vr-bank/src/lib.rs at line 162
[7.4292]→[7.4292:4322](∅→∅) replacement in importers/vr-bank/src/lib.rs at line 164
[7.4369]→[7.4369:4518](∅→∅) − let date = parse_transaction_date(record)?;
−
− let commodity = Commodity::try_from(&record[12]).map_err(|_err| -> Error { todo!() })?;
+ let commodity =
+ Commodity::try_from(record.currency.code()).expect("currencies are valid commodities");
replacement in importers/vr-bank/src/lib.rs at line 169
[7.4582]→[7.4582:4639](∅→∅) − let mut transaction = Transaction::on(date);
+ let mut transaction = Transaction::on(record.value_date);
replacement in importers/vr-bank/src/lib.rs at line 171
[7.4640]→[7.4640:4714](∅→∅) − transaction.set_payee(&record[6]).set_narration(&record[10]);
+ transaction
+ .set_payee(record.transaction_partner_name)
+ .set_narration(record.purpose);
edit in importers/vr-bank/src/lib.rs at line 178
[7.4826]→[7.4826:5098](∅→∅) − let amount = {
− let amount = &record[11];
− let amount = german_decimal::parse(amount).map_err(|_| -> Error { todo!() })?;
−
− Amount::new(amount, commodity)
− };
−
− let opposite_iban = &record[7];
replacement in importers/vr-bank/src/lib.rs at line 181
[7.5191]→[7.5191:5227](∅→∅) + .get(record.transaction_partner_iban)
replacement in importers/vr-bank/src/lib.rs at line 187
[7.5415]→[7.5415:5450](∅→∅) + record.transaction_partner_iban,
replacement in importers/vr-bank/src/lib.rs at line 190
[7.5520]→[7.5520:5568](∅→∅) − posting.set_amount(amount);
+ posting.set_amount(Amount::new(record.amount, commodity));
edit in importers/vr-bank/src/lib.rs at line 198
[7.5767]→[7.5767:6019](∅→∅) −
− let balance = {
− let amount = {
− let amount = &record[13];
− let amount = german_decimal::parse(amount).map_err(|_| -> Error { todo!() })?;
−
− Amount::new(amount, commodity)
− };
replacement in importers/vr-bank/src/lib.rs at line 199
[7.6020]→[7.6020:6118](∅→∅) − Balance::new(date.next_day().unwrap(), self.config.account.base(), amount)
− };
+ let balance = Balance::new(
+ record.value_date.next_day().unwrap(),
+ self.config.account.base(),
+ Amount::new(record.balance, commodity),
+ );
edit in importers/vr-bank/src/lib.rs at line 248
+
+ #[derive(Clone, Copy, Debug, Deserialize)]
+ pub struct Record<'r> {
+ #[serde(rename = "Betrag", with = "german_decimal::serde")]
+ amount: Decimal,
+
+ #[serde(rename = "Saldo nach Buchung", with = "german_decimal::serde")]
+ balance: Decimal,
edit in importers/vr-bank/src/lib.rs at line 257
+ #[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,
+ }
+
edit in importers/vr-bank/src/lib.rs at line 304
[7.7851]→[7.7851:7852](∅→∅),
[7.9105]→[7.9105:9349](∅→∅) −
− fn parse_transaction_date(record: &csv::Record) -> Result<Date, Error> {
− use time::format_description::FormatItem;
−
− const DATE_COLUMN_INDEX: usize = 5;
− const DATE_FORMAT: &[FormatItem] = format_description!("[day].[month].[year]");
replacement in importers/vr-bank/src/lib.rs at line 305
[7.9350]→[7.9350:9430](∅→∅) − Date::parse(&record[DATE_COLUMN_INDEX], DATE_FORMAT).map_err(|_| todo!())
− }
+ time::serde::format_description!(dmy, Date, "[day].[month].[year]");
edit in importers/vr-bank/Cargo.toml at line 20
+ iso_currency.workspace = true
edit in importers/vr-bank/Cargo.toml at line 22
+ rust_decimal.workspace = true
edit in importers/uniondepot/src/lib.rs at line 21
+ use rust_decimal::Decimal;
edit in importers/uniondepot/src/lib.rs at line 26
− use time::format_description::well_known::Iso8601;
edit in importers/uniondepot/src/lib.rs at line 166
+ type Record<'de> = Record<'de>;
replacement in importers/uniondepot/src/lib.rs at line 168
[7.4503]→[7.4503:4622](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
− Some(parse_order_date(record))
+ fn date(&self, record: Record) -> Date {
+ record.date
replacement in importers/uniondepot/src/lib.rs at line 175
[7.4693]→[7.4693:4723](∅→∅) replacement in importers/uniondepot/src/lib.rs at line 177
[7.4770]→[7.4770:5136](∅→∅) − 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)?;
replacement in importers/uniondepot/src/lib.rs at line 180
[7.5202]→[7.5202:5272](∅→∅) − let stock = self.lookup_commodity(context.position).unwrap();
+ let stock = self.lookup_commodity(record.depot_id.position).unwrap();
replacement in importers/uniondepot/src/lib.rs at line 182
[7.5273]→[7.5273:5319](∅→∅) − let date = parse_order_date(record)?;
+ let date = record.date;
replacement in importers/uniondepot/src/lib.rs at line 184
[7.5320]→[7.5320:5713](∅→∅) − 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;
replacement in importers/uniondepot/src/lib.rs at line 188
[7.5714]→[7.5714:5969](∅→∅) − let total = {
− let value: &str = &record[TOTAL_COLUMN_INDEX];
− german_decimal::parse(value).map_err(|_| todo!())
− }?;
− let total = Amount {
− amount: total,
− commodity: currency,
− };
+ let total = Amount::new(record.total, currency);
replacement in importers/uniondepot/src/lib.rs at line 190
[7.5970]→[7.5970:6069](∅→∅) − let transaction_kind = TransactionKind::from_str(&record[TRANSACTION_KIND_COLUMN_INDEX])?;
+ let transaction_kind = record.transaction_kind;
+
edit in importers/uniondepot/src/lib.rs at line 334
+ }
+ }
+
+ #[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 })
replacement in importers/uniondepot/src/lib.rs at line 395
[7.11225]→[7.11225:11277](∅→∅) − impl<'i> TryFrom<&'i str> for TemplateContext<'i> {
+ impl<'r> TryFrom<DepotId<'r>> for TemplateContext<'r> {
replacement in importers/uniondepot/src/lib.rs at line 398
[7.11302]→[7.11302:11444](∅→∅) − fn try_from(value: &'i str) -> Result<Self, Self::Error> {
− let (depot, position) = value.split_once('/').ok_or_else(|| todo!())?;
+ fn try_from(value: DepotId<'r>) -> Result<Self, Self::Error> {
+ let DepotId { depot, position } = value;
replacement in importers/uniondepot/src/lib.rs at line 434
[7.12175]→[7.12175:12205](∅→∅) − #[derive(Clone, Copy, Debug)]
+ #[derive(Clone, Copy, Debug, Deserialize)]
edit in importers/uniondepot/src/lib.rs at line 436
+ #[serde(rename = "Verkauf wg. Depotgebühr UID mit Postbox")]
edit in importers/uniondepot/src/lib.rs at line 438
+
+ #[serde(rename = "Ertragsausschüttung")]
edit in importers/uniondepot/src/lib.rs at line 441
+
+ #[serde(rename = "Fremde Gebühren")]
edit in importers/uniondepot/src/lib.rs at line 444
+
+ #[serde(rename = "Kauf")]
edit in importers/uniondepot/src/lib.rs at line 447
+
+ #[serde(rename = "Storno wegen Rücklastschrift")]
edit in importers/uniondepot/src/lib.rs at line 450
+
+ #[serde(rename = "Verkauf")]
edit in importers/uniondepot/src/lib.rs at line 484
[7.13427]→[7.13427:13632](∅→∅),
[7.13835]→[7.13835:13837](∅→∅) −
− fn parse_order_date(record: &csv::Record) -> Result<Date, Error> {
− const ORDER_DATE_COLUMN_INDEX: usize = 0;
−
− Date::parse(&record[ORDER_DATE_COLUMN_INDEX], &Iso8601::PARSING).map_err(|_| todo!())
− }
edit in importers/uniondepot/Cargo.toml at line 20
+ rust_decimal.workspace = true
edit in importers/paypal/src/lib.rs at line 21
edit in importers/paypal/src/lib.rs at line 27
+ use rust_decimal::Decimal;
edit in importers/paypal/src/lib.rs at line 33
[7.985]→[7.985:1023](∅→∅) − use time::macros::format_description;
replacement in importers/paypal/src/lib.rs at line 105
[7.2933]→[7.2933:3255](∅→∅) − 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);
replacement in importers/paypal/src/lib.rs at line 108
[7.3256]→[7.3256:3397](∅→∅) − 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();
replacement in importers/paypal/src/lib.rs at line 116
[7.3422]→[7.3422:3467](∅→∅) − let currency: &str = &commodity;
+ let currency: &str = &record.currency;
replacement in importers/paypal/src/lib.rs at line 122
[7.3644]→[7.3644:3701](∅→∅) − let mut transaction = Transaction::on(date);
+ let mut transaction = Transaction::on(record.date);
replacement in importers/paypal/src/lib.rs at line 124
[7.3702]→[7.3702:3823](∅→∅) − let kind = TransactionKind::try_from(&record[3])?;
−
− let (payee, opposite_account) = match kind {
+ let (payee, opposite_account) = match record.description {
replacement in importers/paypal/src/lib.rs at line 129
[7.3972]→[7.3972:4014](∅→∅) + .get(record.sender_email)
replacement in importers/paypal/src/lib.rs at line 138
[7.4393]→[7.4393:4466](∅→∅) − self.config.reference_accounts[&record[13]].clone(),
+ self.config.reference_accounts[record.bank_account].clone(),
replacement in importers/paypal/src/lib.rs at line 141
[7.4531]→[7.4531:4581](∅→∅) − non_empty_field(&record[11]),
+ non_empty_field(record.name),
replacement in importers/paypal/src/lib.rs at line 144
[7.4655]→[7.4655:4697](∅→∅) + .get(record.sender_email)
replacement in importers/paypal/src/lib.rs at line 154
[7.4941]→[7.4941:5145](∅→∅) − 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(),
+ );
replacement in importers/paypal/src/lib.rs at line 162
[7.5271]→[7.5271:5343](∅→∅) − .add_meta(common_keys::TRANSACTION_ID, transaction_id);
+ .add_meta(common_keys::TRANSACTION_ID, record.transaction_id);
replacement in importers/paypal/src/lib.rs at line 164
[7.5344]→[7.5344:5413](∅→∅) − if let Some(invoice_id) = non_empty_field(&record[16]) {
+ if let Some(invoice_id) = record.invoice_id {
replacement in importers/paypal/src/lib.rs at line 168
[7.5524]→[7.5524:5605](∅→∅) − if let Some(related_transaction_id) = non_empty_field(&record[17]) {
+ if let Some(related_transaction_id) = record.related_transaction_id {
replacement in importers/paypal/src/lib.rs at line 175
[7.5799]→[7.5799:5892](∅→∅) − let amount = Amount::new(german_decimal::parse(&record[5]).unwrap(), commodity);
+ let amount = Amount::new(record.gross, record.currency);
edit in importers/paypal/src/lib.rs at line 187
+ let date = record.date.next_day().unwrap();
replacement in importers/paypal/src/lib.rs at line 189
[7.6268]→[7.6268:6395](∅→∅) −
− let amount = german_decimal::parse(&record[8]).unwrap();
− let amount = Amount::new(amount, commodity);
+ let amount = Amount::new(record.balance, record.currency);
edit in importers/paypal/src/lib.rs at line 191
[7.6396]→[7.6396:6446](∅→∅) − let date = date.next_day().unwrap();
−
edit in importers/paypal/src/lib.rs at line 192
[7.4607]→[7.4607:4608](∅→∅) edit in importers/paypal/src/lib.rs at line 193
[7.4699]→[7.4699:4700](∅→∅) edit in importers/paypal/src/lib.rs at line 263
+ type Record<'de> = Record<'de>;
replacement in importers/paypal/src/lib.rs at line 265
[7.9589]→[7.9589:9702](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
− Some(parse_date(record))
+ fn date(&self, record: Record) -> Date {
+ record.date
replacement in importers/paypal/src/lib.rs at line 272
[7.9773]→[7.9773:9803](∅→∅) edit in importers/paypal/src/lib.rs at line 363
+ }
+
+ #[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>,
replacement in importers/paypal/src/lib.rs at line 459
[7.13456]→[7.13456:13501](∅→∅) − #[derive(Clone, Copy, Debug, Eq, PartialEq)]
+ #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)]
edit in importers/paypal/src/lib.rs at line 461
+ #[serde(rename = "Rückbuchung")]
edit in importers/paypal/src/lib.rs at line 463
+
+ #[serde(rename = "Allgemeine Währungsumrechnung")]
edit in importers/paypal/src/lib.rs at line 466
+
+ #[serde(
+ alias = "Allgemeine Abbuchung – Bankkonto",
+ rename = "Bankgutschrift auf PayPal-Konto"
+ )]
edit in importers/paypal/src/lib.rs at line 472
+
+ #[serde(
+ alias = "Allgemeine Zahlung",
+ alias = "Handyzahlung",
+ alias = "PayPal Express-Zahlung",
+ alias = "Rückzahlung",
+ alias = "Spendenzahlung",
+ alias = "Website-Zahlung",
+ rename = "Zahlung im Einzugsverfahren mit Zahlungsrechnung"
+ )]
replacement in importers/paypal/src/lib.rs at line 513
[7.14400]→[7.14400:14708](∅→∅) − 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]");
edit in importers/fidor/src/lib.rs at line 32
− use time::macros::format_description;
replacement in importers/fidor/src/lib.rs at line 108
[7.2813]→[7.2813:2843](∅→∅) replacement in importers/fidor/src/lib.rs at line 112
[7.2912]→[7.2912:2950](∅→∅) − let (payee, iban) = record[2]
+ let (payee, iban) = record
+ .description
edit in importers/fidor/src/lib.rs at line 179
+ type Record<'de> = Record<'de>;
replacement in importers/fidor/src/lib.rs at line 181
[7.4887]→[7.4887:5012](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
− Some(parse_transaction_date(record))
+ fn date(&self, record: Record) -> Date {
+ record.date
replacement in importers/fidor/src/lib.rs at line 188
[7.5083]→[7.5083:5113](∅→∅) replacement in importers/fidor/src/lib.rs at line 190
[7.5160]→[7.5160:5260](∅→∅) − let date = parse_transaction_date(record)?;
−
− let commodity = self.config.commodity;
+ let date = record.date;
replacement in importers/fidor/src/lib.rs at line 197
[7.5418]→[7.5418:5489](∅→∅) − let transaction_kind = TransactionKind::try_from(&record[1])?;
+ let transaction_kind = record.transaction_kind;
replacement in importers/fidor/src/lib.rs at line 199
[7.5490]→[7.5490:5696](∅→∅) − let amount = {
− let amount = &record[3];
− let amount = german_decimal::parse(amount).map_err(|_| -> Error { todo!() })?;
−
− Amount::new(amount, commodity)
− };
+ let amount = Amount::new(record.amount, self.config.commodity);
edit in importers/fidor/src/lib.rs at line 360
+ #[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,
+ }
+
replacement in importers/fidor/src/lib.rs at line 404
[7.11776]→[7.11776:11806](∅→∅) − #[derive(Clone, Copy, Debug)]
+ #[derive(Clone, Copy, Debug, Deserialize)]
+ #[serde(try_from = "&str")]
replacement in importers/fidor/src/lib.rs at line 487
[7.14646]→[7.14646:14971](∅→∅) − 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]");
edit in importers/ebase/src/transactions.rs at line 1
edit in importers/ebase/src/transactions.rs at line 18
edit in importers/ebase/src/transactions.rs at line 27
− use tap::Tap as _;
− use tap::TapFallible;
edit in importers/ebase/src/transactions.rs at line 28
− use time::parsing::Parsed;
edit in importers/ebase/src/transactions.rs at line 29
+
+ mod dmy_short {
+ pub fn deserialize<'de, D>(deserializer: D) -> Result<time::Date, D::Error>
+ where
+ D: 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>
+ where
+ E: 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)
+ }
+ }
edit in importers/ebase/src/transactions.rs at line 62
+ deserializer.deserialize_str(Visitor)
+ }
+ }
+
edit in importers/ebase/src/transactions.rs at line 226
+ type Record<'de> = Record<'de>;
replacement in importers/ebase/src/transactions.rs at line 228
[2.5423]→[2.5423:5548](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
− Some(parse_transaction_date(record))
+ fn date(&self, record: Record) -> Date {
+ record.booking_date
replacement in importers/ebase/src/transactions.rs at line 235
[2.5619]→[2.5619:5644](∅→∅) edit in importers/ebase/src/transactions.rs at line 237
[2.5691]→[2.5691:6359](∅→∅) − 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;
−
replacement in importers/ebase/src/transactions.rs at line 239
[2.6399]→[2.6399:6486](∅→∅),
[2.6486]→[2.6486:6487](∅→∅),
[2.6487]→[2.6487:6620](∅→∅) − 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;
replacement in importers/ebase/src/transactions.rs at line 241
[2.6621]→[2.6621:6708](∅→∅),
[2.6708]→[2.6708:6709](∅→∅),
[2.6709]→[2.6709:6825](∅→∅) − 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;
edit in importers/ebase/src/transactions.rs at line 246
[2.6983]→[2.6983:6995](∅→∅),
[2.6995]→[2.6995:7246](∅→∅),
[2.7246]→[2.7246:7247](∅→∅),
[2.7247]→[2.7247:7444](∅→∅),
[2.7444]→[2.7444:7445](∅→∅),
[2.7445]→[2.7445:7584](∅→∅),
[2.7584]→[2.7584:7585](∅→∅),
[2.7585]→[2.7585:7764](∅→∅),
[2.7764]→[2.7764:7765](∅→∅),
[2.7765]→[2.7765:7815](∅→∅) − };
−
− 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)
replacement in importers/ebase/src/transactions.rs at line 248
[2.7827]→[2.7827:8058](∅→∅) − 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);
replacement in importers/ebase/src/transactions.rs at line 251
[2.8059]→[2.8059:8131](∅→∅) − Amount::new(investment_amount, payment_currency)
− };
+ let shares_amount = Amount::new(record.shares, record.isin);
replacement in importers/ebase/src/transactions.rs at line 253
[2.8132]→[2.8132:8422](∅→∅) − 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);
replacement in importers/ebase/src/transactions.rs at line 255
[2.8423]→[2.8423:8648](∅→∅) − exchange_rate.map(|rate| {
− let mut amount = investment_amount_payment.amount * rate;
− amount.rescale(2);
− Amount::new(amount, fund_currency)
− })
− };
+ let investment_amount_payment =
+ Amount::new(record.investment_amount, record.payment_curreny);
replacement in importers/ebase/src/transactions.rs at line 258
[2.8649]→[2.8649:8808](∅→∅) − 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)
+ });
replacement in importers/ebase/src/transactions.rs at line 264
[2.8809]→[2.8809:8889](∅→∅) − let mut transaction = Transaction::on(parse_transaction_date(record)?);
+ let mut transaction = Transaction::on(record.booking_date);
replacement in importers/ebase/src/transactions.rs at line 267
[2.8952]→[2.8952:9103](∅→∅) − .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());
replacement in importers/ebase/src/transactions.rs at line 270
[2.9104]→[2.9104:9125](∅→∅) + match record.transaction_kind {
replacement in importers/ebase/src/transactions.rs at line 280
[2.9591]→[2.9591:9696](∅→∅) − if !fees.is_zero() {
− let fees = Amount::new(fees, payment_currency);
+ if !record.fees.is_zero() {
+ let fees = Amount::new(record.fees, record.payment_curreny);
replacement in importers/ebase/src/transactions.rs at line 305
[2.10573]→[2.10573:10653](∅→∅) − let currency: &str = &payment_currency;
+ let currency: &str = &record.payment_curreny;
replacement in importers/ebase/src/transactions.rs at line 343
[2.12421]→[2.12421:12506](∅→∅) − posting.set_amount(Amount::new(fees, payment_currency));
+ posting.set_amount(Amount::new(record.fees, record.payment_curreny));
replacement in importers/ebase/src/transactions.rs at line 362
[2.13131]→[2.13131:13236](∅→∅) − if !fees.is_zero() {
− let fees = Amount::new(fees, payment_currency);
+ if !record.fees.is_zero() {
+ let fees = Amount::new(record.fees, record.payment_curreny);
replacement in importers/ebase/src/transactions.rs at line 379
[2.13866]→[2.13866:13929](∅→∅) − if payment_currency != fund_currency {
+ if record.payment_curreny != fund_currency {
replacement in importers/ebase/src/transactions.rs at line 381
[2.14038]→[2.14038:14131](∅→∅) − let exchange_rate = german_decimal::parse(&record[14]).unwrap();
+ let exchange_rate = record
+ .exchange_rate
+ .expect("exchange rate is set when currencies differ");
replacement in importers/ebase/src/transactions.rs at line 400
[2.14703]→[2.14703:14808](∅→∅) − if !fees.is_zero() {
− let fees = Amount::new(fees, payment_currency);
+ if !record.fees.is_zero() {
+ let fees = Amount::new(record.fees, record.payment_curreny);
replacement in importers/ebase/src/transactions.rs at line 417
[2.15433]→[2.15433:15496](∅→∅) − if payment_currency != fund_currency {
+ if record.payment_curreny != fund_currency {
replacement in importers/ebase/src/transactions.rs at line 419
[2.15605]→[2.15605:15698](∅→∅) − let exchange_rate = german_decimal::parse(&record[14]).unwrap();
+ let exchange_rate = record
+ .exchange_rate
+ .expect("exchange rate is set when currencies differ");
replacement in importers/ebase/src/transactions.rs at line 438
[2.16269]→[2.16269:16374](∅→∅) − if !fees.is_zero() {
− let fees = Amount::new(fees, payment_currency);
+ if !record.fees.is_zero() {
+ let fees = Amount::new(record.fees, record.payment_curreny);
replacement in importers/ebase/src/transactions.rs at line 463
[2.17251]→[2.17251:17331](∅→∅) − let currency: &str = &payment_currency;
+ let currency: &str = &record.payment_curreny;
replacement in importers/ebase/src/transactions.rs at line 491
[2.18526]→[2.18526:18654](∅→∅) − let date = parse_date(&record[PRICE_DATE_COLUMN_INDEX])?;
− Price::new(date, fund_commodity, share_price)
+ let date = record.price_date;
+ Price::new(date, record.isin, share_price)
edit in importers/ebase/src/transactions.rs at line 559
+
+ #[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,
edit in importers/ebase/src/transactions.rs at line 580
+ #[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,
+ }
+
replacement in importers/ebase/src/transactions.rs at line 680
[2.22401]→[2.22401:22418](∅→∅) + #[derive(Clone, Copy, Debug, Deserialize)]
edit in importers/ebase/src/transactions.rs at line 682
+ #[serde(rename = "Entgeltbelastung Verkauf")]
edit in importers/ebase/src/transactions.rs at line 684
+ #[serde(rename = "Kauf")]
edit in importers/ebase/src/transactions.rs at line 686
+ #[serde(rename = "Wiederanlage Fondsertrag")]
edit in importers/ebase/src/transactions.rs at line 688
+ #[serde(rename = "Ansparplan")]
edit in importers/ebase/src/transactions.rs at line 690
+ #[serde(rename = "Neuabrechnung Ansparplan")]
edit in importers/ebase/src/transactions.rs at line 692
+ #[serde(rename = "Stornierung Ansparplan")]
replacement in importers/ebase/src/transactions.rs at line 728
[2.23804]→[2.23804:23928](∅→∅),
[2.23928]→[2.23928:23929](∅→∅),
[2.23929]→[2.23929:23984](∅→∅) − 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,
replacement in importers/ebase/src/transactions.rs at line 734
[2.23987]→[2.23987:24207](∅→∅) − 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>
+ where
+ D: 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>
+ where
+ E: serde::de::Error,
+ {
+ let (transaction_id, order_date) = v
+ .split_once('/')
+ .ok_or(E::custom("unexpected format for transaction reference"))?;
replacement in importers/ebase/src/transactions.rs at line 759
[2.24208]→[2.24208:24307](∅→∅) − let mut parsed = Parsed::new();
− parsed.parse_items(date.as_bytes(), DATE_FORMAT).unwrap();
+ let order_date = Date::parse(
+ order_date,
+ time::macros::format_description!("[day][month][year]"),
+ )
+ .map_err(E::custom)?;
replacement in importers/ebase/src/transactions.rs at line 765
[2.24308]→[2.24308:24424](∅→∅) − let year_last_two = i32::from(parsed.year_last_two().unwrap());
− parsed.set_year(BASE_YEAR + year_last_two);
+ Ok(TransactionReference {
+ order_date,
+ transaction_id,
+ })
+ }
+ }
replacement in importers/ebase/src/transactions.rs at line 772
[2.24425]→[2.24425:24468](∅→∅) − parsed.try_into().map_err(Error::from)
+ deserializer.deserialize_str(Visitor)
+ }
replacement in importers/ebase/src/transactions.rs at line 776
[2.24471]→[2.24471:24686](∅→∅) − 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"))
+ }
edit in importers/ebase/src/balances.rs at line 14
[2.25220]→[2.25220:25237](∅→∅) edit in importers/ebase/src/balances.rs at line 18
+ use rust_decimal::Decimal;
edit in importers/ebase/src/balances.rs at line 22
[2.25416]→[2.25416:25443](∅→∅) − use snafu::ResultExt as _;
edit in importers/ebase/src/balances.rs at line 23
[2.25461]→[2.25461:25499](∅→∅) − use time::macros::format_description;
edit in importers/ebase/src/balances.rs at line 178
+ type Record<'de> = Record<'de>;
replacement in importers/ebase/src/balances.rs at line 180
[2.29331]→[2.29331:29450](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
− Some(parse_price_date(record))
+ fn date(&self, record: Record) -> Date {
+ record.share_price_date
replacement in importers/ebase/src/balances.rs at line 187
[2.29521]→[2.29521:29546](∅→∅) replacement in importers/ebase/src/balances.rs at line 189
[2.29593]→[2.29593:30219](∅→∅) − 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);
replacement in importers/ebase/src/balances.rs at line 192
[2.30260]→[2.30260:30319](∅→∅) − depot_id: <&Seg>::try_from(depot_id).unwrap(),
+ depot_id: <&Seg>::try_from(record.depot_id).unwrap(),
replacement in importers/ebase/src/balances.rs at line 196
[2.30392]→[2.30392:30464](∅→∅) − let isin = {
− let isin = &record[ISIN_COLUMN_INDEX];
+ let isin = record.isin;
+ let total_shares = Amount::new(record.shares, isin);
edit in importers/ebase/src/balances.rs at line 199
[2.30465]→[2.30465:30767](∅→∅) − Commodity::try_from(isin).context(IsinSnafu { isin })?
− };
− let total_shares = {
− let shares = &record[SHARES_COLUMN_INDEX];
− let shares = german_decimal::parse(shares).context(SharesSnafu { shares })?;
−
− Amount::new(shares, isin)
− };
−
replacement in importers/ebase/src/balances.rs at line 200
[2.30803]→[2.30803:30821](∅→∅) + record.share_price_date,
replacement in importers/ebase/src/balances.rs at line 205
[2.30917]→[2.30917:31285](∅→∅) − 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);
edit in importers/ebase/src/balances.rs at line 208
[2.31286]→[2.31286:31399](∅→∅) − Amount::new(amount, commodity)
− };
−
− let price = Price::new(date, isin, share_price);
−
edit in importers/ebase/src/balances.rs at line 228
+ #[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,
+ }
+
edit in importers/ebase/src/balances.rs at line 294
[2.32963]→[2.32963:33128](∅→∅) − }
−
− pub fn parse_price_date(record: &Record) -> Result<Date, Error> {
− const PRICE_DATE_COLUMN_INDEX: usize = 8;
−
− parse_date(&record[PRICE_DATE_COLUMN_INDEX])
edit in importers/ebase/src/balances.rs at line 295
[2.33130]→[2.33130:33317](∅→∅) −
− pub fn parse_date(date: &str) -> Result<Date, Error> {
− use time::format_description::FormatItem;
−
− const DATE_FORMAT: &[FormatItem] = format_description!("[day].[month].[year]");
replacement in importers/ebase/src/balances.rs at line 296
[2.33318]→[2.33318:33428](∅→∅) − Date::parse(date, DATE_FORMAT)
− .map_err(time::Error::from)
− .context(DateFormatSnafu {})
− }
+ time::serde::format_description!(dmy, Date, "[day].[month].[year]");
edit in importers/csv/src/lib.rs at line 12
+ use serde::Deserialize;
edit in importers/csv/src/lib.rs at line 17
replacement in importers/csv/src/lib.rs at line 24
− #[snafu(display("error(s) while extracting data from records"))]
− Extracting {
+ #[snafu(display("error(s) while processing CSV records"))]
+ Processing {
replacement in importers/csv/src/lib.rs at line 32
− #[snafu(display("error while parsing CSV record"))]
+ #[snafu(display("error while parsing CSV file"))]
replacement in importers/csv/src/lib.rs at line 66
[7.1501]→[7.1501:1854](∅→∅) − 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 error
+ Ok(_) => {}
+
+ Err(error) => {
+ errors.push(error);
+ }
+ }
+ }
+
+ if errors.is_empty() {
+ Ok(date)
+ } else {
+ ProcessingSnafu { errors, data }.fail()
+ }
+ })()
replacement in importers/csv/src/lib.rs at line 117
[7.2087]→[7.2087:2396](∅→∅),
[7.2396]→[5.34:127](∅→∅),
[7.117]→[7.2588:3438](∅→∅),
[5.127]→[7.2588:3438](∅→∅),
[7.2588]→[7.2588:3438](∅→∅) − 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>>
− where
− Err: Diagnostic + Send + Sync,
− {
+ let mut directives = Vec::new();
edit in importers/csv/src/lib.rs at line 121
+ let headers = reader.headers().context(ParsingSnafu {})?.clone();
replacement in importers/csv/src/lib.rs at line 125
[7.3628]→[7.3628:3932](∅→∅) − {
− 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);
replacement in importers/csv/src/lib.rs at line 130
[7.3933]→[7.3933:4145](∅→∅) − 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
+ }));
+ }
replacement in importers/csv/src/lib.rs at line 146
[7.4146]→[7.4146:4272](∅→∅) − // we can skip updating our state when we already have at least one error
− Ok(_) => {}
+ // we can skip updating our state when we already have at least one error
+ Ok(_) => {}
replacement in importers/csv/src/lib.rs at line 149
[7.4273]→[7.4273:4375](∅→∅) − Err(error) => {
− errors.push(error);
− }
+ Err(error) => {
+ errors.push(error);
replacement in importers/csv/src/lib.rs at line 156
[7.4449]→[7.4449:4471](∅→∅) replacement in importers/csv/src/lib.rs at line 158
[7.4488]→[7.4488:4540](∅→∅) − 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);
edit in importers/csv/src/lib.rs at line 166
+
+ let mut reader = self.builder.from_path(file)?;
+ let headers = reader.headers()?;
+
+ Ok(headers == expected_headers)
edit in importers/csv/src/lib.rs at line 173
replacement in importers/csv/src/lib.rs at line 177
[7.4595]→[7.4595:4684](∅→∅) − #[snafu(display("encountered error while extracting record"))]
− pub struct RecordError<E>
+ pub enum RecordError<E>
replacement in importers/csv/src/lib.rs at line 181
[7.4721]→[7.4721:4761](∅→∅) − #[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,
replacement in importers/csv/src/lib.rs at line 194
[7.4762]→[7.4762:4817](∅→∅) − #[label("in this record")]
− offset: SourceSpan,
+ #[label("in this record")]
+ offset: SourceSpan,
+ },
edit in importers/csv/src/lib.rs at line 201
+ type Record<'de>: Deserialize<'de>;
replacement in importers/csv/src/lib.rs at line 203
[7.4900]→[7.4900:4995](∅→∅) − fn date(&self, _record: &Record) -> Option<Result<Date, Self::Error>> {
− None
− }
+ fn date(&self, _record: Self::Record<'_>) -> Date;
replacement in importers/csv/src/lib.rs at line 208
[7.5059]→[7.5059:5084](∅→∅) + record: Self::Record<'_>,
edit in importers/csv/src/lib.rs at line 217
+ type Record<'de> = R::Record<'de>;
replacement in importers/csv/src/lib.rs at line 221
[7.5259]→[7.5259:5342](∅→∅) − fn date(&self, _record: &Record) -> Option<Result<Date, Self::Error>>;
+ fn date(&self, record: Self::Record<'_>) -> Date;
replacement in importers/csv/src/lib.rs at line 225
[7.5429]→[7.5429:5462](∅→∅) + record: Self::Record<'_>,
edit in importers/bw-bank/src/portfolio.rs at line 18
edit in importers/bw-bank/src/portfolio.rs at line 21
+ use rust_decimal::Decimal;
edit in importers/bw-bank/src/portfolio.rs at line 27
− use time::macros::format_description;
edit in importers/bw-bank/src/portfolio.rs at line 126
+ type Record<'de> = Record<'de>;
replacement in importers/bw-bank/src/portfolio.rs at line 128
[7.3302]→[7.3302:3421](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
− Some(parse_order_date(record))
+ fn date(&self, record: Record) -> Date {
+ record.date
replacement in importers/bw-bank/src/portfolio.rs at line 135
[7.3492]→[7.3492:3522](∅→∅) replacement in importers/bw-bank/src/portfolio.rs at line 137
[7.3569]→[7.3569:4090](∅→∅) − 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");
replacement in importers/bw-bank/src/portfolio.rs at line 140
[7.4091]→[7.4091:4138](∅→∅) − let total = parse_amount(&record[9])?;
+ let transaction_kind = record.transaction_kind;
+ let security_name = record.security_name;
+ let order_id = record.order_id;
replacement in importers/bw-bank/src/portfolio.rs at line 145
[7.4213]→[7.4213:4404](∅→∅) − 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,
+ ),
replacement in importers/bw-bank/src/portfolio.rs at line 159
[7.4416]→[7.4416:4488](∅→∅) − let transaction = Transaction::on(date).tap_mut(|transaction| {
+ let transaction = Transaction::on(record.date).tap_mut(|transaction| {
replacement in importers/bw-bank/src/portfolio.rs at line 171
[7.5003]→[7.5003:5068](∅→∅) − .add_meta(common_keys::TRANSACTION_ID, order_id)
+ .add_meta(common_keys::TRANSACTION_ID, record.order_id)
replacement in importers/bw-bank/src/portfolio.rs at line 187
[7.5663]→[7.5663:5723](∅→∅) − let price = Price::new(date, isin, security_price);
+ let price = Price::new(record.date, isin, record.unit_cost);
edit in importers/bw-bank/src/portfolio.rs at line 226
+ }
+
+ #[derive(Clone, Debug, Deserialize)]
+ // TODO derive Copy once ISIN changes to support that
+ pub 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,
replacement in importers/bw-bank/src/portfolio.rs at line 295
[7.7827]→[7.7827:7857](∅→∅) − #[derive(Clone, Copy, Debug)]
+ #[derive(Clone, Copy, Debug, Deserialize)]
edit in importers/bw-bank/src/portfolio.rs at line 297
+ #[serde(rename = "Kauf")]
edit in importers/bw-bank/src/portfolio.rs at line 299
+ #[serde(rename = "Verkauf")]
replacement in importers/bw-bank/src/portfolio.rs at line 315
[7.8155]→[7.8155:8286](∅→∅) − 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>
+ where
+ D: 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")
+ }
replacement in importers/bw-bank/src/portfolio.rs at line 330
[7.8287]→[7.8287:8431](∅→∅) − 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>
+ where
+ E: serde::de::Error,
+ {
+ isin::parse(v).map_err(E::custom)
+ }
+ }
replacement in importers/bw-bank/src/portfolio.rs at line 338
[7.8432]→[7.8432:8471](∅→∅) − Ok(Amount::new(amount, commodity))
+ deserializer.deserialize_str(Visitor)
edit in importers/bw-bank/src/portfolio.rs at line 340
+
+ fn deserialize_german_amount<'de, D>(deserializer: D) -> Result<Amount, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct Visitor;
replacement in importers/bw-bank/src/portfolio.rs at line 347
[7.8474]→[7.8474:8587](∅→∅) − 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>
+ where
+ E: serde::de::Error,
+ {
+ let Some((amount, currency)) = v.split_once(' ') else {
+ return Err(E::custom("unexpected format"));
+ };
replacement in importers/bw-bank/src/portfolio.rs at line 362
[7.8588]→[7.8588:8712](∅→∅) − 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))
+ }
+ }
replacement in importers/bw-bank/src/portfolio.rs at line 368
[7.8713]→[7.8713:8791](∅→∅) − Date::parse(&record[DATE_COLUMN_INDEX], DATE_FORMAT).map_err(|_| todo!())
+ deserializer.deserialize_str(Visitor)
edit in importers/bw-bank/Cargo.toml at line 21
edit in importers/apple/src/transaction_history.rs at line 13
+ use beancount_types::MetadataKey;
edit in importers/apple/src/transaction_history.rs at line 18
+ use iso_currency::Currency;
edit in importers/apple/src/transaction_history.rs at line 21
+ use rust_decimal::Decimal;
edit in importers/apple/src/transaction_history.rs at line 27
− use time::format_description::well_known::Iso8601;
edit in importers/apple/src/transaction_history.rs at line 28
edit in importers/apple/src/transaction_history.rs at line 30
− use time_tz::OffsetDateTimeExt as _;
edit in importers/apple/src/transaction_history.rs at line 144
+ type Record<'de> = Record<'de>;
replacement in importers/apple/src/transaction_history.rs at line 146
[7.3859]→[7.3859:4015](∅→∅) − 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),
+ )
replacement in importers/apple/src/transaction_history.rs at line 156
[7.4086]→[7.4086:4116](∅→∅) replacement in importers/apple/src/transaction_history.rs at line 158
[7.4163]→[7.4163:4892](∅→∅) − 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![]);
replacement in importers/apple/src/transaction_history.rs at line 162
[7.4904]→[7.4904:4967](∅→∅) − let product_name = &record[PRODUCT_NAME_COLUMN_INDEX];
+ let commodity = Commodity::try_from(record.currency.code()).unwrap();
+ let amount = Amount::new(-record.item_total, commodity);
replacement in importers/apple/src/transaction_history.rs at line 165
[7.4968]→[7.4968:5052](∅→∅) − let transaction = Transaction::on(timestamp.date()).tap_mut(|transaction| {
+ let transaction = Transaction::on(record.order_time.date()).tap_mut(|transaction| {
replacement in importers/apple/src/transaction_history.rs at line 174
[7.5378]→[7.5378:5471](∅→∅) − .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);
replacement in importers/apple/src/transaction_history.rs at line 179
[7.5472]→[7.5472:5519](∅→∅) − if let Some(order_id) = order_id {
+ if let Some((invoice_date, invoice_id)) =
+ record.invoice_date.zip(record.document_number)
+ {
replacement in importers/apple/src/transaction_history.rs at line 183
[7.5547]→[7.5547:5729](∅→∅) − .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,
+ );
+ }
replacement in importers/apple/src/transaction_history.rs at line 194
[7.5754]→[7.5754:5841](∅→∅) − .add_meta(common_keys::TIMESTAMP, timestamp.format(&Rfc3339).unwrap())
+ .add_meta(
+ common_keys::TIMESTAMP,
+ record.order_time.format(&Rfc3339).unwrap(),
+ )
edit in importers/apple/src/transaction_history.rs at line 235
+ #[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,
+ }
+
edit in importers/apple/src/transaction_history.rs at line 305
+
+ time::serde::format_description!(mdy, Date, "[month]/[day]/[year]");
replacement in importers/apple/src/transaction_history.rs at line 308
[7.7932]→[7.7932:8065](∅→∅) − 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>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ struct Visitor;
replacement in importers/apple/src/transaction_history.rs at line 314
[7.8066]→[7.8066:8127](∅→∅) − parse_timestamp(&record[ORDER_TIMESTAMP_COLUMN_INDEX])
− }
+ impl<'de> serde::de::Visitor<'de> for Visitor {
+ type Value = Decimal;
replacement in importers/apple/src/transaction_history.rs at line 317
[7.8128]→[7.8128:8290](∅→∅) − 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")
+ }
replacement in importers/apple/src/transaction_history.rs at line 321
[7.8291]→[7.8291:8426](∅→∅) − 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>
+ where
+ E: serde::de::Error,
+ {
+ let v = v
+ .strip_suffix(" €")
+ .ok_or_else(|| E::custom("expected symbol suffix not found"))?
+ .trim();
replacement in importers/apple/src/transaction_history.rs at line 330
[7.8427]→[7.8427:8445](∅→∅) + german_decimal::parse(v).map_err(E::custom)
+ }
+ }
+
+ deserializer.deserialize_str(Visitor)
edit in importers/apple/src/store_balance.rs at line 17
+ use iso_currency::Country;
edit in importers/apple/src/store_balance.rs at line 29
[7.9327]→[7.9327:9412](∅→∅) − use time::PrimitiveDateTime;
− use time_tz::system;
− use time_tz::PrimitiveDateTimeExt;
edit in importers/apple/src/store_balance.rs at line 123
+ type Record<'de> = Record<'de>;
replacement in importers/apple/src/store_balance.rs at line 125
[7.11709]→[7.11709:11860](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
− Some(parse_record_timestamp(record).map(OffsetDateTime::date))
+ fn date(&self, record: Record) -> Date {
+ record.transaction_date.date()
replacement in importers/apple/src/store_balance.rs at line 132
[7.11931]→[7.11931:11961](∅→∅) replacement in importers/apple/src/store_balance.rs at line 134
[7.12008]→[7.12008:12414](∅→∅) − 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(),
replacement in importers/apple/src/store_balance.rs at line 139
[7.12452]→[4.52:113](∅→∅) − let transaction_id = &record[ORDER_ID_COLUMN_INDEX];
+ let transaction_id = record.order_number;
replacement in importers/apple/src/store_balance.rs at line 141
[7.12508]→[7.12508:12801](∅→∅) − 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);
replacement in importers/apple/src/store_balance.rs at line 153
[7.13339]→[7.13339:13412](∅→∅) − let mut transaction = Transaction::on(timestamp.date());
+ let mut transaction = Transaction::on(record.transaction_date.date());
replacement in importers/apple/src/store_balance.rs at line 169
[7.13778]→[7.13778:13852](∅→∅) − timestamp.format(&Rfc3339).map_err(|_| todo!())?,
+ record
+ .transaction_date
+ .format(&Rfc3339)
+ .map_err(|_| todo!())?,
replacement in importers/apple/src/store_balance.rs at line 184
[7.14287]→[7.14287:14360](∅→∅) − let mut transaction = Transaction::on(timestamp.date());
+ let mut transaction = Transaction::on(record.transaction_date.date());
replacement in importers/apple/src/store_balance.rs at line 191
[7.14592]→[7.14592:14666](∅→∅) − timestamp.format(&Rfc3339).map_err(|_| todo!())?,
+ record
+ .transaction_date
+ .format(&Rfc3339)
+ .map_err(|_| todo!())?,
edit in importers/apple/src/store_balance.rs at line 245
+ }
+
+ #[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,
replacement in importers/apple/src/store_balance.rs at line 327
[7.17562]→[7.17562:17685](∅→∅) − 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>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ use serde::de::Error as _;
+ use time_tz::system;
+ use time_tz::PrimitiveDateTimeExt as _;
+ use time_tz::TimeZone as _;
replacement in importers/apple/src/store_balance.rs at line 336
[7.17686]→[7.17686:17863](∅→∅) − let timestamp = PrimitiveDateTime::parse(&record[DATE_COLUMN_INDEX], &Iso8601::PARSING)
− .map_err(|error| {
− dbg!(error);
− todo!()
− })?;
+ let timestamp = iso_primitivedatetime::deserialize(deserializer)?;
replacement in importers/apple/src/store_balance.rs at line 338
[7.17864]→[7.17864:18020](∅→∅) − 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()
+ ))
+ })
edit in importers/apple/src/store_balance.rs at line 346
+
+ 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
+ );
edit in importers/apple/Cargo.toml at line 20
+ iso_country.workspace = true
+ iso_currency.workspace = true
edit in importers/amazon/src/order_history.rs at line 27
edit in importers/amazon/src/order_history.rs at line 29
− use time::PrimitiveDateTime;
− use time_tz::OffsetDateTimeExt as _;
edit in importers/amazon/src/order_history.rs at line 141
+ type Record<'de> = Record<'de>;
replacement in importers/amazon/src/order_history.rs at line 143
[7.3894]→[7.3894:4050](∅→∅) − 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 {
+ record.order_timestamp.date()
replacement in importers/amazon/src/order_history.rs at line 150
[7.4121]→[7.4121:4151](∅→∅) replacement in importers/amazon/src/order_history.rs at line 152
[7.4198]→[7.4198:4442](∅→∅) − const ASIN_COLUMN_INDEX: usize = 12;
− const CURRENCY_COLUMN_INDEX: usize = 4;
− const ORDER_ID_COLUMN_INDEX: usize = 1;
− const PRODUCT_NAME_COLUMN_INDEX: usize = 23;
− const TOTAL_OWED_COLUMN_INDEX: usize = 9;
replacement in importers/amazon/src/order_history.rs at line 154
[7.4443]→[7.4443:4555](∅→∅) − let order_id = &record[ORDER_ID_COLUMN_INDEX];
−
− let timestamp = parse_order_timestamp(record)?;
+ let order_id = record.order_id;
edit in importers/amazon/src/order_history.rs at line 156
[7.4556]→[7.4556:4642](∅→∅) − let commodity = Commodity::try_from(&record[CURRENCY_COLUMN_INDEX]).unwrap();
replacement in importers/amazon/src/order_history.rs at line 157
[7.4665]→[7.4665:4865](∅→∅) − let amount = Decimal::from_str(&record[TOTAL_OWED_COLUMN_INDEX])
− .unwrap()
− .tap_mut(|amount| amount.rescale(2));
− Amount::new(amount, commodity)
+ Amount::new(
+ record.total_owed.tap_mut(|amount| amount.rescale(2)),
+ record.currency,
+ )
replacement in importers/amazon/src/order_history.rs at line 163
[7.4877]→[7.4877:4987](∅→∅) − let asin = &record[ASIN_COLUMN_INDEX];
− let product_name = &record[PRODUCT_NAME_COLUMN_INDEX];
+ let asin = record.asin;
+ let product_name = record.product_name;
replacement in importers/amazon/src/order_history.rs at line 166
[7.4988]→[7.4988:5072](∅→∅) − let transaction = Transaction::on(timestamp.date()).tap_mut(|transaction| {
+ let transaction = Transaction::on(record.order_timestamp.date()).tap_mut(|transaction| {
replacement in importers/amazon/src/order_history.rs at line 168
[7.5100]→[7.5100:5149](∅→∅) − let currency: &str = &commodity;
+ let currency: &str = &record.currency;
replacement in importers/amazon/src/order_history.rs at line 179
[7.5689]→[7.5689:5776](∅→∅) − .add_meta(common_keys::TIMESTAMP, timestamp.format(&Rfc3339).unwrap())
+ .add_meta(
+ common_keys::TIMESTAMP,
+ record.order_timestamp.format(&Rfc3339).unwrap(),
+ )
edit in importers/amazon/src/order_history.rs at line 214
+
+ #[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,
edit in importers/amazon/src/order_history.rs at line 229
+ #[serde(rename = "ASIN")]
+ asin: &'r str,
+
+ #[serde(rename = "Product Name")]
+ product_name: &'r str,
+ }
+
edit in importers/amazon/src/order_history.rs at line 275
[7.7721]→[7.7721:7907](∅→∅) − }
−
− fn parse_order_timestamp(record: &csv::Record) -> Result<OffsetDateTime, Error> {
− const ORDER_DATE_COLUMN_INDEX: usize = 2;
−
− parse_timestamp(&record[ORDER_DATE_COLUMN_INDEX])
edit in importers/amazon/src/order_history.rs at line 276
[7.7909]→[7.7909:7977](∅→∅),
[7.7977]→[7.78:144](∅→∅),
[7.144]→[7.8206:8403](∅→∅),
[7.8206]→[7.8206:8403](∅→∅) −
− 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)
− }
edit in importers/amazon/src/gift_card_balance.rs at line 26
[7.9278]→[7.9278:9316](∅→∅) − use time::macros::format_description;
edit in importers/amazon/src/gift_card_balance.rs at line 156
+ type Record<'de> = Record<'de>;
replacement in importers/amazon/src/gift_card_balance.rs at line 158
[7.13149]→[7.13149:13269](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<Date, Self::Error>> {
− Some(parse_record_date(record))
+ fn date(&self, record: Record) -> Date {
+ record.date
replacement in importers/amazon/src/gift_card_balance.rs at line 165
[7.13340]→[7.13340:13370](∅→∅) replacement in importers/amazon/src/gift_card_balance.rs at line 167
[7.13417]→[7.13417:13464](∅→∅) − let date = parse_record_date(record)?;
+ let date = record.date;
edit in importers/amazon/src/gift_card_balance.rs at line 169
[7.13465]→[3.59:165](∅→∅) − let description = &record[1];
− let kind = TransactionKind::try_from(description).unwrap();
replacement in importers/amazon/src/gift_card_balance.rs at line 170
[7.13555]→[7.13555:13592](∅→∅) − let amount = &record[2];
+ let amount = record.amount;
replacement in importers/amazon/src/gift_card_balance.rs at line 187
− transaction.set_narration(description);
+ transaction.set_narration(record.kind.to_string());
replacement in importers/amazon/src/gift_card_balance.rs at line 189
replacement in importers/amazon/src/gift_card_balance.rs at line 238
[7.15655]→[7.15655:15891](∅→∅) − 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>,
replacement in importers/amazon/src/gift_card_balance.rs at line 247
[7.15892]→[7.15892:15970](∅→∅) − Date::parse(&record[DATE_COLUMN_INDEX], DATE_FORMAT).map_err(|_| todo!())
edit in importers/amazon/src/gift_card_balance.rs at line 249
+
+ time::serde::format_description!(dm_long_y, Date, "[day] [month repr:long] [year]");
replacement in importers/amazon/src/gift_card_balance.rs at line 293
[7.16911]→[7.16911:16941](∅→∅) − #[derive(Clone, Copy, Debug)]
+ #[derive(Clone, Copy, Debug, Deserialize)]
+ #[serde(try_from = "&str")]
replacement in importers/amazon/src/gift_card_balance.rs at line 304
[7.17207]→[7.17207:17431](∅→∅) − 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}"),
replacement in importers/amazon/src/gift_card_balance.rs at line 312
[7.17502]→[7.17502:17531](∅→∅) − type Error = (); // TODO
+ type Error = &'static str; // TODO
edit in importers/amazon/src/digital_items.rs at line 25
[7.19235]→[7.19235:19273](∅→∅) − use time::macros::format_description;
edit in importers/amazon/src/digital_items.rs at line 159
+ type Record<'de> = Record<'de>;
replacement in importers/amazon/src/digital_items.rs at line 161
[7.22977]→[7.22977:23102](∅→∅) − fn date(&self, record: &csv::Record) -> Option<Result<time::Date, Self::Error>> {
− Some(parse_order_date(record))
+ fn date(&self, record: Record) -> Date {
+ Date::max(
+ record.order_date,
+ record.fulfilled_date.unwrap_or(Date::MIN),
+ )
replacement in importers/amazon/src/digital_items.rs at line 171
[7.23173]→[7.23173:23203](∅→∅) replacement in importers/amazon/src/digital_items.rs at line 173
[7.23250]→[7.23250:23541](∅→∅) − 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;
replacement in importers/amazon/src/digital_items.rs at line 175
[7.23542]→[7.23542:23707](∅→∅) − let asin = &record[ASIN_COLUMN_INDEX];
− let product_name = &record[PRODUCT_NAME_COLUMN_INDEX];
− let order_id = &record[ORDER_ID_COLUMN_INDEX];
+ let asin = record.asin;
+ let product_name = record.title;
+ let order_id = record.order_id;
replacement in importers/amazon/src/digital_items.rs at line 179
[7.23708]→[7.23708:24028](∅→∅) − 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,
+ );
replacement in importers/amazon/src/digital_items.rs at line 186
[7.24129]→[7.24129:24178](∅→∅) − let currency: &str = &commodity;
+ let currency: &str = &record.our_price_tax_currency_code;
edit in importers/amazon/src/digital_items.rs at line 193
+ .set_payee(record.seller_of_record)
edit in importers/amazon/src/digital_items.rs at line 229
+
+ #[derive(Clone, Copy, Debug, Deserialize)]
+ #[serde(rename_all = "PascalCase")]
+ pub struct Record<'r> {
+ #[serde(rename = "ASIN")]
+ asin: &'r str,
edit in importers/amazon/src/digital_items.rs at line 236
+ fulfilled_date: Option<Date>,
+
+ order_date: Date,
+
+ order_id: &'r str,
+
+ our_price_tax: Decimal,
+
+ our_price_tax_currency_code: Commodity,
+
+ seller_of_record: &'r str,
+
+ #[serde(rename = "Title")]
+ title: &'r str,
+ }
+
edit in importers/amazon/src/digital_items.rs at line 291
[7.26664]→[7.26664:26830](∅→∅) − }
−
− fn parse_order_date(record: &csv::Record) -> Result<Date, Error> {
− const ORDER_DATE_COLUMN_INDEX: usize = 9;
−
− parse_date(&record[ORDER_DATE_COLUMN_INDEX])
edit in importers/amazon/src/digital_items.rs at line 292
[7.26832]→[7.26832:27088](∅→∅) −
− fn parse_date(input: &str) -> Result<Date, Error> {
− use time::format_description::FormatItem;
−
− const ORDER_DATE_FORMAT: &[FormatItem] = format_description!("[year]-[month]-[day]");
−
− Date::parse(input, ORDER_DATE_FORMAT).map_err(|_| todo!())
− }
file addition: serde.rs (---r------)
+ 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>
+ where
+ D: 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>
+ where
+ E: serde::de::Error,
+ {
+ Ok(None)
+ }
+
+ fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+ where
+ D: 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>
+ where
+ D: 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>
+ where
+ E: serde::de::Error,
+ {
+ Ok(None)
+ }
+
+ fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
+ where
+ D: 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>
+ where
+ D: 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>
+ where
+ E: 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>
+ where
+ D: 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>
+ where
+ E: serde::de::Error,
+ {
+ crate::parse(v).map_err(E::custom)
+ }
+ }
+
+ deserializer.deserialize_str(Visitor)
+ }
edit in common/german-decimal/src/lib.rs at line 5
replacement in common/german-decimal/Cargo.toml at line 4
[7.3259]→[7.3259:3309](∅→∅) − edition.workspace = true
− publish.workspace = true
+ edition.workspace = true
+ publish.workspace = true
replacement in common/german-decimal/Cargo.toml at line 7
[7.3339]→[7.3339:3364](∅→∅) − version.workspace = true
+ version.workspace = true
edit in common/german-decimal/Cargo.toml at line 12
replacement in common/german-decimal/Cargo.toml at line 16
− proptest.workspace = true
+ proptest.workspace = true
edit in Cargo.toml at line 47
edit in Cargo.toml at line 102
+
+ [workspace.dependencies.iso_country]
+ features = ["serde"]
+ version = "0.1.4"
edit in Cargo.toml at line 107
+ [workspace.dependencies.iso_currency]
+ features = ["with-serde"]
+ version = "0.4.4"
+
replacement in Cargo.toml at line 116
[7.2453]→[7.24983:25046](∅→∅),
[7.25046]→[6.1083:1103](∅→∅) − features = ["formatting", "local-offset", "macros", "parsing"]
− version = "0.3.28"
+ features = [
+ "formatting",
+ "local-offset",
+ "macros",
+ "parsing",
+ "serde-human-readable",
+ ]
+ version = "0.3.28"
edit in Cargo.lock at line 135
+ "iso_country",
+ "iso_currency",
edit in Cargo.lock at line 482
edit in Cargo.lock at line 928
edit in Cargo.lock at line 1361
edit in Cargo.lock at line 2104
edit in Cargo.lock at line 2114
edit in Cargo.lock at line 4128
edit in Cargo.lock at line 4223
edit in Cargo.lock at line 4225