-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Given transactions and supported categories it is possible to analyze transactions. The result are both: - matched categories with summed transaction values - unmatched transactions that still need manual classification
- Loading branch information
Showing
11 changed files
with
425 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,21 @@ | ||
from banker.analyzer.analyze import analyze_transactions | ||
from banker.data.category import Category, PaymentType | ||
import argparse | ||
|
||
from banker.parser.html_transactions_parser import HtmlTransactionsParser | ||
|
||
|
||
def main(): | ||
print("Hello world from Banker!") | ||
supported_categories = [ | ||
Category(name="Kaufland", payment_type=PaymentType.Household, matching_regexes=[r"KAUFLAND PL"])] | ||
transactions_parser = HtmlTransactionsParser() | ||
|
||
parser = argparse.ArgumentParser() | ||
parser.add_argument("html_file") | ||
args = parser.parse_args() | ||
|
||
with open(args.html_file, "rb") as file: | ||
all_transactions = transactions_parser.parse_transactions(file.read().decode('utf-8')) | ||
analyze_result = analyze_transactions(all_transactions, supported_categories) | ||
print(analyze_result) | ||
# TODO: format output |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
from dataclasses import dataclass | ||
|
||
from banker.data.category import Category | ||
from banker.data.transaction import Transaction | ||
from logging import getLogger | ||
|
||
analyze_logger = getLogger("Analyze") | ||
|
||
|
||
@dataclass(frozen=True) | ||
class AnalyzeResult: | ||
unmatched_transactions: list[Transaction] | ||
matched_categories: list[Category] | ||
|
||
|
||
def analyze_transactions(transactions: list[Transaction], supported_categories: list[Category]) -> AnalyzeResult: | ||
unmatched_transactions = [] | ||
matched_categories = {} | ||
for transaction in transactions: | ||
matching_categories = transaction.find_matching(supported_categories) | ||
matching_categories_count = len(matching_categories) | ||
if matching_categories_count == 1: | ||
category_name = matching_categories[0].get_name() | ||
matched_category = matched_categories.setdefault(category_name, matching_categories[0]) | ||
matched_category.value += transaction.value | ||
else: | ||
analyze_logger.info(f"Transaction: {transaction} matched to {matching_categories_count} categories") | ||
unmatched_transactions.append(transaction) | ||
return AnalyzeResult(unmatched_transactions=unmatched_transactions, | ||
matched_categories=list(matched_categories.values())) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
from copy import deepcopy | ||
|
||
import pytest | ||
|
||
from moneyed import Money, PLN | ||
|
||
from banker.data.category import Category, PaymentType | ||
from banker.data.transaction import Transaction | ||
from banker.analyzer.analyze import analyze_transactions, AnalyzeResult | ||
|
||
|
||
def make_category_with_value(category: Category, value: Money) -> Category: | ||
category_copy = deepcopy(category) | ||
category_copy.value = value | ||
return category_copy | ||
|
||
|
||
@pytest.mark.parametrize( | ||
'transactions, supported_categories, expected_result', | ||
[ | ||
( | ||
[ | ||
Transaction(date="2023-01-01", value=Money(amount="-11.27", currency=PLN), description="New shoes", | ||
type="Card") | ||
], | ||
[ | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]) | ||
], | ||
AnalyzeResult(unmatched_transactions=[], matched_categories=[ | ||
make_category_with_value( | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]), | ||
Money(amount="-11.27", currency=PLN)) | ||
]) | ||
), | ||
( | ||
[ | ||
Transaction(date="2023-01-01", value=Money(amount="-13.30", currency=PLN), | ||
description="Amazing trekking shoes", type="Card"), | ||
Transaction(date="2023-01-02", value=Money(amount="-16.70", currency=PLN), | ||
description="Casual shoes", type="Card"), | ||
Transaction(date="2023-01-03", value=Money(amount="-20.00", currency=PLN), | ||
description="Dancing shoes", type="Card") | ||
], | ||
[ | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]) | ||
], | ||
AnalyzeResult(unmatched_transactions=[], matched_categories=[ | ||
make_category_with_value( | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]), | ||
Money(amount="-50.00", currency=PLN)) | ||
]) | ||
), | ||
( | ||
[ | ||
Transaction(date="2023-01-01", value=Money(amount="-15.00", currency=PLN), | ||
description="Amazing trekking shoes", type="Card"), | ||
Transaction(date="2023-01-02", value=Money(amount="-33.00", currency=PLN), | ||
description="Cheap shirts", type="Card"), | ||
Transaction(date="2023-01-03", value=Money(amount="-50.00", currency=PLN), | ||
description="Expensive sweets", type="Card") | ||
], | ||
[ | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]), | ||
Category(name="Sweets", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)sweets"]), | ||
Category(name="Shirts", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shirts"]) | ||
], | ||
AnalyzeResult(unmatched_transactions=[], matched_categories=[ | ||
make_category_with_value( | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]), | ||
Money(amount="-15.00", currency=PLN)), | ||
make_category_with_value( | ||
Category(name="Shirts", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shirts"]), | ||
Money(amount="-33.00", currency=PLN)), | ||
make_category_with_value( | ||
Category(name="Sweets", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)sweets"]), | ||
Money(amount="-50.00", currency=PLN)) | ||
]) | ||
), | ||
( | ||
[ | ||
Transaction(date="2023-01-01", value=Money(amount="-11.27", currency=PLN), description="New shoes", | ||
type="Card"), | ||
Transaction(date="2023-01-02", value=Money(amount="-500.00", currency=PLN), description="New game", | ||
type="Card") | ||
], | ||
[ | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]) | ||
], | ||
AnalyzeResult( | ||
unmatched_transactions=[ | ||
Transaction(date="2023-01-02", value=Money(amount="-500.00", currency=PLN), | ||
description="New game", type="Card")], | ||
matched_categories=[ | ||
make_category_with_value( | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]), | ||
Money(amount="-11.27", currency=PLN)) | ||
]) | ||
), | ||
( | ||
[ | ||
Transaction(date="2023-01-01", value=Money(amount="-11.27", currency=PLN), description="New shoes", | ||
type="Card"), | ||
], | ||
[ | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]), | ||
Category(name="New stuff", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)new"]) | ||
], | ||
AnalyzeResult( | ||
unmatched_transactions=[Transaction(date="2023-01-01", value=Money(amount="-11.27", currency=PLN), | ||
description="New shoes", type="Card")], | ||
matched_categories=[]) | ||
), | ||
( | ||
[ | ||
Transaction(date="2023-01-01", value=Money(amount="-11.27", currency=PLN), description="New shoes", | ||
type="Card"), | ||
], | ||
[ | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]), | ||
Category(name="Weirdo", payment_type=PaymentType.Optional, matching_regexes=[r"what?"]) | ||
], | ||
AnalyzeResult( | ||
unmatched_transactions=[], | ||
matched_categories=[ | ||
make_category_with_value( | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]), | ||
Money(amount="-11.27", currency=PLN)) | ||
]) | ||
), | ||
( | ||
[ | ||
Transaction(date="2023-01-01", value=Money(amount="-11.27", currency=PLN), description="New shoes", | ||
type="Card"), | ||
], | ||
[ | ||
Category(name="Weirdo", payment_type=PaymentType.Optional, matching_regexes=[r"what?"]) | ||
], | ||
AnalyzeResult( | ||
unmatched_transactions=[Transaction(date="2023-01-01", value=Money(amount="-11.27", currency=PLN), | ||
description="New shoes", type="Card")], | ||
matched_categories=[]) | ||
), | ||
( | ||
[], | ||
[ | ||
Category(name="Shoes", payment_type=PaymentType.Optional, matching_regexes=[r"(?i)shoes"]), | ||
], | ||
AnalyzeResult( | ||
unmatched_transactions=[], | ||
matched_categories=[]) | ||
), | ||
( | ||
[ | ||
Transaction(date="2023-01-01", value=Money(amount="-11.27", currency=PLN), description="New shoes", | ||
type="Card"), | ||
Transaction(date="2023-01-02", value=Money(amount="-500.00", currency=PLN), description="New game", | ||
type="Card") | ||
], | ||
[], | ||
AnalyzeResult( | ||
unmatched_transactions=[Transaction(date="2023-01-01", value=Money(amount="-11.27", currency=PLN), | ||
description="New shoes", type="Card"), | ||
Transaction(date="2023-01-02", value=Money(amount="-500.00", currency=PLN), | ||
description="New game", type="Card")], | ||
matched_categories=[]) | ||
), | ||
( | ||
[], | ||
[], | ||
AnalyzeResult( | ||
unmatched_transactions=[], | ||
matched_categories=[]) | ||
), | ||
], | ||
ids=["given matching transaction to one category then return matched category with increased value", | ||
"given many matching transactions to one category then return matched category with increased value", | ||
"given many transactions and many categories then return all matched categories with increased values", | ||
"given transaction that does not match then return unmatched transaction", | ||
"given matching transaction to many categories then return unmatched transaction", | ||
"given category that was not matched then exclude it from matched categories", | ||
"given one transaction that does not match then return unmatched transaction and no categories", | ||
"given no transactions then return empty list of both transactions and categories", | ||
"given transactions and empty list of categories then return all transactions as unmatched", | ||
"given empty transactions and empty categories then return empty lists"] | ||
) | ||
def test_given_transactions_and_supported_categories_when_analyze_then_return_result( | ||
transactions, supported_categories, expected_result | ||
): | ||
actual_result = analyze_transactions(transactions, supported_categories) | ||
|
||
assert actual_result == expected_result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Oops, something went wrong.