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:
parent
c5e901ed24
commit
15329e3e84
7 changed files with 188 additions and 30 deletions
|
|
@ -77,10 +77,10 @@ describe('tickets/purchase.post.js', () => {
|
|||
expect(source).toContain('upsert: true')
|
||||
})
|
||||
|
||||
it('treats guest Members as non-members for pricing and validation', () => {
|
||||
// Guests must not receive member pricing — plan §2
|
||||
expect(source).toContain('isRealMember')
|
||||
expect(source).toContain('!== "guest"')
|
||||
it('treats inactive Members (guest, suspended, cancelled) as non-members for pricing and validation', () => {
|
||||
// Only members with access (active or pending_payment) get member pricing.
|
||||
// hasMemberAccess is the shared gate in server/utils/tickets.js.
|
||||
expect(source).toContain('hasMemberAccess(member)')
|
||||
})
|
||||
|
||||
it('sets an auth cookie for new guests and returning guests', () => {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const pastDate = () => new Date(Date.now() - 86400000).toISOString()
|
|||
const baseMember = (overrides = {}) => ({
|
||||
email: 'member@example.com',
|
||||
circle: 'community',
|
||||
status: 'active',
|
||||
...overrides
|
||||
})
|
||||
|
||||
|
|
@ -340,6 +341,59 @@ describe('calculateTicketPrice', () => {
|
|||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
|
|
@ -545,6 +599,57 @@ describe('validateTicketPurchase', () => {
|
|||
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: {
|
||||
|
|
@ -709,6 +814,33 @@ describe('calculateSeriesTicketPrice', () => {
|
|||
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue