-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add .env options for debt tracking * Add loaded debt parameters to Config * Create strategies skeleton * Create standard strategy * Add splitwise balance strategy * Remove redundant option * Import and use strategy classes * Use paid amount instead of owed on flag * Modify processExpense to use correct strategy * Actually use paid amount on flag * Add strategy testing skeleton * Remove redundant option from main * Convert str to float for comparison * minor strategy tests changes * test bugfix * Fix processExpense logic * Remove redundant patch * minor bugfixes * Remove use_payed_amount option from processExpense Handle it as one of the arguments to applyExpenseAmountToTransaction * Use applyExpenseAmountToTransaction in SW balance strategy * Correctly make a deposit to the balance account Make a deposit to the SW balance account of amount "balance" when an expense entails people owe me money, instead of a transfer from the paying account to the SW balance, which doesn't make sense. * bug: handle deposit type txns If type is "deposit", the asset account is the *destination*, not the *source*. * convert str to float for taking negative * handle expenses by others If I payed nothing on an expense, don't make a transaction from my real accounts, only from the balance account. * unify comment style * refactor for clarity explicitly handle expenses where I paid nothing but I still owe something, and the edge case where an expense was recorded but I paid my whole share. * flatten dictionary to comply with signature * clarify amount application method usage Remove ability to pass ExpenseUser to get amount, pass amount explicitly. Rename method to clarify usage. * minor refactor * fix test_sw_balance_strategy Correctly use mock_apply_transaction_amount * Make test robust to trailing zeros * Disable splitwise balance feature in test * Extend README to describe debt tracking
- Loading branch information
Showing
9 changed files
with
248 additions
and
32 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
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,8 @@ | ||
from abc import ABC, abstractmethod | ||
from splitwise import Expense | ||
from splitwise.user import ExpenseUser | ||
|
||
class TransactionStrategy(ABC): | ||
@abstractmethod | ||
def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str]) -> list[dict]: | ||
pass |
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,10 @@ | ||
from .base import TransactionStrategy | ||
from splitwise import Expense | ||
from splitwise.user import ExpenseUser | ||
|
||
class StandardTransactionStrategy(TransactionStrategy): | ||
def __init__(self, get_expense_transaction_body) -> None: | ||
self._get_expense_transaction_body = get_expense_transaction_body | ||
|
||
def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str]) -> list[dict]: | ||
return [self._get_expense_transaction_body(exp, myshare, data)] |
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,34 @@ | ||
from .base import TransactionStrategy | ||
from splitwise import Expense | ||
from splitwise.user import ExpenseUser | ||
|
||
class SWBalanceTransactionStrategy(TransactionStrategy): | ||
def __init__(self, get_expense_transaction_body, sw_balance_account, apply_transaction_amount) -> None: | ||
self._get_expense_transaction_body = get_expense_transaction_body | ||
self._sw_balance_account = sw_balance_account | ||
self._apply_transaction_amount = apply_transaction_amount | ||
|
||
def create_transactions(self, exp: Expense, myshare: ExpenseUser, data: list[str]) -> list[dict]: | ||
txns = {} | ||
paid_txn = self._get_expense_transaction_body(exp, myshare, data) | ||
paid_txn = self._apply_transaction_amount(paid_txn, exp, myshare.getPaidShare()) | ||
if float(paid_txn['amount']) != 0: # I paid; payment txn needed | ||
txns['paid'] = paid_txn | ||
|
||
balance_txn = paid_txn.copy() | ||
balance = float(myshare.getNetBalance()) | ||
if balance != 0: # I owe or am owed; balance txn needed | ||
txns['balance'] = balance_txn | ||
if balance > 0: # I am owed; difference credited to balance account | ||
balance_txn['source_name'] = self._sw_balance_account + " balancer" | ||
balance_txn['destination_name'] = self._sw_balance_account | ||
balance_txn['type'] = 'deposit' | ||
balance_txn['description'] = f"Balance transfer for: {paid_txn['description']}" | ||
balance_txn = self._apply_transaction_amount(balance_txn, exp, balance) | ||
else: # I owe; difference debited from balance account | ||
balance_txn['source_name'] = self._sw_balance_account | ||
balance_txn['destination_name'] = paid_txn['destination_name'] | ||
balance_txn['type'] = "withdrawal" | ||
balance_txn['description'] = f"Balance transfer for: {paid_txn['description']}" | ||
balance_txn = self._apply_transaction_amount(balance_txn, exp, -balance) | ||
return list(txns.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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import pytest | ||
from datetime import datetime | ||
from unittest.mock import Mock, patch | ||
from strategies.standard import StandardTransactionStrategy | ||
from strategies.sw_balance import SWBalanceTransactionStrategy | ||
from splitwise import Expense | ||
from splitwise.user import ExpenseUser | ||
|
||
# Mock objects | ||
mock_expense = Mock(spec=Expense) | ||
mock_expense.getId.return_value = "123" | ||
mock_expense.getDescription.return_value = "Test Expense" | ||
mock_expense.getCurrencyCode.return_value = "USD" | ||
mock_expense.getDate.return_value = "2023-05-01" | ||
mock_expense.getCreatedAt.return_value = "2023-05-01T12:00:00Z" | ||
|
||
mock_user = Mock(spec=ExpenseUser) | ||
mock_user.getId.return_value = "456" | ||
mock_user.getOwedShare.return_value = "60.00" | ||
mock_user.getPaidShare.return_value = "110.00" | ||
mock_user.getNetBalance.return_value = "50.00" | ||
|
||
# Mock getExpenseTransactionBody function | ||
def mock_get_expense_transaction_body(exp, myshare, data): | ||
amount = myshare.getOwedShare() | ||
return { | ||
"amount": amount, | ||
"description": exp.getDescription(), | ||
"date": exp.getDate(), | ||
"source_name": "Test Source", | ||
"destination_name": "Test Destination", | ||
"category_name": "Test Category", | ||
"type": "withdrawal", | ||
} | ||
|
||
def mock_apply_transaction_amount(txn, exp, amount): | ||
txn['amount'] = str(amount) | ||
return txn | ||
|
||
# Tests for StandardTransactionStrategy | ||
def test_standard_strategy(): | ||
strategy = StandardTransactionStrategy(mock_get_expense_transaction_body) | ||
transactions = strategy.create_transactions(mock_expense, mock_user, []) | ||
|
||
assert len(transactions) == 1 | ||
assert transactions[0]["amount"] == "60.00" | ||
assert transactions[0]["description"] == "Test Expense" | ||
|
||
# Tests for SWBalanceTransactionStrategy | ||
def test_sw_balance_strategy(): | ||
strategy = SWBalanceTransactionStrategy(mock_get_expense_transaction_body, "Splitwise Balance", mock_apply_transaction_amount) | ||
transactions = strategy.create_transactions(mock_expense, mock_user, []) | ||
|
||
assert len(transactions) == 2 | ||
assert transactions[0]["amount"] == "110.00" | ||
assert transactions[0]["description"] == "Test Expense" | ||
assert float(transactions[1]["amount"]) == float("50.00") | ||
assert transactions[1]["type"] == "deposit" | ||
assert transactions[1]["destination_name"] == "Splitwise Balance" | ||
|
||
# Test for processExpense function | ||
@patch('main.getDate') | ||
@patch('main.get_transaction_strategy') | ||
@patch('main.updateTransaction') | ||
@patch('main.addTransaction') | ||
@patch('main.searchTransactions') | ||
@patch('main.getSWUrlForExpense') | ||
def test_process_expense(mock_get_url, mock_search, mock_add, mock_update, mock_get_strategy, mock_get_date): | ||
from main import processExpense, Config | ||
|
||
# Mock configuration | ||
mock_config = Mock(spec=Config) | ||
mock_config.SW_BALANCE_ACCOUNT = "Splitwise Balance" | ||
|
||
mock_get_date.return_value = datetime.now().astimezone() # Make sure expense registers as new and not updated | ||
|
||
# Set up mock strategy | ||
mock_strategy = Mock() | ||
mock_strategy.create_transactions.return_value = [ | ||
{"amount": "110.00", "description": "Test Expense"}, | ||
{"amount": "50.00", "description": "Balance transfer for: Test Expense"} | ||
] | ||
mock_get_strategy.return_value = mock_strategy | ||
|
||
# Set up other mocks | ||
mock_get_url.return_value = "http://example.com/expense/123" | ||
mock_search.return_value = [] | ||
|
||
# Call processExpense | ||
processExpense(datetime.now().astimezone(), {}, mock_expense, mock_user, []) | ||
|
||
# Assertions | ||
assert mock_strategy.create_transactions.called | ||
assert mock_add.call_count == 2 | ||
assert mock_update.call_count == 0 | ||
mock_add.assert_any_call({"amount": "110.00", | ||
"description": "Test Expense", | ||
"external_url": "http://example.com/expense/123"}) | ||
mock_add.assert_any_call({"amount": "50.00", | ||
"description": 'Balance transfer for: Test Expense', | ||
"external_url": 'http://example.com/expense/123-balance_transfer-1'}) | ||
|
||
# Test for get_transaction_strategy function | ||
@patch('requests.request') | ||
def test_get_transaction_strategy(mock_request): | ||
mock_request.return_value.json.return_value = {'data': []} | ||
from main import get_transaction_strategy, Config | ||
|
||
# Test with SW_BALANCE_ACCOUNT = False | ||
with patch.dict('main.conf', {'SW_BALANCE_ACCOUNT': ''}): | ||
strategy = get_transaction_strategy() | ||
assert isinstance(strategy, StandardTransactionStrategy) | ||
|
||
# Test with SW_BALANCE_ACCOUNT = True | ||
with patch.dict('main.conf', {'SW_BALANCE_ACCOUNT': 'Splitwise Balance'}): | ||
strategy = get_transaction_strategy() | ||
assert isinstance(strategy, SWBalanceTransactionStrategy) |
902951e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coverage Report
902951e
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coverage Report