diff --git a/tests/conftest.py b/tests/conftest.py index 64f590d..512480e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,7 +40,8 @@ def mock_original_transaction(request): import_payee_name_original='ipno', transaction_date=date(2024, 1, 1), approved=False, - cleared='uncleared') + cleared='uncleared', + transfer_transaction_id=None) @pytest.fixture @@ -48,3 +49,11 @@ def mock_category_repo(): return CategoryRepo(categories=[ CategoryGroup(name='group1', categories=frozenset([Category(id='cid1', name='c_name')])), CategoryGroup(name='group2', categories=frozenset([Category(id='cid2', name='c_name')]))]) + + +@pytest.fixture +def mock_transaction_dict(): + return dict(id='id', amount=1000, date='2024-01-01', category_name='category', category_id='categoryid', + payee_name='payee', payee_id='payeeid', flag_color=None, memo=None, subtransactions=[], + import_payee_name_original=None, import_payee_name=None, transfer_account_id='transfer_account_id', + approved=False, cleared='uncleared', transfer_transaction_id='transfer_transaction_id') diff --git a/tests/test_client.py b/tests/test_client.py index 256f769..16179bd 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -2,6 +2,7 @@ from requests import Response +from ynabtransactionadjuster import Transaction from ynabtransactionadjuster.client import Client from ynabtransactionadjuster.models.payee import Payee @@ -46,3 +47,16 @@ def test_fetch_payees(mock_get): assert p_list[0].name == 'p_name' assert p_list[0].id == 'p_id' assert p_list[0].transfer_account_id == 't_id' + + +@patch('ynabtransactionadjuster.client.requests.get') +def test_fetch_transaction(mock_get, mock_transaction_dict): + # Arrange + resp = MagicMock(spec=Response) + resp.json.return_value = {'data': {'transaction': mock_transaction_dict}} + mock_get.return_value = resp + client = Client(token=MagicMock(), budget=MagicMock(), account=MagicMock()) + + # Act + t = client.fetch_transaction('transaction_id') + assert isinstance(t, Transaction) diff --git a/tests/test_transaction.py b/tests/test_transaction.py index 0f0c371..b393fdb 100644 --- a/tests/test_transaction.py +++ b/tests/test_transaction.py @@ -5,27 +5,21 @@ from ynabtransactionadjuster.models import Transaction, Category, Payee -@pytest.fixture -def mock_transaction_dict(): - return dict(id='id', amount=1000, date='2024-01-01', category_name='category', category_id='categoryid', - payee_name='payee', payee_id='payeeid', flag_color=None, memo=None, subtransactions=[], - import_payee_name_original=None, import_payee_name=None, transfer_account_id=None, - approved=False, cleared='uncleared') - - def test_from_dict(mock_transaction_dict): o = Transaction.from_dict(mock_transaction_dict) assert o.id == mock_transaction_dict['id'] assert o.amount == mock_transaction_dict['amount'] assert o.transaction_date == datetime.strptime(mock_transaction_dict['date'], '%Y-%m-%d').date() assert o.category == Category(id=mock_transaction_dict['category_id'], name=mock_transaction_dict['category_name']) - assert o.payee == Payee(id=mock_transaction_dict['payee_id'], name=mock_transaction_dict['payee_name']) + assert o.payee == Payee(id=mock_transaction_dict['payee_id'], name=mock_transaction_dict['payee_name'], + transfer_account_id=mock_transaction_dict['transfer_account_id']) assert o.flag_color == mock_transaction_dict['flag_color'] assert o.memo == mock_transaction_dict['memo'] assert o.import_payee_name_original == mock_transaction_dict['import_payee_name_original'] assert o.import_payee_name == mock_transaction_dict['import_payee_name'] assert o.approved == mock_transaction_dict['approved'] assert o.cleared == mock_transaction_dict['cleared'] + assert o.transfer_transaction_id == mock_transaction_dict['transfer_transaction_id'] assert not o.subtransactions diff --git a/ynabtransactionadjuster/adjuster.py b/ynabtransactionadjuster/adjuster.py index 60e75dc..6f550ab 100644 --- a/ynabtransactionadjuster/adjuster.py +++ b/ynabtransactionadjuster/adjuster.py @@ -75,7 +75,7 @@ def dry_run(self, pretty_print: bool = False) -> List[ModifiedTransaction]: :raises AdjustError: if there is any error during the adjust process :raises HTTPError: if there is any error with the YNAB API (e.g. wrong credentials) """ - self.check_signatures() + self._check_signatures() filtered_transactions = self.filter(self.transactions) s = Serializer(transactions=filtered_transactions, adjust_func=self.adjust, categories=self.categories) modified_transactions = s.run() @@ -92,7 +92,7 @@ def run(self) -> int: :raises AdjustError: if there is any error during the adjust process :raises HTTPError: if there is any error with the YNAB API (e.g. wrong credentials) """ - self.check_signatures() + self._check_signatures() filtered_transactions = self.filter(self.transactions) s = Serializer(transactions=filtered_transactions, adjust_func=self.adjust, categories=self.categories) modified_transactions = s.run() @@ -102,6 +102,14 @@ def run(self) -> int: return updated return 0 - def check_signatures(self): + def _check_signatures(self): SignatureChecker(func=self.filter, parent_func=Adjuster.filter).check() SignatureChecker(func=self.adjust, parent_func=Adjuster.adjust).check() + + def fetch_transaction(self, transaction_id: str) -> Transaction: + """Fetches an individual transaction from the YNAB account + + :param transaction_id: Transaction ID of the transaction to be fetched + """ + client = Client.from_credentials(credentials=self.credentials) + return client.fetch_transaction(transaction_id=transaction_id) diff --git a/ynabtransactionadjuster/client.py b/ynabtransactionadjuster/client.py index 32d98a7..5a7ea55 100644 --- a/ynabtransactionadjuster/client.py +++ b/ynabtransactionadjuster/client.py @@ -56,6 +56,11 @@ def fetch_transactions(self) -> List[Transaction]: transactions = [Transaction.from_dict(t) for t in transaction_dicts] return transactions + def fetch_transaction(self, transaction_id: str) -> Transaction: + r = requests.get(f'{YNAB_BASE_URL}/budgets/{self._budget}/transactions/{transaction_id}', headers=self._header) + r.raise_for_status() + return Transaction.from_dict(r.json()['data']['transaction']) + def update_transactions(self, transactions: List[ModifiedTransaction]) -> int: """Updates transactions in YNAB. The updates are done in bulk. diff --git a/ynabtransactionadjuster/models/transaction.py b/ynabtransactionadjuster/models/transaction.py index 3074c28..12abc83 100644 --- a/ynabtransactionadjuster/models/transaction.py +++ b/ynabtransactionadjuster/models/transaction.py @@ -22,6 +22,7 @@ class Transaction: :ivar import_payee_name_original: The original payee or memo as recorded by the bank :ivar approved: approval status of the original transaction :ivar cleared: clearance state of the original transaction + :ivar transfer_transaction_id: id of the originating transaction if transaction is transfer """ id: str transaction_date: date @@ -35,6 +36,7 @@ class Transaction: subtransactions: Tuple[SubTransaction, ...] cleared: Literal['uncleared', 'cleared', 'reconciled'] approved: bool + transfer_transaction_id: Optional[str] @classmethod def from_dict(cls, t_dict: dict) -> 'Transaction': @@ -64,7 +66,8 @@ def build_subtransaction(s_dict: dict) -> SubTransaction: subtransactions=tuple([build_subtransaction(st) for st in t_dict['subtransactions']]), amount=t_dict['amount'], approved=t_dict['approved'], - cleared=t_dict['cleared']) + cleared=t_dict['cleared'], + transfer_transaction_id=t_dict['transfer_transaction_id']) def as_dict(self) -> dict: return asdict(self)