Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/moneywiz_api/model/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
from moneywiz_api.model.record import Record
from moneywiz_api.schema_profile import SchemaProfile
from moneywiz_api.types import ID
from moneywiz_api.validation import require_integer_identity, require_valid
from moneywiz_api.validation import (
require_integer_identity,
require_text,
require_valid,
)

ABS_TOLERANCE = 0.001

Expand All @@ -24,7 +28,7 @@ class Transaction(Record, ABC):
reconciled: bool

amount: Decimal
description: str
description: Optional[str]
datetime: datetime
notes: Optional[str]

Expand All @@ -50,8 +54,10 @@ def __init__(self, row):
# Validate
require_valid(self.reconciled is not None, "transaction state is required")
require_valid(self.amount is not None, "transaction amount is required")
require_valid(
self.description is not None, "transaction description is required"
require_text(
self.description,
"transaction description must be an uncoerced string or null",
optional=True,
)
require_valid(self.datetime is not None, "transaction date is required")
# self.notes can be None
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/test_api_snapshot_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def create_read_schema(path) -> None:
(8, "SyncObject", 0, 0),
(9, "Account", 8, 0),
(10, "CashAccount", 9, 0),
(35, "Tag", 8, 0),
(36, "Transaction", 8, 0),
(37, "DepositTransaction", 36, 0),
],
Expand All @@ -31,6 +32,9 @@ def create_read_schema(path) -> None:
"ZORIGINALCURRENCY TEXT, ZORIGINALAMOUNT FLOAT, "
"ZORIGINALEXCHANGERATE FLOAT)"
)
connection.execute(
"CREATE TABLE Z_36TAGS (Z_36TRANSACTIONS INTEGER, Z_35TAGS INTEGER)"
)
connection.execute(
"INSERT INTO ZSYNCOBJECT "
"(Z_PK, Z_ENT, ZOBJECTCREATIONDATE, ZGID, ZDISPLAYORDER, ZGROUPID, "
Expand All @@ -56,6 +60,20 @@ def mutate_between_loads(path) -> None:
connection.close()


def add_nullable_description_transaction(path) -> None:
connection = sqlite3.connect(path)
connection.execute(
"INSERT INTO ZSYNCOBJECT "
"(Z_PK, Z_ENT, ZOBJECTCREATIONDATE, ZGID, ZRECONCILED, ZAMOUNT1, "
"ZDESC2, ZDATE1, ZNOTES1, ZACCOUNT2, ZPAYEE2, ZORIGINALCURRENCY, "
"ZORIGINALAMOUNT, ZORIGINALEXCHANGERATE) "
"VALUES (2, 37, 0, 'transaction-2', 0, 5, NULL, 0, NULL, 1, "
"NULL, 'EUR', 5, 1)"
)
connection.commit()
connection.close()


@pytest.mark.parametrize(
("initial_managers", "expected_managers"),
[
Expand All @@ -81,3 +99,20 @@ def test_scoped_reload_refreshes_loaded_union_atomically(
assert api.transaction_manager.get(2).description == "Income"
assert tuple(snapshot["completeness"]["managers"]) == expected_managers
assert snapshot["records"]["accounts"][0]["name"] == "After"


def test_nullable_transaction_description_preserves_complete_api_read(tmp_path) -> None:
path = tmp_path / "nullable-description.sqlite"
create_read_schema(path)
add_nullable_description_transaction(path)

with MoneywizApi(path, managers=("transactions",)) as api:
completeness = api.completeness()
snapshot = api.snapshot().as_dict()

assert completeness.complete
assert api.transaction_manager.load_report.source_ids == (2,)
assert api.transaction_manager.load_report.parsed_ids == (2,)
assert api.transaction_manager.load_report.skipped == ()
assert api.transaction_manager.get(2).description is None
assert snapshot["records"]["transactions"][0]["description"] is None
24 changes: 24 additions & 0 deletions tests/unit/test_transaction_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,30 @@ def test_transfer_withdraw_preserves_stored_amount_with_rounded_rate() -> None:
assert transaction.original_amount == Decimal("-3.333")


def test_transaction_description_allows_null() -> None:
transaction = TransferWithdrawTransaction(
transfer_withdraw_row(ZDESC2=None, ZORIGINALEXCHANGERATE=1.0)
)

assert transaction.description is None
assert transaction.as_dict()["description"] is None


class _DescriptionSubclass(str):
pass


@pytest.mark.parametrize("description", [b"Transfer", 1, _DescriptionSubclass("x")])
def test_transaction_description_rejects_non_exact_text(description) -> None:
with pytest.raises(
AssertionError,
match="transaction description must be an uncoerced string or null",
):
TransferWithdrawTransaction(
transfer_withdraw_row(ZDESC2=description, ZORIGINALEXCHANGERATE=1.0)
)


class TransferWithdrawManager(RecordManager):
@property
def ents(self):
Expand Down
Loading