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
|
|
@ -4,7 +4,7 @@
|
||||||
<div v-if="loading" class="text-center py-8">
|
<div v-if="loading" class="text-center py-8">
|
||||||
<div
|
<div
|
||||||
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
|
class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"
|
||||||
></div>
|
/>
|
||||||
<p class="text-[--ui-text-muted]">Loading series pass information...</p>
|
<p class="text-[--ui-text-muted]">Loading series pass information...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
}}
|
}}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<form @submit.prevent="handleSubmit" class="space-y-6">
|
<form class="space-y-6" @submit.prevent="handleSubmit">
|
||||||
<!-- Name Field -->
|
<!-- Name Field -->
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
|
|
@ -286,6 +286,7 @@ const handleSubmit = async () => {
|
||||||
const purchaseBody = {
|
const purchaseBody = {
|
||||||
name: form.value.name,
|
name: form.value.name,
|
||||||
email: form.value.email,
|
email: form.value.email,
|
||||||
|
ticketType: passInfo.value.ticket.type,
|
||||||
};
|
};
|
||||||
if (transactionId) purchaseBody.paymentId = transactionId;
|
if (transactionId) purchaseBody.paymentId = transactionId;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { connectDB } from "../../../utils/mongoose.js";
|
||||||
import { sendEventRegistrationEmail } from "../../../utils/resend.js";
|
import { sendEventRegistrationEmail } from "../../../utils/resend.js";
|
||||||
import { validateBody } from "../../../utils/validateBody.js";
|
import { validateBody } from "../../../utils/validateBody.js";
|
||||||
import { eventRegistrationSchema } from "../../../utils/schemas.js";
|
import { eventRegistrationSchema } from "../../../utils/schemas.js";
|
||||||
|
import { hasMemberAccess } from "../../../utils/tickets.js";
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|
@ -63,10 +64,13 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check member status and handle different registration scenarios
|
// Check member status and handle different registration scenarios.
|
||||||
|
// Member access is decoupled from payment status: active and pending_payment
|
||||||
|
// both confer access; guest, suspended, and cancelled do not.
|
||||||
const member = await Member.findOne({ email: body.email.toLowerCase() });
|
const member = await Member.findOne({ email: body.email.toLowerCase() });
|
||||||
|
const memberHasAccess = hasMemberAccess(member);
|
||||||
|
|
||||||
if (eventData.membersOnly && !member) {
|
if (eventData.membersOnly && !memberHasAccess) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
statusMessage:
|
statusMessage:
|
||||||
|
|
@ -78,7 +82,7 @@ export default defineEventHandler(async (event) => {
|
||||||
if (
|
if (
|
||||||
eventData.pricing?.paymentRequired &&
|
eventData.pricing?.paymentRequired &&
|
||||||
!eventData.pricing?.isFree &&
|
!eventData.pricing?.isFree &&
|
||||||
!member
|
!memberHasAccess
|
||||||
) {
|
) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 402, // Payment Required
|
statusCode: 402, // Payment Required
|
||||||
|
|
@ -91,7 +95,7 @@ export default defineEventHandler(async (event) => {
|
||||||
let isMember = false;
|
let isMember = false;
|
||||||
let membershipLevel = "non-member";
|
let membershipLevel = "non-member";
|
||||||
|
|
||||||
if (member) {
|
if (memberHasAccess) {
|
||||||
isMember = true;
|
isMember = true;
|
||||||
membershipLevel = `${member.circle}-${member.contributionTier}`;
|
membershipLevel = `${member.circle}-${member.contributionTier}`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
validateTicketPurchase,
|
validateTicketPurchase,
|
||||||
calculateTicketPrice,
|
calculateTicketPrice,
|
||||||
completeTicketPurchase,
|
completeTicketPurchase,
|
||||||
|
hasMemberAccess,
|
||||||
} from "../../../../utils/tickets.js";
|
} from "../../../../utils/tickets.js";
|
||||||
import { sendEventRegistrationEmail } from "../../../../utils/resend.js";
|
import { sendEventRegistrationEmail } from "../../../../utils/resend.js";
|
||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
|
|
@ -43,16 +44,17 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is a member. Guests don't count as members for pricing/validation.
|
// Check if user is a member. Only members with access (active or
|
||||||
|
// pending_payment) count for pricing/validation; guest, suspended,
|
||||||
|
// and cancelled members are treated as non-members.
|
||||||
let member = await Member.findOne({ email: body.email.toLowerCase() });
|
let member = await Member.findOne({ email: body.email.toLowerCase() });
|
||||||
let accountCreated = false;
|
let accountCreated = false;
|
||||||
const isRealMember = (m) => !!m && m.status !== "guest";
|
|
||||||
|
|
||||||
// Validate ticket purchase
|
// Validate ticket purchase
|
||||||
const validation = validateTicketPurchase(eventData, {
|
const validation = validateTicketPurchase(eventData, {
|
||||||
email: body.email,
|
email: body.email,
|
||||||
name: body.name,
|
name: body.name,
|
||||||
member: isRealMember(member) ? member : null,
|
member: hasMemberAccess(member) ? member : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
|
|
@ -109,15 +111,15 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create registration
|
// Create registration
|
||||||
const realMember = isRealMember(member);
|
const memberHasAccess = hasMemberAccess(member);
|
||||||
const registration = {
|
const registration = {
|
||||||
memberId: member ? member._id : null,
|
memberId: member ? member._id : null,
|
||||||
name: body.name,
|
name: body.name,
|
||||||
email: body.email.toLowerCase(),
|
email: body.email.toLowerCase(),
|
||||||
membershipLevel: realMember
|
membershipLevel: memberHasAccess
|
||||||
? `${member.circle}-${member.contributionTier}`
|
? `${member.circle}-${member.contributionTier}`
|
||||||
: "non-member",
|
: "non-member",
|
||||||
isMember: realMember,
|
isMember: memberHasAccess,
|
||||||
ticketType: ticketInfo.ticketType,
|
ticketType: ticketInfo.ticketType,
|
||||||
ticketPrice: ticketInfo.price,
|
ticketPrice: ticketInfo.price,
|
||||||
paymentStatus: requiresPayment ? "completed" : "not_required",
|
paymentStatus: requiresPayment ? "completed" : "not_required",
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
releaseSeriesTicket,
|
releaseSeriesTicket,
|
||||||
completeSeriesTicketPurchase,
|
completeSeriesTicketPurchase,
|
||||||
registerForAllSeriesEvents,
|
registerForAllSeriesEvents,
|
||||||
|
hasMemberAccess,
|
||||||
} from "../../../../utils/tickets.js";
|
} from "../../../../utils/tickets.js";
|
||||||
import { sendSeriesPassConfirmation } from "../../../../utils/resend.js";
|
import { sendSeriesPassConfirmation } from "../../../../utils/resend.js";
|
||||||
|
|
||||||
|
|
@ -33,7 +34,9 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check membership — prefer JWT auth for accurate member pricing
|
// Check membership — prefer JWT auth for accurate member pricing.
|
||||||
|
// Only members with access (active or pending_payment) get member-tier
|
||||||
|
// pricing; guest, suspended, and cancelled are treated as non-members.
|
||||||
let member = null;
|
let member = null;
|
||||||
try {
|
try {
|
||||||
member = await requireAuth(event);
|
member = await requireAuth(event);
|
||||||
|
|
@ -46,12 +49,13 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
// Resolve canonical email: use authenticated member's email if available
|
// Resolve canonical email: use authenticated member's email if available
|
||||||
const canonicalEmail = member ? member.email : email.toLowerCase();
|
const canonicalEmail = member ? member.email : email.toLowerCase();
|
||||||
|
const accessMember = hasMemberAccess(member) ? member : null;
|
||||||
|
|
||||||
// Validate purchase
|
// Validate purchase
|
||||||
const validation = validateSeriesTicketPurchase(series, {
|
const validation = validateSeriesTicketPurchase(series, {
|
||||||
email: canonicalEmail,
|
email: canonicalEmail,
|
||||||
name,
|
name,
|
||||||
member,
|
member: accessMember,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
|
|
@ -84,8 +88,8 @@ export default defineEventHandler(async (event) => {
|
||||||
memberId: member?._id,
|
memberId: member?._id,
|
||||||
name,
|
name,
|
||||||
email: canonicalEmail,
|
email: canonicalEmail,
|
||||||
membershipLevel: member?.circle || "non-member",
|
membershipLevel: accessMember?.circle || "non-member",
|
||||||
isMember: !!member,
|
isMember: !!accessMember,
|
||||||
ticketType: ticketInfo.ticketType,
|
ticketType: ticketInfo.ticketType,
|
||||||
ticketPrice: ticketInfo.price,
|
ticketPrice: ticketInfo.price,
|
||||||
paymentStatus: ticketInfo.isFree ? "not_required" : "completed",
|
paymentStatus: ticketInfo.isFree ? "not_required" : "completed",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,14 @@
|
||||||
// Ticket business logic utilities
|
// 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
|
* Calculate the applicable ticket price for a user
|
||||||
* @param {Object} event - Event document
|
* @param {Object} event - Event document
|
||||||
|
|
@ -7,15 +16,18 @@
|
||||||
* @returns {Object} { ticketType, price, currency, isEarlyBird }
|
* @returns {Object} { ticketType, price, currency, isEarlyBird }
|
||||||
*/
|
*/
|
||||||
export const calculateTicketPrice = (event, member = null) => {
|
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) {
|
if (!event.tickets?.enabled) {
|
||||||
// Legacy pricing model
|
// Legacy pricing model
|
||||||
if (event.pricing?.paymentRequired && !event.pricing?.isFree) {
|
if (event.pricing?.paymentRequired && !event.pricing?.isFree) {
|
||||||
return {
|
return {
|
||||||
ticketType: member ? "member" : "public",
|
ticketType: accessMember ? "member" : "public",
|
||||||
price: member ? 0 : event.pricing.publicPrice,
|
price: accessMember ? 0 : event.pricing.publicPrice,
|
||||||
currency: event.pricing.currency || "CAD",
|
currency: event.pricing.currency || "CAD",
|
||||||
isEarlyBird: false,
|
isEarlyBird: false,
|
||||||
isFree: member ? true : event.pricing.publicPrice === 0,
|
isFree: accessMember ? true : event.pricing.publicPrice === 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|
@ -30,14 +42,14 @@ export const calculateTicketPrice = (event, member = null) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Member pricing
|
// Member pricing
|
||||||
if (member && event.tickets.member?.available) {
|
if (accessMember && event.tickets.member?.available) {
|
||||||
const memberTicket = event.tickets.member;
|
const memberTicket = event.tickets.member;
|
||||||
let price = memberTicket.price || 0;
|
let price = memberTicket.price || 0;
|
||||||
let isFree = memberTicket.isFree;
|
let isFree = memberTicket.isFree;
|
||||||
|
|
||||||
// Check for circle-specific overrides
|
// Check for circle-specific overrides
|
||||||
if (memberTicket.circleOverrides && member.circle) {
|
if (memberTicket.circleOverrides && accessMember.circle) {
|
||||||
const circleOverride = memberTicket.circleOverrides[member.circle];
|
const circleOverride = memberTicket.circleOverrides[accessMember.circle];
|
||||||
if (circleOverride) {
|
if (circleOverride) {
|
||||||
if (circleOverride.isFree !== undefined) {
|
if (circleOverride.isFree !== undefined) {
|
||||||
isFree = circleOverride.isFree;
|
isFree = circleOverride.isFree;
|
||||||
|
|
@ -200,7 +212,7 @@ export const validateTicketPurchase = (event, user) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check member-only restrictions
|
// Check member-only restrictions
|
||||||
if (event.membersOnly && !user.member) {
|
if (event.membersOnly && !hasMemberAccess(user.member)) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
reason: "This event is for members only. Please join to register.",
|
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 }
|
* @returns {Object} { ticketType, price, currency, isEarlyBird }
|
||||||
*/
|
*/
|
||||||
export const calculateSeriesTicketPrice = (series, member = null) => {
|
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) {
|
if (!series.tickets?.enabled) {
|
||||||
return {
|
return {
|
||||||
ticketType: "guest",
|
ticketType: "guest",
|
||||||
|
|
@ -400,14 +415,14 @@ export const calculateSeriesTicketPrice = (series, member = null) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
// Member pricing
|
// Member pricing
|
||||||
if (member && series.tickets.member?.available) {
|
if (accessMember && series.tickets.member?.available) {
|
||||||
const memberTicket = series.tickets.member;
|
const memberTicket = series.tickets.member;
|
||||||
let price = memberTicket.price || 0;
|
let price = memberTicket.price || 0;
|
||||||
let isFree = memberTicket.isFree;
|
let isFree = memberTicket.isFree;
|
||||||
|
|
||||||
// Check for circle-specific overrides
|
// Check for circle-specific overrides
|
||||||
if (memberTicket.circleOverrides && member.circle) {
|
if (memberTicket.circleOverrides && accessMember.circle) {
|
||||||
const circleOverride = memberTicket.circleOverrides[member.circle];
|
const circleOverride = memberTicket.circleOverrides[accessMember.circle];
|
||||||
if (circleOverride) {
|
if (circleOverride) {
|
||||||
if (circleOverride.isFree !== undefined) {
|
if (circleOverride.isFree !== undefined) {
|
||||||
isFree = circleOverride.isFree;
|
isFree = circleOverride.isFree;
|
||||||
|
|
|
||||||
|
|
@ -77,10 +77,10 @@ describe('tickets/purchase.post.js', () => {
|
||||||
expect(source).toContain('upsert: true')
|
expect(source).toContain('upsert: true')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('treats guest Members as non-members for pricing and validation', () => {
|
it('treats inactive Members (guest, suspended, cancelled) as non-members for pricing and validation', () => {
|
||||||
// Guests must not receive member pricing — plan §2
|
// Only members with access (active or pending_payment) get member pricing.
|
||||||
expect(source).toContain('isRealMember')
|
// hasMemberAccess is the shared gate in server/utils/tickets.js.
|
||||||
expect(source).toContain('!== "guest"')
|
expect(source).toContain('hasMemberAccess(member)')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets an auth cookie for new guests and returning guests', () => {
|
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 = {}) => ({
|
const baseMember = (overrides = {}) => ({
|
||||||
email: 'member@example.com',
|
email: 'member@example.com',
|
||||||
circle: 'community',
|
circle: 'community',
|
||||||
|
status: 'active',
|
||||||
...overrides
|
...overrides
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -340,6 +341,59 @@ describe('calculateTicketPrice', () => {
|
||||||
expect(result).toBeNull()
|
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()
|
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', () => {
|
it('rejects when no tickets available for user status', () => {
|
||||||
const event = ticketedEvent({
|
const event = ticketedEvent({
|
||||||
tickets: {
|
tickets: {
|
||||||
|
|
@ -709,6 +814,33 @@ describe('calculateSeriesTicketPrice', () => {
|
||||||
|
|
||||||
expect(result).toBeNull()
|
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