feat(events): block self-cancel of paid registrations, add refunds policy

Self-cancel endpoint now rejects paid registrations (public, series_pass,
or paid member tickets) with a 403 pointing to /policies/refunds. Free
and $0-member registrations still self-cancel as before. Adds the
refunds policy page referenced in the error message.
This commit is contained in:
Jennie Robinson Faber 2026-04-20 19:34:04 +01:00
parent 4e1888ae8e
commit e227f29bcd
2 changed files with 133 additions and 0 deletions

View file

@ -0,0 +1,116 @@
<template>
<PageShell title="Refund Policy" subtitle="How Ghost Guild handles refund requests">
<div class="policy-prose">
<p class="policy-updated">Last updated: April 20, 2026</p>
<section class="policy-section">
<p>
Ghost Guild is a program of Baby Ghosts, a Canadian non-profit.
Contributions and event ticket revenue go directly toward running the
program. We handle refund requests on a case-by-case basis rather
than by blanket rule.
</p>
</section>
<section class="policy-section">
<h2>Membership dues</h2>
<p>
Membership is pay-what-you-can, and you can change or pause your
contribution any time as your situation changes. We don't refund dues
that have already been charged. If paying ever becomes a problem, the
Solidarity Fund is there for that reason; reach out and we'll work it
out.
</p>
</section>
<section class="policy-section">
<h2>Event tickets</h2>
<p>
Paid event registrations can't be cancelled from your account page.
Refunds for events are considered case-by-case at admin discretion,
taking into account how close to the event you're asking, whether
your spot can be filled, and the circumstances behind the request.
</p>
<p>
If you can no longer attend an event you've paid for, email
<a href="mailto:hello@ghostguild.org">hello@ghostguild.org</a> as
early as you can and we'll sort it out with you.
</p>
</section>
<section class="policy-section">
<h2>Contact</h2>
<p>
Refund requests, questions, anything else:
<a href="mailto:hello@ghostguild.org">hello@ghostguild.org</a>
</p>
</section>
</div>
</PageShell>
</template>
<script setup>
useHead({
title: 'Refund Policy · Ghost Guild',
})
</script>
<style scoped>
.policy-prose {
max-width: 720px;
padding: 32px;
}
.policy-updated {
font-family: "Commit Mono", monospace;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--candle);
margin-bottom: 24px;
}
.policy-section {
padding: 28px 0;
border-bottom: 1px dashed var(--border);
}
.policy-section:first-of-type {
padding-top: 0;
}
.policy-section:last-child {
border-bottom: none;
}
.policy-section h2 {
font-family: "Brygada 1918", serif;
font-size: 20px;
font-weight: 600;
color: var(--text-bright);
letter-spacing: -0.01em;
margin-bottom: 16px;
line-height: 1.25;
}
.policy-section p {
font-size: 13px;
color: var(--text-dim);
line-height: 1.7;
margin-bottom: 12px;
}
.policy-section a {
color: var(--candle);
}
.policy-section strong {
color: var(--text-bright);
font-weight: 600;
}
@media (max-width: 640px) {
.policy-prose {
padding: 20px 16px;
}
}
</style>

View file

@ -42,6 +42,23 @@ export default defineEventHandler(async (event) => {
});
}
const existingRegistration = eventDoc.registrations[registrationIndex];
const ticketType = existingRegistration.ticketType;
const amountPaid = existingRegistration.amountPaid || 0;
// member tickets can be free (default) or paid via circle overrides — gate on amountPaid
const isPaidRegistration =
ticketType === "public" ||
ticketType === "series_pass" ||
(ticketType === "member" && amountPaid > 0);
if (isPaidRegistration) {
throw createError({
statusCode: 403,
statusMessage:
"Paid registrations can't be self-cancelled. Email us for a refund — see /policies/refunds.",
});
}
// Store registration data before removing (convert to plain object)
const registration = {
name: eventDoc.registrations[registrationIndex].name,