Skip to content

Commit

Permalink
fix: Cover rules including imported payees. (#70)
Browse files Browse the repository at this point in the history
Implements the imported_payee type that was forgotten because it implements the exact same behaviour as string. There must be a reason why this was implemented, but as of now it implements exacly what STRING implements.

Also trimmed the imported_payee as the original API does, and set it to the payee if it's missing.
  • Loading branch information
bvanelli authored Sep 8, 2024
1 parent fb81211 commit a341f5a
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 5 deletions.
5 changes: 5 additions & 0 deletions actual/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,16 @@ def create_transaction(
acct = get_account(s, account)
if acct is None:
raise ActualError(f"Account {account} not found")
if imported_payee:
imported_payee = imported_payee.strip()
if not payee:
payee = imported_payee
payee = get_or_create_payee(s, payee)
if category:
category_id = get_or_create_category(s, category).id
else:
category_id = None

return create_transaction_from_ids(
s, date, acct.id, payee.id, notes, category_id, amount, imported_id, cleared, imported_payee
)
Expand Down
13 changes: 8 additions & 5 deletions actual/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,15 @@ class ValueType(enum.Enum):
STRING = "string"
NUMBER = "number"
BOOLEAN = "boolean"
IMPORTED_PAYEE = "imported_payee"

def is_valid(self, operation: ConditionType) -> bool:
"""Returns if a conditional operation for a certain type is valid. For example, if the value is of type string,
then `RuleValueType.STRING.is_valid(ConditionType.GT)` will return false, because there is no logical
greater than defined for strings."""
if self == ValueType.DATE:
return operation.value in ("is", "isapprox", "gt", "gte", "lt", "lte")
elif self == ValueType.STRING:
elif self in (ValueType.STRING, ValueType.IMPORTED_PAYEE):
return operation.value in ("is", "contains", "oneOf", "isNot", "doesNotContain", "notOneOf", "matches")
elif self == ValueType.ID:
return operation.value in ("is", "isNot", "oneOf", "notOneOf")
Expand All @@ -99,7 +100,7 @@ def validate(self, value: typing.Union[int, list[str], str, None], operation: Co
if self == ValueType.ID:
# make sure it's an uuid
return isinstance(value, str) and is_uuid(value)
elif self == ValueType.STRING:
elif self in (ValueType.STRING, ValueType.IMPORTED_PAYEE):
return isinstance(value, str)
elif self == ValueType.DATE:
try:
Expand All @@ -120,8 +121,10 @@ def validate(self, value: typing.Union[int, list[str], str, None], operation: Co
def from_field(cls, field: str | None) -> ValueType:
if field in ("acct", "category", "description"):
return ValueType.ID
elif field in ("notes", "imported_description"):
elif field in ("notes",):
return ValueType.STRING
elif field in ("imported_description",):
return ValueType.IMPORTED_PAYEE
elif field in ("date",):
return ValueType.DATE
elif field in ("cleared", "reconciled"):
Expand All @@ -143,7 +146,7 @@ def get_value(
return datetime.datetime.strptime(str(value), "%Y%m%d").date()
elif value_type is ValueType.BOOLEAN:
return int(value) # database accepts 0 or 1
elif value_type is ValueType.STRING:
elif value_type in (ValueType.STRING, ValueType.IMPORTED_PAYEE):
if isinstance(value, list):
return [get_value(v, value_type) for v in value]
else:
Expand Down Expand Up @@ -193,7 +196,7 @@ def condition_evaluation(
elif op == ConditionType.CONTAINS:
return self_value in true_value
elif op == ConditionType.MATCHES:
return bool(re.match(self_value, true_value, re.IGNORECASE))
return bool(re.search(self_value, true_value, re.IGNORECASE))
elif op == ConditionType.NOT_ONE_OF:
return true_value not in self_value
elif op == ConditionType.DOES_NOT_CONTAIN:
Expand Down
7 changes: 7 additions & 0 deletions tests/test_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,10 @@ def test_model_notes(session):
session.commit()
assert account_with_note.notes == "My note"
assert account_without_note.notes is None


def test_default_imported_payee(session):
t = create_transaction(session, date(2024, 1, 4), create_account(session, "Bank"), imported_payee=" foo ")
session.flush()
assert t.payee.name == "foo"
assert t.imported_description == "foo"
22 changes: 22 additions & 0 deletions tests/test_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,23 @@ def test_string_condition():
assert Condition(field="notes", op="doesNotContain", value="FOOBAR").run(t) is True


@pytest.mark.parametrize(
"op,condition_value,value,expected_result",
[
("contains", "supermarket", "Best Supermarket", True),
("contains", "supermarket", None, False),
("oneOf", ["my supermarket", "other supermarket"], "MY SUPERMARKET", True),
("oneOf", ["supermarket"], None, False),
("matches", "market", "hypermarket", True),
],
)
def test_imported_payee_condition(op, condition_value, value, expected_result):
t = create_transaction(MagicMock(), datetime.date(2024, 1, 1), "Bank", "", amount=5, imported_payee=value)
condition = {"field": "imported_description", "type": "imported_payee", "op": op, "value": condition_value}
cond = Condition.model_validate(condition)
assert cond.run(t) == expected_result


def test_numeric_condition():
t = create_transaction(MagicMock(), datetime.date(2024, 1, 1), "Bank", "", amount=5)
c1 = Condition(field="amount_inflow", op="gt", value=10.0)
Expand Down Expand Up @@ -181,6 +198,8 @@ def test_value_type_condition_validation():
assert ValueType.ID.is_valid(ConditionType.CONTAINS) is False
assert ValueType.STRING.is_valid(ConditionType.CONTAINS) is True
assert ValueType.STRING.is_valid(ConditionType.GT) is False
assert ValueType.IMPORTED_PAYEE.is_valid(ConditionType.CONTAINS) is True
assert ValueType.IMPORTED_PAYEE.is_valid(ConditionType.GT) is False


def test_value_type_value_validation():
Expand All @@ -196,6 +215,8 @@ def test_value_type_value_validation():
assert ValueType.ID.validate("foo") is False
assert ValueType.BOOLEAN.validate(True) is True
assert ValueType.BOOLEAN.validate("") is False
assert ValueType.IMPORTED_PAYEE.validate("") is True
assert ValueType.IMPORTED_PAYEE.validate(1) is False
# list and NoneType
assert ValueType.DATE.validate(None)
assert ValueType.DATE.validate(["2024-10-04"], ConditionType.ONE_OF) is True
Expand All @@ -207,6 +228,7 @@ def test_value_type_from_field():
assert ValueType.from_field("notes") == ValueType.STRING
assert ValueType.from_field("date") == ValueType.DATE
assert ValueType.from_field("cleared") == ValueType.BOOLEAN
assert ValueType.from_field("imported_description") == ValueType.IMPORTED_PAYEE
with pytest.raises(ValueError):
ValueType.from_field("foo")

Expand Down

0 comments on commit a341f5a

Please sign in to comment.