feat: make events dynamic (#43)
--------- Co-authored-by: dni <office@dnilabs.com>
This commit is contained in:
parent
f06bd9a668
commit
9e477ac959
21 changed files with 1164 additions and 1143 deletions
293
views_api.py
293
views_api.py
|
|
@ -1,8 +1,17 @@
|
|||
import asyncio
|
||||
from datetime import datetime, timezone
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from lnbits.core.crud import get_standalone_payment, get_user
|
||||
from fastapi import (
|
||||
APIRouter,
|
||||
Depends,
|
||||
HTTPException,
|
||||
Query,
|
||||
WebSocket,
|
||||
WebSocketDisconnect,
|
||||
)
|
||||
from lnbits.core.crud import get_user
|
||||
from lnbits.core.models import WalletTypeInfo
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.decorators import (
|
||||
|
|
@ -13,7 +22,6 @@ from lnbits.utils.exchange_rates import (
|
|||
fiat_amount_as_satoshis,
|
||||
get_fiat_rate_satoshis,
|
||||
)
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from .crud import (
|
||||
create_event,
|
||||
|
|
@ -26,36 +34,79 @@ from .crud import (
|
|||
get_events,
|
||||
get_ticket,
|
||||
get_tickets,
|
||||
purge_unpaid_tickets,
|
||||
update_event,
|
||||
update_ticket,
|
||||
)
|
||||
from .models import CreateEvent, CreateTicket, Ticket
|
||||
from .services import refund_tickets, set_ticket_paid
|
||||
from .models import (
|
||||
CreateEvent,
|
||||
CreateTicket,
|
||||
Event,
|
||||
PublicEvent,
|
||||
PublicTicket,
|
||||
Ticket,
|
||||
TicketPaymentRequest,
|
||||
)
|
||||
from .services import refund_tickets
|
||||
from .tasks import deregister_payment_listener, register_payment_listener
|
||||
|
||||
events_api_router = APIRouter()
|
||||
events_api_router = APIRouter(prefix="/api/v1/events")
|
||||
tickets_api_router = APIRouter(prefix="/api/v1/tickets")
|
||||
|
||||
|
||||
@events_api_router.get("/api/v1/events")
|
||||
@events_api_router.get("")
|
||||
async def api_events(
|
||||
all_wallets: bool = Query(False),
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
):
|
||||
) -> list[Event]:
|
||||
wallet_ids = [wallet.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
user = await get_user(wallet.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
return [event.dict() for event in await get_events(wallet_ids)]
|
||||
return await get_events(wallet_ids)
|
||||
|
||||
|
||||
@events_api_router.post("/api/v1/events")
|
||||
@events_api_router.put("/api/v1/events/{event_id}")
|
||||
@events_api_router.get("/{event_id}", response_model=PublicEvent)
|
||||
async def api_get_event(event_id: str) -> Event:
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
|
||||
)
|
||||
await purge_unpaid_tickets(event_id)
|
||||
|
||||
is_window_open = datetime.now(timezone.utc) < datetime.strptime(
|
||||
event.closing_date, "%Y-%m-%d"
|
||||
).replace(tzinfo=timezone.utc)
|
||||
is_min_tickets_met = (
|
||||
event.sold >= event.extra.min_tickets if event.extra.conditional else True
|
||||
)
|
||||
if event.amount_tickets < 1:
|
||||
raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is sold out.")
|
||||
if event.extra.conditional and not is_min_tickets_met and not is_window_open:
|
||||
event.canceled = True
|
||||
await update_event(event)
|
||||
await refund_tickets(event_id)
|
||||
|
||||
raise HTTPException(status_code=HTTPStatus.GONE, detail="Event canceled.")
|
||||
|
||||
if not is_window_open:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.GONE, detail="Ticket closing date has passed."
|
||||
)
|
||||
|
||||
return event
|
||||
|
||||
|
||||
@events_api_router.post("")
|
||||
@events_api_router.put("/{event_id}")
|
||||
async def api_event_create(
|
||||
data: CreateEvent,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
event_id: str | None = None,
|
||||
):
|
||||
) -> Event:
|
||||
if event_id:
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
|
|
@ -73,14 +124,14 @@ async def api_event_create(
|
|||
else:
|
||||
event = await create_event(data)
|
||||
|
||||
return event.dict()
|
||||
return event
|
||||
|
||||
|
||||
@events_api_router.put("/api/v1/events/{event_id}/cancel")
|
||||
@events_api_router.put("/{event_id}/cancel")
|
||||
async def api_event_cancel(
|
||||
event_id: str,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
):
|
||||
) -> Event:
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
|
|
@ -93,13 +144,13 @@ async def api_event_cancel(
|
|||
event = await update_event(event)
|
||||
await refund_tickets(event.id)
|
||||
|
||||
return event.dict()
|
||||
return event
|
||||
|
||||
|
||||
@events_api_router.delete("/api/v1/events/{event_id}")
|
||||
@events_api_router.delete("/{event_id}")
|
||||
async def api_form_delete(
|
||||
event_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
):
|
||||
) -> None:
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
|
|
@ -111,47 +162,65 @@ async def api_form_delete(
|
|||
|
||||
await delete_event(event_id)
|
||||
await delete_event_tickets(event_id)
|
||||
return "", HTTPStatus.NO_CONTENT
|
||||
|
||||
|
||||
#########Tickets##########
|
||||
@events_api_router.get(
|
||||
"/{event_id}/tickets",
|
||||
response_model=list[PublicTicket],
|
||||
)
|
||||
async def api_event_tickets(event_id: str) -> list[Ticket]:
|
||||
return await get_event_tickets(event_id)
|
||||
|
||||
|
||||
@events_api_router.get("/api/v1/tickets")
|
||||
@tickets_api_router.get("")
|
||||
async def api_tickets(
|
||||
all_wallets: bool = Query(False),
|
||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
||||
key_info: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> list[Ticket]:
|
||||
wallet_ids = [wallet.wallet.id]
|
||||
wallet_ids = [key_info.wallet.id]
|
||||
|
||||
if all_wallets:
|
||||
user = await get_user(wallet.wallet.user)
|
||||
user = await get_user(key_info.wallet.user)
|
||||
wallet_ids = user.wallet_ids if user else []
|
||||
|
||||
return await get_tickets(wallet_ids)
|
||||
|
||||
|
||||
@events_api_router.post("/api/v1/tickets/{event_id}")
|
||||
async def api_ticket_create(event_id: str, data: CreateTicket):
|
||||
name = data.name
|
||||
email = data.email
|
||||
promo_code = data.promo_code.upper() if data.promo_code else None
|
||||
refund_address = data.refund_address
|
||||
return await api_ticket_make_ticket(
|
||||
event_id, name, email, promo_code, refund_address
|
||||
)
|
||||
@tickets_api_router.get("/{ticket_id}", response_model=PublicTicket)
|
||||
async def api_get_ticket(ticket_id: str) -> Ticket:
|
||||
ticket = await get_ticket(ticket_id)
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist."
|
||||
)
|
||||
event = await get_event(ticket.event)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
|
||||
)
|
||||
return ticket
|
||||
|
||||
|
||||
@events_api_router.get("/api/v1/tickets/{event_id}/{name}/{email}")
|
||||
async def api_ticket_make_ticket(event_id, name, email, promo_code, refund_address):
|
||||
@tickets_api_router.post("/{event_id}")
|
||||
async def api_ticket_create(event_id: str, data: CreateTicket) -> TicketPaymentRequest:
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
|
||||
)
|
||||
|
||||
if event.canceled:
|
||||
raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is canceled.")
|
||||
|
||||
if event.amount_tickets > 0 and event.sold >= event.amount_tickets:
|
||||
raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is sold out.")
|
||||
|
||||
name = data.name
|
||||
email = data.email
|
||||
promo_code = data.promo_code.upper() if data.promo_code else None
|
||||
refund_address = data.refund_address
|
||||
price = event.price_per_ticket
|
||||
extra = {"tag": "events", "name": name, "email": email}
|
||||
extra: dict[str, Any] = {"tag": "events", "name": name, "email": email}
|
||||
|
||||
if promo_code:
|
||||
# check if promo_code exists in event.extra.promo_codes
|
||||
|
|
@ -172,84 +241,76 @@ async def api_ticket_make_ticket(event_id, name, email, promo_code, refund_addre
|
|||
|
||||
price = await fiat_amount_as_satoshis(price, event.currency)
|
||||
|
||||
try:
|
||||
payment = await create_invoice(
|
||||
wallet_id=event.wallet,
|
||||
amount=price,
|
||||
memo=f"{event_id}",
|
||||
extra=extra,
|
||||
)
|
||||
await create_ticket(
|
||||
payment_hash=payment.payment_hash,
|
||||
wallet=event.wallet,
|
||||
event=event.id,
|
||||
name=name,
|
||||
email=email,
|
||||
extra={
|
||||
"applied_promo_code": promo_code,
|
||||
"refund_address": refund_address,
|
||||
"sats_paid": int(price),
|
||||
},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(exc)
|
||||
) from exc
|
||||
return {"payment_hash": payment.payment_hash, "payment_request": payment.bolt11}
|
||||
|
||||
|
||||
@events_api_router.post("/api/v1/tickets/{event_id}/{payment_hash}")
|
||||
async def api_ticket_send_ticket(event_id, payment_hash):
|
||||
event = await get_event(event_id)
|
||||
if not event:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Event could not be fetched.",
|
||||
)
|
||||
|
||||
ticket = await get_ticket(payment_hash)
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Ticket could not be fetched.",
|
||||
)
|
||||
payment = await get_standalone_payment(payment_hash, incoming=True)
|
||||
assert payment
|
||||
|
||||
if ticket.extra.applied_promo_code:
|
||||
promo = next(
|
||||
(
|
||||
pc
|
||||
for pc in event.extra.promo_codes
|
||||
if pc.code == ticket.extra.applied_promo_code
|
||||
),
|
||||
None,
|
||||
)
|
||||
if promo:
|
||||
event.price_per_ticket *= 1 - promo.discount_percent / 100
|
||||
|
||||
price = (
|
||||
event.price_per_ticket * 1000
|
||||
if event.currency == "sats"
|
||||
else await fiat_amount_as_satoshis(event.price_per_ticket, event.currency)
|
||||
* 1000
|
||||
payment = await create_invoice(
|
||||
wallet_id=event.wallet,
|
||||
amount=price,
|
||||
memo=f"{event_id}",
|
||||
extra=extra,
|
||||
)
|
||||
await create_ticket(
|
||||
payment_hash=payment.payment_hash,
|
||||
wallet=event.wallet,
|
||||
event=event.id,
|
||||
name=name,
|
||||
email=email,
|
||||
extra={
|
||||
"applied_promo_code": promo_code,
|
||||
"refund_address": refund_address,
|
||||
"sats_paid": int(price),
|
||||
},
|
||||
)
|
||||
|
||||
# check if price is equal to payment.amount
|
||||
lower_bound = price * 0.99 # 1% decrease
|
||||
|
||||
if not payment.pending and abs(payment.amount) >= lower_bound: # allow 1% error
|
||||
ticket.extra.sats_paid = int(payment.amount / 1000)
|
||||
await set_ticket_paid(ticket)
|
||||
return {"paid": True, "ticket_id": ticket.id}
|
||||
|
||||
return {"paid": False}
|
||||
return TicketPaymentRequest(
|
||||
payment_hash=payment.payment_hash, payment_request=payment.bolt11
|
||||
)
|
||||
|
||||
|
||||
@events_api_router.delete("/api/v1/tickets/{ticket_id}")
|
||||
@tickets_api_router.websocket("/ws/{payment_hash}")
|
||||
async def websocket_endpoint(payment_hash: str, websocket: WebSocket) -> None:
|
||||
await websocket.accept()
|
||||
queue: asyncio.Queue[Ticket] = asyncio.Queue()
|
||||
register_payment_listener(payment_hash, queue)
|
||||
disconnect_task: asyncio.Task | None = None
|
||||
payment_task: asyncio.Task | None = None
|
||||
|
||||
try:
|
||||
ticket = await get_ticket(payment_hash)
|
||||
if ticket and ticket.paid:
|
||||
await websocket.send_json({"paid": True})
|
||||
return
|
||||
|
||||
while True:
|
||||
disconnect_task = asyncio.create_task(websocket.receive_text())
|
||||
payment_task = asyncio.create_task(queue.get())
|
||||
done, pending = await asyncio.wait(
|
||||
{disconnect_task, payment_task}, return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
|
||||
if disconnect_task in done:
|
||||
try:
|
||||
disconnect_task.result()
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
break
|
||||
|
||||
ticket = payment_task.result()
|
||||
await websocket.send_json({"paid": ticket.paid})
|
||||
if ticket.paid:
|
||||
break
|
||||
finally:
|
||||
for pending_task in (disconnect_task, payment_task):
|
||||
if pending_task and not pending_task.done():
|
||||
pending_task.cancel()
|
||||
deregister_payment_listener(payment_hash, queue)
|
||||
|
||||
|
||||
@tickets_api_router.delete("/{ticket_id}")
|
||||
async def api_ticket_delete(
|
||||
ticket_id: str, wallet: WalletTypeInfo = Depends(require_invoice_key)
|
||||
):
|
||||
ticket_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
|
||||
) -> None:
|
||||
ticket = await get_ticket(ticket_id)
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
|
|
@ -262,14 +323,8 @@ async def api_ticket_delete(
|
|||
await delete_ticket(ticket_id)
|
||||
|
||||
|
||||
@events_api_router.get("/api/v1/eventtickets/{event_id}")
|
||||
async def api_event_tickets(event_id: str) -> list[Ticket]:
|
||||
return await get_event_tickets(event_id)
|
||||
|
||||
|
||||
# TODO: PUT, updates db! @tal
|
||||
@events_api_router.get("/api/v1/register/ticket/{ticket_id}")
|
||||
async def api_event_register_ticket(ticket_id) -> list[Ticket]:
|
||||
@tickets_api_router.put("/register/{ticket_id}", response_model=PublicTicket)
|
||||
async def api_event_register_ticket(ticket_id) -> Ticket:
|
||||
ticket = await get_ticket(ticket_id)
|
||||
|
||||
if not ticket:
|
||||
|
|
@ -289,5 +344,5 @@ async def api_event_register_ticket(ticket_id) -> list[Ticket]:
|
|||
|
||||
ticket.registered = True
|
||||
ticket.reg_timestamp = datetime.now(timezone.utc)
|
||||
await update_ticket(ticket)
|
||||
return await get_event_tickets(ticket.event)
|
||||
ticket = await update_ticket(ticket)
|
||||
return ticket
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue