ghostguild-org/tests/server/utils/tickets.test.js
Jennie Robinson Faber 15329e3e84 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.
2026-04-18 17:06:17 +01:00

1068 lines
32 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import {
calculateTicketPrice,
checkTicketAvailability,
validateTicketPurchase,
formatPrice,
calculateSeriesTicketPrice,
checkSeriesTicketAvailability,
validateSeriesTicketPurchase,
checkUserSeriesPass
} from '../../../server/utils/tickets.js'
// ---------------------------------------------------------------------------
// Helpers — build minimal event / series / member stubs
// ---------------------------------------------------------------------------
const futureDate = () => new Date(Date.now() + 86400000).toISOString()
const pastDate = () => new Date(Date.now() - 86400000).toISOString()
const baseMember = (overrides = {}) => ({
email: 'member@example.com',
circle: 'community',
status: 'active',
...overrides
})
const legacyFreeEvent = (overrides = {}) => ({
startDate: futureDate(),
registrations: [],
...overrides
})
const legacyPaidEvent = (overrides = {}) => ({
startDate: futureDate(),
registrations: [],
pricing: { paymentRequired: true, isFree: false, publicPrice: 25, currency: 'CAD' },
...overrides
})
const ticketedEvent = (overrides = {}) => ({
startDate: futureDate(),
registrations: [],
tickets: {
enabled: true,
currency: 'CAD',
capacity: { total: 100 },
member: {
available: true,
price: 10,
isFree: false,
name: 'Member Ticket',
description: 'For members'
},
public: {
available: true,
price: 30,
quantity: 50,
sold: 0,
reserved: 0,
name: 'General Admission',
description: 'Public ticket'
},
waitlist: { enabled: false },
...overrides.tickets
},
...overrides
})
const ticketedSeries = (overrides = {}) => ({
isActive: true,
registrations: [],
tickets: {
enabled: true,
currency: 'CAD',
capacity: { total: 50 },
member: {
available: true,
price: 20,
isFree: false,
name: 'Member Series Pass',
description: 'For members'
},
public: {
available: true,
price: 60,
quantity: 30,
sold: 0,
reserved: 0,
name: 'Series Pass',
description: 'Public series pass'
},
waitlist: { enabled: false },
...overrides.tickets
},
...overrides
})
// ===========================================================================
// calculateTicketPrice
// ===========================================================================
describe('calculateTicketPrice', () => {
describe('legacy pricing (tickets not enabled)', () => {
it('returns free guest ticket for events without payment requirement', () => {
const event = legacyFreeEvent()
const result = calculateTicketPrice(event)
expect(result).toEqual({
ticketType: 'guest',
price: 0,
currency: 'CAD',
isEarlyBird: false,
isFree: true
})
})
it('returns free ticket for member on paid event', () => {
const event = legacyPaidEvent()
const member = baseMember()
const result = calculateTicketPrice(event, member)
expect(result.ticketType).toBe('member')
expect(result.price).toBe(0)
expect(result.isFree).toBe(true)
})
it('returns public price for non-member on paid event', () => {
const event = legacyPaidEvent()
const result = calculateTicketPrice(event, null)
expect(result.ticketType).toBe('public')
expect(result.price).toBe(25)
expect(result.isFree).toBe(false)
})
it('returns guest/free when pricing.isFree is true even if paymentRequired', () => {
const event = legacyFreeEvent({
pricing: { paymentRequired: true, isFree: true, publicPrice: 10 }
})
const result = calculateTicketPrice(event)
expect(result.ticketType).toBe('guest')
expect(result.price).toBe(0)
expect(result.isFree).toBe(true)
})
})
describe('member ticket pricing', () => {
it('returns base member price when isFree is false', () => {
const event = ticketedEvent()
const member = baseMember()
const result = calculateTicketPrice(event, member)
expect(result.ticketType).toBe('member')
expect(result.price).toBe(10)
expect(result.isFree).toBe(false)
expect(result.name).toBe('Member Ticket')
expect(result.description).toBe('For members')
})
it('returns price 0 when member ticket isFree is true', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
currency: 'CAD',
member: { available: true, price: 10, isFree: true },
public: { available: true, price: 30 }
}
})
const member = baseMember()
const result = calculateTicketPrice(event, member)
expect(result.price).toBe(0)
expect(result.isFree).toBe(true)
})
it('applies circle override price', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
currency: 'CAD',
member: {
available: true,
price: 10,
isFree: false,
circleOverrides: {
founder: { price: 5 }
}
},
public: { available: true, price: 30 }
}
})
const member = baseMember({ circle: 'founder' })
const result = calculateTicketPrice(event, member)
expect(result.price).toBe(5)
expect(result.isFree).toBe(false)
})
it('applies circle override isFree', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
currency: 'CAD',
member: {
available: true,
price: 10,
isFree: false,
circleOverrides: {
practitioner: { isFree: true }
}
},
public: { available: true, price: 30 }
}
})
const member = baseMember({ circle: 'practitioner' })
const result = calculateTicketPrice(event, member)
expect(result.price).toBe(0)
expect(result.isFree).toBe(true)
})
it('ignores circle override for a different circle', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
currency: 'CAD',
member: {
available: true,
price: 10,
isFree: false,
circleOverrides: {
practitioner: { isFree: true }
}
},
public: { available: true, price: 30 }
}
})
const member = baseMember({ circle: 'community' })
const result = calculateTicketPrice(event, member)
expect(result.price).toBe(10)
expect(result.isFree).toBe(false)
})
})
describe('public ticket pricing', () => {
it('returns base public price when no member provided', () => {
const event = ticketedEvent()
const result = calculateTicketPrice(event, null)
expect(result.ticketType).toBe('public')
expect(result.price).toBe(30)
expect(result.isEarlyBird).toBe(false)
expect(result.isFree).toBe(false)
expect(result.name).toBe('General Admission')
})
it('returns early bird price before deadline', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
currency: 'CAD',
member: { available: false },
public: {
available: true,
price: 30,
earlyBirdPrice: 20,
earlyBirdDeadline: futureDate()
}
}
})
const result = calculateTicketPrice(event, null)
expect(result.price).toBe(20)
expect(result.isEarlyBird).toBe(true)
expect(result.isFree).toBe(false)
})
it('returns regular price after early bird deadline', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
currency: 'CAD',
member: { available: false },
public: {
available: true,
price: 30,
earlyBirdPrice: 20,
earlyBirdDeadline: pastDate()
}
}
})
const result = calculateTicketPrice(event, null)
expect(result.price).toBe(30)
expect(result.isEarlyBird).toBe(false)
})
it('returns isFree true when public price is 0', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
currency: 'CAD',
member: { available: false },
public: { available: true, price: 0 }
}
})
const result = calculateTicketPrice(event, null)
expect(result.price).toBe(0)
expect(result.isFree).toBe(true)
})
})
describe('no tickets available', () => {
it('returns null when member tickets unavailable and no public tickets', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
member: { available: false },
public: { available: false }
}
})
const result = calculateTicketPrice(event, null)
expect(result).toBeNull()
})
it('returns null for non-member when only member tickets available', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
currency: 'CAD',
member: { available: true, price: 10, isFree: false },
public: { available: false }
}
})
const result = calculateTicketPrice(event, null)
expect(result).toBeNull()
})
})
describe('member access by status', () => {
it('treats pending_payment members as having access (member pricing)', () => {
const event = ticketedEvent()
const member = baseMember({ status: 'pending_payment' })
const result = calculateTicketPrice(event, member)
expect(result.ticketType).toBe('member')
expect(result.price).toBe(10)
})
it('treats guest members as non-members (public pricing)', () => {
const event = ticketedEvent()
const member = baseMember({ status: 'guest' })
const result = calculateTicketPrice(event, member)
expect(result.ticketType).toBe('public')
expect(result.price).toBe(30)
})
it('treats suspended members as non-members (public pricing)', () => {
const event = ticketedEvent()
const member = baseMember({ status: 'suspended' })
const result = calculateTicketPrice(event, member)
expect(result.ticketType).toBe('public')
expect(result.price).toBe(30)
})
it('treats cancelled members as non-members (public pricing)', () => {
const event = ticketedEvent()
const member = baseMember({ status: 'cancelled' })
const result = calculateTicketPrice(event, member)
expect(result.ticketType).toBe('public')
expect(result.price).toBe(30)
})
it('returns null for suspended member when only member tickets available', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
currency: 'CAD',
member: { available: true, price: 10, isFree: false },
public: { available: false }
}
})
const member = baseMember({ status: 'suspended' })
const result = calculateTicketPrice(event, member)
expect(result).toBeNull()
})
})
})
// ===========================================================================
// checkTicketAvailability
// ===========================================================================
describe('checkTicketAvailability', () => {
describe('legacy (tickets not enabled)', () => {
it('returns available with null remaining when no maxAttendees', () => {
const event = legacyFreeEvent()
const result = checkTicketAvailability(event)
expect(result).toEqual({
available: true,
remaining: null,
waitlistAvailable: false
})
})
it('returns available with remaining count when under capacity', () => {
const event = legacyFreeEvent({
maxAttendees: 10,
registrations: [{ email: 'a@b.com' }, { email: 'c@d.com' }]
})
const result = checkTicketAvailability(event)
expect(result.available).toBe(true)
expect(result.remaining).toBe(8)
})
it('returns unavailable at capacity', () => {
const event = legacyFreeEvent({
maxAttendees: 2,
registrations: [{ email: 'a@b.com' }, { email: 'c@d.com' }]
})
const result = checkTicketAvailability(event)
expect(result.available).toBe(false)
expect(result.remaining).toBe(0)
})
})
describe('ticketed events — overall capacity', () => {
it('returns unavailable when total capacity exceeded', () => {
const event = ticketedEvent()
event.tickets.capacity.total = 2
event.registrations = [{ email: 'a@b.com' }, { email: 'c@d.com' }]
const result = checkTicketAvailability(event, 'public')
expect(result.available).toBe(false)
expect(result.remaining).toBe(0)
})
it('respects waitlist flag when capacity exceeded', () => {
const event = ticketedEvent()
event.tickets.capacity.total = 1
event.tickets.waitlist = { enabled: true }
event.registrations = [{ email: 'a@b.com' }]
const result = checkTicketAvailability(event, 'public')
expect(result.available).toBe(false)
expect(result.waitlistAvailable).toBe(true)
})
})
describe('ticketed events — public tickets', () => {
it('returns correct remaining for quantity-limited public tickets', () => {
const event = ticketedEvent()
event.tickets.public.quantity = 10
event.tickets.public.sold = 3
event.tickets.public.reserved = 2
const result = checkTicketAvailability(event, 'public')
expect(result.available).toBe(true)
expect(result.remaining).toBe(5)
})
it('returns unavailable when public tickets sold out', () => {
const event = ticketedEvent()
event.tickets.public.quantity = 5
event.tickets.public.sold = 5
event.tickets.public.reserved = 0
const result = checkTicketAvailability(event, 'public')
expect(result.available).toBe(false)
expect(result.remaining).toBe(0)
})
it('returns unlimited when no quantity set on public tickets', () => {
const event = ticketedEvent()
delete event.tickets.public.quantity
const result = checkTicketAvailability(event, 'public')
expect(result.available).toBe(true)
expect(result.remaining).toBeNull()
})
})
describe('ticketed events — member tickets', () => {
it('returns available with capacity remaining', () => {
const event = ticketedEvent()
event.tickets.capacity.total = 10
event.registrations = [{ email: 'a@b.com' }]
const result = checkTicketAvailability(event, 'member')
expect(result.available).toBe(true)
expect(result.remaining).toBe(9)
})
it('returns unlimited when no total capacity set', () => {
const event = ticketedEvent()
delete event.tickets.capacity
const result = checkTicketAvailability(event, 'member')
expect(result.available).toBe(true)
expect(result.remaining).toBeNull()
})
})
describe('edge cases', () => {
it('returns unavailable for unknown ticket type', () => {
const event = ticketedEvent()
const result = checkTicketAvailability(event, 'vip')
expect(result.available).toBe(false)
expect(result.remaining).toBe(0)
})
})
})
// ===========================================================================
// validateTicketPurchase
// ===========================================================================
describe('validateTicketPurchase', () => {
const validUser = { email: 'user@example.com', name: 'Test User', member: null }
const memberUser = { email: 'member@example.com', name: 'Member', member: baseMember() }
it('rejects cancelled event', () => {
const event = ticketedEvent({ isCancelled: true })
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toBe('Event has been cancelled')
})
it('rejects past event', () => {
const event = ticketedEvent({ startDate: pastDate() })
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toBe('Event has already started')
})
it('rejects when registration deadline has passed', () => {
const event = ticketedEvent({ registrationDeadline: pastDate() })
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toBe('Registration deadline has passed')
})
it('rejects already registered user', () => {
const event = ticketedEvent({
registrations: [{ email: 'user@example.com', cancelledAt: null }]
})
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toBe('You are already registered for this event')
})
it('allows user whose previous registration was cancelled', () => {
const event = ticketedEvent({
registrations: [{ email: 'user@example.com', cancelledAt: new Date() }]
})
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(true)
})
it('rejects non-member for members-only event', () => {
const event = ticketedEvent({ membersOnly: true })
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toContain('members only')
})
it('allows member for members-only event', () => {
const event = ticketedEvent({ membersOnly: true })
const result = validateTicketPurchase(event, memberUser)
expect(result.valid).toBe(true)
expect(result.ticketInfo).toBeDefined()
expect(result.availability).toBeDefined()
})
it('allows pending_payment member for members-only event', () => {
const event = ticketedEvent({ membersOnly: true })
const user = {
email: 'pp@example.com',
name: 'Pending',
member: baseMember({ status: 'pending_payment' })
}
const result = validateTicketPurchase(event, user)
expect(result.valid).toBe(true)
})
it('rejects suspended member from members-only event', () => {
const event = ticketedEvent({ membersOnly: true })
const user = {
email: 'sus@example.com',
name: 'Suspended',
member: baseMember({ status: 'suspended' })
}
const result = validateTicketPurchase(event, user)
expect(result.valid).toBe(false)
expect(result.reason).toContain('members only')
})
it('rejects cancelled member from members-only event', () => {
const event = ticketedEvent({ membersOnly: true })
const user = {
email: 'can@example.com',
name: 'Cancelled',
member: baseMember({ status: 'cancelled' })
}
const result = validateTicketPurchase(event, user)
expect(result.valid).toBe(false)
expect(result.reason).toContain('members only')
})
it('rejects guest member from members-only event', () => {
const event = ticketedEvent({ membersOnly: true })
const user = {
email: 'guest@example.com',
name: 'Guest',
member: baseMember({ status: 'guest' })
}
const result = validateTicketPurchase(event, user)
expect(result.valid).toBe(false)
expect(result.reason).toContain('members only')
})
it('rejects when no tickets available for user status', () => {
const event = ticketedEvent({
tickets: {
enabled: true,
member: { available: false },
public: { available: false }
}
})
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toContain('No tickets available')
})
it('rejects when sold out with waitlist info', () => {
const event = ticketedEvent()
event.tickets.public.quantity = 1
event.tickets.public.sold = 1
event.tickets.waitlist = { enabled: true }
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toBe('Event is sold out')
expect(result.waitlistAvailable).toBe(true)
})
it('returns valid with ticket info and availability for good purchase', () => {
const event = ticketedEvent()
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(true)
expect(result.ticketInfo.ticketType).toBe('public')
expect(result.ticketInfo.price).toBe(30)
expect(result.availability.available).toBe(true)
})
it('is case-insensitive on email match for duplicate check', () => {
const event = ticketedEvent({
registrations: [{ email: 'USER@EXAMPLE.COM', cancelledAt: null }]
})
const result = validateTicketPurchase(event, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toBe('You are already registered for this event')
})
})
// ===========================================================================
// formatPrice
// ===========================================================================
describe('formatPrice', () => {
it('returns "Free" for zero price', () => {
expect(formatPrice(0)).toBe('Free')
})
it('formats CAD price by default', () => {
const result = formatPrice(25)
expect(result).toContain('25.00')
})
it('formats decimal prices', () => {
const result = formatPrice(9.99)
expect(result).toContain('9.99')
})
it('respects USD currency', () => {
const result = formatPrice(25, 'USD')
expect(result).toContain('25.00')
// US$ or $ prefix depending on locale — just confirm it formats
expect(result).toMatch(/\$/)
})
})
// ===========================================================================
// calculateSeriesTicketPrice
// ===========================================================================
describe('calculateSeriesTicketPrice', () => {
it('returns free guest ticket when tickets not enabled', () => {
const series = { tickets: { enabled: false } }
const result = calculateSeriesTicketPrice(series)
expect(result).toEqual({
ticketType: 'guest',
price: 0,
currency: 'CAD',
isEarlyBird: false,
isFree: true
})
})
it('returns member price for authenticated member', () => {
const series = ticketedSeries()
const member = baseMember()
const result = calculateSeriesTicketPrice(series, member)
expect(result.ticketType).toBe('member')
expect(result.price).toBe(20)
expect(result.isFree).toBe(false)
expect(result.name).toBe('Member Series Pass')
})
it('applies circle override to series member price', () => {
const series = ticketedSeries({
tickets: {
enabled: true,
currency: 'CAD',
member: {
available: true,
price: 20,
isFree: false,
circleOverrides: { founder: { price: 10 } }
},
public: { available: true, price: 60 }
}
})
const member = baseMember({ circle: 'founder' })
const result = calculateSeriesTicketPrice(series, member)
expect(result.price).toBe(10)
})
it('returns public price for non-member', () => {
const series = ticketedSeries()
const result = calculateSeriesTicketPrice(series, null)
expect(result.ticketType).toBe('public')
expect(result.price).toBe(60)
})
it('applies early bird pricing before deadline', () => {
const series = ticketedSeries({
tickets: {
enabled: true,
currency: 'CAD',
member: { available: false },
public: {
available: true,
price: 60,
earlyBirdPrice: 40,
earlyBirdDeadline: futureDate()
}
}
})
const result = calculateSeriesTicketPrice(series, null)
expect(result.price).toBe(40)
expect(result.isEarlyBird).toBe(true)
})
it('returns null when no tickets available for user', () => {
const series = ticketedSeries({
tickets: {
enabled: true,
member: { available: false },
public: { available: false }
}
})
const result = calculateSeriesTicketPrice(series, null)
expect(result).toBeNull()
})
it('treats pending_payment members as having access (member pricing)', () => {
const series = ticketedSeries()
const member = baseMember({ status: 'pending_payment' })
const result = calculateSeriesTicketPrice(series, member)
expect(result.ticketType).toBe('member')
expect(result.price).toBe(20)
})
it('treats suspended members as non-members (public pricing)', () => {
const series = ticketedSeries()
const member = baseMember({ status: 'suspended' })
const result = calculateSeriesTicketPrice(series, member)
expect(result.ticketType).toBe('public')
expect(result.price).toBe(60)
})
it('treats cancelled members as non-members (public pricing)', () => {
const series = ticketedSeries()
const member = baseMember({ status: 'cancelled' })
const result = calculateSeriesTicketPrice(series, member)
expect(result.ticketType).toBe('public')
expect(result.price).toBe(60)
})
})
// ===========================================================================
// checkSeriesTicketAvailability
// ===========================================================================
describe('checkSeriesTicketAvailability', () => {
it('returns unavailable when tickets not enabled', () => {
const series = { tickets: { enabled: false } }
const result = checkSeriesTicketAvailability(series)
expect(result.available).toBe(false)
})
it('returns available with remaining for public tickets', () => {
const series = ticketedSeries()
series.tickets.public.quantity = 30
series.tickets.public.sold = 10
series.tickets.public.reserved = 5
const result = checkSeriesTicketAvailability(series, 'public')
expect(result.available).toBe(true)
expect(result.remaining).toBe(15)
})
it('returns unavailable when public tickets sold out', () => {
const series = ticketedSeries()
series.tickets.public.quantity = 5
series.tickets.public.sold = 5
const result = checkSeriesTicketAvailability(series, 'public')
expect(result.available).toBe(false)
expect(result.remaining).toBe(0)
})
it('returns unavailable when total capacity reached', () => {
const series = ticketedSeries()
series.tickets.capacity.total = 2
series.registrations = [
{ email: 'a@b.com' },
{ email: 'c@d.com' }
]
const result = checkSeriesTicketAvailability(series, 'member')
expect(result.available).toBe(false)
expect(result.remaining).toBe(0)
})
it('excludes cancelled registrations from count', () => {
const series = ticketedSeries()
series.tickets.capacity.total = 2
series.registrations = [
{ email: 'a@b.com', cancelledAt: null },
{ email: 'c@d.com', cancelledAt: new Date() }
]
const result = checkSeriesTicketAvailability(series, 'member')
expect(result.available).toBe(true)
expect(result.remaining).toBe(1)
})
it('returns unlimited for member tickets with no capacity', () => {
const series = ticketedSeries()
delete series.tickets.capacity
const result = checkSeriesTicketAvailability(series, 'member')
expect(result.available).toBe(true)
expect(result.remaining).toBeNull()
})
})
// ===========================================================================
// validateSeriesTicketPurchase
// ===========================================================================
describe('validateSeriesTicketPurchase', () => {
const validUser = { email: 'user@example.com', name: 'Test User', member: null }
const memberUser = { email: 'member@example.com', name: 'Member', member: baseMember() }
it('rejects inactive series', () => {
const series = ticketedSeries({ isActive: false })
const result = validateSeriesTicketPurchase(series, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toBe('This series is not currently available')
})
it('rejects already registered user', () => {
const series = ticketedSeries({
registrations: [{ email: 'user@example.com', cancelledAt: null }]
})
const result = validateSeriesTicketPurchase(series, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toBe('You already have a pass for this series')
})
it('allows user whose previous registration was cancelled', () => {
const series = ticketedSeries({
registrations: [{ email: 'user@example.com', cancelledAt: new Date() }]
})
const result = validateSeriesTicketPurchase(series, validUser)
expect(result.valid).toBe(true)
})
it('rejects when no tickets available for user status', () => {
const series = ticketedSeries({
tickets: {
enabled: true,
member: { available: false },
public: { available: false }
}
})
const result = validateSeriesTicketPurchase(series, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toContain('No series passes available')
})
it('rejects when sold out', () => {
const series = ticketedSeries()
series.tickets.public.quantity = 1
series.tickets.public.sold = 1
series.tickets.waitlist = { enabled: true }
const result = validateSeriesTicketPurchase(series, validUser)
expect(result.valid).toBe(false)
expect(result.reason).toBe('Series passes are sold out')
expect(result.waitlistAvailable).toBe(true)
})
it('returns valid with ticket info for good purchase', () => {
const series = ticketedSeries()
const result = validateSeriesTicketPurchase(series, validUser)
expect(result.valid).toBe(true)
expect(result.ticketInfo.ticketType).toBe('public')
expect(result.ticketInfo.price).toBe(60)
expect(result.availability.available).toBe(true)
})
it('returns member ticket info for authenticated member', () => {
const series = ticketedSeries()
const result = validateSeriesTicketPurchase(series, memberUser)
expect(result.valid).toBe(true)
expect(result.ticketInfo.ticketType).toBe('member')
expect(result.ticketInfo.price).toBe(20)
})
})
// ===========================================================================
// checkUserSeriesPass
// ===========================================================================
describe('checkUserSeriesPass', () => {
it('returns hasPass true when user has active registration', () => {
const series = {
registrations: [
{ email: 'user@example.com', cancelledAt: null, paymentStatus: 'completed' }
]
}
const result = checkUserSeriesPass(series, 'user@example.com')
expect(result.hasPass).toBe(true)
expect(result.registration).toBeDefined()
expect(result.registration.email).toBe('user@example.com')
})
it('returns hasPass false when no registration exists', () => {
const series = { registrations: [] }
const result = checkUserSeriesPass(series, 'user@example.com')
expect(result.hasPass).toBe(false)
expect(result.registration).toBeNull()
})
it('returns hasPass false when registration is cancelled', () => {
const series = {
registrations: [
{ email: 'user@example.com', cancelledAt: new Date(), paymentStatus: 'completed' }
]
}
const result = checkUserSeriesPass(series, 'user@example.com')
expect(result.hasPass).toBe(false)
})
it('returns hasPass false when payment failed', () => {
const series = {
registrations: [
{ email: 'user@example.com', cancelledAt: null, paymentStatus: 'failed' }
]
}
const result = checkUserSeriesPass(series, 'user@example.com')
expect(result.hasPass).toBe(false)
})
it('is case-insensitive on email', () => {
const series = {
registrations: [
{ email: 'USER@EXAMPLE.COM', cancelledAt: null, paymentStatus: 'completed' }
]
}
const result = checkUserSeriesPass(series, 'user@example.com')
expect(result.hasPass).toBe(true)
})
it('handles missing registrations array', () => {
const series = {}
const result = checkUserSeriesPass(series, 'user@example.com')
expect(result.hasPass).toBe(false)
expect(result.registration).toBeNull()
})
})