import { z } from "zod"; export const IdSchema = z .string() .regex(/^[a-z0-9][a-z0-9-]*$/, "IDs must be lowercase kebab-case."); export const AssetRefSchema = z .string() .regex(/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/, "Asset refs must be URL/path safe."); export const DateSchema = z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, "Dates must be YYYY-MM-DD."); export const DateTimeSchema = z .string() .regex( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})$/, "Date-times must be ISO-8601 with seconds and timezone offset." ); export const HexColorSchema = z .string() .regex(/^#[0-9A-Fa-f]{6}$/, "Colors must be six-digit hex values."); export const LocaleSchema = z .string() .regex(/^[a-z]{2}(-[A-Z]{2})?$/, "Locales must look like en or en-US."); export const LocalizedTextSchema = z .object({ en: z.string().min(1) }) .catchall(z.string().min(1)); export const ConfederationSchema = z.enum([ "AFC", "CAF", "CONCACAF", "CONMEBOL", "OFC", "UEFA" ]); export const EntityRefSchema = z .object({ type: z.enum([ "tournament", "stadium", "team", "group", "match", "sticker-set", "sticker", "quiz", "prediction", "achievement", "challenge", "memory" ]), id: IdSchema }) .strict(); export const FilterValueSchema = z.union([ z.string(), z.number(), z.boolean(), z.array(z.string()) ]); export const RequirementTargetSchema = z .object({ entityType: z.string().min(1), entityId: IdSchema.optional(), filter: z.record(FilterValueSchema).optional() }) .strict(); export const TimeframeSchema = z .object({ startDate: DateSchema.optional(), endDate: DateSchema.optional(), cadence: z .enum(["one-off", "daily", "weekly", "stage", "tournament", "live-window"]) .optional(), timezone: z.string().min(1).optional() }) .strict(); export const RequirementSchema = z .object({ type: z.enum([ "watch", "collect", "predict", "quiz", "memory", "streak", "login", "share", "check-in", "complete", "custom" ]), description: z.string().min(1), target: RequirementTargetSchema.optional(), operator: z .enum(["at-least", "exactly", "all", "any", "streak", "equals", "before", "after", "between"]) .optional(), threshold: z.number().int().nonnegative().optional(), timeframe: TimeframeSchema.optional() }) .strict(); export const RewardSchema = z .object({ kind: z.enum([ "points", "badge", "sticker-pack", "sticker", "memory-frame", "leaderboard-xp", "title", "avatar-item" ]), quantity: z.number().int().positive().optional(), refId: IdSchema.optional(), label: z.string().min(1).optional() }) .strict(); export const TournamentSchema = z .object({ id: IdSchema, name: z.string().min(1), edition: z.number().int().positive(), hostCountries: z.array(IdSchema).min(1), startDate: DateSchema, endDate: DateSchema, status: z.enum(["planned", "live", "completed"]), defaultLocale: LocaleSchema, contentSeason: IdSchema, dataVersion: z .object({ version: z.string().min(1), releasedAt: DateTimeSchema, source: z.enum(["internal-seed", "official-fifa", "licensed-provider", "manual-editorial"]), notes: z.string().min(1) }) .strict() }) .strict(); export const StadiumSchema = z .object({ id: IdSchema, displayName: z.string().min(1), officialName: z.string().min(1), city: z.string().min(1), region: z.string().min(1), countryCode: z.string().min(2).max(3), timezone: z.string().min(1), capacity: z.number().int().positive(), geo: z .object({ latitude: z.number().min(-90).max(90), longitude: z.number().min(-180).max(180) }) .strict(), roofType: z.enum(["open", "covered", "fixed", "retractable"]), surfaceType: z.enum(["natural-grass", "hybrid-grass", "temporary-natural-grass", "synthetic"]), openedYear: z.number().int().min(1800).max(2026).optional(), officialHostVenue: z.boolean(), collectionTier: z.enum(["core", "premium", "legendary"]), imageAssetId: AssetRefSchema, tenantTags: z.array(IdSchema), collectionTags: z.array(IdSchema) }) .strict(); export const TeamSchema = z .object({ id: IdSchema, displayName: z.string().min(1), shortName: z.string().min(1).max(18), fifaCode: z.string().regex(/^[A-Z0-9]{3}$/), countryCode: z.string().min(2).max(7), confederation: ConfederationSchema, federation: z.string().min(1), qualificationStatus: z.enum(["host", "qualified", "candidate", "placeholder", "eliminated"]), seedPot: z.number().int().min(1).max(4).nullable().optional(), primaryColor: HexColorSchema, secondaryColor: HexColorSchema, flagAssetId: AssetRefSchema, collectionTags: z.array(IdSchema) }) .strict(); export const GroupSlotSchema = z .object({ slotId: IdSchema, position: z.number().int().min(1).max(4), drawLabel: z.string().regex(/^[A-L][1-4]$/), teamId: IdSchema.nullable(), locked: z.boolean() }) .strict(); export const GroupSchema = z .object({ id: IdSchema, tournamentId: IdSchema, label: z.string().regex(/^Group [A-L]$/), order: z.number().int().min(1).max(12), slots: z.array(GroupSlotSchema).length(4), status: z.enum(["pre-draw", "drawn", "in-progress", "completed"]), collectionTags: z.array(IdSchema) }) .strict(); export const MatchSideSchema = z .object({ label: z.string().min(1), teamId: IdSchema.optional(), slotId: IdSchema.optional(), advancesFrom: z.string().min(1).optional() }) .strict(); export const MatchSchema = z .object({ id: IdSchema, matchNumber: z.number().int().min(1).max(104), tournamentId: IdSchema, stage: z.enum([ "group", "round-of-32", "round-of-16", "quarter-final", "semi-final", "third-place", "final" ]), roundLabel: z.string().min(1), groupId: IdSchema.nullable().optional(), kickoffAt: DateTimeSchema.nullable(), venueId: IdSchema, home: MatchSideSchema, away: MatchSideSchema, sourceStatus: z.enum(["official-fifa", "provisional-seed", "author-created"]), scoringStatus: z.enum(["not-started", "in-progress", "final", "abandoned", "void"]), collectionTags: z.array(IdSchema) }) .strict(); export const StickerSetSchema = z .object({ id: IdSchema, title: z.string().min(1), description: z.string().min(1), category: z.enum(["stadium", "team", "moment", "achievement", "legacy"]), sortOrder: z.number().int().nonnegative(), featuredStickerIds: z.array(IdSchema), completionReward: RewardSchema }) .strict(); export const StickerItemSchema = z .object({ id: IdSchema, setId: IdSchema, title: z.string().min(1), description: z.string().min(1), rarity: z.enum(["common", "rare", "epic", "legendary"]), sortOrder: z.number().int().nonnegative(), imageAssetId: AssetRefSchema, entityRef: EntityRefSchema.optional(), dropRules: z.array(RequirementSchema), tags: z.array(IdSchema) }) .strict(); export const StickerSeedSchema = z .object({ sets: z.array(StickerSetSchema).min(1), stickers: z.array(StickerItemSchema).min(1) }) .strict(); export const MemoryPromptSchema = z .object({ id: IdSchema, title: z.string().min(1), category: z.enum([ "matchday", "travel", "watch-party", "prediction", "collection", "social", "legacy", "trivia" ]), inputType: z.enum(["photo", "text", "check-in", "audio", "video", "system"]), prompt: z.string().min(1), privacyDefault: z.enum(["private", "friends", "public"]), unlockRule: RequirementSchema, relatedEntityRefs: z.array(EntityRefSchema), tags: z.array(IdSchema) }) .strict(); export const QuizOptionSchema = z .object({ id: IdSchema, label: z.string().min(1) }) .strict(); export const QuizQuestionSchema = z .object({ id: IdSchema, prompt: z.string().min(1), options: z.array(QuizOptionSchema).min(2), correctOptionId: IdSchema, explanation: z.string().min(1), points: z.number().int().positive(), sourceNote: z.string().min(1) }) .strict(); export const QuizSchema = z .object({ id: IdSchema, date: DateSchema, title: z.string().min(1), difficulty: z.enum(["easy", "medium", "hard"]), locale: LocaleSchema, estimatedSeconds: z.number().int().positive(), tags: z.array(IdSchema), questions: z.array(QuizQuestionSchema).min(1) }) .strict(); export const PredictionOptionSchema = z .object({ id: IdSchema, label: z.string().min(1), value: z.union([z.string(), z.number(), z.boolean()]).optional(), entityRef: EntityRefSchema.optional() }) .strict(); export const PredictionScoringRuleSchema = z .object({ basePoints: z.number().int().positive(), perfectBonus: z.number().int().nonnegative().optional(), riskMultiplier: z.number().positive().optional(), settlementPolicy: z.string().min(1), tieBreakPolicy: z.string().min(1).optional() }) .strict(); export const PredictionResolutionSchema = z .object({ status: z.enum(["pending", "settled", "void"]), winningOptionIds: z.array(IdSchema) }) .strict(); export const PredictionMarketSchema = z .object({ id: IdSchema, title: z.string().min(1), type: z.enum([ "match-result", "group-winner", "bracket", "giant-killing", "player-award", "yes-no", "collection", "trivia", "custom" ]), description: z.string().min(1), status: z.enum(["scheduled", "open", "locked", "settled", "void"]), opensAt: DateTimeSchema, closesAt: DateTimeSchema, entityRefs: z.array(EntityRefSchema), options: z.array(PredictionOptionSchema).min(2), scoringRules: PredictionScoringRuleSchema, resolution: PredictionResolutionSchema, tags: z.array(IdSchema) }) .strict(); export const AchievementSchema = z .object({ id: IdSchema, title: z.string().min(1), description: z.string().min(1), category: z.enum([ "watching", "prediction", "collection", "quiz", "memory", "social", "tournament", "legacy", "exploration" ]), badgeTier: z.enum(["bronze", "silver", "gold", "platinum", "legendary"]), points: z.number().int().nonnegative(), badgeAssetId: AssetRefSchema, visibility: z.enum(["visible", "hidden", "seasonal"]), triggerRequirements: z.array(RequirementSchema).min(1), tags: z.array(IdSchema) }) .strict(); export const ChallengeSchema = z .object({ id: IdSchema, title: z.string().min(1), description: z.string().min(1), category: z.enum([ "watching", "prediction", "collection", "quiz", "memory", "social", "streak", "exploration", "legacy" ]), cadence: z.enum(["one-off", "daily", "weekly", "stage", "tournament", "live-window"]), status: z.enum(["active", "scheduled", "archived"]), priority: z.number().int().nonnegative(), startDate: DateSchema.optional(), endDate: DateSchema.optional(), requirements: z.array(RequirementSchema).min(1), rewards: z.array(RewardSchema).min(1), linkedAchievementIds: z.array(IdSchema), audience: z .object({ segments: z.array(IdSchema), countries: z.array(z.string().min(2).max(3)), minAge: z.number().int().positive().optional() }) .strict(), requiresOptIn: z.boolean(), tags: z.array(IdSchema) }) .strict(); export const TournamentSeedSchema = TournamentSchema; export const StadiumSeedSchema = z.array(StadiumSchema).min(1); export const TeamSeedSchema = z.array(TeamSchema).min(1); export const GroupSeedSchema = z.array(GroupSchema).min(1); export const MatchSeedSchema = z.array(MatchSchema).min(1); export const MemorySeedSchema = z.array(MemoryPromptSchema).min(1); export const QuizSeedSchema = z.array(QuizSchema).min(1); export const PredictionSeedSchema = z.array(PredictionMarketSchema).min(1); export const AchievementSeedSchema = z.array(AchievementSchema).min(1); export const ChallengeSeedSchema = z.array(ChallengeSchema).min(1); export type Tournament = z.infer; export type Stadium = z.infer; export type Team = z.infer; export type Group = z.infer; export type GroupSlot = z.infer; export type Match = z.infer; export type StickerSet = z.infer; export type StickerItem = z.infer; export type StickerSeed = z.infer; export type MemoryPrompt = z.infer; export type Quiz = z.infer; export type PredictionMarket = z.infer; export type Achievement = z.infer; export type Challenge = z.infer; export type Requirement = z.infer; export type Reward = z.infer; export type EntityRef = z.infer;