refactor(events): gate member benefits on hasMemberAccess

Extracts hasMemberAccess(member) in tickets.js and uses it across event
registration, ticket purchase, and series purchase flows so guest, suspended,
and cancelled records no longer count as members while pending_payment still
does.
This commit is contained in:
Jennie Robinson Faber 2026-04-18 17:06:17 +01:00
parent c5e901ed24
commit 15329e3e84
7 changed files with 188 additions and 30 deletions

View file

@ -1,5 +1,14 @@
// Ticket business logic utilities
/**
* Whether a Member document confers member-tier access to events.
* Status `active` and `pending_payment` are equivalent for access (payment is
* decoupled from membership). Status `guest`, `suspended`, `cancelled`, or no
* member at all do not confer access.
*/
export const hasMemberAccess = (member) =>
!!member && (member.status === "active" || member.status === "pending_payment");
/**
* Calculate the applicable ticket price for a user
* @param {Object} event - Event document
@ -7,15 +16,18 @@
* @returns {Object} { ticketType, price, currency, isEarlyBird }
*/
export const calculateTicketPrice = (event, member = null) => {
// Members without access (guest/suspended/cancelled) get public pricing only.
const accessMember = hasMemberAccess(member) ? member : null;
if (!event.tickets?.enabled) {
// Legacy pricing model
if (event.pricing?.paymentRequired && !event.pricing?.isFree) {
return {
ticketType: member ? "member" : "public",
price: member ? 0 : event.pricing.publicPrice,
ticketType: accessMember ? "member" : "public",
price: accessMember ? 0 : event.pricing.publicPrice,
currency: event.pricing.currency || "CAD",
isEarlyBird: false,
isFree: member ? true : event.pricing.publicPrice === 0,
isFree: accessMember ? true : event.pricing.publicPrice === 0,
};
}
return {
@ -30,14 +42,14 @@ export const calculateTicketPrice = (event, member = null) => {
const now = new Date();
// Member pricing
if (member && event.tickets.member?.available) {
if (accessMember && event.tickets.member?.available) {
const memberTicket = event.tickets.member;
let price = memberTicket.price || 0;
let isFree = memberTicket.isFree;
// Check for circle-specific overrides
if (memberTicket.circleOverrides && member.circle) {
const circleOverride = memberTicket.circleOverrides[member.circle];
if (memberTicket.circleOverrides && accessMember.circle) {
const circleOverride = memberTicket.circleOverrides[accessMember.circle];
if (circleOverride) {
if (circleOverride.isFree !== undefined) {
isFree = circleOverride.isFree;
@ -200,7 +212,7 @@ export const validateTicketPurchase = (event, user) => {
}
// Check member-only restrictions
if (event.membersOnly && !user.member) {
if (event.membersOnly && !hasMemberAccess(user.member)) {
return {
valid: false,
reason: "This event is for members only. Please join to register.",
@ -387,6 +399,9 @@ export const formatPrice = (price, currency = "CAD") => {
* @returns {Object} { ticketType, price, currency, isEarlyBird }
*/
export const calculateSeriesTicketPrice = (series, member = null) => {
// Members without access (guest/suspended/cancelled) get public pricing only.
const accessMember = hasMemberAccess(member) ? member : null;
if (!series.tickets?.enabled) {
return {
ticketType: "guest",
@ -400,14 +415,14 @@ export const calculateSeriesTicketPrice = (series, member = null) => {
const now = new Date();
// Member pricing
if (member && series.tickets.member?.available) {
if (accessMember && series.tickets.member?.available) {
const memberTicket = series.tickets.member;
let price = memberTicket.price || 0;
let isFree = memberTicket.isFree;
// Check for circle-specific overrides
if (memberTicket.circleOverrides && member.circle) {
const circleOverride = memberTicket.circleOverrides[member.circle];
if (memberTicket.circleOverrides && accessMember.circle) {
const circleOverride = memberTicket.circleOverrides[accessMember.circle];
if (circleOverride) {
if (circleOverride.isFree !== undefined) {
isFree = circleOverride.isFree;