parse 2 year's worth of transactions

This commit is contained in:
2025-10-16 22:11:05 +02:00
parent 900c9872e1
commit 1dcf8207aa
6 changed files with 724 additions and 827 deletions

View File

@@ -1,28 +1,484 @@
use crate::{Account, Transaction, TransactionDetails};
use chrono::{Datelike, NaiveDate};
#[derive(Debug)]
pub enum Category {
Zero815,
Book,
Aliexpress,
Willhaben,
Amazon,
Transfer,
Post,
Cloudways,
Transport(Transport),
Donation(Donation),
Groceries(Groceries),
Sport(Sport),
Fiverr,
Accomodation,
Rent,
Grundbuchauszug,
PhoneContract,
Soda,
Server,
Mailgun,
Movie,
Restaurant(Restaurant),
Tech(Tech),
Membership(String),
Oeticket,
Household(String),
Obi,
XXXLutz,
Gift,
CoffeeToGo,
Music,
Form(String),
Travel(Travel),
Misc(String),
Hardware(Hardware),
}
pub fn cat(desc: &str) -> Category {
if desc.starts_with("From") && desc.contains(" to ") {
return Category::Transfer;
}
if desc == "Investing" {
return Category::Transfer;
}
if desc == "Main Account" {
return Category::Transfer;
}
if desc == "Backup" {
return Category::Transfer;
}
if desc == "N26 Migration" {
return Category::Transfer;
}
#[derive(Debug)]
pub enum Hardware {
Laptop,
Remarkable,
NUC,
}
if desc == "AMAZON PAYMENTS EUROPE S.C.A." {
impl From<Hardware> for Category {
fn from(value: Hardware) -> Self {
Self::Hardware(value)
}
}
#[derive(Debug)]
pub enum Travel {
eSIM,
}
impl From<Travel> for Category {
fn from(value: Travel) -> Self {
Self::Travel(value)
}
}
#[derive(Debug)]
pub enum Tech {
LLM,
MediaMarkt,
Kagi,
Grammarly,
Komoot,
Todoist,
}
impl From<Tech> for Category {
fn from(value: Tech) -> Self {
Self::Tech(value)
}
}
#[derive(Debug)]
pub enum Restaurant {
Proper,
FastFood,
Mensa,
Snack,
}
impl From<Restaurant> for Category {
fn from(value: Restaurant) -> Self {
Self::Restaurant(value)
}
}
#[derive(Debug)]
pub enum Sport {
Squash,
SportOkay,
Hervis,
Wings4Life,
}
impl From<Sport> for Category {
fn from(value: Sport) -> Self {
Self::Sport(value)
}
}
#[derive(Debug)]
pub enum Groceries {
Hofer,
Lidl,
Billa,
Spar,
Penny,
Unimarkt,
Bakery,
}
impl From<Groceries> for Category {
fn from(value: Groceries) -> Self {
Self::Groceries(value)
}
}
#[derive(Debug)]
pub enum Transport {
LinzLinien,
Nextbike,
Train,
Flight,
Gas,
Bike,
}
impl From<Transport> for Category {
fn from(value: Transport) -> Self {
Self::Transport(value)
}
}
#[derive(Debug)]
pub enum Donation {
RemarkableRCU,
Signal,
Wikipedia,
}
impl From<Donation> for Category {
fn from(value: Donation) -> Self {
Self::Donation(value)
}
}
pub fn cat(details: &TransactionDetails) -> Category {
if details.account == Account::N26
&& (details.desc == "Hofer Philipp"
|| details.desc == "Hofer Philipp"
|| details.desc == "Philipp Hofer")
{
// Einzahlung auf N26
return Category::Transfer;
}
if details.account == Account::N26 && details.desc == "Instant Savings" {
return Category::Transfer;
}
if details.desc == "CityBikeLinz" {
return Transport::Nextbike.into();
}
if details.desc == "SIGNAL FOUNDATION" {
return Donation::Signal.into();
}
if details.desc.starts_with("OEBB")
|| details.desc == "EUROSTAR INTERNATIONAL"
|| details.desc == "westbahn.at"
{
return Transport::Train.into();
}
if details.desc == "DAVIS REMMEL" {
return Donation::RemarkableRCU.into();
}
if details.desc == "Front Food" {
return Restaurant::FastFood.into();
}
if details.date.year() == 2024 && details.desc == "Peek &amp; Cloppenburg" {
return Category::Household("Hemd".into());
}
if details.desc.starts_with("LIBRO FIL.") {
return Category::Household("Libro".into());
}
if details.desc == "Hofer Dankt" || details.desc == "HOFER ONLINE-SHOP" {
return Groceries::Hofer.into();
}
if details.desc == "Great" {
return Restaurant::FastFood.into();
}
if details.desc == "LPD Wien Strafregister" {
return Category::Form("Strafregisterauszug".into());
}
if details.desc.starts_with("Spar Fil. ") || details.desc.starts_with("Spar Dankt") {
return Groceries::Spar.into();
}
if details.desc == "Unimarkt" {
return Groceries::Unimarkt.into();
}
if details.desc == "Angkoon Thai Restauran" {
return Restaurant::Proper.into();
}
if details.desc == "EVERSPORTS* F10 SPORTF" || details.desc == "EVERSPORTS* F10 SPORTS" {
return Sport::Squash.into();
}
if details.desc == "SINCH MAILGUN" {
return Category::Mailgun.into();
}
if details.desc == "Indigo GmbH" {
return Restaurant::Proper.into();
}
if details.desc == "SP NAEVEGANSHOES" {
return Category::Household("Schuhe".into());
}
if details.desc == "THE ECONOMIST NEWSPAPE" {
return Category::Book.into();
}
if details.desc == "WIST OO" {
return Category::Rent;
}
if details.desc == "0815 Online Handel" {
return Category::Zero815;
}
if details.desc == "HOT TELEKOM" {
return Category::PhoneContract;
}
if details.desc.starts_with("FSA") {
return Transport::LinzLinien.into();
}
if details.desc == "bank99 AG" || details.desc.starts_with("Post ") {
return Category::Post;
}
if details.desc == "COCA COLA HBC AUSTRIA" || details.desc == "Coca-Cola HBC Austria" {
return Category::Soda;
}
if details.desc == "RESCH & FRISCH-EINZELH" {
return Groceries::Bakery.into();
}
if details.desc == "Uni Pizza" || details.desc.starts_with("McDonalds ") {
return Restaurant::FastFood.into();
}
if details.desc == "BlochbergerEisproduktG" {
return Restaurant::Snack.into();
}
if details.desc == "OSTERREICHISCHER ALPENVEREIN Alpenverein Linz" {
return Category::Membership("Alpenverein".into());
}
if details.desc == "OEAMTC Linz 4500" {
return Category::Membership("ÖAMTC".into());
}
if details.desc == "OTT* BARKLEYMOVIE" {
return Category::Movie;
}
if details.desc == "BMJ_justizonline.gv.at" {
return Category::Grundbuchauszug;
}
if details.desc == "CHATGPT SUBSCRIPTION"
|| details.desc == "OPENAI *CHATGPT SUBSCR"
|| details.desc == "CLAUDE.AI SUBSCRIPTION"
|| details.desc == "ANTHROPIC"
{
return Tech::LLM.into();
}
if details.desc == "Media Markt" || details.desc == "MEDIAMARKT MARKETPLACE" {
return Tech::MediaMarkt.into();
}
if details.desc == "MENSA LINZ"
|| details.desc == "KHG Mensa"
|| details.desc == "JULIUS RAAB MENSA"
|| details.desc == "SCIENCE CAFE LINZ"
{
return Restaurant::Mensa.into();
}
if details.desc == "BKG*HOTEL AT BOOKING.C"
|| details.desc == "huetten-holiday.de"
|| details.desc == "BKG*BOOKING.COM HOTEL"
|| details.desc == "BOOKING.COM"
|| details.desc == "Booking.com Hotel"
|| details.desc.starts_with("Ibis Styles")
{
return Category::Accomodation;
}
if details.desc == "SIVERS.COM" {
return Category::Book;
}
if details.desc == "willhaben PayLivery" {
return Category::Willhaben;
}
if details.desc.starts_with("PENNY DANKT") {
return Groceries::Penny.into();
}
if details.desc.starts_with("www.lampenwelt.at") {
return Category::Household("Lampe".into());
}
if details.desc.starts_with("AMZN ")
|| details.desc.starts_with("Amazon.de")
|| details.desc.starts_with("AMAZON*")
|| details.desc.starts_with("WWW.AMAZON.*")
{
return Category::Amazon;
}
if details.desc == "OETICKET.COM" {
return Category::Oeticket;
}
if details.desc == "Sparkasse Oberösterrei" {
return Category::Transfer;
}
if details.desc == "WWW.KNIFESTOCK.SK" {
return Category::Household("Schleifstein".into());
}
if details.desc == "Bike24 GmbH" {
return Transport::Bike.into();
}
if details.desc.starts_with("DM-Fil. ") {
return Category::Household("DM".into());
}
if details.desc == "Jysk GmbH" {
return Category::Household("Jysk".into());
}
if details.desc == "OBI Bau- und Heimwerke"
|| details.desc == "OBI SAGT DANKE"
|| details.desc == "OBI Home + Garden GmbH"
{
return Category::Obi;
}
if details.desc.starts_with("XXXLUTZ ") {
return Category::XXXLutz;
}
if details.desc.starts_with("MISTER MINIT") {
return Category::Household("Mister Minit".into());
}
if details.desc == "Sport-Ski Willy" {
return Category::Household("Skiwachs-Zeug".into());
}
if details.desc == "BANDCAMP DEATHWISH INC" {
return Category::Music;
}
if details.desc.starts_with("SHELL ")
|| details.desc.starts_with("Eni ")
|| details.desc.starts_with("DISKONT ")
{
return Transport::Gas.into();
}
if details.desc == "LINDE VERLAG" {
return Category::Book;
}
if details.desc.starts_with("Lidl DANKT") {
return Groceries::Lidl.into();
}
if details.desc.starts_with("BILLA ") {
return Groceries::Billa.into();
}
if details.desc == "RCH-KAGI.COM" {
return Tech::Kagi.into();
}
if details.desc == "TODOIST" {
return Tech::Todoist.into();
}
if details.desc == "Semmering Hirschenkoge" && details.date.year() == 2025 {
return Category::Gift.into();
}
if details.desc.starts_with("Thalia.at") {
return Category::Book;
}
if details.desc == "WASHCOMPLETE.AT" || details.desc == "Miele Operations Pay" {
return Category::Household("Waschkarte".into());
}
if details.desc == "ELLA BARFUSSSCHUHE" {
return Category::Household("Schuhe".into());
}
if details.desc == "AIRALO" {
return Travel::eSIM.into();
}
if details.desc == "Autobahnrasthaus Chiem" || details.desc == "Cafe+Co AT 311530" {
return Category::CoffeeToGo;
}
if details.desc.starts_with("Bäckerei ")
|| details.desc.starts_with("Baeckerei ")
|| details.desc == "HONEDER NATURBACKSTU"
|| details.desc == "ANKER HBHF SALZBURG"
{
return Groceries::Bakery.into();
}
if details.desc.starts_with("Die Obelisk") {
return Restaurant::FastFood.into();
}
if details.desc.starts_with("Sport Okay") {
return Sport::SportOkay.into();
}
if details.desc == "Hervis Sport und Mode" {
return Sport::Hervis.into();
}
if details.desc == "Mol*Moviemento Program" {
return Category::Movie;
}
if details.desc == "GRUNDSTOFF" {
return Category::Household("Leiberl".into());
}
if details.desc == "Wings for Life World R" {
return Sport::Wings4Life.into();
}
if details.desc.starts_with("GRAMMARLY ") {
return Tech::Grammarly.into();
}
if details.desc == "KOMOOT GMBH" {
return Tech::Komoot.into();
}
if details.desc == "Grüne Papaya"
|| details.desc == "Mr. Wen - Asia Food -"
|| details.desc == "Mr Wen"
|| details.desc == "LOsteria Linz"
{
return Restaurant::Proper.into();
}
if details.desc == "REMARKABLE" {
return Hardware::Remarkable.into();
}
if details.desc == "Bookbot" {
return Category::Book.into();
}
if details.desc == "DONAU VERSICHERUNG" {
return Category::Membership("Haftpflichtversicherung".into());
}
if details.desc == "IONOS SE"
|| details.desc == "EASYNAME COM"
|| details.desc == "DIGITALOCEAN.COM"
|| details.desc == "PORKBUN.COM"
{
return Category::Server;
}
if details.desc == "Alois Dallmayr" || details.desc == "Automaten-Service GmbH" {
return Category::CoffeeToGo.into();
}
if details.desc.starts_with("FRAMEWORK* ") {
return Hardware::Laptop.into();
}
if details.desc.starts_with("RYANAIR") {
return Transport::Flight.into();
}
if details.desc == "SPC*sedruck KG" || details.desc == "NYX*Kuario" {
return Category::Misc("Druck".into());
}
if details.desc.starts_with("Pizzeria ") {
return Restaurant::Proper.into();
}
if details.desc.starts_with("SP JUSTIN JOHNSON MU") {
return Category::Music;
}
if details.date == NaiveDate::from_ymd_opt(2024, 10, 10).unwrap()
&& details.desc == "e-tec electronic"
{
return Hardware::NUC.into();
}
if details.date.year() == 2024 && details.desc == "MyLemon" {
return Hardware::NUC.into();
}
if details.desc == "ALIEXPRESS.COM" {
return Category::Aliexpress;
}
if details.date.year() == 2024 && details.desc == "1ASHOP.AT" {
return Hardware::NUC.into();
}
if details.date.year() == 2024 && details.desc == "HISTORIA" {
return Category::Gift;
}
todo!("Handle '{desc}'")
todo!("Handle '{details:#?}'")
}
impl From<TransactionDetails> for Transaction {
fn from(details: TransactionDetails) -> Self {
let cat = cat(&details);
Self { details, cat }
}
}

View File

@@ -1,11 +1,23 @@
pub mod classifier;
use chrono::{DateTime, NaiveDate, Utc};
use classifier::Category;
use crate::classifier::Category;
use chrono::NaiveDate;
pub struct Transaction {
#[derive(Debug, PartialEq)]
pub enum Account {
N26,
}
#[derive(Debug)]
pub struct TransactionDetails {
pub date: NaiveDate,
pub amount_in_cent: i64,
pub desc: String,
pub account: Account,
}
#[derive(Debug)]
pub struct Transaction {
pub details: TransactionDetails,
pub cat: Category,
}

View File

@@ -1,7 +1,9 @@
use chrono::{Date, DateTime, NaiveDate};
use chrono::NaiveDate;
use csv::Reader;
use fin::{Account, TransactionDetails};
use serde::Deserialize;
use std::fs::File;
use tracing::info;
#[derive(Debug, Deserialize)]
pub struct Transaction {
@@ -46,25 +48,28 @@ impl From<Transaction> for fin::Transaction {
value.payment_reference
};
let cat = fin::classifier::cat(&desc);
Self {
let details = TransactionDetails {
date,
amount_in_cent,
desc,
cat,
}
account: Account::N26,
};
details.into()
}
}
impl Transaction {
pub fn from_file(path: &str) -> Vec<fin::Transaction> {
info!("Parsing {path}...");
let mut ret = Vec::new();
let file = File::open(path).unwrap();
let mut rdr = Reader::from_reader(file);
for result in rdr.deserialize::<Transaction>() {
for (idx, result) in rdr.deserialize::<Transaction>().enumerate() {
println!("{idx}");
let tx: Transaction = result.unwrap();
println!("{tx:#?}");
ret.push(tx.into());
}