From 1583021af1360039b488233f07fd54f11d062694 Mon Sep 17 00:00:00 2001 From: Jules Date: Sun, 22 Mar 2026 00:01:10 +0100 Subject: [PATCH] =?UTF-8?q?Webhook=20Copycaisse=20=E2=80=94=20paiement=20d?= =?UTF-8?q?=C3=A9clenche=20impression?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Config admin : - Webhook activable + URL - Option "attendre le paiement" avant impression Flux : 1. Utilisateur clique IMPRIMER 2. Données envoyées au webhook (photos, quantités, format, prix) 3. Si "attendre paiement" : bouton grisé, polling toutes les 2s 4. Copycaisse confirme via POST /api/payment-callback 5. Impression lancée automatiquement Routes Flask ajoutées : - POST /api/payment-callback (reçoit confirmation) - GET /api/payment-status (polling) Fallback : si webhook échoue, impression directe. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/services/upload_server.py | 18 ++++++ src/ui/screens/print_screen.py | 102 +++++++++++++++++++++++++++++++- src/ui/widgets/config_dialog.py | 33 +++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/services/upload_server.py b/src/services/upload_server.py index 16d39eb..abd7e06 100644 --- a/src/services/upload_server.py +++ b/src/services/upload_server.py @@ -282,6 +282,7 @@ class UploadServer: self.signals = UploadSignals() self._callback = None self._queue = queue.Queue() + self._payment_confirmations = {} self.app = Flask(__name__) self._setup_routes() self._thread = None @@ -320,6 +321,23 @@ class UploadServer: return jsonify({"ok": True, "count": len(saved)}) + @self.app.route("/api/payment-callback", methods=["POST"]) + def payment_callback(): + """Reçoit la confirmation de paiement depuis Copycaisse.""" + data = request.get_json(silent=True) or {} + order_id = data.get("order_id", "") + paid = data.get("paid", False) + if order_id and paid: + self._payment_confirmations[order_id] = True + return jsonify({"ok": True}) + + @self.app.route("/api/payment-status") + def payment_status(): + """Vérifié par le polling pour savoir si le paiement est fait.""" + order_id = request.args.get("order_id", "") + paid = self._payment_confirmations.get(order_id, False) + return jsonify({"order_id": order_id, "paid": paid}) + def set_callback(self, callback): """Définit le callback appelé quand des photos sont uploadées.""" self._callback = callback diff --git a/src/ui/screens/print_screen.py b/src/ui/screens/print_screen.py index 88b5f46..76c0b3b 100644 --- a/src/ui/screens/print_screen.py +++ b/src/ui/screens/print_screen.py @@ -225,11 +225,111 @@ class PrintScreen(QWidget): formats = config.get("formats", {}) fmt_conf = formats.get(self.selected_format, {}) printer = fmt_conf.get("printer", "") + total_qty = sum(q for _, q in self.print_list) + price = calculate_price(total_qty, config) + + # Webhook Copycaisse + if config.get("webhook_enabled") and config.get("webhook_url"): + self._send_webhook(config, price, total_qty) + if config.get("webhook_wait_payment"): + self.print_btn.setEnabled(False) + self.print_btn.setText("En attente de paiement...") + self.price_label.setText("Commande envoyée — en attente de paiement") + self.price_label.setStyleSheet("color: #FF9800; font-weight: bold;") + # Le webhook de callback réactivera le bouton + return + + self._do_print(printer) + + def _send_webhook(self, config, price, total_qty): + """Envoie les données au webhook Copycaisse.""" + import threading + import json + import urllib.request + + payload = { + "source": "photostation", + "station_name": config.get("station_name", "Photostation"), + "format": self.selected_format, + "photos": [{"path": p, "quantity": q} for p, q in self.print_list], + "total_quantity": total_qty, + "total_price": price, + "currency": "€", + } + + def send(): + try: + url = config["webhook_url"] + data = json.dumps(payload).encode("utf-8") + req = urllib.request.Request( + url, data=data, + headers={"Content-Type": "application/json"} + ) + resp = urllib.request.urlopen(req, timeout=10) + result = json.loads(resp.read().decode()) + print(f"[Webhook] Response: {result}") + + if config.get("webhook_wait_payment"): + # Stocker l'ID de commande pour le polling + self._order_id = result.get("order_id", "") + self._start_payment_polling(config) + except Exception as e: + print(f"[Webhook] Error: {e}") + # En cas d'erreur, on imprime quand même + from PyQt6.QtCore import QTimer + from functools import partial + QTimer.singleShot(0, partial(self._do_print, + config.get("formats", {}).get(self.selected_format, {}).get("printer", ""))) + + threading.Thread(target=send, daemon=True).start() + + def _start_payment_polling(self, config): + """Poll le webhook pour vérifier le paiement.""" + import threading + import json + import urllib.request + import time + + def poll(): + url = config["webhook_url"] + for _ in range(120): # 2 minutes max + time.sleep(2) + try: + check_url = f"{url}?order_id={self._order_id}&action=check" + resp = urllib.request.urlopen(check_url, timeout=5) + result = json.loads(resp.read().decode()) + if result.get("paid"): + printer = config.get("formats", {}).get( + self.selected_format, {}).get("printer", "") + from PyQt6.QtCore import QTimer + from functools import partial + QTimer.singleShot(0, partial(self._do_print, printer)) + return + except Exception: + pass + + # Timeout — annuler + from PyQt6.QtCore import QTimer + QTimer.singleShot(0, self._payment_timeout) + + threading.Thread(target=poll, daemon=True).start() + + def _payment_timeout(self): + self.print_btn.setEnabled(True) + self.print_btn.setText("IMPRIMER") + self.price_label.setText("Paiement non reçu — réessayez") + self.price_label.setStyleSheet("color: #F44336; font-weight: bold;") + + def _do_print(self, printer=""): + """Lance l'impression réelle.""" + self.print_btn.setEnabled(True) + self.print_btn.setText("IMPRIMER") for path, qty in self.print_list: print(f"[Impression] {path} x{qty} → {self.selected_format} cm" f" (printer: {printer or 'défaut'})") + # TODO: appel CUPS réel total = sum(q for _, q in self.print_list) self.price_label.setText(f"Impression lancée — {total} photo{'s' if total > 1 else ''}") - self.price_label.setStyleSheet("color: #4CAF50; font-weight: bold; font-size: 18px;") + self.price_label.setStyleSheet("color: #4CAF50; font-weight: bold;") diff --git a/src/ui/widgets/config_dialog.py b/src/ui/widgets/config_dialog.py index 6110ef4..b269520 100644 --- a/src/ui/widgets/config_dialog.py +++ b/src/ui/widgets/config_dialog.py @@ -41,6 +41,10 @@ DEFAULT_CONFIG = { "13x18": {"enabled": False, "printer": "", "ratio": 1.385}, "15x20": {"enabled": False, "printer": "", "ratio": 1.333}, }, + # Webhook Copycaisse + "webhook_enabled": False, + "webhook_url": "", + "webhook_wait_payment": False, # Tarification "pricing": { "enabled": True, @@ -343,6 +347,31 @@ class ConfigDialog(QDialog): self.tier_widgets.append({"from": from_sb, "to": to_sb, "price": price_input}) row += 1 + # ── Webhook Copycaisse ── + grid.addWidget(self._section_title("Webhook Copycaisse"), row, 0, 1, 2) + row += 1 + + grid.addWidget(self._label("Activer :"), row, 0) + self.webhook_enabled_cb = QCheckBox("Envoyer au webhook") + self.webhook_enabled_cb.setFont(QFont("", 12)) + self.webhook_enabled_cb.setChecked(self.config.get("webhook_enabled", False)) + grid.addWidget(self.webhook_enabled_cb, row, 1) + row += 1 + + grid.addWidget(self._label("URL webhook :"), row, 0) + self.webhook_url_input = self._input( + self.config.get("webhook_url", ""), "https://caisse.exemple.fr/api/photostation" + ) + grid.addWidget(self.webhook_url_input, row, 1) + row += 1 + + grid.addWidget(self._label("Paiement avant impression :"), row, 0) + self.webhook_wait_cb = QCheckBox("Attendre le paiement") + self.webhook_wait_cb.setFont(QFont("", 12)) + self.webhook_wait_cb.setChecked(self.config.get("webhook_wait_payment", False)) + grid.addWidget(self.webhook_wait_cb, row, 1) + row += 1 + scroll.setWidget(content) layout.addWidget(scroll, 1) @@ -414,6 +443,10 @@ class ConfigDialog(QDialog): "currency": "€", "tiers": tiers, } + # Webhook + self.config["webhook_enabled"] = self.webhook_enabled_cb.isChecked() + self.config["webhook_url"] = self.webhook_url_input.text() + self.config["webhook_wait_payment"] = self.webhook_wait_cb.isChecked() save_config(self.config) self.accept()