mirror of
https://github.com/Such-Software/xmrcheckout.git
synced 2026-10-17 05:51:34 +02:00
Add optional post-confirmation continue URL
This commit is contained in:
+55
-1
@@ -7,7 +7,7 @@ import io
|
||||
import json
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, Response, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.responses import RedirectResponse, StreamingResponse
|
||||
import requests
|
||||
from requests import RequestException
|
||||
from sqlalchemy import func, or_
|
||||
@@ -131,6 +131,20 @@ def _is_donation_invoice(invoice: Invoice) -> bool:
|
||||
return isinstance(metadata, dict) and metadata.get("origin") == "donation"
|
||||
|
||||
|
||||
def _get_checkout_continue_url(invoice: Invoice) -> str | None:
|
||||
metadata = invoice.metadata_json or {}
|
||||
if not isinstance(metadata, dict):
|
||||
return None
|
||||
checkout = metadata.get("checkout")
|
||||
if not isinstance(checkout, dict):
|
||||
return None
|
||||
value = checkout.get("continue_url")
|
||||
if not isinstance(value, str):
|
||||
return None
|
||||
trimmed = value.strip()
|
||||
return trimmed or None
|
||||
|
||||
|
||||
def _ensure_webhook_secret(db: Session, user: User) -> str:
|
||||
if user.webhook_secret_encrypted:
|
||||
return decrypt_secret(user.webhook_secret_encrypted)
|
||||
@@ -288,6 +302,10 @@ def _public_invoice_status_response(
|
||||
update["qr_logo"] = logo
|
||||
if logo == "custom" and isinstance(logo_data_url, str):
|
||||
update["qr_logo_data_url"] = logo_data_url
|
||||
continue_url = _get_checkout_continue_url(invoice)
|
||||
update["checkout_continue_available"] = bool(
|
||||
invoice.status == "confirmed" and continue_url
|
||||
)
|
||||
update["qr_url"] = _qr_url(invoice, request)
|
||||
return response.model_copy(update=update)
|
||||
|
||||
@@ -455,6 +473,36 @@ def get_invoice_status(
|
||||
return _public_invoice_status_response(db, invoice, request)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/core/public/invoice/{invoice_id}/continue",
|
||||
response_class=RedirectResponse,
|
||||
)
|
||||
def continue_invoice_after_confirmation(
|
||||
invoice_id: uuid.UUID,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
invoice = db.query(Invoice).filter(Invoice.id == invoice_id).first()
|
||||
if invoice is None or _is_donation_invoice(invoice):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Invoice not found",
|
||||
)
|
||||
if invoice.status != "confirmed":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Continue link not available",
|
||||
)
|
||||
continue_url = _get_checkout_continue_url(invoice)
|
||||
if not continue_url:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Continue link not available",
|
||||
)
|
||||
response = RedirectResponse(url=continue_url, status_code=status.HTTP_302_FOUND)
|
||||
response.headers["Cache-Control"] = "no-store"
|
||||
return response
|
||||
|
||||
|
||||
@router.get(
|
||||
"/api/core/public/donation/{invoice_id}",
|
||||
response_model=InvoiceStatusResponse,
|
||||
@@ -933,6 +981,12 @@ def create_invoice_for_user(
|
||||
metadata = {"qr": {"logo": user.default_qr_logo}}
|
||||
if user.default_qr_logo == "custom" and user.default_qr_logo_data_url:
|
||||
metadata["qr"]["logo_data_url"] = user.default_qr_logo_data_url
|
||||
if payload.checkout_continue_url is not None:
|
||||
checkout = metadata.get("checkout") if isinstance(metadata, dict) else None
|
||||
if not isinstance(checkout, dict):
|
||||
checkout = {}
|
||||
checkout = {**checkout, "continue_url": str(payload.checkout_continue_url)}
|
||||
metadata = {**(metadata if isinstance(metadata, dict) else {}), "checkout": checkout}
|
||||
address, subaddress_index = create_subaddress_for_user(db, user=user)
|
||||
invoice = Invoice(
|
||||
user_id=user.id,
|
||||
|
||||
@@ -26,6 +26,10 @@ class InvoiceCreate(BaseModel):
|
||||
amount_fiat: Decimal | None = Field(default=None, gt=0)
|
||||
currency: str | None = None
|
||||
confirmation_target: conint(ge=0, le=10) = 2
|
||||
checkout_continue_url: AnyUrl | None = Field(
|
||||
default=None,
|
||||
description="Optional: after the invoice is confirmed, the hosted invoice page can offer a Continue button that navigates to this merchant URL.",
|
||||
)
|
||||
metadata: dict[str, Any] | None = None
|
||||
expires_at: datetime | None = Field(
|
||||
default=None,
|
||||
@@ -49,6 +53,17 @@ class InvoiceCreate(BaseModel):
|
||||
raise ValueError("currency is required when amount_fiat is provided")
|
||||
return self
|
||||
|
||||
@field_validator("checkout_continue_url")
|
||||
@classmethod
|
||||
def _validate_checkout_continue_url(cls, value: AnyUrl | None) -> AnyUrl | None:
|
||||
if value is None:
|
||||
return None
|
||||
if value.scheme == "https":
|
||||
return value
|
||||
if value.scheme == "http" and value.host in ("localhost", "127.0.0.1"):
|
||||
return value
|
||||
raise ValueError("checkout_continue_url must use https (http is allowed for localhost)")
|
||||
|
||||
|
||||
class DonationCreate(BaseModel):
|
||||
amount_xmr: Decimal = Field(gt=0)
|
||||
@@ -90,6 +105,10 @@ class InvoiceCreateUser(BaseModel):
|
||||
amount_fiat: Decimal | None = Field(default=None, gt=0)
|
||||
currency: str | None = None
|
||||
confirmation_target: conint(ge=0, le=10) = 2
|
||||
checkout_continue_url: AnyUrl | None = Field(
|
||||
default=None,
|
||||
description="Optional: after the invoice is confirmed, the hosted invoice page can offer a Continue button that navigates to this merchant URL.",
|
||||
)
|
||||
metadata: dict[str, Any] | None = None
|
||||
expires_at: datetime | None = Field(
|
||||
default=None,
|
||||
@@ -113,6 +132,17 @@ class InvoiceCreateUser(BaseModel):
|
||||
raise ValueError("currency is required when amount_fiat is provided")
|
||||
return self
|
||||
|
||||
@field_validator("checkout_continue_url")
|
||||
@classmethod
|
||||
def _validate_checkout_continue_url(cls, value: AnyUrl | None) -> AnyUrl | None:
|
||||
if value is None:
|
||||
return None
|
||||
if value.scheme == "https":
|
||||
return value
|
||||
if value.scheme == "http" and value.host in ("localhost", "127.0.0.1"):
|
||||
return value
|
||||
raise ValueError("checkout_continue_url must use https (http is allowed for localhost)")
|
||||
|
||||
|
||||
class InvoiceListResponse(BaseModel):
|
||||
items: list[InvoiceResponse]
|
||||
@@ -145,6 +175,7 @@ class InvoiceStatusResponse(BaseModel):
|
||||
btcpay_redirect_automatically: bool | None = None
|
||||
btcpay_order_id: str | None = None
|
||||
btcpay_order_number: str | None = None
|
||||
checkout_continue_available: bool = False
|
||||
quote: dict[str, Any] | None = None
|
||||
qr_url: str | None = None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user