Skip to content

Commit a9076aa

Browse files
committed
Cora OFX Importer refactoring
Move OFX importer for Cora checking account to its own module.
1 parent e34c005 commit a9076aa

File tree

7 files changed

+398
-165
lines changed

7 files changed

+398
-165
lines changed

thebook/bookkeeping/importers/__init__.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import logging
22

3+
from django.conf import settings
34
from django.utils.translation import gettext as _
45

56
from thebook.bookkeeping.importers.csv import CSVImporter
67
from thebook.bookkeeping.importers.ofx import OFXImporter
7-
from thebook.bookkeeping.models import Transaction
8+
from thebook.bookkeeping.models import BankAccount, Transaction
89
from thebook.integrations.cora.credit_card_invoice import CoraCreditCardInvoiceImporter
10+
from thebook.integrations.cora.ofx_importer import CoraOFXImporter
911

1012
logger = logging.getLogger(__name__)
1113

@@ -23,12 +25,21 @@ def __init__(self, message=None):
2325
def import_transactions(
2426
transactions_file, file_type, bank_account, user, start_date, end_date
2527
):
26-
importers = {
27-
"csv": CSVImporter,
28-
"csv_cora_credit_card": CoraCreditCardInvoiceImporter,
29-
"ofx": OFXImporter,
30-
}
31-
importer = importers.get(file_type) or None
28+
cora_bank_account, _ = BankAccount.objects.get_or_create(
29+
name=settings.CORA_BANK_ACCOUNT
30+
)
31+
32+
importer = None
33+
if file_type == "csv":
34+
importer = CSVImporter
35+
elif file_type == "csv_cora_credit_card":
36+
importer = CoraCreditCardInvoiceImporter
37+
elif file_type == "ofx":
38+
if bank_account == cora_bank_account:
39+
importer = CoraOFXImporter
40+
else:
41+
importer = OFXImporter
42+
3243
if importer is None:
3344
raise ImportTransactionsError(
3445
_("Unable to find a suitable file importer for this file.")
@@ -46,6 +57,17 @@ def import_transactions(
4657
update_fields=["description", "amount"],
4758
unique_fields=["reference"],
4859
)
60+
elif file_type == "ofx" and bank_account == cora_bank_account:
61+
ofx_importer = importer(transactions_file)
62+
transactions = ofx_importer.get_transactions(
63+
start_date, end_date, exclude_existing=True
64+
)
65+
Transaction.objects.bulk_create(
66+
transactions,
67+
update_conflicts=True,
68+
update_fields=["description", "amount"],
69+
unique_fields=["reference"],
70+
)
4971
else:
5072
importer(transactions_file, bank_account, user).run(start_date, end_date)
5173
except Exception as err:

thebook/bookkeeping/tests/importers/test_ofx.py

Lines changed: 0 additions & 158 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,6 @@ def test_raise_error_when_providing_invalid_ofx_file(db, bank_account, user):
2828
ofx_importer = OFXImporter(invalid_ofx_file, bank_account, user)
2929

3030

31-
def test_cora_ofx_file_with_one_transaction(db, request, bank_account, user):
32-
ofx_file_path = request.path.parent / "data" / "cora-one-transaction.ofx"
33-
with open(ofx_file_path, "r") as ofx_file:
34-
ofx_importer = OFXImporter(ofx_file, bank_account, user)
35-
36-
transactions = ofx_importer.run()
37-
38-
assert len(transactions) == 1
39-
transaction = transactions[0]
40-
assert transaction.reference == "16b3dab5-d1ca-41e1-87c2-a26920ae70ac"
41-
assert transaction.date == datetime.date(2024, 8, 19)
42-
assert transaction.description == "Debito em Conta"
43-
assert transaction.amount == decimal.Decimal("-2500.50")
44-
assert transaction.notes == ""
45-
assert transaction.bank_account == bank_account
46-
assert transaction.created_by == user
47-
assert transaction.category is None
48-
49-
5031
def test_bradesco_ofx_file_with_one_transaction(db, request, bank_account, user):
5132
ofx_file_path = request.path.parent / "data" / "bradesco-one-transaction.ofx"
5233
with open(ofx_file_path, "r") as ofx_file:
@@ -66,29 +47,6 @@ def test_bradesco_ofx_file_with_one_transaction(db, request, bank_account, user)
6647
assert transaction.category is None
6748

6849

69-
def test_ofx_file_with_multiple_transactions(db, request, bank_account, user):
70-
ofx_file_path = request.path.parent / "data" / "cora-multiple-transactions.ofx"
71-
with open(ofx_file_path, "r") as ofx_file:
72-
ofx_importer = OFXImporter(ofx_file, bank_account, user)
73-
74-
transactions = ofx_importer.run()
75-
76-
references = sorted([transaction.reference for transaction in transactions])
77-
expected_references = sorted(
78-
[
79-
"3825c888-1017-497d-bee2-c0737d5dafc0",
80-
"63c41497-5e0b-4a3c-ada1-d9abebb0af39",
81-
"69b66fe5-0360-466c-b2e5-08efc1768389",
82-
"39221b44-f648-44ec-b6d1-1d8eefe54e02",
83-
"6b01e19f-fc2f-4ea7-b524-6f6c634aea63",
84-
]
85-
)
86-
87-
assert len(transactions) == 5
88-
assert Transaction.objects.count() == 5
89-
assert references == expected_references
90-
91-
9250
def test_bradesco_ofx_file_with_multiple_transactions(db, request, bank_account, user):
9351
ofx_file_path = request.path.parent / "data" / "bradesco-multiple-transactions.ofx"
9452
with open(ofx_file_path, "r") as ofx_file:
@@ -100,122 +58,6 @@ def test_bradesco_ofx_file_with_multiple_transactions(db, request, bank_account,
10058
assert Transaction.objects.count() == 57
10159

10260

103-
def test_import_same_ofx_file_twice_will_not_duplicate_transactions(
104-
db, request, bank_account, user
105-
):
106-
ofx_file_path = request.path.parent / "data" / "cora-multiple-transactions.ofx"
107-
with open(ofx_file_path, "r") as ofx_file:
108-
ofx_importer = OFXImporter(ofx_file, bank_account, user)
109-
transactions = ofx_importer.run()
110-
assert Transaction.objects.all().count() == 5
111-
112-
with open(ofx_file_path, "r") as ofx_file:
113-
ofx_importer = OFXImporter(ofx_file, bank_account, user)
114-
transactions = ofx_importer.run()
115-
assert Transaction.objects.all().count() == 5
116-
117-
118-
def test_import_ofx_file_that_overlaps_other_already_imported_do_not_duplicate_transactions(
119-
db, request, bank_account, user
120-
):
121-
ofx_file_path = request.path.parent / "data" / "cora-multiple-transactions.ofx"
122-
with open(ofx_file_path, "r") as ofx_file:
123-
ofx_importer = OFXImporter(ofx_file, bank_account, user)
124-
_ = ofx_importer.run()
125-
assert Transaction.objects.all().count() == 5
126-
127-
ofx_file_path = (
128-
request.path.parent / "data" / "cora-multiple-transactions-extended.ofx"
129-
)
130-
with open(ofx_file_path, "r") as ofx_file:
131-
ofx_importer = OFXImporter(ofx_file, bank_account, user)
132-
transactions = ofx_importer.run()
133-
assert Transaction.objects.all().count() == 7
134-
135-
references = sorted([transaction.reference for transaction in transactions])
136-
expected_references = sorted(
137-
[
138-
"3825c888-1017-497d-bee2-c0737d5dafc0",
139-
"63c41497-5e0b-4a3c-ada1-d9abebb0af39",
140-
"69b66fe5-0360-466c-b2e5-08efc1768389",
141-
"39221b44-f648-44ec-b6d1-1d8eefe54e02",
142-
"6b01e19f-fc2f-4ea7-b524-6f6c634aea63",
143-
"b3950c28-8b64-49ed-bdfb-d83a4df39c96",
144-
"f5098c7d-81f0-44d9-b0f7-3b8eadee5c34",
145-
]
146-
)
147-
148-
assert len(transactions) == 7
149-
assert references == expected_references
150-
151-
152-
def test_ofx_file_with_multiple_transactions_filter_by_start_date(
153-
db, request, bank_account, user
154-
):
155-
ofx_file_path = request.path.parent / "data" / "cora-multiple-transactions.ofx"
156-
with open(ofx_file_path, "r") as ofx_file:
157-
ofx_importer = OFXImporter(ofx_file, bank_account, user)
158-
159-
transactions = ofx_importer.run(start_date=datetime.date(2024, 8, 25))
160-
161-
references = sorted([transaction.reference for transaction in transactions])
162-
expected_references = sorted(
163-
[
164-
"69b66fe5-0360-466c-b2e5-08efc1768389",
165-
"39221b44-f648-44ec-b6d1-1d8eefe54e02",
166-
"6b01e19f-fc2f-4ea7-b524-6f6c634aea63",
167-
]
168-
)
169-
170-
assert len(transactions) == 3
171-
assert references == expected_references
172-
173-
174-
def test_ofx_file_with_multiple_transactions_filter_by_end_date(
175-
db, request, bank_account, user
176-
):
177-
ofx_file_path = request.path.parent / "data" / "cora-multiple-transactions.ofx"
178-
with open(ofx_file_path, "r") as ofx_file:
179-
ofx_importer = OFXImporter(ofx_file, bank_account, user)
180-
181-
transactions = ofx_importer.run(end_date=datetime.date(2024, 8, 25))
182-
183-
references = sorted([transaction.reference for transaction in transactions])
184-
expected_references = sorted(
185-
[
186-
"3825c888-1017-497d-bee2-c0737d5dafc0",
187-
"63c41497-5e0b-4a3c-ada1-d9abebb0af39",
188-
]
189-
)
190-
191-
assert len(transactions) == 2
192-
assert references == expected_references
193-
194-
195-
def test_ofx_file_with_multiple_transactions_filter_by_start_date_and_end_date(
196-
db, request, bank_account, user
197-
):
198-
ofx_file_path = request.path.parent / "data" / "cora-multiple-transactions.ofx"
199-
with open(ofx_file_path, "r") as ofx_file:
200-
ofx_importer = OFXImporter(ofx_file, bank_account, user)
201-
202-
transactions = ofx_importer.run(
203-
start_date=datetime.date(2024, 8, 24), end_date=datetime.date(2024, 9, 15)
204-
)
205-
206-
references = sorted([transaction.reference for transaction in transactions])
207-
expected_references = sorted(
208-
[
209-
"63c41497-5e0b-4a3c-ada1-d9abebb0af39",
210-
"69b66fe5-0360-466c-b2e5-08efc1768389",
211-
"39221b44-f648-44ec-b6d1-1d8eefe54e02",
212-
]
213-
)
214-
215-
assert len(transactions) == 3
216-
assert references == expected_references
217-
218-
21961
@pytest.mark.parametrize(
22062
"ignored_memos,expected_references",
22163
[
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import csv
2+
import datetime
3+
import decimal
4+
import io
5+
import uuid
6+
7+
import structlog
8+
from ofxparse import OfxParser
9+
10+
from django.conf import settings
11+
from django.contrib.auth import get_user_model
12+
13+
from thebook.bookkeeping.models import BankAccount, Category, Transaction
14+
15+
logger = structlog.get_logger(__name__)
16+
17+
18+
class InvalidCoraOFXFile(Exception): ...
19+
20+
21+
class CoraOFXImporter:
22+
def __init__(self, ofx_file):
23+
try:
24+
self.ofx_parser = OfxParser.parse(ofx_file)
25+
except (UnicodeDecodeError, TypeError, ValueError) as exc:
26+
logger.error(
27+
"CoraOFXImporter.__init__.invalid_cora_ofx_file",
28+
start_date=start_date,
29+
end_date=end_date,
30+
exclude_existing=exclude_existing,
31+
)
32+
raise InvalidCoraOFXFile() from exc
33+
34+
self.cora_bank_account, _ = BankAccount.objects.get_or_create(
35+
name=settings.CORA_BANK_ACCOUNT
36+
)
37+
self.cora_credit_card_bank_account, _ = BankAccount.objects.get_or_create(
38+
name=settings.CORA_CREDIT_CARD_BANK_ACCOUNT
39+
)
40+
self.bank_account_transfer_category, _ = Category.objects.get_or_create(
41+
name="Transferência entre contas bancárias"
42+
)
43+
self.user = get_user_model().objects.get_or_create_automation_user()
44+
45+
def _within_date_range(self, transaction_date, start_date, end_date):
46+
date_rules = []
47+
if start_date is not None:
48+
date_rules.append(transaction_date >= start_date)
49+
if end_date is not None:
50+
date_rules.append(transaction_date <= end_date)
51+
return all(date_rules)
52+
53+
def get_transactions(
54+
self, start_date=None, end_date=None, exclude_existing: bool = True
55+
):
56+
logger.info(
57+
"CoraOFXImporter.get_transactions.start",
58+
start_date=start_date,
59+
end_date=end_date,
60+
exclude_existing=exclude_existing,
61+
)
62+
63+
transactions = []
64+
65+
ofx_transactions = self.ofx_parser.account.statement.transactions
66+
for transaction in ofx_transactions:
67+
if (
68+
exclude_existing
69+
and Transaction.objects.filter(reference=transaction.id).exists()
70+
):
71+
continue
72+
73+
transaction_date = transaction.date.date()
74+
if not self._within_date_range(transaction_date, start_date, end_date):
75+
continue
76+
77+
transaction_notes = ""
78+
transaction_category = None
79+
80+
if "Pagamento da fatura" in transaction.memo:
81+
# Cora Credit Card invoice doesn't provide the information when the invoice
82+
# is paid, so we need to add it based on the payment that is provided in
83+
# the checking account OFX report
84+
bank_account_transfer_reference = uuid.uuid4()
85+
86+
transaction_notes = notes = (
87+
f"Transferência entre contas bancárias - {bank_account_transfer_reference}"
88+
)
89+
transaction_category = self.bank_account_transfer_category
90+
91+
transactions.append(
92+
Transaction(
93+
reference=bank_account_transfer_reference,
94+
date=transaction_date,
95+
description=transaction.memo,
96+
amount=-1 * transaction.amount,
97+
notes=f"Transferência entre contas bancárias - {transaction.id}",
98+
bank_account=self.cora_credit_card_bank_account,
99+
category=transaction_category,
100+
source="cora-ofx-importer",
101+
created_by=self.user,
102+
)
103+
)
104+
105+
transactions.append(
106+
Transaction(
107+
reference=transaction.id,
108+
date=transaction_date,
109+
description=transaction.memo,
110+
notes=transaction_notes,
111+
amount=transaction.amount,
112+
bank_account=self.cora_bank_account,
113+
category=transaction_category,
114+
source="cora-ofx-importer",
115+
created_by=self.user,
116+
)
117+
)
118+
119+
logger.info(
120+
"CoraOFXImporter.get_transactions.completed",
121+
num_transactions=len(transactions),
122+
)
123+
return transactions

thebook/bookkeeping/tests/importers/data/cora-multiple-transactions-extended.ofx renamed to thebook/integrations/tests/cora/data/cora-multiple-transactions-extended.ofx

File renamed without changes.

thebook/bookkeeping/tests/importers/data/cora-multiple-transactions.ofx renamed to thebook/integrations/tests/cora/data/cora-multiple-transactions.ofx

File renamed without changes.

thebook/bookkeeping/tests/importers/data/cora-one-transaction.ofx renamed to thebook/integrations/tests/cora/data/cora-one-transaction.ofx

File renamed without changes.

0 commit comments

Comments
 (0)