"""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