from decimal import Decimal from typing import Any, Dict, List CARD_LAST_DIGITS = "1234" CARD_VALID_UNTIL = "2030-12-31" # 12/30 para todas las tarjetas VOUCHER_MAX_LEN = 20 # SAP rechaza VoucherNum más largo; VoucherNum = name de la orden de Shopify class PaymentConfigError(Exception): """El pago no se puede armar (medio sin configurar, montos que no cuadran, etc.).""" # ============================================================================ def transacciones_efectivas(transactions: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """ Transacciones que representan dinero realmente cobrado: status success y kind sale/capture. Se ignoran failure/pending y refund/void. Si una authorization tiene su capture (parent_id), solo cuenta la capture. """ exitosas = [t for t in transactions if t.get("status") == "success"] autorizaciones_capturadas = {t.get("parent_id") for t in exitosas if t.get("kind") == "capture" and t.get("parent_id")} efectivas = [] for t in exitosas: if t.get("kind") == "sale": efectivas.append(t) elif t.get("kind") == "capture": efectivas.append(t) # authorization sin capture y el resto de kinds no suman return [t for t in efectivas if t.get("id") not in autorizaciones_capturadas] # ============================================================================ def _metodo(store_config: Dict[str, Any], gateway: str) -> Dict[str, Any]: metodos = store_config["sap"].get("payment_methods") or {} metodo = metodos.get(gateway) if not metodo: raise PaymentConfigError(f"Medio de pago '{gateway}' no está configurado en sap.payment_methods") return metodo def construir_incoming_payment( order: Dict[str, Any], transactions: List[Dict[str, Any]], store_config: Dict[str, Any], invoice: Dict[str, Any], invoice_doc_entry: int, invoice_total: float, ) -> Dict[str, Any]: """ Arma el JSON para POST IncomingPayments, aplicado a la Invoice de reserva ya creada (CardCode, CardName y fecha salen de esa Invoice). Los medios de pago salen de las transacciones efectivas (no de payment_gateway_names): cada una según sap.payment_methods[gateway]: - credit_card -> una línea en PaymentCreditCards - transfer -> suma a TransferSum con su transfer_account La suma debe igualar el total de la Invoice. """ series = store_config["sap"]["numbering_series"]["INCOMING_PAYMENT"] doc_date = invoice["DocDate"] efectivas = transacciones_efectivas(transactions) if not efectivas: raise PaymentConfigError("La orden no tiene transacciones de pago exitosas") tarjetas: List[Dict[str, Any]] = [] transferencia = Decimal("0") cuentas_transfer = set() gateways: List[str] = [] for t in efectivas: gateway = t.get("gateway") metodo = _metodo(store_config, gateway) monto = Decimal(str(t["amount"])) gateways.append(gateway) if metodo["type"] == "credit_card": if not metodo.get("credit_card"): raise PaymentConfigError(f"Medio de pago '{gateway}' sin tarjeta de crédito (credit_card) asociada") linea = { "CreditCard": metodo["credit_card"], "CreditCardNumber": CARD_LAST_DIGITS, "CardValidUntil": CARD_VALID_UNTIL, "VoucherNum": str(order.get("name") or "")[:VOUCHER_MAX_LEN], "CreditSum": float(monto), "CreditCur": "CLP", "CreditType": "cr_Regular", "NumOfPayments": metodo.get("num_of_payments", 1), "FirstPaymentDue": doc_date, "SplitPayments": "tYES" if metodo.get("split_payment") else "tNO", } if metodo.get("payment_method_code"): linea["PaymentMethodCode"] = metodo["payment_method_code"] tarjetas.append(linea) elif metodo["type"] == "transfer": if not metodo.get("transfer_account"): raise PaymentConfigError(f"Medio de pago '{gateway}' sin transfer_account") cuentas_transfer.add(metodo["transfer_account"]) transferencia += monto else: raise PaymentConfigError(f"Medio de pago '{gateway}' con type inválido: {metodo['type']!r}") if len(cuentas_transfer) > 1: raise PaymentConfigError(f"Transferencias con cuentas distintas no soportado: {sorted(cuentas_transfer)}") total_pagado = transferencia + sum((Decimal(str(l["CreditSum"])) for l in tarjetas), Decimal("0")) if total_pagado != Decimal(str(invoice_total)): raise PaymentConfigError(f"Suma de pagos ({total_pagado}) no coincide con el total de la Invoice ({invoice_total})") payload: Dict[str, Any] = { "CardCode": invoice["CardCode"], "CardName": invoice["CardName"], "DocType": "rCustomer", "DocDate": doc_date, "Series": series, "DocCurrency": "CLP", "Remarks": " + ".join(dict.fromkeys(gateways)), "PaymentInvoices": [ { "DocEntry": invoice_doc_entry, "InvoiceType": "it_Invoice", "SumApplied": invoice_total, } ], } if tarjetas: payload["PaymentCreditCards"] = tarjetas if transferencia: payload["TransferAccount"] = next(iter(cuentas_transfer)) payload["TransferSum"] = float(transferencia) payload["TransferDate"] = doc_date payload["TransferReference"] = order.get("name") return payload