feat: add resend email button to ticket list (#51)

- resending only possible when ticket is paid.
This commit is contained in:
dni ⚡ 2026-05-13 11:30:14 +02:00 committed by GitHub
commit 4bf867eef0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 111 additions and 13 deletions

View file

@ -1,6 +1,6 @@
{
"id": "events",
"version": "1.3.0",
"version": "1.6.1",
"name": "Events",
"repo": "https://github.com/lnbits/events",
"short_description": "Sell and register event tickets",

View file

@ -20,7 +20,7 @@ from .crud import (
update_event,
update_ticket,
)
from .models import Ticket
from .models import Event, Ticket
DEFAULT_NOSTR_RELAYS = [
"wss://relay.damus.io",
@ -55,16 +55,7 @@ async def _send_ticket_notification(ticket: Ticket) -> None:
logger.warning(f"Event {ticket.event} not found for ticket notification.")
return
ticket_url = _ticket_url(ticket)
subject = (
event.extra.notification_subject.strip()
or f"Your ticket for '{event.name}' is ready"
)
body = (
event.extra.notification_body.strip()
or f"Your ticket for '{event.name}' is ready."
)
message = f"{body}\n\nOpen it here: {ticket_url}"
subject, message = _ticket_notification_message(ticket, event)
updated = False
if (
@ -97,6 +88,35 @@ async def _send_ticket_notification(ticket: Ticket) -> None:
await update_ticket(ticket)
async def resend_ticket_email_notification(ticket: Ticket) -> Ticket:
event = await get_event(ticket.event)
if not event:
raise ValueError("Event does not exist.")
if not settings.lnbits_email_notifications_enabled:
raise ValueError("Email notifications are not enabled.")
if not ticket.email:
raise ValueError("Ticket does not have an email address.")
subject, message = _ticket_notification_message(ticket, event)
await send_email_notification([ticket.email], message, subject)
ticket.extra.email_notification_sent = True
return await update_ticket(ticket)
def _ticket_notification_message(ticket: Ticket, event: Event) -> tuple[str, str]:
ticket_url = _ticket_url(ticket)
subject = (
event.extra.notification_subject.strip()
or f"Your ticket for '{event.name}' is ready"
)
body = (
event.extra.notification_body.strip()
or f"Your ticket for '{event.name}' is ready."
)
return subject, f"{body}\n\nOpen it here: {ticket_url}"
async def _send_nostr_ticket_notification(identifier: str, message: str) -> None:
if "@" in identifier:
await send_user_notification(

View file

@ -4,6 +4,7 @@ window.PageEvents = {
return {
events: [],
tickets: [],
resendingTicketEmails: [],
currencies: [],
eventsTable: {
columns: [
@ -145,6 +146,35 @@ window.PageEvents = {
.catch(LNbits.utils.notifyApiError)
})
},
resendTicketEmail(ticket) {
if (!ticket.paid || !ticket.email) return
const wallet = _.findWhere(this.g.user.wallets, {id: ticket.wallet})
if (!wallet) return
this.resendingTicketEmails.push(ticket.id)
LNbits.api
.request(
'POST',
'/events/api/v1/tickets/' + ticket.id + '/resend-email',
wallet.adminkey
)
.then(response => {
this.tickets = this.tickets.map(obj =>
obj.id === ticket.id ? response.data : obj
)
Quasar.Notify.create({
type: 'positive',
message: 'Ticket email resent.',
icon: null
})
})
.catch(LNbits.utils.notifyApiError)
.finally(() => {
this.resendingTicketEmails = this.resendingTicketEmails.filter(
ticketId => ticketId !== ticket.id
)
})
},
exportticketsCSV() {
LNbits.utils.exportCSV(this.ticketsTable.columns, this.tickets)
},

View file

@ -171,10 +171,12 @@
>
<template v-slot:header="props">
<q-tr :props="props">
<q-th auto-width></q-th>
<q-th auto-width></q-th>
<q-th v-for="col in props.cols" :key="col.name" :props="props">
<span v-text="col.label"></span>
</q-th>
<q-th auto-width></q-th>
</q-tr>
</template>
<template v-slot:body="props">
@ -191,6 +193,20 @@
target="_blank"
></q-btn>
</q-td>
<q-td auto-width>
<q-btn
flat
dense
size="xs"
@click="resendTicketEmail(props.row)"
icon="email"
color="primary"
:disable="!props.row.paid || !props.row.email"
:loading="resendingTicketEmails.includes(props.row.id)"
>
<q-tooltip>Resend ticket email</q-tooltip>
</q-btn>
</q-td>
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<span v-text="col.value"></span>

View file

@ -52,7 +52,7 @@ from .models import (
Ticket,
TicketPaymentRequest,
)
from .services import refund_tickets
from .services import refund_tickets, resend_ticket_email_notification
from .tasks import deregister_payment_listener, register_payment_listener
events_api_router = APIRouter(prefix="/api/v1/events")
@ -388,6 +388,38 @@ async def api_ticket_delete(
await delete_ticket(ticket_id)
@tickets_api_router.post("/{ticket_id}/resend-email")
async def api_ticket_resend_email(
ticket_id: str, wallet: WalletTypeInfo = Depends(require_admin_key)
) -> Ticket:
ticket = await get_ticket(ticket_id)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Ticket does not exist."
)
if ticket.wallet != wallet.wallet.id:
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your ticket.")
if not ticket.paid:
raise HTTPException(
status_code=HTTPStatus.FORBIDDEN,
detail="Only paid tickets can be resent by email.",
)
try:
return await resend_ticket_email_notification(ticket)
except ValueError as exc:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST, detail=str(exc)
) from exc
except Exception as exc:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Failed to resend ticket email.",
) from exc
@tickets_api_router.put("/register/{ticket_id}")
async def api_event_register_ticket(ticket_id) -> Ticket:
ticket = await get_ticket(ticket_id)