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:
parent
4e1888ae8e
commit
e227f29bcd
2 changed files with 133 additions and 0 deletions
116
app/pages/policies/refunds.vue
Normal file
116
app/pages/policies/refunds.vue
Normal 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>
|
||||||
|
|
@ -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)
|
// Store registration data before removing (convert to plain object)
|
||||||
const registration = {
|
const registration = {
|
||||||
name: eventDoc.registrations[registrationIndex].name,
|
name: eventDoc.registrations[registrationIndex].name,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue