Files
first_contas/legacy_reference/workflows/actions/contestacao/helpers.py
2026-06-16 20:54:49 -03:00

1208 lines
37 KiB
Python

from __future__ import annotations
import logging
import re
import unicodedata
from decimal import Decimal
from calendar import monthrange
from datetime import datetime
from typing import Any
from agente_contas_tim.guardrails import validate_contestation_items
from agente_contas_tim.workflows.actions.registry import WorkflowRuntimeContext
from agente_contas_tim.workflows.actions.common.helpers import (
_contains_any_keyword,
_first_text_from_params_or_state,
_first_value_from_params_or_state,
_map_customer_status_code,
_normalize_bool,
_normalize_number_text,
_normalize_service_name,
_result_failed_or_missing_data,
_text_from_keys,
_to_bool,
_to_dict,
)
logger = logging.getLogger(__name__)
_VENCIMENTOS_CONTA_CERTA = (1, 7, 10, 12, 15, 20, 25, 30)
def _resolve_billing_cutoff_reference(
cutoff_day: int,
*,
reference_datetime: datetime | None = None,
) -> tuple[bool, datetime | None]:
if cutoff_day <= 0:
return False, None
reference = reference_datetime or datetime.now()
current_last_day = monthrange(reference.year, reference.month)[1]
current_cutoff_day = min(cutoff_day, current_last_day)
cutoff_reference = datetime(
reference.year,
reference.month,
current_cutoff_day,
)
if reference < cutoff_reference:
previous_month = reference.month - 1
previous_year = reference.year
if previous_month == 0:
previous_month = 12
previous_year -= 1
previous_last_day = monthrange(previous_year, previous_month)[1]
previous_cutoff_day = min(cutoff_day, previous_last_day)
cutoff_reference = datetime(
previous_year,
previous_month,
previous_cutoff_day,
)
return reference >= cutoff_reference, cutoff_reference
def _resolve_next_cutoff_from_cut_date(
cut_date: datetime,
*,
reference_datetime: datetime | None = None,
) -> tuple[bool, datetime]:
"""Retorna se a data de referência já passou do próximo corte.
Regra:
- manualContaCertaIndicator = False quando sysdate <= (cutDate + 1 mês)
- manualContaCertaIndicator = True quando sysdate > (cutDate + 1 mês)
"""
reference = reference_datetime or datetime.now()
next_month = cut_date.month + 1
next_year = cut_date.year
if next_month == 13:
next_month = 1
next_year += 1
next_last_day = monthrange(next_year, next_month)[1]
next_day = min(cut_date.day, next_last_day)
next_cutoff = datetime(next_year, next_month, next_day)
return reference > next_cutoff, next_cutoff
def _is_missing_amount_value(value: Any) -> bool:
text = str(value).strip().lower()
if not text or text in {"null", "none", "nan"}:
return True
normalized = _normalize_number_text(value, default="")
return normalized in {"", "0", "-0"}
def _first_invoice_status_from_items(items: Any) -> str:
if not isinstance(items, (list, tuple)):
return ""
for item in items:
if not isinstance(item, dict):
continue
for key in ("status", "invoiceStatus", "invoice_status", "invoice_status_desc"):
status = str(item.get(key) or "").strip()
if status:
return status
return ""
def _invoice_identifier(value: Any) -> str:
return re.sub(r"\D", "", str(value or "").strip())
def _invoice_item_matches_id(item: dict[str, Any], invoice_id: Any) -> bool:
target = _invoice_identifier(invoice_id)
if not target:
return False
for key in (
"id",
"invoice_id",
"invoiceId",
"invoiceNumber",
"invoice_number",
"current_invoice_number",
"currentInvoiceNumber",
):
if _invoice_identifier(item.get(key)) == target:
return True
return False
def _invoice_items_from_payload(payload: Any) -> list[dict[str, Any]]:
if not isinstance(payload, dict):
return []
out: list[dict[str, Any]] = []
for key in ("paymentItems", "payment_items", "open_invoices", "closed_invoices"):
items = payload.get(key, [])
if not isinstance(items, (list, tuple)):
continue
out.extend(item for item in items if isinstance(item, dict))
return out
def _find_invoice_item_by_id(payload: Any, invoice_id: Any) -> dict[str, Any] | None:
for item in _invoice_items_from_payload(payload):
if _invoice_item_matches_id(item, invoice_id):
return item
return None
def _to_tracking_datetime(value: Any) -> str:
raw = str(value or "").strip()
if not raw:
return ""
raw = raw.replace("Z", "")
if "T" in raw:
raw = raw.split("T", 1)[0]
for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%Y/%m/%d"):
try:
parsed = datetime.strptime(raw, fmt)
return parsed.strftime("%Y-%m-%dT00:00:00.00Z")
except ValueError:
continue
return ""
def _parse_tracking_date(value: Any) -> datetime | None:
"""Converte datas de payload de invoices para datetime (sem horário)."""
iso_candidate = _to_tracking_datetime(value)
if not iso_candidate:
return None
try:
return datetime.strptime(iso_candidate, "%Y-%m-%dT%H:%M:%S.%fZ")
except ValueError:
return None
def _parse_tracking_day(value: Any) -> int | None:
parsed_cut_date = _parse_tracking_date(value)
if parsed_cut_date is None:
return None
return parsed_cut_date.day
def _first_invoice_date_from_items(items: Any, *keys: str) -> str:
if not isinstance(items, (list, tuple)):
return ""
for item in items:
if not isinstance(item, dict):
continue
for key in keys:
formatted = _to_tracking_datetime(item.get(key))
if formatted:
return formatted
return ""
def _first_invoice_date_from_payload_by_id(
payload: Any,
invoice_id: Any,
*keys: str,
) -> str:
item = _find_invoice_item_by_id(payload, invoice_id)
if not item:
return ""
return _first_invoice_date_from_items([item], *keys)
def _first_invoice_text_from_payload_by_id(
payload: Any,
invoice_id: Any,
*keys: str,
) -> str:
item = _find_invoice_item_by_id(payload, invoice_id)
if not item:
return ""
for key in keys:
text = str(item.get(key) or "").strip()
if text:
return text
return ""
def _normalize_due_date_yyyymmdd(value: Any) -> str:
text = str(value).strip()
if not text:
return ""
digits = re.sub(r"\D", "", text)
if len(digits) != 8:
return ""
if "/" in text or "-" in text:
day = digits[:2]
month = digits[2:4]
year = digits[4:]
if day.isdigit() and month.isdigit() and year.isdigit():
if int(day) <= 31 and int(month) <= 12:
return f"{year}{month}{day}"
return digits
def _is_dacc_cartao_parcelamento(payment_method: Any) -> bool:
text = str(payment_method or "").strip().lower()
if not text:
return False
# Aceita variações com/sem acento e separadores.
return _contains_any_keyword(
text,
(
"dacc",
"cartao",
"cartão",
"parcelamento",
),
)
def _is_invoice_paid_status(status_text: Any) -> bool:
return _contains_any_keyword(
str(status_text or ""),
("pago", "quitado", "paid"),
)
def _is_invoice_unpaid_status(status_text: Any) -> bool:
return _contains_any_keyword(
str(status_text or ""),
("em aberto", "a vencer", "nao pago", "não pago", "pendente", "aberto"),
)
def _is_invoice_contested_status(status_text: Any) -> bool:
return _contains_any_keyword(
str(status_text or ""),
("contest",),
)
def _map_invoice_status_to_state(value: Any) -> str:
text = str(value or "").strip().upper()
if not text:
return ""
text = unicodedata.normalize("NFKD", text)
text = "".join(c for c in text if not unicodedata.combining(c))
text = text.replace("/", " ")
text = re.sub(r"\s+", " ", text).strip()
direct_map = {
"PAGO": "Fechada",
"ABERTO CC": "Fechada",
"EM ATRASO CC": "Fechada",
"EM ATRASO DEB AUT": "Fechada",
"ABERTO DEB AUT": "Fechada",
"EM ATRASO PIX": "Fechada",
"ABERTO PIX": "Fechada",
"A VENCER": "Aberto",
"EM ACORDO": "Aberto",
"EM ATRASO": "Aberto",
"EM ABERTO": "Aberto",
"EM PROCESSAMENTO": "Fechada",
"CONSULTA INDISPONIVEL": "Fechada",
"PARCELADO": "Negociada",
"CANCELADO EXPIRADO": "Fechada",
}
return direct_map.get(text, "")
def _map_invoice_status_code(value: Any) -> str:
text = str(value or "").strip().lower()
if not text:
return ""
return "1" if "abert" in text else "0"
def _first_invoice_status_from_payload(payload: Any, *, invoice_id: Any = "") -> str:
if not isinstance(payload, dict):
return ""
matched_item = _find_invoice_item_by_id(payload, invoice_id)
invoice_status = _first_invoice_status_from_items([matched_item])
if invoice_status:
return invoice_status
invoice_id_text = _invoice_identifier(invoice_id)
has_invoice_filter = bool(invoice_id_text)
if has_invoice_filter:
# Se o fluxo já definiu a fatura, somente itens dessa fatura são válidos.
return ""
invoice_status = str(payload.get("invoice_status") or "").strip()
if invoice_status:
return invoice_status
invoice_status = (
_first_invoice_status_from_items(payload.get("open_invoices"))
or _first_invoice_status_from_items(payload.get("closed_invoices"))
)
if invoice_status:
return invoice_status
statuses = payload.get("invoice_statuses", [])
if not isinstance(statuses, (list, tuple)):
return ""
for raw_status in statuses:
status_text = str(raw_status or "").strip()
if status_text:
return status_text
return ""
def _collect_invoice_status_texts(payload: dict[str, Any]) -> list[str]:
out: list[str] = []
if not isinstance(payload, dict):
return out
statuses = payload.get("invoice_statuses", [])
if isinstance(statuses, list):
for raw in statuses:
text = str(raw or "").strip()
if text:
out.append(text)
for key in ("open_invoices", "closed_invoices"):
items = payload.get(key, [])
if not isinstance(items, (list, tuple)):
continue
for item in items:
if not isinstance(item, dict):
continue
for status_key in (
"status",
"invoiceStatus",
"invoice_status",
"invoice_status_desc",
):
text = str(item.get(status_key) or "").strip()
if text:
out.append(text)
break
return out
def _invoice_status_texts_from_payload(
payload: Any,
*,
invoice_id: Any = "",
) -> list[str]:
has_invoice_filter = bool(_invoice_identifier(invoice_id))
item = _find_invoice_item_by_id(payload, invoice_id)
if item:
status = _first_invoice_status_from_items([item])
return [status] if status else []
if has_invoice_filter:
return []
return _collect_invoice_status_texts(payload if isinstance(payload, dict) else {})
def _has_open_bill_from_payload(
payload: Any,
*,
invoice_id: Any = "",
default: bool = False,
) -> bool:
item = _find_invoice_item_by_id(payload, invoice_id)
if item:
status = _first_invoice_status_from_items([item])
return _is_invoice_unpaid_status(status)
if isinstance(payload, dict) and "has_open_bill" in payload:
return bool(payload.get("has_open_bill"))
return default
def _resolve_refund_and_resolution_decision(
*,
payment_method: str,
has_open_bill: bool,
invoice_status_texts: list[str],
) -> dict[str, Any]:
is_dacc = _is_dacc_cartao_parcelamento(payment_method)
is_contested = any(_is_invoice_contested_status(s) for s in invoice_status_texts)
is_paid = any(_is_invoice_paid_status(s) for s in invoice_status_texts) and not has_open_bill
is_unpaid = has_open_bill or any(_is_invoice_unpaid_status(s) for s in invoice_status_texts)
should_generate_boleto = is_unpaid and not is_dacc and not is_contested
refund_option = "1" if should_generate_boleto else "0"
format_text = "sms" if should_generate_boleto else "conta_futura"
resolution_type = "new_boleto" if should_generate_boleto else "credit_bill"
decision_reason = "credito_conta_futura"
if should_generate_boleto:
decision_reason = "fatura_nao_paga"
elif is_dacc:
decision_reason = "forma_pagamento_dacc_cartao_parcelamento"
elif is_contested:
decision_reason = "fatura_ja_contestada"
elif is_paid:
decision_reason = "fatura_paga"
logger.info(
"contestacao.decisao_refund_decision "
"payment_method=%s has_open_bill=%s is_dacc=%s is_contested=%s is_paid=%s is_unpaid=%s "
"refund_option=%s format_text=%s resolution_type=%s decision_reason=%s",
payment_method,
has_open_bill,
is_dacc,
is_contested,
is_paid,
is_unpaid,
"1" if should_generate_boleto else "0",
"sms" if should_generate_boleto else "conta_futura",
"new_boleto" if should_generate_boleto else "credit_bill",
decision_reason,
)
return {
"refund_option": refund_option,
"format_text": format_text,
"resolution_type": resolution_type,
"decision_reason": decision_reason,
"is_dacc": is_dacc,
"is_paid": is_paid,
"is_unpaid": is_unpaid,
"is_contested": is_contested,
}
def _resolve_contestation_due_date(
state: dict[str, Any],
params: dict[str, Any],
) -> str:
raw_due_date = _first_text_from_params_or_state(
state,
params,
"current_invoice_due_date",
"currentInvoiceDueDate",
"CurrentInvoiceDueDate",
"invoice_due_date",
"invoiceDueDate",
"InvoiceDueDate",
)
if raw_due_date:
return _normalize_due_date_yyyymmdd(raw_due_date)
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
invoice_id = _first_text_from_params_or_state(
state,
params,
"invoice_id",
"invoiceId",
"current_invoice_number",
"currentInvoiceNumber",
"invoice_number",
"invoiceNumber",
)
has_invoice_filter = bool(_invoice_identifier(invoice_id))
profile_payload = (
vars_state.get("consultar_perfil_fatura", {})
if isinstance(vars_state, dict)
else {}
)
if isinstance(profile_payload, dict):
profile_due_date_by_id = _first_invoice_text_from_payload_by_id(
profile_payload,
invoice_id,
"dueDate",
"due_date",
)
if profile_due_date_by_id:
return _normalize_due_date_yyyymmdd(profile_due_date_by_id)
profile_due_date = str(
profile_payload.get("due_date")
or profile_payload.get("dueDate")
or ""
).strip()
if profile_due_date and not has_invoice_filter:
return _normalize_due_date_yyyymmdd(profile_due_date)
check_payload = (
vars_state.get("check_invoice_status", {})
if isinstance(vars_state, dict)
else {}
)
if not isinstance(check_payload, dict):
return ""
check_due_date_by_id = _first_invoice_text_from_payload_by_id(
check_payload,
invoice_id,
"dueDate",
"due_date",
)
if check_due_date_by_id:
return _normalize_due_date_yyyymmdd(check_due_date_by_id)
raw_due_date = str(
check_payload.get("due_date") or check_payload.get("dueDate") or ""
).strip()
if raw_due_date and not has_invoice_filter:
return _normalize_due_date_yyyymmdd(raw_due_date)
if has_invoice_filter:
return ""
for invoice_key in ("open_invoices", "closed_invoices"):
invoices = check_payload.get(invoice_key, [])
if not isinstance(invoices, list):
continue
for invoice in invoices:
if not isinstance(invoice, dict):
continue
candidate = str(
invoice.get("dueDate")
or invoice.get("due_date")
or ""
).strip()
if candidate:
return _normalize_due_date_yyyymmdd(candidate)
return ""
def _extrair_dia_vencimento(due_date: str) -> str:
"""Extrai o dia de vencimento e retorna o valor válido mais próximo.
Aceita formatos YYYYMMDD, YYYY-MM-DD, DD/MM/YYYY.
Retorna string com zero-fill: "01", "07", "10", "12", "15", "20", "25", "30".
"""
text = str(due_date).strip()
if not text:
return "01"
digits = re.sub(r"\D", "", text)
day: int | None = None
if len(digits) == 8:
if "/" in text or (len(text) >= 3 and text[2] in "/-"):
# DD/MM/YYYY ou DD-MM-YYYY
day = int(digits[:2])
else:
# YYYYMMDD
day = int(digits[6:8])
elif len(digits) >= 2:
day = int(digits[-2:])
if day is None or day < 1 or day > 31:
return "01"
closest = min(_VENCIMENTOS_CONTA_CERTA, key=lambda v: abs(v - day))
return f"{closest:02d}"
def _is_plano_familia(value: Any) -> bool:
if isinstance(value, str):
normalized = _normalize_service_name(value)
return "familia" in normalized or "family" in normalized
if isinstance(value, dict):
for key in ("is_family", "isFamily", "familia", "family"):
if key in value and _normalize_bool(value.get(key), default=False):
return True
text = _text_from_keys(
value,
(
"desc",
"name",
"item_name",
"nome_plano",
"plan_name",
"planName",
"offer_name",
"offerName",
"commercialName",
"productName",
),
)
normalized = _normalize_service_name(text)
return "familia" in normalized or "family" in normalized
def _iter_plan_candidates(source: Any) -> list[Any]:
if not isinstance(source, dict):
return []
candidates: list[Any] = []
for key in (
"planos",
"plans",
"plan",
"plano",
"nome_plano",
"nomePlano",
"plan_name",
"planName",
"offer",
"oferta",
"offers",
"ofertas",
"product",
"products",
"mainProduct",
"service",
"services",
):
value = source.get(key)
if isinstance(value, list):
candidates.extend(value)
elif value is not None:
candidates.append(value)
return candidates
def _has_plano_familia(*sources: Any) -> bool:
for source in sources:
for candidate in _iter_plan_candidates(source):
if _is_plano_familia(candidate):
return True
if _is_plano_familia(source):
return True
return False
def _is_dependent_access_value(value: Any) -> bool:
if isinstance(value, bool):
return value
text = _normalize_service_name(str(value or ""))
return "depend" in text
def _has_dependent_access(*sources: Any) -> bool:
for source in sources:
if not isinstance(source, dict):
continue
for key in (
"dependent",
"dependente",
"is_dependent",
"isDependent",
"tipo_acesso",
"tipoAcesso",
"access_type",
"accessType",
"line_role",
"lineRole",
"role",
"holder_type",
"holderType",
"subscriber_type",
"subscriberType",
):
if key in source and _is_dependent_access_value(source.get(key)):
return True
for candidate in _iter_plan_candidates(source):
if isinstance(candidate, dict) and _has_dependent_access(candidate):
return True
return False
def _is_plano_familia_dependent_access(
state: dict[str, Any],
params: dict[str, Any],
contract_payload: dict[str, Any] | None = None,
) -> bool:
input_state = state.get("input", {}) if isinstance(state, dict) else {}
sources = [
params if isinstance(params, dict) else {},
input_state if isinstance(input_state, dict) else {},
]
if isinstance(contract_payload, dict):
sources.append(contract_payload)
contract = contract_payload.get("contract")
if isinstance(contract, dict):
sources.append(contract)
billing_profile = contract_payload.get("billing_profile")
if isinstance(billing_profile, dict):
sources.append(billing_profile)
return _has_plano_familia(*sources) and _has_dependent_access(*sources)
def _resolve_manual_conta_certa_indicator_default(
state: dict[str, Any],
params: dict[str, Any],
runtime: WorkflowRuntimeContext,
*,
msisdn: str,
) -> bool:
invoice_id = _first_text_from_params_or_state(
state,
params,
"invoice_id",
"invoiceId",
"current_invoice_number",
"currentInvoiceNumber",
"invoice_number",
"invoiceNumber",
)
if _is_plano_familia_dependent_access(state, params):
return True
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
if isinstance(vars_state, dict):
check_invoice_payload = vars_state.get("check_invoice_status", {})
if isinstance(check_invoice_payload, dict):
invoice_item = (
_find_invoice_item_by_id(check_invoice_payload, invoice_id)
if invoice_id
else None
)
if isinstance(invoice_item, dict):
for key in ("cutDate", "cutoffDate"):
parsed_cut_date = _parse_tracking_date(invoice_item.get(key))
if parsed_cut_date is not None:
return _resolve_next_cutoff_from_cut_date(
parsed_cut_date
)[0]
parsed_cut_day = _parse_tracking_day(invoice_item.get(key))
if parsed_cut_day is not None:
return _resolve_billing_cutoff_reference(parsed_cut_day)[0]
cutoff_payload = vars_state.get("consultar_contrato_corte", {})
if isinstance(cutoff_payload, dict):
if _normalize_bool(
cutoff_payload.get("plano_familia_acesso_dependente"),
default=False,
):
return True
data_corte_flag = str(cutoff_payload.get("data_corte_flag") or "").strip()
if data_corte_flag == "0":
return True
if data_corte_flag == "1":
return False
try:
dia_corte = int(cutoff_payload.get("dia_corte") or 0)
except (TypeError, ValueError):
dia_corte = 0
if dia_corte > 0:
return _resolve_billing_cutoff_reference(dia_corte)[0]
if not msisdn:
return False
result = runtime.factory.create_contrato(msisdn=msisdn).execute()
if _result_failed_or_missing_data(result, state=state):
return False
payload = _to_dict(result.data)
if _is_plano_familia_dependent_access(state, params, payload):
return True
billing = payload.get("billing_profile", {}) if isinstance(payload, dict) else {}
if not isinstance(billing, dict):
billing = {}
date_info = billing.get("date", {})
if not isinstance(date_info, dict):
date_info = {}
try:
dia_corte = int(date_info.get("cutoffDay") or 0)
except (TypeError, ValueError):
dia_corte = 0
if dia_corte <= 0:
return False
return _resolve_billing_cutoff_reference(dia_corte)[0]
def _resolve_contestation_request_rules(
state: dict[str, Any],
params: dict[str, Any],
runtime: WorkflowRuntimeContext,
*,
msisdn: str,
invoice_number: str,
invoice_payload: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""Resolve campos de negócio usados no abrir_contestacao_cliente."""
vars_state = state.get("vars", {}) if isinstance(state, dict) else {}
items = _build_contestation_items(state, params)
if not items:
return {
"validation_error": "items obrigatório para abrir contestação",
"validation_log": [],
"item_validation_skipped": False,
"items": [],
"customer_status": "1",
"invoice_status": "0",
"invoice_due_date": "",
"invoice_amount_open": "0",
"invoice_amount": "0",
"refund_option": "0",
"manual_conta_certa_indicator": False,
}
claimed_total = Decimal("0")
validated_total = Decimal("0")
for item in items:
claimed_amount = _normalize_number_text(item.get("claimed_amount", "0"))
validated_amount = _normalize_number_text(item.get("validated_amount", "0"))
claimed_total += Decimal(claimed_amount)
validated_total += Decimal(validated_amount)
logger.info(
"contestacao.item_values_sum msisdn=%s total_claimed=%s total_validated=%s itens=%s",
msisdn,
format(claimed_total, "f"),
format(validated_total, "f"),
[
{
"item_name": item.get("item_name", ""),
"claimed_amount": _normalize_number_text(
item.get("claimed_amount", "")
),
"validated_amount": _normalize_number_text(
item.get("validated_amount", "")
),
}
for item in items
],
)
customer_status = _first_text_from_params_or_state(
state,
params,
"customer_status",
"customerStatus",
)
if not customer_status:
contract = _first_value_from_params_or_state(state, params, "contract")
if isinstance(contract, dict):
customer_status = str(contract.get("msisdn_status", "")).strip()
customer_status = _map_customer_status_code(customer_status) or "1"
invoice_status = _first_text_from_params_or_state(
state,
params,
"invoice_status",
"invoiceStatus",
)
if not invoice_status and isinstance(vars_state, dict):
invoice_status = _first_invoice_status_from_payload(
vars_state.get("check_invoice_status", {}),
invoice_id=invoice_number,
)
if not invoice_status:
return {
"validation_error": "invoiceStatus da fatura obrigatório para abrir contestação",
"validation_log": [],
"item_validation_skipped": False,
"items": items,
"customer_status": customer_status,
"invoice_status": "0",
"invoice_due_date": "",
"invoice_amount_open": format(claimed_total, "f"),
"invoice_amount": format(validated_total, "f"),
"refund_option": "0",
"manual_conta_certa_indicator": False,
}
refund_option = _first_text_from_params_or_state(
state,
params,
"refund_option",
"refundOption",
)
if not refund_option:
has_open_bill = False
payment_method = ""
invoice_status_texts: list[str] = []
if isinstance(vars_state, dict):
perfil_payload = vars_state.get("consultar_perfil_fatura", {})
if isinstance(perfil_payload, dict):
has_open_bill = _has_open_bill_from_payload(
perfil_payload,
invoice_id=invoice_number,
default=has_open_bill,
)
payment_method = str(
perfil_payload.get("method_payment")
or perfil_payload.get("forma_pagamento")
or ""
).strip()
invoice_status_texts.extend(
_invoice_status_texts_from_payload(
perfil_payload,
invoice_id=invoice_number,
)
)
check_payload = vars_state.get("check_invoice_status", {})
if isinstance(check_payload, dict):
has_open_bill = _has_open_bill_from_payload(
check_payload,
invoice_id=invoice_number,
default=has_open_bill,
)
invoice_status_texts.extend(
_invoice_status_texts_from_payload(
check_payload,
invoice_id=invoice_number,
)
)
decision = _resolve_refund_and_resolution_decision(
payment_method=payment_method,
has_open_bill=has_open_bill,
invoice_status_texts=invoice_status_texts,
)
refund_option = str(decision["refund_option"])
invoice_due_date = _resolve_contestation_due_date(state, params)
raw_invoice_amount_open = _first_text_from_params_or_state(
state,
params,
"invoice_amount_open",
"invoiceAmountOpen",
)
raw_invoice_amount = _first_text_from_params_or_state(
state,
params,
"invoice_amount",
"invoiceAmount",
"valor",
"amount",
)
claimed_total_str = format(claimed_total, "f")
validated_total_str = format(validated_total, "f")
if _is_missing_amount_value(raw_invoice_amount_open):
invoice_amount_open = claimed_total_str
else:
invoice_amount_open = _normalize_number_text(
raw_invoice_amount_open,
default=claimed_total_str,
)
if _is_missing_amount_value(raw_invoice_amount):
invoice_amount = validated_total_str
else:
invoice_amount = _normalize_number_text(
raw_invoice_amount,
default=validated_total_str,
)
resolved_manual_conta_certa_indicator = _resolve_manual_conta_certa_indicator_default(
state,
params,
runtime,
msisdn=msisdn,
)
raw_manual_indicator = _first_value_from_params_or_state(
state,
params,
"manual_conta_certa_indicator",
"manualContaCertaIndicator",
)
manual_conta_certa_indicator = (
resolved_manual_conta_certa_indicator
or _normalize_bool(raw_manual_indicator, default=False)
)
validation_log: list[dict[str, Any]] = []
item_validation_skipped = _should_skip_contestation_item_validation(
state,
params,
items,
)
if item_validation_skipped:
validation_log = []
else:
resolved_invoice_payload = invoice_payload if isinstance(invoice_payload, dict) else {}
items, validation_log, validation_error = validate_contestation_items(
items=items,
invoice_payload=resolved_invoice_payload,
)
if validation_error:
return {
"validation_error": validation_error,
"validation_log": validation_log,
"item_validation_skipped": item_validation_skipped,
"items": items,
"customer_status": customer_status,
"invoice_status": invoice_status,
"invoice_due_date": invoice_due_date,
"invoice_amount_open": invoice_amount_open,
"invoice_amount": invoice_amount,
"refund_option": refund_option,
"manual_conta_certa_indicator": manual_conta_certa_indicator,
}
return {
"validation_error": "",
"validation_log": validation_log,
"item_validation_skipped": item_validation_skipped,
"items": items,
"customer_status": customer_status,
"invoice_status": invoice_status,
"invoice_due_date": invoice_due_date,
"invoice_amount_open": invoice_amount_open,
"invoice_amount": invoice_amount,
"refund_option": refund_option,
"manual_conta_certa_indicator": manual_conta_certa_indicator,
}
def _should_skip_contestation_item_validation(
state: dict[str, Any],
params: dict[str, Any],
items: list[dict[str, Any]],
) -> bool:
if _to_bool(
_first_value_from_params_or_state(
state,
params,
"skip_invoice_item_validation",
"skipInvoiceItemValidation",
)
):
return True
scenario = _first_text_from_params_or_state(
state,
params,
"tipo_atendimento",
"scenario",
)
scenario_key = _normalize_service_name(scenario).replace(" ", "_")
if scenario_key == "pro_rata":
return True
return bool(items) and all(
str(item.get("item_type", "")).strip().upper() == "PRO_RATA"
for item in items
)
def _build_contestation_items(
state: dict[str, Any],
params: dict[str, Any],
) -> list[dict[str, Any]]:
def _resolve_amount_with_fallback(
primary: Any,
fallback: Any,
) -> str:
if _is_missing_amount_value(primary) and not _is_missing_amount_value(fallback):
return _normalize_number_text(fallback, default="0")
if _is_missing_amount_value(primary):
return "0"
return _normalize_number_text(primary, default="0")
raw_items = _first_value_from_params_or_state(
state,
params,
"items",
)
items: list[dict[str, Any]] = []
if isinstance(raw_items, list):
def _first_present_amount(raw_item: dict[str, Any], *keys: str) -> Any:
for key in keys:
value = raw_item.get(key)
if not _is_missing_amount_value(value):
return value
return None
for raw_item in raw_items:
if not isinstance(raw_item, dict):
continue
item_name = str(
raw_item.get("item_name")
or raw_item.get("itemName")
or raw_item.get("name")
or raw_item.get("servico")
or raw_item.get("service_name")
or ""
).strip()
if not item_name:
continue
item_type = str(
raw_item.get("item_type")
or raw_item.get("itemType")
or raw_item.get("type")
or "VAS_AVULSO"
).strip() or "VAS_AVULSO"
claimed_raw = _first_present_amount(
raw_item,
"claimed_amount",
"claimedAmount",
)
validated_raw = _first_present_amount(
raw_item,
"validated_amount",
"validatedAmount",
)
claimed_amount = _resolve_amount_with_fallback(
claimed_raw,
validated_raw,
)
validated_amount = _resolve_amount_with_fallback(
validated_raw,
claimed_raw,
)
items.append(
{
"item_name": item_name,
"item_type": item_type,
"claimed_amount": claimed_amount,
"validated_amount": validated_amount,
}
)
if items:
return items
servico = _first_text_from_params_or_state(
state,
params,
"servico",
"service_name",
"item_name",
"itemName",
"name",
)
valor = _first_text_from_params_or_state(
state,
params,
"valor",
"amount",
"price",
"claimed_amount",
"claimedAmount",
"validated_amount",
"validatedAmount",
)
if not servico:
return []
normalized_value = _normalize_number_text(valor, default="0")
return [
{
"item_name": servico,
"item_type": "VAS_AVULSO",
"claimed_amount": normalized_value,
"validated_amount": normalized_value,
}
]
__all__ = [
'_first_invoice_status_from_items',
'_invoice_identifier',
'_invoice_item_matches_id',
'_invoice_items_from_payload',
'_find_invoice_item_by_id',
'_to_tracking_datetime',
'_first_invoice_date_from_items',
'_first_invoice_date_from_payload_by_id',
'_first_invoice_text_from_payload_by_id',
'_normalize_due_date_yyyymmdd',
'_is_dacc_cartao_parcelamento',
'_is_invoice_paid_status',
'_is_invoice_unpaid_status',
'_is_invoice_contested_status',
'_map_invoice_status_to_state',
'_map_invoice_status_code',
'_first_invoice_status_from_payload',
'_collect_invoice_status_texts',
'_invoice_status_texts_from_payload',
'_has_open_bill_from_payload',
'_resolve_refund_and_resolution_decision',
'_resolve_contestation_due_date',
'_extrair_dia_vencimento',
'_is_plano_familia',
'_iter_plan_candidates',
'_has_plano_familia',
'_is_dependent_access_value',
'_has_dependent_access',
'_is_plano_familia_dependent_access',
'_resolve_manual_conta_certa_indicator_default',
'_resolve_contestation_request_rules',
'_should_skip_contestation_item_validation',
'_build_contestation_items',
]