You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
57 lines
3.2 KiB
57 lines
3.2 KiB
"""Read-only domain validation for review; commit also uses finance posting rules.""" |
|
from datetime import date |
|
from finance.services.base import DomainError, money |
|
from finance.models import Transaction |
|
|
|
|
|
def validate(candidate, decision, account, refs, profile): |
|
issues = list(candidate.get("errors", [])) |
|
if account is None: |
|
return issues + ["ACCOUNT_NOT_FOUND"] |
|
if account.archived: |
|
issues.append("ACCOUNT_ARCHIVED") |
|
if account.currency != "BRL": |
|
issues.append("UNSUPPORTED_CURRENCY") |
|
expected = "BANK" if profile == "BANK_CSV_V1" else "CREDIT_CARD" |
|
if account.kind != expected: |
|
issues.append("ACCOUNT_KIND_MISMATCH") |
|
if issues: |
|
return issues |
|
try: |
|
amount = money(candidate["amount"]) |
|
date.fromisoformat(candidate["posting_date"]) |
|
except (DomainError, ValueError, TypeError, KeyError): |
|
return ["INVALID_MOVEMENT"] |
|
kind = decision.get("kind", candidate["kind"]) |
|
if kind not in {"INCOME", "EXPENSE", "REFUND", "TRANSFER"} or (kind in {"INCOME", "REFUND"} and amount <= 0) or (kind == "EXPENSE" and amount >= 0): |
|
issues.append("INVALID_KIND_OR_SIGN") |
|
for field, table in (("payee_id", "payees"), ("category_id", "categories")): |
|
if decision.get(field): |
|
ref = refs[table].get(decision[field]) |
|
if ref is None or ref.archived: |
|
issues.append("INVALID_" + field.upper()) |
|
category = refs["categories"].get(decision.get("category_id")) |
|
if category and category.kind != {"INCOME": "INCOME", "EXPENSE": "EXPENSE", "REFUND": "EXPENSE"}.get(kind): |
|
issues.append("CATEGORY_KIND_MISMATCH") |
|
statement = refs["statements"].get(decision.get("statement_id")) |
|
if decision.get("statement_id") and (statement is None or statement.account_id != account.pk or account.kind != "CREDIT_CARD"): |
|
issues.append("STATEMENT_ACCOUNT_MISMATCH") |
|
original = refs["transactions"].get(decision.get("refund_of_id")) |
|
if decision.get("refund_of_id") and (original is None or kind != "REFUND" or original.kind != "EXPENSE" or original.account.currency != account.currency): |
|
issues.append("INVALID_REFUND_REFERENCE") |
|
number, total = candidate.get("installment_number"), candidate.get("installment_total") |
|
if number is not None or total is not None: |
|
if not isinstance(number, int) or not isinstance(total, int) or not 1 <= number <= total or kind != "EXPENSE": |
|
issues.append("INVALID_INSTALLMENT") |
|
plan_id, new_plan = decision.get("installment_plan_id"), decision.get("new_plan") |
|
if bool(plan_id) == bool(new_plan): |
|
issues.append("INSTALLMENT_PLAN_REQUIRED") |
|
if plan_id: |
|
plan = refs["plans"].get(plan_id) |
|
if plan is None or plan.account_id != account.pk or plan.total_installments != total: |
|
issues.append("INSTALLMENT_PLAN_MISMATCH") |
|
elif decision.get("action") == "CREATE" and Transaction.objects.filter(installment_plan=plan, installment_number=number).exists(): |
|
issues.append("INSTALLMENT_ALREADY_POSTED") |
|
elif decision.get("installment_plan_id") or decision.get("new_plan"): |
|
issues.append("UNEXPECTED_INSTALLMENT_PLAN") |
|
return issues
|
|
|