import * as z from 'zod' import { ADMIN_ALERT_TYPES } from '../models/adminAlertDismissal.js' import { PAYMENT_METADATA_TYPE_VALUES } from './paymentTypes.js' export const emailSchema = z.object({ email: z.string().trim().toLowerCase().email() }) export const verifyMagicLinkSchema = z.object({ token: z.string().min(1).max(2000) }).strict() export const memberCreateSchema = z.object({ email: z.string().trim().toLowerCase().email(), name: z.string().min(1).max(200), circle: z.enum(['community', 'founder', 'practitioner']), contributionAmount: z.number().int().min(0) }) export const memberProfileUpdateSchema = z.object({ pronouns: z.string().max(100).optional(), timeZone: z.string().max(100).optional(), avatar: z.string().max(500).optional(), studio: z.string().max(200).optional(), bio: z.string().max(5000).optional(), location: z.string().max(200).optional(), socialLinks: z.object({ mastodon: z.string().max(300).optional(), linkedin: z.string().max(300).optional(), website: z.string().max(300).optional(), other: z.string().max(300).optional() }).optional(), showInDirectory: z.boolean().optional(), notifications: z.object({ events: z.boolean().optional() }).optional(), craftTags: z.array(z.string().max(100)).max(16).optional(), boardSlackHandle: z.string().max(200).optional() }) export const eventRegistrationSchema = z.object({ name: z.string().min(1).max(200), email: z.string().trim().toLowerCase().email(), dietary: z.boolean().optional() }) export const paymentVerifySchema = z.object({ cardToken: z.string().min(1), customerId: z.union([z.string(), z.number()]).transform(String) }) // --- Helcim schemas --- export const helcimCreatePlanSchema = z.object({ name: z.string().min(1).max(200), amount: z.union([z.string().min(1), z.number().positive()]), frequency: z.string().min(1).max(50), currency: z.string().max(10).optional() }) export const helcimCustomerSchema = z.object({ name: z.string().min(1).max(200), email: z.string().trim().toLowerCase().email(), circle: z.enum(['community', 'founder', 'practitioner']).optional(), contributionAmount: z.number().int().min(0).optional(), agreedToGuidelines: z.literal(true) }) export const helcimInitializePaymentSchema = z.object({ // amount is accepted but IGNORED for ticket types (server re-derives). // Kept for verify-mode (subscription card-on-file) where 0 is sent. amount: z.number().min(0).optional(), customerCode: z.string().max(200).optional(), metadata: z.object({ type: z.enum(PAYMENT_METADATA_TYPE_VALUES).optional(), eventTitle: z.string().max(500).optional(), eventId: z.string().max(200).optional(), seriesId: z.string().max(200).optional(), email: z.string().trim().toLowerCase().email().optional() }).optional() }) export const helcimSubscriptionSchema = z.object({ customerId: z.union([z.string().min(1), z.number()]), contributionAmount: z.number().int().min(0), customerCode: z.union([z.string().min(1).max(200), z.number()]).transform(String), cardToken: z.string().max(500).optional().nullable(), cadence: z.enum(['monthly', 'annual']).default('monthly') }) export const helcimUpdateCardSchema = z.object({ cardToken: z.string().min(1).max(500) }) export const helcimUpdateBillingSchema = z.object({ customerId: z.union([z.string().min(1), z.number()]), billingAddress: z.object({ name: z.string().max(200).optional(), street: z.string().min(1).max(500), city: z.string().min(1).max(200), province: z.string().max(200).optional(), state: z.string().max(200).optional(), country: z.string().min(1).max(100), postalCode: z.string().min(1).max(20) }) }) // --- Event ticket/registration schemas --- export const ticketPurchaseSchema = z.object({ name: z.string().min(1).max(200), email: z.string().trim().toLowerCase().email(), transactionId: z.string().max(500).optional(), createAccount: z.boolean().optional().default(true) }) export const ticketReserveSchema = z.object({ email: z.string().trim().toLowerCase().email() }) export const ticketEligibilitySchema = z.object({ email: z.string().trim().toLowerCase().email() }) export const waitlistSchema = z.object({ name: z.string().max(200).optional(), email: z.string().trim().toLowerCase().email(), membershipLevel: z.string().max(100).optional() }) export const waitlistDeleteSchema = z.object({ email: z.string().trim().toLowerCase().email() }) export const cancelRegistrationSchema = z.object({ email: z.string().trim().toLowerCase().email() }) export const checkRegistrationSchema = z.object({ email: z.string().trim().toLowerCase().email() }) // --- Member schemas --- export const updateContributionSchema = z.object({ contributionAmount: z.number().int().min(0), cadence: z.enum(['monthly', 'annual']).default('monthly') }) export const updateCircleSchema = z.object({ circle: z.enum(['community', 'founder', 'practitioner']) }) // --- Series ticket schemas --- export const seriesTicketPurchaseSchema = z.object({ name: z.string().min(1).max(200), email: z.string().trim().toLowerCase().email(), paymentId: z.string().max(500).optional(), ticketType: z.enum(['member', 'public', 'guest']), }) export const seriesTicketEligibilitySchema = z.object({ email: z.string().trim().toLowerCase().email() }) // --- Admin schemas --- const emptyStringToUndefined = (v) => (v === '' ? undefined : v) export const adminEventCreateSchema = z.object({ title: z.string().min(1).max(500), description: z.string().min(1).max(50000), startDate: z.string().min(1), endDate: z.string().min(1), location: z.string().max(500).optional(), maxAttendees: z.preprocess(emptyStringToUndefined, z.number().int().positive().optional().nullable()), membersOnly: z.boolean().optional(), registrationDeadline: z.preprocess(emptyStringToUndefined, z.string().optional().nullable()), pricing: z.object({ paymentRequired: z.boolean().optional(), isFree: z.boolean().optional() }).optional(), tickets: z.object({ enabled: z.boolean().optional(), public: z.object({ available: z.boolean().optional(), name: z.string().max(200).optional(), description: z.string().max(2000).optional(), price: z.number().min(0).optional(), quantity: z.preprocess(emptyStringToUndefined, z.number().int().positive().optional().nullable()), earlyBirdPrice: z.preprocess(emptyStringToUndefined, z.number().min(0).optional().nullable()), earlyBirdDeadline: z.preprocess(emptyStringToUndefined, z.string().optional().nullable()) }).optional() }).optional(), image: z.string().url().optional().nullable(), category: z.string().max(100).optional(), tags: z.array(z.string().max(100)).max(20).optional(), series: z.any().optional() }).passthrough() export const adminEventUpdateSchema = z.object({ title: z.string().min(1).max(500), description: z.string().min(1).max(50000), startDate: z.string().min(1), endDate: z.string().min(1), location: z.string().max(500).optional(), maxAttendees: z.preprocess(emptyStringToUndefined, z.number().int().positive().optional().nullable()), membersOnly: z.boolean().optional(), registrationDeadline: z.preprocess(emptyStringToUndefined, z.string().optional().nullable()), pricing: z.object({ paymentRequired: z.boolean().optional(), isFree: z.boolean().optional(), publicPrice: z.number().min(0).optional() }).optional(), tickets: z.object({ enabled: z.boolean().optional(), public: z.object({ available: z.boolean().optional(), name: z.string().max(200).optional(), description: z.string().max(2000).optional(), price: z.number().min(0).optional(), quantity: z.preprocess(emptyStringToUndefined, z.number().int().positive().optional().nullable()), sold: z.number().int().min(0).optional(), earlyBirdPrice: z.preprocess(emptyStringToUndefined, z.number().min(0).optional().nullable()), earlyBirdDeadline: z.preprocess(emptyStringToUndefined, z.string().optional().nullable()) }).optional() }).optional(), image: z.string().url().optional().nullable(), category: z.string().max(100).optional(), tags: z.array(z.string().max(100)).max(20).optional(), series: z.any().optional(), slug: z.string().max(500).optional() }).passthrough() export const adminSeriesCreateSchema = z.object({ id: z.string().min(1).max(200), title: z.string().min(1).max(500), description: z.string().min(1).max(50000), type: z.string().max(100).optional(), totalEvents: z.number().int().positive().optional().nullable() }) export const adminSeriesUpdateSchema = z.object({ id: z.string().min(1).max(200), title: z.string().min(1).max(500), description: z.string().max(50000).optional(), type: z.string().max(100).optional(), totalEvents: z.number().int().positive().optional().nullable() }) export const adminSeriesItemUpdateSchema = z.object({ title: z.string().min(1).max(500).optional(), description: z.string().max(50000).optional(), type: z.string().max(100).optional(), totalEvents: z.number().int().positive().optional().nullable(), isActive: z.boolean().optional() }) export const adminSeriesTicketsSchema = z.object({ id: z.string().min(1).max(200), tickets: z.object({ enabled: z.boolean().optional(), requiresSeriesTicket: z.boolean().optional(), allowIndividualEventTickets: z.boolean().optional(), currency: z.string().max(10).optional(), member: z.object({ available: z.boolean().optional(), isFree: z.boolean().optional(), price: z.number().min(0).optional(), name: z.string().max(200).optional(), description: z.string().max(2000).optional(), circleOverrides: z.record(z.any()).optional() }).optional(), public: z.object({ available: z.boolean().optional(), name: z.string().max(200).optional(), description: z.string().max(2000).optional(), price: z.number().min(0).optional(), quantity: z.number().int().positive().optional().nullable(), sold: z.number().int().min(0).optional(), reserved: z.number().int().min(0).optional(), earlyBirdPrice: z.number().min(0).optional().nullable(), earlyBirdDeadline: z.string().optional().nullable() }).optional(), capacity: z.object({ total: z.number().int().positive().optional().nullable(), reserved: z.number().int().min(0).optional() }).optional(), waitlist: z.object({ enabled: z.boolean().optional(), maxSize: z.number().int().positive().optional().nullable(), entries: z.array(z.any()).optional() }).optional() }) }) export const adminMemberCreateSchema = z.object({ name: z.string().min(1).max(200), email: z.string().trim().toLowerCase().email(), circle: z.enum(['community', 'founder', 'practitioner']), contributionAmount: z.number().int().min(0) }) export const adminMemberUpdateSchema = z.object({ name: z.string().min(1).max(200), email: z.string().trim().toLowerCase().email(), circle: z.enum(['community', 'founder', 'practitioner']), contributionAmount: z.number().int().min(0), status: z.enum(['pending_payment', 'active', 'suspended', 'cancelled']) }) export const adminRoleUpdateSchema = z.object({ role: z.enum(['admin', 'member']) }) export const bulkMemberImportSchema = z.object({ members: z.array(z.object({ name: z.string().min(1).max(200), email: z.string().trim().toLowerCase().email(), circle: z.enum(['community', 'founder', 'practitioner']), contributionAmount: z.number().int().min(0) })).min(1).max(100) }) export const memberInviteSchema = z.object({ memberIds: z.array(z.string().min(1)).min(1).max(100), emailTemplate: z.string().min(1).max(10000) }) // --- Pre-registrant schemas --- export const preRegistrantStatusUpdateSchema = z.object({ status: z.enum(['pending', 'selected']), adminNotes: z.string().max(2000).optional() }) export const preRegistrantBulkStatusSchema = z.object({ ids: z.array(z.string().min(1)).min(1).max(100), status: z.enum(['pending', 'selected']) }) export const preRegistrantInviteSchema = z.object({ preRegistrantIds: z.array(z.string().min(1)).min(1).max(20), emailTemplate: z.string().min(1).max(10000) }) export const inviteVerifySchema = z.object({ token: z.string().min(1) }) export const inviteAcceptSchema = z.object({ preRegistrationId: z.string().min(1), name: z.string().min(1).max(200), pronouns: z.string().max(100).optional(), location: z.string().max(200).optional(), circle: z.enum(['community', 'founder', 'practitioner']), motivation: z.string().max(5000).optional(), contributionAmount: z.number().int().min(0), agreedToGuidelines: z.literal(true), token: z.string().min(1) }) // --- Onboarding schemas --- export const onboardingTrackSchema = z.object({ goal: z.enum(['eventPageVisited', 'boardPageVisited', 'wikiClicked']).optional(), skip: z.enum(['profileTags', 'visitEvent', 'board', 'wiki']).optional(), }).refine((v) => v.goal || v.skip, { message: 'Must provide goal or skip', }) // --- Tag schemas --- export const tagSuggestionSchema = z.object({ label: z.string().min(1).max(100), pool: z.enum(['craft', 'cooperative']) }) // --- Board post / channel schemas --- export const boardPostCreateSchema = z.object({ title: z.string().trim().min(1).max(120), seeking: z.string().max(500).optional(), offering: z.string().max(500).optional(), note: z.string().max(300).optional(), tags: z.array(z.string().max(100)).optional().default([]) }).refine( (data) => (data.seeking || '').trim().length > 0 || (data.offering || '').trim().length > 0, { message: 'At least one of seeking or offering must be provided', path: ['seeking'] } ) export const boardPostUpdateSchema = z.object({ title: z.string().trim().min(1).max(120).optional(), seeking: z.string().max(500).optional(), offering: z.string().max(500).optional(), note: z.string().max(300).optional(), tags: z.array(z.string().max(100)).optional() }) export const boardChannelCreateSchema = z.object({ name: z.string().trim().min(1).max(200), slackChannelId: z.string().trim().min(1).max(50).regex(/^[A-Z0-9]+$/, 'Invalid Slack channel ID').optional(), tagSlugs: z.array(z.string().max(100)).optional().default([]) }) export const boardChannelUpdateSchema = z.object({ name: z.string().trim().min(1).max(200).optional(), slackChannelId: z.string().trim().min(1).max(50).regex(/^[A-Z0-9]+$/, 'Invalid Slack channel ID').optional(), tagSlugs: z.array(z.string().max(100)).optional() }) // --- Admin alert schemas --- export const adminAlertDismissSchema = z.object({ alertType: z.enum(ADMIN_ALERT_TYPES), signature: z.string().min(1).max(128) }) export const adminAlertRestoreSchema = z.object({ alertTypes: z .array(z.enum(ADMIN_ALERT_TYPES)) .min(1) .max(ADMIN_ALERT_TYPES.length) }) // --- Site content (key/value editable copy blocks) --- export const SITE_CONTENT_KEYS = ['homepage.wiki_feature'] export const siteContentUpsertSchema = z.object({ title: z.string().max(300).optional(), body: z.string().max(5000).optional() })