Webhook Copycaisse — paiement déclenche impression
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) <noreply@anthropic.com>
This commit is contained in:
@@ -282,6 +282,7 @@ class UploadServer:
|
|||||||
self.signals = UploadSignals()
|
self.signals = UploadSignals()
|
||||||
self._callback = None
|
self._callback = None
|
||||||
self._queue = queue.Queue()
|
self._queue = queue.Queue()
|
||||||
|
self._payment_confirmations = {}
|
||||||
self.app = Flask(__name__)
|
self.app = Flask(__name__)
|
||||||
self._setup_routes()
|
self._setup_routes()
|
||||||
self._thread = None
|
self._thread = None
|
||||||
@@ -320,6 +321,23 @@ class UploadServer:
|
|||||||
|
|
||||||
return jsonify({"ok": True, "count": len(saved)})
|
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):
|
def set_callback(self, callback):
|
||||||
"""Définit le callback appelé quand des photos sont uploadées."""
|
"""Définit le callback appelé quand des photos sont uploadées."""
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
|
|||||||
@@ -225,11 +225,111 @@ class PrintScreen(QWidget):
|
|||||||
formats = config.get("formats", {})
|
formats = config.get("formats", {})
|
||||||
fmt_conf = formats.get(self.selected_format, {})
|
fmt_conf = formats.get(self.selected_format, {})
|
||||||
printer = fmt_conf.get("printer", "")
|
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:
|
for path, qty in self.print_list:
|
||||||
print(f"[Impression] {path} x{qty} → {self.selected_format} cm"
|
print(f"[Impression] {path} x{qty} → {self.selected_format} cm"
|
||||||
f" (printer: {printer or 'défaut'})")
|
f" (printer: {printer or 'défaut'})")
|
||||||
|
# TODO: appel CUPS réel
|
||||||
|
|
||||||
total = sum(q for _, q in self.print_list)
|
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.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;")
|
||||||
|
|||||||
@@ -41,6 +41,10 @@ DEFAULT_CONFIG = {
|
|||||||
"13x18": {"enabled": False, "printer": "", "ratio": 1.385},
|
"13x18": {"enabled": False, "printer": "", "ratio": 1.385},
|
||||||
"15x20": {"enabled": False, "printer": "", "ratio": 1.333},
|
"15x20": {"enabled": False, "printer": "", "ratio": 1.333},
|
||||||
},
|
},
|
||||||
|
# Webhook Copycaisse
|
||||||
|
"webhook_enabled": False,
|
||||||
|
"webhook_url": "",
|
||||||
|
"webhook_wait_payment": False,
|
||||||
# Tarification
|
# Tarification
|
||||||
"pricing": {
|
"pricing": {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
@@ -343,6 +347,31 @@ class ConfigDialog(QDialog):
|
|||||||
self.tier_widgets.append({"from": from_sb, "to": to_sb, "price": price_input})
|
self.tier_widgets.append({"from": from_sb, "to": to_sb, "price": price_input})
|
||||||
row += 1
|
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)
|
scroll.setWidget(content)
|
||||||
layout.addWidget(scroll, 1)
|
layout.addWidget(scroll, 1)
|
||||||
|
|
||||||
@@ -414,6 +443,10 @@ class ConfigDialog(QDialog):
|
|||||||
"currency": "€",
|
"currency": "€",
|
||||||
"tiers": tiers,
|
"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)
|
save_config(self.config)
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user