import mongoose from "mongoose"; const eventSchema = new mongoose.Schema({ title: { type: String, required: true }, slug: { type: String, unique: true }, // Auto-generated in pre-save hook tagline: String, description: { type: String, required: true }, content: String, featureImage: { url: String, // Cloudinary URL publicId: String, // Cloudinary public ID for transformations alt: String, // Alt text for accessibility }, startDate: { type: Date, required: true }, endDate: { type: Date, required: true }, eventType: { type: String, enum: ["community", "workshop", "social", "showcase"], default: "community", }, // Online-first location handling location: { type: String, required: true, // This will typically be a Slack channel or video conference link validate: { validator: function (v) { // Must be either a valid URL or a Slack channel reference const urlPattern = /^https?:\/\/.+/; const slackPattern = /^#[a-zA-Z0-9-_]+$/; return urlPattern.test(v) || slackPattern.test(v); }, message: "Location must be a valid URL (video conference link) or Slack channel (starting with #)", }, }, isOnline: { type: Boolean, default: true }, // Default to online-first // Visibility and status controls isVisible: { type: Boolean, default: true }, // Hide from public calendar when false isCancelled: { type: Boolean, default: false }, cancellationMessage: String, // Custom message for cancelled events membersOnly: { type: Boolean, default: false }, // Series information - embedded approach for better performance series: { id: String, // Simple string ID to group related events title: String, // Series title (e.g., "Cooperative Game Development Workshop Series") description: String, // Series description type: { type: String, enum: [ "workshop_series", "recurring_meetup", "multi_day", "course", "tournament", ], default: "workshop_series", }, position: Number, // Order within the series (e.g., 1 = first event, 2 = second, etc.) totalEvents: Number, // Total planned events in the series isSeriesEvent: { type: Boolean, default: false }, // Flag to identify series events }, // Event pricing for public attendees (deprecated - use tickets instead) pricing: { isFree: { type: Boolean, default: true }, publicPrice: { type: Number, default: 0 }, // Price for non-members currency: { type: String, default: "CAD" }, paymentRequired: { type: Boolean, default: false }, }, // Ticket configuration tickets: { enabled: { type: Boolean, default: false }, requiresSeriesTicket: { type: Boolean, default: false }, // If true, must buy series pass instead seriesTicketReference: { type: mongoose.Schema.Types.ObjectId, ref: "Series", }, // Reference to series with tickets currency: { type: String, default: "CAD" }, // Member ticket configuration member: { available: { type: Boolean, default: true }, // Members can always register if tickets enabled isFree: { type: Boolean, default: true }, // Most events free for members price: { type: Number, default: 0 }, // Optional member price (discounted) name: { type: String, default: "Member Ticket" }, description: String, // Circle-specific overrides (optional) 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) ticket configuration public: { available: { type: Boolean, default: false }, name: { type: String, default: "Public Ticket" }, description: String, price: { type: Number, default: 0 }, quantity: Number, // null/undefined = unlimited sold: { type: Number, default: 0 }, reserved: { type: Number, default: 0 }, // Temporarily reserved during checkout earlyBirdPrice: Number, earlyBirdDeadline: Date, }, // Capacity management (applies to all ticket types combined) capacity: { total: Number, // null/undefined = unlimited reserved: { type: Number, default: 0 }, // Currently reserved across all types }, // Waitlist configuration waitlist: { enabled: { type: Boolean, default: false }, maxSize: Number, // null = unlimited waitlist entries: [ { name: String, email: String, membershipLevel: String, addedAt: { type: Date, default: Date.now }, notified: { type: Boolean, default: false }, }, ], }, }, // Circle targeting targetCircles: [ { type: String, enum: ["community", "founder", "practitioner"], required: false, }, ], maxAttendees: Number, registrationRequired: { type: Boolean, default: false }, registrationDeadline: Date, agenda: [String], speakers: [ { name: String, role: String, bio: String, }, ], registrations: [ { memberId: { type: mongoose.Schema.Types.ObjectId, ref: "Member" }, // Reference to Member model name: String, email: String, membershipLevel: String, isMember: { type: Boolean, default: false }, // Ticket information ticketType: { type: String, enum: ["member", "public", "guest", "series_pass"], default: "guest", }, ticketPrice: { type: Number, default: 0 }, // Actual price paid (may be early bird, member, etc.) // Series ticket info isSeriesTicketHolder: { type: Boolean, default: false }, // Registered via series pass seriesTicketId: { type: mongoose.Schema.Types.ObjectId, ref: "Series" }, // Reference to series registration // 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, }, ], 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 eventSchema.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; } next(); } catch (error) { console.error("Error in pre-save hook:", error); next(error); } }); export default mongoose.models.Event || mongoose.model("Event", eventSchema);