Source code for cadrumo.application.aggregation.evidence_advisory

"""Evidence-presence diagnostics for IVA-bearing ledger rows.

The diagnostics pair a transaction's IVA settlement side with its evidence
presence: a positive deductible input-IVA row must carry supplier evidence,
while a positive output-IVA row without a linked document remains visible to the
operator. This is the ledger-evidence counterpart of the calculate-path
unconsumed-declarable-IVA advisory and follows the
``no-silent-under-declaration`` discipline: never let a missing-evidence row
pass in silence, and let the filing-grade verification layer decide which
legally grounded side blocks.

What counts as evidence differs by side, and the difference is statutory rather
than a severity preference. LIVA art. 97 is enumerative — "únicamente se
considerarán documentos justificativos del derecho a la deducción" followed by a
closed list — so only the purchase-invoice evidence axis or a linked, validated
reconciliation-catalogue invoice silences the deductible side. The output side
has no equivalent constitutive requirement and no CLI path that mints
issued-invoice evidence, so any linked document silences it. Holding the
deductible side to the looser test would leave the blocking gate laxer than the
statute it cites.

The trigger set is deliberately narrow. A diagnostic fires only on an ``ACTIVE``
business/mixed row with a strictly-positive IVA quota that lacks the evidence its
side requires. Explicit exempt / zero-rated / not-subject IVA categories and
non-declarable sentinels are excluded because they do not route an M303 quota.
Rows with no explicit ``iva_category`` but with a positive ``iva_amount`` remain
in scope: the IVA aggregation layer derives their domestic category from the
stored rate and bank direction before feeding M303.
"""

from __future__ import annotations

from collections.abc import Iterable
from decimal import Decimal

from ...domain.iva.components import registry_category_projection
from ...domain.iva.flow import (
    IvaFlowDirection,
    derive_flow_for_classification,
    flow_direction_for_invoice_kind,
    is_deducible_flow,
    is_devengada_flow,
)
from ...domain.iva.schema import IvaCategory
from ...domain.transactions.enums import BUSINESS_BEARING_STATES, TransactionLifecycleState
from ...domain.transactions.models import Transaction
from .invoice_kind import invoice_kind_for_direction
from .source_mesh import CalculationSourceDiagnostic

#: Diagnostic ``source_kind`` for missing supplier evidence on positive
#: deductible input IVA. Verification treats this as filing-grade blocking.
MISSING_DEDUCTIBLE_IVA_EVIDENCE_SOURCE_KIND = "deductible_iva_evidence"

#: Diagnostic ``source_kind`` for missing linked evidence on positive output IVA.
#: The current transaction model cannot yet distinguish all valid issued-invoice
#: evidence paths, so verification keeps this visible but non-blocking.
MISSING_OUTPUT_IVA_EVIDENCE_SOURCE_KIND = "output_iva_evidence"


def _positive_iva_quota(transaction: Transaction) -> bool:
    """Return whether the row contributes a strictly positive IVA quota."""
    return transaction.iva_amount is not None and transaction.iva_amount > Decimal("0")


def _row_has_linked_evidence(transaction: Transaction) -> bool:
    """Return whether the row carries any linked evidence at all.

    This is the OUTPUT-IVA test, where the requirement is visibility rather than
    legal sufficiency. Use :func:`_row_has_deduction_grade_evidence` for the
    deductible side, which is held to a stricter, statutory standard.
    """
    return bool(transaction.purchase_invoice_evidence_id) or bool(transaction.attachment_ids)


# ALT-EVIDENCE-GRADE-RATIONALE-ADVISORY: this calculate-path test is
# deliberately stricter, on the attachment_ids axis, than
# application.modelo._ledger_evidence_gate._row_has_linked_evidence, the
# filing-grade gate's deductible-side test. LIVA art. 97 is enumerative for
# this side; the filing gate's looser attachment_ids acceptance is a recorded
# divergence (see that function's own docstring), not an oversight.
def _row_has_deduction_grade_evidence(transaction: Transaction) -> bool:
    """Return whether the row carries evidence capable of supporting a deduction.

    LIVA art. 97 is enumerative, not merely constitutive: "únicamente se
    considerarán documentos justificativos del derecho a la deducción" followed
    by a closed list — the original factura, the intra-community supplier's
    factura, the customs liquidation document on an import, and the factura or
    justificante contable under reverse charge. A generic attachment is not in
    that list, so a bank statement, a delivery note or a photograph cannot make a
    deduction defensible however plausible it looks.

    The purchase-invoice evidence axis is the carrier for every one of those four
    document classes; the record's supplier and invoice metadata are optional
    precisely so a customs liquidation or a justificante contable can be
    registered through it. Accepting a bare attachment here would hold the
    blocking gate to a laxer standard than the statute it cites.

    ``invoice_id`` is credited alongside ``purchase_invoice_evidence_id``, not
    instead of it. It references the reconciliation-catalogue
    :class:`~cadrumo.domain.invoices.models.Invoice` record, which only exists behind
    the sanctioned catalogue writer's content validators (RD 1619/2012 art. 6),
    and ``ledger link`` refuses to stamp a ``Transaction.invoice_id`` that does
    not resolve to one already in that catalogue -- so its mere presence on the
    row is evidence the referenced record passed validation, without this
    module reaching into a repository to re-check it. A validated ``Invoice`` is
    STRONGER evidence than a bare ``PurchaseInvoiceEvidence`` blob (whose fields
    are all optional and carry no art. 6 content check), so crediting it here
    only widens what already passes; it never narrows what already blocks.
    """
    return bool(transaction.purchase_invoice_evidence_id) or bool(transaction.invoice_id)


