Skip to content

Commit 03bb4fc

Browse files
add startup check for payment service
1 parent db1e234 commit 03bb4fc

File tree

16 files changed

+128
-18
lines changed

16 files changed

+128
-18
lines changed

src/api/api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from src.programs.addons import ADDONS, CouldNotInstallAddonError
2727
from src.resource_stats import start_resource_tracker
2828
from src.service.nfc_payment_service import NFCPaymentService
29-
from src.startup_checks import can_update, connection_okay, is_python_deprecated
29+
from src.startup_checks import can_update, check_payment_service, connection_okay, is_python_deprecated
3030
from src.updater import UpdateInfo, Updater
3131
from src.utils import get_platform_data
3232

@@ -48,6 +48,11 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
4848
shared.startup_need_time_adjustment.set_issue()
4949
if is_python_deprecated():
5050
shared.startup_python_deprecated.set_issue()
51+
payment_check = check_payment_service()
52+
if not payment_check.ok:
53+
_logger.warning(f"Payment service check failed: {payment_check.reason}. Disabling payment.")
54+
cfg.PAYMENT_TYPE = "Disabled"
55+
shared.startup_payment_issue.set_issue(message=payment_check.reason)
5156
mc = MachineController()
5257
mc.init_machine()
5358
ADDONS.setup_addons()

src/api/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ class IssueData(BaseModel):
111111
deprecated: StartupIssue
112112
internet: StartupIssue
113113
config: StartupIssue
114+
payment: StartupIssue
114115

115116

116117
class DateTimeInput(BaseModel):

src/api/routers/options.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ async def check_issues() -> IssueData:
350350
deprecated=shared.startup_python_deprecated,
351351
internet=shared.startup_need_time_adjustment,
352352
config=shared.startup_config_issue,
353+
payment=shared.startup_payment_issue,
353354
)
354355

355356

@@ -358,6 +359,7 @@ async def ignore_issues() -> ApiMessage:
358359
shared.startup_python_deprecated.set_ignored()
359360
shared.startup_need_time_adjustment.set_ignored()
360361
shared.startup_config_issue.set_ignored()
362+
shared.startup_payment_issue.set_ignored()
361363
return ApiMessage(message="Issues ignored")
362364

363365

src/config/config_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,7 @@ def __init__(self) -> None:
482482
self.startup_need_time_adjustment = StartupIssue()
483483
self.startup_python_deprecated = StartupIssue()
484484
self.startup_config_issue = StartupIssue()
485+
self.startup_payment_issue = StartupIssue()
485486

486487

487488
def version_callback(value: bool) -> None:

src/dialog_handler.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,10 @@ def say_python_deprecated(self, sys_python: str, program_python: str) -> None:
478478
"""Inform that the given system python is older than the recommended python for the program."""
479479
self.__output_language_dialog("python_deprecated", sys_python=sys_python, program_python=program_python)
480480

