BTCPay: add paid/receipt flow and redirect support

This commit is contained in:
xmrcheckout
2026-01-08 16:43:11 +00:00
parent b53f6dca21
commit 6eb8d381c9
11 changed files with 475 additions and 12 deletions
+19 -2
View File
@@ -201,7 +201,13 @@ def startup():
connection.execute(
text(
"ALTER TABLE users "
"ADD COLUMN IF NOT EXISTS btcpay_checkout_style VARCHAR DEFAULT 'standard'"
"ADD COLUMN IF NOT EXISTS btcpay_checkout_style VARCHAR DEFAULT 'btcpay_classic'"
)
)
connection.execute(
text(
"ALTER TABLE users "
"ALTER COLUMN btcpay_checkout_style SET DEFAULT 'btcpay_classic'"
)
)
connection.execute(
@@ -212,10 +218,21 @@ def startup():
)
connection.execute(
text(
"UPDATE users SET btcpay_checkout_style = 'standard' "
"UPDATE users SET btcpay_checkout_style = 'btcpay_classic' "
"WHERE btcpay_checkout_style IS NULL"
)
)
connection.execute(
text(
"UPDATE users "
"SET btcpay_checkout_style = 'btcpay_classic' "
"WHERE btcpay_checkout_style = 'standard' "
"AND NOT EXISTS ("
" SELECT 1 FROM profile_history ph "
" WHERE ph.user_id = users.id AND ph.field_name = 'btcpay_checkout_style'"
")"
)
)
connection.execute(
text(
"UPDATE users SET default_confirmation_target = 2 "
+1 -1
View File
@@ -128,7 +128,7 @@ class User(Base):
default_confirmation_target = Column(Integer, nullable=False, server_default="2")
default_qr_logo = Column(String, nullable=False, server_default="monero")
default_qr_logo_data_url = Column(String, nullable=True)
btcpay_checkout_style = Column(String, nullable=False, server_default="standard")
btcpay_checkout_style = Column(String, nullable=False, server_default="btcpay_classic")
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
+12
View File
@@ -37,12 +37,24 @@ def ensure_invoice_qr_png(
detail="QR storage is not configured",
)
os.makedirs(storage_dir, exist_ok=True)
try:
os.chmod(storage_dir, 0o755)
except Exception:
pass
filename = f"{invoice.id}.png"
path = os.path.join(storage_dir, filename)
if os.path.exists(path):
try:
os.chmod(path, 0o644)
except Exception:
pass
return path
png_bytes = build_invoice_qr_png_bytes(invoice=invoice, settings=settings)
_atomic_write(path, png_bytes)
try:
os.chmod(path, 0o644)
except Exception:
pass
return path
+28
View File
@@ -4,6 +4,7 @@ from datetime import datetime, timedelta, timezone
from decimal import Decimal, ROUND_DOWN
import csv
import io
import json
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
from fastapi.responses import StreamingResponse
@@ -239,6 +240,11 @@ def _public_invoice_status_response(
response = InvoiceStatusResponse.model_validate(invoice)
metadata = invoice.metadata_json or {}
update: dict[str, Any] = {}
if invoice.total_paid_atomic is not None:
total_paid = (Decimal(invoice.total_paid_atomic) / Decimal("1000000000000")).quantize(
Decimal("0.000000000001"), rounding=ROUND_DOWN
)
update["amount_paid_xmr"] = format_xmr_amount(total_paid)
btcpay_data = metadata.get("btcpay") if isinstance(metadata, dict) else None
if isinstance(btcpay_data, dict):
amount = btcpay_data.get("amount")
@@ -250,8 +256,30 @@ def _public_invoice_status_response(
user = db.query(User).filter(User.id == invoice.user_id).first()
if user and user.btcpay_checkout_style:
update["btcpay_checkout_style"] = user.btcpay_checkout_style
checkout = btcpay_data.get("checkout")
if isinstance(checkout, dict):
redirect_url = checkout.get("redirectURL")
if isinstance(redirect_url, str) and redirect_url.strip():
update["btcpay_redirect_url"] = redirect_url.strip()
redirect_auto = checkout.get("redirectAutomatically")
if isinstance(redirect_auto, bool):
update["btcpay_redirect_automatically"] = redirect_auto
if isinstance(metadata, dict) and isinstance(metadata.get("quote"), dict):
update["quote"] = metadata.get("quote")
if isinstance(metadata, dict) and isinstance(metadata.get("posData"), str):
try:
pos_data = json.loads(metadata.get("posData", ""))
except (TypeError, ValueError, json.JSONDecodeError):
pos_data = None
if isinstance(pos_data, dict):
woo = pos_data.get("WooCommerce")
if isinstance(woo, dict):
order_id = woo.get("Order ID")
order_number = woo.get("Order Number")
if order_id is not None:
update["btcpay_order_id"] = str(order_id)
if order_number is not None:
update["btcpay_order_number"] = str(order_number)
qr = metadata.get("qr") if isinstance(metadata, dict) else None
if isinstance(qr, dict):
logo = qr.get("logo")
+6 -1
View File
@@ -126,6 +126,7 @@ class InvoiceStatusResponse(BaseModel):
address: str
subaddress_index: int | None = None
amount_xmr: Decimal
amount_paid_xmr: str | None = None
status: InvoiceStatus
confirmation_target: int
confirmations: int = 0
@@ -140,6 +141,10 @@ class InvoiceStatusResponse(BaseModel):
btcpay_amount: str | None = None
btcpay_currency: str | None = None
btcpay_checkout_style: BtcpayCheckoutStyle | None = None
btcpay_redirect_url: str | None = None
btcpay_redirect_automatically: bool | None = None
btcpay_order_id: str | None = None
btcpay_order_number: str | None = None
quote: dict[str, Any] | None = None
qr_url: str | None = None
@@ -156,7 +161,7 @@ class ProfileResponse(BaseModel):
default_confirmation_target: conint(ge=0, le=10) = 2
default_qr_logo: QrLogoMode = "monero"
default_qr_logo_data_url: str | None = None
btcpay_checkout_style: BtcpayCheckoutStyle = "standard"
btcpay_checkout_style: BtcpayCheckoutStyle = "btcpay_classic"
created_at: datetime