Skip to content

Commit 4bf867e

Browse files
authored
feat: add resend email button to ticket list (#51)
- resending only possible when ticket is paid.
1 parent 6768b78 commit 4bf867e

5 files changed

Lines changed: 111 additions & 13 deletions

File tree

config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"id": "events",
3-
"version": "1.3.0",
3+
"version": "1.6.1",
44
"name": "Events",
55
"repo": "https://github.com/lnbits/events",
66
"short_description": "Sell and register event tickets",

services.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
update_event,
2121
update_ticket,
2222
)
23-
from .models import Ticket
23+
from .models import Event, Ticket
2424

2525
DEFAULT_NOSTR_RELAYS = [
2626
"wss://relay.damus.io",
@@ -55,16 +55,7 @@ async def _send_ticket_notification(ticket: Ticket) -> None:
5555
logger.warning(f"Event {ticket.event} not found for ticket notification.")
5656
return
5757

58-
ticket_url = _ticket_url(ticket)
59-
subject = (
60-
event.extra.notification_subject.strip()
61-
or f"Your ticket for '{event.name}' is ready"
62-
)
63-
body = (
64-
event.extra.notification_body.strip()
65-
or f"Your ticket for '{event.name}' is ready."
66-
)
67-
message = f"{body}\n\nOpen it here: {ticket_url}"
58+
subject, message = _ticket_notification_message(ticket, event)
6859
updated = False
6960

7061
if (
@@ -97,6 +88,35 @@ async def _send_ticket_notification(ticket: Ticket) -> None:
9788
await update_ticket(ticket)
9889

9990

91+
async def resend_ticket_email_notification(ticket: Ticket) -> Ticket:
92+
event = await get_event(ticket.event)
93+
if not event:
94+
raise ValueError("Event does not exist.")
95+
if not settings.lnbits_email_notifications_enabled:
96+
raise ValueError("Email notifications are not enabled.")
97+
if not ticket.email:
98+
raise ValueError("Ticket does not have an email address.")
99+
100+
subject, message = _ticket_notification_message(ticket, event)
101+
await send_email_notification([ticket.email], message, subject)
102+
ticket.extra.email_notification_sent = True
103+
return await update_ticket(ticket)
104+
105+
106+
def _ticket_notification_message(ticket: Ticket, event: Event) -> tuple[str, str]:
107+
ticket_url = _ticket_url(ticket)
108+
subject = (
109+
event.extra.notification_subject.strip()
110+
or f"Your ticket for '{event.name}' is ready"
111+
)
112+
body = (
113+
event.extra.notification_body.strip()
114+
or f"Your ticket for '{event.name}' is ready."
115+
)
116+
117+
return subject, f"{body}\n\nOpen it here: {ticket_url}"
118+
119+
100120
async def _send_nostr_ticket_notification(identifier: str, message: str) -> None:
101121
if "@" in identifier:
102122
await send_user_notification(

static/js/index.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ window.PageEvents = {
44
return {
55
events: [],
66
tickets: [],
7+
resendingTicketEmails: [],
78
currencies: [],
89
eventsTable: {
910
columns: [
@@ -145,6 +146,35 @@ window.PageEvents = {
145146
.catch(LNbits.utils.notifyApiError)
146147
})
147148
},
149+
resendTicketEmail(ticket) {
150+
if (!ticket.paid || !ticket.email) return
151+
const wallet = _.findWhere(this.g.user.wallets, {id: ticket.wallet})
152+
if (!wallet) return
153+
154+
this.resendingTicketEmails.push(ticket.id)
155+
LNbits.api
156+
.request(
157+
'POST',
158+
'/events/api/v1/tickets/' + ticket.id + '/resend-email',
159+
wallet.adminkey
160+
)
161+
.then(response => {
162+
this.tickets = this.tickets.map(obj =>
163+
obj.id === ticket.id ? response.data : obj
164+
)
165+
Quasar.Notify.create({
166+
type: 'positive',
167+
message: 'Ticket email resent.',
168+
icon: null
169+
})
170+
})
171+
.catch(LNbits.utils.notifyApiError)
172+
.finally(() => {
173+
this.resendingTicketEmails = this.resendingTicketEmails.filter(
174+
ticketId => ticketId !== ticket.id
175+
)
176+
})
177+
},
148178
exportticketsCSV() {
149179
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
150180
},

static/js/index.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,10 +171,12 @@
171171
>
172172
<template v-slot:header="props">
173173
<q-tr :props="props">
174+
<q-th auto-width></q-th>
174175
<q-th auto-width></q-th>
175176
<q-th v-for="col in props.cols" :key="col.name" :props="props">
176177
<span v-text="col.label"></span>
177178
</q-th>
179+
<q-th auto-width></q-th>
178180
</q-tr>
179181
</template>
180182
<template v-slot:body="props">
@@ -191,6 +193,20 @@
191193
target="_blank"
192194
></q-btn>
193195
</q-td>
196+
<q-td auto-width>
197+
<q-btn
198+
flat
199+
dense
200+
size="xs"
201+
@click="resendTicketEmail(props.row)"
202+
icon="email"
203+
color="primary"
204+
:disable="!props.row.paid || !props.row.email"
205+
:loading="resendingTicketEmails.includes(props.row.id)"
206+
>
207+
<q-tooltip>Resend ticket email</q-tooltip>
208+
</q-btn>
209+
</q-td>
194210

195211
<q-td v-for="col in props.cols" :key="col.name" :props="props">
196212
<span v-text="col.value"></span>

views_api.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
Ticket,
5353
TicketPaymentRequest,
5454
)
55-
from .services import refund_tickets
55+
from .services import refund_tickets, resend_ticket_email_notification
5656
from .tasks import deregister_payment_listener, register_payment_listener
5757

5858
events_api_router = APIRouter(prefix="/api/v1/events")
@@ -388,6 +388,38 @@ async def api_ticket_delete(
388388
await delete_ticket(ticket_id)
389389

390390

391+
@tickets_api_router.post("/{ticket_id}/resend-email")
392+
async def api_ticket_resend_email(
393+
ticket_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
394+
) -> Ticket:
395+
ticket = await get_ticket(ticket_id)
396+
if not ticket:
397+
raise HTTPException(
398+
status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist."
399+
)
400+
401+
if ticket.wallet != wallet.wallet.id:
402+
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
403+
404+
if not ticket.paid:
405+
raise HTTPException(
406+
status_code=HTTPStatus.FORBIDDEN,
407+
detail="Only paid tickets can be resent by email.",
408+
)
409+
410+
try:
411+
return await resend_ticket_email_notification(ticket)
412+
except ValueError as exc:
413+
raise HTTPException(
414+
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
415+
) from exc
416+
except Exception as exc:
417+
raise HTTPException(
418+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
419+
detail="Failed to resend ticket email.",
420+
) from exc
421+
422+
391423
@tickets_api_router.put("/register/{ticket_id}")
392424
async def api_event_register_ticket(ticket_id) -> Ticket:
393425
ticket = await get_ticket(ticket_id)

0 commit comments

Comments
 (0)