ghostguild-org/server/models/series.js

173 lines
5.1 KiB
JavaScript

import mongoose from "mongoose";
const seriesSchema = new mongoose.Schema({
id: { type: String, required: true, unique: true }, // Simple string ID (e.g., "coop-game-dev-2025")
title: { type: String, required: true },
slug: { type: String, unique: true }, // Auto-generated in pre-save hook
description: String,
type: {
type: String,
enum: [
"workshop_series",
"recurring_meetup",
"multi_day",
"course",
"tournament",
],
default: "workshop_series",
},
// Visibility and status
isVisible: { type: Boolean, default: true },
isActive: { type: Boolean, default: true },
// Date range (calculated from events or set manually)
startDate: Date,
endDate: Date,
// Series ticketing configuration
tickets: {
enabled: { type: Boolean, default: false },
requiresSeriesTicket: { type: Boolean, default: false }, // If true, must buy series pass
allowIndividualEventTickets: { type: Boolean, default: true }, // Allow drop-in for individual events
currency: { type: String, default: "CAD" },
// Member series pass configuration
member: {
available: { type: Boolean, default: true },
isFree: { type: Boolean, default: true },
price: { type: Number, default: 0 },
name: { type: String, default: "Member Series Pass" },
description: String,
// Circle-specific overrides
circleOverrides: {
community: {
isFree: { type: Boolean },
price: { type: Number },
},
founder: {
isFree: { type: Boolean },
price: { type: Number },
},
practitioner: {
isFree: { type: Boolean },
price: { type: Number },
},
},
},
// Public (non-member) series pass configuration
public: {
available: { type: Boolean, default: false },
name: { type: String, default: "Series Pass" },
description: String,
price: { type: Number, default: 0 },
quantity: Number, // null/undefined = unlimited
sold: { type: Number, default: 0 },
reserved: { type: Number, default: 0 },
earlyBirdPrice: Number,
earlyBirdDeadline: Date,
},
// Series-wide capacity
capacity: {
total: Number, // null/undefined = unlimited
reserved: { type: Number, default: 0 },
},
// Waitlist configuration
waitlist: {
enabled: { type: Boolean, default: false },
maxSize: Number,
entries: [
{
name: String,
email: String,
membershipLevel: String,
addedAt: { type: Date, default: Date.now },
notified: { type: Boolean, default: false },
},
],
},
},
// Series pass purchases (registrations)
registrations: [
{
memberId: { type: mongoose.Schema.Types.ObjectId, ref: "Member" },
name: String,
email: String,
membershipLevel: String,
isMember: { type: Boolean, default: false },
// Ticket information
ticketType: {
type: String,
enum: ["member", "public", "guest"],
default: "guest",
},
ticketPrice: { type: Number, default: 0 },
// Payment information
paymentStatus: {
type: String,
enum: ["pending", "completed", "failed", "refunded", "not_required"],
default: "not_required",
},
paymentId: String, // Helcim transaction ID
amountPaid: { type: Number, default: 0 },
// Metadata
registeredAt: { type: Date, default: Date.now },
cancelledAt: Date,
refundedAt: Date,
refundAmount: Number,
// Events they've been registered for (references)
eventRegistrations: [
{
eventId: { type: mongoose.Schema.Types.ObjectId, ref: "Event" },
registrationId: mongoose.Schema.Types.ObjectId, // ID of registration in event.registrations
},
],
},
],
// Target circles
targetCircles: [
{
type: String,
enum: ["community", "founder", "practitioner"],
},
],
// Metadata
totalEvents: { type: Number, default: 0 }, // Number of events in this series
createdBy: { type: String, required: true },
createdAt: { type: Date, default: Date.now },
updatedAt: { type: Date, default: Date.now },
});
// Generate slug from title
function generateSlug(title) {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
// Pre-save hook to generate slug
seriesSchema.pre("save", async function (next) {
try {
// Always generate slug if it doesn't exist or if title has changed
if (!this.slug || this.isNew || this.isModified("title")) {
let baseSlug = generateSlug(this.title);
let slug = baseSlug;
let counter = 1;
// Ensure slug is unique
while (await this.constructor.findOne({ slug, _id: { $ne: this._id } })) {
slug = `${baseSlug}-${counter}`;
counter++;
}
this.slug = slug;
}
// Update timestamps
this.updatedAt = new Date();
next();
} catch (error) {
console.error("Error in pre-save hook:", error);
next(error);
}
});
export default mongoose.models.Series || mongoose.model("Series", seriesSchema);