Skip to content

Commit

Permalink
refactor config queries into ImporterConfig
Browse files Browse the repository at this point in the history
- move matching methods into ImporterConfig
- try to avoid code duplication between importers

Signed-off-by: Peter Nirschl <peter.nirschl@gmail.com>
  • Loading branch information
petermax2 committed Jun 24, 2024
1 parent eea3e5f commit 2b76dd1
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 311 deletions.
136 changes: 136 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{
importers::revolut::RevolutConfig,
};
use homedir::get_my_home;
use regex::RegexBuilder;
use serde::Deserialize;
use std::str::FromStr;

Expand Down Expand Up @@ -59,6 +60,129 @@ impl ImporterConfig {
Err(_) => Err(ImportError::ConfigRead(path)),
}
}

pub fn identify_iban_opt(&self, iban: &Option<String>) -> Option<ImporterConfigTarget> {
match iban {
Some(iban) => self.identify_iban(iban),
None => None,
}
}

pub fn identify_iban(&self, iban: &str) -> Option<ImporterConfigTarget> {
self.ibans
.iter()
.find(|rule| rule.iban == iban)
.map(|rule| ImporterConfigTarget {
account: rule.account.clone(),
note: rule.note.clone(),
})
}

pub fn identify_card_opt(&self, card_number: &Option<String>) -> Option<ImporterConfigTarget> {
match card_number {
Some(card_number) => self.identify_card(card_number),
None => None,
}
}

pub fn identify_card(&self, card_number: &str) -> Option<ImporterConfigTarget> {
self.cards
.iter()
.find(|rule| rule.card == card_number)
.map(|rule| ImporterConfigTarget {
account: rule.account.clone(),
note: rule.note.clone(),
})
}

pub fn match_category(&self, category: &str) -> Option<ImporterConfigTarget> {
self.categories
.iter()
.find(|rule| category.contains(&rule.pattern))
.map(|rule| ImporterConfigTarget {
account: rule.account.clone(),
note: rule.note.clone(),
})
}

pub fn match_sepa_creditor_opt(
&self,
sepa_creditor_id: &Option<String>,
) -> Option<ImporterConfigTarget> {
match sepa_creditor_id {
Some(sepa_creditor_id) => self.match_sepa_creditor(sepa_creditor_id),
None => None,
}
}

pub fn match_sepa_creditor(&self, sepa_creditor_id: &str) -> Option<ImporterConfigTarget> {
self.sepa
.creditors
.iter()
.find(|rule| rule.creditor_id == sepa_creditor_id)
.map(|rule| ImporterConfigTarget {
account: rule.account.clone(),
note: rule.note.clone(),
})
}

pub fn match_sepa_mandate_opt(
&self,
sepa_mandate_id: &Option<String>,
) -> Option<ImporterConfigTarget> {
match sepa_mandate_id {
Some(sepa_mandate_id) => self.match_sepa_mandate(sepa_mandate_id),
None => None,
}
}

pub fn match_sepa_mandate(&self, sepa_mandate_id: &str) -> Option<ImporterConfigTarget> {
self.sepa
.mandates
.iter()
.find(|rule| rule.mandate_id == sepa_mandate_id)
.map(|rule| ImporterConfigTarget {
account: rule.account.clone(),
note: rule.note.clone(),
})
}

pub fn match_mapping_opt(
&self,
field: &Option<String>,
) -> Result<Option<ImporterConfigTarget>> {
match field {
Some(field) => self.match_mapping(field),
None => Ok(None),
}
}

pub fn match_mapping(&self, field: &str) -> Result<Option<ImporterConfigTarget>> {
for rule in &self.mapping {
if rule.matches(field)? {
return Ok(Some(ImporterConfigTarget {
account: rule.account.clone(),
note: rule.note.clone(),
}));
}
}
Ok(None)
}

pub fn fallback(&self) -> Option<ImporterConfigTarget> {
self.fallback_account
.as_ref()
.map(|fallback| ImporterConfigTarget {
account: fallback.clone(),
note: None,
})
}
}

#[derive(Debug)]
pub struct ImporterConfigTarget {
pub account: String,
pub note: Option<String>,
}

#[derive(Debug, Deserialize, PartialEq, Eq)]
Expand Down Expand Up @@ -130,6 +254,18 @@ pub struct SimpleMapping {
pub note: Option<String>,
}

impl SimpleMapping {
pub fn matches(&self, field: &str) -> Result<bool> {
let regex = RegexBuilder::new(&self.search)
.case_insensitive(true)
.build();
match regex {
Ok(regex) => Ok(!field.is_empty() && regex.is_match(field)),
Err(e) => Err(ImportError::Regex(e.to_string())),
}
}
}

/// Represents a more complex mapping that enables the importer to post to different accounts,
/// depending on the given transaction
#[derive(Debug, Deserialize, PartialEq, Eq)]
Expand Down
85 changes: 24 additions & 61 deletions src/importers/cardcomplete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use bigdecimal::BigDecimal;
use chrono::NaiveDate;
use fast_xml::de::from_reader;
use fast_xml::DeError;
use regex::RegexBuilder;
use serde::Deserialize;

use crate::config::ImporterConfig;
Expand Down Expand Up @@ -102,83 +101,47 @@ struct CCTransaction {

impl CCTransaction {
pub fn into_hledger(self, config: &ImporterConfig) -> Result<Transaction> {
let mut note = None;
let mut postings = Vec::new();

let posting_date = self.posting_date()?;
let postings = self.postings(config)?;
let tags = self.tags()?;
let state = self.state();

Ok(Transaction {
date: posting_date,
code: None,
payee: self.merchant_name,
note: None,
state,
comment: None,
tags,
postings,
})
}

pub fn postings(&self, config: &ImporterConfig) -> Result<Vec<Posting>> {
let mut postings = Vec::new();

let account = if let Some(card_number) = &self.card_number {
config
.cards
.iter()
.find(|card| &card.card == card_number)
.map(|mapping| mapping.account.clone())
} else {
None
};

if let Some(account) = account {
let amount = self.amount()?;
let own_target = config.identify_card_opt(&self.card_number);
if let Some(own_target) = own_target {
note.clone_from(&own_target.note);
postings.push(Posting {
account,
amount: Some(amount),
account: own_target.account,
amount: Some(self.amount()?),
comment: None,
tags: Vec::new(),
});
}

let mut other_account = None;

for rule in &config.mapping {
let regex = RegexBuilder::new(&rule.search)
.case_insensitive(true)
.build();
match regex {
Ok(regex) => {
if regex.is_match(&self.merchant_name) {
other_account = Some(rule.account.clone());
break;
}
}
Err(e) => return Err(ImportError::Regex(e.to_string())),
}
}

if other_account.is_none() {
other_account = config
.categories
.iter()
.find(|rule| self.category.contains(&rule.pattern))
.map(|rule| rule.account.clone());
}

// TODO these queries need refactoring...

if let Some(account) = other_account {
let other_target = config
.match_mapping(&self.merchant_name)?
.or(config.match_category(&self.category));
if let Some(other_target) = other_target {
note.clone_from(&other_target.note);
postings.push(Posting {
account,
account: other_target.account,
amount: None,
comment: None,
tags: Vec::new(),
});
}

Ok(postings)
Ok(Transaction {
date: posting_date,
code: None,
payee: self.merchant_name,
note,
state,
comment: None,
tags,
postings,
})
}

pub fn tags(&self) -> Result<Vec<Tag>> {
Expand Down
Loading

0 comments on commit 2b76dd1

Please sign in to comment.