481+
def say_payment_disabled(self, reason: str) -> None:
482+
"""Inform the user that the payment service was disabled at startup."""
483+
self.__output_language_dialog("payment_disabled", reason=reason)
484+
481485
def say_welcome_message(self) -> None:
482486
"""Display the welcome dialog, show version and platform info."""
483487
self.__output_language_dialog(

src/language.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,9 @@ dialog:
184184
python_deprecated:
185185
en: 'Your system Python is {sys_python}. In the future, {program_python} is required. Please upgrade, otherwise a future CocktailBerry update will break your program. Also, the auto update function will not show new updates until then.'
186186
de: 'Die Python Version des Systems is {sys_python}. In Zukunft wird {program_python} benötigt. Bitte upgrade diese bald, sonst wird mit einem kommenden Update CocktailBerry nicht mehr funktionieren. Die Autoupdate Funktion wird solange keine neuen Updates anzeigen.'
187+
payment_disabled:
188+
en: "The payment service was disabled because of a startup issue:\n\n{reason}\n\nPlease check your configuration."
189+
de: "Der Zahlungsservice wurde aufgrund eines Startproblems deaktiviert:\n\n{reason}\n\nBitte überprüfe die Konfiguration."
187190
ask_export_data:
188191
en: 'Export cocktail and ingredient data, as well reset possible values?'
189192
de: 'Cocktail und Zutaten Daten exportieren und mögliche Werte zurücksetzen?'

src/machine/controller.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def cleanup(self) -> None:
236236

237237
def _stop_pumps(self, pin_list: list[int], print_prefix: str = "") -> None:
238238
"""Informs and closes all given pins."""
239-
_logger.debug(f"{print_prefix}<x> Closing Pins: {pin_list}")
239+
_logger.info(f"{print_prefix}<x> Closing Pins: {pin_list}")
240240
self.pin_controller.close_pin_list(pin_list)
241241

242242
def default_led(self) -> None:

src/service/sumup_payment_service.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import ast
2+
import json
13
import time
24
from collections.abc import Callable
35
from dataclasses import dataclass
@@ -62,11 +64,40 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Result[T]:
6264
if isinstance(exc, APIError):
6365
reason = str(exc.body)
6466
code = exc.status
65-
return Err(error=reason, code=code)
67+
return Err(error=_format_sumup_error(reason), code=code)
6668

6769
return wrapper
6870

6971

72+
def _format_sumup_error(error: str) -> str:
73+
"""Normalize SumUp error strings for readability."""
74+
parsed = _parse_sumup_error_payload(error)
75+
if parsed is None:
76+
return error
77+
try:
78+
return json.dumps(parsed)
79+
except (TypeError, ValueError):
80+
return error
81+
82+
83+
def _parse_sumup_error_payload(error: str) -> dict | list | None:
84+
if not error:
85+
return None
86+
try:
87+
parsed = json.loads(error)
88+
if isinstance(parsed, (dict, list)):
89+
return parsed
90+
except json.JSONDecodeError:
91+
pass
92+
try:
93+
parsed = ast.literal_eval(error)
94+
if isinstance(parsed, (dict, list)):
95+
return parsed
96+
except (SyntaxError, ValueError):
97+
return None
98+
return None
99+
100+
70101
class _SumupSdkClient:
71102
def __init__(self, api_key: str, merchant_code: str) -> None:
72103
self._client = Sumup(api_key=api_key)
@@ -146,7 +177,7 @@ def __init__(self, api_key: str | None = None, merchant_code: str | None = None)
146177
if getattr(self, "_initialized", False):
147178
return
148179
if not api_key or not merchant_code:
149-
raise ValueError("api_key and merchant_code are required for first initialization")
180+
raise ValueError("Sumup api_key and merchant_code are required for this payment service")
150181
self._client: _SumupSdkClient = _SumupSdkClient(api_key=api_key, merchant_code=merchant_code)
151182
self._initialized = True
152183
_logger.info("SumupPaymentService initialized")
@@ -158,6 +189,9 @@ def get_all_readers(self) -> list[Reader]:
158189
return []
159190
return result.data
160191

192+
def get_all_readers_result(self) -> Result[list[Reader]]:
193+
return self._client.list_readers()
194+
161195
def create_reader(self, name: str, pairing_code: str) -> Result[Reader]:
162196
return self._client.create_reader(name, pairing_code)
163197

src/startup_checks.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
import sys
2+
from dataclasses import dataclass
23

34
from src import FUTURE_PYTHON_VERSION
45
from src.config.config_manager import CONFIG as cfg
6+
from src.logger_handler import LoggerHandler
7+
from src.machine.rfid import RFIDReader
8+
from src.service.sumup_payment_service import Err, SumupPaymentService
59
from src.updater import UpdateInfo, Updater
610
from src.utils import has_connection
711

12+
_logger = LoggerHandler("startup_checks")
13+
14+
15+
@dataclass
16+
class PaymentCheckResult:
17+
"""Result of a payment startup check."""
18+
19+
ok: bool
20+
reason: str = ""
21+
822

923
def can_update() -> UpdateInfo:
1024
"""Check if there is an update and it is possible."""
@@ -26,3 +40,40 @@ def is_python_deprecated() -> bool:
2640
"""Check if to display the deprecation warning for newer python version install."""
2741
sys_python = sys.version_info
2842
return sys_python < FUTURE_PYTHON_VERSION
43+
44+
45+
def check_payment_service() -> PaymentCheckResult:
46+
"""Check if the configured payment service can start properly."""
47+
if not cfg.payment_enabled:
48+
return PaymentCheckResult(ok=True)
49+
if cfg.sumup_payment:
50+
return _check_sumup()
51+
if cfg.cocktailberry_payment:
52+
return _check_cocktailberry_nfc()
53+
return PaymentCheckResult(ok=True)
54+
55+
56+
def _check_sumup() -> PaymentCheckResult:
57+
"""Validate SumUp payment prerequisites."""
58+
# Try to initialize the client and list readers to verify credentials
59+
try:
60+
service = SumupPaymentService(
61+
api_key=cfg.PAYMENT_SUMUP_API_KEY,
62+
merchant_code=cfg.PAYMENT_SUMUP_MERCHANT_CODE,
63+
)
64+
result = service.get_all_readers_result()
65+
if isinstance(result, Err):
66+
return PaymentCheckResult(ok=False, reason=f"SumUp API error: {result.error} (code: {result.code})")
67+
_logger.info(f"SumUp startup check passed, {len(result.data)} reader(s) found.")
68+
except Exception as e:
69+
return PaymentCheckResult(ok=False, reason=f"SumUp API error: {e}")
70+
if not cfg.PAYMENT_SUMUP_TERMINAL_ID:
71+
_logger.warning("Reader is not set, but it is required for payments. Please set it up.")
72+
return PaymentCheckResult(ok=True)
73+
74+
75+
def _check_cocktailberry_nfc() -> PaymentCheckResult:
76+
"""Validate CocktailBerry NFC payment prerequisites."""
77+
if RFIDReader().rfid is None:
78+
return PaymentCheckResult(ok=False, reason=f"Could not set up or use {cfg.RFID_READER} reader, see logs.")
79+
return PaymentCheckResult(ok=True)

src/tabs/maker.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,6 @@ def _build_comment_maker(cocktail: Cocktail) -> str:
4444
return comment
4545

4646

47-
def _log_cocktail(cocktail_volume: int, real_volume: int, cocktail_name: str, taken_time: float) -> None:
48-
"""Enter a log entry for the made cocktail."""
49-
volume_string = f"{cocktail_volume} ml"
50-
cancel_log_addition = ""
51-
if shared.cocktail_status.status == PrepareResult.CANCELED:
52-
cancel_log_addition = f" - Recipe canceled at {round(taken_time, 1)} s - {real_volume} ml"
53-
_logger.log_event("INFO", f"{volume_string:6} - {cocktail_name}{cancel_log_addition}")
54-
55-
5647
def prepare_cocktail(
5748
cocktail: Cocktail,
5849
w: MainScreen | None = None,
@@ -82,7 +73,6 @@ def prepare_cocktail(
8273

8374
percentage_made = taken_time / max_time
8475
real_volume = round(cocktail.adjusted_amount * percentage_made)
85-
_log_cocktail(cocktail.adjusted_amount, real_volume, display_name, taken_time)
8676

8777
# run Addons after cocktail preparation
8878
addon_data["consumption"] = consumption
@@ -177,5 +167,3 @@ def prepare_ingredient(ingredient: Ingredient, w: MainScreen | None = None) -> N
177167
consumed_volume = made_volume[0]
178168
DBC = DatabaseCommander()
179169
DBC.increment_ingredient_consumption(ingredient.name, consumed_volume)
180-
volume_string = f"{consumed_volume} ml"
181-
_logger.log_event("INFO", f"{volume_string:6} | {ingredient.name}")

0 commit comments

Comments
 (0)