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)