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