Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ test:
PYTHONUNBUFFERED=1 \
DEBUG=true \
uv run pytest

test-js:
node --test tests/test_registration.js
install-pre-commit-hook:
@echo "Installing pre-commit hook to git"
@echo "Uninstall the hook with uv run pre-commit uninstall"
Expand Down
4 changes: 3 additions & 1 deletion __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from loguru import logger

from .crud import db
from .tasks import wait_for_paid_invoices
from .tasks import close_expired_events, wait_for_paid_invoices
from .views import events_generic_router
from .views_api import events_api_router, qr_api_router, tickets_api_router

Expand Down Expand Up @@ -37,6 +37,8 @@ def events_start():

task = create_permanent_unique_task("ext_events", wait_for_paid_invoices)
scheduled_tasks.append(task)
task = create_permanent_unique_task("ext_events_close", close_expired_events)
scheduled_tasks.append(task)


__all__ = ["db", "events_ext", "events_start", "events_static_files", "events_stop"]
2 changes: 1 addition & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"short_description": "Sell and register event tickets",
"description": "",
"tile": "/events/static/image/events.png",
"min_lnbits_version": "1.5.5",
"min_lnbits_version": "1.6.0",
"contributors": [
{
"name": "talvasconcelos",
Expand Down
63 changes: 63 additions & 0 deletions crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

from lnbits.db import Database, Filters, Page
from lnbits.helpers import urlsafe_short_hash
from sqlalchemy.exc import IntegrityError # type: ignore[import-untyped]

from .models import (
CreateEvent,
Event,
Ticket,
TicketExtra,
TicketFilters,
TicketRefund,
sync_event_ticket_waves,
)

Expand Down Expand Up @@ -41,6 +43,40 @@ async def update_ticket(ticket: Ticket) -> Ticket:
return ticket


async def register_ticket(ticket: Ticket) -> bool:
now = datetime.now(timezone.utc)
result = await db.execute(
f"""
UPDATE events.ticket SET registered = true,
reg_timestamp = {db.timestamp_placeholder("reg_timestamp")}
WHERE id = :id AND paid = true AND registered = false
""",
{"id": ticket.id, "reg_timestamp": now.timestamp()},
)
if result.rowcount != 1:
return False
ticket.registered = True
ticket.reg_timestamp = now
return True


async def get_ticket_refund(ticket_id: str) -> TicketRefund | None:
return await db.fetchone(
"SELECT * FROM events.refunds WHERE id = :id", {"id": ticket_id}, TicketRefund
)


async def create_ticket_refund(refund: TicketRefund) -> TicketRefund:
try:
await db.insert("events.refunds", refund)
except IntegrityError:
existing = await get_ticket_refund(refund.id)
if not existing:
raise ValueError("Refund invoice is already assigned.") from None
return existing
return refund


async def get_ticket(payment_hash: str) -> Ticket | None:
return await db.fetchone(
"SELECT * FROM events.ticket WHERE id = :id",
Expand Down Expand Up @@ -139,6 +175,33 @@ async def get_events(wallet_ids: str | list[str]) -> list[Event]:
return [cast(Event, sync_event_ticket_waves(event)) for event in events]


async def get_closed_events() -> list[Event]:
return await db.fetchall(
"""
SELECT * FROM events.events WHERE close_processed = false
AND (canceled = true OR closing_date < :today)
""",
{"today": datetime.now(timezone.utc).date().isoformat()},
Event,
)


async def mark_event_closed(event: Event) -> None:
await db.execute(
"""
UPDATE events.events SET close_processed = true
WHERE id = :id AND sold = :sold AND canceled = :canceled
AND closing_date = :closing_date
""",
{
"id": event.id,
"sold": event.sold,
"canceled": event.canceled,
"closing_date": event.closing_date,
},
)


async def delete_event(event_id: str) -> None:
await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id})

Expand Down
17 changes: 17 additions & 0 deletions migrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,20 @@ async def m008_add_fiat_currency(db):
ALTER TABLE events.events
ADD COLUMN fiat_currency TEXT NOT NULL DEFAULT 'GBP';
""")


async def m009_add_ticket_refunds(db):
await db.execute("""
CREATE TABLE events.refunds (
id TEXT PRIMARY KEY,
payment_request TEXT NOT NULL,
payment_hash TEXT NOT NULL UNIQUE
);
""")


async def m010_add_close_processed(db):
await db.execute("""
ALTER TABLE events.events
ADD COLUMN close_processed BOOLEAN NOT NULL DEFAULT FALSE;
""")
23 changes: 18 additions & 5 deletions models.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,19 @@ class TicketWave(BaseModel):
price_per_ticket: float = Field(default=0, ge=0)


class EventExtra(BaseModel):
promo_codes: list[PromoCode] = Field(default_factory=list)
class PublicEventExtra(BaseModel):
ticket_waves: list[TicketWave] = Field(default_factory=list)
conditional: bool = False
min_tickets: int = 1
email_notifications: bool = False
nostr_notifications: bool = False
onchain_enabled: bool = False


class EventExtra(PublicEventExtra):
promo_codes: list[PromoCode] = Field(default_factory=list)
notification_subject: str = ""
notification_body: str = ""
onchain_enabled: bool = False
onchain_wallet_id: str | None = None
onchain_zeroconf: bool = False
onchain_fasttrack: bool = False
Expand Down Expand Up @@ -74,6 +77,7 @@ class Event(BaseModel):
info: str
closing_date: str
canceled: bool = False
close_processed: bool = False
event_start_date: str
event_end_date: str
currency: str
Expand All @@ -100,7 +104,7 @@ class PublicEvent(BaseModel):
fiat_currency: str = "GBP"
price_per_ticket: float
banner: str | None
extra: EventExtra = Field(default_factory=EventExtra)
extra: PublicEventExtra = Field(default_factory=PublicEventExtra)


class TicketExtra(BaseModel):
Expand All @@ -110,7 +114,6 @@ class TicketExtra(BaseModel):
sats_paid: int | None = None
refund_address: str | None = None
nostr_identifier: str | None = None
ticket_base_url: str | None = None
email_notification_sent: bool = False
nostr_notification_sent: bool = False
refunded: bool = False
Expand Down Expand Up @@ -168,6 +171,16 @@ class PublicTicket(BaseModel):
reg_timestamp: datetime


class TicketRegistration(PublicTicket):
id: str


class TicketRefund(BaseModel):
id: str
payment_request: str
payment_hash: str


class TicketPaymentRequest(BaseModel):
payment_hash: str
payment_request: str | None = None
Expand Down
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ requires-python = ">=3.10,<3.13"
description = "LNbits, free and open-source Lightning wallet and accounts system."
authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/events" }
dependencies = [ "lnbits>1" ]
dependencies = [ "lnbits>=1.6.0" ]

[dependency-groups]
dev = [
Expand All @@ -20,6 +20,9 @@ dev = [
[tool.poetry]
package-mode = false

# Keep UV configuration independent of a parent LNbits checkout.
[tool.uv]

[tool.mypy]
exclude = "(nostr/*)"
plugins = ["pydantic.mypy"]
Expand Down
Loading
Loading