Files
KPK/bankterm_service.py
2026-06-23 15:20:56 +02:00

210 lines
7.7 KiB
Python

from __future__ import annotations
import json
import uuid
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import Any
import requests
from requests.auth import HTTPBasicAuth
@dataclass
class BankTerminalConfig:
terminal_type: str
url: str = ""
terminal_id: str = ""
sale_id: str = "Alto/foodw32"
user: str = ""
password: str = ""
currency: str = "CZK"
timeout: int = 60
log_path: str = "terminal_log.jsonl"
extra: dict[str, Any] = field(default_factory=dict)
@dataclass
class BankTerminalResult:
success: bool
raw: dict[str, Any] | None = None
error: str = ""
service_id: str = ""
def legacy_dict(self) -> dict[str, Any]:
result: dict[str, Any] = {
"success": self.success,
"service_id": self.service_id,
}
if self.raw is not None:
result["raw"] = self.raw
if self.error:
result["error"] = self.error
return result
class BankTerminalClient:
def __init__(self, config: BankTerminalConfig):
self.config = config
def payment(self, amount: float) -> BankTerminalResult:
raise NotImplementedError
def abort(self, service_id: str) -> BankTerminalResult:
return BankTerminalResult(success=False, error="Abort nie je podporovany.", service_id=service_id)
def refund(self, original_service_id: str, amount: float) -> BankTerminalResult:
return BankTerminalResult(success=False, error="Refund nie je podporovany.", service_id=original_service_id)
def extract_receipt_text(self, response: dict[str, Any]) -> str:
return ""
def _log_transaction(self, data: dict[str, Any]) -> None:
log_path = Path(self.config.log_path or "terminal_log.jsonl")
with log_path.open("a", encoding="utf-8") as f:
f.write(json.dumps({
"time": datetime.utcnow().isoformat(),
"terminal_type": self.config.terminal_type,
"data": data,
}, ensure_ascii=False) + "\n")
class BesteronTerminalClient(BankTerminalClient):
def _message_header(self, category: str, service_id: str) -> dict[str, str]:
return {
"ProtocolVersion": "3.0",
"MessageClass": "Service",
"MessageCategory": category,
"MessageType": "Request",
"ServiceID": service_id,
"SaleID": self.config.sale_id,
"POIID": self.config.terminal_id,
}
def payment(self, amount: float) -> BankTerminalResult:
service_id = str(uuid.uuid4())[:10]
payload = {
"SaleToPOIRequest": {
"MessageHeader": self._message_header("Payment", service_id),
"PaymentRequest": {
"SaleData": {
"SaleTransactionID": {
"TransactionID": service_id,
"TimeStamp": datetime.utcnow().isoformat(),
}
},
"PaymentTransaction": {
"AmountsReq": {
"Currency": self.config.currency,
"RequestedAmount": float(amount),
}
},
},
}
}
try:
response = requests.post(
self.config.url,
json=payload,
auth=HTTPBasicAuth(self.config.user, self.config.password),
headers={"Content-Type": "application/json"},
timeout=self.config.timeout,
)
response.raise_for_status()
data = response.json()
self._log_transaction(data)
payment_response = data.get("SaleToPOIResponse", {}).get("PaymentResponse", {})
result = payment_response.get("Response", {}).get("Result")
return BankTerminalResult(success=result == "Success", raw=data, service_id=service_id)
except Exception as exc:
return BankTerminalResult(success=False, error=str(exc), service_id=service_id)
def abort(self, service_id: str) -> BankTerminalResult:
if not service_id:
return BankTerminalResult(success=False, error="Chyba service_id pre abort.")
payload = {
"SaleToPOIRequest": {
"MessageHeader": self._message_header("Abort", service_id),
"AbortRequest": {
"AbortReason": "MerchantAbort",
},
}
}
try:
response = requests.post(
self.config.url,
json=payload,
auth=HTTPBasicAuth(self.config.user, self.config.password),
headers={"Content-Type": "application/json"},
timeout=min(self.config.timeout, 10),
)
response.raise_for_status()
data = response.json() if response.text else {}
if data:
self._log_transaction(data)
return BankTerminalResult(success=True, raw=data, service_id=service_id)
except Exception as exc:
return BankTerminalResult(success=False, error=str(exc), service_id=service_id)
def refund(self, original_service_id: str, amount: float) -> BankTerminalResult:
service_id = str(uuid.uuid4())[:10]
payload = {
"SaleToPOIRequest": {
"MessageHeader": self._message_header("Reversal", service_id),
"ReversalRequest": {
"OriginalPOITransaction": {
"POITransactionID": {
"TransactionID": original_service_id,
}
},
"ReversedAmount": float(amount),
},
}
}
try:
response = requests.post(
self.config.url,
json=payload,
auth=HTTPBasicAuth(self.config.user, self.config.password),
headers={"Content-Type": "application/json"},
timeout=min(self.config.timeout, 30),
)
response.raise_for_status()
data = response.json()
self._log_transaction(data)
reversal_response = data.get("SaleToPOIResponse", {}).get("ReversalResponse", {})
result = reversal_response.get("Response", {}).get("Result")
return BankTerminalResult(success=result in ("Success", None), raw=data, service_id=service_id)
except Exception as exc:
return BankTerminalResult(success=False, error=str(exc), service_id=service_id)
def extract_receipt_text(self, response: dict[str, Any]) -> str:
receipts = response.get("PaymentReceipt")
if receipts is None:
receipts = (
response.get("SaleToPOIResponse", {})
.get("PaymentResponse", {})
.get("PaymentReceipt", [])
)
lines: list[str] = []
for receipt in receipts:
content = receipt.get("OutputContent", {}).get("OutputText", [])
lines.extend(str(line.get("Text", "")) for line in content if line.get("Text"))
return "\n".join(lines)
class UnsupportedBankTerminalClient(BankTerminalClient):
def payment(self, amount: float) -> BankTerminalResult:
return BankTerminalResult(
success=False,
error=f"Terminal {self.config.terminal_type or '-'} zatial nie je implementovany.",
)
def create_bank_terminal_client(config: BankTerminalConfig) -> BankTerminalClient:
terminal_type = (config.terminal_type or "").strip().upper()
if terminal_type == "BESTERON":
return BesteronTerminalClient(config)
return UnsupportedBankTerminalClient(config)