Ajustes conforme relatorio de testes 2026-08-27
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user