Skip to content

Commit

Permalink
feat(banks): add bank of america combined statement
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamin-awd committed Nov 15, 2024
1 parent af90705 commit 251afb5
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 3 deletions.
2 changes: 2 additions & 0 deletions src/monopoly/banks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
from typing import Type

from .bank_of_america import BankOfAmerica
from .base import BankBase
from .chase import Chase
from .citibank import Citibank
Expand All @@ -15,6 +16,7 @@
from .zkb import ZurcherKantonalBank

banks: list[Type["BankBase"]] = [
BankOfAmerica,
Chase,
Citibank,
Dbs,
Expand Down
3 changes: 3 additions & 0 deletions src/monopoly/banks/bank_of_america/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .boa import BankOfAmerica

__all__ = ["BankOfAmerica"]
39 changes: 39 additions & 0 deletions src/monopoly/banks/bank_of_america/boa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import logging
from re import compile as regex

from monopoly.config import DateOrder, StatementConfig
from monopoly.constants import BankNames, CreditTransactionPatterns, EntryType
from monopoly.constants.date import ISO8601
from monopoly.identifiers import MetadataIdentifier

from ..base import BankBase

logger = logging.getLogger(__name__)


class BankOfAmerica(BankBase):
name = BankNames.BANK_OF_AMERICA

credit = StatementConfig(
statement_type=EntryType.CREDIT,
statement_date_pattern=regex(rf"for .* to {ISO8601.MMMM_DD_YYYY}"),
statement_date_order=DateOrder("MDY"),
transaction_date_order=DateOrder("MDY"),
header_pattern=regex(r"(Date.*Description.*Amount)"),
transaction_pattern=CreditTransactionPatterns.BANK_OF_AMERICA,
multiline_transactions=True,
safety_check=False,
auto_polarity=False,
)

identifiers = [
[
MetadataIdentifier(
format="PDF 1.5",
creator="Bank of America",
producer="TargetStream StreamEDS",
)
]
]

statement_configs = [credit]
4 changes: 4 additions & 0 deletions src/monopoly/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ class StatementConfig:
number of spaces will be ignored. For example, if `transaction_bound` = 32:
"01 NOV BALANCE B/F 190.77" (will be ignored)
"01 NOV YA KUN KAYA TOAST 12.00 " (will be kept)
- `auto_polarity` controls whether transaction amounts are set as negative.
or positive if they have 'CR' or '+' as a suffix. Enabled by default.
If enabled, only 'CR' or '+' will make a transaction positive. Disabled by default.
- `safety_check` controls whether the safety check for banks. Use
for banks that don't provide total amount (or total debit/credit)
in the statement. Enabled by default.
Expand All @@ -62,6 +65,7 @@ class StatementConfig:
transaction_bound: Optional[int] = None
prev_balance_pattern: Optional[Pattern[str] | RegexEnum] = None
safety_check: bool = True
auto_polarity: bool = True


@dataclass
Expand Down
1 change: 1 addition & 0 deletions src/monopoly/constants/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class ISO8601(RegexEnum):
rf"\b({DateFormats.DD}[-\s]{DateFormats.MMM}[,\s]{{1,2}}{DateFormats.YYYY})"
)
MM_DD = rf"\b({DateFormats.MM}[\/\-\s]{DateFormats.DD})"
MM_DD_YY = rf"\b({DateFormats.MM}[\/\-\s]{DateFormats.DD}[\/\-\s]{DateFormats.YY})"
MMMM_DD_YYYY = (
rf"\b({DateFormats.MMMM}\s{DateFormats.DD}[,\s]{{1,2}}{DateFormats.YYYY})"
)
Expand Down
11 changes: 9 additions & 2 deletions src/monopoly/constants/statement.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class EntryType(AutoEnum):


class BankNames(AutoEnum):
BANK_OF_AMERICA = auto()
CHASE = auto()
CITIBANK = auto()
DBS = auto()
Expand Down Expand Up @@ -42,7 +43,7 @@ class Columns(AutoEnum):
class SharedPatterns(StrEnum):
"""
AMOUNT matches the following patterns:
1,123.12 | 123.12 | (123.12) | ( 123.12) | 123.12 CR
1,123.12 | 123.12 | (123.12) | ( 123.12) | 123.12 CR | -1,123.12
AMOUNT_EXTENDED is generally used for credit statements and to
find statement balances, while AMOUNT_EXTENDED_WITHOUT_EOL is used
Expand All @@ -52,9 +53,10 @@ class SharedPatterns(StrEnum):

COMMA_FORMAT = r"\d{1,3}(,\d{3})*\.\d*"
ENCLOSED_COMMA_FORMAT = rf"\({COMMA_FORMAT}\s{{0,1}}\))"
OPTIONAL_NEGATIVE_SYMBOL = r"(?:-)?"
DEBIT_CREDIT_SUFFIX = r"(?P<suffix>CR\b|DR\b|\+|\-)?\s*"

AMOUNT = rf"(?P<amount>{COMMA_FORMAT}|{ENCLOSED_COMMA_FORMAT}\s*"
AMOUNT = rf"(?P<amount>{OPTIONAL_NEGATIVE_SYMBOL}{COMMA_FORMAT}|{ENCLOSED_COMMA_FORMAT}\s*"
AMOUNT_EXTENDED_WITHOUT_EOL = AMOUNT + DEBIT_CREDIT_SUFFIX
AMOUNT_EXTENDED = AMOUNT_EXTENDED_WITHOUT_EOL + r"$"

Expand Down Expand Up @@ -99,6 +101,11 @@ class StatementBalancePatterns(RegexEnum):


class CreditTransactionPatterns(RegexEnum):
BANK_OF_AMERICA = (
rf"(?P<transaction_date>{ISO8601.MM_DD_YY})\s+"
+ SharedPatterns.DESCRIPTION
+ SharedPatterns.AMOUNT_EXTENDED
)
DBS = (
rf"(?P<transaction_date>{ISO8601.DD_MMM})\s+"
+ SharedPatterns.DESCRIPTION
Expand Down
5 changes: 4 additions & 1 deletion src/monopoly/statements/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ def get_transactions(self) -> list[Transaction] | None:
lines=page.lines,
idx=line_num,
)
transaction = Transaction(**processed_match.groupdict)
transaction = Transaction(
**processed_match.groupdict,
auto_polarity=self.config.auto_polarity,
)
transactions.append(transaction)

if not transactions:
Expand Down
6 changes: 6 additions & 0 deletions src/monopoly/statements/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ class Transaction:
amount: float
date: str = Field(alias="transaction_date")
suffix: Optional[str] = None
# avoid storing config logic, since the Transaction object is used to create
# a single unique hash which should not change
auto_polarity: bool = Field(default=True, init=True, repr=False)

def as_raw_dict(self, show_suffix=False):
"""Returns stringified dictionary version of the transaction"""
Expand Down Expand Up @@ -138,6 +141,9 @@ def convert_credit_amount_to_negative(self: "Transaction") -> "Transaction":
if self.amount == 0:
return self

if not self.auto_polarity:
return

if self.suffix in ("CR", "+"):
self.amount = abs(self.amount)

Expand Down

0 comments on commit 251afb5

Please sign in to comment.