Ajustes conforme relatorio de testes 2026-08-27

This commit is contained in:
2026-08-29 09:53:32 -03:00
parent 0ecff719b7
commit 88e1f070d7
791 changed files with 27040 additions and 29038 deletions

View File

@@ -36,15 +36,54 @@ def _money(value: Decimal) -> Decimal:
def _parse_amount(value: str) -> Decimal | None:
if not value:
"""Parse monetary values without assuming that every dot is a thousands separator.
Accepted examples include Brazilian and API/JSON representations such as
``R$ 19,99``, ``19.99``, ``1.999,99`` and ``1,999.99``.
"""
if value is None:
return None
cleaned = (
str(value)
.replace("R$", "")
.replace(" ", "")
.replace(".", "")
.replace(",", ".")
)
cleaned = str(value).strip().replace("R$", "").replace(" ", "")
if not cleaned:
return None
# Keep only a numeric sign and decimal/grouping separators. This avoids
# accidentally feeding currency labels or other text to Decimal.
cleaned = re.sub(r"[^0-9, .+\-]", "", cleaned).replace(" ", "")
if not cleaned:
return None
comma = cleaned.rfind(",")
dot = cleaned.rfind(".")
if comma >= 0 and dot >= 0:
# The rightmost separator is the decimal separator; the other one is
# grouping. This supports both 1.999,99 and 1,999.99.
if comma > dot:
cleaned = cleaned.replace(".", "").replace(",", ".")
else:
cleaned = cleaned.replace(",", "")
elif comma >= 0:
# pt-BR decimal notation. Multiple commas are treated conservatively
# by preserving only the last one as decimal separator.
if cleaned.count(",") > 1:
head, tail = cleaned.rsplit(",", 1)
cleaned = head.replace(",", "") + "." + tail
else:
cleaned = cleaned.replace(",", ".")
elif dot >= 0:
# A single dot followed by 1-2 digits is decimal notation (the form
# normally returned by JSON/backends). For multiple dots, keep the
# last one as decimal only when it looks like cents; otherwise treat
# them as grouping separators.
if cleaned.count(".") > 1:
head, tail = cleaned.rsplit(".", 1)
if 1 <= len(tail) <= 2:
cleaned = head.replace(".", "") + "." + tail
else:
cleaned = cleaned.replace(".", "")
try:
return Decimal(cleaned)
except Exception:
@@ -427,7 +466,48 @@ def validate_contestation_items(
0 if _normalize_match_text(candidate.get("name", "")) == _normalize_match_text(item_name) else 1,
)
)
matched_candidate = matching_candidates[0] if matching_candidates else None
# When the same subject appears more than once on the invoice, the
# requested amount is useful evidence for selecting the correct
# occurrence. Prefer an exact amount match. For a partial adjustment
# choose the smallest invoice occurrence that can cover the requested
# amount. If none can, select the largest occurrence so the generic
# ``validated > item_amount`` rule below rejects the request.
matched_candidate = None
if matching_candidates:
requested_amount = validated if validated > 0 else claimed
if requested_amount > 0 and len(matching_candidates) > 1:
monetary_candidates = [
candidate
for candidate in matching_candidates
if isinstance(candidate.get("amount"), Decimal)
and candidate.get("amount") > 0
]
exact_matches = [
candidate
for candidate in monetary_candidates
if _money(candidate["amount"]) == _money(requested_amount)
]
if exact_matches:
matched_candidate = exact_matches[0]
else:
sufficient = sorted(
(
candidate
for candidate in monetary_candidates
if candidate["amount"] >= requested_amount
),
key=lambda candidate: candidate["amount"],
)
if sufficient:
matched_candidate = sufficient[0]
elif monetary_candidates:
matched_candidate = max(
monetary_candidates,
key=lambda candidate: candidate["amount"],
)
if matched_candidate is None:
matched_candidate = matching_candidates[0]
if matched_candidate is None:
_record_failure(
item_log,