Ajustes no agente de contas
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -244,6 +244,19 @@ def _preflight_subject(name: str, args: dict[str, Any]) -> dict[str, Any] | None
|
||||
return None
|
||||
if bool(args.get("clarification_resolved")):
|
||||
return None
|
||||
|
||||
# A pre-validation transaction boundary may already have resolved a multi-item
|
||||
# cancellation into authoritative ``items[]``. At execution time that list is
|
||||
# the source of truth; re-parsing the presentation ``subject`` (for example
|
||||
# ``"Tamboro Mensal, Paramount+"``) would incorrectly turn two canonical
|
||||
# entities back into one ambiguous free-text reference.
|
||||
if name == "cancelar_vas_avulso":
|
||||
resolved_items = [
|
||||
item for item in (args.get("items") or [])
|
||||
if isinstance(item, dict) and str(item.get("name") or item.get("subject") or "").strip()
|
||||
]
|
||||
if len(resolved_items) >= 2:
|
||||
return None
|
||||
msisdn = str(args.get("msisdn") or "").strip()
|
||||
subject = str(args.get("subject") or "").strip()
|
||||
if not msisdn or not subject:
|
||||
@@ -986,6 +999,49 @@ async def _validate_contestation(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"entity_resolution": "invoice_evidence",
|
||||
},
|
||||
}
|
||||
|
||||
# A claimed amount larger/different from the authoritative billed amount is
|
||||
# recoverable input, not a reason to emit a generic safety failure. Keep the
|
||||
# subject frozen and recollect only ``valor``, while telling the customer the
|
||||
# concrete amount found in the invoice evidence.
|
||||
amount_mismatch = next((
|
||||
entry for entry in (validation_log or [])
|
||||
if isinstance(entry, dict) and str(entry.get("erro") or "") in {
|
||||
"valor_ajuste_maior_que_item", "valor_ajuste_divergente_item"
|
||||
}
|
||||
), None)
|
||||
if amount_mismatch is not None:
|
||||
billed = amount_mismatch.get("valor_item_fatura")
|
||||
canonical_subject = str(
|
||||
amount_mismatch.get("item_fatura_resolvido")
|
||||
or args.get("resolved_subject") or args.get("subject") or "item"
|
||||
).strip()
|
||||
billed_ptbr = _money_ptbr(billed) if billed not in (None, "") else ""
|
||||
message = (
|
||||
f"O valor encontrado na fatura para {canonical_subject} é R$ {billed_ptbr}. "
|
||||
f"O valor informado (R$ {_money_ptbr(requested_value)}) não corresponde a essa cobrança. "
|
||||
"Informe o valor da cobrança que deseja contestar."
|
||||
if billed_ptbr else
|
||||
"O valor informado não corresponde à cobrança encontrada na fatura. Informe o valor correto da cobrança que deseja contestar."
|
||||
)
|
||||
return {
|
||||
"eligible": False,
|
||||
"status": "NEEDS_PARAMETER",
|
||||
"parameter": "valor",
|
||||
"reason": "CVAL",
|
||||
"recoverable_reason": "amount_not_supported_by_invoice",
|
||||
"subject": canonical_subject,
|
||||
"resolved_value": billed,
|
||||
"parameter_message": message,
|
||||
"validation_log": validation_log,
|
||||
"items": validated,
|
||||
"metadata": {
|
||||
"side_effect_free": True,
|
||||
"target_tool": args.get("target_tool") or "contestar_cobranca",
|
||||
"guardrail_code": "CVAL",
|
||||
"entity_resolution": "invoice_evidence",
|
||||
},
|
||||
}
|
||||
return {
|
||||
"eligible": False,
|
||||
"status": "BLOCKED",
|
||||
@@ -1145,14 +1201,79 @@ def _vas_domain_policy_from_invoice_detail(canonical: str, args: dict[str, Any])
|
||||
return "", item_type
|
||||
|
||||
|
||||
def _resolve_multiple_vas_subjects(subject: str, catalog: list[dict[str, Any]], args: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
"""Resolve explicit multi-entity cancellation references against authorized evidence.
|
||||
|
||||
Named references are split into conversational segments and each segment must
|
||||
resolve uniquely through the same catalog resolver used for single entities.
|
||||
Explicit mass expansion is restricted to "todos os VAS/serviços avulsos";
|
||||
generic "todos" is deliberately not expanded.
|
||||
"""
|
||||
raw = str(subject or "").strip()
|
||||
norm = _norm_entity_reference(raw)
|
||||
if not norm:
|
||||
return []
|
||||
explicit_all_avulso = any(token in norm for token in (
|
||||
"todos os vas avulsos", "todos vas avulsos", "todos os servicos avulsos",
|
||||
"todos servicos avulsos", "todas as assinaturas avulsas",
|
||||
))
|
||||
selected_names: list[str] = []
|
||||
if explicit_all_avulso:
|
||||
selected_names = [str(row.get("name") or "").strip() for row in catalog if row.get("name")]
|
||||
else:
|
||||
cleaned = re.sub(r"(?i)\b(cancelar|cancela|cancele|retirar|retire|tirar|tire|desativar|desative)\b", " ", raw)
|
||||
segments = [seg.strip(" .:-") for seg in re.split(r"\s*(?:,|;|/|\be\b|\bmais\b)\s*", cleaned, flags=re.I) if seg.strip(" .:-")]
|
||||
if len(segments) < 2:
|
||||
return []
|
||||
for segment in segments:
|
||||
canonical, _ = _resolve_catalog_entity(segment, catalog)
|
||||
if canonical:
|
||||
selected_names.append(canonical)
|
||||
selected: list[dict[str, Any]] = []
|
||||
for name in list(dict.fromkeys(selected_names)):
|
||||
resolved_class, resolved_type = _vas_domain_policy_from_invoice_detail(name, args)
|
||||
if explicit_all_avulso and resolved_class != "cancelar_vas_avulso":
|
||||
continue
|
||||
selected.append({
|
||||
"name": name,
|
||||
"msisdn": _invoice_detail_msisdn(args.get("invoice_detail"), name) or str(args.get("msisdn") or ""),
|
||||
"tool_category": resolved_class or None,
|
||||
"item_type": resolved_type or None,
|
||||
})
|
||||
return selected
|
||||
|
||||
|
||||
async def _validate_vas_subject(args: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Side-effect-free entity resolution used before confirmation/execution."""
|
||||
await _enrich_invoice_context("validar_vas_subject", args)
|
||||
subject = str(args.get("subject") or "").strip()
|
||||
catalog = _vas_entity_catalog(args)
|
||||
canonical, matches = _resolve_catalog_entity(subject, catalog)
|
||||
requested_tool = str(args.get("target_tool") or "").strip()
|
||||
multi = _resolve_multiple_vas_subjects(subject, catalog, args) if requested_tool == "cancelar_vas_avulso" else []
|
||||
if len(multi) >= 2 or (multi and any(token in _norm_entity_reference(subject) for token in ("todos os vas avulsos", "todos os servicos avulsos"))):
|
||||
names = [str(item.get("name") or "").strip() for item in multi if item.get("name")]
|
||||
return {
|
||||
"eligible": True,
|
||||
"status": "ELIGIBLE",
|
||||
"subject": subject,
|
||||
"resolved_subject": names[0] if len(names) == 1 else ", ".join(names),
|
||||
"resolved_subjects": names,
|
||||
"entity_resolution": "vas_evidence_multiple",
|
||||
"transaction_decision": {
|
||||
"resolved_arguments": {"subject": ", ".join(names), "items": multi},
|
||||
"target_tool": requested_tool,
|
||||
"action_changed": False,
|
||||
"requires_reconfirmation": False,
|
||||
"confirmation_message": (
|
||||
f"Você confirma o cancelamento dos serviços {', '.join(names)}? "
|
||||
"Responda 'sim' para executar ou 'não' para cancelar."
|
||||
),
|
||||
"domain_policy": {"class": "cancelar_vas_avulso", "item_type": "multiple"},
|
||||
},
|
||||
"metadata": {"side_effect_free": True, "target_tool": requested_tool, "catalog_size": len(catalog), "resolved_count": len(names)},
|
||||
}
|
||||
if canonical:
|
||||
requested_tool = str(args.get("target_tool") or "").strip()
|
||||
effective_tool = requested_tool
|
||||
resolved_class = ""
|
||||
resolved_type = ""
|
||||
|
||||
Reference in New Issue
Block a user