def _is_cuota_bearing_iva_category(category: IvaCategory | None) -> bool:
    """Return whether ``category`` is legally expected to bear a routed cuota.

    ``None`` is treated as "not yet a cuota-bearing classification" and is
    likewise excluded, alongside the categories named in the published 0084
    evidence-exempt projection.
    """
    return category is None or category not in registry_category_projection("evidence_exempt")


def _flow_for_transaction(transaction: Transaction) -> IvaFlowDirection | None:
    """Return the IVA settlement flow for an evidence-significance test."""
    invoice_kind = invoice_kind_for_direction(transaction.direction)
    if invoice_kind is None:
        return None
    if transaction.iva_category is None:
        return flow_direction_for_invoice_kind(invoice_kind)
    if not _is_cuota_bearing_iva_category(transaction.iva_category):
        return None
    return derive_flow_for_classification(
        category=transaction.iva_category,
        invoice_direction=invoice_kind,
    )


def _transaction_missing_evidence_flow(transaction: Transaction) -> IvaFlowDirection | None:
    """Return the IVA flow requiring evidence, or ``None`` when out of scope.

    The evidence test is applied per settlement side rather than once up front,
    because the two sides answer to different standards. The deductible side is
    held to LIVA art. 97's closed list of documents that establish the right to
    deduct; the output side, which has no equivalent constitutive requirement and
    no CLI path that mints issued-invoice evidence, is satisfied by any linked
    document. That asymmetry mirrors the per-category severity split downstream,
    where only the deductible finding blocks.
    """
    if transaction.lifecycle_state is not TransactionLifecycleState.ACTIVE:
        return None
    if transaction.business_classification not in BUSINESS_BEARING_STATES:
        return None
    if not _positive_iva_quota(transaction):
        return None
    if not _is_cuota_bearing_iva_category(transaction.iva_category):
        return None
    flow = _flow_for_transaction(transaction)
    if flow is None:
        return None
    if is_deducible_flow(flow):
        return None if _row_has_deduction_grade_evidence(transaction) else flow
    return None if _row_has_linked_evidence(transaction) else flow


def _missing_evidence_diagnostic(
    transaction: Transaction,
    *,
    role: str,
    source_kind: str,
    gap: str,
) -> CalculationSourceDiagnostic:
    """Build the missing-evidence diagnostic for one IVA-bearing row.

    ``gap`` states what is actually absent, which differs by side. Saying "no
    attachment" on a deductible row that carries one would be false, and a
    refusal an operator can disprove by looking at the row teaches them to
    distrust the gate.
    """
    return CalculationSourceDiagnostic(
        reason="missing_transaction_evidence",
        source_kind=source_kind,
        binding_id=transaction.transaction_id,
        message=(f"{role} transaction {transaction.transaction_id!r} declares a positive IVA quota but {gap}."),
    )


[docs] def missing_evidence_advisory_observations( transactions: Iterable[Transaction], ) -> tuple[CalculationSourceDiagnostic, ...]: """Return missing-evidence diagnostics for positive IVA rows. A :class:`CalculationSourceDiagnostic` (reason ``missing_transaction_evidence``) is emitted for each ``ACTIVE`` business/mixed row with a strictly-positive IVA quota that lacks the evidence its settlement side requires: - a deductible input-IVA row carrying no purchase-invoice evidence and no linked, validated reconciliation-catalogue invoice. A generic attachment does NOT satisfy this side: LIVA art. 97 enumerates the documents that establish the right to deduct and an attachment is not among them. This diagnostic blocks verification downstream. - an output-IVA row whose IVA category is legally expected to bear a devengada cuota — i.e. not in the published 0084 evidence-exempt projection — carrying no linked evidence of any kind (the issued-invoice evidence gap remains visible but advisory). Rows that legitimately bear no evidence requirement — non-business / personal, exempt / zero-rated / not-subject / sentinel IVA categories, no positive IVA quota, and non-ACTIVE lifecycle states — are excluded and never fire. Args: transactions: The revision's source transactions to inspect. Returns: A tuple of non-blocking missing-evidence diagnostics, in input order. """ diagnostics: list[CalculationSourceDiagnostic] = [] for transaction in transactions: flow = _transaction_missing_evidence_flow(transaction) if flow is None: continue if is_deducible_flow(flow): diagnostics.append( _missing_evidence_diagnostic( transaction, role="deductible IVA", source_kind=MISSING_DEDUCTIBLE_IVA_EVIDENCE_SOURCE_KIND, gap=( "carries no purchase invoice evidence; LIVA art. 97 lists the " "documents that establish the right to deduct, and a generic " "attachment is not one of them, so register the invoice and " "link it before filing" ), ), ) elif is_devengada_flow(flow): diagnostics.append( _missing_evidence_diagnostic( transaction, role="output IVA", source_kind=MISSING_OUTPUT_IVA_EVIDENCE_SOURCE_KIND, gap=( "carries no linked evidence (no purchase invoice and no " "attachment); attach the supporting document before filing" ), ), ) return tuple(diagnostics)
__all__ = [ "MISSING_DEDUCTIBLE_IVA_EVIDENCE_SOURCE_KIND", "MISSING_OUTPUT_IVA_EVIDENCE_SOURCE_KIND", "missing_evidence_advisory_observations", ]