Skip to content

Commit 42de6d4

Browse files
talvasconcelosdni
andauthored
feat: add promo codes and conditional events (#40)
* add extra column * add conditional events * refunds * conditional events working * adding promo codes * promo codes logic --------- Co-authored-by: dni ⚡ <[email protected]>
1 parent ee70c30 commit 42de6d4

File tree

10 files changed

+456
-65
lines changed

10 files changed

+456
-65
lines changed

crud.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
from lnbits.db import Database
44
from lnbits.helpers import urlsafe_short_hash
55

6-
from .models import CreateEvent, Event, Ticket
6+
from .models import CreateEvent, Event, Ticket, TicketExtra
77

88
db = Database("ext_events")
99

1010

1111
async def create_ticket(
12-
payment_hash: str, wallet: str, event: str, name: str, email: str
12+
payment_hash: str, wallet: str, event: str, name: str, email: str, extra: dict
1313
) -> Ticket:
1414
now = datetime.now(timezone.utc)
1515
ticket = Ticket(
@@ -22,6 +22,7 @@ async def create_ticket(
2222
paid=False,
2323
reg_timestamp=now,
2424
time=now,
25+
extra=TicketExtra(**extra) if extra else TicketExtra(),
2526
)
2627
await db.insert("events.ticket", ticket)
2728
return ticket

migrations.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,21 @@ async def m005_add_image_banner(db):
160160
Add a column to allow an image banner for the event
161161
"""
162162
await db.execute("ALTER TABLE events.events ADD COLUMN banner TEXT;")
163+
164+
165+
async def m006_add_extra_fields(db):
166+
"""
167+
Add a canceled and 'extra' column to events and ticket tables
168+
to support promo codes and ticket metadata.
169+
"""
170+
# Add canceled and 'extra' columns to events table
171+
await db.execute(
172+
"""
173+
ALTER TABLE events.events
174+
ADD COLUMN canceled BOOLEAN NOT NULL DEFAULT FALSE,
175+
ADD COLUMN extra TEXT;
176+
"""
177+
)
178+
179+
# Add 'extra' column to ticket table
180+
await db.execute("ALTER TABLE events.ticket ADD COLUMN extra TEXT;")

models.py

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,29 @@
11
from datetime import datetime
22

33
from fastapi import Query
4-
from pydantic import BaseModel, EmailStr
4+
from pydantic import BaseModel, EmailStr, Field, validator
5+
6+
7+
class PromoCode(BaseModel):
8+
code: str
9+
discount_percent: float = 0.0
10+
active: bool = True
11+
12+
# make the promo code uppercase
13+
@validator("code")
14+
def uppercase_code(cls, v):
15+
return v.upper()
16+
17+
@validator("discount_percent")
18+
def validate_discount_percent(cls, v):
19+
assert 0 <= v <= 100, "Discount must be between 0 and 100."
20+
return v
21+
22+
23+
class EventExtra(BaseModel):
24+
promo_codes: list[PromoCode] = Field(default_factory=list)
25+
conditional: bool = False
26+
min_tickets: int = 1
527

628

729
class CreateEvent(BaseModel):
@@ -15,11 +37,7 @@ class CreateEvent(BaseModel):
1537
amount_tickets: int = Query(..., ge=0)
1638
price_per_ticket: float = Query(..., ge=0)
1739
banner: str | None = None
18-
19-
20-
class CreateTicket(BaseModel):
21-
name: str
22-
email: EmailStr
40+
extra: EventExtra = Field(default_factory=EventExtra)
2341

2442

2543
class Event(BaseModel):
@@ -28,6 +46,7 @@ class Event(BaseModel):
2846
name: str
2947
info: str
3048
closing_date: str
49+
canceled: bool = False
3150
event_start_date: str
3251
event_end_date: str
3352
currency: str
@@ -36,6 +55,21 @@ class Event(BaseModel):
3655
time: datetime
3756
sold: int = 0
3857
banner: str | None = None
58+
extra: EventExtra = Field(default_factory=EventExtra)
59+
60+
61+
class TicketExtra(BaseModel):
62+
applied_promo_code: str | None = None
63+
sats_paid: int | None = None
64+
refund_address: str | None = None
65+
refunded: bool = False
66+
67+
68+
class CreateTicket(BaseModel):
69+
name: str
70+
email: EmailStr
71+
promo_code: str | None = None
72+
refund_address: str | None = None
3973

4074

4175
class Ticket(BaseModel):
@@ -48,3 +82,4 @@ class Ticket(BaseModel):
4882
paid: bool
4983
time: datetime
5084
reg_timestamp: datetime
85+
extra: TicketExtra = Field(default_factory=TicketExtra)

services.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
from .crud import get_event, update_event, update_ticket
1+
from lnurl import execute
2+
from loguru import logger
3+
4+
from .crud import (
5+
get_event,
6+
get_event_tickets,
7+
purge_unpaid_tickets,
8+
update_event,
9+
update_ticket,
10+
)
211
from .models import Ticket
312

413

@@ -16,3 +25,30 @@ async def set_ticket_paid(ticket: Ticket) -> Ticket:
1625
await update_event(event)
1726

1827
return ticket
28+
29+
30+
async def refund_tickets(event_id: str):
31+
"""
32+
Refund tickets for an event that has not met the minimum ticket requirement.
33+
This function should be called when the event is closed and the minimum ticket
34+
condition is not met.
35+
"""
36+
await purge_unpaid_tickets(event_id)
37+
tickets = await get_event_tickets(event_id)
38+
39+
if not tickets:
40+
return
41+
42+
for ticket in tickets:
43+
if ticket.extra.refunded:
44+
continue
45+
if ticket.paid and ticket.extra.refund_address and ticket.extra.sats_paid:
46+
try:
47+
res = await execute(
48+
ticket.extra.refund_address, str(ticket.extra.sats_paid)
49+
)
50+
if res:
51+
ticket.extra.refunded = True
52+
await update_ticket(ticket)
53+
except Exception as e:
54+
logger.error(f"Error refunding ticket {ticket.id}: {e}")

static/js/display.js

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ window.app = Vue.createApp({
99
show: false,
1010
data: {
1111
name: '',
12-
email: ''
12+
email: '',
13+
refund: ''
1314
}
1415
},
1516
ticketLink: {
@@ -29,7 +30,8 @@ window.app = Vue.createApp({
2930
this.info = event_info
3031
this.info = this.info.substring(1, this.info.length - 1)
3132
this.banner = event_banner
32-
await this.purgeUnpaidTickets()
33+
this.extra = event_extra
34+
this.hasPromoCodes = has_promoCodes
3335
},
3436
computed: {
3537
formatDescription() {
@@ -41,6 +43,7 @@ window.app = Vue.createApp({
4143
e.preventDefault()
4244
this.formDialog.data.name = ''
4345
this.formDialog.data.email = ''
46+
this.formDialog.data.refund = ''
4447
},
4548

4649
closeReceiveDialog() {
@@ -60,12 +63,12 @@ window.app = Vue.createApp({
6063
const regex = /^[\w\.-]+@[a-zA-Z\d\.-]+\.[a-zA-Z]{2,}$/
6164
return regex.test(val) || 'Please enter valid email.'
6265
},
63-
6466
Invoice() {
6567
axios
6668
.post(`/events/api/v1/tickets/${event_id}`, {
6769
name: this.formDialog.data.name,
68-
email: this.formDialog.data.email
70+
email: this.formDialog.data.email,
71+
promo_code: this.formDialog.data.promo_code || null
6972
})
7073
.then(response => {
7174
this.paymentReq = response.data.payment_request
@@ -122,13 +125,6 @@ window.app = Vue.createApp({
122125
}, 2000)
123126
})
124127
.catch(LNbits.utils.notifyApiError)
125-
},
126-
async purgeUnpaidTickets() {
127-
try {
128-
await LNbits.api.request('GET', `/events/api/v1/purge/${event_id}`)
129-
} catch (error) {
130-
LNbits.utils.notifyApiError(error)
131-
}
132128
}
133129
}
134130
})

0 commit comments

Comments
 (0)