diff --git a/Pipfile b/Pipfile index f3f7986..cfe52e8 100644 --- a/Pipfile +++ b/Pipfile @@ -9,6 +9,7 @@ pytest = "*" black = "*" importlib-metadata = "*" # required on python-3.7 dataclasses = "*" # required by black on python-3.6 +syrupy = "*" [packages] ofxstatement = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 3cffbb5..9408153 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ce857bf8455922fc2b2e0440300b58b6bf27cc748b547832d08f28cd5c016a40" + "sha256": "a32a957beb2728a316b6b32d3f91ad1d7f032824b37bd37f99859f662a2ae6b0" }, "pipfile-spec": 6, "requires": {}, @@ -93,11 +93,11 @@ }, "iniconfig": { "hashes": [ - "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", - "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760" + "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", + "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" ], - "markers": "python_version >= '3.8'", - "version": "==2.1.0" + "markers": "python_version >= '3.10'", + "version": "==2.3.0" }, "mypy": { "hashes": [ @@ -201,6 +201,15 @@ "markers": "python_version >= '3.9'", "version": "==8.4.2" }, + "syrupy": { + "hashes": [ + "sha256:3282fe963fa5d4d3e47231b16d1d4d0f4523705e8199eeb99a22a1bc9f5942f2", + "sha256:c848e1a980ca52a28715cd2d2b4d434db424699c05653bd1158fb31cf56e9546" + ], + "index": "pypi", + "markers": "python_version >= '3.10'", + "version": "==5.0.0" + }, "typing-extensions": { "hashes": [ "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", diff --git a/src/ofxstatement_wise/wise.py b/src/ofxstatement_wise/wise.py index f2115ab..1d23283 100644 --- a/src/ofxstatement_wise/wise.py +++ b/src/ofxstatement_wise/wise.py @@ -1,14 +1,14 @@ -from typing import Set, List, Iterable, TextIO, Optional -import itertools +from csv import DictReader +from datetime import datetime +from typing import Dict, Iterable, Optional from decimal import Decimal from ofxstatement.plugin import Plugin -from ofxstatement.parser import CsvStatementParser +from ofxstatement.parser import StatementParser from ofxstatement.statement import ( Statement, StatementLine, BankAccount, - generate_unique_transaction_id, ) @@ -18,25 +18,17 @@ class TransferwisePlugin(Plugin): def get_parser(self, filename: str) -> "TransferwiseParser": default_ccy = self.settings.get("currency") account_id = self.settings.get("account") - return TransferwiseParser(open(filename, "rt"), default_ccy, account_id) + return TransferwiseParser(filename, default_ccy, account_id) -class TransferwiseParser(CsvStatementParser): - date_format: str = "%d-%m-%Y" - mappings = { - "amount": 3, - "date": 1, - "memo": 5, - "refnum": 0, - } - +class TransferwiseParser(StatementParser[Dict[str, str]]): def __init__( - self, fin: TextIO, currency: str | None = None, account_id: str | None = None + self, filename: str, currency: str | None = None, account_id: str | None = None ) -> None: - super().__init__(fin) + super().__init__() + self.filename = filename self.currency = currency self.account_id = account_id - self._unique: Set[str] = set() def parse(self) -> Statement: stmt = super().parse() @@ -44,39 +36,40 @@ def parse(self) -> Statement: stmt.account_id = self.account_id return stmt - def split_records(self) -> Iterable[List[str]]: - items = super().split_records() - # Skip the header line - yield from itertools.islice(items, 1, None) + def split_records(self) -> Iterable[Dict[str, str]]: + with open(self.filename, "rt") as f: + yield from DictReader(f) - def parse_record(self, line: List[str]) -> Optional[StatementLine]: + def parse_record(self, line: Dict[str, str]) -> Optional[StatementLine]: """Parse given transaction line and return StatementLine object""" - sl = super().parse_record(line) - if sl is None: - return None + sl = StatementLine() + + sl.id = line["TransferWise ID"] + sl.date = datetime.strptime(line["Date Time"], "%d-%m-%Y %H:%M:%S.%f") + sl.memo = line["Description"] + sl.amount = Decimal(line["Amount"]) - ccy = line[4] - if ccy != self.currency: + currency = line["Currency"] + if currency != self.currency: # Skip lines in some other currencies return None sl.memo = self._make_memo(line) - sl.id = generate_unique_transaction_id(sl, self._unique) - payee_acc_no = line[12] + payee_acc_no = line["Payee Account Number"] if payee_acc_no: sl.bank_account_to = BankAccount("", payee_acc_no) assert sl.amount is not None - sl.trntype = "DEBIT" if sl.amount > Decimal(0) else "CREDIT" + sl.trntype = line["Transaction Type"] return sl - def _make_memo(self, line: List[str]) -> str: - descr = line[4] - payref = line[5] - exc_from = line[7] - exc_to = line[8] - exc_rate = line[9] + def _make_memo(self, line: Dict[str, str]) -> str: + descr = line["Description"] + payref = line["Payment Reference"] + exc_from = line["Exchange From"] + exc_to = line["Exchange To"] + exc_rate = line["Exchange Rate"] memo = descr if payref: diff --git a/tests/__snapshots__/test_transferwise.ambr b/tests/__snapshots__/test_transferwise.ambr new file mode 100644 index 0000000..af3fb8b --- /dev/null +++ b/tests/__snapshots__/test_transferwise.ambr @@ -0,0 +1,58 @@ +# serializer version: 1 +# name: test_transferwise + { 'account_id': 'TW1', + 'account_type': 'CHECKING', + 'bank_id': None, + 'currency': 'USD', + 'invest_lines': [], + 'lines': [ { 'amount': Decimal('-357.38'), + 'bank_account_to': { 'acct_id': 'LT21 1111 2222 3333 4444', + 'acct_key': None, + 'acct_type': 'CHECKING', + 'bank_id': '', + 'branch_id': None}, + 'check_no': None, + 'date': datetime.datetime(2020, 8, 24, 13, 43, 54, 496000), + 'date_user': None, + 'id': 'TRANSFER-175545673', + 'memo': 'Sent money to John Doe (Moving to Revolut), 0.84840 USD/EUR', + 'payee': None, + 'refnum': None, + 'trntype': 'DEBIT'}, + { 'amount': Decimal('-11.35'), + 'check_no': None, + 'date': datetime.datetime(2020, 8, 24, 8, 12, 37, 495000), + 'date_user': None, + 'id': 'TRANSFER-175545673', + 'memo': 'TransferWise Charges for: TRANSFER-175545673 (Sending money)', + 'payee': None, + 'refnum': None, + 'trntype': 'DEBIT'}, + { 'amount': Decimal('125.00'), + 'check_no': None, + 'date': datetime.datetime(2020, 8, 24, 19, 27, 5, 726000), + 'date_user': None, + 'id': 'TRANSFER-175522746', + 'memo': 'Received money from the friend. with reference ', + 'payee': None, + 'refnum': None, + 'trntype': 'CREDIT'}, + { 'amount': Decimal('20.17'), + 'check_no': None, + 'date': datetime.datetime(2020, 8, 20, 16, 1, 22, 725000), + 'date_user': None, + 'id': 'TRANSFER-174549828', + 'memo': 'Topped up balance', + 'payee': None, + 'refnum': None, + 'trntype': 'CREDIT'}, + { 'amount': Decimal('-0.17'), + 'check_no': None, + 'date': datetime.datetime(2020, 8, 20, 5, 55, 41, 960000), + 'date_user': None, + 'id': 'TRANSFER-174549828', + 'memo': 'TransferWise Charges for: TRANSFER-174549828', + 'payee': None, + 'refnum': None, + 'trntype': 'DEBIT'}]} +# --- diff --git a/tests/sample-statement.csv b/tests/sample-statement.csv index 52b1962..12f77b1 100644 --- a/tests/sample-statement.csv +++ b/tests/sample-statement.csv @@ -1,6 +1,6 @@ -"TransferWise ID",Date,"Date Time",Amount,Currency,Description,"Payment Reference","Running Balance","Exchange From","Exchange To","Exchange Rate","Payer Name","Payee Name","Payee Account Number",Merchant,"Total fees" -TRANSFER-175545673,24-08-2020,"24-08-2020 13:43:54",-357.38,USD,"Sent money to John Doe","Moving to Revolut",787.62,USD,EUR,0.84840,,"John Doe","LT21 1111 2222 3333 4444",,11.35 -TRANSFER-175545673,24-08-2020,"24-08-2020 08:12:37",-11.35,USD,"TransferWise Charges for: TRANSFER-175545673","Sending money",776.27,,,,,TransferWise,,,0 -TRANSFER-175522746,24-08-2020,"24-08-2020 19:27:05",125.00,USD,"Received money from the friend. with reference ",,145.00,,,,"Friend.",,,,0.00 -TRANSFER-174549828,20-08-2020,"20-08-2020 16:01:22",20.17,USD,"Topped up balance",,20.17,,,,,,,,0.17 -TRANSFER-174549828,20-08-2020,"20-08-2020 05:55:41",-0.17,USD,"TransferWise Charges for: TRANSFER-174549828",,20.00,,,,,,,,0 +"TransferWise ID",Date,"Date Time",Amount,Currency,Description,"Payment Reference","Running Balance","Exchange From","Exchange To","Exchange Rate","Payer Name","Payee Name","Payee Account Number",Merchant,"Card Last Four Digits","Card Holder Full Name",Attachment,"Total fees","Exchange To Amount","Transaction Type","Transaction Details Type" +TRANSFER-175545673,24-08-2020,"24-08-2020 13:43:54.496",-357.38,USD,"Sent money to John Doe","Moving to Revolut",787.62,USD,EUR,0.84840,,"John Doe","LT21 1111 2222 3333 4444",,,,,11.35,,DEBIT, +TRANSFER-175545673,24-08-2020,"24-08-2020 08:12:37.495",-11.35,USD,"TransferWise Charges for: TRANSFER-175545673","Sending money",776.27,,,,,TransferWise,,,,,,0,,DEBIT, +TRANSFER-175522746,24-08-2020,"24-08-2020 19:27:05.726",125.00,USD,"Received money from the friend. with reference ",,145.00,,,,"Friend.",,,,,,,0.00,,CREDIT, +TRANSFER-174549828,20-08-2020,"20-08-2020 16:01:22.725",20.17,USD,"Topped up balance",,20.17,,,,,,,,,,,0.17,,CREDIT, +TRANSFER-174549828,20-08-2020,"20-08-2020 05:55:41.960",-0.17,USD,"TransferWise Charges for: TRANSFER-174549828",,20.00,,,,,,,,,,,0,,DEBIT, diff --git a/tests/test_transferwise.py b/tests/test_transferwise.py index d31b4cc..27d5a2a 100644 --- a/tests/test_transferwise.py +++ b/tests/test_transferwise.py @@ -5,7 +5,7 @@ from ofxstatement_wise.wise import TransferwisePlugin -def test_transferwise() -> None: +def test_transferwise(snapshot) -> None: config = {"currency": "USD", "account": "TW1"} plugin = TransferwisePlugin(UI(), config) here = os.path.dirname(__file__) @@ -14,9 +14,4 @@ def test_transferwise() -> None: parser = plugin.get_parser(sample_filename) statement = parser.parse() - assert statement is not None - - assert len(statement.lines) == 5 - # all ids are unique - assert len(set(ln.id for ln in statement.lines)) == 5 - assert all(ln.amount for ln in statement.lines) + assert statement == snapshot