116 lines
3.5 KiB
Python
116 lines
3.5 KiB
Python
import os
|
|
from typing import Annotated
|
|
|
|
import qrcode
|
|
import qrcode.image.svg
|
|
from fastapi import FastAPI, Form, HTTPException, Request
|
|
from fastapi.responses import HTMLResponse, RedirectResponse
|
|
from fastapi.templating import Jinja2Templates
|
|
from mollie.api.client import Client
|
|
|
|
app = FastAPI()
|
|
templates = Jinja2Templates(directory="resources")
|
|
|
|
public_url = os.environ.get("PUBLIC_URL", "http://localhost:8000").rstrip("/")
|
|
mollie_apikey = os.environ.get("MOLLIE_API_KEY", "test_test")
|
|
mollie_client = Client()
|
|
mollie_client.set_api_key(mollie_apikey)
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def welcome(request: Request):
|
|
return templates.TemplateResponse(request=request, name="welcome.html.j2")
|
|
|
|
|
|
@app.post("/", response_class=RedirectResponse)
|
|
async def pay(request: Request, amount: Annotated[int, Form()]):
|
|
# Amount is in cents. Depositing less than a euro does not really make sense, so interpret this
|
|
# as full Euro's.
|
|
if amount < 100:
|
|
amount *= 100
|
|
|
|
payment = mollie_client.payments.create(
|
|
{
|
|
"amount": {
|
|
"currency": "EUR",
|
|
"value": f"{amount // 100}.{amount % 100:02}",
|
|
},
|
|
"description": "Bitlair bar tegoed",
|
|
"redirectUrl": f"{public_url}/return",
|
|
"metadata": {"revbank_status": "unspent"},
|
|
}
|
|
)
|
|
mollie_client.payments.update(
|
|
payment.id, {"redirectUrl": f"{payment.redirect_url}/{payment.id}"}
|
|
)
|
|
|
|
return RedirectResponse(payment.checkout_url, status_code=302)
|
|
|
|
|
|
@app.get("/return/{id}", response_class=HTMLResponse)
|
|
async def return_(request: Request, id: str):
|
|
qr = qrcode.make(id, image_factory=qrcode.image.svg.SvgPathImage)
|
|
return templates.TemplateResponse(
|
|
request=request,
|
|
name="return.html.j2",
|
|
context={
|
|
"code": id,
|
|
"qr_svg": qr.to_string(encoding="unicode"),
|
|
},
|
|
)
|
|
|
|
|
|
def not_ok(msg: str):
|
|
return {"ok": False, "message": msg}
|
|
|
|
|
|
def ok(**kwargs):
|
|
return {"ok": True, **kwargs}
|
|
|
|
|
|
@app.post("/revbank_plugin_backend")
|
|
async def rb_backend(
|
|
id: Annotated[str, Form()], action: Annotated[str | None, Form()] = None
|
|
):
|
|
payment = mollie_client.payments.get(id)
|
|
|
|
if not payment.is_paid():
|
|
return not_ok(f"payment {payment.status}")
|
|
|
|
assert payment.amount["currency"] == "EUR"
|
|
rb_status = payment.metadata.get("revbank_status", None)
|
|
assert rb_status is not None
|
|
|
|
# Called to reserve a payment in a Revbank transction.
|
|
if action is None:
|
|
if rb_status != "unspent":
|
|
return not_ok("already spent")
|
|
mollie_client.payments.update(
|
|
payment.id,
|
|
{"metadata": {"revbank_status": "pending"}},
|
|
)
|
|
|
|
if mollie_apikey.startswith("test_"):
|
|
return ok(amount="0.00", test_amount=payment.amount["value"])
|
|
|
|
return ok(amount=payment.amount["value"])
|
|
|
|
# Called when a pending Revbank transaction is aborted by the user.
|
|
if action == "abort":
|
|
if rb_status != "pending":
|
|
return not_ok(f"can't abort non-pending, {rb_status}")
|
|
mollie_client.payments.update(
|
|
payment.id,
|
|
{"metadata": {"revbank_status": "unspent"}},
|
|
)
|
|
return ok()
|
|
|
|
# Called when Revbank finishes a pending transaction.
|
|
if action == "finalize":
|
|
mollie_client.payments.update(
|
|
payment.id,
|
|
{"metadata": {"revbank_status": "spent"}},
|
|
)
|
|
return ok()
|
|
|
|
raise HTTPException(status_code=400, detail=f"invalid action {action}")
|