Compare commits

..

6 commits

Author SHA1 Message Date
Patrick Mulligan
2740d73678 feat: add user_id ticket support and public events endpoint
Some checks failed
lint / lint (push) Has been cancelled
- Tickets can be created with user_id instead of name/email
- name/email default to empty string in DB (not NULL-safe)
- New endpoints: GET /api/v1/events/public, GET /api/v1/tickets/user/{user_id}
- POST /api/v1/tickets/{event_id} accepts user_id in body
- Migration m007 adds user_id column to tickets table
- CreateTicket validates: either user_id or (name + email) required

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-24 02:55:18 -04:00
dni ⚡
f06bd9a668
chore: prepare release, fix lint and uv warnings (#44)
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
2026-04-15 17:37:34 +02:00
PatMulligan
78433a7d85
Fix: SQLite migration syntax error in m006 (#41)
Ran into this issue on my lnbits 1.4 on NixOS using the flake

- Fix m006_add_extra_fields migration that fails on SQLite with syntax error
- Split multi-column ALTER TABLE into separate statements (SQLite doesn't support adding multiple columns in one statement)
2026-04-15 17:30:34 +02:00
DoktorShift
1dd6f8b67e
docs: changes to more pages (#42)
* Changes to more pages
* Update description.md

---------

Co-authored-by: dni  <office@dnilabs.com>
2026-01-28 17:22:09 +01:00
Tiago Vasconcelos
42de6d4791
feat: add promo codes and conditional events (#40)
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
* add extra column
* add conditional events
* refunds
* conditional events working
* adding promo codes
* promo codes logic

---------

Co-authored-by: dni  <office@dnilabs.com>
2025-12-09 10:48:00 +00:00
arbadacarba
ee70c300f6
Fix typos (#39) 2025-12-09 10:28:48 +01:00
12 changed files with 1282 additions and 1436 deletions

View file

@ -1,56 +0,0 @@
# Events API Documentation
## Public Events Endpoint
### GET `/api/v1/events/public`
Retrieve all events in the database with read-only access. No authentication required.
**Authentication:** None required (public endpoint)
**Headers:**
```
None required
```
**Query Parameters:**
- None
**Response:**
```json
[
{
"id": "event_id",
"wallet": "wallet_id",
"name": "Event Name",
"info": "Event description",
"closing_date": "2024-12-31",
"event_start_date": "2024-12-01",
"event_end_date": "2024-12-02",
"currency": "sat",
"amount_tickets": 100,
"price_per_ticket": 1000.0,
"time": "2024-01-01T00:00:00Z",
"sold": 0,
"banner": null
}
]
```
**Example Usage:**
```bash
curl http://your-lnbits-instance/events/api/v1/events/public
```
**Notes:**
- This endpoint allows read-only access to all events in the database
- No authentication required (truly public endpoint)
- Returns events ordered by creation time (newest first)
- Suitable for public event listings or read-only integrations
## Comparison with Existing Endpoints
| Endpoint | Authentication | Scope | Use Case |
|----------|---------------|-------|----------|
| `/api/v1/events` | Invoice Key | User's wallets only | Private event management |
| `/api/v1/events/public` | None | All events | Public event browsing |

View file

@ -1,10 +1,20 @@
<a href="https://lnbits.com" target="_blank" rel="noopener noreferrer">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i.imgur.com/QE6SIrs.png">
<img src="https://i.imgur.com/fyKPgVT.png" alt="LNbits" style="width:280px">
</picture>
</a>
[![License: MIT](https://img.shields.io/badge/License-MIT-success?logo=open-source-initiative&logoColor=white)](./LICENSE)
[![Built for LNbits](https://img.shields.io/badge/Built%20for-LNbits-4D4DFF?logo=lightning&logoColor=white)](https://github.com/lnbits/lnbits)
# Events - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small> # Events - <small>[LNbits](https://github.com/lnbits/lnbits) extension</small>
<small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions)</small> <small>For more about LNBits extension check [this tutorial](https://github.com/lnbits/lnbits/wiki/LNbits-Extensions)</small>
## Sell tickets for events and use the built-in scanner for registering attendees ## Sell tickets for events and use the built-in scanner for registering attendees
Events alows you to make tickets for an event. Each ticket is in the form of a unique QR code. After registering, and paying for ticket, the user gets a QR code to present at registration/entrance. Events allows you to create tickets for an event. Each ticket is in the form of a unique QR code. After registering and paying, the user gets a QR code to present at registration/entrance.
Events includes a shareable ticket scanner, which can be used to register attendees. Events includes a shareable ticket scanner, which can be used to register attendees.
@ -33,3 +43,10 @@ Events includes a shareable ticket scanner, which can be used to register attend
4. Use the built-in ticket scanner to validate registered, and paid, attendees\ 4. Use the built-in ticket scanner to validate registered, and paid, attendees\
![ticket scanner](https://i.imgur.com/zrm9202.jpg) ![ticket scanner](https://i.imgur.com/zrm9202.jpg)
## Powered by LNbits
[LNbits](https://lnbits.com) is a free and open-source lightning accounts system.
[![Visit LNbits Shop](https://img.shields.io/badge/Visit-LNbits%20Shop-7C3AED?logo=shopping-cart&logoColor=white&labelColor=5B21B6)](https://shop.lnbits.com/)
[![Try myLNbits SaaS](https://img.shields.io/badge/Try-myLNbits%20SaaS-2563EB?logo=lightning&logoColor=white&labelColor=1E40AF)](https://my.lnbits.com/login)

View file

@ -1,8 +1,11 @@
{ {
"id": "events",
"version": "1.2.1",
"name": "Events", "name": "Events",
"repo": "https://github.com/lnbits/events",
"short_description": "Sell and register event tickets", "short_description": "Sell and register event tickets",
"description": "",
"tile": "/events/static/image/events.png", "tile": "/events/static/image/events.png",
"lnbits": "1.1.0",
"min_lnbits_version": "1.3.0", "min_lnbits_version": "1.3.0",
"contributors": [ "contributors": [
{ {
@ -51,5 +54,9 @@
], ],
"description_md": "https://raw.githubusercontent.com/lnbits/events/main/description.md", "description_md": "https://raw.githubusercontent.com/lnbits/events/main/description.md",
"terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/events/main/toc.md", "terms_and_conditions_md": "https://raw.githubusercontent.com/lnbits/events/main/toc.md",
"license": "MIT" "license": "MIT",
"paid_features": "",
"tags": ["Fun & Social", "Ticketing"],
"donate": "",
"hidden": false
} }

128
crud.py
View file

@ -1,4 +1,3 @@
import json
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
@ -7,30 +6,6 @@ from lnbits.helpers import urlsafe_short_hash
from .models import CreateEvent, Event, Ticket, TicketExtra from .models import CreateEvent, Event, Ticket, TicketExtra
def _parse_ticket_row(row) -> dict:
"""
Parse a database row into a dict suitable for Ticket model creation.
Handles:
- Empty string to None conversion for name/email
- JSON string to dict conversion for extra field
"""
ticket_data = dict(row)
# Convert empty strings back to None for the model
if ticket_data.get("name") == "":
ticket_data["name"] = None
if ticket_data.get("email") == "":
ticket_data["email"] = None
# Parse extra field from JSON string if needed
# (db.insert() serializes to JSON, but manual fetchone/fetchall returns string)
extra = ticket_data.get("extra")
if isinstance(extra, str):
ticket_data["extra"] = json.loads(extra)
return ticket_data
db = Database("ext_events") db = Database("ext_events")
@ -38,48 +13,13 @@ async def create_ticket(
payment_hash: str, payment_hash: str,
wallet: str, wallet: str,
event: str, event: str,
name: Optional[str] = None, name: str = "",
email: Optional[str] = None, email: str = "",
user_id: Optional[str] = None, user_id: Optional[str] = None,
extra: Optional[dict] = None, extra: Optional[dict] = None,
) -> Ticket: ) -> Ticket:
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
ticket = Ticket(
# TODO: Check if this empty string workaround is still needed.
# This converts None to empty strings for database storage because:
# 1. Database may have NOT NULL constraints on name/email columns
# 2. When user_id is provided, name/email are not used (mutually exclusive)
# 3. The get_ticket() functions convert empty strings back to None when reading
# Consider using nullable columns instead of this empty string pattern.
if user_id:
db_name = ""
db_email = ""
else:
db_name = name or ""
db_email = email or ""
# Create ticket with database-compatible values for insertion
# Using db.insert() ensures proper serialization of the extra field (TicketExtra)
# across all database backends (SQLite, PostgreSQL, CockroachDB)
db_ticket = Ticket(
id=payment_hash,
wallet=wallet,
event=event,
name=db_name,
email=db_email,
user_id=user_id,
registered=False,
paid=False,
reg_timestamp=now,
time=now,
extra=TicketExtra(**extra) if extra else TicketExtra(),
)
await db.insert("events.ticket", db_ticket)
# Return ticket with original name/email values (not empty strings)
# This maintains consistency with how get_ticket() converts empty strings back to None
return Ticket(
id=payment_hash, id=payment_hash,
wallet=wallet, wallet=wallet,
event=event, event=event,
@ -92,54 +32,32 @@ async def create_ticket(
time=now, time=now,
extra=TicketExtra(**extra) if extra else TicketExtra(), extra=TicketExtra(**extra) if extra else TicketExtra(),
) )
await db.insert("events.ticket", ticket)
async def update_ticket(ticket: Ticket) -> Ticket:
# Create a new Ticket object with corrected values for database constraints
ticket_dict = ticket.dict()
# Convert None values to empty strings for database constraints
if ticket_dict.get("name") is None:
ticket_dict["name"] = ""
if ticket_dict.get("email") is None:
ticket_dict["email"] = ""
# Create a new Ticket object with the corrected values
corrected_ticket = Ticket(**ticket_dict)
await db.update("events.ticket", corrected_ticket)
return ticket return ticket
async def get_ticket(payment_hash: str) -> Optional[Ticket]: async def update_ticket(ticket: Ticket) -> Ticket:
row = await db.fetchone( await db.update("events.ticket", ticket)
return ticket
async def get_ticket(payment_hash: str) -> Ticket | None:
return await db.fetchone(
"SELECT * FROM events.ticket WHERE id = :id", "SELECT * FROM events.ticket WHERE id = :id",
{"id": payment_hash}, {"id": payment_hash},
Ticket,
) )
if not row:
return None
return Ticket(**_parse_ticket_row(row))
async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]: async def get_tickets(wallet_ids: str | list[str]) -> list[Ticket]:
if isinstance(wallet_ids, str): if isinstance(wallet_ids, str):
wallet_ids = [wallet_ids] wallet_ids = [wallet_ids]
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids]) q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
rows = await db.fetchall(f"SELECT * FROM events.ticket WHERE wallet IN ({q})") return await db.fetchall(
f"SELECT * FROM events.ticket WHERE wallet IN ({q})",
return [Ticket(**_parse_ticket_row(row)) for row in rows] model=Ticket,
async def get_tickets_by_user_id(user_id: str) -> list[Ticket]:
"""Get all tickets for a specific user by their user_id"""
rows = await db.fetchall(
"SELECT * FROM events.ticket WHERE user_id = :user_id ORDER BY time DESC",
{"user_id": user_id}
) )
return [Ticket(**_parse_ticket_row(row)) for row in rows]
async def delete_ticket(payment_hash: str) -> None: async def delete_ticket(payment_hash: str) -> None:
await db.execute("DELETE FROM events.ticket WHERE id = :id", {"id": payment_hash}) await db.execute("DELETE FROM events.ticket WHERE id = :id", {"id": payment_hash})
@ -193,21 +111,29 @@ async def get_events(wallet_ids: str | list[str]) -> list[Event]:
async def get_all_events() -> list[Event]: async def get_all_events() -> list[Event]:
"""Get all events from the database without wallet filtering.""" """Get all events without wallet filtering (public endpoint)."""
return await db.fetchall( return await db.fetchall(
"SELECT * FROM events.events ORDER BY time DESC", "SELECT * FROM events.events ORDER BY time DESC",
model=Event, model=Event,
) )
async def get_tickets_by_user_id(user_id: str) -> list[Ticket]:
"""Get all tickets for a specific user by their user_id."""
return await db.fetchall(
"SELECT * FROM events.ticket WHERE user_id = :user_id ORDER BY time DESC",
{"user_id": user_id},
model=Ticket,
)
async def delete_event(event_id: str) -> None: async def delete_event(event_id: str) -> None:
await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id}) await db.execute("DELETE FROM events.events WHERE id = :id", {"id": event_id})
async def get_event_tickets(event_id: str) -> list[Ticket]: async def get_event_tickets(event_id: str) -> list[Ticket]:
rows = await db.fetchall( return await db.fetchall(
"SELECT * FROM events.ticket WHERE event = :event", "SELECT * FROM events.ticket WHERE event = :event",
{"event": event_id}, {"event": event_id},
Ticket,
) )
return [Ticket(**_parse_ticket_row(row)) for row in rows]

View file

@ -1,5 +1,10 @@
Sell tickets for events and use the built-in scanner for registering attendants Sell tickets for events and manage attendee registration with a built-in QR scanner.
Events alows you to make tickets for an event. Each ticket is in the form of a uniqque QR code. After registering, and paying for ticket, the user gets a QR code to present at registration/entrance. Its features include:
Events includes a shareable ticket scanner, which can be used to register attendees. - Creating events with ticket pricing
- Generating unique QR code tickets after payment
- Providing a shareable ticket scanner for check-in
- Tracking registered and checked-in attendees
A complete ticketing solution for event organizers, meetup hosts, and conference planners who want to sell tickets and manage attendance with Bitcoin.

View file

@ -162,32 +162,24 @@ async def m005_add_image_banner(db):
await db.execute("ALTER TABLE events.events ADD COLUMN banner TEXT;") await db.execute("ALTER TABLE events.events ADD COLUMN banner TEXT;")
async def m006_add_user_id_support(db): async def m006_add_extra_fields(db):
"""
Add user_id column to tickets table to support LNbits user-id as identifier
Make name and email optional when user_id is provided
"""
await db.execute("ALTER TABLE events.ticket ADD COLUMN user_id TEXT;")
# Since SQLite doesn't support changing column constraints directly,
# we'll work around this by allowing the application logic to handle
# the validation that either (name AND email) OR user_id is provided
# The database will continue to expect name and email as NOT NULL
# but we'll insert empty strings for user_id tickets
async def m007_add_extra_fields(db):
""" """
Add a canceled and 'extra' column to events and ticket tables Add a canceled and 'extra' column to events and ticket tables
to support promo codes and ticket metadata. to support promo codes and ticket metadata.
""" """
# Add canceled and 'extra' columns to events table # Add canceled and 'extra' columns to events table
# SQLite requires separate ALTER TABLE statements for each column
await db.execute( await db.execute(
"ALTER TABLE events.events ADD COLUMN canceled BOOLEAN NOT NULL DEFAULT FALSE;" "ALTER TABLE events.events ADD COLUMN canceled BOOLEAN NOT NULL DEFAULT FALSE;"
) )
await db.execute( await db.execute("ALTER TABLE events.events ADD COLUMN extra TEXT;")
"ALTER TABLE events.events ADD COLUMN extra TEXT;"
)
# Add 'extra' column to ticket table # Add 'extra' column to ticket table
await db.execute("ALTER TABLE events.ticket ADD COLUMN extra TEXT;") await db.execute("ALTER TABLE events.ticket ADD COLUMN extra TEXT;")
async def m007_add_user_id(db):
"""
Add user_id column to tickets table.
Allows ticket purchase via LNbits user-id without name/email.
"""
await db.execute("ALTER TABLE events.ticket ADD COLUMN user_id TEXT;")

View file

@ -37,31 +37,10 @@ class CreateEvent(BaseModel):
currency: str = "sat" currency: str = "sat"
amount_tickets: int = Query(..., ge=0) amount_tickets: int = Query(..., ge=0)
price_per_ticket: float = Query(..., ge=0) price_per_ticket: float = Query(..., ge=0)
banner: Optional[str] = None banner: str | None = None
extra: EventExtra = Field(default_factory=EventExtra) extra: EventExtra = Field(default_factory=EventExtra)
class CreateTicket(BaseModel):
name: Optional[str] = None
email: Optional[EmailStr] = None
user_id: Optional[str] = None
promo_code: Optional[str] = None
refund_address: Optional[str] = None
@root_validator
def validate_identifiers(cls, values):
# Ensure either (name AND email) OR user_id is provided
name = values.get('name')
email = values.get('email')
user_id = values.get('user_id')
if not user_id and not (name and email):
raise ValueError("Either user_id or both name and email must be provided")
if user_id and (name or email):
raise ValueError("Cannot provide both user_id and name/email")
return values
class Event(BaseModel): class Event(BaseModel):
id: str id: str
wallet: str wallet: str
@ -87,12 +66,29 @@ class TicketExtra(BaseModel):
refunded: bool = False refunded: bool = False
class CreateTicket(BaseModel):
name: Optional[str] = None
email: Optional[str] = None
user_id: Optional[str] = None
promo_code: str | None = None
refund_address: str | None = None
@root_validator
def validate_identifiers(cls, values):
user_id = values.get("user_id")
name = values.get("name")
email = values.get("email")
if not user_id and not (name and email):
raise ValueError("Either user_id or both name and email must be provided")
return values
class Ticket(BaseModel): class Ticket(BaseModel):
id: str id: str
wallet: str wallet: str
event: str event: str
name: Optional[str] = None name: str = ""
email: Optional[str] = None email: str = ""
user_id: Optional[str] = None user_id: Optional[str] = None
registered: bool registered: bool
paid: bool paid: bool

View file

@ -7,11 +7,8 @@ authors = [{ name = "Alan Bits", email = "alan@lnbits.com" }]
urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/events" } urls = { Homepage = "https://lnbits.com", Repository = "https://github.com/lnbits/events" }
dependencies = [ "lnbits>1" ] dependencies = [ "lnbits>1" ]
[tool.poetry] [dependency-groups]
package-mode = false dev = [
[tool.uv]
dev-dependencies = [
"black", "black",
"pytest-asyncio", "pytest-asyncio",
"pytest", "pytest",
@ -20,6 +17,9 @@ dev-dependencies = [
"ruff", "ruff",
] ]
[tool.poetry]
package-mode = false
[tool.mypy] [tool.mypy]
exclude = "(nostr/*)" exclude = "(nostr/*)"
plugins = ["pydantic.mypy"] plugins = ["pydantic.mypy"]

View file

@ -21,12 +21,8 @@ async def on_invoice_paid(payment: Payment) -> None:
if not payment.extra or "events" != payment.extra.get("tag"): if not payment.extra or "events" != payment.extra.get("tag"):
return return
# Check if ticket has either name/email or user_id if not payment.extra.get("name") or not payment.extra.get("email"):
has_name_email = payment.extra.get("name") and payment.extra.get("email") logger.warning(f"Ticket {payment.payment_hash} missing name or email.")
has_user_id = payment.extra.get("user_id")
if not has_name_email and not has_user_id:
logger.warning(f"Ticket {payment.payment_hash} missing name/email or user_id.")
return return
ticket = await get_ticket(payment.payment_hash) ticket = await get_ticket(payment.payment_hash)

View file

@ -1,108 +0,0 @@
import pytest
from fastapi.testclient import TestClient
from unittest.mock import AsyncMock, patch
from ..views_api import events_api_router
from ..models import Event
from datetime import datetime, timezone
@pytest.mark.asyncio
async def test_api_events_public():
"""Test the new public events API endpoint"""
from fastapi import FastAPI
app = FastAPI()
app.include_router(events_api_router)
# Mock the database
with patch('events.crud.get_all_events') as mock_get_all_events:
# Create mock events
mock_events = [
Event(
id="test_event_1",
wallet="test_wallet_1",
name="Test Event 1",
info="Test event description",
closing_date="2024-12-31",
event_start_date="2024-12-01",
event_end_date="2024-12-02",
currency="sat",
amount_tickets=100,
price_per_ticket=1000.0,
time=datetime.now(timezone.utc),
sold=0,
banner=None
),
Event(
id="test_event_2",
wallet="test_wallet_2",
name="Test Event 2",
info="Another test event",
closing_date="2024-12-31",
event_start_date="2024-12-03",
event_end_date="2024-12-04",
currency="sat",
amount_tickets=50,
price_per_ticket=500.0,
time=datetime.now(timezone.utc),
sold=0,
banner=None
)
]
mock_get_all_events.return_value = mock_events
client = TestClient(app)
# Test the endpoint without any authentication
response = client.get("/api/v1/events/public")
# Verify the response
assert response.status_code == 200
data = response.json()
assert len(data) == 2
assert data[0]["id"] == "test_event_1"
assert data[1]["id"] == "test_event_2"
assert data[0]["name"] == "Test Event 1"
assert data[1]["name"] == "Test Event 2"
@pytest.mark.asyncio
async def test_get_all_events_crud():
"""Test the get_all_events CRUD function"""
from events.crud import get_all_events
with patch('events.crud.db.fetchall') as mock_fetchall:
# Mock database response
mock_events = [
{
"id": "test_event_1",
"wallet": "test_wallet_1",
"name": "Test Event 1",
"info": "Test event description",
"closing_date": "2024-12-31",
"event_start_date": "2024-12-01",
"event_end_date": "2024-12-02",
"currency": "sat",
"amount_tickets": 100,
"price_per_ticket": 1000.0,
"time": datetime.now(timezone.utc),
"sold": 0,
"banner": None
}
]
mock_fetchall.return_value = mock_events
events = await get_all_events()
# Verify the function was called with correct parameters
mock_fetchall.assert_called_once_with(
"SELECT * FROM events.events ORDER BY time DESC",
model=Event,
)
# Verify the result
assert len(events) == 1
assert events[0]["id"] == "test_event_1"

2144
uv.lock generated

File diff suppressed because it is too large Load diff

View file

@ -21,13 +21,13 @@ from .crud import (
delete_event, delete_event,
delete_event_tickets, delete_event_tickets,
delete_ticket, delete_ticket,
get_all_events,
get_event, get_event,
get_event_tickets, get_event_tickets,
get_events, get_events,
get_ticket, get_ticket,
get_tickets, get_tickets,
get_tickets_by_user_id, get_tickets_by_user_id,
purge_unpaid_tickets,
update_event, update_event,
update_ticket, update_ticket,
) )
@ -53,14 +53,8 @@ async def api_events(
@events_api_router.get("/api/v1/events/public") @events_api_router.get("/api/v1/events/public")
async def api_events_public(): async def api_events_public():
""" """Retrieve all events (read-only, no auth required)."""
Retrieve all events in the database with read-only access. return [event.dict() for event in await get_all_events()]
This endpoint allows access to all events using any valid API key (read access).
"""
# Get all events from the database without wallet filtering
from .crud import get_all_events
events = await get_all_events()
return [event.dict() for event in events]
@events_api_router.post("/api/v1/events") @events_api_router.post("/api/v1/events")
@ -147,7 +141,7 @@ async def api_tickets(
@events_api_router.get("/api/v1/tickets/user/{user_id}") @events_api_router.get("/api/v1/tickets/user/{user_id}")
async def api_tickets_by_user_id(user_id: str) -> list[Ticket]: async def api_tickets_by_user_id(user_id: str) -> list[Ticket]:
"""Get all tickets for a specific user by their user_id""" """Get all tickets for a specific user by their user_id."""
return await get_tickets_by_user_id(user_id) return await get_tickets_by_user_id(user_id)
@ -155,55 +149,11 @@ async def api_tickets_by_user_id(user_id: str) -> list[Ticket]:
async def api_ticket_create(event_id: str, data: CreateTicket): async def api_ticket_create(event_id: str, data: CreateTicket):
if data.user_id: if data.user_id:
return await api_ticket_make_ticket_with_user_id(event_id, data.user_id) return await api_ticket_make_ticket_with_user_id(event_id, data.user_id)
else: promo_code = data.promo_code.upper() if data.promo_code else None
promo_code = data.promo_code.upper() if data.promo_code else None refund_address = data.refund_address
refund_address = data.refund_address return await api_ticket_make_ticket(
return await api_ticket_make_ticket( event_id, data.name, data.email, promo_code, refund_address
event_id, data.name, data.email, promo_code, refund_address )
)
async def api_ticket_make_ticket_with_user_id(event_id: str, user_id: str):
event = await get_event(event_id)
if not event:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
)
price = event.price_per_ticket
extra = {"tag": "events", "user_id": user_id}
if event.currency != "sats":
price = await fiat_amount_as_satoshis(event.price_per_ticket, event.currency)
extra["fiat"] = True
extra["currency"] = event.currency
extra["fiatAmount"] = event.price_per_ticket
extra["rate"] = await get_fiat_rate_satoshis(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,
user_id=user_id,
)
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.get("/api/v1/tickets/{event_id}/user/{user_id}")
async def api_ticket_make_ticket_user_id(event_id: str, user_id: str):
return await api_ticket_make_ticket_with_user_id(event_id, user_id)
@events_api_router.get("/api/v1/tickets/{event_id}/{name}/{email}") @events_api_router.get("/api/v1/tickets/{event_id}/{name}/{email}")
@ -262,6 +212,43 @@ async def api_ticket_make_ticket(event_id, name, email, promo_code, refund_addre
return {"payment_hash": payment.payment_hash, "payment_request": payment.bolt11} return {"payment_hash": payment.payment_hash, "payment_request": payment.bolt11}
async def api_ticket_make_ticket_with_user_id(event_id: str, user_id: str):
event = await get_event(event_id)
if not event:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Event does not exist."
)
price = event.price_per_ticket
extra = {"tag": "events", "user_id": user_id}
if event.currency != "sats":
extra["fiat"] = True
extra["currency"] = event.currency
extra["fiatAmount"] = event.price_per_ticket
extra["rate"] = await get_fiat_rate_satoshis(event.currency)
price = await fiat_amount_as_satoshis(event.price_per_ticket, 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,
user_id=user_id,
)
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}") @events_api_router.post("/api/v1/tickets/{event_id}/{payment_hash}")
async def api_ticket_send_ticket(event_id, payment_hash): async def api_ticket_send_ticket(event_id, payment_hash):
event = await get_event(event_id) event = await get_event(event_id)